| // 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 'dart:async'; | |
| import 'package:analysis_server/lsp_protocol/protocol.dart'; | |
| import 'package:analysis_server/src/lsp/constants.dart'; | |
| import 'package:analysis_server/src/lsp/server_capabilities_computer.dart'; | |
| import 'package:analysis_server/src/plugin/plugin_isolate.dart'; | |
| import 'package:analysis_server/src/scheduler/message_scheduler.dart'; | |
| import 'package:analyzer/file_system/memory_file_system.dart'; | |
| import 'package:analyzer/src/util/file_paths.dart' as file_paths; | |
| import 'package:language_server_protocol/json_parsing.dart'; | |
| import 'package:test/test.dart'; | |
| import 'package:test_reflective_loader/test_reflective_loader.dart'; | |
| import '../utils/lsp_protocol_extensions.dart'; | |
| import 'change_workspace_folders_test.dart'; | |
| import 'request_helpers_mixin.dart'; | |
| import 'server_abstract.dart'; | |
| void main() { | |
| defineReflectiveSuite(() { | |
| defineReflectiveTests(InitializationTest); | |
| defineReflectiveTests(StringIdInitializationTest); | |
| defineReflectiveTests(SlowInitializationTest); | |
| }); | |
| } | |
| @reflectiveTest | |
| class InitializationTest extends AbstractLspAnalysisServerTest { | |
| /// The default value of [MessageScheduler.allowOverlappingHandlers] before | |
| /// and test sets it (so that [tearDown] can revert it). | |
| final allowOverlappingHandlersDefault = | |
| MessageScheduler.allowOverlappingHandlers; | |
| var isAnalysisOptionsSelector = TypeMatcher<TextDocumentFilterScheme>() | |
| .having( | |
| (selector) => selector.pattern.asString, | |
| 'pattern', | |
| '**/analysis_options.yaml', | |
| ); | |
| var isDartSelector = TypeMatcher<TextDocumentFilterScheme>().having( | |
| (selector) => selector.language, | |
| 'language', | |
| 'dart', | |
| ); | |
| var isPubspecSelector = TypeMatcher<TextDocumentFilterScheme>().having( | |
| (selector) => selector.pattern.asString, | |
| 'pattern', | |
| '**/pubspec.yaml', | |
| ); | |
| /// Waits for any in-progress analysis context rebuild. | |
| /// | |
| /// Pumps the event queue before and after, to ensure any server code that | |
| /// runs after the rebuild has had chance to run. | |
| Future<void> get contextRebuildComplete async { | |
| await pumpEventQueue(times: 5000); | |
| await server.analysisContextsRebuilt; | |
| await pumpEventQueue(times: 5000); | |
| } | |
| Future<void> assertDynamicRegistration( | |
| String name, | |
| Set<Method> expectedResult, | |
| ) async { | |
| setTextDocumentDynamicRegistration(name); | |
| setWorkspaceDynamicRegistration(name); | |
| // Check that when the server calls client/registerCapability it only includes | |
| // the items we advertised dynamic registration support for. | |
| var registrations = <Registration>[]; | |
| await monitorDynamicRegistrations(registrations, initialize); | |
| var registeredMethods = registrations | |
| .map((registration) => registration.method) | |
| .toSet(); | |
| var result = expectedResult.map((method) => method.toJson()).toSet(); | |
| expect(registeredMethods, equals(result)); | |
| } | |
| /// Expects successful initialization, but a window/showMessage warning | |
| /// [expectedMessage] for the given [experimentalCapabilities]. | |
| Future<void> expectInvalidExperimentalParams( | |
| Map<String, Object?> experimentalCapabilities, | |
| String expectedMessage, | |
| ) async { | |
| var warning = showMessageNotifications.firstWhere( | |
| (m) => m.type == MessageType.Warning, | |
| ); | |
| await initialize(experimentalCapabilities: experimentalCapabilities); | |
| var message = (await warning).message; | |
| expect( | |
| message, | |
| allOf( | |
| contains('errors parsing your experimental client capabilities'), | |
| contains(expectedMessage), | |
| ), | |
| ); | |
| } | |
| TextDocumentRegistrationOptions registrationOptionsFor( | |
| List<Registration> registrations, | |
| Method method, | |
| ) { | |
| var options = registrationFor(registrations, method)?.registerOptions; | |
| if (options == null) { | |
| throw 'Registration options for $method were not found. ' | |
| 'Perhaps dynamicRegistration is missing from ' | |
| 'setAllSupportedTextDocumentDynamicRegistrations?'; | |
| } | |
| return TextDocumentRegistrationOptions.fromJson( | |
| options as Map<String, Object?>, | |
| ); | |
| } | |
| @override | |
| Future<void> tearDown() async { | |
| await super.tearDown(); | |
| MessageScheduler.allowOverlappingHandlers = allowOverlappingHandlersDefault; | |
| } | |
| Future<void> test_blazeWorkspace() async { | |
| var workspacePath = '/home/user/ws'; | |
| // Make it a Blaze workspace. | |
| newFile('$workspacePath/${file_paths.blazeWorkspaceMarker}', ''); | |
| var packagePath = '$workspacePath/team/project1'; | |
| // Make it a Blaze project. | |
| newFile(convertPath('$packagePath/BUILD'), ''); | |
| var file1 = convertPath('$packagePath/lib/file1.dart'); | |
| newFile(file1, ''); | |
| await initialize(allowEmptyRootUri: true); | |
| // Expect that context manager includes a whole package. | |
| await openFile(pathContext.toUri(file1), ''); | |
| expect( | |
| server.contextManager.includedPaths, | |
| equals([convertPath(packagePath)]), | |
| ); | |
| } | |
| Future<void> test_completionRegistrations_triggerCharacters() async { | |
| // Support dynamic registration for everything we support. | |
| setAllSupportedTextDocumentDynamicRegistrations(); | |
| setAllSupportedWorkspaceDynamicRegistrations(); | |
| var registrations = <Registration>[]; | |
| var initResponse = await monitorDynamicRegistrations( | |
| registrations, | |
| () => initialize(), | |
| ); | |
| var initResult = InitializeResult.fromJson( | |
| initResponse.result as Map<String, Object?>, | |
| ); | |
| expect(initResult.capabilities, isNotNull); | |
| // Check Dart-only registration. | |
| var dartRegistration = registrationForDart( | |
| registrations, | |
| Method.textDocument_completion, | |
| ); | |
| var dartOptions = CompletionRegistrationOptions.fromJson( | |
| dartRegistration.registerOptions as Map<String, Object?>, | |
| ); | |
| expect(dartOptions.documentSelector, hasLength(1)); | |
| expect(dartOptions.documentSelector![0].language, dartLanguageId); | |
| expect(dartOptions.triggerCharacters, isNotEmpty); | |
| // Check non-Dart registration. | |
| var nonDartRegistration = registrations.singleWhere( | |
| (r) => | |
| r.method == Method.textDocument_completion.toJson() && | |
| r != dartRegistration, | |
| ); | |
| var nonDartOptions = CompletionRegistrationOptions.fromJson( | |
| nonDartRegistration.registerOptions as Map<String, Object?>, | |
| ); | |
| var otherLanguages = nonDartOptions.documentSelector! | |
| .map((selector) => selector.language) | |
| .toList(); | |
| expect(otherLanguages, isNot(contains('dart'))); | |
| expect(nonDartOptions.triggerCharacters, isNull); | |
| } | |
| Future<void> test_completionRegistrations_withDartPlugin() async { | |
| // This tests for a bug that occurred with an analysis server plugin | |
| // that works on Dart files. When computing completion registrations we | |
| // usually have separate registrations for Dart + non-Dart to account for | |
| // different trigger characters. However, the plugin types were all being | |
| // included in the non-Dart registration even if they included Dart. | |
| // | |
| // The result was two registrations including Dart, which caused duplicate | |
| // requests for Dart completions, which resulted in duplicate items | |
| // appearing in the editor. | |
| setAllSupportedTextDocumentDynamicRegistrations(); | |
| // Track all current registrations. | |
| var registrations = <Registration>[]; | |
| // Perform normal registration (without plugins) to get the initial set. | |
| await monitorDynamicRegistrations(registrations, initialize); | |
| // Expect only a single registration that includes Dart files. | |
| expect( | |
| registrationsForDart(registrations, Method.textDocument_completion), | |
| hasLength(1), | |
| ); | |
| // Monitor the unregistration/new registrations during the plugin activation. | |
| await monitorDynamicReregistration(registrations, () async { | |
| var plugin = configureTestPlugin(); | |
| plugin.currentSession = PluginSession(plugin) | |
| ..interestingFileGlobs = ['*.dart']; | |
| pluginManager.pluginsChangedController.add(null); | |
| }); | |
| // Expect that there is still only a single registration for Dart. | |
| expect( | |
| registrationsForDart(registrations, Method.textDocument_completion), | |
| hasLength(1), | |
| ); | |
| } | |
| Future<void> test_dynamicRegistration_areNotInterleaved() async { | |
| // Some of the issues in https://github.com/dart-lang/sdk/issues/47851 | |
| // (duplicate hovers/code actions/etc.) were caused by duplicate | |
| // registrations. This happened when we tried to rebuild registrations | |
| // concurrently as the code would: | |
| // | |
| // 1. compute the new set of registrations | |
| // 2. compute which had changed | |
| // 3. send _and await_ the unregistration | |
| // 4. send _and await_ the new registration | |
| // | |
| // If the code was triggered multiple times concurrently, it was possible | |
| // for step 3 in second request to occur before step 4 in the first, which | |
| // would result in unregistrations being sent for registrations that had | |
| // not been sent. They would be silently dropped, and then we'd end up with | |
| // multiple registrations. | |
| // | |
| // This test triggers the rebuild via pluginManager.pluginsChangedController | |
| // multiple times and ensures the events all arrive in the correct order, | |
| // that is, no unregistration ever contains the ID of something that is not | |
| // currently registered. | |
| setAllSupportedTextDocumentDynamicRegistrations(); | |
| var registrations = <Registration>[]; | |
| await monitorDynamicRegistrations(registrations, initialize); | |
| var knownRegistrationsIds = registrations | |
| .map((registration) => registration.id) | |
| .toSet(); | |
| var numberOfReregistrations = 10; | |
| // Listen to incoming registrations, ensure they're not in the set, and add | |
| // them. | |
| var registrationsDone = requestsFromServer | |
| .where((n) => n.method == Method.client_registerCapability) | |
| .take(numberOfReregistrations) | |
| .listen((request) { | |
| respondTo(request, null); | |
| var registrations = RegistrationParams.fromJson( | |
| request.params as Map<String, Object?>, | |
| ).registrations; | |
| for (var registration in registrations) { | |
| var id = registration.id; | |
| if (!knownRegistrationsIds.add(id)) { | |
| throw 'Registration $id was already in the existing set!'; | |
| } | |
| } | |
| }) | |
| .asFuture(); | |
| // Listen to incoming unregistrations, verify they're in the set, and remove | |
| // them. | |
| var unregistrationsDone = requestsFromServer | |
| .where((n) => n.method == Method.client_unregisterCapability) | |
| .take(numberOfReregistrations) | |
| .listen((request) { | |
| respondTo(request, null); | |
| var unregistrations = UnregistrationParams.fromJson( | |
| request.params as Map<String, Object?>, | |
| ).unregisterations; | |
| for (var unregistration in unregistrations) { | |
| var id = unregistration.id; | |
| if (!knownRegistrationsIds.remove(id)) { | |
| throw 'Registration $id was not in the existing set!'; | |
| } | |
| } | |
| }) | |
| .asFuture(); | |
| // Trigger multiple plugin events that will rebuild the registrations. | |
| for (var i = 0; i < numberOfReregistrations; i++) { | |
| var plugin = configureTestPlugin(); | |
| plugin.currentSession = PluginSession(plugin); | |
| // Ensure they have different file types so the registrations change, | |
| // otherwise they will be optimised out as not changing. | |
| plugin.currentSession!.interestingFileGlobs = ['*.foo$i']; | |
| pluginManager.pluginsChangedController.add(null); | |
| await null; // Allow the server to begin processing the change. | |
| } | |
| // Wait for both streams to have handled all of the numberOfReregistrations | |
| // expected events (without throwing). | |
| await Future.wait([registrationsDone, unregistrationsDone]); | |
| } | |
| Future<void> test_dynamicRegistration_config_allHierarchy() => | |
| assertDynamicRegistration('callHierarchy', { | |
| Method.textDocument_prepareCallHierarchy, | |
| }); | |
| Future<void> test_dynamicRegistration_config_codeAction() => | |
| assertDynamicRegistration('codeAction', {Method.textDocument_codeAction}); | |
| Future<void> test_dynamicRegistration_config_codeLens() => | |
| assertDynamicRegistration('codeLens', {Method.textDocument_codeLens}); | |
| Future<void> test_dynamicRegistration_config_colorProvider() => | |
| assertDynamicRegistration('colorProvider', { | |
| Method.textDocument_documentColor, | |
| }); | |
| Future<void> test_dynamicRegistration_config_completion() => | |
| assertDynamicRegistration('completion', {Method.textDocument_completion}); | |
| Future<void> test_dynamicRegistration_config_definition() => | |
| assertDynamicRegistration('definition', {Method.textDocument_definition}); | |
| Future<void> test_dynamicRegistration_config_didChangeConfiguration() => | |
| assertDynamicRegistration('didChangeConfiguration', { | |
| Method.workspace_didChangeConfiguration, | |
| }); | |
| Future<void> test_dynamicRegistration_config_documentHighlight() => | |
| assertDynamicRegistration('documentHighlight', { | |
| Method.textDocument_documentHighlight, | |
| }); | |
| Future<void> test_dynamicRegistration_config_documentLink() => | |
| assertDynamicRegistration('documentLink', { | |
| Method.textDocument_documentLink, | |
| }); | |
| Future<void> test_dynamicRegistration_config_documentSymbol() => | |
| assertDynamicRegistration('documentSymbol', { | |
| Method.textDocument_documentSymbol, | |
| }); | |
| Future<void> test_dynamicRegistration_config_fileOperations() => | |
| assertDynamicRegistration('fileOperations', { | |
| Method.workspace_willRenameFiles, | |
| }); | |
| Future<void> test_dynamicRegistration_config_foldingRange() => | |
| assertDynamicRegistration('foldingRange', { | |
| Method.textDocument_foldingRange, | |
| }); | |
| Future<void> test_dynamicRegistration_config_formatting() => | |
| assertDynamicRegistration('formatting', {Method.textDocument_formatting}); | |
| Future<void> test_dynamicRegistration_config_hover() => | |
| assertDynamicRegistration('hover', {Method.textDocument_hover}); | |
| Future<void> test_dynamicRegistration_config_implementation() => | |
| assertDynamicRegistration('implementation', { | |
| Method.textDocument_implementation, | |
| }); | |
| Future<void> test_dynamicRegistration_config_inlayHint() => | |
| assertDynamicRegistration('inlayHint', {Method.textDocument_inlayHint}); | |
| Future<void> test_dynamicRegistration_config_onTypeFormatting() => | |
| assertDynamicRegistration('onTypeFormatting', { | |
| Method.textDocument_onTypeFormatting, | |
| }); | |
| Future<void> test_dynamicRegistration_config_rangeFormatting() => | |
| assertDynamicRegistration('rangeFormatting', { | |
| Method.textDocument_rangeFormatting, | |
| }); | |
| Future<void> test_dynamicRegistration_config_references() => | |
| assertDynamicRegistration('references', {Method.textDocument_references}); | |
| Future<void> test_dynamicRegistration_config_rename() => | |
| assertDynamicRegistration('rename', {Method.textDocument_rename}); | |
| Future<void> test_dynamicRegistration_config_selectionRange() => | |
| assertDynamicRegistration('selectionRange', { | |
| Method.textDocument_selectionRange, | |
| }); | |
| Future<void> test_dynamicRegistration_config_semanticTokens() => | |
| assertDynamicRegistration('semanticTokens', { | |
| CustomMethods.semanticTokenDynamicRegistration, | |
| }); | |
| Future<void> test_dynamicRegistration_config_signatureHelp() => | |
| assertDynamicRegistration('signatureHelp', { | |
| Method.textDocument_signatureHelp, | |
| }); | |
| Future<void> test_dynamicRegistration_config_synchronization() => | |
| assertDynamicRegistration('synchronization', { | |
| Method.textDocument_didOpen, | |
| Method.textDocument_didChange, | |
| Method.textDocument_didClose, | |
| }); | |
| Future<void> test_dynamicRegistration_config_typeDefinition() => | |
| assertDynamicRegistration('typeDefinition', { | |
| Method.textDocument_typeDefinition, | |
| }); | |
| Future<void> test_dynamicRegistration_config_typeHierarchy() => | |
| assertDynamicRegistration('typeHierarchy', { | |
| Method.textDocument_prepareTypeHierarchy, | |
| }); | |
| Future<void> test_dynamicRegistration_containsAppropriateSettings() async { | |
| // Support file operations. | |
| setFileOperationDynamicRegistration(); | |
| // Support dynamic registration for text sync, hovers, and code actions. | |
| setTextSyncDynamicRegistration(); | |
| setHoverDynamicRegistration(); | |
| setTextDocumentDynamicRegistration('codeAction'); | |
| // Basic check that the server responds with the capabilities we'd expect, | |
| // for ex including analysis_options.yaml in text synchronization (and code | |
| // actions) but not for hovers. | |
| var registrations = <Registration>[]; | |
| var initResponse = await monitorDynamicRegistrations( | |
| registrations, | |
| initialize, | |
| ); | |
| // Because we support dynamic registration for synchronization, we won't send | |
| // static registrations for them. | |
| // https://github.com/dart-lang/sdk/issues/38490 | |
| var initResult = InitializeResult.fromJson( | |
| initResponse.result as Map<String, Object?>, | |
| ); | |
| expect(initResult.serverInfo!.name, 'Dart SDK LSP Analysis Server'); | |
| expect(initResult.serverInfo!.version, isNotNull); | |
| expect(initResult.capabilities, isNotNull); | |
| expect(initResult.capabilities.textDocumentSync, isNull); | |
| // Should contain Hover, DidOpen, DidClose, DidChange, CodeAction, | |
| // WillRenameFiles. | |
| expect(registrations, hasLength(6)); | |
| var hover = registrationOptionsFor( | |
| registrations, | |
| Method.textDocument_hover, | |
| ); | |
| var change = registrationOptionsFor( | |
| registrations, | |
| Method.textDocument_didChange, | |
| ); | |
| var codeAction = registrationOptionsFor( | |
| registrations, | |
| Method.textDocument_codeAction, | |
| ); | |
| var rename = FileOperationRegistrationOptions.fromJson( | |
| registrationFor( | |
| registrations, | |
| Method.workspace_willRenameFiles, | |
| )?.registerOptions | |
| as Map<String, Object?>, | |
| ); | |
| expect( | |
| registrationOptionsFor(registrations, Method.textDocument_didOpen), | |
| isNotNull, | |
| ); | |
| expect( | |
| registrationOptionsFor(registrations, Method.textDocument_didClose), | |
| isNotNull, | |
| ); | |
| // The hover capability should only specific Dart. | |
| expect(hover, isNotNull); | |
| expect(hover.documentSelector, hasLength(1)); | |
| expect(hover.documentSelector!.single, isDartSelector); | |
| // didChange should also include pubspec + analysis_options. | |
| expect(change, isNotNull); | |
| expect(change.documentSelector, hasLength(greaterThanOrEqualTo(3))); | |
| expect( | |
| change.documentSelector, | |
| containsAll([ | |
| isDartSelector, | |
| isPubspecSelector, | |
| isAnalysisOptionsSelector, | |
| ]), | |
| ); | |
| // codeAction should also include pubspec + analysis_options. | |
| expect(codeAction, isNotNull); | |
| expect(codeAction.documentSelector, hasLength(greaterThanOrEqualTo(3))); | |
| expect( | |
| change.documentSelector, | |
| containsAll([ | |
| isDartSelector, | |
| isPubspecSelector, | |
| isAnalysisOptionsSelector, | |
| ]), | |
| ); | |
| expect(rename, equals(fileOperationRegistrationOptions)); | |
| } | |
| Future<void> test_dynamicRegistration_notSupportedByClient() async { | |
| // If the client doesn't send any dynamicRegistration settings then there | |
| // should be no `client/registerCapability` calls. | |
| // Set a flag if any registerCapability request comes through. | |
| var didGetRegisterCapabilityRequest = false; | |
| requestsFromServer | |
| .where((n) => n.method == Method.client_registerCapability) | |
| .listen((_) => didGetRegisterCapabilityRequest = true); | |
| // Initialize with no dynamic registrations advertised. | |
| var initResponse = await initialize(); | |
| await pumpEventQueue(); | |
| var initResult = InitializeResult.fromJson( | |
| initResponse.result as Map<String, Object?>, | |
| ); | |
| expect(initResult.capabilities, isNotNull); | |
| // When dynamic registration is not supported, we will always statically | |
| // request text document open/close and incremental updates. | |
| expect(initResult.capabilities.textDocumentSync, isNotNull); | |
| initResult.capabilities.textDocumentSync!.map( | |
| (_) => throw 'Expected textDocumentSync capabilities to be a TextDocumentSyncOptions', | |
| (options) { | |
| expect(options.openClose, isTrue); | |
| expect(options.change, equals(TextDocumentSyncKind.Incremental)); | |
| }, | |
| ); | |
| expect(initResult.capabilities.callHierarchyProvider, isNotNull); | |
| expect(initResult.capabilities.completionProvider, isNotNull); | |
| expect(initResult.capabilities.hoverProvider, isNotNull); | |
| expect(initResult.capabilities.signatureHelpProvider, isNotNull); | |
| expect(initResult.capabilities.referencesProvider, isNotNull); | |
| expect(initResult.capabilities.colorProvider, isNotNull); | |
| expect(initResult.capabilities.documentHighlightProvider, isNotNull); | |
| expect(initResult.capabilities.documentFormattingProvider, isNotNull); | |
| expect(initResult.capabilities.documentOnTypeFormattingProvider, isNotNull); | |
| expect(initResult.capabilities.documentRangeFormattingProvider, isNotNull); | |
| expect(initResult.capabilities.definitionProvider, isNotNull); | |
| expect(initResult.capabilities.codeActionProvider, isNotNull); | |
| expect(initResult.capabilities.renameProvider, isNotNull); | |
| expect(initResult.capabilities.foldingRangeProvider, isNotNull); | |
| expect( | |
| initResult.capabilities.workspace!.fileOperations!.willRename, | |
| equals(fileOperationRegistrationOptions), | |
| ); | |
| expect(initResult.capabilities.selectionRangeProvider, isNotNull); | |
| expect(initResult.capabilities.semanticTokensProvider, isNotNull); | |
| expect(didGetRegisterCapabilityRequest, isFalse); | |
| } | |
| Future<void> test_dynamicRegistration_suppressesStaticRegistration() async { | |
| // Support dynamic registration for everything we support. | |
| setAllSupportedTextDocumentDynamicRegistrations(); | |
| setAllSupportedWorkspaceDynamicRegistrations(); | |
| // If the client sends dynamicRegistration settings then there | |
| // should not be static registrations for the same capabilities. | |
| var registrations = <Registration>[]; | |
| var initResponse = await monitorDynamicRegistrations( | |
| registrations, | |
| initialize, | |
| ); | |
| var initResult = InitializeResult.fromJson( | |
| initResponse.result as Map<String, Object?>, | |
| ); | |
| expect(initResult.capabilities, isNotNull); | |
| // Ensure no static registrations. This list should include all server equivalents | |
| // of the dynamic registrations listed in `ClientDynamicRegistrations.supported`. | |
| expect(initResult.capabilities.textDocumentSync, isNull); | |
| expect(initResult.capabilities.callHierarchyProvider, isNull); | |
| expect(initResult.capabilities.completionProvider, isNull); | |
| expect(initResult.capabilities.hoverProvider, isNull); | |
| expect(initResult.capabilities.inlayHintProvider, isNull); | |
| expect(initResult.capabilities.signatureHelpProvider, isNull); | |
| expect(initResult.capabilities.referencesProvider, isNull); | |
| expect(initResult.capabilities.colorProvider, isNull); | |
| expect(initResult.capabilities.documentHighlightProvider, isNull); | |
| expect(initResult.capabilities.documentFormattingProvider, isNull); | |
| expect(initResult.capabilities.documentOnTypeFormattingProvider, isNull); | |
| expect(initResult.capabilities.documentRangeFormattingProvider, isNull); | |
| expect(initResult.capabilities.definitionProvider, isNull); | |
| expect(initResult.capabilities.codeActionProvider, isNull); | |
| expect(initResult.capabilities.renameProvider, isNull); | |
| expect(initResult.capabilities.foldingRangeProvider, isNull); | |
| expect(initResult.capabilities.workspace!.fileOperations, isNull); | |
| expect(initResult.capabilities.selectionRangeProvider, isNull); | |
| expect(initResult.capabilities.semanticTokensProvider, isNull); | |
| // Ensure all expected dynamic registrations. | |
| for (var expectedRegistration in ClientDynamicRegistrations.supported) { | |
| // We have two completion registrations (to handle different trigger | |
| // characters), so exclude that here and check it manually below. | |
| if (expectedRegistration == Method.textDocument_completion) { | |
| continue; | |
| } | |
| var registration = registrationOptionsFor( | |
| registrations, | |
| expectedRegistration, | |
| ); | |
| expect( | |
| registration, | |
| isNotNull, | |
| reason: 'Missing dynamic registration for $expectedRegistration', | |
| ); | |
| } | |
| // Check the were two completion registrations. | |
| var completionRegistrations = registrations.where( | |
| (reg) => reg.method == Method.textDocument_completion.toJson(), | |
| ); | |
| expect(completionRegistrations, hasLength(2)); | |
| } | |
| Future<void> test_dynamicRegistration_unregistersOutdatedAfterChange() async { | |
| // Initialize by supporting dynamic registrations everywhere | |
| setAllSupportedTextDocumentDynamicRegistrations(); | |
| var registrations = <Registration>[]; | |
| await monitorDynamicRegistrations(registrations, initialize); | |
| var unregisterRequest = await expectRequest( | |
| Method.client_unregisterCapability, | |
| () { | |
| var plugin = configureTestPlugin(); | |
| plugin.currentSession = PluginSession(plugin) | |
| ..interestingFileGlobs = ['*.foo']; | |
| pluginManager.pluginsChangedController.add(null); | |
| }, | |
| ); | |
| var unregistrations = UnregistrationParams.fromJson( | |
| unregisterRequest.params as Map<String, Object?>, | |
| ).unregisterations; | |
| // folding method should have been unregistered as the server now supports | |
| // *.foo files for it as well. | |
| var registrationIdForFolding = registrations | |
| .singleWhere((r) => r.method == 'textDocument/foldingRange') | |
| .id; | |
| expect( | |
| unregistrations, | |
| contains( | |
| isA<Unregistration>() | |
| .having((r) => r.method, 'method', 'textDocument/foldingRange') | |
| .having((r) => r.id, 'id', registrationIdForFolding), | |
| ), | |
| ); | |
| } | |
| Future<void> test_dynamicRegistration_updatesWithPlugins() async { | |
| setTextDocumentDynamicRegistration('foldingRange'); | |
| await initialize(); | |
| // The server will send an unregister request followed by another register | |
| // request to change document filter on folding. We need to respond to the | |
| // unregister request as the server awaits that. | |
| // This is set up as a future callback and should not be awaited here. | |
| unawaited( | |
| requestsFromServer | |
| .firstWhere((r) => r.method == Method.client_unregisterCapability) | |
| .then((request) { | |
| respondTo(request, null); | |
| }), | |
| ); | |
| var request = await expectRequest(Method.client_registerCapability, () { | |
| var plugin = configureTestPlugin(); | |
| plugin.currentSession = PluginSession(plugin) | |
| ..interestingFileGlobs = ['*.sql']; | |
| pluginManager.pluginsChangedController.add(null); | |
| }); | |
| var registrations = RegistrationParams.fromJson( | |
| request.params as Map<String, Object?>, | |
| ).registrations; | |
| var documentFilterSql = TextDocumentFilterScheme( | |
| scheme: 'file', | |
| pattern: .t1('**/*.sql'), | |
| ); | |
| var documentFilterDart = TextDocumentFilterScheme( | |
| language: 'dart', | |
| scheme: 'file', | |
| ); | |
| expect( | |
| registrations, | |
| contains( | |
| isA<Registration>() | |
| .having((r) => r.method, 'method', 'textDocument/foldingRange') | |
| .having( | |
| (r) => TextDocumentRegistrationOptions.fromJson( | |
| r.registerOptions as Map<String, Object?>, | |
| ).documentSelector, | |
| 'registerOptions.documentSelector', | |
| containsAll([documentFilterSql, documentFilterDart]), | |
| ), | |
| ), | |
| ); | |
| } | |
| /// Tests that when there are no explicit analysis roots (instead they are | |
| /// implied by open files), requests for open files are successful even if | |
| /// sent _immediately_ after opening the file. | |
| /// | |
| /// https://github.com/Dart-Code/Dart-Code/issues/3929 | |
| /// | |
| /// This test uses getDocumentSymbols which requires a resolved result. | |
| Future<void> | |
| test_emptyAnalysisRoots_handlesFileRequestsImmediately_resolved() async { | |
| const content = 'void f() {}'; | |
| var file1 = join(projectFolderPath, 'file1.dart'); | |
| var file1Uri = pathContext.toUri(file1); | |
| newFile(file1, content); | |
| newPubspecYamlFile(projectFolderPath, ''); | |
| await initialize(allowEmptyRootUri: true); | |
| unawaited(openFile(file1Uri, content)); // Don't wait | |
| var result = await getDocumentSymbols(file1Uri); | |
| var symbols = result.map( | |
| (docSymbols) => docSymbols, | |
| (symbolInfos) => symbolInfos, | |
| ); | |
| expect(symbols, hasLength(1)); | |
| } | |
| /// Tests that when there are no explicit analysis roots (instead they are | |
| /// implied by open files), requests for open files are successful even if | |
| /// sent _immediately_ after opening the file. | |
| /// | |
| /// https://github.com/Dart-Code/Dart-Code/issues/3929 | |
| /// | |
| /// This test uses getSelectionRanges which requires only a parsed result. | |
| Future<void> | |
| test_emptyAnalysisRoots_handlesFileRequestsImmediately_unresolved() async { | |
| const content = 'void f() {}'; | |
| var file1 = join(projectFolderPath, 'file1.dart'); | |
| var file1Uri = pathContext.toUri(file1); | |
| newFile(file1, content); | |
| newPubspecYamlFile(projectFolderPath, ''); | |
| await initialize(allowEmptyRootUri: true); | |
| unawaited(openFile(file1Uri, content)); // Don't wait | |
| var result = await getSelectionRanges(file1Uri, startOfDocPos); | |
| expect(result, isNotNull); | |
| } | |
| Future<void> test_emptyAnalysisRoots_multipleOpenFiles() async { | |
| var file1 = join(projectFolderPath, 'file1.dart'); | |
| var file1Uri = pathContext.toUri(file1); | |
| newFile(file1, ''); | |
| var file2 = join(projectFolderPath, 'file2.dart'); | |
| var file2Uri = pathContext.toUri(file2); | |
| newFile(file2, ''); | |
| newPubspecYamlFile(projectFolderPath, ''); | |
| await initialize(allowEmptyRootUri: true); | |
| // Opening both files should only add the project folder once. | |
| await openFile(file1Uri, ''); | |
| await openFile(file2Uri, ''); | |
| await contextRebuildComplete; | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| // Closing only one of the files should not remove the project folder | |
| // since there are still open files. | |
| resetContextBuildCounter(); | |
| await closeFile(file1Uri); | |
| await pumpEventQueue(times: 5000); | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| expectNoContextBuilds(); | |
| // Closing the last file should remove the project folder and remove | |
| // the context. | |
| resetContextBuildCounter(); | |
| await closeFile(file2Uri); | |
| await contextRebuildComplete; | |
| expect(server.contextManager.includedPaths, equals([])); | |
| expect(server.contextManager.driverMap, hasLength(0)); | |
| expectContextBuilds(); | |
| } | |
| Future<void> test_emptyAnalysisRoots_projectWithoutPubspec() async { | |
| projectFolderPath = convertPath('/home/empty'); | |
| var nestedFilePath = join( | |
| projectFolderPath, | |
| 'nested', | |
| 'deeply', | |
| 'in', | |
| 'folders', | |
| 'test.dart', | |
| ); | |
| var nestedFileUri = pathContext.toUri(nestedFilePath); | |
| newFile(nestedFilePath, ''); | |
| // The project folder shouldn't be added to start with. | |
| await initialize(allowEmptyRootUri: true); | |
| expect(server.contextManager.includedPaths, equals([])); | |
| // Opening the file will add a root for it. | |
| await openFile(nestedFileUri, ''); | |
| expect(server.contextManager.includedPaths, equals([nestedFilePath])); | |
| } | |
| Future<void> test_emptyAnalysisRoots_projectWithPubspec() async { | |
| var nestedFilePath = join( | |
| projectFolderPath, | |
| 'nested', | |
| 'deeply', | |
| 'in', | |
| 'folders', | |
| 'test.dart', | |
| ); | |
| var nestedFileUri = pathContext.toUri(nestedFilePath); | |
| newFile(nestedFilePath, ''); | |
| newPubspecYamlFile(projectFolderPath, ''); | |
| // The project folder shouldn't be added to start with. | |
| await initialize(allowEmptyRootUri: true); | |
| expect(server.contextManager.includedPaths, equals([])); | |
| // Opening a file nested within the project should add the project folder. | |
| await openFile(nestedFileUri, ''); | |
| await contextRebuildComplete; | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| // Ensure the file was cached in each driver. This happens as a result of | |
| // adding to priority files, but if that's done before the file is in an | |
| // analysis root it will not occur. | |
| // https://github.com/dart-lang/sdk/issues/37338 | |
| for (var driver in server.driverMap.values) { | |
| expect(driver.getCachedResolvedUnit(nestedFilePath), isNotNull); | |
| } | |
| // Closing the file should remove it. | |
| await closeFile(nestedFileUri); | |
| expect(server.contextManager.includedPaths, equals([])); | |
| } | |
| Future<void> test_excludedFolders_absolute() async { | |
| var excludedFolderPath = join(projectFolderPath, 'excluded'); | |
| await provideConfig( | |
| initialize, | |
| // Exclude the folder with a relative path. | |
| { | |
| 'analysisExcludedFolders': [excludedFolderPath], | |
| }, | |
| ); | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| expect(server.contextManager.excludedPaths, equals([excludedFolderPath])); | |
| } | |
| Future<void> test_excludedFolders_nonList() async { | |
| var excludedFolderPath = join(projectFolderPath, 'excluded'); | |
| await provideConfig( | |
| initialize, | |
| // Include a single string instead of an array since it's an easy mistake | |
| // to make without editor validation of settings. | |
| {'analysisExcludedFolders': 'excluded'}, | |
| ); | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| expect(server.contextManager.excludedPaths, equals([excludedFolderPath])); | |
| } | |
| Future<void> test_excludedFolders_relative() async { | |
| var excludedFolderPath = join(projectFolderPath, 'aaa', 'bbb'); | |
| await provideConfig( | |
| initialize, | |
| // Exclude the folder with a relative path. | |
| { | |
| 'analysisExcludedFolders': ['aaa', 'bbb'].join(pathContext.separator), | |
| }, | |
| ); | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| expect(server.contextManager.excludedPaths, equals([excludedFolderPath])); | |
| } | |
| /// Check exclusions using explicit posix paths even if we're running | |
| /// on Windows. It's common for shared configs in VS Code to always use | |
| /// forward slashes in paths. | |
| Future<void> test_excludedFolders_relative_posix() async { | |
| var excludedFolderPath = join(projectFolderPath, 'aaa', 'bbb'); | |
| await provideConfig( | |
| initialize, | |
| // Exclude the folder with a relative path using forward slashes. | |
| { | |
| 'analysisExcludedFolders': ['aaa', 'bbb'].join('/'), | |
| }, | |
| ); | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| expect(server.contextManager.excludedPaths, equals([excludedFolderPath])); | |
| } | |
| Future<void> test_excludedFolders_trailingSlash() async { | |
| var excludedFolderPath = join(projectFolderPath, 'aaa', 'bbb'); | |
| await provideConfig( | |
| initialize, | |
| // Exclude the folder with a trailing slash. | |
| { | |
| 'analysisExcludedFolders': [ | |
| 'aaa', | |
| 'bbb', | |
| '', | |
| ].join(pathContext.separator), | |
| }, | |
| ); | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| expect(server.contextManager.excludedPaths, equals([excludedFolderPath])); | |
| } | |
| /// Check exclusions using explicit posix paths even if we're running | |
| /// on Windows. It's common for shared configs in VS Code to always use | |
| /// forward slashes in paths. | |
| Future<void> test_excludedFolders_trailingSlash_posix() async { | |
| var excludedFolderPath = join(projectFolderPath, 'aaa', 'bbb'); | |
| await provideConfig( | |
| initialize, | |
| // Exclude the folder with a forward trailing slash. | |
| { | |
| 'analysisExcludedFolders': ['aaa', 'bbb', ''].join('/'), | |
| }, | |
| ); | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| expect(server.contextManager.excludedPaths, equals([excludedFolderPath])); | |
| } | |
| /// Tests that requests that requires a unit result are handled correctly even | |
| /// if sent immediately after the `initialized` notification and do not result | |
| /// in "File not Analyzed"-style errors because roots are set asynchronously. | |
| Future<void> test_immediateRequests() async { | |
| newFile(mainFilePath, 'void f() {}'); | |
| late Future<Either2<List<DocumentSymbol>, List<SymbolInformation>>> result; | |
| await provideConfig( | |
| () => initialize( | |
| immediatelyAfterInitialized: () { | |
| result = getDocumentSymbols(mainFileUri); | |
| }, | |
| ), | |
| {}, | |
| ); | |
| var symbols = (await result).map( | |
| (docSymbols) => docSymbols, | |
| (symbolInfos) => symbolInfos, | |
| ); | |
| expect(symbols, hasLength(1)); | |
| } | |
| Future<void> test_initialize() async { | |
| var response = await initialize(); | |
| expect(response, isNotNull); | |
| expect(response.error, isNull); | |
| expect(response.result, isNotNull); | |
| expect( | |
| InitializeResult.canParse(response.result, nullLspJsonReporter), | |
| isTrue, | |
| ); | |
| var result = InitializeResult.fromJson( | |
| response.result as Map<String, Object?>, | |
| ); | |
| expect(result.capabilities, isNotNull); | |
| // Check some basic capabilities that are unlikely to change. | |
| expect(result.capabilities.textDocumentSync, isNotNull); | |
| result.capabilities.textDocumentSync!.map( | |
| (_) => throw 'Expected textDocumentSync capabilities to be a TextDocumentSyncOptions', | |
| (options) { | |
| // We'll always request open/closed notifications and incremental updates. | |
| expect(options.openClose, isTrue); | |
| expect(options.change, equals(TextDocumentSyncKind.Incremental)); | |
| }, | |
| ); | |
| } | |
| Future<void> test_initialize_invalidParams() async { | |
| var params = {'processId': 'invalid'}; | |
| var request = RequestMessage( | |
| id: Either2<int, String>.t1(1), | |
| method: Method.initialize, | |
| params: params, | |
| jsonrpc: jsonRpcVersion, | |
| ); | |
| var response = await sendRequestToServer(request); | |
| expect(response.id, equals(request.id)); | |
| expect(response.error, isNotNull); | |
| expect(response.error!.code, equals(ErrorCodes.InvalidParams)); | |
| expect(response.result, isNull); | |
| } | |
| Future<void> test_initialize_onlyAllowedOnce() async { | |
| await initialize(); | |
| var response = await initialize(throwOnFailure: false); | |
| expect(response, isNotNull); | |
| expect(response.result, isNull); | |
| expect(response.error, isNotNull); | |
| expect( | |
| response.error!.code, | |
| equals(ServerErrorCodes.serverAlreadyInitialized), | |
| ); | |
| } | |
| Future<void> test_initialize_rootPath() async { | |
| await initialize(rootPath: projectFolderPath); | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| } | |
| Future<void> test_initialize_rootPath_trailingSlash() async { | |
| await initialize(rootPath: withTrailingSlash(projectFolderPath)); | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| } | |
| Future<void> test_initialize_rootUri() async { | |
| await initialize(rootUri: projectFolderUri); | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| } | |
| Future<void> test_initialize_rootUri_encodedDriveLetterColon() async { | |
| await initialize(rootUri: withEncodedDriveLetterColon(projectFolderUri)); | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| } | |
| Future<void> test_initialize_rootUri_trailingSlash() async { | |
| await initialize(rootUri: withTrailingSlashUri(projectFolderUri)); | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| } | |
| Future<void> test_initialize_workspaceFolders() async { | |
| await initialize(workspaceFolders: [projectFolderUri]); | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| } | |
| Future<void> | |
| test_initialize_workspaceFolders_encodedDriveLetterColon() async { | |
| await initialize( | |
| workspaceFolders: [withEncodedDriveLetterColon(projectFolderUri)], | |
| ); | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| } | |
| Future<void> test_initialize_workspaceFolders_trailingSlash() async { | |
| await initialize( | |
| workspaceFolders: [withTrailingSlashUri(projectFolderUri)], | |
| ); | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| } | |
| Future<void> test_interleavedRequests_default() async { | |
| await initialize(rootUri: projectFolderUri, initializationOptions: {}); | |
| expect( | |
| MessageScheduler.allowOverlappingHandlers, | |
| allowOverlappingHandlersDefault, | |
| ); | |
| } | |
| Future<void> test_interleavedRequests_explicitFalse() async { | |
| await initialize( | |
| rootUri: projectFolderUri, | |
| initializationOptions: {'allowOverlappingHandlers': false}, | |
| ); | |
| expect(MessageScheduler.allowOverlappingHandlers, isFalse); | |
| } | |
| Future<void> test_interleavedRequests_explicitTrue() async { | |
| await initialize( | |
| rootUri: projectFolderUri, | |
| initializationOptions: {'allowOverlappingHandlers': true}, | |
| ); | |
| expect(MessageScheduler.allowOverlappingHandlers, isTrue); | |
| } | |
| Future<void> test_invalidExperimental_commands() async { | |
| await expectInvalidExperimentalParams({ | |
| 'commands': 1, | |
| }, 'ClientCapabilities.experimental.commands must be a List<String>?'); | |
| } | |
| Future<void> test_invalidExperimental_legacySnippetTextEdit() async { | |
| await expectInvalidExperimentalParams({ | |
| 'snippetTextEdit': 1, | |
| }, 'ClientCapabilities.experimental.snippetTextEdit must be a bool?'); | |
| } | |
| Future<void> test_nonFileScheme_rootUri() async { | |
| // We expect an error notification about the invalid file we try to open. | |
| failTestOnAnyErrorNotification = false; | |
| var rootUri = Uri.parse('vsls://'); | |
| var fileUri = rootUri.replace(path: '/file1.dart'); | |
| await initialize(rootUri: rootUri); | |
| expect(server.contextManager.includedPaths, equals([])); | |
| // Also open a non-file file to ensure it doesn't cause the root to be added. | |
| await openFile(fileUri, ''); | |
| expect(server.contextManager.includedPaths, equals([])); | |
| } | |
| /// Verifies a workspace that contains non-file workspace folders is handled. | |
| /// | |
| /// Related tests for didChangeWorkspaceFolders are in | |
| /// [ChangeWorkspaceFoldersTest]. | |
| Future<void> test_nonFileScheme_workspaceFolders() async { | |
| // We expect an error notification about the invalid file we try to open. | |
| failTestOnAnyErrorNotification = false; | |
| newPubspecYamlFile(projectFolderPath, ''); | |
| var rootUri = Uri.parse('vsls://'); | |
| var fileUri = rootUri.replace(path: '/file1.dart'); | |
| await initialize( | |
| workspaceFolders: [rootUri, pathContext.toUri(projectFolderPath)], | |
| ); | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| // Also open a non-file file to ensure it doesn't cause the root to be added. | |
| await openFile(fileUri, ''); | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| } | |
| Future<void> test_nonProjectFiles_basicWorkspace() async { | |
| var file1 = convertPath('/home/nonProject/file1.dart'); | |
| newFile(file1, ''); | |
| await initialize(allowEmptyRootUri: true); | |
| // Because the file is not in a project, it should be added itself. | |
| await openFile(pathContext.toUri(file1), ''); | |
| expect(server.contextManager.includedPaths, equals([file1])); | |
| } | |
| Future<void> test_nonProjectFiles_blazeWorkspace() async { | |
| var file1 = convertPath('/home/nonProject/file1.dart'); | |
| newFile(file1, ''); | |
| // Make /home a Blaze workspace. | |
| newFile('/home/${file_paths.blazeWorkspaceMarker}', ''); | |
| await initialize(allowEmptyRootUri: true); | |
| // Because the file is not in a project, it should be added itself. | |
| await openFile(pathContext.toUri(file1), ''); | |
| expect(server.contextManager.includedPaths, equals([file1])); | |
| } | |
| Future<void> test_onlyAnalyzeProjectsWithOpenFiles_fullyInitializes() async { | |
| // Enable some dynamic registrations, else registerCapability will not | |
| // be called. | |
| setAllSupportedTextDocumentDynamicRegistrations(); | |
| // Ensure when we use onlyAnalyzeProjectsWithOpenFiles that we still | |
| // fully initialize (eg. capabilities are registered). | |
| projectFolderPath = convertPath('/home/empty'); | |
| await expectRequest( | |
| Method.client_registerCapability, | |
| () => initialize( | |
| rootUri: projectFolderUri, | |
| initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true}, | |
| ), | |
| ); | |
| } | |
| Future<void> test_onlyAnalyzeProjectsWithOpenFiles_multipleFiles() async { | |
| var file1 = join(projectFolderPath, 'file1.dart'); | |
| var file1Uri = pathContext.toUri(file1); | |
| newFile(file1, ''); | |
| var file2 = join(projectFolderPath, 'file2.dart'); | |
| var file2Uri = pathContext.toUri(file2); | |
| newFile(file2, ''); | |
| newPubspecYamlFile(projectFolderPath, ''); | |
| await initialize( | |
| rootUri: projectFolderUri, | |
| initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true}, | |
| ); | |
| // Opening both files should only add the project folder once. | |
| await openFile(file1Uri, ''); | |
| await openFile(file2Uri, ''); | |
| await contextRebuildComplete; | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| expect(server.contextManager.driverMap, hasLength(1)); | |
| // Closing only one of the files should not remove the root or rebuild the context. | |
| resetContextBuildCounter(); | |
| await closeFile(file1Uri); | |
| await pumpEventQueue(times: 5000); | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| expect(server.contextManager.driverMap, hasLength(1)); | |
| expectNoContextBuilds(); | |
| // Closing the last file should remove the project folder and remove | |
| // the context. | |
| resetContextBuildCounter(); | |
| await closeFile(file2Uri); | |
| await contextRebuildComplete; | |
| expect(server.contextManager.includedPaths, equals([])); | |
| expect(server.contextManager.driverMap, hasLength(0)); | |
| expectContextBuilds(); | |
| } | |
| Future<void> test_onlyAnalyzeProjectsWithOpenFiles_withoutPubpsec() async { | |
| projectFolderPath = convertPath('/home/empty'); | |
| var nestedFilePath = join( | |
| projectFolderPath, | |
| 'nested', | |
| 'deeply', | |
| 'in', | |
| 'folders', | |
| 'test.dart', | |
| ); | |
| var nestedFileUri = pathContext.toUri(nestedFilePath); | |
| newFile(nestedFilePath, ''); | |
| // The project folder shouldn't be added to start with. | |
| await initialize( | |
| rootUri: projectFolderUri, | |
| initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true}, | |
| ); | |
| expect(server.contextManager.includedPaths, equals([])); | |
| // Opening the file should trigger it to be added. | |
| await openFile(nestedFileUri, ''); | |
| expect(server.contextManager.includedPaths, equals([nestedFilePath])); | |
| } | |
| Future<void> test_onlyAnalyzeProjectsWithOpenFiles_withPubpsec() async { | |
| var nestedFilePath = join( | |
| projectFolderPath, | |
| 'nested', | |
| 'deeply', | |
| 'in', | |
| 'folders', | |
| 'test.dart', | |
| ); | |
| var nestedFileUri = pathContext.toUri(nestedFilePath); | |
| newFile(nestedFilePath, ''); | |
| newPubspecYamlFile(projectFolderPath, ''); | |
| // The project folder shouldn't be added to start with. | |
| await initialize( | |
| rootUri: projectFolderUri, | |
| initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true}, | |
| ); | |
| expect(server.contextManager.includedPaths, equals([])); | |
| // Opening a file nested within the project should cause the project folder | |
| // to be added | |
| await openFile(nestedFileUri, ''); | |
| await contextRebuildComplete; | |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); | |
| // Ensure the file was cached in each driver. This happens as a result of | |
| // adding to priority files, but if that's done before the file is in an | |
| // analysis root it will not occur. | |
| // https://github.com/dart-lang/sdk/issues/37338 | |
| for (var driver in server.driverMap.values) { | |
| expect(driver.getCachedResolvedUnit(nestedFilePath), isNotNull); | |
| } | |
| // Closing the file should remove it. | |
| await closeFile(nestedFileUri); | |
| expect(server.contextManager.includedPaths, equals([])); | |
| } | |
| Future<void> test_survey_alwaysEnabledForLsp() async { | |
| await initialize(); | |
| expect(server.surveyManager, isNotNull); | |
| } | |
| Future<void> test_uninitialized_dropsNotifications() async { | |
| var notification = makeNotification( | |
| Method.fromJson('randomNotification'), | |
| null, | |
| ); | |
| var nextNotification = errorNotificationsFromServer.first; | |
| channel.sendNotificationToServer(notification); | |
| // Wait up to 1sec to ensure no error/log notifications were sent back. | |
| var didTimeout = false; | |
| var notificationFromServer = await nextNotification | |
| .then<NotificationMessage?>((notification) => notification) | |
| .timeout( | |
| const Duration(seconds: 1), | |
| onTimeout: () { | |
| didTimeout = true; | |
| return null; | |
| }, | |
| ); | |
| expect(notificationFromServer, isNull); | |
| expect(didTimeout, isTrue); | |
| } | |
| Future<void> test_uninitialized_rejectsRequests() async { | |
| var request = makeRequest(Method.fromJson('randomRequest'), null); | |
| var response = await channel.sendRequestToServer(request); | |
| expect(response.id, equals(request.id)); | |
| expect(response.result, isNull); | |
| expect(response.error, isNotNull); | |
| expect(response.error!.code, ErrorCodes.ServerNotInitialized); | |
| } | |
| } | |
| /// Runs all initialization tests with a resource provider that slowly | |
| /// initializes watchers to simulate delays in real fs watchers. | |
| @reflectiveTest | |
| class SlowInitializationTest extends InitializationTest { | |
| @override | |
| MemoryResourceProvider resourceProvider = MemoryResourceProvider( | |
| // Force the in-memory file watchers to be slowly initialized to emulate | |
| // the physical watchers (for test_concurrentContextRebuilds). | |
| delayWatcherInitialization: Duration(milliseconds: 1), | |
| ); | |
| } | |
| /// Runs all initialization tests using String IDs instead of integers (both | |
| /// are valid). | |
| @reflectiveTest | |
| class StringIdInitializationTest extends InitializationTest { | |
| @override | |
| LspMessageIdMode get idMode => LspMessageIdMode.string; | |
| } |