diff --git a/packages/duoyun-ui/src/lib/encode.ts b/packages/duoyun-ui/src/lib/encode.ts index a7e6be81..6dc997a4 100644 --- a/packages/duoyun-ui/src/lib/encode.ts +++ b/packages/duoyun-ui/src/lib/encode.ts @@ -16,7 +16,7 @@ export function b64ToUtf8(str: string) { } export function base64ToArrayBuffer(str: string) { - return new Uint8Array([...self.atob(safeUrlToBase64Str(str))].map((char) => char.charCodeAt(0))).buffer; + return new Uint8Array([...atob(safeUrlToBase64Str(str))].map((char) => char.charCodeAt(0))).buffer; } function base64ToSafeUrl(str: string) { @@ -25,7 +25,7 @@ function base64ToSafeUrl(str: string) { /**Converted string to Base64, `isSafe` indicates URL safe */ export function utf8ToB64(str: string, isSafe?: boolean) { - const base64 = self.btoa( + const base64 = btoa( encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(Number(`0x${p1}`))), ); return isSafe ? base64ToSafeUrl(base64) : base64; @@ -33,7 +33,7 @@ export function utf8ToB64(str: string, isSafe?: boolean) { // https://github.com/tc39/proposal-arraybuffer-base64 export function arrayBufferToBase64(arrayBuffer: ArrayBuffer, isSafe?: boolean) { - const base64 = self.btoa(String.fromCharCode(...new Uint8Array(arrayBuffer))); + const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer))); return isSafe ? base64ToSafeUrl(base64) : base64; } diff --git a/packages/duoyun-ui/src/lib/image.ts b/packages/duoyun-ui/src/lib/image.ts index 6717eba3..d464d257 100644 --- a/packages/duoyun-ui/src/lib/image.ts +++ b/packages/duoyun-ui/src/lib/image.ts @@ -15,7 +15,7 @@ export function createCanvas(width?: number, height?: number) { } export function createDataURLFromSVG(rawStr: string) { - return `data:image/svg+xml;base64,${self.btoa(rawStr)}`; + return `data:image/svg+xml;base64,${btoa(rawStr)}`; } // if `bg` is't `HexColor`, text fill color error diff --git a/packages/duoyun-ui/src/lib/timer.ts b/packages/duoyun-ui/src/lib/timer.ts index 7bacfcce..93c218d4 100644 --- a/packages/duoyun-ui/src/lib/timer.ts +++ b/packages/duoyun-ui/src/lib/timer.ts @@ -1,5 +1,7 @@ import { logger } from '@mantou/gem/helper/logger'; +const setTimeout = globalThis.setTimeout as typeof self.setTimeout; + /**Until the callback function resolve */ export async function forever(fn: () => Promise, interval = 1000): Promise { try { @@ -21,7 +23,7 @@ export function polling(fn: (args?: any[]) => any, delay: number) { } catch { } finally { if (!hasExit) { - timer = self.setTimeout(poll, delay); + timer = setTimeout(poll, delay); } } }; @@ -48,7 +50,7 @@ export function throttle any>( let timer = 0; let first = 0; const exec = (...rest: Parameters) => { - timer = self.setTimeout(() => (timer = 0), wait); + timer = setTimeout(() => (timer = 0), wait); fn(...(rest as any)); }; return (...rest: Parameters) => { @@ -62,7 +64,7 @@ export function throttle any>( exec(...rest); } else { clearTimeout(timer); - timer = self.setTimeout(() => exec(...rest), wait); + timer = setTimeout(() => exec(...rest), wait); } }; } @@ -76,9 +78,9 @@ export function debounce any>( return function (...args: Parameters) { return new Promise>>((resolve, reject) => { clearTimeout(timer); - timer = self.setTimeout( + timer = setTimeout( () => { - timer = self.setTimeout(() => (timer = 0), wait); + timer = setTimeout(() => (timer = 0), wait); Promise.resolve(fn(...(args as any))) .then(resolve) .catch(reject); diff --git a/packages/duoyun-ui/src/lib/utils.ts b/packages/duoyun-ui/src/lib/utils.ts index d1b03a5f..54d29b49 100644 --- a/packages/duoyun-ui/src/lib/utils.ts +++ b/packages/duoyun-ui/src/lib/utils.ts @@ -260,7 +260,7 @@ export function createCacheStore>( ); }; - self.addEventListener('pagehide', saveStore); + addEventListener('pagehide', saveStore); return { store, saveStore }; } diff --git a/packages/language-service/package.json b/packages/language-service/package.json index abab4203..51ea590e 100644 --- a/packages/language-service/package.json +++ b/packages/language-service/package.json @@ -1,6 +1,6 @@ { "name": "vscode-gem-languageservice", - "version": "0.0.1", + "version": "0.0.2", "description": "Language service for Gem", "keywords": [ "gem", @@ -14,9 +14,13 @@ ], "scripts": {}, "dependencies": { + "@vscode/emmet-helper": "^2.9.3", + "duoyun-ui": "^2.2.0", "gem-analyzer": "^2.2.0", "ts-morph": "^13.0.0", "typescript": "^5.6.2", + "vscode-css-languageservice": "^6.3.1", + "vscode-html-languageservice": "^5.3.1", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.12" }, diff --git a/packages/vscode-gem-plugin/src/providers/cache.ts b/packages/language-service/src/cache.ts similarity index 78% rename from packages/vscode-gem-plugin/src/providers/cache.ts rename to packages/language-service/src/cache.ts index ade7481c..7fb7380f 100644 --- a/packages/vscode-gem-plugin/src/providers/cache.ts +++ b/packages/language-service/src/cache.ts @@ -1,4 +1,5 @@ -import type { CompletionList, TextDocument, Position } from 'vscode'; +import type { CompletionList } from 'vscode-languageserver'; +import type { Position, TextDocument } from 'vscode-languageserver-textdocument'; export class CompletionsCache { #cachedCompletionsFile?: string; @@ -13,7 +14,7 @@ export class CompletionsCache { getCached(doc: TextDocument, position: Position) { if ( this.#completions && - doc.fileName === this.#cachedCompletionsFile && + doc.uri === this.#cachedCompletionsFile && this.#equalPositions(position, this.#cachedCompletionsPosition) && doc.getText() === this.#cachedCompletionsContent ) { @@ -24,7 +25,7 @@ export class CompletionsCache { } updateCached(context: TextDocument, position: Position, completions: CompletionList) { - this.#cachedCompletionsFile = context.fileName; + this.#cachedCompletionsFile = context.uri; this.#cachedCompletionsPosition = position; this.#cachedCompletionsContent = context.getText(); this.#completions = completions; diff --git a/packages/language-service/src/color.ts b/packages/language-service/src/color.ts new file mode 100644 index 00000000..ddd2a1e9 --- /dev/null +++ b/packages/language-service/src/color.ts @@ -0,0 +1,31 @@ +import { rgbToHexColor, parseHexColor } from 'duoyun-ui/lib/color'; +import { Range, Color } from 'vscode-languageserver/node'; +import type { ColorInformation, ColorPresentation } from 'vscode-languageserver/node'; +import type { HexColor } from 'duoyun-ui/lib/color'; +import type { TextDocument } from 'vscode-languageserver-textdocument'; + +import { COLOR_REG } from './constants'; + +export class ColorProvider { + provideDocumentColors(document: TextDocument) { + COLOR_REG.exec('null'); + + const documentText = document.getText(); + const colors: ColorInformation[] = []; + + let match: RegExpExecArray | null; + while ((match = COLOR_REG.exec(documentText)) !== null) { + const hex = match.groups!.content as HexColor; + const [red, green, blue, alpha] = parseHexColor(hex); + const offset = match.index + (match.groups!.start?.length || 0); + const range = Range.create(document.positionAt(offset), document.positionAt(offset + hex.length)); + const color = Color.create(red / 255, green / 255, blue / 255, alpha); + colors.push({ range, color }); + } + return colors; + } + + provideColorPresentations({ red, green, blue, alpha }: Color): ColorPresentation[] { + return [{ label: rgbToHexColor([red * 255, green * 255, blue * 255, alpha]) }]; + } +} diff --git a/packages/language-service/src/constants.ts b/packages/language-service/src/constants.ts new file mode 100644 index 00000000..17f51b28 --- /dev/null +++ b/packages/language-service/src/constants.ts @@ -0,0 +1,10 @@ +export const COLOR_REG = /(?'|")?(?#([0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4}))($1|\s*;|\s*\))/g; + +// 直接通过正则匹配 css 片段,通过条件的结束 ` 号匹配 +export const CSS_REG = /(?\/\*\s*css\s*\*\/\s*`|(?.*?)(`(?=;|,?\s*\)))/gs; +// 直接通过正则匹配 style 片段,通过条件的结束 ` 号匹配 +// 语言服务和高亮都只支持 styled 写法 +export const STYLE_REG = /(?\/\*\s*style\s*\*\/\s*`|(?.*?)(`(?=,|\s*}\s*\)))/gs; + +// 处理后进行正则匹配,所以不需要验证后面的 ` 号 +export const HTML_REG = /(?\/\*\s*html\s*\*\/\s*`|(?[^`]*)(`)/g; diff --git a/packages/vscode-gem-plugin/src/providers/css.ts b/packages/language-service/src/css.ts similarity index 67% rename from packages/vscode-gem-plugin/src/providers/css.ts rename to packages/language-service/src/css.ts index e0020b74..a3b7a988 100644 --- a/packages/vscode-gem-plugin/src/providers/css.ts +++ b/packages/language-service/src/css.ts @@ -1,21 +1,13 @@ -import type { - CompletionList, - CompletionItem, - TextDocument, - Position, - CancellationToken, - CompletionItemProvider, -} from 'vscode'; import type { LanguageService as HTMLanguageService } from 'vscode-html-languageservice'; +import type { Position, TextDocument } from 'vscode-languageserver-textdocument'; import { getLanguageService as getHTMLanguageService, TokenType as HTMLTokenType } from 'vscode-html-languageservice'; -import { getCSSLanguageService as getCSSLanguageService } from 'vscode-css-languageservice'; - -import { matchOffset, createVirtualDocument, translateCompletionList, removeSlot } from '../util'; -import { CSS_REG, HTML_REG } from '../constants'; +import { getCSSLanguageService } from 'vscode-css-languageservice'; +import { matchOffset, createVirtualDocument, removeSlot, translateCompletionList } from './util'; +import { CSS_REG, HTML_REG } from './constants'; import { CompletionsCache } from './cache'; -export function getRegionAtOffset(regions: IEmbeddedRegion[], offset: number) { +function getRegionAtOffset(regions: IEmbeddedRegion[], offset: number) { for (const region of regions) { if (region.start <= offset) { if (offset <= region.end) { @@ -28,7 +20,7 @@ export function getRegionAtOffset(regions: IEmbeddedRegion[], offset: number) { return null; } -export interface IEmbeddedRegion { +interface IEmbeddedRegion { languageId: string; start: number; end: number; @@ -36,7 +28,7 @@ export interface IEmbeddedRegion { content: string; } -export function getLanguageRegions(service: HTMLanguageService, data: string) { +function getLanguageRegions(service: HTMLanguageService, data: string) { const scanner = service.createScanner(data); const regions: IEmbeddedRegion[] = []; let tokenType: HTMLTokenType; @@ -60,35 +52,30 @@ export function getLanguageRegions(service: HTMLanguageService, data: string) { return regions; } -export class HTMLStyleCompletionItemProvider implements CompletionItemProvider { +export class HTMLStyleCompletionItemProvider { #cssLanguageService = getCSSLanguageService(); #htmlLanguageService = getHTMLanguageService(); #cache = new CompletionsCache(); - provideCompletionItems(document: TextDocument, position: Position, _token: CancellationToken) { + provideCompletionItems(document: TextDocument, position: Position) { const cached = this.#cache.getCached(document, position); if (cached) return cached; - const currentLine = document.lineAt(position.line); - const empty: CompletionList = { isIncomplete: false, items: [] }; - - if (currentLine.isEmptyOrWhitespace) return empty; - const currentOffset = document.offsetAt(position); const documentText = document.getText(); const match = matchOffset(HTML_REG, documentText, currentOffset); - if (!match) return empty; + if (!match) return; const matchContent = match.groups!.content; const matchStartOffset = match.index + match.groups!.start.length; const regions = getLanguageRegions(this.#htmlLanguageService, matchContent); - if (regions.length <= 0) return empty; + if (regions.length <= 0) return; const region = getRegionAtOffset(regions, currentOffset - matchStartOffset); - if (!region) return empty; + if (!region) return; const virtualOffset = currentOffset - (matchStartOffset + region.start); const virtualDocument = createVirtualDocument('css', removeSlot(region.content)); @@ -100,32 +87,23 @@ export class HTMLStyleCompletionItemProvider implements CompletionItemProvider { stylesheet, ); - return this.#cache.updateCached(document, position, translateCompletionList(completions, currentLine)); - } - - resolveCompletionItem(item: CompletionItem, _token: CancellationToken) { - return item; + return this.#cache.updateCached(document, position, translateCompletionList(completions, position)); } } -export class CSSCompletionItemProvider implements CompletionItemProvider { +export class CSSCompletionItemProvider { #cssLanguageService = getCSSLanguageService(); #cache = new CompletionsCache(); - provideCompletionItems(document: TextDocument, position: Position, _token: CancellationToken) { + provideCompletionItems(document: TextDocument, position: Position) { const cached = this.#cache.getCached(document, position); if (cached) return cached; - const currentLine = document.lineAt(position.line); - const empty: CompletionList = { isIncomplete: false, items: [] }; - - if (currentLine.isEmptyOrWhitespace) return empty; - const currentOffset = document.offsetAt(position); const documentText = document.getText(); const match = matchOffset(CSS_REG, documentText, currentOffset); - if (!match) return empty; + if (!match) return; const matchContent = match.groups!.content; const matchStartOffset = match.index + match.groups!.start.length; @@ -139,10 +117,6 @@ export class CSSCompletionItemProvider implements CompletionItemProvider { vCss, ); - return this.#cache.updateCached(document, position, translateCompletionList(completions, currentLine)); - } - - resolveCompletionItem(item: CompletionItem, _token: CancellationToken) { - return item; + return this.#cache.updateCached(document, position, translateCompletionList(completions, position)); } } diff --git a/packages/language-service/src/diagnostic.ts b/packages/language-service/src/diagnostic.ts new file mode 100644 index 00000000..9623b114 --- /dev/null +++ b/packages/language-service/src/diagnostic.ts @@ -0,0 +1,45 @@ +// 只对 CSS 语法和属性做了简单的检查,不做值检查 +// TODO: 激活扩展、打开工作区时需要自动诊断所有文件 +// TODO: 使用 LRU 缓存 + +import { getCSSLanguageService } from 'vscode-css-languageservice'; +import { Range } from 'vscode-languageserver/node'; +import type { Diagnostic } from 'vscode-languageserver/node'; +import type { TextDocument } from 'vscode-languageserver-textdocument'; + +import { CSS_REG, STYLE_REG } from './constants'; +import { createVirtualDocument, removeSlot } from './util'; + +const cssLanguageService = getCSSLanguageService(); + +export function getDiagnostics(document: TextDocument, _relatedInformation: boolean) { + const diagnostics: Diagnostic[] = []; + const text = document.getText(); + + const matchFragments = (regexp: RegExp, appendBefore: string, appendAfter: string) => { + regexp.exec('null'); + + let match; + while ((match = regexp.exec(text))) { + const matchContent = match.groups!.content; + const offset = match.index + match.groups!.start.length; + const virtualDocument = createVirtualDocument('css', `${appendBefore}${removeSlot(matchContent)}${appendAfter}`); + const vCss = cssLanguageService.parseStylesheet(virtualDocument); + const oDiagnostics = cssLanguageService.doValidation(virtualDocument, vCss) as Diagnostic[]; + for (const { message, range } of oDiagnostics) { + const { start, end } = range; + const startOffset = virtualDocument.offsetAt(start) - appendBefore.length + offset; + const endOffset = virtualDocument.offsetAt(end) - appendBefore.length + offset; + diagnostics.push({ + range: Range.create(document.positionAt(startOffset), document.positionAt(endOffset)), + message, + }); + } + } + }; + + matchFragments(CSS_REG, '', ''); + matchFragments(STYLE_REG, ':host { ', ' }'); + + return diagnostics; +} diff --git a/packages/language-service/src/hover.ts b/packages/language-service/src/hover.ts new file mode 100644 index 00000000..6a533710 --- /dev/null +++ b/packages/language-service/src/hover.ts @@ -0,0 +1,73 @@ +import { getLanguageService as getHtmlLanguageService } from 'vscode-html-languageservice'; +import { getCSSLanguageService } from 'vscode-css-languageservice'; +import type { LanguageService as CssLanguageService } from 'vscode-css-languageservice'; +import type { Position, TextDocument } from 'vscode-languageserver-textdocument'; + +import { createVirtualDocument, matchOffset, removeHTMLSlot, removeSlot } from './util'; +import { CSS_REG, HTML_REG, STYLE_REG } from './constants'; + +export class HTMLHoverProvider { + #htmlLanguageService = getHtmlLanguageService(); + + provideHover(document: TextDocument, position: Position) { + const currentOffset = document.offsetAt(position); + const documentText = removeHTMLSlot(document.getText(), currentOffset); + const match = matchOffset(HTML_REG, documentText, currentOffset); + + if (!match) return null; + + const matchContent = match.groups!.content; + const matchStartOffset = match.index + match.groups!.start.length; + const virtualOffset = currentOffset - matchStartOffset; + const virtualDocument = createVirtualDocument('html', matchContent); + const html = this.#htmlLanguageService.parseHTMLDocument(virtualDocument); + return this.#htmlLanguageService.doHover(virtualDocument, virtualDocument.positionAt(virtualOffset), html, { + documentation: true, + references: true, + }); + } +} + +export class CSSHoverProvider { + #cssLanguageService: CssLanguageService = getCSSLanguageService(); + + provideHover(document: TextDocument, position: Position) { + const currentOffset = document.offsetAt(position); + const documentText = document.getText(); + const match = matchOffset(CSS_REG, documentText, currentOffset); + + if (!match) return null; + + const matchContent = match.groups!.content; + const matchStartOffset = match.index + match.groups!.start.length; + const virtualOffset = currentOffset - matchStartOffset; + const virtualDocument = createVirtualDocument('css', removeSlot(matchContent)); + const stylesheet = this.#cssLanguageService.parseStylesheet(virtualDocument); + return this.#cssLanguageService.doHover(virtualDocument, virtualDocument.positionAt(virtualOffset), stylesheet, { + documentation: true, + references: true, + }); + } +} + +export class StyleHoverProvider { + #cssLanguageService: CssLanguageService = getCSSLanguageService(); + + provideHover(document: TextDocument, position: Position) { + const currentOffset = document.offsetAt(position); + const documentText = document.getText(); + const match = matchOffset(STYLE_REG, documentText, currentOffset); + + if (!match) return null; + + const matchContent = match.groups!.content; + const matchStartOffset = match.index + match.groups!.start.length; + const virtualOffset = currentOffset - matchStartOffset + 8; + const virtualDocument = createVirtualDocument('css', `:host { ${removeSlot(matchContent)} }`); + const stylesheet = this.#cssLanguageService.parseStylesheet(virtualDocument); + return this.#cssLanguageService.doHover(virtualDocument, virtualDocument.positionAt(virtualOffset), stylesheet, { + documentation: true, + references: true, + }); + } +} diff --git a/packages/language-service/src/html.ts b/packages/language-service/src/html.ts new file mode 100644 index 00000000..b249ce58 --- /dev/null +++ b/packages/language-service/src/html.ts @@ -0,0 +1,85 @@ +import type { CompletionList as HTMLCompletionList } from 'vscode-html-languageservice'; +import { getLanguageService as getHTMLanguageService } from 'vscode-html-languageservice'; +import { doComplete as doEmmetComplete, type VSCodeEmmetConfig } from '@vscode/emmet-helper'; +import type { Position, TextDocument } from 'vscode-languageserver-textdocument'; +import type { Connection } from 'vscode-languageserver'; + +import { matchOffset, createVirtualDocument, removeHTMLSlot, translateCompletionList } from './util'; +import { HTML_REG } from './constants'; +import { CompletionsCache } from './cache'; + +export class HTMLCompletionItemProvider { + #htmlLanguageService = getHTMLanguageService(); + #cache = new CompletionsCache(); + #connection: Connection; + #emmetConfig: VSCodeEmmetConfig; + + constructor(connection: Connection) { + this.#connection = connection; + } + + async #getEmmetConfig() { + if (!this.#emmetConfig) { + const emmetConfig = await this.#connection.workspace.getConfiguration('emmet'); + this.#emmetConfig = { + showExpandedAbbreviation: emmetConfig.showExpandedAbbreviation, + showAbbreviationSuggestions: emmetConfig.showAbbreviationSuggestions, + syntaxProfiles: emmetConfig.syntaxProfiles, + variables: emmetConfig.variables, + }; + } + return this.#emmetConfig; + } + + async provideCompletionItems(document: TextDocument, position: Position) { + const emmetConfig = await this.#getEmmetConfig(); + const cached = this.#cache.getCached(document, position); + if (cached) return cached; + + const currentOffset = document.offsetAt(position); + const documentText = removeHTMLSlot(document.getText(), currentOffset); + const match = matchOffset(HTML_REG, documentText, currentOffset); + + if (!match) return; + + const matchContent = match.groups!.content; + const matchStartOffset = match.index + match.groups!.start.length; + const virtualOffset = currentOffset - matchStartOffset; + const virtualDocument = createVirtualDocument('html', matchContent); + const vHtml = this.#htmlLanguageService.parseHTMLDocument(virtualDocument); + + let emmetResults: HTMLCompletionList = { isIncomplete: true, items: [] }; + this.#htmlLanguageService.setCompletionParticipants([ + { + onHtmlContent: async () => { + const pos = virtualDocument.positionAt(virtualOffset); + const result = doEmmetComplete(virtualDocument, pos, 'html', emmetConfig); + if (result) { + emmetResults = { + ...result, + items: result.items.map((item) => ({ + ...item, + command: { + title: 'Emmet Expand Abbreviation', + command: 'editor.emmet.action.expandAbbreviation', + }, + })), + }; + } + }, + }, + ]); + + const completions = this.#htmlLanguageService.doComplete( + virtualDocument, + virtualDocument.positionAt(virtualOffset), + vHtml, + ); + + if (emmetResults.items.length) { + completions.items.push(...emmetResults.items); + } + + return this.#cache.updateCached(document, position, translateCompletionList(completions, position)); + } +} diff --git a/packages/language-service/src/index.ts b/packages/language-service/src/index.ts index 40cf0362..15b1f2e7 100755 --- a/packages/language-service/src/index.ts +++ b/packages/language-service/src/index.ts @@ -1,15 +1,21 @@ #!/usr/bin/env -S node --experimental-transform-types -import type { Diagnostic, InitializeParams } from 'vscode-languageserver/node'; +import type { InitializeParams } from 'vscode-languageserver/node'; import { createConnection, TextDocuments, - DiagnosticSeverity, ProposedFeatures, DidChangeConfigurationNotification, - CompletionItemKind, TextDocumentSyncKind, } from 'vscode-languageserver/node'; import { TextDocument } from 'vscode-languageserver-textdocument'; +import { debounce } from 'duoyun-ui/lib/timer'; + +import { ColorProvider } from './color'; +import { getDiagnostics } from './diagnostic'; +import { CSSHoverProvider, HTMLHoverProvider, StyleHoverProvider } from './hover'; +import { CSSCompletionItemProvider, HTMLStyleCompletionItemProvider } from './css'; +import { HTMLCompletionItemProvider } from './html'; +import { StyleCompletionItemProvider } from './style'; const connection = createConnection(ProposedFeatures.all); const documents = new TextDocuments(TextDocument); @@ -24,8 +30,13 @@ connection.onInitialize(({ capabilities }: InitializeParams) => { hasDiagnosticRelatedInformationCapability = !!capabilities.textDocument?.publishDiagnostics?.relatedInformation; return { capabilities: { + completionProvider: { + resolveProvider: true, + triggerCharacters: ['!', '.', '}', ':', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '<'], + }, + hoverProvider: true, + colorProvider: true, textDocumentSync: TextDocumentSyncKind.Incremental, - completionProvider: { resolveProvider: true }, workspace: !hasWorkspaceFolderCapability ? undefined : { workspaceFolders: { supported: true } }, }, }; @@ -60,7 +71,7 @@ connection.onDidChangeConfiguration((change) => { documents.all().forEach(validateTextDocument); }); -async function getDocumentSettings(resource: string) { +function _getDocumentSettings(resource: string) { if (!hasConfigurationCapability) return globalSettings; if (!documentSettings.has(resource)) { const settings = connection.workspace.getConfiguration({ scopeUri: resource, section: 'languageServerGem' }); @@ -73,74 +84,47 @@ documents.onDidClose((e) => documentSettings.delete(e.document.uri)); documents.onDidChangeContent((change) => validateTextDocument(change.document)); -async function validateTextDocument(textDocument: TextDocument) { - const settings = await getDocumentSettings(textDocument.uri); - - const text = textDocument.getText(); - const pattern = /\b[A-Z]{20,}\b/g; - let m: RegExpExecArray | null; - - let problems = 0; - const diagnostics: Diagnostic[] = []; - while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) { - problems++; - const diagnostic: Diagnostic = { - severity: DiagnosticSeverity.Warning, - range: { start: textDocument.positionAt(m.index), end: textDocument.positionAt(m.index + m[0].length) }, - message: `${m[0]} is all uppercase.`, - source: 'vscode-gem', - }; - if (hasDiagnosticRelatedInformationCapability) { - diagnostic.relatedInformation = [ - { - location: { - uri: textDocument.uri, - range: Object.assign({}, diagnostic.range), - }, - message: 'Spelling matters', - }, - { - location: { - uri: textDocument.uri, - range: Object.assign({}, diagnostic.range), - }, - message: 'Particularly for names', - }, - ]; - } - diagnostics.push(diagnostic); - } - connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); -} +const validateTextDocument = debounce((textDocument: TextDocument) => { + connection.sendDiagnostics({ + uri: textDocument.uri, + diagnostics: getDiagnostics(textDocument, hasDiagnosticRelatedInformationCapability), + }); +}); connection.onDidChangeWatchedFiles((_change) => { connection.console.log('We received a file change event'); }); -connection.onCompletion((_textDocumentPosition) => { - return [ - { - label: 'TypeScript', - kind: CompletionItemKind.Text, - data: 1, - }, - { - label: 'JavaScript', - kind: CompletionItemKind.Text, - data: 2, - }, - ]; +const completionItemProviders = [ + new CSSCompletionItemProvider(), + new HTMLStyleCompletionItemProvider(), + new HTMLCompletionItemProvider(connection), + new StyleCompletionItemProvider(), +]; +connection.onCompletion(async ({ textDocument, position }) => { + for (const provider of completionItemProviders) { + const result = await provider.provideCompletionItems(documents.get(textDocument.uri)!, position); + if (result) return { isIncomplete: true, items: result.items }; + } +}); + +connection.onCompletionResolve((item) => item); + +const colorProvider = new ColorProvider(); +connection.onColorPresentation(({ color }) => { + return colorProvider.provideColorPresentations(color); +}); + +connection.onDocumentColor(({ textDocument }) => { + return colorProvider.provideDocumentColors(documents.get(textDocument.uri)!); }); -connection.onCompletionResolve((item) => { - if (item.data === 1) { - item.detail = 'TypeScript details'; - item.documentation = 'TypeScript documentation'; - } else if (item.data === 2) { - item.detail = 'JavaScript details'; - item.documentation = 'JavaScript documentation'; +const hoverProviders = [new CSSHoverProvider(), new StyleHoverProvider(), new HTMLHoverProvider()]; +connection.onHover(({ textDocument, position }) => { + for (const provider of hoverProviders) { + const result = provider.provideHover(documents.get(textDocument.uri)!, position); + if (result) return result; } - return item; }); documents.listen(connection); diff --git a/packages/vscode-gem-plugin/src/providers/style.ts b/packages/language-service/src/style.ts similarity index 60% rename from packages/vscode-gem-plugin/src/providers/style.ts rename to packages/language-service/src/style.ts index 322d4705..978ff110 100644 --- a/packages/vscode-gem-plugin/src/providers/style.ts +++ b/packages/language-service/src/style.ts @@ -1,36 +1,23 @@ -import type { - CompletionList, - CompletionItem, - TextDocument, - Position, - CancellationToken, - CompletionItemProvider, -} from 'vscode'; import { getCSSLanguageService as getCSSLanguageService } from 'vscode-css-languageservice'; +import type { Position, TextDocument } from 'vscode-languageserver-textdocument'; -import { matchOffset, createVirtualDocument, translateCompletionList, removeSlot } from '../util'; -import { STYLE_REG } from '../constants'; - +import { matchOffset, createVirtualDocument, removeSlot, translateCompletionList } from './util'; +import { STYLE_REG } from './constants'; import { CompletionsCache } from './cache'; -export class StyleCompletionItemProvider implements CompletionItemProvider { +export class StyleCompletionItemProvider { #cssLanguageService = getCSSLanguageService(); #cache = new CompletionsCache(); - provideCompletionItems(document: TextDocument, position: Position, _token: CancellationToken): CompletionList { + provideCompletionItems(document: TextDocument, position: Position) { const cached = this.#cache.getCached(document, position); if (cached) return cached; - const currentLine = document.lineAt(position.line); - const empty: CompletionList = { isIncomplete: false, items: [] }; - - if (currentLine.isEmptyOrWhitespace) return empty; - const currentOffset = document.offsetAt(position); const documentText = document.getText(); const match = matchOffset(STYLE_REG, documentText, currentOffset); - if (!match) return empty; + if (!match) return; const matchContent = match.groups!.content; const matchStartOffset = match.index + match.groups!.start.length; @@ -44,10 +31,6 @@ export class StyleCompletionItemProvider implements CompletionItemProvider { vCss, ); - return this.#cache.updateCached(document, position, translateCompletionList(completions, currentLine)); - } - - resolveCompletionItem(item: CompletionItem, _token: CancellationToken) { - return item; + return this.#cache.updateCached(document, position, translateCompletionList(completions, position)); } } diff --git a/packages/vscode-gem-plugin/src/util.ts b/packages/language-service/src/util.ts similarity index 54% rename from packages/vscode-gem-plugin/src/util.ts rename to packages/language-service/src/util.ts index 2b05470c..84c01546 100644 --- a/packages/vscode-gem-plugin/src/util.ts +++ b/packages/language-service/src/util.ts @@ -1,8 +1,6 @@ -// eslint-disable-next-line import/no-unresolved -import { Position, Range } from 'vscode'; import { TextDocument } from 'vscode-html-languageservice'; -import type { TextLine, CompletionItem } from 'vscode'; -import type { CompletionList as HtmlCompletionList } from 'vscode-html-languageservice'; +import { Position, Range } from 'vscode-languageserver/node'; +import type { CompletionItem } from 'vscode-languageserver/node'; export function removeSlot(text: string) { const v = text.replace(/\$\{[^${]*?\}/g, (str) => str.replaceAll(/[^\n]/g, 'x')); @@ -19,33 +17,28 @@ export function removeHTMLSlot(text: string, position: number) { return left2 + removeSlot(right); } -export function translateCompletionList(list: HtmlCompletionList, line: TextLine, expand?: boolean) { - return { - ...list, - items: list.items.map((item) => { - const result = item as CompletionItem; - - if (item.textEdit && 'range' in item.textEdit) { - result.range = new Range( - new Position(line.lineNumber, item.textEdit.range.start.character), - new Position(line.lineNumber, item.textEdit.range.end.character), - ); - } - delete result.textEdit; - - if (expand) { - // i use this to both expand html abbreviations and auto complete tags - result.command = { - title: 'Emmet Expand Abbreviation', - command: 'editor.emmet.action.expandAbbreviation', - }; - } +export function translateCompletionList(result: any, position: Position) { + const getRange = (item: CompletionItem): Range | undefined => { + if (item.textEdit && 'range' in item.textEdit) { + const { start, end } = item.textEdit.range; + return Range.create( + Position.create(position.line, start.character), + Position.create(position.line, end.character), + ); + } + }; - return result; - }), + return { + ...result, + items: result?.items.map((item: CompletionItem) => ({ + ...item, + textEdit: item.textEdit && { + ...item.textEdit, + range: getRange(item), + }, + })), }; } - export function matchOffset(regex: RegExp, docText: string, offset: number) { regex.exec('null'); diff --git a/packages/vscode-gem-plugin/package.json b/packages/vscode-gem-plugin/package.json index 41d9718b..a23febc0 100644 --- a/packages/vscode-gem-plugin/package.json +++ b/packages/vscode-gem-plugin/package.json @@ -159,10 +159,8 @@ "publish": "vsce publish --no-dependencies --skip-license" }, "dependencies": { - "@vscode/emmet-helper": "^2.9.3", - "vscode-css-languageservice": "^6.3.1", - "vscode-gem-languageservice": "^0.0.1", - "vscode-html-languageservice": "^5.3.1", + "duoyun-ui": "^2.2.0", + "vscode-gem-languageservice": "^0.0.2", "vscode-languageclient": "^9.0.1" }, "devDependencies": { @@ -171,7 +169,6 @@ "@types/vscode": "^1.94.0", "@vscode/test-cli": "^0.0.10", "@vscode/test-electron": "^2.4.1", - "duoyun-ui": "^2.2.0", "esbuild": "^0.24.0", "typescript": "^5.6.2" }, diff --git a/packages/vscode-gem-plugin/src/constants.ts b/packages/vscode-gem-plugin/src/constants.ts index 5ebd648e..47ccd584 100644 --- a/packages/vscode-gem-plugin/src/constants.ts +++ b/packages/vscode-gem-plugin/src/constants.ts @@ -1,12 +1 @@ -export const COLOR_REG = /(?'|")?(?#([0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4}))($1|\s*;)/g; - -// 直接通过正则匹配 css 片段,通过条件的结束 ` 号匹配 -export const CSS_REG = /(?\/\*\s*css\s*\*\/\s*`|(?.*?)(`(?=;|,?\s*\)))/gs; -// 直接通过正则匹配 style 片段,通过条件的结束 ` 号匹配 -// 语言服务和高亮都只支持 styled 写法 -export const STYLE_REG = /(?\/\*\s*style\s*\*\/\s*`|(?.*?)(`(?=,|\s*}\s*\)))/gs; - -// 处理后进行正则匹配,所以不需要验证后面的 ` 号 -export const HTML_REG = /(?\/\*\s*html\s*\*\/\s*`|(?[^`]*)(`)/g; - export const LANG_SELECTOR = ['typescriptreact', 'javascriptreact', 'typescript', 'javascript']; diff --git a/packages/vscode-gem-plugin/src/diagnostic.ts b/packages/vscode-gem-plugin/src/diagnostic.ts deleted file mode 100644 index c152c907..00000000 --- a/packages/vscode-gem-plugin/src/diagnostic.ts +++ /dev/null @@ -1,68 +0,0 @@ -// 只对 CSS 语法和属性做了简单的检查,不做值检查 -// TODO: 激活扩展、打开工作区时需要自动诊断所有文件 -// TODO: 使用 LRU 缓存 - -// eslint-disable-next-line import/no-unresolved -import { workspace, languages, window, Range, Diagnostic } from 'vscode'; -import { getCSSLanguageService as getCSSLanguageService } from 'vscode-css-languageservice'; -import type { ExtensionContext, TextDocument } from 'vscode'; -import { debounce } from 'duoyun-ui/lib/timer'; - -import { CSS_REG, LANG_SELECTOR, STYLE_REG } from './constants'; -import { createVirtualDocument, removeSlot } from './util'; - -const diagnosticCollection = languages.createDiagnosticCollection('gem'); -const cssLanguageService = getCSSLanguageService(); - -const updateDiagnostic = debounce((document: TextDocument) => { - if (!LANG_SELECTOR.includes(document.languageId)) return; - const diagnostics: Diagnostic[] = []; - const text = document.getText(); - - const matchFragments = (regexp: RegExp, appendBefore: string, appendAfter: string) => { - regexp.exec('null'); - - let match; - while ((match = regexp.exec(text))) { - const matchContent = match.groups!.content; - const offset = match.index + match.groups!.start.length; - const virtualDocument = createVirtualDocument('css', `${appendBefore}${removeSlot(matchContent)}${appendAfter}`); - const vCss = cssLanguageService.parseStylesheet(virtualDocument); - const oDiagnostics = cssLanguageService.doValidation(virtualDocument, vCss) as Diagnostic[]; - for (const { message, range } of oDiagnostics) { - const { start, end } = range; - const startOffset = virtualDocument.offsetAt(start) - appendBefore.length + offset; - const endOffset = virtualDocument.offsetAt(end) - appendBefore.length + offset; - const nRange = new Range(document.positionAt(startOffset), document.positionAt(endOffset)); - diagnostics.push(new Diagnostic(nRange, message)); - } - } - }; - - matchFragments(CSS_REG, '', ''); - matchFragments(STYLE_REG, ':host { ', ' }'); - - diagnosticCollection.set(document.uri, diagnostics); -}); - -export function markDiagnostic(context: ExtensionContext) { - context.subscriptions.push(diagnosticCollection); - - context.subscriptions.push( - window.onDidChangeActiveTextEditor((editor) => { - if (editor) { - updateDiagnostic(editor.document); - } - }), - ); - - context.subscriptions.push( - workspace.onDidChangeTextDocument(({ document }) => { - updateDiagnostic(document); - }), - ); - - if (window.activeTextEditor) { - updateDiagnostic(window.activeTextEditor.document); - } -} diff --git a/packages/vscode-gem-plugin/src/extension.ts b/packages/vscode-gem-plugin/src/extension.ts index 87db3390..19393b06 100644 --- a/packages/vscode-gem-plugin/src/extension.ts +++ b/packages/vscode-gem-plugin/src/extension.ts @@ -1,23 +1,16 @@ import * as path from 'node:path'; // eslint-disable-next-line import/no-unresolved -import { languages, commands, window, workspace } from 'vscode'; +import { commands, window, workspace } from 'vscode'; import type { ExtensionContext } from 'vscode'; import { LanguageClient, TransportKind } from 'vscode-languageclient/node'; -import { HTMLCompletionItemProvider } from './providers/html'; -import { CSSCompletionItemProvider, HTMLStyleCompletionItemProvider } from './providers/css'; -import { StyleCompletionItemProvider } from './providers/style'; -import { ColorProvider } from './providers/color'; -import { HTMLHoverProvider, CSSHoverProvider, StyleHoverProvider } from './providers/hover'; import { markDecorators } from './decorators'; -import { markDiagnostic } from './diagnostic'; import { LANG_SELECTOR } from './constants'; -const triggers = ['!', '.', '}', ':', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; - let client: LanguageClient | undefined; -function useLS(context: ExtensionContext) { + +export function activate(context: ExtensionContext) { const serverModule = context.asAbsolutePath(path.join('dist', 'server.js')); client = new LanguageClient( 'languageServerGem', @@ -36,40 +29,13 @@ function useLS(context: ExtensionContext) { }, ); client.start(); -} -export function activate(context: ExtensionContext) { markDecorators(context); - markDiagnostic(context); - - context.subscriptions.push(languages.registerColorProvider(LANG_SELECTOR, new ColorProvider())); - context.subscriptions.push(languages.registerHoverProvider(LANG_SELECTOR, new HTMLHoverProvider())); - context.subscriptions.push(languages.registerHoverProvider(LANG_SELECTOR, new StyleHoverProvider())); - context.subscriptions.push(languages.registerHoverProvider(LANG_SELECTOR, new CSSHoverProvider())); - context.subscriptions.push( - languages.registerCompletionItemProvider(LANG_SELECTOR, new HTMLCompletionItemProvider(), '<', ...triggers), - ); - context.subscriptions.push( - languages.registerCompletionItemProvider(LANG_SELECTOR, new HTMLStyleCompletionItemProvider(), ...triggers), - ); - context.subscriptions.push( - languages.registerCompletionItemProvider(LANG_SELECTOR, new CSSCompletionItemProvider(), ...triggers), - ); - context.subscriptions.push( - languages.registerCompletionItemProvider(LANG_SELECTOR, new StyleCompletionItemProvider(), ...triggers), - ); - context.subscriptions.push( commands.registerCommand('vscode-plugin-gem.helloWorld', () => { window.showInformationMessage('Hello World from vscode-plugin-gem!'); }), ); - - // TODO: 扩展配置 - const enabledLS = false; - if (enabledLS) { - useLS(context); - } } export function deactivate() { diff --git a/packages/vscode-gem-plugin/src/providers/color.ts b/packages/vscode-gem-plugin/src/providers/color.ts deleted file mode 100644 index 65b30116..00000000 --- a/packages/vscode-gem-plugin/src/providers/color.ts +++ /dev/null @@ -1,30 +0,0 @@ -// eslint-disable-next-line import/no-unresolved -import { ColorPresentation, ColorInformation, Range, Color } from 'vscode'; -import { rgbToHexColor, parseHexColor } from 'duoyun-ui/lib/color'; -import type { HexColor } from 'duoyun-ui/lib/color'; -import type { DocumentColorProvider, TextDocument } from 'vscode'; - -import { COLOR_REG } from '../constants'; - -export class ColorProvider implements DocumentColorProvider { - provideDocumentColors(document: TextDocument) { - COLOR_REG.exec('null'); - - const documentText = document.getText(); - const colors: ColorInformation[] = []; - - let match: RegExpExecArray | null; - while ((match = COLOR_REG.exec(documentText)) !== null) { - const hex = match.groups!.content as HexColor; - const [red, green, blue, alpha] = parseHexColor(hex); - const offset = match.index + (match.groups!.start?.length || 0); - const range = new Range(document.positionAt(offset), document.positionAt(offset + hex.length)); - colors.push(new ColorInformation(range, new Color(red / 255, green / 255, blue / 255, alpha))); - } - return colors; - } - - provideColorPresentations({ red, green, blue, alpha }: Color) { - return [new ColorPresentation(rgbToHexColor([red * 255, green * 255, blue * 255, alpha]))]; - } -} diff --git a/packages/vscode-gem-plugin/src/providers/hover.ts b/packages/vscode-gem-plugin/src/providers/hover.ts deleted file mode 100644 index 13d378b8..00000000 --- a/packages/vscode-gem-plugin/src/providers/hover.ts +++ /dev/null @@ -1,107 +0,0 @@ -// eslint-disable-next-line import/no-unresolved -import { Hover, MarkdownString } from 'vscode'; -import { getLanguageService as getHtmlLanguageService } from 'vscode-html-languageservice'; -import { getCSSLanguageService as getCssLanguageService } from 'vscode-css-languageservice'; -import type { HoverProvider, TextDocument, Position, CancellationToken } from 'vscode'; -import type { Hover as HtmlHover } from 'vscode-html-languageservice'; -import type { LanguageService as CssLanguageService } from 'vscode-css-languageservice'; - -import { createVirtualDocument, matchOffset, removeHTMLSlot, removeSlot } from '../util'; -import { CSS_REG, HTML_REG, STYLE_REG } from '../constants'; - -function translateHover(hover: HtmlHover | null): Hover | null { - if (!hover) return null; - const { contents } = hover; - if (typeof contents === 'object' && 'kind' in contents) { - const markedStr = new MarkdownString(); - if (contents.kind === 'plaintext') { - markedStr.appendText(contents.value); - } else { - markedStr.appendMarkdown(contents.value); - } - return new Hover(markedStr); - } - return new Hover(contents); -} - -export class HTMLHoverProvider implements HoverProvider { - #htmlLanguageService = getHtmlLanguageService(); - - provideHover(document: TextDocument, position: Position, _token: CancellationToken) { - const currentOffset = document.offsetAt(position); - const documentText = removeHTMLSlot(document.getText(), currentOffset); - const match = matchOffset(HTML_REG, documentText, currentOffset); - - if (!match) return null; - - const matchContent = match.groups!.content; - const matchStartOffset = match.index + match.groups!.start.length; - const virtualOffset = currentOffset - matchStartOffset; - const virtualDocument = createVirtualDocument('html', matchContent); - const html = this.#htmlLanguageService.parseHTMLDocument(virtualDocument); - const hover = this.#htmlLanguageService.doHover(virtualDocument, virtualDocument.positionAt(virtualOffset), html, { - documentation: true, - references: true, - }); - - return translateHover(hover); - } -} - -export class CSSHoverProvider implements HoverProvider { - #cssLanguageService: CssLanguageService = getCssLanguageService(); - - provideHover(document: TextDocument, position: Position, _token: CancellationToken) { - const currentOffset = document.offsetAt(position); - const documentText = document.getText(); - const match = matchOffset(CSS_REG, documentText, currentOffset); - - if (!match) return null; - - const matchContent = match.groups!.content; - const matchStartOffset = match.index + match.groups!.start.length; - const virtualOffset = currentOffset - matchStartOffset; - const virtualDocument = createVirtualDocument('css', removeSlot(matchContent)); - const stylesheet = this.#cssLanguageService.parseStylesheet(virtualDocument); - const hover = this.#cssLanguageService.doHover( - virtualDocument, - virtualDocument.positionAt(virtualOffset), - stylesheet, - { - documentation: true, - references: true, - }, - ); - - return translateHover(hover); - } -} - -export class StyleHoverProvider implements HoverProvider { - #cssLanguageService: CssLanguageService = getCssLanguageService(); - - provideHover(document: TextDocument, position: Position, _token: CancellationToken) { - const currentOffset = document.offsetAt(position); - const documentText = document.getText(); - const match = matchOffset(STYLE_REG, documentText, currentOffset); - - if (!match) return null; - - const matchContent = match.groups!.content; - const matchStartOffset = match.index + match.groups!.start.length; - const virtualOffset = currentOffset - matchStartOffset + 8; - const virtualDocument = createVirtualDocument('css', `:host { ${removeSlot(matchContent)} }`); - const stylesheet = this.#cssLanguageService.parseStylesheet(virtualDocument); - const hover = this.#cssLanguageService.doHover( - virtualDocument, - virtualDocument.positionAt(virtualOffset), - stylesheet, - { - documentation: true, - references: true, - }, - ); - - return translateHover(hover); - } -} diff --git a/packages/vscode-gem-plugin/src/providers/html.ts b/packages/vscode-gem-plugin/src/providers/html.ts deleted file mode 100644 index b2212c07..00000000 --- a/packages/vscode-gem-plugin/src/providers/html.ts +++ /dev/null @@ -1,88 +0,0 @@ -// eslint-disable-next-line import/no-unresolved -import { workspace } from 'vscode'; -import type { - CompletionList, - CompletionItem, - TextDocument, - Position, - CancellationToken, - CompletionItemProvider, -} from 'vscode'; -import type { CompletionList as HTMLCompletionList } from 'vscode-html-languageservice'; -import { getLanguageService as getHTMLanguageService } from 'vscode-html-languageservice'; -import { doComplete as doEmmetComplete } from '@vscode/emmet-helper'; -import type { VSCodeEmmetConfig } from '@vscode/emmet-helper'; - -import { matchOffset, createVirtualDocument, translateCompletionList, removeHTMLSlot } from '../util'; -import { HTML_REG } from '../constants'; - -import { CompletionsCache } from './cache'; - -export function getEmmetConfiguration() { - const emmetConfig = workspace.getConfiguration('emmet'); - return { - useNewEmmet: true, - showExpandedAbbreviation: emmetConfig.showExpandedAbbreviation, - showAbbreviationSuggestions: emmetConfig.showAbbreviationSuggestions, - syntaxProfiles: emmetConfig.syntaxProfiles, - variables: emmetConfig.variables, - } as VSCodeEmmetConfig; -} - -export class HTMLCompletionItemProvider implements CompletionItemProvider { - #htmlLanguageService = getHTMLanguageService(); - #cache = new CompletionsCache(); - - provideCompletionItems(document: TextDocument, position: Position, _token: CancellationToken) { - const cached = this.#cache.getCached(document, position); - if (cached) return cached; - - const currentLine = document.lineAt(position.line); - const empty: CompletionList = { isIncomplete: false, items: [] }; - - if (currentLine.isEmptyOrWhitespace) return empty; - - const currentOffset = document.offsetAt(position); - const documentText = removeHTMLSlot(document.getText(), currentOffset); - const match = matchOffset(HTML_REG, documentText, currentOffset); - - if (!match) return empty; - - const matchContent = match.groups!.content; - const matchStartOffset = match.index + match.groups!.start.length; - const virtualOffset = currentOffset - matchStartOffset; - const virtualDocument = createVirtualDocument('html', matchContent); - const vHtml = this.#htmlLanguageService.parseHTMLDocument(virtualDocument); - - let emmetResults: HTMLCompletionList = { isIncomplete: true, items: [] }; - this.#htmlLanguageService.setCompletionParticipants([ - { - onHtmlContent: () => - (emmetResults = - doEmmetComplete( - virtualDocument, - virtualDocument.positionAt(virtualOffset), - 'html', - getEmmetConfiguration(), - ) || emmetResults), - }, - ]); - - const completions = this.#htmlLanguageService.doComplete( - virtualDocument, - virtualDocument.positionAt(virtualOffset), - vHtml, - ); - - if (emmetResults.items.length) { - completions.isIncomplete = true; - completions.items.push(...emmetResults.items); - } - - return this.#cache.updateCached(document, position, translateCompletionList(completions, currentLine, true)); - } - - resolveCompletionItem(item: CompletionItem, _token: CancellationToken) { - return item; - } -} diff --git a/packages/vscode-gem-plugin/src/test/extension.test.ts b/packages/vscode-gem-plugin/src/test/extension.test.ts index 0c957282..5624f8ae 100644 --- a/packages/vscode-gem-plugin/src/test/extension.test.ts +++ b/packages/vscode-gem-plugin/src/test/extension.test.ts @@ -1,6 +1,6 @@ // https://code.visualstudio.com/api/language-extensions/language-server-extension-guide#testing-the-language-server -import * as assert from 'assert'; +import * as assert from 'node:assert'; // eslint-disable-next-line import/no-unresolved import * as vscode from 'vscode'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7baae80f..e87fecc1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -334,6 +334,12 @@ importers: packages/language-service: dependencies: + '@vscode/emmet-helper': + specifier: ^2.9.3 + version: 2.9.3 + duoyun-ui: + specifier: ^2.2.0 + version: link:../duoyun-ui gem-analyzer: specifier: ^2.2.0 version: link:../gem-analyzer @@ -343,6 +349,12 @@ importers: typescript: specifier: ^5.6.2 version: 5.6.2 + vscode-css-languageservice: + specifier: ^6.3.1 + version: 6.3.1 + vscode-html-languageservice: + specifier: ^5.3.1 + version: 5.3.1 vscode-languageserver: specifier: ^9.0.1 version: 9.0.1 @@ -356,18 +368,12 @@ importers: packages/vscode-gem-plugin: dependencies: - '@vscode/emmet-helper': - specifier: ^2.9.3 - version: 2.9.3 - vscode-css-languageservice: - specifier: ^6.3.1 - version: 6.3.1 + duoyun-ui: + specifier: ^2.2.0 + version: link:../duoyun-ui vscode-gem-languageservice: specifier: ^0.0.1 version: link:../language-service - vscode-html-languageservice: - specifier: ^5.3.1 - version: 5.3.1 vscode-languageclient: specifier: ^9.0.1 version: 9.0.1 @@ -387,9 +393,6 @@ importers: '@vscode/test-electron': specifier: ^2.4.1 version: 2.4.1 - duoyun-ui: - specifier: ^2.2.0 - version: link:../duoyun-ui esbuild: specifier: ^0.24.0 version: 0.24.0 @@ -1022,28 +1025,24 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [musl] '@biomejs/cli-linux-arm64@1.8.3': resolution: {integrity: sha512-fed2ji8s+I/m8upWpTJGanqiJ0rnlHOK3DdxsyVLZQ8ClY6qLuPc9uehCREBifRJLl/iJyQpHIRufLDeotsPtw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [glibc] '@biomejs/cli-linux-x64-musl@1.8.3': resolution: {integrity: sha512-UHrGJX7PrKMKzPGoEsooKC9jXJMa28TUSMjcIlbDnIO4EAavCoVmNQaIuUSH0Ls2mpGMwUIf+aZJv657zfWWjA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [musl] '@biomejs/cli-linux-x64@1.8.3': resolution: {integrity: sha512-I8G2QmuE1teISyT8ie1HXsjFRz9L1m5n83U1O6m30Kw+kPMPSKjag6QGUn+sXT8V+XWIZxFFBoTDEDZW2KPDDw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [glibc] '@biomejs/cli-win32-arm64@1.8.3': resolution: {integrity: sha512-J+Hu9WvrBevfy06eU1Na0lpc7uR9tibm9maHynLIoAjLZpQU3IW+OKHUtyL8p6/3pT2Ju5t5emReeIS2SAxhkQ==} @@ -1646,28 +1645,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@nx/nx-linux-arm64-musl@19.6.5': resolution: {integrity: sha512-MgidKilQ0KWxQbTnaqXGjASu7wtAC9q6zAwFNKFENkwJq3nThaQH6jQVlnINE4lL9NSgyyg0AS/ix31hiqAgvA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@nx/nx-linux-x64-gnu@19.6.5': resolution: {integrity: sha512-rGDylAoslIlk5TDbEJ6YoQOYxxYP9gCpi6FLke2mFgXVzOmVlLKHfVsegIHYVMYYF26h3NJh0NLGGzGdoBjWgQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@nx/nx-linux-x64-musl@19.6.5': resolution: {integrity: sha512-C/pNjDL/bDEcrDypgBo4r1AOiPTk8gWJwBsFE1QHIvg7//5WFSreqRj34rJu/GZ95eLYJH5tje1VW6z+atEGkQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@nx/nx-win32-arm64-msvc@19.6.5': resolution: {integrity: sha512-mMi8i16OFux17xed2iLPWwUdCbS1mYA9Ny/gnoNUCosmihmXX9wrzaGBkNAMsHA28huYQtPhGormsEs+zuiVFg==} @@ -1845,55 +1840,46 @@ packages: resolution: {integrity: sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.21.2': resolution: {integrity: sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.21.2': resolution: {integrity: sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.21.2': resolution: {integrity: sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-powerpc64le-gnu@4.21.2': resolution: {integrity: sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.21.2': resolution: {integrity: sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.21.2': resolution: {integrity: sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.21.2': resolution: {integrity: sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.21.2': resolution: {integrity: sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.21.2': resolution: {integrity: sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ==} @@ -1924,25 +1910,21 @@ packages: resolution: {integrity: sha512-svPOFlem7s6T33tX8a28uD5Ngc7bdML96ioiH7Fhi0J/at+WAthor4GeUNwkwuzBQI/Nc9XCgiYPcE0pzP7c6w==} cpu: [arm64] os: [linux] - libc: [glibc] '@rspack/binding-linux-arm64-musl@1.0.5': resolution: {integrity: sha512-cysqogEUNc0TgzzXcK9bkv12eoCjqhLzOvGXQU1zSEU9Hov7tuzMDl3Z6R3A7NgOCmWu84/wOnTrkSOI28caew==} cpu: [arm64] os: [linux] - libc: [musl] '@rspack/binding-linux-x64-gnu@1.0.5': resolution: {integrity: sha512-qIEMsWOzTKpVm0Sg553gKkua49Kd/sElLD1rZcXjjxjAsD97uq8AiNncArMfYdDKgkKbtwtW/Fb3uVuafTLnZg==} cpu: [x64] os: [linux] - libc: [glibc] '@rspack/binding-linux-x64-musl@1.0.5': resolution: {integrity: sha512-yulltMSQN3aBt3NMURYTmJcpAJBi4eEJ4i9qF0INE8f0885sJpI0j35/31POkCghG1ZOSZkYALFrheKKP9e8pg==} cpu: [x64] os: [linux] - libc: [musl] '@rspack/binding-win32-arm64-msvc@1.0.5': resolution: {integrity: sha512-5oF/qN6TnUj28UAdaOgSIWKq7HG5QgI4p37zvQBBTXZHhrwN2kE6H+TaofWnSqWJynwmGIxJIx8bGo3lDfFbfA==} @@ -2046,28 +2028,24 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [glibc] '@swc/core-linux-arm64-musl@1.7.24': resolution: {integrity: sha512-0jJx0IcajcyOXaJsx1jXy86lYVrbupyy2VUj/OiJux/ic4oBJLjfL+WOuc8T8/hZj2p6X0X4jvfSCqWSuic4kA==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [musl] '@swc/core-linux-x64-gnu@1.7.24': resolution: {integrity: sha512-2+3aKQpSGjVnWKDTKUPuJzitQlTQrGorg+PVFMRkv6l+RcNCHZQNe/8VYpMhyBhxDMb3LUlbp7776FRevcruxg==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [glibc] '@swc/core-linux-x64-musl@1.7.24': resolution: {integrity: sha512-PMQ6SkCtMoj0Ks77DiishpEmIuHpYjFLDuVOzzJCzGeGoii0yRP5lKy/VeglFYLPqJzmhK9BHlpVehVf/8ZpvA==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [musl] '@swc/core-win32-arm64-msvc@1.7.24': resolution: {integrity: sha512-SNdCa4DtGXNWrPVHqctVUxgEVZVETuqERpqF50KFHO0Bvf5V/m1IJ4hFr2BxXlrzgnIW4t1Dpi6YOJbcGbEmnA==} @@ -2497,10 +2475,10 @@ packages: resolution: {integrity: sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==} '@xtuc/ieee754@1.2.0': - resolution: {integrity: sha1-7vAUoxRa5Hehy8AM0eVSM23Ot5A=} + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} '@xtuc/long@4.2.2': - resolution: {integrity: sha1-0pHGpOl5ibXGHZrPOWrk/hM6cY0=} + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} '@yarnpkg/lockfile@1.1.0': resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} @@ -3647,7 +3625,7 @@ packages: engines: {node: '>=12'} dtrace-provider@0.8.8: - resolution: {integrity: sha1-KZbVSQw34TR74mO0I+17KX+w2X4=} + resolution: {integrity: sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==} engines: {node: '>=0.10'} duplexer@0.1.2: @@ -3700,7 +3678,7 @@ packages: resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==} encoding@0.1.13: - resolution: {integrity: sha1-VldK/deR9UqOmyeFwFgqLSYhD6k=} + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} @@ -3884,7 +3862,7 @@ packages: optional: true eslint-scope@5.1.1: - resolution: {integrity: sha1-54blmmbLkrP2wfsNUIqrF0hI9Iw=} + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} eslint-scope@7.2.2: @@ -3926,7 +3904,7 @@ packages: engines: {node: '>=4.0'} estraverse@4.3.0: - resolution: {integrity: sha1-OYrT88WiSUi+dyXoPRGn3ijNvR0=} + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} engines: {node: '>=4.0'} estraverse@5.3.0: @@ -5536,7 +5514,7 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} mv@2.1.1: - resolution: {integrity: sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=} + resolution: {integrity: sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==} engines: {node: '>=0.8.0'} mz@2.7.0: @@ -6521,7 +6499,7 @@ packages: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} safe-json-stringify@1.2.0: - resolution: {integrity: sha1-NW5EvJjx+TzkXfFLzXwBzahuCv0=} + resolution: {integrity: sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==} safe-regex-test@1.0.3: resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} @@ -11952,7 +11930,7 @@ snapshots: debug: 4.3.7(supports-color@8.1.1) enhanced-resolve: 5.17.1 eslint: 8.57.0 - eslint-module-utils: 2.11.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) + eslint-module-utils: 2.11.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.8.0 is-bun-module: 1.2.1 @@ -11965,7 +11943,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.11.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0): + eslint-module-utils@2.11.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7(supports-color@5.5.0) optionalDependencies: @@ -11987,7 +11965,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.11.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) + eslint-module-utils: 2.11.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3