Skip to content

Commit 5d84439

Browse files
DanTupCommit Queue
authored and
Commit Queue
committed
[analyzer] Add support for "formatter" in analysis_options + use page_width in LSP
This adds support for validation + completion for `formatter/page_width` in analysis_options, and uses this value in preference to the client-supplied value in the LSP server. It does not yet add support for the legacy protocol, and there are a few questions in #56864 (comment). See #56864 Change-Id: I7844e3abd1191beb9cc24197bbc8aa7c9e9ad4fa Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/388822 Commit-Queue: Brian Wilkerson <[email protected]> Reviewed-by: Brian Wilkerson <[email protected]> Reviewed-by: Konstantin Shcheglov <[email protected]>
1 parent c8ee0e7 commit 5d84439

File tree

18 files changed

+303
-11
lines changed

18 files changed

+303
-11
lines changed

pkg/analysis_server/lib/src/lsp/client_configuration.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,8 @@ class LspResourceClientConfiguration {
286286
true;
287287
}
288288

289-
/// The line length used when formatting documents.
289+
/// The line length used when formatting documents if not specified in
290+
/// `analysis_options.yaml`.
290291
///
291292
/// If null, the formatters default will be used.
292293
int? get lineLength =>

pkg/analysis_server/lib/src/lsp/handlers/handler_format_on_type.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class FormatOnTypeHandler extends SharedMessageHandler<
3939
}
4040

4141
var lineLength = server.lspClientConfiguration.forResource(path).lineLength;
42-
return generateEditsForFormatting(result, lineLength);
42+
return generateEditsForFormatting(result, defaultPageWidth: lineLength);
4343
}
4444

4545
@override

pkg/analysis_server/lib/src/lsp/handlers/handler_format_range.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ class FormatRangeHandler extends SharedMessageHandler<
3939
}
4040

4141
var lineLength = server.lspClientConfiguration.forResource(path).lineLength;
42-
return generateEditsForFormatting(result, lineLength, range: range);
42+
return generateEditsForFormatting(result,
43+
defaultPageWidth: lineLength, range: range);
4344
}
4445

4546
@override

pkg/analysis_server/lib/src/lsp/handlers/handler_formatting.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class FormattingHandler
3939
}
4040

4141
var lineLength = server.lspClientConfiguration.forResource(path).lineLength;
42-
return generateEditsForFormatting(result, lineLength);
42+
return generateEditsForFormatting(result, defaultPageWidth: lineLength);
4343
}
4444

4545
@override

pkg/analysis_server/lib/src/lsp/source_edits.dart

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,25 @@ ErrorOr<({String content, List<plugin.SourceEdit> edits})>
7878
return ErrorOr.success((content: newContent, edits: serverEdits));
7979
}
8080

