Skip to content

Commit

Permalink
[analysis_server] Refactor servers to allow producing edits for LSP o…
Browse files Browse the repository at this point in the history
…ver Legacy

This is a no-op refactor extracted from an upcoming change to enable the onWillRenameFiles request to work for LSP over the Legacy protocol.

It's mostly lifting some API from the LSP server to the base server and some into a new mixin (`LspVerifyEditHelpersMixin`) to provide the required methods for verifying edits without the usual LSP base test classes.

Change-Id: Id6084a80e3afb9c6af1a84d9e8a18d460b5f4cce
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/329980
Commit-Queue: Keerti Parthasarathy <keertip@google.com>
Reviewed-by: Keerti Parthasarathy <keertip@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
  • Loading branch information
DanTup authored and Commit Queue committed Oct 10, 2023
1 parent 07bb4d1 commit 800421b
Show file tree
Hide file tree
Showing 11 changed files with 128 additions and 66 deletions.
40 changes: 39 additions & 1 deletion pkg/analysis_server/lib/src/analysis_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import 'dart:io' as io;
import 'dart:io';

import 'package:analysis_server/lsp_protocol/protocol.dart' as lsp
show MessageType;
show MessageType, OptionalVersionedTextDocumentIdentifier;
import 'package:analysis_server/src/analytics/analytics_manager.dart';
import 'package:analysis_server/src/collections.dart';
import 'package:analysis_server/src/context_manager.dart';
Expand All @@ -29,6 +29,7 @@ import 'package:analysis_server/src/services/correction/namespace.dart';
import 'package:analysis_server/src/services/pub/pub_api.dart';
import 'package:analysis_server/src/services/pub/pub_command.dart';
import 'package:analysis_server/src/services/pub/pub_package_service.dart';
import 'package:analysis_server/src/services/refactoring/legacy/refactoring.dart';
import 'package:analysis_server/src/services/search/element_visitors.dart';
import 'package:analysis_server/src/services/search/search_engine.dart';
import 'package:analysis_server/src/services/search/search_engine_internal.dart';
Expand All @@ -49,6 +50,7 @@ import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/overlay_file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/instrumentation/instrumentation.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/dart/analysis/byte_store.dart';
import 'package:analyzer/src/dart/analysis/driver.dart' as analysis;
import 'package:analyzer/src/dart/analysis/driver.dart';
Expand Down Expand Up @@ -202,6 +204,10 @@ abstract class AnalysisServer {
/// Starts completed and will be replaced each time a context rebuild starts.
Completer<void> analysisContextRebuildCompleter = Completer()..complete();

/// The workspace for rename refactorings.
late final refactoringWorkspace =
RefactoringWorkspace(driverMap.values, searchEngine);

AnalysisServer(
this.options,
this.sdkManager,
Expand Down Expand Up @@ -477,6 +483,9 @@ abstract class AnalysisServer {
DartdocDirectiveInfo();
}

/// Gets the current version number of a document (if known).
int? getDocumentVersion(String path);

/// Return a [Future] that completes with the [Element] at the given
/// [offset] of the given [file], or with `null` if there is no node at the
/// [offset] or the node does not have an element.
Expand Down Expand Up @@ -524,6 +533,25 @@ abstract class AnalysisServer {
return element;
}

/// Return a [LineInfo] for the file with the given [path].
///
/// If the file does not exist or cannot be read, returns `null`.
///
/// This method supports non-Dart files but uses the current content of the
/// file which may not be the latest analyzed version of the file if it was
/// recently modified, so using the lineInfo from an analyzed result may be
/// preferable.
LineInfo? getLineInfo(String path) {
try {
final content = resourceProvider.getFile(path).readAsStringSync();
return LineInfo.fromContent(content);
} on FileSystemException {
// If the file does not exist or cannot be read, return null to allow
// the caller to decide how to handle this.
return null;
}
}

/// Return a [Future] that completes with the resolved [AstNode] at the
/// given [offset] of the given [file], or with `null` if there is no node as
/// the [offset].
Expand Down Expand Up @@ -606,6 +634,16 @@ abstract class AnalysisServer {
});
}

/// Gets the version of a document known to the server, returning a
/// [lsp.OptionalVersionedTextDocumentIdentifier] with a version of `null` if the
/// document version is not known.
lsp.OptionalVersionedTextDocumentIdentifier getVersionedDocumentIdentifier(
String path) {
return lsp.OptionalVersionedTextDocumentIdentifier(
uri: resourceProvider.pathContext.toUri(path),
version: getDocumentVersion(path));
}

