| // Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'package:analysis_server/lsp_protocol/protocol.dart'; | |
| import 'package:analysis_server/src/lsp/constants.dart'; | |
| import 'package:analyzer/src/test_utilities/platform.dart'; | |
| import 'package:analyzer/src/test_utilities/test_code_format.dart'; | |
| import 'package:test/test.dart'; | |
| import 'package:test_reflective_loader/test_reflective_loader.dart'; | |
| import '../tool/lsp_spec/matchers.dart'; | |
| import '../utils/matchers.dart'; | |
| import '../utils/test_code_extensions.dart'; | |
| import 'server_abstract.dart'; | |
| void main() { | |
| defineReflectiveSuite(() { | |
| defineReflectiveTests(FormatTest); | |
| }); | |
| } | |
| @reflectiveTest | |
| class FormatTest extends AbstractLspAnalysisServerTest { | |
| /// Some sample code that is over 50 characters and will be wrapped if the | |
| /// page width is set to 40. | |
| static const codeThatWrapsAt40 = | |
| "var a = ' 10 20 30 40';"; | |
| /// Verifies the result of a format-on-type request for the character in the | |
| /// marked range in [content]. | |
| /// | |
| /// If the marked range has a position, it will be used for the request, | |
| /// otherwise the end point of the range will be used. | |
| Future<List<TextEdit>?> expectFormatOnTypeResult( | |
| String content, | |
| Matcher matcher, | |
| ) async { | |
| var code = TestCode.parseNormalized(content); | |
| // We use range to make it really explicit which character is typed and not | |
| // have to worry about whether it's before or after the ^. | |
| expect(code.ranges, hasLength(1)); | |
| expect(code.positions, hasLength(anyOf(0, 1))); | |
| var typedCharacter = code.code.substring( | |
| code.range.sourceRange.offset, | |
| code.range.sourceRange.end, | |
| ); | |
| await initialize(); | |
| await openFile(mainFileUri, code.code); | |
| var result = await formatOnType( | |
| mainFileUri, | |
| code.positions.isNotEmpty ? code.position.position : code.range.range.end, | |
| typedCharacter, | |
| ); | |
| expect( | |
| result, | |
| matcher, | |
| reason: | |
| 'Typing the character "$typedCharacter" at the marked location ' | |
| 'should match the supplied matcher', | |
| ); | |
| return result; | |
| } | |
| Future<List<TextEdit>> expectFormattedContents( | |
| Uri uri, | |
| String original, | |
| String expected, | |
| ) async { | |
| var formatEdits = (await formatDocument(uri))!; | |
| var formattedContents = applyTextEdits(original, formatEdits); | |
| expect(formattedContents, equalsNormalized(expected)); | |
| return formatEdits; | |
| } | |
| Future<List<TextEdit>> expectRangeFormattedContents( | |
| Uri uri, | |
| TestCode code, | |
| String expected, | |
| ) async { | |
| var formatEdits = (await formatRange(uri, code.range.range))!; | |
| var formattedContents = applyTextEdits(code.code, formatEdits); | |
| expect(formattedContents, equalsNormalized(expected)); | |
| return formatEdits; | |
| } | |
| Future<String> formatContents(Uri uri, String original) async { | |
| var formatEdits = (await formatDocument(uri))!; | |
| return applyTextEdits(original, formatEdits); | |
| } | |
| Future<void> test_alreadyFormatted() async { | |
| var code = TestCode.parseNormalized(''' | |
| void f() { | |
| print('test'); | |
| } | |
| '''); | |
| await initialize(); | |
| await openFile(mainFileUri, code.code); | |
| var formatEdits = await formatDocument(mainFileUri); | |
| expect(formatEdits, isNull); | |
| } | |
| /// Formatting a file with '\r\n' and then a file with '\n' should not result | |
| /// in '\r' being added to the second file. | |
| Future<void> test_changedLineEndings() async { | |
| await provideConfig( | |
| initialize, | |
| // The bug only occurred with an explicit line length because reuse | |
| // of the formatter accidentally required lineLength match the formatters | |
| // (non-null) line length. | |
| {'lineLength': 80}, | |
| ); | |
| // First format the doc with '\r\n'. | |
| await openFile(mainFileUri, 'int? a;\r\n'); | |
| await formatDocument(mainFileUri); | |
| // Now replace and format with '\n'. | |
| await replaceFile(2, mainFileUri, 'int? a;\n'); | |
| var formatEdits = await formatDocument(mainFileUri); | |
| // Expect no edits because this document was already formatted. | |
| // When the bug occurs, we'd see edits to add a '\r'. | |
| expect(formatEdits, isNull); | |
| } | |
| Future<void> test_complex() async { | |
| failTestOnErrorDiagnostic = false; | |
| var code = TestCode.parseNormalized(''' | |
| ErrorOr<Pair<A, List<B>>> c( | |
| String d, | |
| List< | |
| Either2<E, | |
| F>> | |
| g, { | |
| h = false, | |
| }) { | |
| } | |
| '''); | |
| var expected = ''' | |
| ErrorOr<Pair<A, List<B>>> c(String d, List<Either2<E, F>> g, {h = false}) {} | |
| '''; | |
| await initialize(); | |
| await openFile(mainFileUri, code.code); | |
| await expectFormattedContents(mainFileUri, code.code, expected); | |
| } | |
| /// Ensures we use the same registration ID when unregistering even if the | |
| /// server has regenerated registrations multiple times. | |
| Future<void> test_dynamicRegistration_correctIdAfterMultipleChanges() async { | |
| setDocumentFormattingDynamicRegistration(); | |
| setDidChangeConfigurationDynamicRegistration(); | |
| var registrations = <Registration>[]; | |
| // Provide empty config and collect dynamic registrations during | |
| // initialization. | |
| await provideConfig( | |
| () => monitorDynamicRegistrations(registrations, initialize), | |
| {}, | |
| ); | |
| Registration? registration(Method method) => | |
| registrationFor(registrations, method); | |
| // By default, the formatters should have been registered. | |
| expect(registration(Method.textDocument_formatting), isNotNull); | |
| expect(registration(Method.textDocument_onTypeFormatting), isNotNull); | |
| expect(registration(Method.textDocument_rangeFormatting), isNotNull); | |
| // Sending config updates causes the server to rebuild its list of registrations | |
| // which exposes a previous bug where we'd retain newly-built registrations | |
| // that may not have been sent to the client (because they had previously | |
| // been sent), resulting in the wrong ID being used for unregistration. | |
| await updateConfig({'foo1': true}); | |
| await updateConfig({'foo1': null}); | |
| // They should be unregistered if we change the config to disabled. | |
| await monitorDynamicUnregistrations( | |
| registrations, | |
| () => updateConfig({'enableSdkFormatter': false}), | |
| ); | |
| expect(registration(Method.textDocument_formatting), isNull); | |
| expect(registration(Method.textDocument_onTypeFormatting), isNull); | |
| expect(registration(Method.textDocument_rangeFormatting), isNull); | |
| } | |
| Future<void> test_dynamicRegistration_forConfiguration() async { | |
| setDocumentFormattingDynamicRegistration(); | |
| setDidChangeConfigurationDynamicRegistration(); | |
| var registrations = <Registration>[]; | |
| // Provide empty config and collect dynamic registrations during | |
| // initialization. | |
| await provideConfig( | |
| () => monitorDynamicRegistrations(registrations, initialize), | |
| {}, | |
| ); | |
| Registration? registration(Method method) => | |
| registrationFor(registrations, method); | |
| // By default, the formatters should have been registered. | |
| expect(registration(Method.textDocument_formatting), isNotNull); | |
| expect(registration(Method.textDocument_onTypeFormatting), isNotNull); | |
| expect(registration(Method.textDocument_rangeFormatting), isNotNull); | |
| // They should be unregistered if we change the config to disabled. | |
| await monitorDynamicUnregistrations( | |
| registrations, | |
| () => updateConfig({'enableSdkFormatter': false}), | |
| ); | |
| expect(registration(Method.textDocument_formatting), isNull); | |
| expect(registration(Method.textDocument_onTypeFormatting), isNull); | |
| expect(registration(Method.textDocument_rangeFormatting), isNull); | |
| // They should be reregistered if we change the config to enabled. | |
| await monitorDynamicRegistrations( | |
| registrations, | |
| () => updateConfig({'enableSdkFormatter': true}), | |
| ); | |
| expect(registration(Method.textDocument_formatting), isNotNull); | |
| expect(registration(Method.textDocument_onTypeFormatting), isNotNull); | |
| expect(registration(Method.textDocument_rangeFormatting), isNotNull); | |
| } | |
| Future<void> test_formatOnType_bad_brace_inComment() async { | |
| await expectFormatOnTypeResult(''' | |
| void f () | |
| { | |
| // hello[!}!] | |
| print('test'); | |
| } | |
| ''', isNull); | |
| } | |
| Future<void> test_formatOnType_bad_brace_inString() async { | |
| await expectFormatOnTypeResult(''' | |
| void f () | |
| { | |
| print('te[!}!]st'); | |
| } | |
| ''', isNull); | |
| } | |
| /// Ensure the semicolon after enum constants does not trigger formatting | |
| /// because the formatter removes it if there is nothing after it. | |
| /// | |
| /// https://github.com/dart-lang/sdk/issues/61909 | |
| Future<void> test_formatOnType_bad_semiColon_enumBodySeparator() async { | |
| await expectFormatOnTypeResult(''' | |
| enum E { a, b[!;!] } | |
| ''', isNull); | |
| } | |
| Future<void> test_formatOnType_bad_semicolon_inComment() async { | |
| await expectFormatOnTypeResult(''' | |
| void f () | |
| { | |
| // hello[!;!] | |
| print('test'); | |
| } | |
| ''', isNull); | |
| } | |
| Future<void> test_formatOnType_bad_semicolon_inString() async { | |
| await expectFormatOnTypeResult(''' | |
| void f () | |
| { | |
| print('te[!;!]st'); | |
| } | |
| ''', isNull); | |
| } | |
| Future<void> test_formatOnType_good_brace_blockEnd() async { | |
| await expectFormatOnTypeResult(''' | |
| void f () | |
| { | |
| print('test'); | |
| [!}!] | |
| ''', isNotNull); | |
| } | |
| Future<void> test_formatOnType_good_formatsCode() async { | |
| var code = TestCode.parseNormalized(''' | |
| void f () | |
| { | |
| print('test'); | |
| [!}!] | |
| '''); | |
| var expected = '''void f() { | |
| print('test'); | |
| } | |
| '''; | |
| var formatEdits = (await expectFormatOnTypeResult( | |
| code.markedCode, | |
| isNotNull, | |
| ))!; | |
| var formattedContents = applyTextEdits(code.code, formatEdits); | |
| expect(formattedContents, equalsNormalized(expected)); | |
| } | |
| Future<void> test_formatOnType_good_positionAtEnd() async { | |
| await expectFormatOnTypeResult(''' | |
| void f () | |
| { | |
| [!}^!] | |
| ''', isNotNull); | |
| } | |
| Future<void> test_formatOnType_good_positionAtOffset() async { | |
| await expectFormatOnTypeResult(''' | |
| void f () | |
| { | |
| [!^}!] | |
| ''', isNotNull); | |
| } | |
| Future<void> test_formatOnType_good_semiColon_enumConstructor() async { | |
| await expectFormatOnTypeResult(''' | |
| enum X { | |
| a, | |
| b; | |
| const X()[!;!] | |
| } | |
| ''', isNotNull); | |
| } | |
| Future<void> test_formatOnType_good_semicolon_statementEnd() async { | |
| await expectFormatOnTypeResult(''' | |
| void f () | |
| { | |
| print('test')[!;!] | |
| } | |
| ''', isNotNull); | |
| } | |
| Future<void> test_formatRange_editsOverlapRange() async { | |
| // Only ranges that are fully contained by the range should be applied, | |
| // not those that intersect the start/end. | |
| var code = TestCode.parseNormalized(''' | |
| void f() | |
| { | |
| [! print('test'); | |
| print('test'); | |
| !] print('test'); | |
| } | |
| '''); | |
| var expected = ''' | |
| void f() | |
| { | |
| print('test'); | |
| print('test'); | |
| print('test'); | |
| } | |
| '''; | |
| await initialize(); | |
| await openFile(mainFileUri, code.code); | |
| await expectRangeFormattedContents(mainFileUri, code, expected); | |
| } | |
| Future<void> test_formatRange_expandsLeadingWhitespaceToNearestLine() async { | |
| var code = TestCode.parseNormalized(''' | |
| void f() | |
| { | |
| [! print('test'); // line 2 | |
| print('test'); // line 3 | |
| print('test'); // line 4!] | |
| } | |
| '''); | |
| const expected = ''' | |
| void f() | |
| { | |
| print('test'); // line 2 | |
| print('test'); // line 3 | |
| print('test'); // line 4 | |
| } | |
| '''; | |
| await initialize(); | |
| await openFile(mainFileUri, code.code); | |
| await expectRangeFormattedContents(mainFileUri, code, expected); | |
| } | |
| Future<void> test_formatRange_invalidRange() async { | |
| var code = TestCode.parseNormalized(''' | |
| void f() | |
| { | |
| print('test'); | |
| } | |
| '''); | |
| await initialize(); | |
| await openFile(mainFileUri, code.code); | |
| var formatRangeRequest = formatRange( | |
| mainFileUri, | |
| Range( | |
| start: Position(line: 0, character: 0), | |
| end: Position(line: 10000, character: 0), | |
| ), | |
| ); | |
| await expectLater( | |
| formatRangeRequest, | |
| throwsA(isResponseError(ServerErrorCodes.invalidFileLineCol)), | |
| ); | |
| } | |
| Future<void> test_formatRange_simple() async { | |
| var code = TestCode.parseNormalized(''' | |
| main () | |
| { | |
| print('test'); | |
| } | |
| [!main2 () | |
| { | |
| print('test'); | |
| }!] | |
| main3 () | |
| { | |
| print('test'); | |
| } | |
| '''); | |
| var expected = ''' | |
| main () | |
| { | |
| print('test'); | |
| } | |
| main2() { | |
| print('test'); | |
| } | |
| main3 () | |
| { | |
| print('test'); | |
| } | |
| '''; | |
| await initialize(); | |
| await openFile(mainFileUri, code.code); | |
| await expectRangeFormattedContents(mainFileUri, code, expected); | |
| } | |
| Future<void> test_formatRange_trailingNewline_47702() async { | |
| // Check we complete when a formatted block ends with a newline. | |
| // https://github.com/dart-lang/sdk/issues/47702 | |
| var code = TestCode.parseNormalized(''' | |
| int? a; | |
| [! | |
| int? b; | |
| !] | |
| '''); | |
| var expected = ''' | |
| int? a; | |
| int? b; | |
| '''; | |
| await initialize(); | |
| await openFile(mainFileUri, code.code); | |
| await expectRangeFormattedContents(mainFileUri, code, expected); | |
| } | |
| Future<void> test_invalidSyntax() async { | |
| failTestOnErrorDiagnostic = false; | |
| var code = TestCode.parseNormalized(''' | |
| void f(((( { | |
| print('test'); | |
| } | |
| '''); | |
| await initialize(); | |
| await openFile(mainFileUri, code.code); | |
| var formatEdits = await formatDocument(mainFileUri); | |
| expect(formatEdits, isNull); | |
| } | |
| Future<void> test_lineLength() async { | |
| var code = TestCode.parseNormalized(''' | |
| void f() => | |
| print( | |
| '123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789' | |
| ); | |
| '''); | |
| var expectedDefault = ''' | |
| void f() => print( | |
| '123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789', | |
| ); | |
| '''; | |
| var expectedLongLines = ''' | |
| void f() => print('123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789'); | |
| '''; | |
| // Initialize with config support, supplying an empty config when requested. | |
| await provideConfig( | |
| initialize, | |
| {}, // empty config | |
| ); | |
| await openFile(mainFileUri, code.code); | |
| await expectFormattedContents(mainFileUri, code.code, expectedDefault); | |
| await updateConfig({'lineLength': 500}); | |
| await expectFormattedContents(mainFileUri, code.code, expectedLongLines); | |
| } | |
| Future<void> test_lineLength_analysisOptions() async { | |
| const codeContent = codeThatWrapsAt40; | |
| const optionsContent = ''' | |
| formatter: | |
| page_width: 40 | |
| '''; | |
| newFile(analysisOptionsPath, optionsContent); | |
| newFile(mainFilePath, codeContent); | |
| await initialize(); | |
| // Ignore trailing newlines when checking for wrapping. | |
| var formatted = await formatContents(mainFileUri, codeContent); | |
| expect(formatted.trim(), contains('\n')); | |
| } | |
| Future<void> test_lineLength_analysisOptions_nestedEmpty() async { | |
| const codeContent = codeThatWrapsAt40; | |
| const optionsContent = ''' | |
| formatter: | |
| page_width: 40 | |
| '''; | |
| var nestedAnalysisOptionsPath = join( | |
| projectFolderPath, | |
| 'lib', | |
| 'analysis_options.yaml', | |
| ); | |
| newFile(analysisOptionsPath, optionsContent); | |
| newFile( | |
| nestedAnalysisOptionsPath, | |
| '# empty', | |
| ); // suppress the parent options. | |
| newFile(mainFilePath, codeContent); | |
| await initialize(); | |
| // Ignore trailing newlines when checking for wrapping. | |
| var formatted = await formatContents(mainFileUri, codeContent); | |
| expect(formatted.trim(), isNot(contains('\n'))); | |
| } | |
| Future<void> test_lineLength_analysisOptions_nestedIncludes() async { | |
| const codeContent = codeThatWrapsAt40; | |
| const optionsContent = ''' | |
| formatter: | |
| page_width: 40 | |
| '''; | |
| var nestedAnalysisOptionsPath = join( | |
| projectFolderPath, | |
| 'lib', | |
| 'analysis_options.yaml', | |
| ); | |
| newFile(analysisOptionsPath, optionsContent); | |
| newFile(nestedAnalysisOptionsPath, 'include: ../analysis_options.yaml'); | |
| newFile(mainFilePath, codeContent); | |
| await initialize(); | |
| // Ignore trailing newlines when checking for wrapping. | |
| var formatted = await formatContents(mainFileUri, codeContent); | |
| expect(formatted.trim(), contains('\n')); | |
| } | |
| Future<void> test_lineLength_analysisOptions_overridesConfig() async { | |
| const codeContent = codeThatWrapsAt40; | |
| const optionsContent = ''' | |
| formatter: | |
| page_width: 40 | |
| '''; | |
| newFile(analysisOptionsPath, optionsContent); | |
| newFile(mainFilePath, codeContent); | |
| await provideConfig(initialize, { | |
| // This won't apply because analysis_options wins. | |
| 'lineLength': 100, | |
| }); | |
| // Ignore trailing newlines when checking for wrapping. | |
| var formatted = await formatContents(mainFileUri, codeContent); | |
| expect(formatted.trim(), contains('\n')); | |
| } | |
| Future<void> test_lineLength_outsideWorkspaceFolders() async { | |
| var code = TestCode.parseNormalized(''' | |
| void f() { | |
| print('123456789 ''123456789 ''123456789 '); | |
| } | |
| '''); | |
| const expectedContents = ''' | |
| void f() { | |
| print( | |
| '123456789 ' | |
| '123456789 ' | |
| '123456789 ', | |
| ); | |
| } | |
| '''; | |
| await provideConfig( | |
| () => initialize( | |
| // Use empty roots so the test file is not inside any known | |
| // WorkspaceFolder. | |
| allowEmptyRootUri: true, | |
| ), | |
| // Global config (this should be used). | |
| {'lineLength': 10}, | |
| ); | |
| await openFile(mainFileUri, code.code); | |
| await expectFormattedContents(mainFileUri, code.code, expectedContents); | |
| } | |
| Future<void> test_lineLength_workspaceFolderSpecified() async { | |
| var code = TestCode.parseNormalized(''' | |
| void f() { | |
| print('123456789 ''123456789 ''123456789 '); | |
| } | |
| '''); | |
| const expectedContents = ''' | |
| void f() { | |
| print( | |
| '123456789 ' | |
| '123456789 ' | |
| '123456789 ', | |
| ); | |
| } | |
| '''; | |
| await provideConfig( | |
| initialize, | |
| // Global config. | |
| {'lineLength': 200}, | |
| folderConfig: { | |
| // WorkspaceFolder config for this project (this should be used). | |
| projectFolderPath: {'lineLength': 10}, | |
| }, | |
| ); | |
| await openFile(mainFileUri, code.code); | |
| await expectFormattedContents(mainFileUri, code.code, expectedContents); | |
| } | |
| Future<void> test_lineLength_workspaceFolderUnspecified() async { | |
| var code = TestCode.parseNormalized(''' | |
| void f() { | |
| print('123456789 ''123456789 ''123456789 '); | |
| } | |
| '''); | |
| const expectedContents = ''' | |
| void f() { | |
| print( | |
| '123456789 ' | |
| '123456789 ' | |
| '123456789 ', | |
| ); | |
| } | |
| '''; | |
| await provideConfig( | |
| initialize, | |
| // Global config (this should be used). | |
| {'lineLength': 10}, | |
| folderConfig: { | |
| // WorkspaceFolder config for this project that doesn't specific | |
| // lineLength. | |
| projectFolderPath: {'someOtherValue': 'foo'}, | |
| }, | |
| ); | |
| await openFile(mainFileUri, code.code); | |
| await expectFormattedContents(mainFileUri, code.code, expectedContents); | |
| } | |
| Future<void> test_minimalEdits_addWhitespace() async { | |
| // Check we only get one edit to add the required whitespace and not | |
| // an entire document replacement. | |
| var code = TestCode.parseNormalized(''' | |
| void f(){} | |
| '''); | |
| const expected = ''' | |
| void f() {} | |
| '''; | |
| await initialize(); | |
| await openFile(mainFileUri, code.code); | |
| var formatEdits = await expectFormattedContents( | |
| mainFileUri, | |
| code.code, | |
| expected, | |
| ); | |
| expect(formatEdits, hasLength(1)); | |
| expect(formatEdits[0].newText, ' '); | |
| expect(formatEdits[0].range.start, equals(Position(line: 0, character: 8))); | |
| } | |
| Future<void> test_minimalEdits_removeFileLeadingWhitespace() async { | |
| // Check whitespace before the first token is handled. | |
| var code = TestCode.parseNormalized(''' | |
| void f() {} | |
| '''); | |
| const expected = ''' | |
| void f() {} | |
| '''; | |
| await initialize(); | |
| await openFile(mainFileUri, code.code); | |
| var formatEdits = await expectFormattedContents( | |
| mainFileUri, | |
| code.code, | |
| expected, | |
| ); | |
| expect(formatEdits, hasLength(1)); | |
| expect(formatEdits[0].newText, ''); | |
| expect(formatEdits[0].range.start, equals(Position(line: 0, character: 0))); | |
| expect(formatEdits[0].range.end, equals(Position(line: 3, character: 0))); | |
| } | |
| Future<void> test_minimalEdits_removeFileTrailingWhitespace() async { | |
| // Check whitespace after the last token is handled. | |
| var code = TestCode.parseNormalized(''' | |
| void f() {} | |
| '''); | |
| const expected = ''' | |
| void f() {} | |
| '''; | |
| await initialize(); | |
| await openFile(mainFileUri, code.code); | |
| var formatEdits = await expectFormattedContents( | |
| mainFileUri, | |
| code.code, | |
| expected, | |
| ); | |
| expect(formatEdits, hasLength(1)); | |
| expect(formatEdits[0].newText, ''); | |
| expect(formatEdits[0].range.start, equals(Position(line: 1, character: 0))); | |
| expect(formatEdits[0].range.end, equals(Position(line: 5, character: 0))); | |
| } | |
| Future<void> test_minimalEdits_removePartialWhitespaceAfter() async { | |
| // Check we get an edit only to remove the unnecessary trailing whitespace | |
| // and not to replace the whole whitespace with a single space. | |
| var code = TestCode.parseNormalized(''' | |
| void f() {} | |
| '''); | |
| const expected = ''' | |
| void f() {} | |
| '''; | |
| await initialize(); | |
| await openFile(mainFileUri, code.code); | |
| var formatEdits = await expectFormattedContents( | |
| mainFileUri, | |
| code.code, | |
| expected, | |
| ); | |
| expect(formatEdits, hasLength(1)); | |
| expect( | |
| formatEdits[0], | |
| equals( | |
| TextEdit( | |
| range: Range( | |
| start: Position(line: 0, character: 9), | |
| end: Position(line: 0, character: 15), | |
| ), | |
| newText: '', | |
| ), | |
| ), | |
| ); | |
| } | |
| Future<void> test_minimalEdits_removePartialWhitespaceBefore() async { | |
| // Check we get an edit only to remove the unnecessary leading whitespace | |
| // and not to replace the whole whitespace with a single space. | |
| var code = TestCode.parseNormalized(''' | |
| void f() | |
| {} | |
| '''); | |
| const expected = ''' | |
| void f() {} | |
| '''; | |
| await initialize(); | |
| await openFile(mainFileUri, code.code); | |
| var formatEdits = await expectFormattedContents( | |
| mainFileUri, | |
| code.code, | |
| expected, | |
| ); | |
| expect(formatEdits, hasLength(1)); | |
| expect( | |
| formatEdits[0], | |
| equals( | |
| TextEdit( | |
| range: Range( | |
| start: Position(line: 0, character: 8), | |
| end: Position(line: 3, character: 0), | |
| ), | |
| newText: '', | |
| ), | |
| ), | |
| ); | |
| } | |
| Future<void> test_minimalEdits_removeWhitespace() async { | |
| // Check we only get two edits to remove the unwanted whitespace and not | |
| // an entire document replacement. | |
| var code = TestCode.parseNormalized(''' | |
| void f( ) { } | |
| '''); | |
| const expected = ''' | |
| void f() {} | |
| '''; | |
| await initialize(); | |
| await openFile(mainFileUri, code.code); | |
| var formatEdits = await expectFormattedContents( | |
| mainFileUri, | |
| code.code, | |
| expected, | |
| ); | |
| expect(formatEdits, hasLength(2)); | |
| expect(formatEdits[0].newText, isEmpty); | |
| expect(formatEdits[0].range.start, equals(Position(line: 0, character: 7))); | |
| expect(formatEdits[1].newText, isEmpty); | |
| expect( | |
| formatEdits[1].range.start, | |
| equals(Position(line: 0, character: 11)), | |
| ); | |
| } | |
| Future<void> test_minimalEdits_withComments() async { | |
| // Check we can get edits that span a comment (which does not appear in the | |
| // main token list). | |
| var code = TestCode.parseNormalized(''' | |
| void f() { | |
| var a = 1; | |
| // Comment | |
| print(a); | |
| } | |
| '''); | |
| const expected = ''' | |
| void f() { | |
| var a = 1; | |
| // Comment | |
| print(a); | |
| } | |
| '''; | |
| await initialize(); | |
| await openFile(mainFileUri, code.code); | |
| var formatEdits = await expectFormattedContents( | |
| mainFileUri, | |
| code.code, | |
| expected, | |
| ); | |
| expect(formatEdits, hasLength(3)); | |
| expect( | |
| formatEdits[0], | |
| equals( | |
| TextEdit( | |
| range: Range( | |
| start: Position(line: 1, character: 2), | |
| end: Position(line: 1, character: 8), | |
| ), | |
| newText: '', | |
| ), | |
| ), | |
| ); | |
| expect( | |
| formatEdits[1], | |
| equals( | |
| TextEdit( | |
| range: Range( | |
| start: Position(line: 2, character: 2), | |
| end: Position(line: 2, character: 8), | |
| ), | |
| newText: '', | |
| ), | |
| ), | |
| ); | |
| expect( | |
| formatEdits[2], | |
| equals( | |
| TextEdit( | |
| range: Range( | |
| start: Position(line: 3, character: 2), | |
| end: Position(line: 3, character: 8), | |
| ), | |
| newText: '', | |
| ), | |
| ), | |
| ); | |
| } | |
| Future<void> test_nonDartFile() async { | |
| await initialize(); | |
| await openFile(pubspecFileUri, simplePubspecContent); | |
| var formatEdits = await formatOnType(pubspecFileUri, startOfDocPos, '}'); | |
| expect(formatEdits, isNull); | |
| } | |
| Future<void> test_path_doesNotExist() async { | |
| await initialize(); | |
| await expectLater( | |
| formatDocument(toUri(join(projectFolderPath, 'missing.dart'))), | |
| throwsA( | |
| isResponseError( | |
| ServerErrorCodes.invalidFilePath, | |
| message: 'File does not exist', | |
| ), | |
| ), | |
| ); | |
| } | |
| Future<void> test_path_invalidFormat() async { | |
| await initialize(); | |
| await expectLater( | |
| formatDocument( | |
| // Add some invalid path characters to the end of a valid file:// URI. | |
| Uri.parse(mainFileUri.toString() + r'###***\\\///:::.dart'), | |
| ), | |
| throwsA( | |
| isResponseError( | |
| ServerErrorCodes.invalidFilePath, | |
| message: 'URI does not contain a valid file path', | |
| ), | |
| ), | |
| ); | |
| } | |
| Future<void> test_path_notFileScheme() async { | |
| await initialize(); | |
| await expectLater( | |
| formatDocument(Uri.parse('a:/a.dart')), | |
| throwsA( | |
| isResponseError( | |
| ServerErrorCodes.invalidFilePath, | |
| message: | |
| "URI scheme 'a' is not supported. Allowed schemes are 'file'.", | |
| ), | |
| ), | |
| ); | |
| } | |
| Future<void> test_simple() async { | |
| var code = TestCode.parseNormalized(''' | |
| void f () | |
| { | |
| print('test'); | |
| } | |
| '''); | |
| var expected = ''' | |
| void f() { | |
| print('test'); | |
| } | |
| '''; | |
| await initialize(); | |
| await openFile(mainFileUri, code.code); | |
| await expectFormattedContents(mainFileUri, code.code, expected); | |
| } | |
| Future<void> test_trailingCommas_automate() async { | |
| const optionsContent = ''' | |
| formatter: | |
| trailing_commas: automate | |
| '''; | |
| const initialContent = ''' | |
| enum A { | |
| a, | |
| b, | |
| } | |
| '''; | |
| const expectedContent = ''' | |
| enum A { a, b } | |
| '''; | |
| newFile(analysisOptionsPath, optionsContent); | |
| newFile(mainFilePath, initialContent); | |
| await initialize(); | |
| // Ignore trailing newlines when checking for wrapping. | |
| var formatted = await formatContents(mainFileUri, initialContent); | |
| expect(formatted.trim(), expectedContent.trim()); | |
| } | |
| Future<void> test_trailingCommas_preserve() async { | |
| var optionsContent = normalizeNewlinesForPlatform(''' | |
| formatter: | |
| trailing_commas: preserve | |
| '''); | |
| var initialContent = normalizeNewlinesForPlatform(''' | |
| enum A { a, b, } | |
| '''); | |
| var expectedContent = normalizeNewlinesForPlatform(''' | |
| enum A { | |
| a, | |
| b, | |
| } | |
| '''); | |
| newFile(analysisOptionsPath, optionsContent); | |
| newFile(mainFilePath, initialContent); | |
| await initialize(); | |
| // Ignore trailing newlines when checking for wrapping. | |
| var formatted = await formatContents(mainFileUri, initialContent); | |
| expect(formatted.trim(), expectedContent.trim()); | |
| } | |
| Future<void> test_trailingCommas_unspecified() async { | |
| const initialContent = ''' | |
| enum A { | |
| a, | |
| b, | |
| } | |
| '''; | |
| const expectedContent = ''' | |
| enum A { a, b } | |
| '''; | |
| newFile(mainFilePath, initialContent); | |
| await initialize(); | |
| // Ignore trailing newlines when checking for wrapping. | |
| var formatted = await formatContents(mainFileUri, initialContent); | |
| expect(formatted.trim(), expectedContent.trim()); | |
| } | |
| Future<void> test_trailingComment_trailingWhitespace_issue63746() async { | |
| var code = TestCode.parseNormalized(''' | |
| const x = 42; // Watermelon /**/ | |
| const y = 42; // Cantaloupe /**/ | |
| '''); | |
| var expected = ''' | |
| const x = 42; // Watermelon | |
| const y = 42; // Cantaloupe | |
| '''; | |
| await initialize(); | |
| await openFile(mainFileUri, code.code); | |
| await expectFormattedContents(mainFileUri, code.code, expected); | |
| } | |
| Future<void> test_unopenFile() async { | |
| var code = TestCode.parseNormalized(''' | |
| void f () | |
| { | |
| print('test'); | |
| } | |
| '''); | |
| var expected = ''' | |
| void f() { | |
| print('test'); | |
| } | |
| '''; | |
| newFile(mainFilePath, code.code); | |
| await initialize(); | |
| await expectFormattedContents(mainFileUri, code.code, expected); | |
| } | |
| Future<void> test_validSyntax_withErrors() async { | |
| failTestOnErrorDiagnostic = false; | |
| // We should still be able to format syntactically valid code even if it has | |
| // analysis errors. | |
| var code = TestCode.parseNormalized(''' | |
| void f() { | |
| print(unresolved); | |
| } | |
| '''); | |
| const expected = ''' | |
| void f() { | |
| print(unresolved); | |
| } | |
| '''; | |
| await initialize(); | |
| await openFile(mainFileUri, code.code); | |
| await expectFormattedContents(mainFileUri, code.code, expected); | |
| } | |
| } |