81+
/// Generates a list of [TextEdit]s to format the code for [result].
82+
///
83+
/// [defaultPageWidth] will be used as the default page width if [result] does
84+
/// not have an analysis_options file that defines a page width.
85+
///
86+
/// If [range] is provided, only edits that intersect with this range will be
87+
/// returned.
8188
ErrorOr<List<TextEdit>?> generateEditsForFormatting(
82-
ParsedUnitResult result,
83-
int? lineLength, {
89+
ParsedUnitResult result, {
90+
int? defaultPageWidth,
8491
Range? range,
8592
}) {
8693
var unformattedSource = result.content;
8794

95+
// The analysis options page width always takes priority over the default from
96+
// the LSP configuration.
97+
var effectivePageWidth =
98+
result.analysisOptions.formatterOptions.pageWidth ?? defaultPageWidth;
99+
88100
var code = SourceCode(unformattedSource);
89101
SourceCode formattedResult;
90102
try {
@@ -94,9 +106,9 @@ ErrorOr<List<TextEdit>?> generateEditsForFormatting(
94106
var languageVersion =
95107
result.unit.declaredElement?.library.languageVersion.effective ??
96108
DartFormatter.latestLanguageVersion;
97-
formattedResult =
98-
DartFormatter(pageWidth: lineLength, languageVersion: languageVersion)
99-
.formatSource(code);
109+
var formatter = DartFormatter(
110+
pageWidth: effectivePageWidth, languageVersion: languageVersion);
111+
formattedResult = formatter.formatSource(code);
100112
} on FormatterException {
101113
// If the document fails to parse, just return no edits to avoid the
102114
// use seeing edits on every save with invalid code (if LSP gains the

pkg/analysis_server/lib/src/services/completion/yaml/analysis_options_generator.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ class AnalysisOptionsGenerator extends YamlCompletionGenerator {
3939
AnalyzerOptions.codeStyle: MapProducer({
4040
AnalyzerOptions.format: BooleanProducer(),
4141
}),
42+
AnalyzerOptions.formatter: MapProducer({
43+
AnalyzerOptions.pageWidth: EmptyProducer(),
44+
}),
4245
// TODO(brianwilkerson): Create a producer to produce `package:` URIs.
4346
AnalyzerOptions.include: EmptyProducer(),
4447
// TODO(brianwilkerson): Create constants for 'linter' and 'rules'.

pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -829,7 +829,7 @@ class BulkFixProcessor {
829829
continue;
830830
}
831831

832-
var formatResult = generateEditsForFormatting(result, null);
832+
var formatResult = generateEditsForFormatting(result);
833833
await formatResult.mapResult((formatResult) async {
834834
var edits = formatResult ?? [];
835835
if (edits.isNotEmpty) {

pkg/analysis_server/test/domain_completion_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2035,6 +2035,8 @@ suggestions
20352035
kind: identifier
20362036
|code-style: |
20372037
kind: identifier
2038+
|formatter: |
2039+
kind: identifier
20382040
|include: |
20392041
kind: identifier
20402042
|linter: |

pkg/analysis_server/test/lsp/format_test.dart

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ void main() {
2020

2121
@reflectiveTest
2222
class FormatTest extends AbstractLspAnalysisServerTest {
23+
/// Some sample code that is over 50 characters and will be wrapped if the
24+
/// page width is set to 40.
25+
static const codeThatWrapsAt40 =
26+
"var a = ' 10 20 30 40';";
27+
2328
Future<List<TextEdit>> expectFormattedContents(
2429
Uri uri, String original, String expected) async {
2530
var formatEdits = (await formatDocument(uri))!;
@@ -36,6 +41,11 @@ class FormatTest extends AbstractLspAnalysisServerTest {
3641
return formatEdits;
3742
}
3843

44+
Future<String> formatContents(Uri uri, String original) async {
45+
var formatEdits = (await formatDocument(uri))!;
46+
return applyTextEdits(original, formatEdits);
47+
}
48+
3949
Future<void> test_alreadyFormatted() async {
4050
const contents = '''
4151
void f() {
@@ -383,6 +393,83 @@ void f() => print('123456789 123456789 123456789 123456789 123456789 123456789 1
383393
await expectFormattedContents(mainFileUri, contents, expectedLongLines);
384394
}
385395

396+
Future<void> test_lineLength_analysisOptions() async {
397+
const codeContent = codeThatWrapsAt40;
398+
const optionsContent = '''
399+
formatter:
400+
page_width: 40
401+
''';
402+
403+
newFile(analysisOptionsPath, optionsContent);
404+
newFile(mainFilePath, codeContent);
405+
await initialize();
406+
407+
// Ignore trailing newlines when checking for wrapping.
408+
var formatted = await formatContents(mainFileUri, codeContent);
409+
expect(formatted.trim(), contains('\n'));
410+
}
411+
412+
Future<void> test_lineLength_analysisOptions_nestedEmpty() async {
413+
const codeContent = codeThatWrapsAt40;
414+
const optionsContent = '''
415+
formatter:
416+
page_width: 40
417+
''';
418+
var nestedAnalysisOptionsPath =
419+
join(projectFolderPath, 'lib', 'analysis_options.yaml');
420+
421+
newFile(analysisOptionsPath, optionsContent);
422+
newFile(
423+
nestedAnalysisOptionsPath, '# empty'); // suppress the parent options.
424+
newFile(mainFilePath, codeContent);
425+
await initialize();
426+
427+
// Ignore trailing newlines when checking for wrapping.
428+
var formatted = await formatContents(mainFileUri, codeContent);
429+
expect(formatted.trim(), isNot(contains('\n')));
430+
}
431+
432+
Future<void> test_lineLength_analysisOptions_nestedIncludes() async {
433+
const codeContent = codeThatWrapsAt40;
434+
const optionsContent = '''
435+
formatter:
436+
page_width: 40
437+
''';
438+
var nestedAnalysisOptionsPath =
439+
join(projectFolderPath, 'lib', 'analysis_options.yaml');
440+
441+
newFile(analysisOptionsPath, optionsContent);
442+
newFile(nestedAnalysisOptionsPath, 'include: ../analysis_options.yaml');
443+
newFile(mainFilePath, codeContent);
444+
await initialize();
445+
446+
// Ignore trailing newlines when checking for wrapping.
447+
var formatted = await formatContents(mainFileUri, codeContent);
448+
expect(formatted.trim(), contains('\n'));
449+
}
450+
451+
Future<void> test_lineLength_analysisOptions_overridesConfig() async {
452+
const codeContent = codeThatWrapsAt40;
453+
const optionsContent = '''
454+
formatter:
455+
page_width: 40
456+
''';
457+
458+
newFile(analysisOptionsPath, optionsContent);
459+
newFile(mainFilePath, codeContent);
460+
await provideConfig(
461+
initialize,
462+
{
463+
// This won't apply because analysis_options wins.
464+
'lineLength': 100,
465+
},
466+
);
467+
468+
// Ignore trailing newlines when checking for wrapping.
469+
var formatted = await formatContents(mainFileUri, codeContent);
470+
expect(formatted.trim(), contains('\n'));
471+
}
472+
386473
Future<void> test_lineLength_outsideWorkspaceFolders() async {
387474
const contents = '''
388475
void f() {

pkg/analysis_server/test/src/services/completion/yaml/analysis_options_generator_test.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,20 @@ code-style:
128128
getCompletions('^');
129129
assertSuggestion('${AnalyzerOptions.analyzer}: ');
130130
assertSuggestion('${AnalyzerOptions.codeStyle}: ');
131+
assertSuggestion('${AnalyzerOptions.formatter}: ');
131132
assertSuggestion('${AnalyzerOptions.include}: ');
132133
// TODO(brianwilkerson): Replace this with a constant.
133134
assertSuggestion('linter: ');
134135
}
135136

137+
void test_formatter() {
138+
getCompletions('''
139+
formatter:
140+
^
141+
''');
142+
assertSuggestion('${AnalyzerOptions.pageWidth}: ');
143+
}
144+
136145
void test_linter() {
137146
getCompletions('''
138147
linter:

pkg/analysis_server/tool/lsp_spec/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Client workspace settings are requested with `workspace/configuration` during in
3636

3737
- `dart.analysisExcludedFolders` (`List<String>?`): An array of paths (absolute or relative to each workspace folder) that should be excluded from analysis.
3838
- `dart.enableSdkFormatter` (`bool?`): When set to `false`, prevents registration (or unregisters) the SDK formatter. When set to `true` or not supplied, will register/reregister the SDK formatter.
39-
- `dart.lineLength` (`int?`): The number of characters the formatter should wrap code at. If unspecified, code will be wrapped at `80` characters.
39+
- `dart.lineLength` (`int?`): Sets a default value for the formatter to wrap code at if no value is specified in `formatter.page_width` in `analysis_options.yaml`. If unspecified by both, code will be wrapped at `80` characters.
4040
- `dart.completeFunctionCalls` (`bool?`): When set to true, completes functions/methods with their required parameters.
4141
- `dart.showTodos` (`bool?`): Whether to generate diagnostics for TODO comments. If unspecified, diagnostics will not be generated.
4242
- `dart.renameFilesWithClasses` (`String`): When set to `"always"`, will include edits to rename files when classes are renamed if the filename matches the class name (but in snake_form). When set to `"prompt"`, a prompt will be shown on each class rename asking to confirm the file rename. Otherwise, files will not be renamed. Renames are performed using LSP's ResourceOperation edits - that means the rename is simply included in the resulting `WorkspaceEdit` and must be handled by the client.

pkg/analyzer/lib/dart/analysis/analysis_options.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'package:analyzer/dart/analysis/code_style_options.dart';
66
import 'package:analyzer/dart/analysis/features.dart';
7+
import 'package:analyzer/dart/analysis/formatter_options.dart';
78
import 'package:analyzer/source/error_processor.dart';
89
import 'package:analyzer/src/lint/linter.dart';
910
import 'package:pub_semver/src/version_constraint.dart';
@@ -38,6 +39,9 @@ abstract class AnalysisOptions {
3839
/// analysis.
3940
List<String> get excludePatterns;
4041

42+
/// Return the options used to control the formatter.
43+
FormatterOptions get formatterOptions;
44+
4145
/// Return `true` if analysis is to generate hint results (e.g. best practices
4246
/// and analysis based on certain annotations).
4347
@Deprecated("Use 'warning' instead")
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
/// A set of options related to the formatter that apply to the code within a
6+
/// single analysis context.
7+
///
8+
/// Clients may not extend, implement or mix-in this class.
9+
abstract class FormatterOptions {
10+
/// The width configured for where the formatter should wrap code.
11+
int? get pageWidth;
12+
}

pkg/analyzer/lib/src/analysis_options/apply_options.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
import 'package:analyzer/dart/analysis/code_style_options.dart';
66
import 'package:analyzer/dart/analysis/features.dart';
7+
import 'package:analyzer/dart/analysis/formatter_options.dart';
78
import 'package:analyzer/error/error.dart';
89
import 'package:analyzer/source/error_processor.dart';
910
import 'package:analyzer/src/analysis_options/code_style_options.dart';
11+
import 'package:analyzer/src/analysis_options/formatter_options.dart';
1012
import 'package:analyzer/src/dart/analysis/experiments.dart';
1113
import 'package:analyzer/src/generated/engine.dart';
1214
import 'package:analyzer/src/generated/utilities_general.dart';
@@ -148,6 +150,18 @@ extension on AnalysisOptionsImpl {
148150
return CodeStyleOptionsImpl(this, useFormatter: useFormatter);
149151
}
150152

153+
FormatterOptions buildFormatterOptions(YamlNode? formatter) {
154+
int? pageWidth;
155+
if (formatter is YamlMap) {
156+
var formatNode = formatter.valueAt(AnalyzerOptions.pageWidth);
157+
var formatValue = formatNode?.value;
158+
if (formatValue is int && formatValue > 0) {
159+
pageWidth = formatValue;
160+
}
161+
}
162+
return FormatterOptionsImpl(this, pageWidth: pageWidth);
163+
}
164+
151165
void _applyLegacyPlugins(YamlNode? plugins) {
152166
var pluginName = plugins.stringValue;
153167
if (pluginName != null) {
@@ -226,6 +240,10 @@ extension AnalysisOptionsImplExtensions on AnalysisOptionsImpl {
226240
var codeStyle = optionMap.valueAt(AnalyzerOptions.codeStyle);
227241
codeStyleOptions = buildCodeStyleOptions(codeStyle);
228242

243+
// Process the 'formatter' option.
244+
var formatter = optionMap.valueAt(AnalyzerOptions.formatter);
245+
formatterOptions = buildFormatterOptions(formatter);
246+
229247
var config = parseConfig(optionMap);
230248
if (config != null) {
231249
var enabledRules = Registry.ruleRegistry.enabled(config);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:analyzer/dart/analysis/analysis_options.dart';
6+
import 'package:analyzer/dart/analysis/formatter_options.dart';
7+
8+
/// The concrete implementation of [FormatterOptions].
9+
class FormatterOptionsImpl implements FormatterOptions {
10+
/// The analysis options that owns this instance.
11+
final AnalysisOptions options;
12+
13+
/// The width configured for where the formatter should wrap code.
14+
@override
15+
final int? pageWidth;
16+
17+
FormatterOptionsImpl(this.options, {this.pageWidth});
18+
}

pkg/analyzer/lib/src/generated/engine.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import 'package:analyzer/dart/analysis/analysis_options.dart';
99
import 'package:analyzer/dart/analysis/code_style_options.dart';
1010
import 'package:analyzer/dart/analysis/declared_variables.dart';
1111
import 'package:analyzer/dart/analysis/features.dart';
12+
import 'package:analyzer/dart/analysis/formatter_options.dart';
1213
import 'package:analyzer/error/error.dart';
1314
import 'package:analyzer/file_system/file_system.dart';
1415
import 'package:analyzer/instrumentation/instrumentation.dart';
1516
import 'package:analyzer/source/error_processor.dart';
1617
import 'package:analyzer/source/line_info.dart';
1718
import 'package:analyzer/source/source.dart';
1819
import 'package:analyzer/src/analysis_options/code_style_options.dart';
20+
import 'package:analyzer/src/analysis_options/formatter_options.dart';
1921
import 'package:analyzer/src/dart/analysis/experiments.dart';
2022
import 'package:analyzer/src/generated/source.dart' show SourceFactory;
2123
import 'package:analyzer/src/lint/linter.dart';
@@ -239,6 +241,9 @@ class AnalysisOptionsImpl implements AnalysisOptions {
239241
@override
240242
late CodeStyleOptions codeStyleOptions;
241243

244+
@override
245+
late FormatterOptions formatterOptions;
246+
242247
/// The set of "un-ignorable" error names, as parsed in [AnalyzerOptions] from
243248
/// an analysis options file.
244249
Set<String> unignorableNames = {};
@@ -247,12 +252,14 @@ class AnalysisOptionsImpl implements AnalysisOptions {
247252
/// values.
248253
AnalysisOptionsImpl({this.file}) {
249254
codeStyleOptions = CodeStyleOptionsImpl(this, useFormatter: false);
255+
formatterOptions = FormatterOptionsImpl(this);
250256
}
251257

252258
/// Initialize a newly created set of analysis options to have the same values
253259
/// as those in the given set of analysis [options].
254260
AnalysisOptionsImpl.from(AnalysisOptions options) {
255261
codeStyleOptions = options.codeStyleOptions;
262+
formatterOptions = options.formatterOptions;
256263
contextFeatures = options.contextFeatures;
257264
enabledLegacyPluginNames = options.enabledLegacyPluginNames;
258265
errorProcessors = options.errorProcessors;

0 commit comments

Comments
 (0)