@mustCallSuper
FutureOr<void> handleAnalysisStatusChange(analysis.AnalysisStatus status) {
if (isFirstAnalysisSinceContextsBuilt && !status.isAnalyzing) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import 'package:analysis_server/src/handler/legacy/legacy_handler.dart';
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analysis_server/src/lsp/handlers/handler_states.dart';
import 'package:analysis_server/src/lsp/handlers/handlers.dart' as lsp;
import 'package:analyzer/dart/analysis/session.dart';
import 'package:language_server_protocol/json_parsing.dart';
import 'package:language_server_protocol/protocol_custom_generated.dart';
import 'package:language_server_protocol/protocol_generated.dart';
import 'package:language_server_protocol/protocol_special.dart';

/// The handler for the `lsp.handle` request.
Expand Down Expand Up @@ -71,6 +73,11 @@ class LspOverLegacyHandler extends LegacyHandler {
ErrorOr<Object?> result;
try {
result = await handler.handleMessage(message, messageInfo);
} on InconsistentAnalysisException {
result = error(
ErrorCodes.ContentModified,
'Document was modified before operation completed',
);
} catch (e) {
final errorMessage =
'An error occurred while handling ${message.method} request: $e';
Expand Down
12 changes: 7 additions & 5 deletions pkg/analysis_server/lib/src/legacy_analysis_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ import 'package:analysis_server/src/server/sdk_configuration.dart';
import 'package:analysis_server/src/services/completion/completion_state.dart';
import 'package:analysis_server/src/services/execution/execution_context.dart';
import 'package:analysis_server/src/services/flutter/widget_descriptions.dart';
import 'package:analysis_server/src/services/refactoring/legacy/refactoring.dart';
import 'package:analysis_server/src/services/refactoring/legacy/refactoring_manager.dart';
import 'package:analysis_server/src/services/user_prompts/dart_fix_prompt_manager.dart';
import 'package:analysis_server/src/utilities/process.dart';
Expand Down Expand Up @@ -320,9 +319,6 @@ class LegacyAnalysisServer extends AnalysisServer {
/// The state used by the completion domain handlers.
final CompletionState completionState = CompletionState();

/// The workspace for rename refactorings.
late RefactoringWorkspace refactoringWorkspace;

/// The object used to manage uncompleted refactorings.
late RefactoringManager? _refactoringManager;

Expand Down Expand Up @@ -428,7 +424,6 @@ class LegacyAnalysisServer extends AnalysisServer {
);
debounceRequests(channel, discardedRequests)
.listen(handleRequestOrResponse, onDone: done, onError: error);
refactoringWorkspace = RefactoringWorkspace(driverMap.values, searchEngine);
_newRefactoringManager();
}

Expand Down Expand Up @@ -513,6 +508,13 @@ class LegacyAnalysisServer extends AnalysisServer {
return driver?.getCachedResult(path);
}

/// Gets the current version number of a document.
///
/// For the legacy server we do not track version numbers, these are
/// LSP-specific.
@override
int? getDocumentVersion(String path) => null;

@override
FutureOr<void> handleAnalysisStatusChange(analysis.AnalysisStatus status) {
super.handleAnalysisStatusChange(status);
Expand Down
34 changes: 4 additions & 30 deletions pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import 'package:analysis_server/src/server/detachable_filesystem_manager.dart';
import 'package:analysis_server/src/server/diagnostic_server.dart';
import 'package:analysis_server/src/server/error_notifier.dart';
import 'package:analysis_server/src/server/performance.dart';
import 'package:analysis_server/src/services/refactoring/legacy/refactoring.dart';
import 'package:analysis_server/src/services/user_prompts/dart_fix_prompt_manager.dart';
import 'package:analysis_server/src/utilities/flutter.dart';
import 'package:analysis_server/src/utilities/process.dart';
Expand All @@ -39,7 +38,6 @@ import 'package:analyzer/error/error.dart';
import 'package:analyzer/exception/exception.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/instrumentation/instrumentation.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/dart/analysis/driver.dart' as analysis;
import 'package:analyzer/src/dart/analysis/status.dart' as analysis;
import 'package:analyzer/src/generated/sdk.dart';
Expand Down Expand Up @@ -78,10 +76,6 @@ class LspAnalysisServer extends AnalysisServer {
/// be sent.
final LspServerCommunicationChannel channel;

/// The workspace for rename refactorings. Should be accessed through the
/// refactoringWorkspace getter to be automatically created (lazily).
RefactoringWorkspace? _refactoringWorkspace;

/// The versions of each document known to the server (keyed by path), used to
/// send back to the client for server-initiated edits so that the client can
/// ensure they have a matching version of the document before applying them.
Expand Down Expand Up @@ -260,9 +254,6 @@ class LspAnalysisServer extends AnalysisServer {
}
}

RefactoringWorkspace get refactoringWorkspace => _refactoringWorkspace ??=
RefactoringWorkspace(driverMap.values, searchEngine);

/// Whether or not the client has advertised support for
/// 'window/showMessageRequest'.
///
Expand Down Expand Up @@ -355,30 +346,13 @@ class LspAnalysisServer extends AnalysisServer {
}

/// Gets the current version number of a document.
@override
int? getDocumentVersion(String path) => documentVersions[path]?.version;

/// Return a [LineInfo] for the file with the given [path].
///
/// If the file does not exist or cannot be read, returns `null`.
///
/// This method supports non-Dart files but uses the current content of the
/// file which may not be the latest analyzed version of the file if it was
/// recently modified, so using the lineInfo from an analyzed result may be
/// preferable.
LineInfo? getLineInfo(String path) {
try {
final content = resourceProvider.getFile(path).readAsStringSync();
return LineInfo.fromContent(content);
} on FileSystemException {
// If the file does not exist or cannot be read, return null to allow
// the caller to decide how to handle this.
return null;
}
}

/// Gets the version of a document known to the server, returning a
/// [OptionalVersionedTextDocumentIdentifier] with a version of `null` if the document
/// version is not known.
/// [OptionalVersionedTextDocumentIdentifier] with a version of `null` if the
/// document version is not known.
@override
OptionalVersionedTextDocumentIdentifier getVersionedDocumentIdentifier(
String path) {
return OptionalVersionedTextDocumentIdentifier(
Expand Down
5 changes: 3 additions & 2 deletions pkg/analysis_server/lib/src/lsp/mapping.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'package:analysis_server/lsp_protocol/protocol.dart' as lsp;
import 'package:analysis_server/lsp_protocol/protocol.dart' hide Declaration;
import 'package:analysis_server/src/analysis_server.dart';
import 'package:analysis_server/src/collections.dart';
import 'package:analysis_server/src/computer/computer_hover.dart';
import 'package:analysis_server/src/lsp/client_capabilities.dart';
Expand Down Expand Up @@ -81,7 +82,7 @@ lsp.Either2<lsp.MarkupContent, String> asMarkupContentOrString(
/// it's important to call this immediately after computing edits to ensure
/// the document is not modified before the version number is read.
lsp.WorkspaceEdit createPlainWorkspaceEdit(
lsp.LspAnalysisServer server, List<server.SourceFileEdit> edits) {
AnalysisServer server, List<server.SourceFileEdit> edits) {
return toWorkspaceEdit(
// Client capabilities are always available after initialization.
server.lspClientCapabilities!,
Expand Down Expand Up @@ -133,7 +134,7 @@ WorkspaceEdit createRenameEdit(
/// it's important to call this immediately after computing edits to ensure
/// the document is not modified before the version number is read.
lsp.WorkspaceEdit createWorkspaceEdit(
lsp.LspAnalysisServer server,
AnalysisServer server,
server.SourceChange change, {
// The caller must specify whether snippets are valid here for where they're
// sending this edit. Right now, support is limited to CodeActions.
Expand Down
25 changes: 14 additions & 11 deletions pkg/analysis_server/test/lsp/change_verifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:collection/collection.dart';
import 'package:test/test.dart' hide expect;
import 'package:test/test.dart';

import 'server_abstract.dart';
import 'request_helpers_mixin.dart';

/// Applies LSP [WorkspaceEdit]s to produce a flattened string describing the
/// new file contents and any create/rename/deletes to use in test expectations.
Expand All @@ -21,18 +21,18 @@ class LspChangeVerifier {
/// Changes collected while applying the edit.
final _changes = <Uri, _Change>{};

/// A base test class used to obtain the current content of a file.
final LspAnalysisServerTestMixin _server;
/// A mixin with helpers for applying LSP edits.
final LspVerifyEditHelpersMixin editHelpers;

/// The [WorkspaceEdit] being applied/verified.
final WorkspaceEdit edit;

LspChangeVerifier(this._server, this.edit) {
LspChangeVerifier(this.editHelpers, this.edit) {
_applyEdit();
}

void verifyFiles(String expected, {Map<Uri, int>? expectedVersions}) {
_server.expect(_toChangeString(), equals(expected));
expect(_toChangeString(), equals(expected));
if (expectedVersions != null) {
_verifyDocumentVersions(expectedVersions);
}
Expand Down Expand Up @@ -134,11 +134,13 @@ class LspChangeVerifier {
final indexedEdits =
edit.edits.mapIndexed(TextEditWithIndex.fromUnion).toList();
indexedEdits.sort(TextEditWithIndex.compare);
return indexedEdits.map((e) => e.edit).fold(content, _server.applyTextEdit);
return indexedEdits
.map((e) => e.edit)
.fold(content, editHelpers.applyTextEdit);
}

String _applyTextEdits(String content, List<TextEdit> changes) =>
_server.applyTextEdits(content, changes);
editHelpers.applyTextEdits(content, changes);

_Change _change(Uri fileUri) => _changes.putIfAbsent(
fileUri, () => _Change(_getCurrentFileContent(fileUri)));
Expand All @@ -150,12 +152,13 @@ class LspChangeVerifier {
final uri = edit.textDocument.uri;
final expectedVersion = expectedVersions[uri];

_server.expect(edit.textDocument.version, equals(expectedVersion));
expect(edit.textDocument.version, equals(expectedVersion));
}

String? _getCurrentFileContent(Uri uri) => _server.getCurrentFileContent(uri);
String? _getCurrentFileContent(Uri uri) =>
editHelpers.getCurrentFileContent(uri);

String _relativeUri(Uri uri) => _server.relativeUri(uri);
String _relativeUri(Uri uri) => editHelpers.relativeUri(uri);

String _toChangeString() {
final buffer = StringBuffer();
Expand Down
27 changes: 27 additions & 0 deletions pkg/analysis_server/test/lsp/request_helpers_mixin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import 'package:analysis_server/src/lsp/mapping.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:collection/collection.dart';
import 'package:language_server_protocol/json_parsing.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart' as test show expect;
import 'package:test/test.dart' hide expect;

import 'change_verifier.dart';

/// A mixin with helpers for applying LSP edits to strings.
mixin LspEditHelpersMixin {
String applyTextEdit(String content, TextEdit edit) {
final startPos = edit.range.start;
Expand Down Expand Up @@ -660,3 +662,28 @@ mixin LspRequestHelpersMixin {
};
}
}

/// A mixin with helpers for verifying LSP edits in a given project.
///
/// Extends [LspEditHelpersMixin] with methods for accessing file state and
/// information about the project to build paths.
mixin LspVerifyEditHelpersMixin on LspEditHelpersMixin {
path.Context get pathContext;

String get projectFolderPath;

/// A function to get the current contents of a file to apply edits.
String? getCurrentFileContent(Uri uri);

/// Formats a path relative to the project root always using forward slashes.
///
/// This is used in the text format for comparing edits.
String relativePath(String filePath) => pathContext
.relative(filePath, from: projectFolderPath)
.replaceAll(r'\', '/');

/// Formats a path relative to the project root always using forward slashes.
///
/// This is used in the text format for comparing edits.
String relativeUri(Uri uri) => relativePath(pathContext.fromUri(uri));
}
13 changes: 1 addition & 12 deletions pkg/analysis_server/test/lsp/server_abstract.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ abstract class AbstractLspAnalysisServerTest
ClientCapabilitiesHelperMixin,
LspRequestHelpersMixin,
LspEditHelpersMixin,
LspVerifyEditHelpersMixin,
LspAnalysisServerTestMixin,
ConfigurationFilesMixin {
late MockLspServerChannel channel;
Expand Down Expand Up @@ -1372,18 +1373,6 @@ mixin LspAnalysisServerTestMixin
);
}

/// Formats a path relative to the project root always using forward slashes.
///
/// This is used in the text format for comparing edits.
String relativePath(String filePath) => pathContext
.relative(filePath, from: projectFolderPath)
.replaceAll(r'\', '/');

/// Formats a path relative to the project root always using forward slashes.
///
/// This is used in the text format for comparing edits.
String relativeUri(Uri uri) => relativePath(pathContext.fromUri(uri));

Future<WorkspaceEdit?> rename(
Uri uri,
int? version,
Expand Down
Loading

0 comments on commit 800421b

Please sign in to comment.