Implement LSP workspace/symbol
Change-Id: I4303a146dce9a5a4b40345f7ea8ef8355163337b
Reviewed-on: https://dart-review.googlesource.com/c/92602
Commit-Queue: Danny Tuppeny <dantup@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_initialize.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_initialize.dart
index 8d9b2fb..a67c282 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_initialize.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_initialize.dart
@@ -92,7 +92,7 @@
true, // referencesProvider
true, // documentHighlightProvider
true, // documentSymbolProvider
- null,
+ true, // workspaceSymbolProvider
// "The `CodeActionOptions` return type is only valid if the client
// signals code action literal support via the property
// `textDocument.codeAction.codeActionLiteralSupport`."
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart
index 36eb607..7594631 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart
@@ -25,6 +25,7 @@
import 'package:analysis_server/src/lsp/handlers/handler_signature_help.dart';
import 'package:analysis_server/src/lsp/handlers/handler_text_document_changes.dart';
import 'package:analysis_server/src/lsp/handlers/handler_change_workspace_folders.dart';
+import 'package:analysis_server/src/lsp/handlers/handler_workspace_symbols.dart';
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
@@ -68,6 +69,7 @@
registerHandler(new RenameHandler(server));
registerHandler(new FoldingHandler(server));
registerHandler(new DiagnosticServerHandler(server));
+ registerHandler(new WorkspaceSymbolHandler(server));
}
}
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_workspace_symbols.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_workspace_symbols.dart
new file mode 100644
index 0000000..03d6678
--- /dev/null
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_workspace_symbols.dart
@@ -0,0 +1,103 @@
+// Copyright (c) 2019, 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 'dart:collection';
+
+import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
+import 'package:analysis_server/lsp_protocol/protocol_special.dart';
+import 'package:analysis_server/src/lsp/handlers/handler_document_symbols.dart'
+ show defaultSupportedSymbolKinds;
+import 'package:analysis_server/src/lsp/handlers/handlers.dart';
+import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
+import 'package:analysis_server/src/lsp/mapping.dart';
+import 'package:analyzer/source/line_info.dart';
+
+import 'package:analyzer/src/dart/analysis/search.dart' as search;
+
+class WorkspaceSymbolHandler
+ extends MessageHandler<WorkspaceSymbolParams, List<SymbolInformation>> {
+ WorkspaceSymbolHandler(LspAnalysisServer server) : super(server);
+ Method get handlesMessage => Method.workspace_symbol;
+
+ @override
+ WorkspaceSymbolParams convertParams(Map<String, dynamic> json) =>
+ WorkspaceSymbolParams.fromJson(json);
+
+ Future<ErrorOr<List<SymbolInformation>>> handle(
+ WorkspaceSymbolParams params) async {
+ // Respond to empty queries with an empty list. The spec says this should
+ // be non-empty, however VS Code's client sends empty requests (but then
+ // appears to not render the results we supply anyway).
+ final query = params?.query ?? '';
+ if (query == '') {
+ return success([]);
+ }
+
+ final symbolCapabilities = server?.clientCapabilities?.workspace?.symbol;
+
+ final clientSupportedSymbolKinds =
+ symbolCapabilities?.symbolKind?.valueSet != null
+ ? new HashSet<SymbolKind>.of(symbolCapabilities.symbolKind.valueSet)
+ : defaultSupportedSymbolKinds;
+
+ // Convert the string input into a case-insensitive regex that has wildcards
+ // between every character and at start/end to allow for fuzzy matching.
+ final fuzzyQuery = query.split('').map(RegExp.escape).join('.*');
+ final partialFuzzyQuery = '.*$fuzzyQuery.*';
+ final regex = new RegExp(partialFuzzyQuery, caseSensitive: false);
+
+ // Cap the number of results we'll return because short queries may match
+ // huge numbers on large projects.
+ var remainingResults = 500;
+
+ final declarations = <search.Declaration>[];
+ final filePathsHashSet = new LinkedHashSet<String>();
+ for (var driver in server.driverMap.values) {
+ final driverResults = await driver.search
+ .declarations(regex, remainingResults, filePathsHashSet);
+ declarations.addAll(driverResults);
+ remainingResults -= driverResults.length;
+ }
+
+ // Convert the file paths to something we can quickly index into since
+ // we'll be looking things up by index a lot.
+ final filePaths = filePathsHashSet.toList();
+ // We'll need line information to convert locations, so fetch
+ // them once and allow looking up using the file index.
+ final lineInfos = filePaths.map(server.getLineInfo).toList();
+
+ // Map the results to SymbolInformations and flatten the list of lists.
+ final symbols = declarations
+ .map((declaration) => _asSymbolInformation(
+ declaration,
+ clientSupportedSymbolKinds,
+ filePaths,
+ lineInfos,
+ ))
+ .toList();
+
+ return success(symbols);
+ }
+
+ SymbolInformation _asSymbolInformation(
+ search.Declaration declaration,
+ HashSet<SymbolKind> clientSupportedSymbolKinds,
+ List<String> filePaths,
+ List<LineInfo> lineInfos,
+ ) {
+ final filePath = filePaths[declaration.fileIndex];
+ final lineInfo = lineInfos[declaration.fileIndex];
+ return new SymbolInformation(
+ declaration.name,
+ declarationKindToSymbolKind(
+ clientSupportedSymbolKinds, declaration.kind),
+ null, // We don't have easy access to isDeprecated here.
+ new Location(
+ Uri.file(filePath).toString(),
+ toRange(lineInfo, declaration.codeOffset, declaration.codeLength),
+ ),
+ declaration.className ?? declaration.mixinName);
+ }
+}
diff --git a/pkg/analysis_server/lib/src/lsp/mapping.dart b/pkg/analysis_server/lib/src/lsp/mapping.dart
index edf1327..2f9dd65 100644
--- a/pkg/analysis_server/lib/src/lsp/mapping.dart
+++ b/pkg/analysis_server/lib/src/lsp/mapping.dart
@@ -15,6 +15,8 @@
import 'package:analyzer/dart/analysis/results.dart' as server;
import 'package:analyzer/error/error.dart' as server;
import 'package:analyzer/source/line_info.dart' as server;
+import 'package:analyzer/src/dart/analysis/search.dart' as server
+ show DeclarationKind;
import 'package:analyzer/src/generated/source.dart' as server;
import 'package:analyzer_plugin/utilities/fixes/fixes.dart' as server;
@@ -47,6 +49,48 @@
.toList());
}
+lsp.SymbolKind declarationKindToSymbolKind(
+ HashSet<lsp.SymbolKind> clientSupportedSymbolKinds,
+ server.DeclarationKind kind,
+) {
+ bool isSupported(lsp.SymbolKind kind) =>
+ clientSupportedSymbolKinds.contains(kind);
+
+ List<lsp.SymbolKind> getKindPreferences() {
+ switch (kind) {
+ case server.DeclarationKind.CLASS:
+ case server.DeclarationKind.CLASS_TYPE_ALIAS:
+ return const [lsp.SymbolKind.Class];
+ case server.DeclarationKind.CONSTRUCTOR:
+ return const [lsp.SymbolKind.Constructor];
+ case server.DeclarationKind.ENUM:
+ case server.DeclarationKind.ENUM_CONSTANT:
+ return const [lsp.SymbolKind.Enum];
+ case server.DeclarationKind.FIELD:
+ return const [lsp.SymbolKind.Field];
+ case server.DeclarationKind.FUNCTION:
+ return const [lsp.SymbolKind.Function];
+ case server.DeclarationKind.FUNCTION_TYPE_ALIAS:
+ return const [lsp.SymbolKind.Class];
+ case server.DeclarationKind.GETTER:
+ return const [lsp.SymbolKind.Property];
+ case server.DeclarationKind.METHOD:
+ return const [lsp.SymbolKind.Method];
+ case server.DeclarationKind.MIXIN:
+ return const [lsp.SymbolKind.Class];
+ case server.DeclarationKind.SETTER:
+ return const [lsp.SymbolKind.Property];
+ case server.DeclarationKind.VARIABLE:
+ return const [lsp.SymbolKind.Variable];
+ default:
+ assert(false, 'Unexpected declaration kind $kind');
+ return null;
+ }
+ }
+
+ return getKindPreferences().firstWhere(isSupported, orElse: () => null);
+}
+
lsp.CompletionItemKind elementKindToCompletionItemKind(
HashSet<lsp.CompletionItemKind> clientSupportedCompletionKinds,
server.ElementKind kind,
diff --git a/pkg/analysis_server/test/lsp/server_abstract.dart b/pkg/analysis_server/test/lsp/server_abstract.dart
index 94c60f2..a1735ec 100644
--- a/pkg/analysis_server/test/lsp/server_abstract.dart
+++ b/pkg/analysis_server/test/lsp/server_abstract.dart
@@ -403,6 +403,14 @@
return expectSuccessfulResponseTo(request);
}
+ Future<List<SymbolInformation>> getWorkspaceSymbols(String query) {
+ final request = makeRequest(
+ Method.workspace_symbol,
+ new WorkspaceSymbolParams(query),
+ );
+ return expectSuccessfulResponseTo(request);
+ }
+
Future<List<FoldingRange>> getFoldingRegions(Uri uri) {
final request = makeRequest(
Method.textDocument_foldingRange,
diff --git a/pkg/analysis_server/test/lsp/test_all.dart b/pkg/analysis_server/test/lsp/test_all.dart
index 2745a6b..bdd25b3 100644
--- a/pkg/analysis_server/test/lsp/test_all.dart
+++ b/pkg/analysis_server/test/lsp/test_all.dart
@@ -15,6 +15,7 @@
import 'document_highlights_test.dart' as document_highlights_test;
import 'document_symbols_test.dart' as document_symbols_test;
import 'file_modification_test.dart' as file_modification_test;
+import 'folding_test.dart' as folding_test;
import 'format_test.dart' as format_test;
import 'hover_test.dart' as hover_test;
import 'initialization_test.dart' as initialization_test;
@@ -23,7 +24,7 @@
import 'rename_test.dart' as rename_test;
import 'server_test.dart' as server_test;
import 'signature_help_test.dart' as signature_help_test;
-import 'folding_test.dart' as folding_test;
+import 'workspace_symbols_test.dart' as workspace_symbols_test;
main() {
defineReflectiveSuite(() {
@@ -46,5 +47,6 @@
packet_transformer_tests.main();
rename_test.main();
folding_test.main();
+ workspace_symbols_test.main();
}, name: 'lsp');
}
diff --git a/pkg/analysis_server/test/lsp/workspace_symbols_test.dart b/pkg/analysis_server/test/lsp/workspace_symbols_test.dart
new file mode 100644
index 0000000..72cad45
--- /dev/null
+++ b/pkg/analysis_server/test/lsp/workspace_symbols_test.dart
@@ -0,0 +1,107 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'server_abstract.dart';
+
+main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(WorkspaceSymbolsTest);
+ });
+}
+
+@reflectiveTest
+class WorkspaceSymbolsTest extends AbstractLspAnalysisServerTest {
+ test_fullMatch() async {
+ const content = '''
+ [[String topLevel = '']];
+ class MyClass {
+ int myField;
+ MyClass(this.myField);
+ myMethod() {}
+ }
+ ''';
+ newFile(mainFilePath, content: withoutMarkers(content));
+ await initialize();
+
+ final symbols = await getWorkspaceSymbols('topLevel');
+
+ final topLevel = symbols.firstWhere((s) => s.name == 'topLevel');
+ expect(topLevel.kind, equals(SymbolKind.Variable));
+ expect(topLevel.containerName, isNull);
+ expect(topLevel.location.uri, equals(mainFileUri.toString()));
+ expect(topLevel.location.range, equals(rangeFromMarkers(content)));
+
+ // Ensure we didn't get some things that definitely do not match.
+ expect(symbols.any((s) => s.name == 'MyClass'), isFalse);
+ expect(symbols.any((s) => s.name == 'myMethod'), isFalse);
+ }
+
+ test_fuzzyMatch() async {
+ const content = '''
+ String topLevel = '';
+ class MyClass {
+ [[int myField]];
+ MyClass(this.myField);
+ myMethod() {}
+ }
+ ''';
+ newFile(mainFilePath, content: withoutMarkers(content));
+ await initialize();
+
+ // meld should match myField
+ final symbols = await getWorkspaceSymbols('meld');
+
+ final field = symbols.firstWhere((s) => s.name == 'myField');
+ expect(field.kind, equals(SymbolKind.Field));
+ expect(field.containerName, equals('MyClass'));
+ expect(field.location.uri, equals(mainFileUri.toString()));
+ expect(field.location.range, equals(rangeFromMarkers(content)));
+
+ // Ensure we didn't get some things that definitely do not match.
+ expect(symbols.any((s) => s.name == 'MyClass'), isFalse);
+ expect(symbols.any((s) => s.name == 'myMethod'), isFalse);
+ }
+
+ test_partialMatch() async {
+ const content = '''
+ String topLevel = '';
+ class MyClass {
+ [[int myField]];
+ MyClass(this.myField);
+ [[myMethod() {}]]
+ }
+ ''';
+ newFile(mainFilePath, content: withoutMarkers(content));
+ await initialize();
+
+ final symbols = await getWorkspaceSymbols('my');
+ final ranges = rangesFromMarkers(content);
+ final fieldRange = ranges[0];
+ final methodRange = ranges[1];
+
+ final field = symbols.firstWhere((s) => s.name == 'myField');
+ expect(field.kind, equals(SymbolKind.Field));
+ expect(field.containerName, equals('MyClass'));
+ expect(field.location.uri, equals(mainFileUri.toString()));
+ expect(field.location.range, equals(fieldRange));
+
+ final klass = symbols.firstWhere((s) => s.name == 'MyClass');
+ expect(klass.kind, equals(SymbolKind.Class));
+ expect(klass.containerName, isNull);
+ expect(klass.location.uri, equals(mainFileUri.toString()));
+
+ final method = symbols.firstWhere((s) => s.name == 'myMethod');
+ expect(method.kind, equals(SymbolKind.Method));
+ expect(method.containerName, equals('MyClass'));
+ expect(method.location.uri, equals(mainFileUri.toString()));
+ expect(method.location.range, equals(methodRange));
+
+ // Ensure we didn't get some things that definitely do not match.
+ expect(symbols.any((s) => s.name == 'topLevel'), isFalse);
+ }
+}
diff --git a/pkg/analysis_server/tool/lsp_spec/README.md b/pkg/analysis_server/tool/lsp_spec/README.md
index b696b00..5c0ab08 100644
--- a/pkg/analysis_server/tool/lsp_spec/README.md
+++ b/pkg/analysis_server/tool/lsp_spec/README.md
@@ -41,7 +41,7 @@
| workspace/didChangeWorkspaceFolders | ✅ | ✅ | ✅ | ✅ |
| workspace/configuration | | | | |
| workspace/didChangeWatchedFiles | | | | | unused, server does own watching |
-| workspace/symbol | | | | |
+| workspace/symbol | ✅ | ✅ | ✅ | ✅ |
| workspace/executeCommand | ✅ | ✅ | ✅ | ✅ |
| workspace/applyEdit | ✅ | ✅ | ✅ | ✅ |
| textDocument/didOpen | ✅ | ✅ | ✅ | ✅ |