From 384d2800930169c595fe07522cb6c50d76a61585 Mon Sep 17 00:00:00 2001 From: KazariEX <1364035137@qq.com> Date: Sun, 2 Mar 2025 01:49:28 +0800 Subject: [PATCH] feat: make `IHTMLDataProvider` async --- src/htmlLanguageService.ts | 12 +- src/htmlLanguageTypes.ts | 6 +- src/languageFacts/dataManager.ts | 13 +- src/parser/htmlParser.ts | 4 +- src/services/htmlCompletion.ts | 66 +++---- src/services/htmlFolding.ts | 4 +- src/services/htmlHover.ts | 23 +-- src/services/htmlSelectionRange.ts | 4 +- src/test/completion.test.ts | 232 ++++++++++++------------- src/test/completionParticipant.test.ts | 52 +++--- src/test/completionUtil.ts | 16 +- src/test/customProviders.test.ts | 30 ++-- src/test/folding.test.ts | 64 +++---- src/test/highlighting.test.ts | 72 ++++---- src/test/hover.test.ts | 48 ++--- src/test/hoverUtil.ts | 12 +- src/test/linkedEditing.test.ts | 42 ++--- src/test/matchingTagPosition.test.ts | 36 ++-- src/test/parser.test.ts | 111 ++++++------ src/test/pathCompletions.test.ts | 2 +- src/test/rename.test.ts | 70 ++++---- src/test/selectionRange.test.ts | 82 ++++----- src/test/symbols.test.ts | 36 ++-- 23 files changed, 523 insertions(+), 514 deletions(-) diff --git a/src/htmlLanguageService.ts b/src/htmlLanguageService.ts index e6672958..fbcc3ff3 100644 --- a/src/htmlLanguageService.ts +++ b/src/htmlLanguageService.ts @@ -30,20 +30,20 @@ export * from './htmlLanguageTypes'; export interface LanguageService { setDataProviders(useDefaultDataProvider: boolean, customDataProviders: IHTMLDataProvider[]): void; createScanner(input: string, initialOffset?: number): Scanner; - parseHTMLDocument(document: TextDocument): HTMLDocument; + parseHTMLDocument(document: TextDocument): Promise; findDocumentHighlights(document: TextDocument, position: Position, htmlDocument: HTMLDocument): DocumentHighlight[]; - doComplete(document: TextDocument, position: Position, htmlDocument: HTMLDocument, options?: CompletionConfiguration): CompletionList; + doComplete(document: TextDocument, position: Position, htmlDocument: HTMLDocument, options?: CompletionConfiguration): Promise; doComplete2(document: TextDocument, position: Position, htmlDocument: HTMLDocument, documentContext: DocumentContext, options?: CompletionConfiguration): Promise; setCompletionParticipants(registeredCompletionParticipants: ICompletionParticipant[]): void; - doHover(document: TextDocument, position: Position, htmlDocument: HTMLDocument, options?: HoverSettings): Hover | null; + doHover(document: TextDocument, position: Position, htmlDocument: HTMLDocument, options?: HoverSettings): Promise; format(document: TextDocument, range: Range | undefined, options: HTMLFormatConfiguration): TextEdit[]; findDocumentLinks(document: TextDocument, documentContext: DocumentContext): DocumentLink[]; findDocumentSymbols(document: TextDocument, htmlDocument: HTMLDocument): SymbolInformation[]; findDocumentSymbols2(document: TextDocument, htmlDocument: HTMLDocument): DocumentSymbol[]; doQuoteComplete(document: TextDocument, position: Position, htmlDocument: HTMLDocument, options?: CompletionConfiguration): string | null; - doTagComplete(document: TextDocument, position: Position, htmlDocument: HTMLDocument): string | null; - getFoldingRanges(document: TextDocument, context?: { rangeLimit?: number }): FoldingRange[]; - getSelectionRanges(document: TextDocument, positions: Position[]): SelectionRange[]; + doTagComplete(document: TextDocument, position: Position, htmlDocument: HTMLDocument): Promise; + getFoldingRanges(document: TextDocument, context?: { rangeLimit?: number }): Promise; + getSelectionRanges(document: TextDocument, positions: Position[]): Promise; doRename(document: TextDocument, position: Position, newName: string, htmlDocument: HTMLDocument): WorkspaceEdit | null; findMatchingTagPosition(document: TextDocument, position: Position, htmlDocument: HTMLDocument): Position | null; /** Deprecated, Use findLinkedEditingRanges instead */ diff --git a/src/htmlLanguageTypes.ts b/src/htmlLanguageTypes.ts index db120260..d82c0abf 100644 --- a/src/htmlLanguageTypes.ts +++ b/src/htmlLanguageTypes.ts @@ -193,9 +193,9 @@ export interface IHTMLDataProvider { getId(): string; isApplicable(languageId: string): boolean; - provideTags(): ITagData[]; - provideAttributes(tag: string): IAttributeData[]; - provideValues(tag: string, attribute: string): IValueData[]; + provideTags(): Promise | ITagData[]; + provideAttributes(tag: string): Promise | IAttributeData[]; + provideValues(tag: string, attribute: string): Promise | IValueData[]; } /** diff --git a/src/languageFacts/dataManager.ts b/src/languageFacts/dataManager.ts index b9184f8b..726f45b5 100644 --- a/src/languageFacts/dataManager.ts +++ b/src/languageFacts/dataManager.ts @@ -30,14 +30,15 @@ export class HTMLDataManager { return !!e && arrays.binarySearch(voidElements, e.toLowerCase(), (s1: string, s2: string) => s1.localeCompare(s2)) >= 0; } - getVoidElements(languageId: string): string[]; - getVoidElements(dataProviders: IHTMLDataProvider[]): string[]; - getVoidElements(languageOrProviders: string | IHTMLDataProvider[]): string[] { + getVoidElements(languageId: string): Promise; + getVoidElements(dataProviders: IHTMLDataProvider[]): Promise; + async getVoidElements(languageOrProviders: string | IHTMLDataProvider[]): Promise { const dataProviders = Array.isArray(languageOrProviders) ? languageOrProviders : this.getDataProviders().filter(p => p.isApplicable(languageOrProviders!)); const voidTags: string[] = []; - dataProviders.forEach((provider) => { - provider.provideTags().filter(tag => tag.void).forEach(tag => voidTags.push(tag.name)); - }); + for (const provider of dataProviders) { + const tags = await provider.provideTags(); + tags.filter(tag => tag.void).forEach(tag => voidTags.push(tag.name)); + } return voidTags.sort(); } diff --git a/src/parser/htmlParser.ts b/src/parser/htmlParser.ts index 675d2ef3..4b639000 100644 --- a/src/parser/htmlParser.ts +++ b/src/parser/htmlParser.ts @@ -68,8 +68,8 @@ export class HTMLParser { } - public parseDocument(document: TextDocument): HTMLDocument { - return this.parse(document.getText(), this.dataManager.getVoidElements(document.languageId)); + public async parseDocument(document: TextDocument): Promise { + return this.parse(document.getText(), await this.dataManager.getVoidElements(document.languageId)); } public parse(text: string, voidElements: string[]): HTMLDocument { diff --git a/src/services/htmlCompletion.ts b/src/services/htmlCompletion.ts index df4694df..ad275ab7 100644 --- a/src/services/htmlCompletion.ts +++ b/src/services/htmlCompletion.ts @@ -40,7 +40,7 @@ export class HTMLCompletion { const contributedParticipants = this.completionParticipants; this.completionParticipants = [participant as ICompletionParticipant].concat(contributedParticipants); - const result = this.doComplete(document, position, htmlDocument, settings); + const result = await this.doComplete(document, position, htmlDocument, settings); try { const pathCompletionResult = await participant.computeCompletions(document, documentContext); return { @@ -52,12 +52,12 @@ export class HTMLCompletion { } } - doComplete(document: TextDocument, position: Position, htmlDocument: HTMLDocument, settings?: CompletionConfiguration): CompletionList { - const result = this._doComplete(document, position, htmlDocument, settings); + async doComplete(document: TextDocument, position: Position, htmlDocument: HTMLDocument, settings?: CompletionConfiguration): Promise { + const result = await this._doComplete(document, position, htmlDocument, settings); return this.convertCompletionList(result); } - private _doComplete(document: TextDocument, position: Position, htmlDocument: HTMLDocument, settings?: CompletionConfiguration): CompletionList { + private async _doComplete(document: TextDocument, position: Position, htmlDocument: HTMLDocument, settings?: CompletionConfiguration): Promise { const result: CompletionList = { isIncomplete: false, items: [] @@ -86,10 +86,11 @@ export class HTMLCompletion { return { start: document.positionAt(replaceStart), end: document.positionAt(replaceEnd) }; } - function collectOpenTagSuggestions(afterOpenBracket: number, tagNameEnd?: number): CompletionList { + async function collectOpenTagSuggestions(afterOpenBracket: number, tagNameEnd?: number): Promise { const range = getReplaceRange(afterOpenBracket, tagNameEnd); - dataProviders.forEach((provider) => { - provider.provideTags().forEach(tag => { + for (const provider of dataProviders) { + const tags = await provider.provideTags(); + for (const tag of tags) { result.items.push({ label: tag.name, kind: CompletionItemKind.Property, @@ -97,8 +98,8 @@ export class HTMLCompletion { textEdit: TextEdit.replace(range, tag.name), insertTextFormat: InsertTextFormat.PlainText }); - }); - }); + } + } return result; } @@ -117,7 +118,7 @@ export class HTMLCompletion { return text.substring(0, offset); } - function collectCloseTagSuggestions(afterOpenBracket: number, inOpenTag: boolean, tagNameEnd: number = offset): CompletionList { + async function collectCloseTagSuggestions(afterOpenBracket: number, inOpenTag: boolean, tagNameEnd: number = offset): Promise { const range = getReplaceRange(afterOpenBracket, tagNameEnd); const closeTag = isFollowedBy(text, tagNameEnd, ScannerState.WithinEndTag, TokenType.EndTagClose) ? '' : '>'; let curr: Node | undefined = node; @@ -150,26 +151,27 @@ export class HTMLCompletion { return result; } - dataProviders.forEach(provider => { - provider.provideTags().forEach(tag => { + for (const provider of dataProviders) { + const tags = await provider.provideTags(); + for (const tag of tags) { result.items.push({ label: '/' + tag.name, kind: CompletionItemKind.Property, documentation: generateDocumentation(tag, undefined, doesSupportMarkdown), - filterText: '/' + tag.name + closeTag, + filterText: '/' + tag.name, textEdit: TextEdit.replace(range, '/' + tag.name + closeTag), insertTextFormat: InsertTextFormat.PlainText }); - }); - }); + } + } return result; } - const collectAutoCloseTagSuggestion = (tagCloseEnd: number, tag: string): CompletionList => { + const collectAutoCloseTagSuggestion = async (tagCloseEnd: number, tag: string): Promise => { if (settings && settings.hideAutoCompleteProposals) { return result; } - voidElements ??= this.dataManager.getVoidElements(dataProviders); + voidElements ??= await this.dataManager.getVoidElements(dataProviders); if (!this.dataManager.isVoidElement(tag, voidElements)) { const pos = document.positionAt(tagCloseEnd); result.items.push({ @@ -197,7 +199,7 @@ export class HTMLCompletion { return existingAttributes; } - function collectAttributeNameSuggestions(nameStart: number, nameEnd: number = offset): CompletionList { + async function collectAttributeNameSuggestions(nameStart: number, nameEnd: number = offset): Promise { let replaceEnd = offset; while (replaceEnd < nameEnd && text[replaceEnd] !== '<') { // < is a valid attribute name character, but we rather assume the attribute name ends. See #23236. replaceEnd++; @@ -220,10 +222,11 @@ export class HTMLCompletion { // include current typing attribute seenAttributes[currentAttribute] = false; - dataProviders.forEach(provider => { - provider.provideAttributes(currentTag).forEach(attr => { + for (const provider of dataProviders) { + const attributes = await provider.provideAttributes(currentTag); + for (const attr of attributes) { if (seenAttributes[attr.name]) { - return; + continue; } seenAttributes[attr.name] = true; @@ -247,8 +250,8 @@ export class HTMLCompletion { insertTextFormat: InsertTextFormat.Snippet, command }); - }); - }); + } + } collectDataAttributesSuggestions(range, seenAttributes); return result; } @@ -280,7 +283,7 @@ export class HTMLCompletion { })); } - function collectAttributeValueSuggestions(valueStart: number, valueEnd: number = offset): CompletionList { + async function collectAttributeValueSuggestions(valueStart: number, valueEnd: number = offset): Promise { let range: Range; let addQuotes: boolean; let valuePrefix: string; @@ -315,20 +318,21 @@ export class HTMLCompletion { } } - dataProviders.forEach(provider => { - provider.provideValues(currentTag, currentAttributeName).forEach(value => { + for (const provider of dataProviders) { + const values = await provider.provideValues(currentTag, currentAttributeName); + for (const value of values) { const insertText = addQuotes ? '"' + value.name + '"' : value.name; result.items.push({ label: value.name, - filterText: insertText, kind: CompletionItemKind.Unit, documentation: generateDocumentation(value, undefined, doesSupportMarkdown), textEdit: TextEdit.replace(range, insertText), insertTextFormat: InsertTextFormat.PlainText }); - }); - }); + } + + } collectCharacterEntityProposals(); return result; } @@ -528,7 +532,7 @@ export class HTMLCompletion { return null; } - doTagComplete(document: TextDocument, position: Position, htmlDocument: HTMLDocument): string | null { + async doTagComplete(document: TextDocument, position: Position, htmlDocument: HTMLDocument): Promise { const offset = document.offsetAt(position); if (offset <= 0) { return null; @@ -537,7 +541,7 @@ export class HTMLCompletion { if (char === '>') { const node = htmlDocument.findNodeBefore(offset); if (node && node.tag && node.start < offset && (!node.endTagStart || node.endTagStart > offset)) { - const voidElements = this.dataManager.getVoidElements(document.languageId); + const voidElements = await this.dataManager.getVoidElements(document.languageId); if (!this.dataManager.isVoidElement(node.tag, voidElements)) { const scanner = createScanner(document.getText(), node.start); let token = scanner.scan(); diff --git a/src/services/htmlFolding.ts b/src/services/htmlFolding.ts index 127b9489..ee04ce26 100644 --- a/src/services/htmlFolding.ts +++ b/src/services/htmlFolding.ts @@ -83,7 +83,7 @@ export class HTMLFolding { return result; } - public getFoldingRanges(document: TextDocument, context: { rangeLimit?: number } | undefined): FoldingRange[] { + public async getFoldingRanges(document: TextDocument, context: { rangeLimit?: number } | undefined): Promise { const scanner = createScanner(document.getText()); let token = scanner.scan(); const ranges: FoldingRange[] = []; @@ -114,7 +114,7 @@ export class HTMLFolding { if (!lastTagName) { break; } - voidElements ??= this.dataManager.getVoidElements(document.languageId); + voidElements ??= await this.dataManager.getVoidElements(document.languageId); if (!this.dataManager.isVoidElement(lastTagName, voidElements)) { break; } diff --git a/src/services/htmlHover.ts b/src/services/htmlHover.ts index 141dcc3f..991df0d5 100644 --- a/src/services/htmlHover.ts +++ b/src/services/htmlHover.ts @@ -18,7 +18,7 @@ export class HTMLHover { constructor(private lsOptions: LanguageServiceOptions, private dataManager: HTMLDataManager) { } - doHover(document: TextDocument, position: Position, htmlDocument: HTMLDocument, options?: HoverSettings): Hover | null { + async doHover(document: TextDocument, position: Position, htmlDocument: HTMLDocument, options?: HoverSettings): Promise { const convertContents = this.convertContents.bind(this); const doesSupportMarkdown = this.doesSupportMarkdown(); @@ -30,11 +30,12 @@ export class HTMLHover { } const dataProviders = this.dataManager.getDataProviders().filter(p => p.isApplicable(document.languageId)); - function getTagHover(currTag: string, range: Range, open: boolean): Hover | null { + async function getTagHover(currTag: string, range: Range, open: boolean): Promise { for (const provider of dataProviders) { let hover: Hover | null = null; - provider.provideTags().forEach(tag => { + const tags = await provider.provideTags(); + for (const tag of tags) { if (tag.name.toLowerCase() === currTag.toLowerCase()) { let markupContent = generateDocumentation(tag, options, doesSupportMarkdown); if (!markupContent) { @@ -45,7 +46,7 @@ export class HTMLHover { } hover = { contents: markupContent, range }; } - }); + } if (hover) { (hover as Hover).contents = convertContents((hover as Hover).contents); @@ -55,11 +56,12 @@ export class HTMLHover { return null; } - function getAttrHover(currTag: string, currAttr: string, range: Range): Hover | null { + async function getAttrHover(currTag: string, currAttr: string, range: Range): Promise { for (const provider of dataProviders) { let hover: Hover | null = null; - provider.provideAttributes(currTag).forEach(attr => { + const attributes = await provider.provideAttributes(currTag); + for (const attr of attributes) { if (currAttr === attr.name && attr.description) { const contentsDoc = generateDocumentation(attr, options, doesSupportMarkdown); if (contentsDoc) { @@ -68,7 +70,7 @@ export class HTMLHover { hover = null; } } - }); + } if (hover) { (hover as Hover).contents = convertContents((hover as Hover).contents); @@ -78,11 +80,12 @@ export class HTMLHover { return null; } - function getAttrValueHover(currTag: string, currAttr: string, currAttrValue: string, range: Range): Hover | null { + async function getAttrValueHover(currTag: string, currAttr: string, currAttrValue: string, range: Range): Promise { for (const provider of dataProviders) { let hover: Hover | null = null; - provider.provideValues(currTag, currAttr).forEach(attrValue => { + const values = await provider.provideValues(currTag, currAttr); + for (const attrValue of values) { if (currAttrValue === attrValue.name && attrValue.description) { const contentsDoc = generateDocumentation(attrValue, options, doesSupportMarkdown); if (contentsDoc) { @@ -91,7 +94,7 @@ export class HTMLHover { hover = null; } } - }); + } if (hover) { (hover as Hover).contents = convertContents((hover as Hover).contents); diff --git a/src/services/htmlSelectionRange.ts b/src/services/htmlSelectionRange.ts index a41e4ac7..2168b84c 100644 --- a/src/services/htmlSelectionRange.ts +++ b/src/services/htmlSelectionRange.ts @@ -12,8 +12,8 @@ export class HTMLSelectionRange { constructor(private htmlParser: HTMLParser) { } - public getSelectionRanges(document: TextDocument, positions: Position[]): SelectionRange[] { - const htmlDocument = this.htmlParser.parseDocument(document); + public async getSelectionRanges(document: TextDocument, positions: Position[]): Promise { + const htmlDocument = await this.htmlParser.parseDocument(document); return positions.map(p => this.getSelectionRange(p, document, htmlDocument)); } private getSelectionRange(position: Position, document: TextDocument, htmlDocument: HTMLDocument): SelectionRange { diff --git a/src/test/completion.test.ts b/src/test/completion.test.ts index 249c07ed..76fdadb0 100644 --- a/src/test/completion.test.ts +++ b/src/test/completion.test.ts @@ -6,8 +6,8 @@ import { testCompletionFor, testQuoteCompletion, testTagCompletion } from "./completionUtil"; suite('HTML Completion', () => { - test('Complete', function (): any { - testCompletionFor('<|', { + test('Complete', async () => { + await testCompletionFor('<|', { items: [ { label: '!DOCTYPE', resultText: '' }, { label: 'iframe', resultText: ' { ] }); - testCompletionFor('\n<|', { + await testCompletionFor('\n<|', { items: [{ label: '!DOCTYPE', notAvailable: true }, { label: 'iframe' }, { label: 'h1' }, { label: 'div' }] }); - testCompletionFor('< |', { + await testCompletionFor('< |', { items: [ { label: 'iframe', resultText: ' { ] }); - testCompletionFor(' { ] }); - testCompletionFor(' { ] }); - testCompletionFor(' { ] }); - testCompletionFor(' { ] }); - testCompletionFor(' { ] }); - testCompletionFor(' { ] }); - testCompletionFor('', { + await testCompletionFor('
', { items: [ { label: 'ltr', resultText: '
' }, { label: 'rtl', resultText: '
' } ] }); - testCompletionFor('