vscode-gem-languageservice 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vscode-gem-languageservice",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Language service for Gem",
5
5
  "keywords": [
6
6
  "gem",
@@ -14,9 +14,13 @@
14
14
  ],
15
15
  "scripts": {},
16
16
  "dependencies": {
17
+ "@vscode/emmet-helper": "^2.9.3",
18
+ "duoyun-ui": "^2.2.0",
17
19
  "gem-analyzer": "^2.2.0",
18
20
  "ts-morph": "^13.0.0",
19
21
  "typescript": "^5.6.2",
22
+ "vscode-css-languageservice": "^6.3.1",
23
+ "vscode-html-languageservice": "^5.3.1",
20
24
  "vscode-languageserver": "^9.0.1",
21
25
  "vscode-languageserver-textdocument": "^1.0.12"
22
26
  },
package/src/cache.ts ADDED
@@ -0,0 +1,34 @@
1
+ import type { CompletionList } from 'vscode-languageserver';
2
+ import type { Position, TextDocument } from 'vscode-languageserver-textdocument';
3
+
4
+ export class CompletionsCache {
5
+ #cachedCompletionsFile?: string;
6
+ #cachedCompletionsPosition?: Position;
7
+ #cachedCompletionsContent?: string;
8
+ #completions?: CompletionList;
9
+
10
+ #equalPositions(left: Position, right?: Position) {
11
+ return !!right && left.line === right.line && left.character === right.character;
12
+ }
13
+
14
+ getCached(doc: TextDocument, position: Position) {
15
+ if (
16
+ this.#completions &&
17
+ doc.uri === this.#cachedCompletionsFile &&
18
+ this.#equalPositions(position, this.#cachedCompletionsPosition) &&
19
+ doc.getText() === this.#cachedCompletionsContent
20
+ ) {
21
+ return this.#completions;
22
+ }
23
+
24
+ return undefined;
25
+ }
26
+
27
+ updateCached(context: TextDocument, position: Position, completions: CompletionList) {
28
+ this.#cachedCompletionsFile = context.uri;
29
+ this.#cachedCompletionsPosition = position;
30
+ this.#cachedCompletionsContent = context.getText();
31
+ this.#completions = completions;
32
+ return completions;
33
+ }
34
+ }
package/src/color.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { rgbToHexColor, parseHexColor } from 'duoyun-ui/lib/color';
2
+ import { Range, Color } from 'vscode-languageserver/node';
3
+ import type { ColorInformation, ColorPresentation } from 'vscode-languageserver/node';
4
+ import type { HexColor } from 'duoyun-ui/lib/color';
5
+ import type { TextDocument } from 'vscode-languageserver-textdocument';
6
+
7
+ import { COLOR_REG } from './constants';
8
+
9
+ export class ColorProvider {
10
+ provideDocumentColors(document: TextDocument) {
11
+ COLOR_REG.exec('null');
12
+
13
+ const documentText = document.getText();
14
+ const colors: ColorInformation[] = [];
15
+
16
+ let match: RegExpExecArray | null;
17
+ while ((match = COLOR_REG.exec(documentText)) !== null) {
18
+ const hex = match.groups!.content as HexColor;
19
+ const [red, green, blue, alpha] = parseHexColor(hex);
20
+ const offset = match.index + (match.groups!.start?.length || 0);
21
+ const range = Range.create(document.positionAt(offset), document.positionAt(offset + hex.length));
22
+ const color = Color.create(red / 255, green / 255, blue / 255, alpha);
23
+ colors.push({ range, color });
24
+ }
25
+ return colors;
26
+ }
27
+
28
+ provideColorPresentations({ red, green, blue, alpha }: Color): ColorPresentation[] {
29
+ return [{ label: rgbToHexColor([red * 255, green * 255, blue * 255, alpha]) }];
30
+ }
31
+ }
@@ -0,0 +1,10 @@
1
+ export const COLOR_REG = /(?<start>'|")?(?<content>#([0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4}))($1|\s*;|\s*\))/g;
2
+
3
+ // 直接通过正则匹配 css 片段,通过条件的结束 ` 号匹配
4
+ export const CSS_REG = /(?<start>\/\*\s*css\s*\*\/\s*`|(?<!`)(?:css|less|scss)\s*`)(?<content>.*?)(`(?=;|,?\s*\)))/gs;
5
+ // 直接通过正则匹配 style 片段,通过条件的结束 ` 号匹配
6
+ // 语言服务和高亮都只支持 styled 写法
7
+ export const STYLE_REG = /(?<start>\/\*\s*style\s*\*\/\s*`|(?<!`)styled?\s*`)(?<content>.*?)(`(?=,|\s*}\s*\)))/gs;
8
+
9
+ // 处理后进行正则匹配,所以不需要验证后面的 ` 号
10
+ export const HTML_REG = /(?<start>\/\*\s*html\s*\*\/\s*`|(?<!`)(?:html|raw)\s*`)(?<content>[^`]*)(`)/g;
package/src/css.ts ADDED
@@ -0,0 +1,122 @@
1
+ import type { LanguageService as HTMLanguageService } from 'vscode-html-languageservice';
2
+ import type { Position, TextDocument } from 'vscode-languageserver-textdocument';
3
+ import { getLanguageService as getHTMLanguageService, TokenType as HTMLTokenType } from 'vscode-html-languageservice';
4
+ import { getCSSLanguageService } from 'vscode-css-languageservice';
5
+
6
+ import { matchOffset, createVirtualDocument, removeSlot, translateCompletionList } from './util';
7
+ import { CSS_REG, HTML_REG } from './constants';
8
+ import { CompletionsCache } from './cache';
9
+
10
+ function getRegionAtOffset(regions: IEmbeddedRegion[], offset: number) {
11
+ for (const region of regions) {
12
+ if (region.start <= offset) {
13
+ if (offset <= region.end) {
14
+ return region;
15
+ }
16
+ } else {
17
+ break;
18
+ }
19
+ }
20
+ return null;
21
+ }
22
+
23
+ interface IEmbeddedRegion {
24
+ languageId: string;
25
+ start: number;
26
+ end: number;
27
+ length: number;
28
+ content: string;
29
+ }
30
+
31
+ function getLanguageRegions(service: HTMLanguageService, data: string) {
32
+ const scanner = service.createScanner(data);
33
+ const regions: IEmbeddedRegion[] = [];
34
+ let tokenType: HTMLTokenType;
35
+
36
+ while ((tokenType = scanner.scan()) !== HTMLTokenType.EOS) {
37
+ switch (tokenType) {
38
+ case HTMLTokenType.Styles:
39
+ regions.push({
40
+ languageId: 'css',
41
+ start: scanner.getTokenOffset(),
42
+ end: scanner.getTokenEnd(),
43
+ length: scanner.getTokenLength(),
44
+ content: scanner.getTokenText(),
45
+ });
46
+ break;
47
+ default:
48
+ break;
49
+ }
50
+ }
51
+
52
+ return regions;
53
+ }
54
+
55
+ export class HTMLStyleCompletionItemProvider {
56
+ #cssLanguageService = getCSSLanguageService();
57
+ #htmlLanguageService = getHTMLanguageService();
58
+ #cache = new CompletionsCache();
59
+
60
+ provideCompletionItems(document: TextDocument, position: Position) {
61
+ const cached = this.#cache.getCached(document, position);
62
+ if (cached) return cached;
63
+
64
+ const currentOffset = document.offsetAt(position);
65
+ const documentText = document.getText();
66
+ const match = matchOffset(HTML_REG, documentText, currentOffset);
67
+
68
+ if (!match) return;
69
+
70
+ const matchContent = match.groups!.content;
71
+ const matchStartOffset = match.index + match.groups!.start.length;
72
+ const regions = getLanguageRegions(this.#htmlLanguageService, matchContent);
73
+
74
+ if (regions.length <= 0) return;
75
+
76
+ const region = getRegionAtOffset(regions, currentOffset - matchStartOffset);
77
+
78
+ if (!region) return;
79
+
80
+ const virtualOffset = currentOffset - (matchStartOffset + region.start);
81
+ const virtualDocument = createVirtualDocument('css', removeSlot(region.content));
82
+ const stylesheet = this.#cssLanguageService.parseStylesheet(virtualDocument);
83
+
84
+ const completions = this.#cssLanguageService.doComplete(
85
+ virtualDocument,
86
+ virtualDocument.positionAt(virtualOffset),
87
+ stylesheet,
88
+ );
89
+
90
+ return this.#cache.updateCached(document, position, translateCompletionList(completions, position));
91
+ }
92
+ }
93
+
94
+ export class CSSCompletionItemProvider {
95
+ #cssLanguageService = getCSSLanguageService();
96
+ #cache = new CompletionsCache();
97
+
98
+ provideCompletionItems(document: TextDocument, position: Position) {
99
+ const cached = this.#cache.getCached(document, position);
100
+ if (cached) return cached;
101
+
102
+ const currentOffset = document.offsetAt(position);
103
+ const documentText = document.getText();
104
+ const match = matchOffset(CSS_REG, documentText, currentOffset);
105
+
106
+ if (!match) return;
107
+
108
+ const matchContent = match.groups!.content;
109
+ const matchStartOffset = match.index + match.groups!.start.length;
110
+ const virtualOffset = currentOffset - matchStartOffset;
111
+ const virtualDocument = createVirtualDocument('css', removeSlot(matchContent));
112
+ const vCss = this.#cssLanguageService.parseStylesheet(virtualDocument);
113
+
114
+ const completions = this.#cssLanguageService.doComplete(
115
+ virtualDocument,
116
+ virtualDocument.positionAt(virtualOffset),
117
+ vCss,
118
+ );
119
+
120
+ return this.#cache.updateCached(document, position, translateCompletionList(completions, position));
121
+ }
122
+ }
@@ -0,0 +1,45 @@
1
+ // 只对 CSS 语法和属性做了简单的检查,不做值检查
2
+ // TODO: 激活扩展、打开工作区时需要自动诊断所有文件
3
+ // TODO: 使用 LRU 缓存
4
+
5
+ import { getCSSLanguageService } from 'vscode-css-languageservice';
6
+ import { Range } from 'vscode-languageserver/node';
7
+ import type { Diagnostic } from 'vscode-languageserver/node';
8
+ import type { TextDocument } from 'vscode-languageserver-textdocument';
9
+
10
+ import { CSS_REG, STYLE_REG } from './constants';
11
+ import { createVirtualDocument, removeSlot } from './util';
12
+
13
+ const cssLanguageService = getCSSLanguageService();
14
+
15
+ export function getDiagnostics(document: TextDocument, _relatedInformation: boolean) {
16
+ const diagnostics: Diagnostic[] = [];
17
+ const text = document.getText();
18
+
19
+ const matchFragments = (regexp: RegExp, appendBefore: string, appendAfter: string) => {
20
+ regexp.exec('null');
21
+
22
+ let match;
23
+ while ((match = regexp.exec(text))) {
24
+ const matchContent = match.groups!.content;
25
+ const offset = match.index + match.groups!.start.length;
26
+ const virtualDocument = createVirtualDocument('css', `${appendBefore}${removeSlot(matchContent)}${appendAfter}`);
27
+ const vCss = cssLanguageService.parseStylesheet(virtualDocument);
28
+ const oDiagnostics = cssLanguageService.doValidation(virtualDocument, vCss) as Diagnostic[];
29
+ for (const { message, range } of oDiagnostics) {
30
+ const { start, end } = range;
31
+ const startOffset = virtualDocument.offsetAt(start) - appendBefore.length + offset;
32
+ const endOffset = virtualDocument.offsetAt(end) - appendBefore.length + offset;
33
+ diagnostics.push({
34
+ range: Range.create(document.positionAt(startOffset), document.positionAt(endOffset)),
35
+ message,
36
+ });
37
+ }
38
+ }
39
+ };
40
+
41
+ matchFragments(CSS_REG, '', '');
42
+ matchFragments(STYLE_REG, ':host { ', ' }');
43
+
44
+ return diagnostics;
45
+ }
package/src/hover.ts ADDED
@@ -0,0 +1,73 @@
1
+ import { getLanguageService as getHtmlLanguageService } from 'vscode-html-languageservice';
2
+ import { getCSSLanguageService } from 'vscode-css-languageservice';
3
+ import type { LanguageService as CssLanguageService } from 'vscode-css-languageservice';
4
+ import type { Position, TextDocument } from 'vscode-languageserver-textdocument';
5
+
6
+ import { createVirtualDocument, matchOffset, removeHTMLSlot, removeSlot } from './util';
7
+ import { CSS_REG, HTML_REG, STYLE_REG } from './constants';
8
+
9
+ export class HTMLHoverProvider {
10
+ #htmlLanguageService = getHtmlLanguageService();
11
+
12
+ provideHover(document: TextDocument, position: Position) {
13
+ const currentOffset = document.offsetAt(position);
14
+ const documentText = removeHTMLSlot(document.getText(), currentOffset);
15
+ const match = matchOffset(HTML_REG, documentText, currentOffset);
16
+
17
+ if (!match) return null;
18
+
19
+ const matchContent = match.groups!.content;
20
+ const matchStartOffset = match.index + match.groups!.start.length;
21
+ const virtualOffset = currentOffset - matchStartOffset;
22
+ const virtualDocument = createVirtualDocument('html', matchContent);
23
+ const html = this.#htmlLanguageService.parseHTMLDocument(virtualDocument);
24
+ return this.#htmlLanguageService.doHover(virtualDocument, virtualDocument.positionAt(virtualOffset), html, {
25
+ documentation: true,
26
+ references: true,
27
+ });
28
+ }
29
+ }
30
+
31
+ export class CSSHoverProvider {
32
+ #cssLanguageService: CssLanguageService = getCSSLanguageService();
33
+
34
+ provideHover(document: TextDocument, position: Position) {
35
+ const currentOffset = document.offsetAt(position);
36
+ const documentText = document.getText();
37
+ const match = matchOffset(CSS_REG, documentText, currentOffset);
38
+
39
+ if (!match) return null;
40
+
41
+ const matchContent = match.groups!.content;
42
+ const matchStartOffset = match.index + match.groups!.start.length;
43
+ const virtualOffset = currentOffset - matchStartOffset;
44
+ const virtualDocument = createVirtualDocument('css', removeSlot(matchContent));
45
+ const stylesheet = this.#cssLanguageService.parseStylesheet(virtualDocument);
46
+ return this.#cssLanguageService.doHover(virtualDocument, virtualDocument.positionAt(virtualOffset), stylesheet, {
47
+ documentation: true,
48
+ references: true,
49
+ });
50
+ }
51
+ }
52
+
53
+ export class StyleHoverProvider {
54
+ #cssLanguageService: CssLanguageService = getCSSLanguageService();
55
+
56
+ provideHover(document: TextDocument, position: Position) {
57
+ const currentOffset = document.offsetAt(position);
58
+ const documentText = document.getText();
59
+ const match = matchOffset(STYLE_REG, documentText, currentOffset);
60
+
61
+ if (!match) return null;
62
+
63
+ const matchContent = match.groups!.content;
64
+ const matchStartOffset = match.index + match.groups!.start.length;
65
+ const virtualOffset = currentOffset - matchStartOffset + 8;
66
+ const virtualDocument = createVirtualDocument('css', `:host { ${removeSlot(matchContent)} }`);
67
+ const stylesheet = this.#cssLanguageService.parseStylesheet(virtualDocument);
68
+ return this.#cssLanguageService.doHover(virtualDocument, virtualDocument.positionAt(virtualOffset), stylesheet, {
69
+ documentation: true,
70
+ references: true,
71
+ });
72
+ }
73
+ }
package/src/html.ts ADDED
@@ -0,0 +1,85 @@
1
+ import type { CompletionList as HTMLCompletionList } from 'vscode-html-languageservice';
2
+ import { getLanguageService as getHTMLanguageService } from 'vscode-html-languageservice';
3
+ import { doComplete as doEmmetComplete, type VSCodeEmmetConfig } from '@vscode/emmet-helper';
4
+ import type { Position, TextDocument } from 'vscode-languageserver-textdocument';
5
+ import type { Connection } from 'vscode-languageserver';
6
+
7
+ import { matchOffset, createVirtualDocument, removeHTMLSlot, translateCompletionList } from './util';
8
+ import { HTML_REG } from './constants';
9
+ import { CompletionsCache } from './cache';
10
+
11
+ export class HTMLCompletionItemProvider {
12
+ #htmlLanguageService = getHTMLanguageService();
13
+ #cache = new CompletionsCache();
14
+ #connection: Connection;
15
+ #emmetConfig: VSCodeEmmetConfig;
16
+
17
+ constructor(connection: Connection) {
18
+ this.#connection = connection;
19
+ }
20
+
21
+ async #getEmmetConfig() {
22
+ if (!this.#emmetConfig) {
23
+ const emmetConfig = await this.#connection.workspace.getConfiguration('emmet');
24
+ this.#emmetConfig = {
25
+ showExpandedAbbreviation: emmetConfig.showExpandedAbbreviation,
26
+ showAbbreviationSuggestions: emmetConfig.showAbbreviationSuggestions,
27
+ syntaxProfiles: emmetConfig.syntaxProfiles,
28
+ variables: emmetConfig.variables,
29
+ };
30
+ }
31
+ return this.#emmetConfig;
32
+ }
33
+
34
+ async provideCompletionItems(document: TextDocument, position: Position) {
35
+ const emmetConfig = await this.#getEmmetConfig();
36
+ const cached = this.#cache.getCached(document, position);
37
+ if (cached) return cached;
38
+
39
+ const currentOffset = document.offsetAt(position);
40
+ const documentText = removeHTMLSlot(document.getText(), currentOffset);
41
+ const match = matchOffset(HTML_REG, documentText, currentOffset);
42
+
43
+ if (!match) return;
44
+
45
+ const matchContent = match.groups!.content;
46
+ const matchStartOffset = match.index + match.groups!.start.length;
47
+ const virtualOffset = currentOffset - matchStartOffset;
48
+ const virtualDocument = createVirtualDocument('html', matchContent);
49
+ const vHtml = this.#htmlLanguageService.parseHTMLDocument(virtualDocument);
50
+
51
+ let emmetResults: HTMLCompletionList = { isIncomplete: true, items: [] };
52
+ this.#htmlLanguageService.setCompletionParticipants([
53
+ {
54
+ onHtmlContent: async () => {
55
+ const pos = virtualDocument.positionAt(virtualOffset);
56
+ const result = doEmmetComplete(virtualDocument, pos, 'html', emmetConfig);
57
+ if (result) {
58
+ emmetResults = {
59
+ ...result,
60
+ items: result.items.map((item) => ({
61
+ ...item,
62
+ command: {
63
+ title: 'Emmet Expand Abbreviation',
64
+ command: 'editor.emmet.action.expandAbbreviation',
65
+ },
66
+ })),
67
+ };
68
+ }
69
+ },
70
+ },
71
+ ]);
72
+
73
+ const completions = this.#htmlLanguageService.doComplete(
74
+ virtualDocument,
75
+ virtualDocument.positionAt(virtualOffset),
76
+ vHtml,
77
+ );
78
+
79
+ if (emmetResults.items.length) {
80
+ completions.items.push(...emmetResults.items);
81
+ }
82
+
83
+ return this.#cache.updateCached(document, position, translateCompletionList(completions, position));
84
+ }
85
+ }
package/src/index.ts CHANGED
@@ -1,15 +1,21 @@
1
1
  #!/usr/bin/env -S node --experimental-transform-types
2
- import type { Diagnostic, InitializeParams } from 'vscode-languageserver/node';
2
+ import type { InitializeParams } from 'vscode-languageserver/node';
3
3
  import {
4
4
  createConnection,
5
5
  TextDocuments,
6
- DiagnosticSeverity,
7
6
  ProposedFeatures,
8
7
  DidChangeConfigurationNotification,
9
- CompletionItemKind,
10
8
  TextDocumentSyncKind,
11
9
  } from 'vscode-languageserver/node';
12
10
  import { TextDocument } from 'vscode-languageserver-textdocument';
11
+ import { debounce } from 'duoyun-ui/lib/timer';
12
+
13
+ import { ColorProvider } from './color';
14
+ import { getDiagnostics } from './diagnostic';
15
+ import { CSSHoverProvider, HTMLHoverProvider, StyleHoverProvider } from './hover';
16
+ import { CSSCompletionItemProvider, HTMLStyleCompletionItemProvider } from './css';
17
+ import { HTMLCompletionItemProvider } from './html';
18
+ import { StyleCompletionItemProvider } from './style';
13
19
 
14
20
  const connection = createConnection(ProposedFeatures.all);
15
21
  const documents = new TextDocuments(TextDocument);
@@ -24,8 +30,13 @@ connection.onInitialize(({ capabilities }: InitializeParams) => {
24
30
  hasDiagnosticRelatedInformationCapability = !!capabilities.textDocument?.publishDiagnostics?.relatedInformation;
25
31
  return {
26
32
  capabilities: {
33
+ completionProvider: {
34
+ resolveProvider: true,
35
+ triggerCharacters: ['!', '.', '}', ':', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '<'],
36
+ },
37
+ hoverProvider: true,
38
+ colorProvider: true,
27
39
  textDocumentSync: TextDocumentSyncKind.Incremental,
28
- completionProvider: { resolveProvider: true },
29
40
  workspace: !hasWorkspaceFolderCapability ? undefined : { workspaceFolders: { supported: true } },
30
41
  },
31
42
  };
@@ -60,7 +71,7 @@ connection.onDidChangeConfiguration((change) => {
60
71
  documents.all().forEach(validateTextDocument);
61
72
  });
62
73
 
63
- async function getDocumentSettings(resource: string) {
74
+ function _getDocumentSettings(resource: string) {
64
75
  if (!hasConfigurationCapability) return globalSettings;
65
76
  if (!documentSettings.has(resource)) {
66
77
  const settings = connection.workspace.getConfiguration({ scopeUri: resource, section: 'languageServerGem' });
@@ -73,74 +84,47 @@ documents.onDidClose((e) => documentSettings.delete(e.document.uri));
73
84
 
74
85
  documents.onDidChangeContent((change) => validateTextDocument(change.document));
75
86
 
76
- async function validateTextDocument(textDocument: TextDocument) {
77
- const settings = await getDocumentSettings(textDocument.uri);
78
-
79
- const text = textDocument.getText();
80
- const pattern = /\b[A-Z]{20,}\b/g;
81
- let m: RegExpExecArray | null;
82
-
83
- let problems = 0;
84
- const diagnostics: Diagnostic[] = [];
85
- while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
86
- problems++;
87
- const diagnostic: Diagnostic = {
88
- severity: DiagnosticSeverity.Warning,
89
- range: { start: textDocument.positionAt(m.index), end: textDocument.positionAt(m.index + m[0].length) },
90
- message: `${m[0]} is all uppercase.`,
91
- source: 'vscode-gem',
92
- };
93
- if (hasDiagnosticRelatedInformationCapability) {
94
- diagnostic.relatedInformation = [
95
- {
96
- location: {
97
- uri: textDocument.uri,
98
- range: Object.assign({}, diagnostic.range),
99
- },
100
- message: 'Spelling matters',
101
- },
102
- {
103
- location: {
104
- uri: textDocument.uri,
105
- range: Object.assign({}, diagnostic.range),
106
- },
107
- message: 'Particularly for names',
108
- },
109
- ];
110
- }
111
- diagnostics.push(diagnostic);
112
- }
113
- connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
114
- }
87
+ const validateTextDocument = debounce((textDocument: TextDocument) => {
88
+ connection.sendDiagnostics({
89
+ uri: textDocument.uri,
90
+ diagnostics: getDiagnostics(textDocument, hasDiagnosticRelatedInformationCapability),
91
+ });
92
+ });
115
93
 
116
94
  connection.onDidChangeWatchedFiles((_change) => {
117
95
  connection.console.log('We received a file change event');
118
96
  });
119
97
 
120
- connection.onCompletion((_textDocumentPosition) => {
121
- return [
122
- {
123
- label: 'TypeScript',
124
- kind: CompletionItemKind.Text,
125
- data: 1,
126
- },
127
- {
128
- label: 'JavaScript',
129
- kind: CompletionItemKind.Text,
130
- data: 2,
131
- },
132
- ];
98
+ const completionItemProviders = [
99
+ new CSSCompletionItemProvider(),
100
+ new HTMLStyleCompletionItemProvider(),
101
+ new HTMLCompletionItemProvider(connection),
102
+ new StyleCompletionItemProvider(),
103
+ ];
104
+ connection.onCompletion(async ({ textDocument, position }) => {
105
+ for (const provider of completionItemProviders) {
106
+ const result = await provider.provideCompletionItems(documents.get(textDocument.uri)!, position);
107
+ if (result) return { isIncomplete: true, items: result.items };
108
+ }
109
+ });
110
+
111
+ connection.onCompletionResolve((item) => item);
112
+
113
+ const colorProvider = new ColorProvider();
114
+ connection.onColorPresentation(({ color }) => {
115
+ return colorProvider.provideColorPresentations(color);
116
+ });
117
+
118
+ connection.onDocumentColor(({ textDocument }) => {
119
+ return colorProvider.provideDocumentColors(documents.get(textDocument.uri)!);
133
120
  });
134
121
 
135
- connection.onCompletionResolve((item) => {
136
- if (item.data === 1) {
137
- item.detail = 'TypeScript details';
138
- item.documentation = 'TypeScript documentation';
139
- } else if (item.data === 2) {
140
- item.detail = 'JavaScript details';
141
- item.documentation = 'JavaScript documentation';
122
+ const hoverProviders = [new CSSHoverProvider(), new StyleHoverProvider(), new HTMLHoverProvider()];
123
+ connection.onHover(({ textDocument, position }) => {
124
+ for (const provider of hoverProviders) {
125
+ const result = provider.provideHover(documents.get(textDocument.uri)!, position);
126
+ if (result) return result;
142
127
  }
143
- return item;
144
128
  });
145
129
 
146
130
  documents.listen(connection);
package/src/style.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { getCSSLanguageService as getCSSLanguageService } from 'vscode-css-languageservice';
2
+ import type { Position, TextDocument } from 'vscode-languageserver-textdocument';
3
+
4
+ import { matchOffset, createVirtualDocument, removeSlot, translateCompletionList } from './util';
5
+ import { STYLE_REG } from './constants';
6
+ import { CompletionsCache } from './cache';
7
+
8
+ export class StyleCompletionItemProvider {
9
+ #cssLanguageService = getCSSLanguageService();
10
+ #cache = new CompletionsCache();
11
+
12
+ provideCompletionItems(document: TextDocument, position: Position) {
13
+ const cached = this.#cache.getCached(document, position);
14
+ if (cached) return cached;
15
+
16
+ const currentOffset = document.offsetAt(position);
17
+ const documentText = document.getText();
18
+ const match = matchOffset(STYLE_REG, documentText, currentOffset);
19
+
20
+ if (!match) return;
21
+
22
+ const matchContent = match.groups!.content;
23
+ const matchStartOffset = match.index + match.groups!.start.length;
24
+ const virtualOffset = currentOffset - matchStartOffset + 8; // accounting for :host { }
25
+ const virtualDocument = createVirtualDocument('css', `:host { ${removeSlot(matchContent)} }`);
26
+ const vCss = this.#cssLanguageService.parseStylesheet(virtualDocument);
27
+
28
+ const completions = this.#cssLanguageService.doComplete(
29
+ virtualDocument,
30
+ virtualDocument.positionAt(virtualOffset),
31
+ vCss,
32
+ );
33
+
34
+ return this.#cache.updateCached(document, position, translateCompletionList(completions, position));
35
+ }
36
+ }
package/src/util.ts ADDED
@@ -0,0 +1,59 @@
1
+ import { TextDocument } from 'vscode-html-languageservice';
2
+ import { Position, Range } from 'vscode-languageserver/node';
3
+ import type { CompletionItem } from 'vscode-languageserver/node';
4
+
5
+ export function removeSlot(text: string) {
6
+ const v = text.replace(/\$\{[^${]*?\}/g, (str) => str.replaceAll(/[^\n]/g, 'x'));
7
+ if (v === text) return v;
8
+ return removeSlot(v);
9
+ }
10
+
11
+ export function removeHTMLSlot(text: string, position: number) {
12
+ const left = text.slice(0, position);
13
+ const right = text.slice(position);
14
+ const left1 = removeSlot(left);
15
+ // 处理在插槽中的情况,只保留光标附件的 html 标签
16
+ const left2 = left1.replace(/(.*(?=html`))/s, (str) => str.replaceAll(/[^\n]/g, ' '));
17
+ return left2 + removeSlot(right);
18
+ }
19
+
20
+ export function translateCompletionList(result: any, position: Position) {
21
+ const getRange = (item: CompletionItem): Range | undefined => {
22
+ if (item.textEdit && 'range' in item.textEdit) {
23
+ const { start, end } = item.textEdit.range;
24
+ return Range.create(
25
+ Position.create(position.line, start.character),
26
+ Position.create(position.line, end.character),
27
+ );
28
+ }
29
+ };
30
+
31
+ return {
32
+ ...result,
33
+ items: result?.items.map((item: CompletionItem) => ({
34
+ ...item,
35
+ textEdit: item.textEdit && {
36
+ ...item.textEdit,
37
+ range: getRange(item),
38
+ },
39
+ })),
40
+ };
41
+ }
42
+ export function matchOffset(regex: RegExp, docText: string, offset: number) {
43
+ regex.exec('null');
44
+
45
+ let match: RegExpExecArray | null;
46
+ while ((match = regex.exec(docText)) !== null) {
47
+ const [fullStr, startStr] = match;
48
+ const start = match.index + startStr.length;
49
+ const end = match.index + fullStr.length;
50
+ if (offset > start && offset < end) {
51
+ return match;
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+
57
+ export function createVirtualDocument(languageId: string, content: string) {
58
+ return TextDocument.create(`embedded://document.${languageId}`, languageId, 1, content);
59
+ }