diff --git a/packages/language-server/src/ls-config.ts b/packages/language-server/src/ls-config.ts index 20769941b..a5fd41f7e 100644 --- a/packages/language-server/src/ls-config.ts +++ b/packages/language-server/src/ls-config.ts @@ -37,7 +37,8 @@ const defaultLSConfig: LSConfig = { completions: { enable: true, emmet: true }, tagComplete: { enable: true }, documentSymbols: { enable: true }, - renameTags: { enable: true } + renameTags: { enable: true }, + linkedEditing: { enable: true } }, svelte: { enable: true, @@ -156,6 +157,9 @@ export interface LSHTMLConfig { renameTags: { enable: boolean; }; + linkedEditing: { + enable: boolean; + }; } export type CompilerWarningsSettings = Record; diff --git a/packages/language-server/src/plugins/PluginHost.ts b/packages/language-server/src/plugins/PluginHost.ts index 3ab09fb9a..c32c47228 100644 --- a/packages/language-server/src/plugins/PluginHost.ts +++ b/packages/language-server/src/plugins/PluginHost.ts @@ -12,6 +12,7 @@ import { Diagnostic, FormattingOptions, Hover, + LinkedEditingRanges, Location, Position, Range, @@ -415,6 +416,22 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { ); } + async getLinkedEditingRanges( + textDocument: TextDocumentIdentifier, + position: Position + ): Promise { + const document = this.getDocument(textDocument.uri); + if (!document) { + throw new Error('Cannot call methods on an unopened document'); + } + + return await this.execute( + 'getLinkedEditingRanges', + [document, position], + ExecuteMode.FirstNonNull + ); + } + onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): void { for (const support of this.plugins) { support.onWatchFileChanges?.(onWatchFileChangesParas); diff --git a/packages/language-server/src/plugins/html/HTMLPlugin.ts b/packages/language-server/src/plugins/html/HTMLPlugin.ts index 65b0a9493..874376b95 100644 --- a/packages/language-server/src/plugins/html/HTMLPlugin.ts +++ b/packages/language-server/src/plugins/html/HTMLPlugin.ts @@ -14,7 +14,8 @@ import { CompletionItemKind, TextEdit, Range, - WorkspaceEdit + WorkspaceEdit, + LinkedEditingRanges } from 'vscode-languageserver'; import { DocumentManager, @@ -24,10 +25,16 @@ import { } from '../../lib/documents'; import { LSConfigManager, LSHTMLConfig } from '../../ls-config'; import { svelteHtmlDataProvider } from './dataProvider'; -import { HoverProvider, CompletionsProvider, RenameProvider } from '../interfaces'; +import { + HoverProvider, + CompletionsProvider, + RenameProvider, + LinkedEditingRangesProvider +} from '../interfaces'; import { isInsideMoustacheTag, toRange } from '../../lib/documents/utils'; -export class HTMLPlugin implements HoverProvider, CompletionsProvider, RenameProvider { +export class HTMLPlugin + implements HoverProvider, CompletionsProvider, RenameProvider, LinkedEditingRangesProvider { private configManager: LSConfigManager; private lang = getLanguageService({ customDataProviders: [svelteHtmlDataProvider], @@ -210,6 +217,7 @@ export class HTMLPlugin implements HoverProvider, CompletionsProvider, RenamePro if (!this.featureEnabled('renameTags')) { return null; } + const html = this.documents.get(document); if (!html) { return null; @@ -248,6 +256,25 @@ export class HTMLPlugin implements HoverProvider, CompletionsProvider, RenamePro return toRange(document.getText(), tagNameStart, tagNameStart + node.tag.length); } + getLinkedEditingRanges(document: Document, position: Position): LinkedEditingRanges | null { + if (!this.featureEnabled('linkedEditing')) { + return null; + } + + const html = this.documents.get(document); + if (!html) { + return null; + } + + const ranges = this.lang.findLinkedEditingRanges(document, position, html); + + if (!ranges) { + return null; + } + + return { ranges }; + } + /** * * The language service is case insensitive, and would provide diff --git a/packages/language-server/src/plugins/interfaces.ts b/packages/language-server/src/plugins/interfaces.ts index a4a7ac37d..a4d1122c5 100644 --- a/packages/language-server/src/plugins/interfaces.ts +++ b/packages/language-server/src/plugins/interfaces.ts @@ -1,6 +1,7 @@ import { CompletionContext, FileChangeType, + LinkedEditingRanges, SemanticTokens, SignatureHelpContext, TextDocumentContentChangeEvent @@ -151,6 +152,13 @@ export interface SemanticTokensProvider { getSemanticTokens(textDocument: Document, range?: Range): Resolvable; } +export interface LinkedEditingRangesProvider { + getLinkedEditingRanges( + document: Document, + position: Position + ): Resolvable; +} + export interface OnWatchFileChangesPara { fileName: string; changeType: FileChangeType; @@ -177,7 +185,8 @@ type ProviderBase = DiagnosticsProvider & FindReferencesProvider & RenameProvider & SignatureHelpProvider & - SemanticTokensProvider; + SemanticTokensProvider & + LinkedEditingRangesProvider; export type LSProvider = ProviderBase & BackwardsCompatibleDefinitionsProvider; diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 9b8d677d9..f1dbda7df 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -15,7 +15,8 @@ import { WorkspaceEdit, SemanticTokensRequest, SemanticTokensRangeRequest, - DidChangeWatchedFilesParams + DidChangeWatchedFilesParams, + LinkedEditingRangeRequest } from 'vscode-languageserver'; import { IPCMessageReader, IPCMessageWriter, createConnection } from 'vscode-languageserver/node'; import { DiagnosticsManager } from './lib/DiagnosticsManager'; @@ -203,7 +204,8 @@ export function startServer(options?: LSOptions) { legend: getSemanticTokenLegends(), range: true, full: true - } + }, + linkedEditingRangeProvider: true } }; }); @@ -329,6 +331,11 @@ export function startServer(options?: LSOptions) { pluginHost.getSemanticTokens(evt.textDocument, evt.range) ); + connection.onRequest( + LinkedEditingRangeRequest.type, + async (evt) => await pluginHost.getLinkedEditingRanges(evt.textDocument, evt.position) + ); + docManager.on( 'documentChange', _.debounce(async (document: Document) => diagnosticsManager.update(document), 500) diff --git a/packages/language-server/test/plugins/html/HTMLPlugin.test.ts b/packages/language-server/test/plugins/html/HTMLPlugin.test.ts index 938b17b8b..f1301c421 100644 --- a/packages/language-server/test/plugins/html/HTMLPlugin.test.ts +++ b/packages/language-server/test/plugins/html/HTMLPlugin.test.ts @@ -177,4 +177,16 @@ describe('HTML Plugin', () => { renameInfo ); }); + + it('provides linked editing ranges', async () => { + const { plugin, document } = setup('
'); + + const ranges = plugin.getLinkedEditingRanges(document, Position.create(0, 3)); + assert.deepStrictEqual(ranges, { + ranges: [ + { start: { line: 0, character: 1 }, end: { line: 0, character: 4 } }, + { start: { line: 0, character: 7 }, end: { line: 0, character: 10 } } + ] + }); + }); }); diff --git a/packages/svelte-vscode/README.md b/packages/svelte-vscode/README.md index 2f5dcb235..0f3c157cf 100644 --- a/packages/svelte-vscode/README.md +++ b/packages/svelte-vscode/README.md @@ -198,6 +198,10 @@ Enable HTML tag auto closing. _Default_: `true` Enable document symbols for HTML. _Default_: `true` +##### `svelte.plugin.html.linkedEditing.enable` + +Enable Linked Editing for HTML. _Default_: `true` + ##### `svelte.plugin.html.renameTags.enable` Enable rename tags for the opening/closing tag pairs in HTML. _Default_: `true` diff --git a/packages/svelte-vscode/package.json b/packages/svelte-vscode/package.json index c305a6d6f..9e60d091a 100644 --- a/packages/svelte-vscode/package.json +++ b/packages/svelte-vscode/package.json @@ -242,6 +242,12 @@ "title": "HTML: Symbols in Outline", "description": "Enable document symbols for HTML" }, + "svelte.plugin.html.linkedEditing.enable": { + "type": "boolean", + "default": true, + "title": "HTML: Linked Editing", + "description": "Enable Linked Editing for HTML" + }, "svelte.plugin.html.renameTags.enable": { "type": "boolean", "default": true,