[Analyzer] Add support for LSP experimental SnippetTextEdit

Change-Id: Id37a1954c71fb10c4968c5af6a873ad0ef864a5a
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/191403
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Danny Tuppeny
2021-03-16 18:40:50 +00:00
committed by commit-bot@chromium.org
parent 217cd49aaf
commit 44275c6001
14 changed files with 471 additions and 34 deletions
@@ -1573,3 +1573,135 @@ class PublishOutlineParams implements ToJsonable {
@override
String toString() => jsonEncoder.convert(toJson());
}
class SnippetTextEdit implements TextEdit, ToJsonable {
static const jsonHandler =
LspJsonHandler(SnippetTextEdit.canParse, SnippetTextEdit.fromJson);
SnippetTextEdit(
{@required this.insertTextFormat,
@required this.range,
@required this.newText}) {
if (insertTextFormat == null) {
throw 'insertTextFormat is required but was not provided';
}
if (range == null) {
throw 'range is required but was not provided';
}
if (newText == null) {
throw 'newText is required but was not provided';
}
}
static SnippetTextEdit fromJson(Map<String, dynamic> json) {
final insertTextFormat = json['insertTextFormat'] != null
? InsertTextFormat.fromJson(json['insertTextFormat'])
: null;
final range = json['range'] != null ? Range.fromJson(json['range']) : null;
final newText = json['newText'];
return SnippetTextEdit(
insertTextFormat: insertTextFormat, range: range, newText: newText);
}
final InsertTextFormat insertTextFormat;
/// The string to be inserted. For delete operations use an empty string.
final String newText;
/// The range of the text document to be manipulated. To insert text into a
/// document create a range where start === end.
final Range range;
Map<String, dynamic> toJson() {
var __result = <String, dynamic>{};
__result['insertTextFormat'] = insertTextFormat?.toJson() ??
(throw 'insertTextFormat is required but was not set');
__result['range'] =
range?.toJson() ?? (throw 'range is required but was not set');
__result['newText'] =
newText ?? (throw 'newText is required but was not set');
return __result;
}
static bool canParse(Object obj, LspJsonReporter reporter) {
if (obj is Map<String, dynamic>) {
reporter.push('insertTextFormat');
try {
if (!obj.containsKey('insertTextFormat')) {
reporter.reportError('must not be undefined');
return false;
}
if (obj['insertTextFormat'] == null) {
reporter.reportError('must not be null');
return false;
}
if (!(InsertTextFormat.canParse(obj['insertTextFormat'], reporter))) {
reporter.reportError('must be of type InsertTextFormat');
return false;
}
} finally {
reporter.pop();
}
reporter.push('range');
try {
if (!obj.containsKey('range')) {
reporter.reportError('must not be undefined');
return false;
}
if (obj['range'] == null) {
reporter.reportError('must not be null');
return false;
}
if (!(Range.canParse(obj['range'], reporter))) {
reporter.reportError('must be of type Range');
return false;
}
} finally {
reporter.pop();
}
reporter.push('newText');
try {
if (!obj.containsKey('newText')) {
reporter.reportError('must not be undefined');
return false;
}
if (obj['newText'] == null) {
reporter.reportError('must not be null');
return false;
}
if (!(obj['newText'] is String)) {
reporter.reportError('must be of type String');
return false;
}
} finally {
reporter.pop();
}
return true;
} else {
reporter.reportError('must be of type SnippetTextEdit');
return false;
}
}
@override
bool operator ==(Object other) {
if (other is SnippetTextEdit && other.runtimeType == SnippetTextEdit) {
return insertTextFormat == other.insertTextFormat &&
range == other.range &&
newText == other.newText &&
true;
}
return false;
}
@override
int get hashCode {
var hash = 0;
hash = JenkinsSmiHash.combine(hash, insertTextFormat.hashCode);
hash = JenkinsSmiHash.combine(hash, range.hashCode);
hash = JenkinsSmiHash.combine(hash, newText.hashCode);
return JenkinsSmiHash.finish(hash);
}
@override
String toString() => jsonEncoder.convert(toJson());
}
@@ -30666,14 +30666,17 @@ class TextDocumentEdit implements ToJsonable {
? OptionalVersionedTextDocumentIdentifier.fromJson(json['textDocument'])
: null;
final edits = json['edits']
?.map((item) => TextEdit.canParse(item, nullLspJsonReporter)
? Either2<TextEdit, AnnotatedTextEdit>.t1(
item != null ? TextEdit.fromJson(item) : null)
?.map((item) => SnippetTextEdit.canParse(item, nullLspJsonReporter)
? Either3<SnippetTextEdit, AnnotatedTextEdit, TextEdit>.t1(
item != null ? SnippetTextEdit.fromJson(item) : null)
: (AnnotatedTextEdit.canParse(item, nullLspJsonReporter)
? Either2<TextEdit, AnnotatedTextEdit>.t2(
? Either3<SnippetTextEdit, AnnotatedTextEdit, TextEdit>.t2(
item != null ? AnnotatedTextEdit.fromJson(item) : null)
: (throw '''${item} was not one of (TextEdit, AnnotatedTextEdit)''')))
?.cast<Either2<TextEdit, AnnotatedTextEdit>>()
: (TextEdit.canParse(item, nullLspJsonReporter)
? Either3<SnippetTextEdit, AnnotatedTextEdit, TextEdit>.t3(
item != null ? TextEdit.fromJson(item) : null)
: (throw '''${item} was not one of (SnippetTextEdit, AnnotatedTextEdit, TextEdit)'''))))
?.cast<Either3<SnippetTextEdit, AnnotatedTextEdit, TextEdit>>()
?.toList();
return TextDocumentEdit(textDocument: textDocument, edits: edits);
}
@@ -30681,7 +30684,7 @@ class TextDocumentEdit implements ToJsonable {
/// The edits to be applied.
/// @since 3.16.0 - support for AnnotatedTextEdit. This is guarded by the
/// client capability `workspace.workspaceEdit.changeAnnotationSupport`
final List<Either2<TextEdit, AnnotatedTextEdit>> edits;
final List<Either3<SnippetTextEdit, AnnotatedTextEdit, TextEdit>> edits;
/// The text document to change.
final OptionalVersionedTextDocumentIdentifier textDocument;
@@ -30726,10 +30729,12 @@ class TextDocumentEdit implements ToJsonable {
return false;
}
if (!((obj['edits'] is List &&
(obj['edits'].every((item) => (TextEdit.canParse(item, reporter) ||
AnnotatedTextEdit.canParse(item, reporter))))))) {
(obj['edits'].every((item) =>
(SnippetTextEdit.canParse(item, reporter) ||
AnnotatedTextEdit.canParse(item, reporter) ||
TextEdit.canParse(item, reporter))))))) {
reporter.reportError(
'must be of type List<Either2<TextEdit, AnnotatedTextEdit>>');
'must be of type List<Either3<SnippetTextEdit, AnnotatedTextEdit, TextEdit>>');
return false;
}
} finally {
@@ -30749,8 +30754,9 @@ class TextDocumentEdit implements ToJsonable {
listEqual(
edits,
other.edits,
(Either2<TextEdit, AnnotatedTextEdit> a,
Either2<TextEdit, AnnotatedTextEdit> b) =>
(Either3<SnippetTextEdit, AnnotatedTextEdit, TextEdit> a,
Either3<SnippetTextEdit, AnnotatedTextEdit, TextEdit>
b) =>
a == b) &&
true;
}
@@ -31777,6 +31783,9 @@ class TextEdit implements ToJsonable {
if (AnnotatedTextEdit.canParse(json, nullLspJsonReporter)) {
return AnnotatedTextEdit.fromJson(json);
}
if (SnippetTextEdit.canParse(json, nullLspJsonReporter)) {
return SnippetTextEdit.fromJson(json);
}
final range = json['range'] != null ? Range.fromJson(json['range']) : null;
final newText = json['newText'];
return TextEdit(range: range, newText: newText);
@@ -78,6 +78,7 @@ class LspClientCapabilities {
final Set<SymbolKind> workspaceSymbolKinds;
final Set<CompletionItemKind> completionItemKinds;
final Set<InsertTextMode> completionInsertTextModes;
final bool experimentalSnippetTextEdit;
LspClientCapabilities(this.raw)
: applyEdit = raw?.workspace?.applyEdit ?? false,
@@ -125,7 +126,10 @@ class LspClientCapabilities {
workDoneProgress = raw.window?.workDoneProgress ?? false,
workspaceSymbolKinds = _listToSet(
raw?.workspace?.symbol?.symbolKind?.valueSet,
defaults: defaultSupportedSymbolKinds);
defaults: defaultSupportedSymbolKinds),
experimentalSnippetTextEdit =
raw.experimental is Map<String, dynamic> &&
raw.experimental['snippetTextEdit'] == true;
static Set<MarkupKind> _completionDocumentationFormats(
ClientCapabilities raw) {
@@ -87,7 +87,7 @@ class PerformRefactorCommandHandler extends SimpleEditCommandHandler {
return fileModifiedError;
}
final edit = createWorkspaceEdit(server, change.edits);
final edit = createWorkspaceEdit(server, change);
return await sendWorkspaceEditToClient(edit);
} on InconsistentAnalysisException {
return fileModifiedError;
@@ -143,7 +143,7 @@ class CodeActionHandler extends MessageHandler<CodeActionParams,
title: assist.change.message,
kind: toCodeActionKind(assist.change.id, CodeActionKind.Refactor),
diagnostics: const [],
edit: createWorkspaceEdit(server, assist.change.edits),
edit: createWorkspaceEdit(server, assist.change),
);
}
@@ -156,7 +156,7 @@ class CodeActionHandler extends MessageHandler<CodeActionParams,
title: fix.change.message,
kind: toCodeActionKind(fix.change.id, CodeActionKind.QuickFix),
diagnostics: [diagnostic],
edit: createWorkspaceEdit(server, fix.change.edits),
edit: createWorkspaceEdit(server, fix.change),
);
}
@@ -146,7 +146,8 @@ class CompletionResolveHandler
// a command that the client will call to apply those edits later.
Command command;
if (otherFilesChanges.isNotEmpty) {
final workspaceEdit = createWorkspaceEdit(server, otherFilesChanges);
final workspaceEdit =
createPlainWorkspaceEdit(server, otherFilesChanges);
command = Command(
title: 'Add import',
command: Commands.sendWorkspaceEdit,
@@ -187,7 +187,7 @@ class RenameHandler extends MessageHandler<RenameParams, WorkspaceEdit> {
return fileModifiedError;
}
final workspaceEdit = createWorkspaceEdit(server, change.edits);
final workspaceEdit = createWorkspaceEdit(server, change);
return success(workspaceEdit);
});
}
@@ -53,7 +53,7 @@ class WillRenameFilesHandler extends MessageHandler<RenameFilesParams, void> {
}
final change = await refactoring.createChange();
final edit = createWorkspaceEdit(server, change.edits);
final edit = createWorkspaceEdit(server, change);
return success(edit);
}
+91 -9
View File
@@ -108,10 +108,12 @@ String buildSnippetStringWithTabStops(
return output.join('');
}
/// Creates a [lsp.WorkspaceEdit] from simple [server.SourceFileEdit]s.
///
/// Note: This code will fetch the version of each document being modified so
/// 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.WorkspaceEdit createPlainWorkspaceEdit(
lsp.LspAnalysisServer server, List<server.SourceFileEdit> edits) {
return toWorkspaceEdit(
server.clientCapabilities,
@@ -126,6 +128,52 @@ lsp.WorkspaceEdit createWorkspaceEdit(
.toList());
}
/// Creates a [lsp.WorkspaceEdit] from a [server.SourceChange] that can include
/// experimental [server.SnippetTextEdit]s if the client has indicated support
/// for these in the experimental section of their client capabilities.
///
/// Note: This code will fetch the version of each document being modified so
/// 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, server.SourceChange change) {
// In order to return snippets, we must ensure we are only modifying a single
// existing file with a single edit and that there is a linked edit group with
// only one position and no suggestions.
if (!server.clientCapabilities.experimentalSnippetTextEdit ||
change.edits.length != 1 ||
change.edits.first.fileStamp == -1 || // new file
change.edits.first.edits.length != 1 ||
change.linkedEditGroups.isEmpty ||
change.linkedEditGroups.first.positions.length != 1 ||
change.linkedEditGroups.first.suggestions.isNotEmpty) {
return createPlainWorkspaceEdit(server, change.edits);
}
// Additionally, the selection must fall within the edit offset.
final edit = change.edits.first.edits.first;
final selectionOffset = change.linkedEditGroups.first.positions.first.offset;
final selectionLength = change.linkedEditGroups.first.length;
if (selectionOffset < edit.offset ||
selectionOffset + selectionLength > edit.offset + edit.length) {
return createPlainWorkspaceEdit(server, change.edits);
}
return toWorkspaceEdit(
server.clientCapabilities,
change.edits
.map((e) => FileEditInformation(
server.getVersionedDocumentIdentifier(e.file),
server.getLineInfo(e.file),
e.edits,
selectionOffsetRelative: selectionOffset - edit.offset,
selectionLength: selectionLength,
newFile: e.fileStamp == -1,
))
.toList());
}
lsp.CompletionItemKind declarationKindToCompletionItemKind(
Set<lsp.CompletionItemKind> supportedCompletionKinds,
dec.DeclarationKind kind,
@@ -1224,6 +1272,21 @@ lsp.SignatureHelp toSignatureHelp(Set<lsp.MarkupKind> preferredFormats,
);
}
lsp.SnippetTextEdit toSnippetTextEdit(
LspClientCapabilities capabilities,
server.LineInfo lineInfo,
server.SourceEdit edit,
int selectionOffsetRelative,
int selectionLength) {
assert(selectionOffsetRelative != null);
return lsp.SnippetTextEdit(
insertTextFormat: lsp.InsertTextFormat.Snippet,
range: toRange(lineInfo, edit.offset, edit.length),
newText: buildSnippetStringWithTabStops(
edit.replacement, [selectionOffsetRelative, selectionLength ?? 0]),
);
}
ErrorOr<server.SourceRange> toSourceRange(
server.LineInfo lineInfo, Range range) {
if (range == null) {
@@ -1247,14 +1310,33 @@ ErrorOr<server.SourceRange> toSourceRange(
return success(server.SourceRange(startOffset, endOffset - startOffset));
}
lsp.TextDocumentEdit toTextDocumentEdit(FileEditInformation edit) {
lsp.TextDocumentEdit toTextDocumentEdit(
LspClientCapabilities capabilities, FileEditInformation edit) {
return lsp.TextDocumentEdit(
textDocument: edit.doc,
edits: edit.edits
.map((e) => Either2<lsp.TextEdit, lsp.AnnotatedTextEdit>.t1(
toTextEdit(edit.lineInfo, e)))
.toList(),
);
textDocument: edit.doc,
edits: edit.edits
.map((e) => toTextDocumentEditEdit(capabilities, edit.lineInfo, e,
selectionOffsetRelative: edit.selectionOffsetRelative,
selectionLength: edit.selectionLength))
.toList());
}
Either3<lsp.SnippetTextEdit, lsp.AnnotatedTextEdit, lsp.TextEdit>
toTextDocumentEditEdit(
LspClientCapabilities capabilities,
server.LineInfo lineInfo,
server.SourceEdit edit, {
int selectionOffsetRelative,
int selectionLength,
}) {
if (!capabilities.experimentalSnippetTextEdit ||
selectionOffsetRelative == null) {
return Either3<lsp.SnippetTextEdit, lsp.AnnotatedTextEdit, lsp.TextEdit>.t3(
toTextEdit(lineInfo, edit));
}
return Either3<lsp.SnippetTextEdit, lsp.AnnotatedTextEdit, lsp.TextEdit>.t1(
toSnippetTextEdit(capabilities, lineInfo, edit, selectionOffsetRelative,
selectionLength));
}
lsp.TextEdit toTextEdit(server.LineInfo lineInfo, server.SourceEdit edit) {
@@ -1286,7 +1368,7 @@ lsp.WorkspaceEdit toWorkspaceEdit(
changes.add(createUnion);
}
final textDocEdit = toTextDocumentEdit(edit);
final textDocEdit = toTextDocumentEdit(capabilities, edit);
final textDocEditUnion = Either4<lsp.TextDocumentEdit, lsp.CreateFile,
lsp.RenameFile, lsp.DeleteFile>.t1(textDocEdit);
changes.add(textDocEditUnion);
@@ -307,6 +307,12 @@ class FileEditInformation {
final List<server.SourceEdit> edits;
final bool newFile;
/// The selection offset, relative to the edit.
final int selectionOffsetRelative;
final int selectionLength;
FileEditInformation(this.doc, this.lineInfo, this.edits,
{this.newFile = false});
{this.newFile = false,
this.selectionOffsetRelative,
this.selectionLength});
}
@@ -2,7 +2,9 @@
// 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_custom_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
@@ -16,6 +18,15 @@ void main() {
@reflectiveTest
class AssistsCodeActionsTest extends AbstractCodeActionsTest {
@override
void setUp() {
super.setUp();
writePackageConfig(
projectFolderPath,
flutter: true,
);
}
Future<void> test_appliesCorrectEdits_withDocumentChangesSupport() async {
// This code should get an assist to add a show combinator.
const content = '''
@@ -107,4 +118,171 @@ class AssistsCodeActionsTest extends AbstractCodeActionsTest {
await getCodeActions(pubspecFileUri.toString(), range: startOfDocRange);
expect(codeActions, isEmpty);
}
Future<void> test_snippetTextEdits_supported() async {
// This tests experimental support for including Snippets in TextEdits.
// https://github.com/rust-analyzer/rust-analyzer/blob/b35559a2460e7f0b2b79a7029db0c5d4e0acdb44/docs/dev/lsp-extensions.md#snippet-textedit
//
// This allows setting the cursor position/selection in TextEdits included
// in CodeActions, for example Flutter's "Wrap with widget" assist that
// should select the text "widget".
const content = '''
import 'package:flutter/widgets.dart';
build() {
return Container(
child: Row(
children: [^
Text('111'),
Text('222'),
Container(),
],
),
);
}
''';
// For testing, the snippet will be inserted literally into the text, as
// this requires some magic on the client. The expected text should therefore
// contain the snippets in the standard format.
const expectedContent = r'''
import 'package:flutter/widgets.dart';
build() {
return Container(
child: Row(
children: [
${0:widget}(
children: [
Text('111'),
Text('222'),
Container(),
],
),
],
),
);
}
''';
newFile(mainFilePath, content: withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.Refactor]),
workspaceCapabilities:
withDocumentChangesSupport(emptyWorkspaceClientCapabilities),
experimentalCapabilities: {
'snippetTextEdit': true,
},
);
final marker = positionFromMarker(content);
final codeActions = await getCodeActions(mainFileUri.toString(),
range: Range(start: marker, end: marker));
final assist = findEditAction(codeActions,
CodeActionKind('refactor.flutter.wrap.generic'), 'Wrap with widget...');
// Ensure the edit came back, and using documentChanges.
expect(assist, isNotNull);
expect(assist.edit.documentChanges, isNotNull);
expect(assist.edit.changes, isNull);
// Ensure applying the changes will give us the expected content.
final contents = {
mainFilePath: withoutMarkers(content),
};
applyDocumentChanges(contents, assist.edit.documentChanges);
expect(contents[mainFilePath], equals(expectedContent));
// Also ensure there was a single edit that was correctly marked
// as a SnippetTextEdit.
final textEdits = _extractTextDocumentEdits(assist.edit.documentChanges)
.expand((tde) => tde.edits)
.map((edit) => edit.map(
(e) => e,
(e) => throw 'Expected SnippetTextEdit, got AnnotatedTextEdit',
(e) => throw 'Expected SnippetTextEdit, got TextEdit',
))
.toList();
expect(textEdits, hasLength(1));
expect(textEdits.first.insertTextFormat, equals(InsertTextFormat.Snippet));
}
Future<void> test_snippetTextEdits_unsupported() async {
// This tests experimental support for including Snippets in TextEdits
// is not active when the client capabilities do not advertise support for it.
// https://github.com/rust-analyzer/rust-analyzer/blob/b35559a2460e7f0b2b79a7029db0c5d4e0acdb44/docs/dev/lsp-extensions.md#snippet-textedit
const content = '''
import 'package:flutter/widgets.dart';
build() {
return Container(
child: Row(
children: [^
Text('111'),
Text('222'),
Container(),
],
),
);
}
''';
newFile(mainFilePath, content: withoutMarkers(content));
await initialize(
textDocumentCapabilities: withCodeActionKinds(
emptyTextDocumentClientCapabilities, [CodeActionKind.Refactor]),
workspaceCapabilities:
withDocumentChangesSupport(emptyWorkspaceClientCapabilities),
);
final marker = positionFromMarker(content);
final codeActions = await getCodeActions(mainFileUri.toString(),
range: Range(start: marker, end: marker));
final assist = findEditAction(codeActions,
CodeActionKind('refactor.flutter.wrap.generic'), 'Wrap with widget...');
// Ensure the edit came back, and using documentChanges.
expect(assist, isNotNull);
expect(assist.edit.documentChanges, isNotNull);
expect(assist.edit.changes, isNull);
// Extract just TextDocumentEdits, create/rename/delete are not relevant.
final textDocumentEdits =
_extractTextDocumentEdits(assist.edit.documentChanges);
final textEdits = textDocumentEdits
.expand((tde) => tde.edits)
.map((edit) => edit.map((e) => e, (e) => e, (e) => e))
.toList();
// Ensure the edit does _not_ have a format of Snippet, nor does it include
// any $ characters that would indicate snippet text.
for (final edit in textEdits) {
expect(edit, isNot(TypeMatcher<SnippetTextEdit>()));
expect(edit.newText, isNot(contains(r'$')));
}
}
List<TextDocumentEdit> _extractTextDocumentEdits(
Either2<
List<TextDocumentEdit>,
List<
Either4<TextDocumentEdit, CreateFile, RenameFile,
DeleteFile>>>
documentChanges) =>
documentChanges.map(
// Already TextDocumentEdits
(edits) => edits,
// Extract TextDocumentEdits from union of resource changes
(changes) => changes
.map(
(change) => change.map(
(textDocEdit) => textDocEdit,
(create) => null,
(rename) => null,
(delete) => null,
),
)
.where((e) => e != null)
.toList(),
);
}
@@ -650,10 +650,10 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin {
});
}
String applyTextEdit(
String content, Either2<TextEdit, AnnotatedTextEdit> change) {
String applyTextEdit(String content,
Either3<SnippetTextEdit, AnnotatedTextEdit, TextEdit> change) {
// Both sites of the union can cast to TextEdit.
final edit = change.map((e) => e, (e) => e);
final edit = change.map((e) => e, (e) => e, (e) => e);
final startPos = edit.range.start;
final endPos = edit.range.end;
final lineInfo = LineInfo.fromContent(content);
@@ -714,8 +714,8 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin {
);
for (final change in sortedChanges) {
newContent = applyTextEdit(
newContent, Either2<TextEdit, AnnotatedTextEdit>.t1(change));
newContent = applyTextEdit(newContent,
Either3<SnippetTextEdit, AnnotatedTextEdit, TextEdit>.t3(change));
}
return newContent;
@@ -1217,6 +1217,7 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin {
TextDocumentClientCapabilities textDocumentCapabilities,
ClientCapabilitiesWorkspace workspaceCapabilities,
ClientCapabilitiesWindow windowCapabilities,
Map<String, Object> experimentalCapabilities,
Map<String, Object> initializationOptions,
bool throwOnFailure = true,
bool allowEmptyRootUri = false,
@@ -1225,6 +1226,7 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin {
workspace: workspaceCapabilities,
textDocument: textDocumentCapabilities,
window: windowCapabilities,
experimental: experimentalCapabilities,
);
// Handle any standard incoming requests that aren't test-specific, for example
@@ -272,6 +272,26 @@ List<AstNode> getCustomClasses() {
],
baseType: 'CompletionItemResolutionInfo',
),
// Custom types for experimental SnippetTextEdits
// https://github.com/rust-analyzer/rust-analyzer/blob/b35559a2460e7f0b2b79a7029db0c5d4e0acdb44/docs/dev/lsp-extensions.md#snippet-textedit
interface(
'SnippetTextEdit',
[
field('insertTextFormat', type: 'InsertTextFormat'),
],
baseType: 'TextEdit',
),
TypeAlias(
null,
Token.identifier('TextDocumentEditEdits'),
ArrayType(
UnionType([
Type.identifier('SnippetTextEdit'),
Type.identifier('AnnotatedTextEdit'),
Type.identifier('TextEdit'),
]),
),
)
];
return customTypes;
}
@@ -124,6 +124,9 @@ String getImprovedType(String interfaceName, String fieldName) {
},
'ServerCapabilities': {
'changeNotifications': 'bool',
},
'TextDocumentEdit': {
'edits': 'TextDocumentEditEdits',
}
};