Skip to content

Commit

Permalink
feat: Semantic document highlight (#1408)
Browse files Browse the repository at this point in the history
* ts, html, css

* WIP

* fast forward

* general svelte blocks and tag

* only same file highlights

* else highlights for if block

* await block

* highlight in style attribute

* exclude unsupported

* don't filter out highlights if original is not in it
function and class highlght its name

* config

* ts, html and css test

* svelte test

* format

* merge two utils files

* revert unnecessary changes

* single config, mark as experimental

* move the svelte highlight to ts using ast returned by svelte2tsx

* config description

* word based highlight for unsupported languages

* format

* ignore for svelte 5

* fix svelte 5 issues. workaround the await block issue and fixing it separately

* Apply suggestions from code review

Co-authored-by: Simon H <[email protected]>

* prevent word-base fallback

* fix nested if, if block without else if

* remove experimental
keep the config so people can still go back to the word base highlight

* fix svelte 4 again

* fix

* Update packages/language-server/src/plugins/PluginHost.ts

* fix the fix

---------

Co-authored-by: Simon H <[email protected]>
Co-authored-by: Simon Holthausen <[email protected]>
  • Loading branch information
3 people authored Jan 8, 2025
1 parent 646f2e6 commit 931dd85
Show file tree
Hide file tree
Showing 20 changed files with 1,023 additions and 33 deletions.
4 changes: 4 additions & 0 deletions packages/language-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
8 changes: 8 additions & 0 deletions packages/language-server/src/lib/documents/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}
20 changes: 20 additions & 0 deletions packages/language-server/src/plugins/PluginHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
CompletionList,
DefinitionLink,
Diagnostic,
DocumentHighlight,
FoldingRange,
FormattingOptions,
Hover,
Expand Down Expand Up @@ -677,6 +678,25 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
);
}

findDocumentHighlight(
textDocument: TextDocumentIdentifier,
position: Position
): Promise<DocumentHighlight[] | null> {
const document = this.getDocument(textDocument.uri);
if (!document) {
throw new Error('Cannot call methods on an unopened document');
}

return (
this.execute<DocumentHighlight[] | null>(
'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);
Expand Down
62 changes: 61 additions & 1 deletion packages/language-server/src/plugins/css/CSSPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
CompletionItem,
CompletionItemKind,
SelectionRange,
DocumentHighlight,
WorkspaceFolder
} from 'vscode-languageserver';
import {
Expand All @@ -36,6 +37,7 @@ import {
CompletionsProvider,
DiagnosticsProvider,
DocumentColorsProvider,
DocumentHighlightProvider,
DocumentSymbolsProvider,
FoldingRangeProvider,
HoverProvider,
Expand All @@ -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,
Expand All @@ -61,6 +67,7 @@ export class CSSPlugin
ColorPresentationsProvider,
DocumentSymbolsProvider,
SelectionRangeProvider,
DocumentHighlightProvider,
FoldingRangeProvider
{
__name = 'css';
Expand Down Expand Up @@ -388,7 +395,6 @@ export class CSSPlugin
}

const cssDocument = this.getCSSDoc(document);

if (shouldUseIndentBasedFolding(cssDocument.languageId)) {
return this.nonSyntacticFolding(document, document.styleInfo);
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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':
Expand Down
43 changes: 40 additions & 3 deletions packages/language-server/src/plugins/html/HTMLPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import {
WorkspaceEdit,
LinkedEditingRanges,
CompletionContext,
FoldingRange
FoldingRange,
DocumentHighlight
} from 'vscode-languageserver';
import {
DocumentManager,
Expand All @@ -33,22 +34,28 @@ import {
CompletionsProvider,
RenameProvider,
LinkedEditingRangesProvider,
FoldingRangeProvider
FoldingRangeProvider,
DocumentHighlightProvider
} from '../interfaces';
import { isInsideMoustacheTag, toRange } from '../../lib/documents/utils';
import { isNotNullOrUndefined, possiblyComponent } from '../../utils';
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
HoverProvider,
CompletionsProvider,
RenameProvider,
LinkedEditingRangesProvider,
FoldingRangeProvider
FoldingRangeProvider,
DocumentHighlightProvider
{
__name = 'html';
private lang = getLanguageService({
Expand Down Expand Up @@ -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.
*/
Expand Down
11 changes: 10 additions & 1 deletion packages/language-server/src/plugins/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
CompletionList,
DefinitionLink,
Diagnostic,
DocumentHighlight,
FoldingRange,
FormattingOptions,
Hover,
Expand Down Expand Up @@ -243,6 +244,13 @@ export interface FoldingRangeProvider {
getFoldingRanges(document: Document): Resolvable<FoldingRange[]>;
}

export interface DocumentHighlightProvider {
findDocumentHighlight(
document: Document,
position: Position
): Resolvable<DocumentHighlight[] | null>;
}

export interface OnWatchFileChanges {
onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): void;
}
Expand Down Expand Up @@ -273,7 +281,8 @@ type ProviderBase = DiagnosticsProvider &
InlayHintProvider &
CallHierarchyProvider &
FoldingRangeProvider &
CodeLensProvider;
CodeLensProvider &
DocumentHighlightProvider;

export type LSProvider = ProviderBase & BackwardsCompatibleDefinitionsProvider;

Expand Down
Loading

0 comments on commit 931dd85

Please sign in to comment.