diff --git a/packages/language-server/README.md b/packages/language-server/README.md index 9372d9687..0862842b9 100644 --- a/packages/language-server/README.md +++ b/packages/language-server/README.md @@ -289,6 +289,10 @@ Whether or not to show a code lens at the top of Svelte files indicating if they The default language to use when generating new script tags in Svelte. _Default_: `none` +#### `svelte.plugin.svelte.documentHighlight.enable` + +Enable document highlight support. Requires a restart. _Default_: `true` + ## Credits - [James Birtles](https://github.com/jamesbirtles) for creating the foundation which this language server is built on diff --git a/packages/language-server/src/lib/documentHighlight/wordHighlight.ts b/packages/language-server/src/lib/documentHighlight/wordHighlight.ts new file mode 100644 index 000000000..8e90a6f35 --- /dev/null +++ b/packages/language-server/src/lib/documentHighlight/wordHighlight.ts @@ -0,0 +1,89 @@ +import { + DocumentHighlight, + DocumentHighlightKind, + Position, + Range +} from 'vscode-languageserver-types'; +import { Document, TagInformation } from '../documents'; + +export function wordHighlightForTag( + document: Document, + position: Position, + tag: TagInformation | null, + wordPattern: RegExp +): DocumentHighlight[] | null { + if (!tag || tag.start === tag.end) { + return null; + } + + const offset = document.offsetAt(position); + + const text = document.getText(); + if ( + offset < tag.start || + offset > tag.end || + // empty before and after the cursor + !text.slice(offset - 1, offset + 1).trim() + ) { + return null; + } + + const word = wordAt(document, position, wordPattern); + if (!word) { + return null; + } + + const searching = document.getText().slice(tag.start, tag.end); + + const highlights: DocumentHighlight[] = []; + + let index = 0; + while (index < searching.length) { + index = searching.indexOf(word, index); + if (index === -1) { + break; + } + + const start = tag.start + index; + highlights.push({ + range: { + start: document.positionAt(start), + end: document.positionAt(start + word.length) + }, + kind: DocumentHighlightKind.Text + }); + + index += word.length; + } + + return highlights; +} + +function wordAt(document: Document, position: Position, wordPattern: RegExp): string | null { + const line = document + .getText( + Range.create(Position.create(position.line, 0), Position.create(position.line + 1, 0)) + ) + .trimEnd(); + + wordPattern.lastIndex = 0; + + let start: number | undefined; + let end: number | undefined; + const matchEnd = Math.min(position.character, line.length); + while (wordPattern.lastIndex < matchEnd) { + const match = wordPattern.exec(line); + if (!match) { + break; + } + + start = match.index; + end = match.index + match[0].length; + } + + if (start === undefined || end === undefined || end < position.character) { + return null; + } + + return line.slice(start, end); +} diff --git a/packages/language-server/src/lib/documents/utils.ts b/packages/language-server/src/lib/documents/utils.ts index ff8b4c016..66043af0e 100644 --- a/packages/language-server/src/lib/documents/utils.ts +++ b/packages/language-server/src/lib/documents/utils.ts @@ -453,3 +453,11 @@ export function isInsideMoustacheTag(html: string, tagStart: number | null, posi return charactersInNode.lastIndexOf('{') > charactersInNode.lastIndexOf('}'); } } + +export function inStyleOrScript(document: Document, position: Position) { + return ( + isInTag(position, document.styleInfo) || + isInTag(position, document.scriptInfo) || + isInTag(position, document.moduleScriptInfo) + ); +} diff --git a/packages/language-server/src/plugins/PluginHost.ts b/packages/language-server/src/plugins/PluginHost.ts index d1613134f..8a4c3974d 100644 --- a/packages/language-server/src/plugins/PluginHost.ts +++ b/packages/language-server/src/plugins/PluginHost.ts @@ -16,6 +16,7 @@ import { CompletionList, DefinitionLink, Diagnostic, + DocumentHighlight, FoldingRange, FormattingOptions, Hover, @@ -677,6 +678,25 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { ); } + findDocumentHighlight( + textDocument: TextDocumentIdentifier, + position: Position + ): Promise { + const document = this.getDocument(textDocument.uri); + if (!document) { + throw new Error('Cannot call methods on an unopened document'); + } + + return ( + this.execute( + 'findDocumentHighlight', + [document, position], + ExecuteMode.FirstNonNull, + 'high' + ) ?? [] // fall back to empty array to prevent fallback to word-based highlighting + ); + } + onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): void { for (const support of this.plugins) { support.onWatchFileChanges?.(onWatchFileChangesParas); diff --git a/packages/language-server/src/plugins/css/CSSPlugin.ts b/packages/language-server/src/plugins/css/CSSPlugin.ts index d5a38d427..f89a6285e 100644 --- a/packages/language-server/src/plugins/css/CSSPlugin.ts +++ b/packages/language-server/src/plugins/css/CSSPlugin.ts @@ -14,6 +14,7 @@ import { CompletionItem, CompletionItemKind, SelectionRange, + DocumentHighlight, WorkspaceFolder } from 'vscode-languageserver'; import { @@ -36,6 +37,7 @@ import { CompletionsProvider, DiagnosticsProvider, DocumentColorsProvider, + DocumentHighlightProvider, DocumentSymbolsProvider, FoldingRangeProvider, HoverProvider, @@ -50,8 +52,12 @@ import { StyleAttributeDocument } from './StyleAttributeDocument'; import { getDocumentContext } from '../documentContext'; import { FoldingRange, FoldingRangeKind } from 'vscode-languageserver-types'; import { indentBasedFoldingRangeForTag } from '../../lib/foldingRange/indentFolding'; +import { wordHighlightForTag } from '../../lib/documentHighlight/wordHighlight'; import { isNotNullOrUndefined, urlToPath } from '../../utils'; +// https://github.com/microsoft/vscode/blob/c6f507deeb99925e713271b1048f21dbaab4bd54/extensions/css/language-configuration.json#L34 +const wordPattern = /(#?-?\d*\.\d\w*%?)|(::?[\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\w-?]+%?|[@#!.])/g; + export class CSSPlugin implements HoverProvider, @@ -61,6 +67,7 @@ export class CSSPlugin ColorPresentationsProvider, DocumentSymbolsProvider, SelectionRangeProvider, + DocumentHighlightProvider, FoldingRangeProvider { __name = 'css'; @@ -388,7 +395,6 @@ export class CSSPlugin } const cssDocument = this.getCSSDoc(document); - if (shouldUseIndentBasedFolding(cssDocument.languageId)) { return this.nonSyntacticFolding(document, document.styleInfo); } @@ -441,6 +447,48 @@ export class CSSPlugin return ranges.sort((a, b) => a.startLine - b.startLine); } + findDocumentHighlight(document: Document, position: Position): DocumentHighlight[] | null { + const cssDocument = this.getCSSDoc(document); + if (cssDocument.isInGenerated(position)) { + if (shouldExcludeDocumentHighlights(cssDocument)) { + return wordHighlightForTag(document, position, document.styleInfo, wordPattern); + } + + return this.findDocumentHighlightInternal(cssDocument, position); + } + + const attributeContext = getAttributeContextAtPosition(document, position); + if ( + attributeContext && + this.inStyleAttributeWithoutInterpolation(attributeContext, document.getText()) + ) { + const [start, end] = attributeContext.valueRange; + return this.findDocumentHighlightInternal( + new StyleAttributeDocument(document, start, end, this.cssLanguageServices), + position + ); + } + + return null; + } + + private findDocumentHighlightInternal( + cssDocument: CSSDocumentBase, + position: Position + ): DocumentHighlight[] | null { + const kind = extractLanguage(cssDocument); + + const result = getLanguageService(this.cssLanguageServices, kind) + .findDocumentHighlights( + cssDocument, + cssDocument.getGeneratedPosition(position), + cssDocument.stylesheet + ) + .map((highlight) => mapObjWithRangeToOriginal(cssDocument, highlight)); + + return result; + } + private getCSSDoc(document: Document) { let cssDoc = this.cssDocuments.get(document); if (!cssDoc || cssDoc.version < document.version) { @@ -535,6 +583,18 @@ function shouldUseIndentBasedFolding(kind?: string) { } } +function shouldExcludeDocumentHighlights(document: CSSDocumentBase) { + switch (extractLanguage(document)) { + case 'postcss': + case 'sass': + case 'stylus': + case 'styl': + return true; + default: + return false; + } +} + function isSASS(document: CSSDocumentBase) { switch (extractLanguage(document)) { case 'sass': diff --git a/packages/language-server/src/plugins/html/HTMLPlugin.ts b/packages/language-server/src/plugins/html/HTMLPlugin.ts index ca830b3e2..75d1c3193 100644 --- a/packages/language-server/src/plugins/html/HTMLPlugin.ts +++ b/packages/language-server/src/plugins/html/HTMLPlugin.ts @@ -18,7 +18,8 @@ import { WorkspaceEdit, LinkedEditingRanges, CompletionContext, - FoldingRange + FoldingRange, + DocumentHighlight } from 'vscode-languageserver'; import { DocumentManager, @@ -33,7 +34,8 @@ import { CompletionsProvider, RenameProvider, LinkedEditingRangesProvider, - FoldingRangeProvider + FoldingRangeProvider, + DocumentHighlightProvider } from '../interfaces'; import { isInsideMoustacheTag, toRange } from '../../lib/documents/utils'; import { isNotNullOrUndefined, possiblyComponent } from '../../utils'; @@ -41,6 +43,10 @@ import { importPrettier } from '../../importPackage'; import path from 'path'; import { Logger } from '../../logger'; import { indentBasedFoldingRangeForTag } from '../../lib/foldingRange/indentFolding'; +import { wordHighlightForTag } from '../../lib/documentHighlight/wordHighlight'; + +// https://github.com/microsoft/vscode/blob/c6f507deeb99925e713271b1048f21dbaab4bd54/extensions/html/language-configuration.json#L34 +const wordPattern = /(-?\d*\.\d\w*)|([^`~!@$^&*()=+[{\]}\|;:'",.<>\/\s]+)/g; export class HTMLPlugin implements @@ -48,7 +54,8 @@ export class HTMLPlugin CompletionsProvider, RenameProvider, LinkedEditingRangesProvider, - FoldingRangeProvider + FoldingRangeProvider, + DocumentHighlightProvider { __name = 'html'; private lang = getLanguageService({ @@ -409,6 +416,36 @@ export class HTMLPlugin return result.concat(templateRange); } + findDocumentHighlight(document: Document, position: Position): DocumentHighlight[] | null { + const html = this.documents.get(document); + if (!html) { + return null; + } + + const templateResult = wordHighlightForTag( + document, + position, + document.templateInfo, + wordPattern + ); + + if (templateResult) { + return templateResult; + } + + const node = html.findNodeAt(document.offsetAt(position)); + if (possiblyComponent(node)) { + return null; + } + const result = this.lang.findDocumentHighlights(document, position, html); + + if (!result.length) { + return null; + } + + return result; + } + /** * Returns true if rename happens at the tag name, not anywhere inbetween. */ diff --git a/packages/language-server/src/plugins/interfaces.ts b/packages/language-server/src/plugins/interfaces.ts index 7570c727c..73c7bbbbd 100644 --- a/packages/language-server/src/plugins/interfaces.ts +++ b/packages/language-server/src/plugins/interfaces.ts @@ -21,6 +21,7 @@ import { CompletionList, DefinitionLink, Diagnostic, + DocumentHighlight, FoldingRange, FormattingOptions, Hover, @@ -243,6 +244,13 @@ export interface FoldingRangeProvider { getFoldingRanges(document: Document): Resolvable; } +export interface DocumentHighlightProvider { + findDocumentHighlight( + document: Document, + position: Position + ): Resolvable; +} + export interface OnWatchFileChanges { onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): void; } @@ -273,7 +281,8 @@ type ProviderBase = DiagnosticsProvider & InlayHintProvider & CallHierarchyProvider & FoldingRangeProvider & - CodeLensProvider; + CodeLensProvider & + DocumentHighlightProvider; export type LSProvider = ProviderBase & BackwardsCompatibleDefinitionsProvider; diff --git a/packages/language-server/src/plugins/svelte/features/getCompletions.ts b/packages/language-server/src/plugins/svelte/features/getCompletions.ts index eefe5c3f7..8a4f0d5c0 100644 --- a/packages/language-server/src/plugins/svelte/features/getCompletions.ts +++ b/packages/language-server/src/plugins/svelte/features/getCompletions.ts @@ -9,10 +9,10 @@ import { MarkupKind } from 'vscode-languageserver'; import { SvelteTag, documentation, getLatestOpeningTag } from './SvelteTags'; -import { isInTag, Document } from '../../../lib/documents'; +import { Document } from '../../../lib/documents'; import { AttributeContext, getAttributeContextAtPosition } from '../../../lib/documents/parseHtml'; import { getModifierData } from './getModifierData'; -import { attributeCanHaveEventModifier } from './utils'; +import { attributeCanHaveEventModifier, inStyleOrScript } from './utils'; const HTML_COMMENT_START = '