Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat) Semantic document highlight #1408

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
71f285c
ts, html, css
jasonlyu123 Mar 23, 2021
86d7689
Merge branch 'master' of https://github.com/sveltejs/language-tools i…
jasonlyu123 Apr 21, 2021
62f5cc3
WIP
jasonlyu123 Apr 21, 2021
5f6070d
Merge branch 'master' of https://github.com/sveltejs/language-tools i…
jasonlyu123 Feb 22, 2022
376b24a
fast forward
jasonlyu123 Feb 23, 2022
c2f7afa
general svelte blocks and tag
jasonlyu123 Mar 1, 2022
a9111cf
only same file highlights
jasonlyu123 Mar 1, 2022
a191b1c
else highlights for if block
jasonlyu123 Mar 1, 2022
824dca2
await block
jasonlyu123 Mar 2, 2022
417eb12
Merge branch 'master' of https://github.com/sveltejs/language-tools i…
jasonlyu123 Mar 3, 2022
c83869d
highlight in style attribute
jasonlyu123 Mar 3, 2022
c665124
exclude unsupported
jasonlyu123 Mar 3, 2022
f409412
don't filter out highlights if original is not in it
jasonlyu123 Mar 3, 2022
afefc49
config
jasonlyu123 Mar 8, 2022
98892a1
ts, html and css test
jasonlyu123 Mar 8, 2022
68582bd
svelte test
jasonlyu123 Mar 9, 2022
ac2147c
format
jasonlyu123 Mar 9, 2022
6ca9eb7
merge two utils files
jasonlyu123 Mar 9, 2022
fe105df
revert unnecessary changes
jasonlyu123 Mar 9, 2022
d2c6933
Merge branch 'master' of https://github.com/sveltejs/language-tools i…
jasonlyu123 Jan 16, 2024
0ec1b8e
single config, mark as experimental
jasonlyu123 Jan 16, 2024
fc193fd
move the svelte highlight to ts using ast returned by svelte2tsx
jasonlyu123 Jan 17, 2024
a6ce555
config description
jasonlyu123 Jan 17, 2024
27b5122
word based highlight for unsupported languages
jasonlyu123 Jan 17, 2024
4802f6d
Merge branch 'master' of https://github.com/sveltejs/language-tools i…
jasonlyu123 Oct 12, 2024
9a5ca9f
format
jasonlyu123 Oct 12, 2024
d47f48d
ignore for svelte 5
jasonlyu123 Oct 12, 2024
8971042
fix svelte 5 issues. workaround the await block issue and fixing it s…
jasonlyu123 Oct 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/language-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,10 @@ Enable selection range for Svelte. _Default_: `true`

The default language to use when generating new script tags in Svelte. _Default_: `none`

#### `svelte.experimental.documentHighlight.enable`

Enable experimental document highlight support. Require restart. _Default_: `false`

## 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)
);
}
18 changes: 18 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 @@ -657,6 +658,23 @@ 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'
);
}

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,6 +52,10 @@ 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';

// 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
Expand All @@ -60,6 +66,7 @@ export class CSSPlugin
ColorPresentationsProvider,
DocumentSymbolsProvider,
SelectionRangeProvider,
DocumentHighlightProvider,
FoldingRangeProvider
{
__name = 'css';
Expand Down Expand Up @@ -383,7 +390,6 @@ export class CSSPlugin
}

const cssDocument = this.getCSSDoc(document);

if (shouldUseIndentBasedFolding(cssDocument.languageId)) {
return this.nonSyntacticFolding(document, document.styleInfo);
}
Expand Down Expand Up @@ -436,6 +442,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 @@ -530,6 +578,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 @@ -403,6 +410,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
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<!--';

Expand All @@ -36,16 +36,12 @@ export function getCompletions(
): CompletionList | null {
const offset = svelteDoc.offsetAt(position);

const isInStyleOrScript =
isInTag(position, svelteDoc.style) ||
isInTag(position, svelteDoc.script) ||
isInTag(position, svelteDoc.moduleScript);
const lastCharactersBeforePosition = svelteDoc
.getText()
// use last 10 characters, should cover 99% of all cases
.substr(Math.max(offset - 10, 0), Math.min(offset, 10));
const precededByOpeningBracket = /[\s\S]*{\s*[#:/@]\w*$/.test(lastCharactersBeforePosition);
if (isInStyleOrScript) {
if (inStyleOrScript(svelteDoc, position)) {
return null;
}

Expand Down
Loading