70358239d8
This removes all code related to handling requests for (and sending notifications of modifications of) the virtual files for macros. Clients would never call this handler unless the analysis server had previously told them about these virtual files with the `dart-macro+file` scheme, which never happens because the implementation was previously removed. This does not remove the `clientUriConverter` (which as well as handling conversions to/from the macro scheme, also handles conversions between URIs and Paths to support using URIs in the legacy protocol) because I'm not yet certain that is unused. Change-Id: I148e2383a48b5f6e3a28eff3dd11506fc86353b0 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/461120 Reviewed-by: Samuel Rawlins <srawlins@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
1383 lines
50 KiB
Dart
1383 lines
50 KiB
Dart
// 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 'change_workspace_folders_test.dart';
|
|
import 'server_abstract.dart';
|
|
|
|
void main() {
|
|
defineReflectiveSuite(() {
|
|
defineReflectiveTests(InitializationTest);
|
|
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,
|
|
'pattern',
|
|
'**/analysis_options.yaml',
|
|
);
|
|
|
|
var isDartSelector = TypeMatcher<TextDocumentFilterScheme>().having(
|
|
(selector) => selector.language,
|
|
'language',
|
|
'dart',
|
|
);
|
|
|
|
var isPubspecSelector = TypeMatcher<TextDocumentFilterScheme>().having(
|
|
(selector) => selector.pattern,
|
|
'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: '**/*.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, hasLength(1));
|
|
}
|
|
|
|
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_dartCodeAction() async {
|
|
await expectInvalidExperimentalParams(
|
|
{'dartCodeAction': 1},
|
|
'ClientCapabilities.experimental.dartCodeAction must be a Map<String, Object?>?',
|
|
);
|
|
}
|
|
|
|
Future<void>
|
|
test_invalidExperimental_dartCodeAction_commandParameterSupport() async {
|
|
await expectInvalidExperimentalParams(
|
|
{
|
|
'dartCodeAction': {'commandParameterSupport': 1},
|
|
},
|
|
'ClientCapabilities.experimental.dartCodeAction.commandParameterSupport must be a Map<String, Object?>?',
|
|
);
|
|
}
|
|
|
|
Future<void>
|
|
test_invalidExperimental_dartCodeAction_commandParameterSupport_supportedKinds() async {
|
|
await expectInvalidExperimentalParams(
|
|
{
|
|
'dartCodeAction': {
|
|
'commandParameterSupport': {'supportedKinds': 1},
|
|
},
|
|
},
|
|
'ClientCapabilities.experimental.dartCodeAction.commandParameterSupport.supportedKinds must be a List<String>?',
|
|
);
|
|
}
|
|
|
|
Future<void> test_invalidExperimental_snippetTextEdit() 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),
|
|
);
|
|
}
|