Skip to content

Commit 23f0990

Browse files
Merge pull request #344 from Workiva/add-fast-format
CPLAT-11875: Add Fast Format Command
2 parents d00da8a + 7ec4f71 commit 23f0990

17 files changed

+609
-31
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
name: "SDK: stable"
1212
script:
1313
- pub run dart_dev analyze
14-
- pub run dependency_validator -i pedantic
14+
- pub run dependency_validator -i pedantic over_react_format
1515
- pub run dart_dev format --check
1616
- pub run dart_dev test
1717
- pub publish --dry-run

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## [3.6.0](https://github.com/Workiva/dart_dev/compare/3.5.0...3.6.0)
4+
5+
- Support a faster format command for better integration with JetBrains file
6+
watching for format-on-save functionality.
7+
38
## [3.5.0](https://github.com/Workiva/dart_dev/compare/3.4.0...3.5.0)
49

510
- Added an optional `collapseDirectories` param to `FormatTool.getInputs()`.

README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Easily configurable.
1313
- [Project-Level Configuration](#project-level-configuration)
1414
- [Extending/Composing Functionality](#extendingcomposing-functionality)
1515
- [Shared Configuration](#shared-configuration)
16+
- [Format on save](#format-on-save)
1617
- [Additional Docs][docs]
1718

1819
## Quick Start
@@ -231,8 +232,94 @@ final config = {
231232
};
232233
```
233234

235+
## Format on save
236+
dart_dev can be used to facilitate formatting on save inside of JetBrains IDEs. For setup instructions, see below.
237+
238+
### A Note on VS Code
239+
A VS code extension exists to run either `dartfmt` or `over_react_format` on save. For information on it, see [its project](vs-code-formatter). However, that VS Code extension does not run `dart_dev`, but rather has its own logic to run a formatting command.
240+
241+
### JetBrains IDEs (WebStorm, IntelliJ, etc.)
242+
Webstorm exposes a File Watcher utility that can be used to run commands when a file saves. For this approach, all you need to do is set up the file watcher. Shoutout to @patkujawa-wf for creating the original inspiration of this solution!
243+
244+
__NOTE:__ Before setting up the watcher, there are three basic limitations when using it:
245+
1. dart_dev's minimum must be at least version `3.6.0` in the projects that uses the watcher.
246+
1. Only dart_dev's `FormatTool` and OverReact Format's `OverReactFormatTool` are supported.
247+
1. Literals need to be used when possible when configuring the formatter. This primarily pertains to the formatter tool itself and setting the property that is responsible for line-length. For example:
248+
```dart
249+
// Good
250+
final Map<String, DevTool> config = {
251+
// ... other config options
252+
'format': FormatTool()
253+
..formatter = Formatter.dartStyle
254+
..formatterArgs = ['-l', '120'],
255+
};
256+
257+
// Bad
258+
259+
// Example 1: Line-length as a variable
260+
const lineLength = 120;
261+
262+
final Map<String, DevTool> config = {
263+
// ... other config options
264+
'format': FormatTool()
265+
..formatter = Formatter.dartStyle
266+
..formatterArgs = ['-l', lineLength],
267+
};
268+
269+
// Example 2: Args as a variable
270+
const formatterArgs = ['-l', '120'];
271+
272+
final Map<String, DevTool> config = {
273+
// ... other config options
274+
'format': FormatTool()
275+
..formatter = Formatter.dartStyle
276+
..formatterArgs = formatterArgs,
277+
};
278+
279+
// Example 3: Formatter as a variable
280+
final formatter = FormatTool()
281+
..formatter = Formatter.dartStyle
282+
..formatterArgs = ['-l', '120'];
283+
284+
final Map<String, DevTool> config = {
285+
// ... other config options
286+
'format': formatter,
287+
};
288+
```
289+
290+
#### Setting Up the File Watcher
291+
1. Go into Webstorm's preferences. It doesn't matter what project you do this in, as you'll ultimately want to make the watcher global. More on that later, though!
292+
1. Navigate to the "File Watchers" settings. This is under "Preferences > Tools > File Watchers". The File Watcher pane should look something like:
293+
294+
<img src='./images/file_watcher_pane.png' width="75%" alt='File Watcher Pane'>
295+
296+
1. Clicking on the import icon on the bottom toolbar.
297+
1. Import the `format_on_save.xml` file found in this project, at "[dart_dev/tool/file_watchers/format_on_save.xml](tool/file_watchers/ddev_format_on_save.xml)".
298+
1. After importing, change the watcher scoping (AKA "level") to `Global` on the right hand side under the "level" column, which makes the watcher available for use in all projects.
299+
1. In each project, you will also have to enable the watcher by checking the box on the file watcher's row.
300+
301+
For additional reference on how the watcher is set up, see [JetBrains File Watcher Configuration](#jetbrains-file-watcher-configuration).
302+
303+
304+
#### JetBrains File Watcher Configuration
305+
<img src='./images/file_watcher_config.png' width="75%" alt='Final File Watcher Configuration'>
306+
307+
1. __The Name:__ Webstorm treats this like the process name, so it's the identifier that will be used to display any output that the process is running. It can be whatever you like!
308+
1. __File Type:__ `Dart`, since that's what the formatter was built for.
309+
1. __Scope:__ `Project Files` will produce the desired effect, but if a different option works better for you then feel free! For more information on scoping, see [the docs](file-watcher-docs).
310+
1. __Program:__ The exutable to run. In this case, it can just be `pub`. If there are any issues, providing a full path to the executable may have the desired outcome. For pub, this is most likely `/usr/local/bin/pub`.
311+
1. __Arguments:__ The rest of the command, and by default should be `run dart_dev hackFastFormat "$FilePathRelativeToProjectRoot$"`. Here's the breakdown:
312+
- `run dart_dev hackFastFormat`: Simply the process to run.
313+
- `"$FilePathRelativeToProjectRoot$"`: The environment variable that will target only the changed file.
314+
1. __Output Paths to Refresh:__ `"$FilePathRelativeToProjectRoot$"`.
315+
1. __Working Directory:__ `$ContentRoot$`.
316+
1. __Advanced Options:__ Uncheck all the boxes. Again, if you experiment and find having some of them checked is better then feel free! However, the expected behavior occurs when none of them are checked.
317+
318+
234319
[api-docs]: https://pub.dev/documentation/dart_dev/latest/dart_dev/dart_dev-library.html
235320
[build-filter]: https://github.com/dart-lang/build/blob/master/build_runner/CHANGELOG.md#new-feature-build-filters
236321
[core-config]: https://github.com/Workiva/dart_dev/blob/master/lib/src/core_config.dart
237322
[docs]: https://github.com/Workiva/dart_dev/blob/master/doc/
323+
[file-watcher-docs]: https://www.jetbrains.com/help/webstorm/using-file-watchers.html#
238324
[upgrade-guide]: https://github.com/Workiva/dart_dev/blob/master/doc/v3-upgrade-guide.md
325+
[vs-code-formatter]: https://github.com/Workiva/vs-code-format-on-save/blob/master/README.md

images/file_watcher_config.png

230 KB
Loading

images/file_watcher_pane.png

76.8 KB
Loading

lib/src/executable.dart

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import 'dart:async';
22
import 'dart:io';
3+
import 'dart:math';
34

5+
import 'package:analyzer/dart/analysis/utilities.dart';
46
import 'package:args/command_runner.dart';
7+
import 'package:dart_dev/dart_dev.dart';
58
import 'package:dart_dev/src/dart_dev_tool.dart';
9+
import 'package:dart_dev/src/utils/format_tool_builder.dart';
610
import 'package:dart_dev/src/utils/parse_flag_from_args.dart';
711
import 'package:io/ansi.dart';
812
import 'package:io/io.dart' show ExitCode;
913
import 'package:logging/logging.dart';
1014
import 'package:path/path.dart' as p;
1115

16+
import '../utils.dart';
1217
import 'dart_dev_runner.dart';
18+
import 'tools/over_react_format_tool.dart';
1319
import 'utils/assert_dir_is_dart_package.dart';
1420
import 'utils/dart_tool_cache.dart';
1521
import 'utils/ensure_process_exit.dart';
@@ -30,25 +36,28 @@ final _relativeDevDartPath = p.relative(
3036
from: p.absolute(p.dirname(_runScriptPath)),
3137
);
3238

33-
final _log = Logger('DartDev');
34-
3539
Future<void> run(List<String> args) async {
3640
attachLoggerToStdio(args);
37-
3841
final configExists = File(_configPath).existsSync();
3942
final oldDevDartExists = File(_oldDevDartPath).existsSync();
4043

4144
if (!configExists) {
42-
_log.fine('No custom `tool/dart_dev/config.dart` file found; '
45+
log.fine('No custom `tool/dart_dev/config.dart` file found; '
4346
'using default config.');
4447
}
4548
if (oldDevDartExists) {
46-
_log.warning(yellow.wrap(
49+
log.warning(yellow.wrap(
4750
'dart_dev v3 now expects configuration to be at `$_configPath`,\n'
4851
'but `$_oldDevDartPath` still exists. View the guide to see how to upgrade:\n'
4952
'https://github.com/Workiva/dart_dev/blob/master/doc/v3-upgrade-guide.md'));
5053
}
5154

55+
if (args.contains('hackFastFormat') && !oldDevDartExists) {
56+
await handleFastFormat(args);
57+
58+
return;
59+
}
60+
5261
generateRunScript();
5362
final process = await Process.start(
5463
Platform.executable, [_runScriptPath, ...args],
@@ -57,9 +66,45 @@ Future<void> run(List<String> args) async {
5766
exitCode = await process.exitCode;
5867
}
5968

69+
Future<void> handleFastFormat(List<String> args) async {
70+
assertDirIsDartPackage();
71+
72+
DevTool formatTool;
73+
final configFile = File(_configPath);
74+
if (configFile.existsSync()) {
75+
final toolBuilder = FormatToolBuilder();
76+
parseString(content: configFile.readAsStringSync())
77+
.unit
78+
.accept(toolBuilder);
79+
formatTool = toolBuilder
80+
.formatDevTool; // could be null if no custom `format` entry found
81+
82+
if (formatTool == null && toolBuilder.failedToDetectAKnownFormatter) {
83+
exitCode = ExitCode.config.code;
84+
log.severe('Failed to reconstruct the format tool\'s configuration.\n\n'
85+
'This is likely because dart_dev expects either the FormatTool class or the\n'
86+
'OverReactFormatTool class.');
87+
return;
88+
}
89+
}
90+
91+
formatTool ??= chooseDefaultFormatTool();
92+
93+
try {
94+
exitCode = await DartDevRunner({'hackFastFormat': formatTool}).run(args);
95+
} catch (error, stack) {
96+
log.severe('Uncaught Exception:', error, stack);
97+
if (!parseFlagFromArgs(args, 'verbose', abbr: 'v')) {
98+
// Always print the stack trace for an uncaught exception.
99+
stderr.writeln(stack);
100+
}
101+
exitCode = ExitCode.unavailable.code;
102+
}
103+
}
104+
60105
void generateRunScript() {
61106
if (shouldWriteRunScript) {
62-
logTimedSync(_log, 'Generating run script', () {
107+
logTimedSync(log, 'Generating run script', () {
63108
createCacheDir();
64109
_runScript.writeAsStringSync(buildDartDevRunScriptContents());
65110
}, level: Level.INFO);
@@ -93,7 +138,7 @@ Future<void> runWithConfig(
93138
try {
94139
assertDirIsDartPackage();
95140
} on DirectoryIsNotPubPackage catch (error) {
96-
_log.severe(error);
141+
log.severe(error);
97142
return ExitCode.usage.code;
98143
}
99144

@@ -119,11 +164,25 @@ Future<void> runWithConfig(
119164
stderr.writeln(error);
120165
exitCode = ExitCode.usage.code;
121166
} catch (error, stack) {
122-
_log.severe('Uncaught Exception:', error, stack);
167+
log.severe('Uncaught Exception:', error, stack);
123168
if (!parseFlagFromArgs(args, 'verbose', abbr: 'v')) {
124169
// Always print the stack trace for an uncaught exception.
125170
stderr.writeln(stack);
126171
}
127172
exitCode = ExitCode.unavailable.code;
128173
}
129174
}
175+
176+
/// Returns [OverReactFormatTool] if `over_react_format` is a direct dependency,
177+
/// and the default [FormatTool] otherwise.
178+
DevTool chooseDefaultFormatTool({String path}) {
179+
final pubspec = cachedPubspec(path: path);
180+
const orf = 'over_react_format';
181+
final hasOverReactFormat = pubspec.dependencies.containsKey(orf) ||
182+
pubspec.devDependencies.containsKey(orf) ||
183+
pubspec.dependencyOverrides.containsKey(orf);
184+
185+
return hasOverReactFormat
186+
? OverReactFormatTool()
187+
: (FormatTool()..formatter = Formatter.dartStyle);
188+
}

lib/src/tools/format_tool.dart

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -354,10 +354,8 @@ Iterable<String> buildArgs(
354354
'-n',
355355
'--set-exit-if-changed',
356356
],
357-
if (mode == FormatMode.overwrite)
358-
'-w',
359-
if (mode == FormatMode.dryRun)
360-
'-n',
357+
if (mode == FormatMode.overwrite) '-w',
358+
if (mode == FormatMode.dryRun) '-n',
361359

362360
// 2. Statically configured args from [FormatTool.formatterArgs]
363361
...?configuredFormatterArgs,
@@ -402,9 +400,14 @@ FormatExecution buildExecution(
402400
String path,
403401
}) {
404402
FormatMode mode;
403+
404+
final useRestForInputs = (context?.argResults?.rest?.isNotEmpty ?? false) &&
405+
context.commandName == 'hackFastFormat';
406+
405407
if (context.argResults != null) {
406408
assertNoPositionalArgsNorArgsAfterSeparator(
407409
context.argResults, context.usageException,
410+
allowRest: useRestForInputs,
408411
commandName: context.commandName,
409412
usageFooter: 'Arguments can be passed to the "dartfmt" process via the '
410413
'--formatter-args option.');
@@ -422,11 +425,21 @@ FormatExecution buildExecution(
422425
'format tool to use "dartfmt" instead.'));
423426
return FormatExecution.exitEarly(ExitCode.config.code);
424427
}
425-
final inputs = FormatTool.getInputs(
426-
exclude: exclude,
427-
root: path,
428-
collapseDirectories: true,
429-
);
428+
429+
if (context.commandName == 'hackFastFormat' && !useRestForInputs) {
430+
context.usageException('"hackFastFormat" must specify targets to format.\n'
431+
'hackFastFormat should only be used to format specific files. '
432+
'Running the command over an entire project may format files that '
433+
'would be excluded using the standard "format" command.');
434+
}
435+
436+
final inputs = useRestForInputs
437+
? FormatterInputs({...context.argResults.rest})
438+
: FormatTool.getInputs(
439+
exclude: exclude,
440+
root: path,
441+
collapseDirectories: true,
442+
);
430443

431444
if (inputs.includedFiles.isEmpty) {
432445
_log.severe('The formatter cannot run because no inputs could be found '
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import 'dart:async';
2+
import 'dart:io';
3+
4+
import 'package:dart_dev/dart_dev.dart';
5+
import 'package:dart_dev/utils.dart';
6+
7+
import '../tools/format_tool.dart';
8+
9+
class OverReactFormatTool extends DevTool {
10+
/// Wrap lines longer than this.
11+
///
12+
/// Default is 80.
13+
int lineLength;
14+
15+
@override
16+
String description =
17+
'Format dart files in this package with over_react_format.';
18+
19+
@override
20+
FutureOr<int> run([DevToolExecutionContext context]) async {
21+
Iterable<String> paths = context?.argResults?.rest;
22+
if (paths?.isEmpty ?? true) {
23+
context?.usageException(
24+
'"hackFastFormat" must specify targets to format.\n'
25+
'hackFastFormat should only be used to format specific files. '
26+
'Running the command over an entire project may format files that '
27+
'would be excluded using the standard "format" command.');
28+
}
29+
final args = [
30+
'run',
31+
'over_react_format',
32+
if (lineLength != null) '--line-length=$lineLength'
33+
];
34+
final process = ProcessDeclaration('pub', [...args, ...paths],
35+
mode: ProcessStartMode.inheritStdio);
36+
logCommand('pub', paths, args, verbose: context?.verbose);
37+
return runProcessAndEnsureExit(process);
38+
}
39+
}

lib/src/tools/test_tool.dart

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,7 @@ List<String> buildArgs({
198198
// 1. Statically configured args from [WebdevServeTool.buildArgs]
199199
...?configuredBuildArgs,
200200
// 2. Pass through the --release flag if provided.
201-
if (flagValue(argResults, 'release') ?? false)
202-
'--release',
201+
if (flagValue(argResults, 'release') ?? false) '--release',
203202
// 3. Build filters to narrow the build to only the target tests.
204203
// (If no test dirs/files are passed in as args, then no build filters
205204
// will be created.)
@@ -236,15 +235,12 @@ List<String> buildArgs({
236235
return [
237236
// `pub run test` or `pub run build_runner test`
238237
'run',
239-
if (useBuildTest)
240-
'build_runner',
238+
if (useBuildTest) 'build_runner',
241239
'test',
242240

243241
// Add the args targeting the build_runner command.
244-
if (useBuildTest)
245-
...buildArgs,
246-
if (useBuildTest && testArgs.isNotEmpty)
247-
'--',
242+
if (useBuildTest) ...buildArgs,
243+
if (useBuildTest && testArgs.isNotEmpty) '--',
248244

249245
// Add the args targeting the test command.
250246
...testArgs,

lib/src/tools/webdev_serve_tool.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,7 @@ List<String> buildArgs(
142142
// 1. Statically configured args from [WebdevServeTool.webdevArgs]
143143
...?configuredWebdevArgs,
144144
// 2. The -r|--release flag
145-
if (argResults != null && argResults['release'] ?? false)
146-
'--release',
145+
if (argResults != null && argResults['release'] ?? false) '--release',
147146
// 3. Args passed to --webdev-args
148147
...?splitSingleOptionValue(argResults, 'webdev-args'),
149148
];

0 commit comments

Comments
 (0)