From a2f53794bbdfc01cdbd145610da3cbd2ab748379 Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Mon, 31 May 2021 21:09:34 +0800 Subject: [PATCH] feat: support for TS 4.3 close #221 --- .../src/protocol.d.ts | 6 + .../src/services/completion.ts | 15 ++- .../src/services/completionResolve.ts | 12 +- .../src/services/hover.ts | 4 +- .../src/utils/previewer.ts | 127 ++++++++++++++---- .../src/services/completion.ts | 7 +- .../src/sourceFile.ts | 14 +- yarn.lock | 6 +- 8 files changed, 140 insertions(+), 51 deletions(-) diff --git a/packages/vscode-typescript-languageservice/src/protocol.d.ts b/packages/vscode-typescript-languageservice/src/protocol.d.ts index e81fe81f2d..853fd543f3 100644 --- a/packages/vscode-typescript-languageservice/src/protocol.d.ts +++ b/packages/vscode-typescript-languageservice/src/protocol.d.ts @@ -6,7 +6,13 @@ declare enum ServerType { Semantic = 'semantic', } declare module 'typescript/lib/protocol' { + interface Response { readonly _serverType?: ServerType; } + + interface JSDocLinkDisplayPart { + target: Proto.FileSpan; + } } + diff --git a/packages/vscode-typescript-languageservice/src/services/completion.ts b/packages/vscode-typescript-languageservice/src/services/completion.ts index f39e5fa052..159affba9f 100644 --- a/packages/vscode-typescript-languageservice/src/services/completion.ts +++ b/packages/vscode-typescript-languageservice/src/services/completion.ts @@ -40,7 +40,7 @@ export function register(languageService: ts.LanguageService, getTextDocument: ( let item: CompletionItem = { label: entry.name, labelDetails: { - qualifier: entry.source && path.isAbsolute(entry.source) ? path.relative(rootDir, entry.source) : undefined, + qualifier: entry.source && path.isAbsolute(entry.source) ? path.relative(rootDir, entry.source) : entry.source, }, kind: convertKind(entry.kind), sortText: entry.sortText, @@ -48,11 +48,14 @@ export function register(languageService: ts.LanguageService, getTextDocument: ( preselect: entry.isRecommended, commitCharacters: getCommitCharacters(entry, info.isNewIdentifierLocation), data: { - fileName, - offset, - source: entry.source, - name: entry.name, - options: _options, + __volar__: { + fileName, + offset, + source: entry.source, + name: entry.name, + options: _options, + }, + ...entry.data, }, } diff --git a/packages/vscode-typescript-languageservice/src/services/completionResolve.ts b/packages/vscode-typescript-languageservice/src/services/completionResolve.ts index 75259cd5f3..0649373997 100644 --- a/packages/vscode-typescript-languageservice/src/services/completionResolve.ts +++ b/packages/vscode-typescript-languageservice/src/services/completionResolve.ts @@ -6,14 +6,14 @@ import { handleKindModifiers } from './completion'; export function register(languageService: ts.LanguageService, getTextDocument: (uri: string) => TextDocument | undefined, ts: typeof import('typescript')) { return (item: CompletionItem, newOffset?: number): CompletionItem => { - const fileName = item.data.fileName; - const offset = newOffset ?? item.data.offset; - const name = item.data.name; - const source = item.data.source; - const options = item.data.options; + const fileName = item.data.__volar__.fileName; + const offset = newOffset ?? item.data.__volar__.offset; + const name = item.data.__volar__.name; + const source = item.data.__volar__.source; + const options = item.data.__volar__.options; let detail: ts.CompletionEntryDetails | undefined; try { - detail = languageService.getCompletionEntryDetails(fileName, offset, name, {}, source, options); + detail = languageService.getCompletionEntryDetails(fileName, offset, name, {}, source, options, item.data); } catch (err) { item.detail = `[TS Error] ${err}`; diff --git a/packages/vscode-typescript-languageservice/src/services/hover.ts b/packages/vscode-typescript-languageservice/src/services/hover.ts index 6681655ef7..a5a6e2edac 100644 --- a/packages/vscode-typescript-languageservice/src/services/hover.ts +++ b/packages/vscode-typescript-languageservice/src/services/hover.ts @@ -7,7 +7,7 @@ import { Position, } from 'vscode-languageserver/node'; import * as previewer from '../utils/previewer'; -import { uriToFsPath } from '@volar/shared'; +import { uriToFsPath, fsPathToUri } from '@volar/shared'; import type { TextDocument } from 'vscode-languageserver-textdocument'; export function register(languageService: ts.LanguageService, getTextDocument: (uri: string) => TextDocument | undefined, ts: typeof import('typescript')) { @@ -22,7 +22,7 @@ export function register(languageService: ts.LanguageService, getTextDocument: ( const parts: string[] = []; const displayString = ts.displayPartsToString(info.displayParts); - const documentation = previewer.markdownDocumentation(info.documentation, info.tags); + const documentation = previewer.markdownDocumentation(info.documentation ?? [], info.tags ?? [], { toResource: fsPathToUri }); if (displayString && !documentOnly) { parts.push(['```typescript', displayString, '```'].join('\n')); diff --git a/packages/vscode-typescript-languageservice/src/utils/previewer.ts b/packages/vscode-typescript-languageservice/src/utils/previewer.ts index 60d6c48987..d76f8a73d0 100644 --- a/packages/vscode-typescript-languageservice/src/utils/previewer.ts +++ b/packages/vscode-typescript-languageservice/src/utils/previewer.ts @@ -5,6 +5,13 @@ import type * as Proto from '../protocol'; +export interface IFilePathToResourceConverter { + /** + * Convert a typescript filepath to a VS Code resource. + */ + toResource(filepath: string): string; +} + function replaceLinks(text: string): string { return text // Http(s) links @@ -23,7 +30,10 @@ function processInlineTags(text: string): string { return replaceLinks(text); } -function getTagBodyText(tag: Proto.JSDocTagInfo): string | undefined { +function getTagBodyText( + tag: Proto.JSDocTagInfo, + filePathConverter: IFilePathToResourceConverter, +): string | undefined { if (!tag.text) { return undefined; } @@ -36,38 +46,42 @@ function getTagBodyText(tag: Proto.JSDocTagInfo): string | undefined { return '```\n' + text + '\n```'; } + const text = convertLinkTags(tag.text, filePathConverter); switch (tag.name) { case 'example': // check for caption tags, fix for #79704 - const captionTagMatches = tag.text.match(/(.*?)<\/caption>\s*(\r\n|\n)/); + const captionTagMatches = text.match(/(.*?)<\/caption>\s*(\r\n|\n)/); if (captionTagMatches && captionTagMatches.index === 0) { - return captionTagMatches[1] + '\n\n' + makeCodeblock(tag.text.substr(captionTagMatches[0].length)); + return captionTagMatches[1] + '\n\n' + makeCodeblock(text.substr(captionTagMatches[0].length)); } else { - return makeCodeblock(tag.text); + return makeCodeblock(text); } case 'author': // fix obsucated email address, #80898 - const emailMatch = tag.text.match(/(.+)\s<([-.\w]+@[-.\w]+)>/); + const emailMatch = text.match(/(.+)\s<([-.\w]+@[-.\w]+)>/); if (emailMatch === null) { - return tag.text; + return text; } else { return `${emailMatch[1]} ${emailMatch[2]}`; } case 'default': - return makeCodeblock(tag.text); + return makeCodeblock(text); } - return processInlineTags(tag.text); + return processInlineTags(text); } -function getTagDocumentation(tag: Proto.JSDocTagInfo): string | undefined { +function getTagDocumentation( + tag: Proto.JSDocTagInfo, + filePathConverter: IFilePathToResourceConverter, +): string | undefined { switch (tag.name) { case 'augments': case 'extends': case 'param': case 'template': - const body = (tag.text || '').split(/^(\S+)\s*-?\s*/); + const body = (convertLinkTags(tag.text, filePathConverter)).split(/^(\S+)\s*-?\s*/); if (body?.length === 3) { const param = body[1]; const doc = body[2]; @@ -81,42 +95,107 @@ function getTagDocumentation(tag: Proto.JSDocTagInfo): string | undefined { // Generic tag const label = `*@${tag.name}*`; - const text = getTagBodyText(tag); + const text = getTagBodyText(tag, filePathConverter); if (!text) { return label; } return label + (text.match(/\r\n|\n/g) ? ' \n' + text : ` — ${text}`); } -export function plain(parts: Proto.SymbolDisplayPart[] | string): string { - return processInlineTags( - typeof parts === 'string' - ? parts - : parts.map(part => part.text).join('')); +export function plainWithLinks( + parts: readonly Proto.SymbolDisplayPart[] | string, + filePathConverter: IFilePathToResourceConverter, +): string { + return processInlineTags(convertLinkTags(parts, filePathConverter)); +} + +/** + * Convert `@link` inline tags to markdown links + */ +function convertLinkTags( + parts: readonly Proto.SymbolDisplayPart[] | string | undefined, + filePathConverter: IFilePathToResourceConverter, +): string { + if (!parts) { + return ''; + } + + if (typeof parts === 'string') { + return parts; + } + + const out: string[] = []; + + let currentLink: { name?: string, target?: Proto.FileSpan, text?: string } | undefined; + for (const part of parts) { + switch (part.kind) { + case 'link': + if (currentLink) { + const text = currentLink.text ?? currentLink.name; + if (currentLink.target) { + const link = filePathConverter.toResource(currentLink.target.file) + '#' + `L${currentLink.target.start.line},${currentLink.target.start.offset}` + + out.push(`[${text}](${link})`); + } else { + if (text) { + out.push(text); + } + } + currentLink = undefined; + } else { + currentLink = {}; + } + break; + + case 'linkName': + if (currentLink) { + currentLink.name = part.text; + // TODO: remove cast once we pick up TS 4.3 + currentLink.target = (part as any as Proto.JSDocLinkDisplayPart).target; + } + break; + + case 'linkText': + if (currentLink) { + currentLink.text = part.text; + } + break; + + default: + out.push(part.text); + break; + } + } + return processInlineTags(out.join('')); } -export function tagsMarkdownPreview(tags: Proto.JSDocTagInfo[]): string { - return tags.map(getTagDocumentation).join(' \n\n'); +export function tagsMarkdownPreview( + tags: readonly Proto.JSDocTagInfo[], + filePathConverter: IFilePathToResourceConverter, +): string { + return tags.map(tag => getTagDocumentation(tag, filePathConverter)).join(' \n\n'); } export function markdownDocumentation( - documentation: Proto.SymbolDisplayPart[] | string | undefined, - tags: Proto.JSDocTagInfo[] | undefined + documentation: Proto.SymbolDisplayPart[] | string, + tags: Proto.JSDocTagInfo[], + filePathConverter: IFilePathToResourceConverter, ): string { - return addMarkdownDocumentation('', documentation, tags); + return addMarkdownDocumentation('', documentation, tags, filePathConverter); } export function addMarkdownDocumentation( out: string, documentation: Proto.SymbolDisplayPart[] | string | undefined, - tags: Proto.JSDocTagInfo[] | undefined + tags: Proto.JSDocTagInfo[] | undefined, + converter: IFilePathToResourceConverter, ): string { if (documentation) { - out += plain(documentation); + out += plainWithLinks(documentation, converter); } if (tags) { - const tagsPreview = tagsMarkdownPreview(tags); + const tagsPreview = tagsMarkdownPreview(tags, converter); if (tagsPreview) { out += '\n\n' + tagsPreview; } diff --git a/packages/vscode-vue-languageservice/src/services/completion.ts b/packages/vscode-vue-languageservice/src/services/completion.ts index 3779d39f67..fa173790fc 100644 --- a/packages/vscode-vue-languageservice/src/services/completion.ts +++ b/packages/vscode-vue-languageservice/src/services/completion.ts @@ -197,6 +197,7 @@ export function register({ sourceFiles, tsLanguageService, documentContext, vueH let tsItems = tsLanguageService.doComplete(sourceMap.mappedDocument.uri, tsRange.start, { quotePreference, includeCompletionsForModuleExports: ['script', 'scriptSetup'].includes(tsRange.data.vueTag ?? ''), // TODO: read ts config + includeCompletionsForImportStatements: ['script', 'scriptSetup'].includes(tsRange.data.vueTag ?? ''), // TODO: read ts config triggerCharacter: context?.triggerCharacter as ts.CompletionsTriggerCharacter, }); if (tsRange.data.vueTag === 'template') { @@ -271,7 +272,7 @@ export function register({ sourceFiles, tsLanguageService, documentContext, vueH for (const componentName of componentNames) { const attributes: html.IAttributeData[] = componentName === '*' ? globalAttributes : []; for (const prop of bind) { - const _name: string = prop.data.name; + const _name: string = prop.data.__volar__.name; const name = nameCases.attr === 'pascalCase' ? _name : hyphenate(_name); if (hyphenate(name).startsWith('on-')) { const propName = '@' + name.substr('on-'.length); @@ -299,7 +300,7 @@ export function register({ sourceFiles, tsLanguageService, documentContext, vueH } } for (const event of on) { - const name = nameCases.attr === 'pascalCase' ? event.data.name : hyphenate(event.data.name); + const name = nameCases.attr === 'pascalCase' ? event.data.__volar__.name : hyphenate(event.data.__volar__.name); const propName = '@' + name; const propKey = componentName + ':' + propName; attributes.push({ @@ -309,7 +310,7 @@ export function register({ sourceFiles, tsLanguageService, documentContext, vueH tsItems.set(propKey, event); } for (const _slot of slot) { - const propName = '#' + _slot.data.name; + const propName = '#' + _slot.data.__volar__.name; const propKey = componentName + ':' + propName; slots.push({ name: propName, diff --git a/packages/vscode-vue-languageservice/src/sourceFile.ts b/packages/vscode-vue-languageservice/src/sourceFile.ts index 02a164064b..dd369461db 100644 --- a/packages/vscode-vue-languageservice/src/sourceFile.ts +++ b/packages/vscode-vue-languageservice/src/sourceFile.ts @@ -383,15 +383,15 @@ export function createSourceFile( const globalEls = docText.indexOf(SearchTexts.HtmlElements) >= 0 ? tsLanguageService.doComplete(doc.uri, doc.positionAt(doc.getText().indexOf(SearchTexts.HtmlElements))) : []; components = components.filter(entry => { - const name = entry.data.name as string; + const name = entry.data.__volar__.name as string; return name.indexOf('$') === -1 && !name.startsWith('_'); }); - const contextNames = context.map(entry => entry.data.name); - const componentNames = components.map(entry => entry.data.name); - const propNames = props.map(entry => entry.data.name); - const setupReturnNames = setupReturns.map(entry => entry.data.name); - const htmlElementNames = globalEls.map(entry => entry.data.name); + const contextNames = context.map(entry => entry.data.__volar__.name); + const componentNames = components.map(entry => entry.data.__volar__.name); + const propNames = props.map(entry => entry.data.__volar__.name); + const setupReturnNames = setupReturns.map(entry => entry.data.__volar__.name); + const htmlElementNames = globalEls.map(entry => entry.data.__volar__.name); if (eqSet(new Set(contextNames), new Set(templateScriptData.context)) && eqSet(new Set(componentNames), new Set(templateScriptData.components)) @@ -958,7 +958,7 @@ export function createSourceFile( const doc = virtualTemplateGen.textDocument.value; const text = doc.getText(); for (const tag of [...templateScriptData.componentItems, ...templateScriptData.htmlElementItems]) { - const tagName = tag.data.name; + const tagName = tag.data.__volar__.name; let bind: CompletionItem[] = []; let on: CompletionItem[] = []; let slot: CompletionItem[] = []; diff --git a/yarn.lock b/yarn.lock index 1d34bda9d4..7e654055a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8283,9 +8283,9 @@ typescript-vscode-sh-plugin@^0.6.14: integrity sha512-AkNlRBbI6K7gk29O92qthNSvc6jjmNQ6isVXoYxkFwPa8D04tIv2SOPd+sd+mNpso4tNdL2gy7nVtrd5yFqvlA== typescript@latest: - version "4.2.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961" - integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg== + version "4.3.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805" + integrity sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6"