diff --git a/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart b/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart index c67d3f769c7..d55238b96d7 100644 --- a/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart +++ b/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart @@ -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 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 toJson() { + var __result = {}; + __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) { + 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()); +} diff --git a/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart b/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart index 215bf08db5e..d3c551b65ad 100644 --- a/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart +++ b/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart @@ -30666,14 +30666,17 @@ class TextDocumentEdit implements ToJsonable { ? OptionalVersionedTextDocumentIdentifier.fromJson(json['textDocument']) : null; final edits = json['edits'] - ?.map((item) => TextEdit.canParse(item, nullLspJsonReporter) - ? Either2.t1( - item != null ? TextEdit.fromJson(item) : null) + ?.map((item) => SnippetTextEdit.canParse(item, nullLspJsonReporter) + ? Either3.t1( + item != null ? SnippetTextEdit.fromJson(item) : null) : (AnnotatedTextEdit.canParse(item, nullLspJsonReporter) - ? Either2.t2( + ? Either3.t2( item != null ? AnnotatedTextEdit.fromJson(item) : null) - : (throw '''${item} was not one of (TextEdit, AnnotatedTextEdit)'''))) - ?.cast>() + : (TextEdit.canParse(item, nullLspJsonReporter) + ? Either3.t3( + item != null ? TextEdit.fromJson(item) : null) + : (throw '''${item} was not one of (SnippetTextEdit, AnnotatedTextEdit, TextEdit)''')))) + ?.cast>() ?.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> edits; + final List> 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>'); + 'must be of type List>'); return false; } } finally { @@ -30749,8 +30754,9 @@ class TextDocumentEdit implements ToJsonable { listEqual( edits, other.edits, - (Either2 a, - Either2 b) => + (Either3 a, + Either3 + 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); diff --git a/pkg/analysis_server/lib/src/lsp/client_capabilities.dart b/pkg/analysis_server/lib/src/lsp/client_capabilities.dart index 2c16de49b77..10479613541 100644 --- a/pkg/analysis_server/lib/src/lsp/client_capabilities.dart +++ b/pkg/analysis_server/lib/src/lsp/client_capabilities.dart @@ -78,6 +78,7 @@ class LspClientCapabilities { final Set workspaceSymbolKinds; final Set completionItemKinds; final Set 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 && + raw.experimental['snippetTextEdit'] == true; static Set _completionDocumentationFormats( ClientCapabilities raw) { diff --git a/pkg/analysis_server/lib/src/lsp/handlers/commands/perform_refactor.dart b/pkg/analysis_server/lib/src/lsp/handlers/commands/perform_refactor.dart index 6ac454701f6..581c49979c9 100644 --- a/pkg/analysis_server/lib/src/lsp/handlers/commands/perform_refactor.dart +++ b/pkg/analysis_server/lib/src/lsp/handlers/commands/perform_refactor.dart @@ -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; diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart index f51367fb4d5..f7afd252d99 100644 --- a/pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart +++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart @@ -143,7 +143,7 @@ class CodeActionHandler extends MessageHandler { return fileModifiedError; } - final workspaceEdit = createWorkspaceEdit(server, change.edits); + final workspaceEdit = createWorkspaceEdit(server, change); return success(workspaceEdit); }); } diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_will_rename_files.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_will_rename_files.dart index 2beb369de3c..3d32e86eb19 100644 --- a/pkg/analysis_server/lib/src/lsp/handlers/handler_will_rename_files.dart +++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_will_rename_files.dart @@ -53,7 +53,7 @@ class WillRenameFilesHandler extends MessageHandler { } final change = await refactoring.createChange(); - final edit = createWorkspaceEdit(server, change.edits); + final edit = createWorkspaceEdit(server, change); return success(edit); } diff --git a/pkg/analysis_server/lib/src/lsp/mapping.dart b/pkg/analysis_server/lib/src/lsp/mapping.dart index 9e59ec6407b..a409c060fbd 100644 --- a/pkg/analysis_server/lib/src/lsp/mapping.dart +++ b/pkg/analysis_server/lib/src/lsp/mapping.dart @@ -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 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 supportedCompletionKinds, dec.DeclarationKind kind, @@ -1224,6 +1272,21 @@ lsp.SignatureHelp toSignatureHelp(Set 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 toSourceRange( server.LineInfo lineInfo, Range range) { if (range == null) { @@ -1247,14 +1310,33 @@ ErrorOr 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.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 + toTextDocumentEditEdit( + LspClientCapabilities capabilities, + server.LineInfo lineInfo, + server.SourceEdit edit, { + int selectionOffsetRelative, + int selectionLength, +}) { + if (!capabilities.experimentalSnippetTextEdit || + selectionOffsetRelative == null) { + return Either3.t3( + toTextEdit(lineInfo, edit)); + } + return Either3.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.t1(textDocEdit); changes.add(textDocEditUnion); diff --git a/pkg/analysis_server/lib/src/lsp/source_edits.dart b/pkg/analysis_server/lib/src/lsp/source_edits.dart index e1cc0139bbe..d97576f53ed 100644 --- a/pkg/analysis_server/lib/src/lsp/source_edits.dart +++ b/pkg/analysis_server/lib/src/lsp/source_edits.dart @@ -307,6 +307,12 @@ class FileEditInformation { final List 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}); } diff --git a/pkg/analysis_server/test/lsp/code_actions_assists_test.dart b/pkg/analysis_server/test/lsp/code_actions_assists_test.dart index 03def9f4ee2..b70a2c2a04a 100644 --- a/pkg/analysis_server/test/lsp/code_actions_assists_test.dart +++ b/pkg/analysis_server/test/lsp/code_actions_assists_test.dart @@ -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 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 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 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())); + expect(edit.newText, isNot(contains(r'$'))); + } + } + + List _extractTextDocumentEdits( + Either2< + List, + List< + Either4>> + 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(), + ); } diff --git a/pkg/analysis_server/test/lsp/server_abstract.dart b/pkg/analysis_server/test/lsp/server_abstract.dart index 2cf21e5658d..dd35465d293 100644 --- a/pkg/analysis_server/test/lsp/server_abstract.dart +++ b/pkg/analysis_server/test/lsp/server_abstract.dart @@ -650,10 +650,10 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin { }); } - String applyTextEdit( - String content, Either2 change) { + String applyTextEdit(String content, + Either3 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.t1(change)); + newContent = applyTextEdit(newContent, + Either3.t3(change)); } return newContent; @@ -1217,6 +1217,7 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin { TextDocumentClientCapabilities textDocumentCapabilities, ClientCapabilitiesWorkspace workspaceCapabilities, ClientCapabilitiesWindow windowCapabilities, + Map experimentalCapabilities, Map 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 diff --git a/pkg/analysis_server/tool/lsp_spec/generate_all.dart b/pkg/analysis_server/tool/lsp_spec/generate_all.dart index 782aa26620a..ace8ef14780 100644 --- a/pkg/analysis_server/tool/lsp_spec/generate_all.dart +++ b/pkg/analysis_server/tool/lsp_spec/generate_all.dart @@ -272,6 +272,26 @@ List 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; } diff --git a/pkg/analysis_server/tool/lsp_spec/typescript.dart b/pkg/analysis_server/tool/lsp_spec/typescript.dart index 575d66e9d5a..801b093062d 100644 --- a/pkg/analysis_server/tool/lsp_spec/typescript.dart +++ b/pkg/analysis_server/tool/lsp_spec/typescript.dart @@ -124,6 +124,9 @@ String getImprovedType(String interfaceName, String fieldName) { }, 'ServerCapabilities': { 'changeNotifications': 'bool', + }, + 'TextDocumentEdit': { + 'edits': 'TextDocumentEditEdits', } };