From 48669534c1b6cb667db139bd1c21af10426e203c Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Fri, 16 Dec 2022 18:14:46 +0800 Subject: [PATCH 1/7] feat: add angular examples --- .vscode/launch.json | 17 + examples/angular-language-core/LICENSE | 21 + examples/angular-language-core/package.json | 25 + examples/angular-language-core/src/index.ts | 3 + .../angular-language-core/src/modules/html.ts | 466 ++++++++++++++++++ .../angular-language-core/src/modules/ts.ts | 155 ++++++ .../angular-language-core/tsconfig.build.json | 20 + examples/angular-language-server/LICENSE | 21 + .../bin/angular-language-server.js | 8 + examples/angular-language-server/package.json | 24 + examples/angular-language-server/src/index.ts | 64 +++ .../tsconfig.build.json | 26 + examples/svelte-language-core/src/index.ts | 11 +- examples/vscode-angular/.vscodeignore | 3 + examples/vscode-angular/LICENSE | 21 + ...gular-template-language-configuration.json | 8 + examples/vscode-angular/package.json | 99 ++++ examples/vscode-angular/src/client.ts | 56 +++ .../syntaxes/angular-directives.json | 114 +++++ .../syntaxes/angular-interpolations.json | 44 ++ examples/vscode-angular/tsconfig.build.json | 24 + examples/vscode-svelte/package.json | 2 +- examples/vscode-svelte/src/client.ts | 3 - .../language-core/src/documentRegistry.ts | 16 +- packages/language-core/src/languageContext.ts | 6 +- packages/language-core/src/types.ts | 6 +- .../src/features/customFeatures.ts | 2 +- packages/language-service/src/documents.ts | 4 +- .../src/languageFeatures/fileRename.ts | 2 +- packages/language-service/src/types.ts | 2 + .../src/utils/definePlugin.ts | 14 +- .../src/utils/featureWorkers.ts | 6 +- packages/typescript/src/index.ts | 2 +- pnpm-lock.yaml | 280 +++++++---- tsconfig.build.json | 6 + .../src/generators/template.ts | 2 +- .../vue-language-core/src/sourceFile.ts | 31 ++ .../vue-language-core/src/utils/transform.ts | 4 +- .../vue-language-service/src/helpers.ts | 2 +- .../src/plugins/vue-autoinsert-parentheses.ts | 2 +- .../src/plugins/vue-twoslash-queries.ts | 2 +- 41 files changed, 1474 insertions(+), 150 deletions(-) create mode 100644 examples/angular-language-core/LICENSE create mode 100644 examples/angular-language-core/package.json create mode 100644 examples/angular-language-core/src/index.ts create mode 100644 examples/angular-language-core/src/modules/html.ts create mode 100644 examples/angular-language-core/src/modules/ts.ts create mode 100644 examples/angular-language-core/tsconfig.build.json create mode 100644 examples/angular-language-server/LICENSE create mode 100755 examples/angular-language-server/bin/angular-language-server.js create mode 100644 examples/angular-language-server/package.json create mode 100644 examples/angular-language-server/src/index.ts create mode 100644 examples/angular-language-server/tsconfig.build.json create mode 100644 examples/vscode-angular/.vscodeignore create mode 100644 examples/vscode-angular/LICENSE create mode 100644 examples/vscode-angular/languages/angular-template-language-configuration.json create mode 100644 examples/vscode-angular/package.json create mode 100644 examples/vscode-angular/src/client.ts create mode 100644 examples/vscode-angular/syntaxes/angular-directives.json create mode 100644 examples/vscode-angular/syntaxes/angular-interpolations.json create mode 100644 examples/vscode-angular/tsconfig.build.json diff --git a/.vscode/launch.json b/.vscode/launch.json index 86fc0f720..cf487d4b6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -36,6 +36,23 @@ "script": "watch" } }, + { + "name": "Launch Angular Example", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--disable-extensions", + "--extensionDevelopmentPath=${workspaceRoot}/examples/vscode-angular" + ], + "outFiles": [ + "${workspaceRoot}/*/*/out/**/*.js" + ], + "preLaunchTask": { + "type": "npm", + "script": "watch" + } + }, { "type": "extensionHost", "request": "launch", diff --git a/examples/angular-language-core/LICENSE b/examples/angular-language-core/LICENSE new file mode 100644 index 000000000..b55e47a7e --- /dev/null +++ b/examples/angular-language-core/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021-present Johnson Chu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/angular-language-core/package.json b/examples/angular-language-core/package.json new file mode 100644 index 000000000..5ce7866e9 --- /dev/null +++ b/examples/angular-language-core/package.json @@ -0,0 +1,25 @@ +{ + "name": "@volar-examples/angular-language-core", + "version": "1.0.13", + "main": "out/index.js", + "license": "MIT", + "files": [ + "out/**/*.js", + "out/**/*.d.ts" + ], + "repository": { + "type": "git", + "url": "https://github.com/johnsoncodehk/volar.git", + "directory": "examples/angular-language-core" + }, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "^15.1.0", + "@volar/language-core": "1.0.13", + "vscode-languageserver-textdocument": "^1.0.7", + "vscode-uri": "^3.0.6" + }, + "devDependencies": { + "@angular/compiler": "^15.0.4", + "@typescript-eslint/types": "^5.46.1" + } +} diff --git a/examples/angular-language-core/src/index.ts b/examples/angular-language-core/src/index.ts new file mode 100644 index 000000000..cfa180365 --- /dev/null +++ b/examples/angular-language-core/src/index.ts @@ -0,0 +1,3 @@ +export * from '@volar/language-core'; +export * from './modules/ts'; +export * from './modules/html'; diff --git a/examples/angular-language-core/src/modules/html.ts b/examples/angular-language-core/src/modules/html.ts new file mode 100644 index 000000000..e37b02bd8 --- /dev/null +++ b/examples/angular-language-core/src/modules/html.ts @@ -0,0 +1,466 @@ +import { DocumentCapabilities, EmbeddedFileKind, LanguageModule, SourceFile } from '@volar/language-core'; +import type { TmplAstNode, TmplAstTemplate, ParsedTemplate, ParseSourceSpan } from '@angular/compiler'; +import { Codegen } from './ts'; +import type * as ts from 'typescript/lib/tsserverlibrary'; + +const { parseTemplate }: typeof import('@angular/compiler') = require('@angular-eslint/bundled-angular-compiler'); + +export class HTMLTemplateFile implements SourceFile { + + public text: string; + public capabilities: DocumentCapabilities = { + diagnostic: true, + }; + public kind = EmbeddedFileKind.TextFile; + public mappings: SourceFile['mappings'] = []; + public embeddeds: SourceFile['embeddeds'] = []; + public parsed: ParsedTemplate; + + constructor( + private ts: typeof import('typescript/lib/tsserverlibrary'), + public fileName: string, + public snapshot: ts.IScriptSnapshot, + ) { + + console.log('createSourceFile', fileName); + const generated = generate(ts, fileName, snapshot.getText(0, snapshot.getLength())); + + this.text = snapshot.getText(0, snapshot.getLength()); // TODO: use empty string with snapshot + this.mappings = [ + { + data: { + diagnostic: true, + }, + generatedRange: [0, this.text.length], + sourceRange: [0, this.text.length], + }, + ]; + this.embeddeds = [ + { + fileName: fileName + '.__template.ts', + text: generated.codegen.text, + capabilities: { + diagnostic: true, + foldingRange: false, + documentFormatting: false, + documentSymbol: false, + codeAction: false, + inlayHint: true, + }, + kind: EmbeddedFileKind.TypeScriptHostFile, + mappings: generated.codegen.mappings, + embeddeds: [], + }, + ]; + this.parsed = generated.parsed; + } + + update(snapshot: ts.IScriptSnapshot) { + console.log('updateSourceFile', this.fileName); + const generated = generate(this.ts, this.fileName, snapshot.getText(0, snapshot.getLength())); + this.text = snapshot.getText(0, snapshot.getLength()); + this.mappings = [ + { + data: { + diagnostic: true, + }, + generatedRange: [0, this.text.length], + sourceRange: [0, this.text.length], + }, + ]; + this.embeddeds[0].text = generated.codegen.text; + this.embeddeds[0].mappings = generated.codegen.mappings; + this.parsed = generated.parsed; + } +} + +export function createHtmlLanguageModule(ts: typeof import('typescript/lib/tsserverlibrary')): LanguageModule { + return { + createSourceFile(fileName, snapshot) { + if (fileName.endsWith('.html')) { + return new HTMLTemplateFile(ts, fileName, snapshot); + } + }, + updateSourceFile(sourceFile, snapshot) { + sourceFile.update(snapshot); + }, + }; +} + +function generate( + ts: typeof import('typescript/lib/tsserverlibrary'), + fileName: string, + fileText: string, +) { + + const parsed = parseTemplate(fileText, fileName, { + preserveWhitespaces: true, + }); + const codegen = new Codegen(fileText); + const localVars: Record = {}; + const templateBlocksConditions: Record = {}; + const conditions: string[] = []; + const ngTemplates: TmplAstTemplate[] = []; + + codegen.text += 'export { };\n'; + codegen.text += `declare const __ctx: __Components['${fileName}'];\n`; + + const visitor: Parameters[0] = { + visit(node) { + console.log('visit'); + node.visit(visitor); + }, + visitElement(element) { + console.log('visitElement'); + codegen.text += `{\n`; + for (const input of element.inputs) { + codegen.text += '('; + // console.log(input.sourceSpan.start.offset, input.sourceSpan.end.offset); + addInterpolationFragment(input.value.sourceSpan.start, input.value.sourceSpan.end); + codegen.text += `);\n`; + } + for (const child of element.children) { + child.visit(visitor); + } + codegen.text += `}\n`; + }, + visitTemplate(template) { + console.log('visitTemplate', template.tagName); + + if (template.tagName === 'ng-template') { + ngTemplates.push(template); + } + + let conditionText = 'true'; + let forOfSource: ParseSourceSpan | undefined; + let forOfBinding: ParseSourceSpan | undefined; + + for (const attr of template.templateAttrs) { + if (attr.name === 'ngIf' && attr.valueSpan) { + codegen.text += `if (`; + conditionText = addInterpolationFragment(attr.valueSpan.start.offset, attr.valueSpan.end.offset); + codegen.text += `) {\n`; + conditions.push(conditionText); + for (const child of template.children) { + child.visit(visitor); + } + conditions.pop(); + codegen.text += `}\n`; + } + if (attr.name === 'ngIfElse' && attr.valueSpan) { + codegen.text += '(__templates).'; + const templateBlock = codegen.addSourceText(attr.valueSpan.start.offset, attr.valueSpan.end.offset); + codegen.text += ';\n'; + templateBlocksConditions[templateBlock] ??= []; + templateBlocksConditions[templateBlock].push([ + ...conditions, + `!(${conditionText})`, + ]); + } + if (attr.name === 'ngForOf' && attr.valueSpan) { + forOfSource = attr.valueSpan; + } + } + + for (const v of template.variables) { + if (v.value === '$implicit') { + forOfBinding = v.keySpan; + } + } + + if (forOfSource && forOfBinding) { + codegen.text += `for (const `; + const binding = codegen.addSourceText(forOfBinding.start.offset, forOfBinding.end.offset); + codegen.text += ` of `; + addInterpolationFragment(forOfSource.start.offset, forOfSource.end.offset); + codegen.text += `) {\n`; + localVars[binding] = localVars[binding] ? localVars[binding] + 1 : 1; + for (const child of template.children) { + child.visit(visitor); + } + localVars[binding]--; + codegen.text += `}\n`; + + // console.log(JSON.stringify(template)); + } + }, + visitContent(content) { + console.log('visitContent'); + content.visit(visitor); + }, + visitVariable(variable) { + console.log('visitVariable'); + variable.visit(visitor); + }, + visitReference(reference) { + console.log('visitReference'); + reference.visit(visitor); + }, + visitTextAttribute(attribute) { + console.log('visitTextAttribute'); + attribute.visit(visitor); + }, + visitBoundAttribute(attribute) { + console.log('visitBoundAttribute'); + attribute.visit(visitor); + }, + visitBoundEvent(event) { + console.log('visitBoundEvent'); + event.visit(visitor); + }, + visitText(_text) { + console.log('visitText'); + // text.visit(visitor); + }, + visitBoundText(text) { + const content = fileText.substring(text.value.sourceSpan.start, text.value.sourceSpan.end); + console.log('visitBoundText'); + const interpolations = content.matchAll(/{{[\s\S]*?}}/g); + for (const interpolation of interpolations) { + const start = text.value.sourceSpan.start + interpolation.index! + '{{'.length; + const length = interpolation[0].length - '{{'.length - '}}'.length; + addInterpolationFragment(start, start + length); + codegen.text += ';\n'; + } + }, + visitIcu(icu) { + console.log('visitIcu'); + icu.visit(visitor); + }, + }; + + for (const node of parsed.nodes) { + node.visit(visitor); + } + + codegen.text += 'var __templates = {\n'; + for (const template of ngTemplates) { + for (const reference of template.references) { + const templateBlock = codegen.addSourceText(reference.keySpan.start.offset, reference.keySpan.end.offset); + codegen.text += ': (() => {\n'; + let ifBlockOpen = false; + if (templateBlocksConditions[templateBlock]) { + ifBlockOpen = true; + codegen.text += `if (`; + codegen.text += templateBlocksConditions[templateBlock].map(conditions => conditions.join(' && ')).join(' || '); + codegen.text += `) {\n`; + } + for (const child of template.children) { + child.visit(visitor); + } + if (ifBlockOpen) { + ifBlockOpen = false; + codegen.text += `}\n`; + } + codegen.text += `}) as unknown as typeof import('@angular/core').TemplateRef,\n`; + } + } + codegen.text += '};\n'; + + return { + codegen, + parsed, + }; + + function addInterpolationFragment(start: number, end: number) { + const code = fileText.substring(start, end); + const ast = ts.createSourceFile(fileName + '.ts', code, ts.ScriptTarget.Latest); + let full = ''; + walkInterpolationFragment(ts, code, ast, (fragment, offset, isJustForErrorMapping) => { + full += fragment; + if (offset !== undefined) { + codegen.addSourceText( + start + offset, + start + offset + fragment.length, + isJustForErrorMapping ? { diagnostic: true } : undefined, + ); + } + else { + codegen.text += fragment; + } + }, localVars,); + return full; + } +} + +function walkInterpolationFragment( + ts: typeof import('typescript/lib/tsserverlibrary'), + code: string, + ast: ts.SourceFile, + cb: (fragment: string, offset: number | undefined, isJustForErrorMapping?: boolean) => void, + localVars: Record, +) { + + let ctxVars: { + text: string, + isShorthand: boolean, + offset: number, + }[] = []; + + const varCb = (id: ts.Identifier, isShorthand: boolean) => { + if ( + !!localVars[id.text] || + id.text.startsWith('__') + ) { + return; + } + ctxVars.push({ + text: id.text, + isShorthand: isShorthand, + offset: id.getStart(ast), + }); + }; + ast.forEachChild(node => walkIdentifiers(ts, node, varCb, localVars)); + + ctxVars = ctxVars.sort((a, b) => a.offset - b.offset); + + if (ctxVars.length) { + + if (ctxVars[0].isShorthand) { + cb(code.substring(0, ctxVars[0].offset + ctxVars[0].text.length), 0); + cb(': ', undefined); + } + else { + cb(code.substring(0, ctxVars[0].offset), 0); + } + + for (let i = 0; i < ctxVars.length - 1; i++) { + + cb('(__ctx).', undefined); + if (ctxVars[i + 1].isShorthand) { + cb(code.substring(ctxVars[i].offset, ctxVars[i + 1].offset + ctxVars[i + 1].text.length), ctxVars[i].offset); + cb(': ', undefined); + } + else { + cb(code.substring(ctxVars[i].offset, ctxVars[i + 1].offset), ctxVars[i].offset); + } + } + + cb('', ctxVars[ctxVars.length - 1].offset, true); + cb('(__ctx).', undefined); + cb(code.substring(ctxVars[ctxVars.length - 1].offset), ctxVars[ctxVars.length - 1].offset); + } + else { + cb(code, 0); + } + + return ctxVars; +} + +function walkIdentifiers( + ts: typeof import('typescript/lib/tsserverlibrary'), + node: ts.Node, + cb: (varNode: ts.Identifier, isShorthand: boolean) => void, + localVars: Record, +) { + + const blockVars: string[] = []; + + if (ts.isIdentifier(node)) { + cb(node, false); + } + else if (ts.isShorthandPropertyAssignment(node)) { + cb(node.name, true); + } + else if (ts.isPropertyAccessExpression(node)) { + walkIdentifiers(ts, node.expression, cb, localVars); + } + else if (ts.isVariableDeclaration(node)) { + + colletVars(ts, node.name, blockVars); + + for (const varName of blockVars) + localVars[varName] = (localVars[varName] ?? 0) + 1; + + if (node.initializer) + walkIdentifiers(ts, node.initializer, cb, localVars); + } + else if (ts.isArrowFunction(node)) { + + const functionArgs: string[] = []; + + for (const param of node.parameters) { + colletVars(ts, param.name, functionArgs); + if (param.type) { + walkIdentifiers(ts, param.type, cb, localVars); + } + } + + for (const varName of functionArgs) + localVars[varName] = (localVars[varName] ?? 0) + 1; + + walkIdentifiers(ts, node.body, cb, localVars); + + for (const varName of functionArgs) + localVars[varName]--; + } + else if (ts.isObjectLiteralExpression(node)) { + for (const prop of node.properties) { + if (ts.isPropertyAssignment(prop)) { + // fix https://github.com/johnsoncodehk/volar/issues/1176 + if (ts.isComputedPropertyName(prop.name)) { + walkIdentifiers(ts, prop.name.expression, cb, localVars); + } + walkIdentifiers(ts, prop.initializer, cb, localVars); + } + // fix https://github.com/johnsoncodehk/volar/issues/1156 + else if (ts.isShorthandPropertyAssignment(prop)) { + walkIdentifiers(ts, prop, cb, localVars); + } + // fix https://github.com/johnsoncodehk/volar/issues/1148#issuecomment-1094378126 + else if (ts.isSpreadAssignment(prop)) { + // TODO: cannot report "Spread types may only be created from object types.ts(2698)" + walkIdentifiers(ts, prop.expression, cb, localVars); + } + } + } + else if (ts.isTypeReferenceNode(node)) { + // fix https://github.com/johnsoncodehk/volar/issues/1422 + node.forEachChild(node => walkIdentifiersInTypeReference(ts, node, cb)); + } + else { + node.forEachChild(node => walkIdentifiers(ts, node, cb, localVars)); + } + + for (const varName of blockVars) { + localVars[varName]--; + } +} + +function walkIdentifiersInTypeReference( + ts: typeof import('typescript/lib/tsserverlibrary'), + node: ts.Node, + cb: (varNode: ts.Identifier, isShorthand: boolean) => void, +) { + if (ts.isTypeQueryNode(node) && ts.isIdentifier(node.exprName)) { + cb(node.exprName, false); + } + else { + node.forEachChild(node => walkIdentifiersInTypeReference(ts, node, cb)); + } +} + +function colletVars( + ts: typeof import('typescript/lib/tsserverlibrary'), + node: ts.Node, + result: string[], +) { + if (ts.isIdentifier(node)) { + result.push(node.text); + } + else if (ts.isObjectBindingPattern(node)) { + for (const el of node.elements) { + colletVars(ts, el.name, result); + } + } + else if (ts.isArrayBindingPattern(node)) { + for (const el of node.elements) { + if (ts.isBindingElement(el)) { + colletVars(ts, el.name, result); + } + } + } + else { + node.forEachChild(node => colletVars(ts, node, result)); + } +} diff --git a/examples/angular-language-core/src/modules/ts.ts b/examples/angular-language-core/src/modules/ts.ts new file mode 100644 index 000000000..30adc2e45 --- /dev/null +++ b/examples/angular-language-core/src/modules/ts.ts @@ -0,0 +1,155 @@ +import { LanguageModule, SourceFile, EmbeddedFileKind, PositionCapabilities } from '@volar/language-core'; +import type * as ts from 'typescript/lib/tsserverlibrary'; +import * as path from 'path'; +import type { Mapping } from '@volar/source-map'; + +export function createTsLanguageModule( + ts: typeof import('typescript/lib/tsserverlibrary'), +) { + + const languageModule: LanguageModule = { + createSourceFile(fileName, snapshot) { + if (fileName.endsWith('.ts')) { + const text = snapshot.getText(0, snapshot.getLength()); + const ast = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest); + const virtualFile = createVirtualFile(ast); + return { + ast, + snapshot, + fileName, + text: virtualFile.text, + capabilities: { + diagnostic: true, + foldingRange: true, + documentFormatting: true, + documentSymbol: true, + codeAction: true, + inlayHint: true, + }, + kind: EmbeddedFileKind.TypeScriptHostFile, + mappings: virtualFile.mappings, + embeddeds: [], + }; + } + }, + updateSourceFile(sourceFile, snapshot) { + const text = snapshot.getText(0, snapshot.getLength()); + const change = snapshot.getChangeRange(sourceFile.snapshot); + + // incremental update is important for performance + sourceFile.ast = change + ? sourceFile.ast.update(text, change) + : ts.createSourceFile(sourceFile.fileName, text, ts.ScriptTarget.Latest); + sourceFile.snapshot = snapshot; + + const gen = createVirtualFile(sourceFile.ast); + sourceFile.text = gen.text; + sourceFile.mappings = gen.mappings; + }, + }; + return languageModule; + + function createVirtualFile(ast: ts.SourceFile) { + + const classComponents: { + templateUrl?: string, + urlNodes: ts.Node[], + decoratorName: string, + className: string, + }[] = []; + + ast.forEachChild(node => { + if (ts.isClassDeclaration(node)) { + if (node.modifiers?.find(mod => mod.kind === ts.SyntaxKind.ExportKeyword)) { + const decorator = node.modifiers.find((mod) => ts.isDecorator(mod)) as ts.Decorator | undefined; + if ( + decorator + && ts.isCallExpression(decorator.expression) + && decorator.expression.arguments.length + && ts.isObjectLiteralExpression(decorator.expression.arguments[0]) + ) { + const decoratorName = decorator.expression.expression.getText(ast); + const className = node.name?.getText(ast) || ''; + const classComponent: typeof classComponents[number] = { + className, + decoratorName, + urlNodes: [], + }; + const templateUrlProp = decorator.expression.arguments[0].properties.find((prop) => prop.name?.getText(ast) === 'templateUrl'); + if (templateUrlProp && ts.isPropertyAssignment(templateUrlProp) && ts.isStringLiteral(templateUrlProp.initializer)) { + const templateUrl = path.resolve(path.dirname(ast.fileName), templateUrlProp.initializer.text); + classComponent.templateUrl = templateUrl; + classComponent.urlNodes.push(templateUrlProp.initializer); + } + const styleUrlsProp = decorator.expression.arguments[0].properties.find((prop) => prop.name?.getText(ast) === 'styleUrls'); + if (styleUrlsProp && ts.isPropertyAssignment(styleUrlsProp) && ts.isArrayLiteralExpression(styleUrlsProp.initializer)) { + for (const url of styleUrlsProp.initializer.elements) { + if (ts.isStringLiteral(url)) { + classComponent.urlNodes.push(url); + } + } + } + classComponents.push(classComponent); + } + } + } + }); + + const codegen = new Codegen(ast.getText()); + + codegen.addSourceText(0, ast.end); + + if (classComponents.length) { + codegen.text += `\n/* Volar: Virtual Code */\n`; + for (const classComponent of classComponents) { + for (const urlNode of classComponent.urlNodes) { + codegen.text += `import `; + codegen.addSourceText(urlNode.getStart(ast), urlNode.getEnd()); + codegen.text += `;\n`; + } + } + const classComponentsWithTemplateUrl = classComponents.filter(component => !!component.templateUrl); + if (classComponentsWithTemplateUrl.length) { + codegen.text += `type __WithComponent

= C1 extends import('@angular/core').Component ? { [k in P]: C2 } : {};\n`; + codegen.text += `declare global {\n`; + codegen.text += `interface __Components extends\n`; + codegen.text += classComponentsWithTemplateUrl.map((component) => { + return `__WithComponent<'${component.templateUrl}', ${component.decoratorName}, ${component.className}>`; + }).join(',\n'); + codegen.text += `\n{ }\n`; + codegen.text += `}\n`; + } + } + + return codegen; + } +} + +const fullCap: PositionCapabilities = { + hover: true, + references: true, + definition: true, + rename: true, + completion: true, + diagnostic: true, + semanticTokens: true, +}; + +export class Codegen { + + constructor(public sourceCode: string) { } + + public text = ''; + public mappings: Mapping[] = []; + + public addSourceText(start: number, end: number, data: PositionCapabilities = fullCap) { + this.mappings.push({ + sourceRange: [start, end], + generatedRange: [this.text.length, this.text.length + end - start], + data, + }); + const addText = this.sourceCode.substring(start, end); + this.text += addText; + return addText; + } +} diff --git a/examples/angular-language-core/tsconfig.build.json b/examples/angular-language-core/tsconfig.build.json new file mode 100644 index 000000000..9d1438807 --- /dev/null +++ b/examples/angular-language-core/tsconfig.build.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "out", + "rootDir": "src", + }, + "include": [ + "src" + ], + "exclude": [ + "node_modules", + ".vscode-test" + ], + "references": [ + { + "path": "../../packages/language-core/tsconfig.build.json" + } + ] +} \ No newline at end of file diff --git a/examples/angular-language-server/LICENSE b/examples/angular-language-server/LICENSE new file mode 100644 index 000000000..b55e47a7e --- /dev/null +++ b/examples/angular-language-server/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021-present Johnson Chu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/angular-language-server/bin/angular-language-server.js b/examples/angular-language-server/bin/angular-language-server.js new file mode 100755 index 000000000..d18b06411 --- /dev/null +++ b/examples/angular-language-server/bin/angular-language-server.js @@ -0,0 +1,8 @@ +#!/usr/bin/env node +if (process.argv.includes("--version")) { + const pkgJSON = require("../package.json"); + console.log(`${pkgJSON["version"]}`); +} +else { + require("../out/index.js"); +} diff --git a/examples/angular-language-server/package.json b/examples/angular-language-server/package.json new file mode 100644 index 000000000..10e3e84a6 --- /dev/null +++ b/examples/angular-language-server/package.json @@ -0,0 +1,24 @@ +{ + "name": "@volar-examples/angular-language-server", + "version": "1.0.13", + "main": "out/index.js", + "license": "MIT", + "files": [ + "out/**/*.js", + "out/**/*.d.ts" + ], + "bin": { + "angular-language-server": "./bin/angular-language-server.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/johnsoncodehk/volar.git", + "directory": "examples/angular-language-server" + }, + "dependencies": { + "@volar-examples/angular-language-core": "1.0.13", + "@volar-plugins/typescript": "1.0.13", + "@volar/language-server": "1.0.13", + "@volar/shared": "1.0.13" + } +} diff --git a/examples/angular-language-server/src/index.ts b/examples/angular-language-server/src/index.ts new file mode 100644 index 000000000..7fc9b633a --- /dev/null +++ b/examples/angular-language-server/src/index.ts @@ -0,0 +1,64 @@ +import { createTsLanguageModule, createHtmlLanguageModule, HTMLTemplateFile } from '@volar-examples/angular-language-core'; +import createTsPlugin from '@volar-plugins/typescript'; +import { createLanguageServer, LanguageServerPlugin } from '@volar/language-server/node'; +import type { LanguageServicePlugin, SourceFileDocuments, Diagnostic } from '@volar/language-service'; + +const plugin: LanguageServerPlugin = () => ({ + extraFileExtensions: [{ extension: 'html', isMixedContent: true, scriptKind: 7 }], + semanticService: { + getLanguageModules(host) { + return [ + createTsLanguageModule(host.getTypeScriptModule()), + createHtmlLanguageModule(host.getTypeScriptModule()), + ]; + }, + getServicePlugins(_host, service) { + return [ + createTsPlugin(), + createNgTemplateLsPlugin(service.context.documents), + ]; + }, + }, + syntacticService: { + getLanguageModules(ts) { + return [ + createTsLanguageModule(ts), + createHtmlLanguageModule(ts), + ]; + }, + getServicePlugins() { + return [ + createTsPlugin(), + ]; + } + }, +}); + +function createNgTemplateLsPlugin(docs: SourceFileDocuments): LanguageServicePlugin { + + return { + + validation: { + + onSyntactic(document) { + + const file = docs.get(document.uri)?.file; + + if (file instanceof HTMLTemplateFile) { + return (file.parsed.errors ?? []).map(error => ({ + range: { + start: { line: error.span.start.line, character: error.span.start.col }, + end: { line: error.span.end.line, character: error.span.end.col }, + }, + severity: error.level === 1 ? 1 : 2, + source: 'ng-template', + message: error.msg, + })); + } + }, + } + }; +} + +createLanguageServer([plugin]); + diff --git a/examples/angular-language-server/tsconfig.build.json b/examples/angular-language-server/tsconfig.build.json new file mode 100644 index 000000000..f47b88dde --- /dev/null +++ b/examples/angular-language-server/tsconfig.build.json @@ -0,0 +1,26 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "out", + "rootDir": "src", + }, + "include": [ + "src" + ], + "exclude": [ + "node_modules", + ".vscode-test" + ], + "references": [ + { + "path": "../../packages/language-server/tsconfig.build.json" + }, + { + "path": "../../plugins/typescript/tsconfig.build.json" + }, + { + "path": "../angular-language-core/tsconfig.build.json" + } + ] +} \ No newline at end of file diff --git a/examples/svelte-language-core/src/index.ts b/examples/svelte-language-core/src/index.ts index 55584b942..2b1b3b5ea 100644 --- a/examples/svelte-language-core/src/index.ts +++ b/examples/svelte-language-core/src/index.ts @@ -11,10 +11,19 @@ export const languageModule: LanguageModule = { if (fileName.endsWith('.svelte')) { const text = snapshot.getText(0, snapshot.getLength()); return { - snapshot, fileName, text, + kind: EmbeddedFileKind.TextFile, embeddeds: getEmbeddeds(fileName, text), + capabilities: { + diagnostic: true, + foldingRange: true, + documentFormatting: true, + documentSymbol: true, + codeAction: true, + inlayHint: true, + }, + mappings: [], }; } }, diff --git a/examples/vscode-angular/.vscodeignore b/examples/vscode-angular/.vscodeignore new file mode 100644 index 000000000..8a5911c6e --- /dev/null +++ b/examples/vscode-angular/.vscodeignore @@ -0,0 +1,3 @@ +src +tsconfig.build.json +tsconfig.build.tsbuildinfo diff --git a/examples/vscode-angular/LICENSE b/examples/vscode-angular/LICENSE new file mode 100644 index 000000000..b55e47a7e --- /dev/null +++ b/examples/vscode-angular/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021-present Johnson Chu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/vscode-angular/languages/angular-template-language-configuration.json b/examples/vscode-angular/languages/angular-template-language-configuration.json new file mode 100644 index 000000000..70fc48500 --- /dev/null +++ b/examples/vscode-angular/languages/angular-template-language-configuration.json @@ -0,0 +1,8 @@ +{ + "colorizedBracketPairs": [ + [ + "{{", + "}}" + ], + ] +} \ No newline at end of file diff --git a/examples/vscode-angular/package.json b/examples/vscode-angular/package.json new file mode 100644 index 000000000..4c99cce59 --- /dev/null +++ b/examples/vscode-angular/package.json @@ -0,0 +1,99 @@ +{ + "private": true, + "name": "example-vscode-angular", + "version": "1.0.13", + "repository": { + "type": "git", + "url": "https://github.com/johnsoncodehk/volar.git", + "directory": "examples/vscode-angular" + }, + "displayName": "Angular (Volar Example)", + "description": "Angular (Volar Example)", + "author": "johnsoncodehk", + "publisher": "johnsoncodehk", + "engines": { + "vscode": "^1.67.0" + }, + "activationEvents": [ + "onLanguage:html", + "onLanguage:typescript" + ], + "main": "out/client", + "contributes": { + "languages": [ + { + "id": "angular-directives" + }, + { + "id": "angular-interpolations" + }, + { + "id": "html", + "configuration": "./languages/angular-template-language-configuration.json" + } + ], + "grammars": [ + { + "language": "angular-directives", + "scopeName": "text.html.angular.directives", + "path": "./syntaxes/angular-directives.json", + "injectTo": [ + "text.html.derivative" + ], + "embeddedLanguages": { + "source.ts": "typescript" + } + }, + { + "language": "angular-interpolations", + "scopeName": "angular.interpolations", + "path": "./syntaxes/angular-interpolations.json", + "injectTo": [ + "text.html.derivative" + ] + } + ], + "commands": [ + { + "command": "volar-angular.action.showVirtualFiles", + "title": "Show Virtual Files", + "category": "Volar-Angular (Debug)" + } + ], + "configurationDefaults": { + "[html]": { + "editor.semanticHighlighting.enabled": true + } + }, + "configuration": { + "type": "object", + "title": "Volar-Angular", + "properties": { + "volar-angular-language-server.trace.server": { + "scope": "window", + "type": "string", + "enum": [ + "off", + "messages", + "verbose" + ], + "default": "off", + "description": "Traces the communication between VS Code and the language server." + } + } + } + }, + "scripts": { + "release": "vsce publish" + }, + "devDependencies": { + "@types/vscode": "1.67.0", + "vsce": "latest" + }, + "dependencies": { + "@volar-examples/angular-language-server": "1.0.13", + "@volar/vscode-language-client": "1.0.13", + "typesafe-path": "^0.2.2", + "vscode-languageclient": "^8.0.2" + } +} diff --git a/examples/vscode-angular/src/client.ts b/examples/vscode-angular/src/client.ts new file mode 100644 index 000000000..1a5b395fc --- /dev/null +++ b/examples/vscode-angular/src/client.ts @@ -0,0 +1,56 @@ +import { LanguageServerInitializationOptions } from '@volar/language-server'; +import * as path from 'typesafe-path'; +import * as vscode from 'vscode'; +import * as lsp from 'vscode-languageclient/node'; +import { registerShowVirtualFiles, registerTsConfig } from '@volar/vscode-language-client'; + +let client: lsp.BaseLanguageClient; + +export async function activate(context: vscode.ExtensionContext) { + + const documentSelector: lsp.DocumentFilter[] = [ + { language: 'html' }, + { language: 'typescript' }, + ]; + const initializationOptions: LanguageServerInitializationOptions = { + typescript: { + tsdk: path.join( + vscode.env.appRoot as path.OsPath, + 'extensions/node_modules/typescript/lib' as path.PosixPath, + ), + }, + }; + const serverModule = vscode.Uri.joinPath(context.extensionUri, 'node_modules', '@volar-examples', 'angular-language-server', 'bin', 'angular-language-server.js'); + const runOptions = { execArgv: [] }; + const debugOptions = { execArgv: ['--nolazy', '--inspect=' + 6009] }; + const serverOptions: lsp.ServerOptions = { + run: { + module: serverModule.fsPath, + transport: lsp.TransportKind.ipc, + options: runOptions + }, + debug: { + module: serverModule.fsPath, + transport: lsp.TransportKind.ipc, + options: debugOptions + }, + }; + const clientOptions: lsp.LanguageClientOptions = { + documentSelector, + initializationOptions, + }; + client = new lsp.LanguageClient( + 'volar-angular-language-server', + 'Angular Language Server (Volar)', + serverOptions, + clientOptions, + ); + await client.start(); + + registerShowVirtualFiles('volar-angular.action.showVirtualFiles', context, client); + registerTsConfig('volar-angular.action.showTsConfig', context, client, document => documentSelector.some(selector => selector.language === document.languageId)); +} + +export function deactivate(): Thenable | undefined { + return client?.stop(); +} diff --git a/examples/vscode-angular/syntaxes/angular-directives.json b/examples/vscode-angular/syntaxes/angular-directives.json new file mode 100644 index 000000000..d914de9a5 --- /dev/null +++ b/examples/vscode-angular/syntaxes/angular-directives.json @@ -0,0 +1,114 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "fileTypes": [ ], + "injectionSelector": "L:meta.tag -meta.attribute -source.tsx -source.js.jsx, L:meta.element -meta.attribute", + "patterns": [ + { + "include": "#directives" + } + ], + "repository": { + "directives": { + "begin": "((\\*?)(\\[)((ngIf)|(ngFor)|([^\\]]*))(\\]))|((\\*)((ngIf)|(ngFor)|([\\w-]*)))", + "beginCaptures": { + "2": { + "name": "punctuation.separator.key-value.html.angular" + }, + "3": { + "name": "punctuation.separator.key-value.html.angular" + }, + "5": { + "name": "keyword.control.conditional.angular" + }, + "6": { + "name": "keyword.control.loop.angular" + }, + "7": { + "name": "entity.other.attribute-name.html.angular" + }, + "8": { + "name": "punctuation.separator.key-value.html.angular" + }, + "10": { + "name": "punctuation.separator.key-value.html.angular" + }, + "12": { + "name": "keyword.control.conditional.angular" + }, + "13": { + "name": "keyword.control.loop.angular" + }, + "14": { + "name": "entity.other.attribute-name.html.angular" + } + }, + "end": "(?=\\s*+[^=\\s])", + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.html.angular" + } + }, + "name": "meta.attribute.directive.angular", + "patterns": [ + { + "include": "#directives-expression" + } + ] + }, + "directives-expression": { + "patterns": [ + { + "begin": "(=)\\s*('|\"|`)", + "beginCaptures": { + "1": { + "name": "punctuation.separator.key-value.html.angular" + }, + "2": { + "name": "punctuation.definition.string.begin.html.angular" + } + }, + "end": "(\\2)", + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.html.angular" + } + }, + "patterns": [ + { + "begin": "(?<=('|\"|`))", + "end": "(?=\\1)", + "name": "source.ts.embedded.html.angular", + "patterns": [ + { + "include": "source.ts" + } + ] + } + ] + }, + { + "begin": "(=)\\s*(?=[^'\"`])", + "beginCaptures": { + "1": { + "name": "punctuation.separator.key-value.html.angular" + } + }, + "end": "(?=(\\s|>|\\/>))", + "patterns": [ + { + "begin": "(?=[^'\"`])", + "end": "(?=(\\s|>|\\/>))", + "name": "source.ts.embedded.html.angular", + "patterns": [ + { + "include": "source.ts" + } + ] + } + ] + } + ] + } + }, + "scopeName": "text.html.angular.directives" +} \ No newline at end of file diff --git a/examples/vscode-angular/syntaxes/angular-interpolations.json b/examples/vscode-angular/syntaxes/angular-interpolations.json new file mode 100644 index 000000000..44be88b68 --- /dev/null +++ b/examples/vscode-angular/syntaxes/angular-interpolations.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "fileTypes": [], + "injectionSelector": "L:text.pug -comment -string.comment, L:text.html.derivative -comment.block, L:text.html.markdown -comment.block", + "patterns": [ + { + "include": "#angular-interpolations" + } + ], + "repository": { + "angular-interpolations": { + "patterns": [ + { + "begin": "\\{\\{", + "beginCaptures": [ + { + "name": "punctuation.definition.tag.begin.html.angular" + } + ], + "end": "\\}\\}", + "endCaptures": [ + { + "name": "punctuation.definition.tag.end.html.angular" + } + ], + "name": "expression.embbeded.angular", + "patterns": [ + { + "begin": "\\G", + "end": "(?=\\}\\})", + "name": "source.ts.embedded.html.angular", + "patterns": [ + { + "include": "source.ts" + } + ] + } + ] + } + ] + } + }, + "scopeName": "angular.interpolations" +} \ No newline at end of file diff --git a/examples/vscode-angular/tsconfig.build.json b/examples/vscode-angular/tsconfig.build.json new file mode 100644 index 000000000..3f29db9bf --- /dev/null +++ b/examples/vscode-angular/tsconfig.build.json @@ -0,0 +1,24 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "out", + "rootDir": "src", + }, + "include": [ + "src", + "src/**/*.json" + ], + "exclude": [ + "node_modules", + ".vscode-test" + ], + "references": [ + { + "path": "../../packages/vscode-language-client/tsconfig.build.json" + }, + { + "path": "../angular-language-server/tsconfig.build.json" + } + ] +} \ No newline at end of file diff --git a/examples/vscode-svelte/package.json b/examples/vscode-svelte/package.json index 0c1ae1291..7727508fe 100644 --- a/examples/vscode-svelte/package.json +++ b/examples/vscode-svelte/package.json @@ -5,7 +5,7 @@ "repository": { "type": "git", "url": "https://github.com/johnsoncodehk/volar.git", - "directory": "examples/svelte" + "directory": "examples/vscode-svelte" }, "displayName": "Svelte Language Server (Example)", "description": "Svelte Language Server Example", diff --git a/examples/vscode-svelte/src/client.ts b/examples/vscode-svelte/src/client.ts index c8b9b8952..fa0fe2c0a 100644 --- a/examples/vscode-svelte/src/client.ts +++ b/examples/vscode-svelte/src/client.ts @@ -35,9 +35,6 @@ export async function activate(context: vscode.ExtensionContext) { const clientOptions: lsp.LanguageClientOptions = { documentSelector, initializationOptions, - synchronize: { - fileEvents: vscode.workspace.createFileSystemWatcher('{**/*.svelte,**/*.js,**/*.jsx,**/*.ts,**/*.tsx,**/*.json}') - }, middleware: { workspace: { configuration(params, token, next) { diff --git a/packages/language-core/src/documentRegistry.ts b/packages/language-core/src/documentRegistry.ts index 40b6b6293..d167f53e6 100644 --- a/packages/language-core/src/documentRegistry.ts +++ b/packages/language-core/src/documentRegistry.ts @@ -3,12 +3,10 @@ import { computed, shallowReactive } from '@vue/reactivity'; import { Teleport } from './sourceMaps'; import type { EmbeddedFile, LanguageModule, SourceFile } from './types'; -export function forEachEmbeddeds(input: EmbeddedFile[], cb: (embedded: EmbeddedFile) => void) { - for (const child of input) { - if (child) { - cb(child); - } - forEachEmbeddeds(child.embeddeds, cb); +export function forEachEmbeddeds(file: EmbeddedFile, cb: (embedded: EmbeddedFile) => void) { + cb(file); + for (const child of file.embeddeds) { + forEachEmbeddeds(child, cb); } } @@ -22,7 +20,7 @@ export function createDocumentRegistry() { const embeddedDocumentsMap = computed(() => { const map = new WeakMap(); for (const [sourceFile] of all.value) { - forEachEmbeddeds(sourceFile.embeddeds, embedded => { + forEachEmbeddeds(sourceFile, embedded => { map.set(embedded, sourceFile); }); } @@ -31,7 +29,7 @@ export function createDocumentRegistry() { const sourceMapsByFileName = computed(() => { const map = new Map(); for (const [sourceFile] of all.value) { - forEachEmbeddeds(sourceFile.embeddeds, embedded => { + forEachEmbeddeds(sourceFile, embedded => { map.set(normalizePath(embedded.fileName), { sourceFile, embedded }); }); } @@ -41,7 +39,7 @@ export function createDocumentRegistry() { const map = new Map(); for (const key in files) { const [sourceFile] = files[key]!; - forEachEmbeddeds(sourceFile.embeddeds, embedded => { + forEachEmbeddeds(sourceFile, embedded => { if (embedded.teleportMappings) { map.set(normalizePath(embedded.fileName), getTeleport(sourceFile, embedded.teleportMappings)); } diff --git a/packages/language-core/src/languageContext.ts b/packages/language-core/src/languageContext.ts index f21c727b2..49b82f12f 100644 --- a/packages/language-core/src/languageContext.ts +++ b/packages/language-core/src/languageContext.ts @@ -199,7 +199,7 @@ export function createEmbeddedLanguageServiceHost( for (const [sourceFile, languageModule, snapshot] of sourceFilesToUpdate) { - forEachEmbeddeds(sourceFile.embeddeds, embedded => { + forEachEmbeddeds(sourceFile, embedded => { fileVersions.delete(embedded.fileName); }); @@ -207,7 +207,7 @@ export function createEmbeddedLanguageServiceHost( const newScripts: Record = {}; if (!tsFileUpdated) { - forEachEmbeddeds(sourceFile.embeddeds, embedded => { + forEachEmbeddeds(sourceFile, embedded => { if (embedded.kind) { oldScripts[embedded.fileName] = embedded.text; } @@ -218,7 +218,7 @@ export function createEmbeddedLanguageServiceHost( documentRegistry.onSourceFileUpdated(sourceFile); if (!tsFileUpdated) { - forEachEmbeddeds(sourceFile.embeddeds, embedded => { + forEachEmbeddeds(sourceFile, embedded => { if (embedded.kind) { newScripts[embedded.fileName] = embedded.text; } diff --git a/packages/language-core/src/types.ts b/packages/language-core/src/types.ts index 124642e0a..d9aae3531 100644 --- a/packages/language-core/src/types.ts +++ b/packages/language-core/src/types.ts @@ -48,10 +48,8 @@ export interface TextRange { end: number, } -export interface SourceFile { - fileName: string, - text: string, - embeddeds: EmbeddedFile[], +export interface SourceFile extends EmbeddedFile { + // TODO: snapshot } export enum EmbeddedFileKind { diff --git a/packages/language-server/src/features/customFeatures.ts b/packages/language-server/src/features/customFeatures.ts index 6737e7e82..413d1a887 100644 --- a/packages/language-server/src/features/customFeatures.ts +++ b/packages/language-server/src/features/customFeatures.ts @@ -77,7 +77,7 @@ export function register( if (project) { const sourceFile = project.project?.getLanguageService().context.core.mapper.get(shared.getPathOfUri(document.uri))?.[0]; if (sourceFile) { - forEachEmbeddeds(sourceFile.embeddeds, e => { + forEachEmbeddeds(sourceFile, e => { if (e.text && e.kind === 1) { fileNames.push(e.fileName); } diff --git a/packages/language-service/src/documents.ts b/packages/language-service/src/documents.ts index 87ca00540..17cf2eccd 100644 --- a/packages/language-service/src/documents.ts +++ b/packages/language-service/src/documents.ts @@ -258,14 +258,14 @@ export function parseSourceFileDocument(sourceFile: SourceFile) { )); const allSourceMaps = computed(() => { const result: EmbeddedDocumentSourceMap[] = []; - forEachEmbeddeds(sourceFile.embeddeds, embedded => { + forEachEmbeddeds(sourceFile, embedded => { result.push(getSourceMap(embedded)); }); return result; }); const teleports = computed(() => { const result: TeleportSourceMap[] = []; - forEachEmbeddeds(sourceFile.embeddeds, embedded => { + forEachEmbeddeds(sourceFile, embedded => { if (embedded.teleportMappings) { const embeddedDocument = getEmbeddedDocument(embedded)!; const sourceMap = new TeleportSourceMap( diff --git a/packages/language-service/src/languageFeatures/fileRename.ts b/packages/language-service/src/languageFeatures/fileRename.ts index 4163f8361..b139a22af 100644 --- a/packages/language-service/src/languageFeatures/fileRename.ts +++ b/packages/language-service/src/languageFeatures/fileRename.ts @@ -14,7 +14,7 @@ export function register(context: LanguageServiceRuntimeContext) { let tsExt: string | undefined; - forEachEmbeddeds(vueDocument.file.embeddeds, embedded => { + forEachEmbeddeds(vueDocument.file, embedded => { if (embedded.kind && embedded.fileName.replace(vueDocument.file.fileName, '').match(/^\.(js|ts)x?$/)) { tsExt = embedded.fileName.substring(embedded.fileName.lastIndexOf('.')); } diff --git a/packages/language-service/src/types.ts b/packages/language-service/src/types.ts index 151190941..37fcff76f 100644 --- a/packages/language-service/src/types.ts +++ b/packages/language-service/src/types.ts @@ -7,6 +7,8 @@ import type { SchemaRequestService } from 'vscode-json-languageservice'; import { URI } from 'vscode-uri'; import type * as vscode from 'vscode-languageserver-protocol'; +export * from 'vscode-languageserver-protocol'; + export interface DocumentServiceRuntimeContext { typescript: typeof import('typescript/lib/tsserverlibrary'); plugins: LanguageServicePlugin[]; diff --git a/packages/language-service/src/utils/definePlugin.ts b/packages/language-service/src/utils/definePlugin.ts index b81e75fcd..df51ca712 100644 --- a/packages/language-service/src/utils/definePlugin.ts +++ b/packages/language-service/src/utils/definePlugin.ts @@ -3,20 +3,20 @@ import { EmbeddedDocumentSourceMap, SourceFileDocument } from '../documents'; export async function visitEmbedded( vueDocument: SourceFileDocument, cb: (sourceMap: EmbeddedDocumentSourceMap) => Promise, - embeddeds = vueDocument.file.embeddeds, + current = vueDocument.file, ) { - for (const embedded of embeddeds) { + for (const embedded of current.embeddeds) { - if (!await visitEmbedded(vueDocument, cb, embedded.embeddeds)) { + if (!await visitEmbedded(vueDocument, cb, embedded)) { return false; } + } - const sourceMap = vueDocument.getSourceMap(embedded); + const sourceMap = vueDocument.getSourceMap(current); - if (!await cb(sourceMap)) { - return false; - } + if (!await cb(sourceMap)) { + return false; } return true; diff --git a/packages/language-service/src/utils/featureWorkers.ts b/packages/language-service/src/utils/featureWorkers.ts index f3b3a8711..62c34f7d5 100644 --- a/packages/language-service/src/utils/featureWorkers.ts +++ b/packages/language-service/src/utils/featureWorkers.ts @@ -72,8 +72,7 @@ export async function documentArgFeatureWorker( return true; }); } - - if (results.length === 0 || !!combineResult) { + else if (results.length === 0 || !!combineResult) { context.prepareLanguageServices(document); @@ -149,8 +148,7 @@ export async function languageFeatureWorker( return true; }); } - - if (document && (results.length === 0 || !!combineResult)) { + else if (document && (results.length === 0 || !!combineResult)) { for (const plugin of context.plugins) { diff --git a/packages/typescript/src/index.ts b/packages/typescript/src/index.ts index 691ab3238..6c827b0c6 100644 --- a/packages/typescript/src/index.ts +++ b/packages/typescript/src/index.ts @@ -49,7 +49,7 @@ export function createLanguageService(host: embedded.LanguageServiceHost, mods: const file = core.mapper.get(args.fileName); let edits: readonly ts.FileTextChanges[] = []; if (file) { - embedded.forEachEmbeddeds(file[0].embeddeds, embedded => { + embedded.forEachEmbeddeds(file[0], embedded => { if (embedded.kind && embedded.capabilities.codeAction) { edits = edits.concat(ls.organizeImports({ ...args, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c2aaff57..8c22315a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,13 +15,42 @@ importers: optionalDependencies: '@lerna-lite/cli': 1.13.0 '@vscode/test-web': 0.0.33 - vitepress: 1.0.0-alpha.31_@types+node@18.11.13 + vitepress: 1.0.0-alpha.31_@types+node@18.11.15 vue: 3.2.45 devDependencies: - '@types/node': 18.11.13 + '@types/node': 18.11.15 typescript: 4.9.4 - vite: 4.0.0_@types+node@18.11.13 - vitest: 0.25.7 + vite: 4.0.1_@types+node@18.11.15 + vitest: 0.25.8 + + examples/angular-language-core: + specifiers: + '@angular-eslint/bundled-angular-compiler': ^15.1.0 + '@angular/compiler': ^15.0.4 + '@typescript-eslint/types': ^5.46.1 + '@volar/language-core': 1.0.13 + vscode-languageserver-textdocument: ^1.0.7 + vscode-uri: ^3.0.6 + dependencies: + '@angular-eslint/bundled-angular-compiler': 15.1.0 + '@volar/language-core': link:../../packages/language-core + vscode-languageserver-textdocument: 1.0.7 + vscode-uri: 3.0.6 + devDependencies: + '@angular/compiler': 15.0.4 + '@typescript-eslint/types': 5.46.1 + + examples/angular-language-server: + specifiers: + '@volar-examples/angular-language-core': 1.0.13 + '@volar-plugins/typescript': 1.0.13 + '@volar/language-server': 1.0.13 + '@volar/shared': 1.0.13 + dependencies: + '@volar-examples/angular-language-core': link:../angular-language-core + '@volar-plugins/typescript': link:../../plugins/typescript + '@volar/language-server': link:../../packages/language-server + '@volar/shared': link:../../packages/shared examples/svelte-language-core: specifiers: @@ -65,6 +94,23 @@ importers: '@volar-examples/svelte-language-core': link:../svelte-language-core '@volar/typescript': link:../../packages/typescript + examples/vscode-angular: + specifiers: + '@types/vscode': 1.67.0 + '@volar-examples/angular-language-server': 1.0.13 + '@volar/vscode-language-client': 1.0.13 + typesafe-path: ^0.2.2 + vsce: latest + vscode-languageclient: ^8.0.2 + dependencies: + '@volar-examples/angular-language-server': link:../angular-language-server + '@volar/vscode-language-client': link:../../packages/vscode-language-client + typesafe-path: 0.2.2 + vscode-languageclient: 8.0.2 + devDependencies: + '@types/vscode': 1.67.0 + vsce: 2.15.0 + examples/vscode-svelte: specifiers: '@types/vscode': 1.67.0 @@ -105,7 +151,7 @@ importers: dependencies: typescript-vue-plugin-forward: file:extensions/vscode-typescript-vue-plugin/typescript-vue-plugin-forward devDependencies: - esbuild: 0.16.4 + esbuild: 0.16.7 typescript-vue-plugin: link:../../vue-language-tools/typescript-vue-plugin vsce: 2.15.0 @@ -138,8 +184,8 @@ importers: '@volar/vscode-language-client': link:../../packages/vscode-language-client '@volar/vue-language-core': link:../../vue-language-tools/vue-language-core '@volar/vue-language-server': link:../../vue-language-tools/vue-language-server - esbuild: 0.16.4 - esbuild-plugin-copy: 2.0.1_esbuild@0.16.4 + esbuild: 0.16.7 + esbuild-plugin-copy: 2.0.1_esbuild@0.16.7 esbuild-visualizer: 0.4.0 path-browserify: 1.0.1 punycode: 2.1.1 @@ -644,6 +690,22 @@ packages: dev: false optional: true + /@angular-eslint/bundled-angular-compiler/15.1.0: + resolution: {integrity: sha512-zcOx+PnYuVDIG3wd/JVzCYdEUarKGtgIcN4iU9ZF+BVk5e8i9cbD3U8U3EDJKbrrokbFl9GBBJMCOa6XYTGJwQ==} + dev: false + + /@angular/compiler/15.0.4: + resolution: {integrity: sha512-KtxgRJUGZamOXpIILFG2FTUW+bbc2phi/o6955/Q4LR1HOICQrYEy8PrT1Gp+lVXFKgDG+6cb01lH14LoBQvyw==} + engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0} + peerDependencies: + '@angular/core': 15.0.4 + peerDependenciesMeta: + '@angular/core': + optional: true + dependencies: + tslib: 2.4.0 + dev: true + /@babel/code-frame/7.18.6: resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} engines: {node: '>=6.9.0'} @@ -742,176 +804,176 @@ packages: resolution: {integrity: sha512-8HqW8EVqjnCmWXVpqAOZf+EGESdkR27odcMMMGefgKXtar00SoYNSryGv//TELI4T3QFsECo78p+0lmalk/CFA==} dev: false - /@esbuild/android-arm/0.16.4: - resolution: {integrity: sha512-rZzb7r22m20S1S7ufIc6DC6W659yxoOrl7sKP1nCYhuvUlnCFHVSbATG4keGUtV8rDz11sRRDbWkvQZpzPaHiw==} + /@esbuild/android-arm/0.16.7: + resolution: {integrity: sha512-yhzDbiVcmq6T1/XEvdcJIVcXHdLjDJ5cQ0Dp9R9p9ERMBTeO1dR5tc8YYv8zwDeBw1xZm+Eo3MRo8cwclhBS0g==} engines: {node: '>=12'} cpu: [arm] os: [android] requiresBuild: true optional: true - /@esbuild/android-arm64/0.16.4: - resolution: {integrity: sha512-VPuTzXFm/m2fcGfN6CiwZTlLzxrKsWbPkG7ArRFpuxyaHUm/XFHQPD4xNwZT6uUmpIHhnSjcaCmcla8COzmZ5Q==} + /@esbuild/android-arm64/0.16.7: + resolution: {integrity: sha512-tYFw0lBJSEvLoGzzYh1kXuzoX1iPkbOk3O29VqzQb0HbOy7t/yw1hGkvwoJhXHwzQUPsShyYcTgRf6bDBcfnTw==} engines: {node: '>=12'} cpu: [arm64] os: [android] requiresBuild: true optional: true - /@esbuild/android-x64/0.16.4: - resolution: {integrity: sha512-MW+B2O++BkcOfMWmuHXB15/l1i7wXhJFqbJhp82IBOais8RBEQv2vQz/jHrDEHaY2X0QY7Wfw86SBL2PbVOr0g==} + /@esbuild/android-x64/0.16.7: + resolution: {integrity: sha512-3P2OuTxwAtM3k/yEWTNUJRjMPG1ce8rXs51GTtvEC5z1j8fC1plHeVVczdeHECU7aM2/Buc0MwZ6ciM/zysnWg==} engines: {node: '>=12'} cpu: [x64] os: [android] requiresBuild: true optional: true - /@esbuild/darwin-arm64/0.16.4: - resolution: {integrity: sha512-a28X1O//aOfxwJVZVs7ZfM8Tyih2Za4nKJrBwW5Wm4yKsnwBy9aiS/xwpxiiTRttw3EaTg4Srerhcm6z0bu9Wg==} + /@esbuild/darwin-arm64/0.16.7: + resolution: {integrity: sha512-VUb9GK23z8jkosHU9yJNUgQpsfJn+7ZyBm6adi2Ec5/U241eR1tAn82QicnUzaFDaffeixiHwikjmnec/YXEZg==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] requiresBuild: true optional: true - /@esbuild/darwin-x64/0.16.4: - resolution: {integrity: sha512-e3doCr6Ecfwd7VzlaQqEPrnbvvPjE9uoTpxG5pyLzr2rI2NMjDHmvY1E5EO81O/e9TUOLLkXA5m6T8lfjK9yAA==} + /@esbuild/darwin-x64/0.16.7: + resolution: {integrity: sha512-duterlv3tit3HI9vhzMWnSVaB1B6YsXpFq1Ntd6Fou82BB1l4tucYy3FI9dHv3tvtDuS0NiGf/k6XsdBqPZ01w==} engines: {node: '>=12'} cpu: [x64] os: [darwin] requiresBuild: true optional: true - /@esbuild/freebsd-arm64/0.16.4: - resolution: {integrity: sha512-Oup3G/QxBgvvqnXWrBed7xxkFNwAwJVHZcklWyQt7YCAL5bfUkaa6FVWnR78rNQiM8MqqLiT6ZTZSdUFuVIg1w==} + /@esbuild/freebsd-arm64/0.16.7: + resolution: {integrity: sha512-9kkycpBFes/vhi7B7o0cf+q2WdJi+EpVzpVTqtWFNiutARWDFFLcB93J8PR1cG228sucsl3B+7Ts27izE6qiaQ==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] requiresBuild: true optional: true - /@esbuild/freebsd-x64/0.16.4: - resolution: {integrity: sha512-vAP+eYOxlN/Bpo/TZmzEQapNS8W1njECrqkTpNgvXskkkJC2AwOXwZWai/Kc2vEFZUXQttx6UJbj9grqjD/+9Q==} + /@esbuild/freebsd-x64/0.16.7: + resolution: {integrity: sha512-5Ahf6jzWXJ4J2uh9dpy5DKOO+PeRUE/9DMys6VuYfwgQzd6n5+pVFm58L2Z2gRe611RX6SdydnNaiIKM3svY7g==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] requiresBuild: true optional: true - /@esbuild/linux-arm/0.16.4: - resolution: {integrity: sha512-A47ZmtpIPyERxkSvIv+zLd6kNIOtJH03XA0Hy7jaceRDdQaQVGSDt4mZqpWqJYgDk9rg96aglbF6kCRvPGDSUA==} + /@esbuild/linux-arm/0.16.7: + resolution: {integrity: sha512-QqJnyCfu5OF78Olt7JJSZ7OSv/B4Hf+ZJWp4kkq9xwMsgu7yWq3crIic8gGOpDYTqVKKMDAVDgRXy5Wd/nWZyQ==} engines: {node: '>=12'} cpu: [arm] os: [linux] requiresBuild: true optional: true - /@esbuild/linux-arm64/0.16.4: - resolution: {integrity: sha512-2zXoBhv4r5pZiyjBKrOdFP4CXOChxXiYD50LRUU+65DkdS5niPFHbboKZd/c81l0ezpw7AQnHeoCy5hFrzzs4g==} + /@esbuild/linux-arm64/0.16.7: + resolution: {integrity: sha512-2wv0xYDskk2+MzIm/AEprDip39a23Chptc4mL7hsHg26P0gD8RUhzmDu0KCH2vMThUI1sChXXoK9uH0KYQKaDg==} engines: {node: '>=12'} cpu: [arm64] os: [linux] requiresBuild: true optional: true - /@esbuild/linux-ia32/0.16.4: - resolution: {integrity: sha512-uxdSrpe9wFhz4yBwt2kl2TxS/NWEINYBUFIxQtaEVtglm1eECvsj1vEKI0KX2k2wCe17zDdQ3v+jVxfwVfvvjw==} + /@esbuild/linux-ia32/0.16.7: + resolution: {integrity: sha512-APVYbEilKbD5ptmKdnIcXej2/+GdV65TfTjxR2Uk8t1EsOk49t6HapZW6DS/Bwlvh5hDwtLapdSumIVNGxgqLg==} engines: {node: '>=12'} cpu: [ia32] os: [linux] requiresBuild: true optional: true - /@esbuild/linux-loong64/0.16.4: - resolution: {integrity: sha512-peDrrUuxbZ9Jw+DwLCh/9xmZAk0p0K1iY5d2IcwmnN+B87xw7kujOkig6ZRcZqgrXgeRGurRHn0ENMAjjD5DEg==} + /@esbuild/linux-loong64/0.16.7: + resolution: {integrity: sha512-5wPUAGclplQrAW7EFr3F84Y/d++7G0KykohaF4p54+iNWhUnMVU8Bh2sxiEOXUy4zKIdpHByMgJ5/Ko6QhtTUw==} engines: {node: '>=12'} cpu: [loong64] os: [linux] requiresBuild: true optional: true - /@esbuild/linux-mips64el/0.16.4: - resolution: {integrity: sha512-sD9EEUoGtVhFjjsauWjflZklTNr57KdQ6xfloO4yH1u7vNQlOfAlhEzbyBKfgbJlW7rwXYBdl5/NcZ+Mg2XhQA==} + /@esbuild/linux-mips64el/0.16.7: + resolution: {integrity: sha512-hxzlXtWF6yWfkE/SMTscNiVqLOAn7fOuIF3q/kiZaXxftz1DhZW/HpnTmTTWrzrS7zJWQxHHT4QSxyAj33COmA==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] requiresBuild: true optional: true - /@esbuild/linux-ppc64/0.16.4: - resolution: {integrity: sha512-X1HSqHUX9D+d0l6/nIh4ZZJ94eQky8d8z6yxAptpZE3FxCWYWvTDd9X9ST84MGZEJx04VYUD/AGgciddwO0b8g==} + /@esbuild/linux-ppc64/0.16.7: + resolution: {integrity: sha512-WM83Dac0LdXty5xPhlOuCD5Egfk1xLND/oRLYeB7Jb/tY4kzFSDgLlq91wYbHua/s03tQGA9iXvyjgymMw62Vw==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] requiresBuild: true optional: true - /@esbuild/linux-riscv64/0.16.4: - resolution: {integrity: sha512-97ANpzyNp0GTXCt6SRdIx1ngwncpkV/z453ZuxbnBROCJ5p/55UjhbaG23UdHj88fGWLKPFtMoU4CBacz4j9FA==} + /@esbuild/linux-riscv64/0.16.7: + resolution: {integrity: sha512-3nkNnNg4Ax6MS/l8O8Ynq2lGEVJYyJ2EoY3PHjNJ4PuZ80EYLMrFTFZ4L/Hc16AxgtXKwmNP9TM0YKNiBzBiJQ==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] requiresBuild: true optional: true - /@esbuild/linux-s390x/0.16.4: - resolution: {integrity: sha512-pUvPQLPmbEeJRPjP0DYTC1vjHyhrnCklQmCGYbipkep+oyfTn7GTBJXoPodR7ZS5upmEyc8lzAkn2o29wD786A==} + /@esbuild/linux-s390x/0.16.7: + resolution: {integrity: sha512-3SA/2VJuv0o1uD7zuqxEP+RrAyRxnkGddq0bwHQ98v1KNlzXD/JvxwTO3T6GM5RH6JUd29RTVQTOJfyzMkkppA==} engines: {node: '>=12'} cpu: [s390x] os: [linux] requiresBuild: true optional: true - /@esbuild/linux-x64/0.16.4: - resolution: {integrity: sha512-N55Q0mJs3Sl8+utPRPBrL6NLYZKBCLLx0bme/+RbjvMforTGGzFvsRl4xLTZMUBFC1poDzBEPTEu5nxizQ9Nlw==} + /@esbuild/linux-x64/0.16.7: + resolution: {integrity: sha512-xi/tbqCqvPIzU+zJVyrpz12xqciTAPMi2fXEWGnapZymoGhuL2GIWIRXg4O2v5BXaYA5TSaiKYE14L0QhUTuQg==} engines: {node: '>=12'} cpu: [x64] os: [linux] requiresBuild: true optional: true - /@esbuild/netbsd-x64/0.16.4: - resolution: {integrity: sha512-LHSJLit8jCObEQNYkgsDYBh2JrJT53oJO2HVdkSYLa6+zuLJh0lAr06brXIkljrlI+N7NNW1IAXGn/6IZPi3YQ==} + /@esbuild/netbsd-x64/0.16.7: + resolution: {integrity: sha512-NUsYbq3B+JdNKn8SXkItFvdes9qTwEoS3aLALtiWciW/ystiCKM20Fgv9XQBOXfhUHyh5CLEeZDXzLOrwBXuCQ==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] requiresBuild: true optional: true - /@esbuild/openbsd-x64/0.16.4: - resolution: {integrity: sha512-nLgdc6tWEhcCFg/WVFaUxHcPK3AP/bh+KEwKtl69Ay5IBqUwKDaq/6Xk0E+fh/FGjnLwqFSsarsbPHeKM8t8Sw==} + /@esbuild/openbsd-x64/0.16.7: + resolution: {integrity: sha512-qjwzsgeve9I8Tbsko2FEkdSk2iiezuNGFgipQxY/736NePXDaDZRodIejYGWOlbYXugdxb0nif5yvypH6lKBmA==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] requiresBuild: true optional: true - /@esbuild/sunos-x64/0.16.4: - resolution: {integrity: sha512-08SluG24GjPO3tXKk95/85n9kpyZtXCVwURR2i4myhrOfi3jspClV0xQQ0W0PYWHioJj+LejFMt41q+PG3mlAQ==} + /@esbuild/sunos-x64/0.16.7: + resolution: {integrity: sha512-mFWDz4RoBTzPphTCkM7Kc7Qpa0o/Z01acajR+Ai7LdfKgcP/C6jYOaKwv7nKzD0+MjOT20j7You9g4ozYy1dKQ==} engines: {node: '>=12'} cpu: [x64] os: [sunos] requiresBuild: true optional: true - /@esbuild/win32-arm64/0.16.4: - resolution: {integrity: sha512-yYiRDQcqLYQSvNQcBKN7XogbrSvBE45FEQdH8fuXPl7cngzkCvpsG2H9Uey39IjQ6gqqc+Q4VXYHsQcKW0OMjQ==} + /@esbuild/win32-arm64/0.16.7: + resolution: {integrity: sha512-m39UmX19RvEIuC8sYZ0M+eQtdXw4IePDSZ78ZQmYyFaXY9krq4YzQCK2XWIJomNLtg4q+W5aXr8bW3AbqWNoVg==} engines: {node: '>=12'} cpu: [arm64] os: [win32] requiresBuild: true optional: true - /@esbuild/win32-ia32/0.16.4: - resolution: {integrity: sha512-5rabnGIqexekYkh9zXG5waotq8mrdlRoBqAktjx2W3kb0zsI83mdCwrcAeKYirnUaTGztR5TxXcXmQrEzny83w==} + /@esbuild/win32-ia32/0.16.7: + resolution: {integrity: sha512-1cbzSEZA1fANwmT6rjJ4G1qQXHxCxGIcNYFYR9ctI82/prT38lnwSRZ0i5p/MVXksw9eMlHlet6pGu2/qkXFCg==} engines: {node: '>=12'} cpu: [ia32] os: [win32] requiresBuild: true optional: true - /@esbuild/win32-x64/0.16.4: - resolution: {integrity: sha512-sN/I8FMPtmtT2Yw+Dly8Ur5vQ5a/RmC8hW7jO9PtPSQUPkowxWpcUZnqOggU7VwyT3Xkj6vcXWd3V/qTXwultQ==} + /@esbuild/win32-x64/0.16.7: + resolution: {integrity: sha512-QaQ8IH0JLacfGf5cf0HCCPnQuCTd/dAI257vXBgb/cccKGbH/6pVtI1gwhdAQ0Y48QSpTIFrh9etVyNdZY+zzw==} engines: {node: '>=12'} cpu: [x64] os: [win32] @@ -1485,11 +1547,11 @@ packages: /@types/chai-subset/1.3.3: resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} dependencies: - '@types/chai': 4.3.3 + '@types/chai': 4.3.4 dev: true - /@types/chai/4.3.3: - resolution: {integrity: sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==} + /@types/chai/4.3.4: + resolution: {integrity: sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==} dev: true /@types/minimatch/5.1.2: @@ -1501,8 +1563,8 @@ packages: dev: false optional: true - /@types/node/18.11.13: - resolution: {integrity: sha512-IASpMGVcWpUsx5xBOrxMj7Bl8lqfuTY7FKAnPmu5cHkfQVWF8GulWS1jbRqA934qZL35xh5xN/+Xe/i26Bod4w==} + /@types/node/18.11.15: + resolution: {integrity: sha512-VkhBbVo2+2oozlkdHXLrb3zjsRkpdnaU2bXmX8Wgle3PUi569eLRaHGlgETQHR7lLL1w7GiG3h9SnePhxNDecw==} dev: true /@types/node/18.8.0: @@ -1532,14 +1594,19 @@ packages: '@types/node': 18.8.0 dev: true - /@vitejs/plugin-vue/4.0.0_vite@4.0.0+vue@3.2.45: + /@typescript-eslint/types/5.46.1: + resolution: {integrity: sha512-Z5pvlCaZgU+93ryiYUwGwLl9AQVB/PQ1TsJ9NZ/gHzZjN7g9IAn6RSDkpCV8hqTwAiaj6fmCcKSQeBPlIpW28w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@vitejs/plugin-vue/4.0.0_vite@4.0.1+vue@3.2.45: resolution: {integrity: sha512-e0X4jErIxAB5oLtDqbHvHpJe/uWNkdpYV83AOG2xo2tEVSzCzewgJMtREZM30wXnM5ls90hxiOtAuVU6H5JgbA==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: vite: ^4.0.0 vue: ^3.2.25 dependencies: - vite: 4.0.0_@types+node@18.11.13 + vite: 4.0.1_@types+node@18.11.15 vue: 3.2.45 dev: false optional: true @@ -2097,13 +2164,13 @@ packages: dev: false optional: true - /chai/4.3.6: - resolution: {integrity: sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==} + /chai/4.3.7: + resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} engines: {node: '>=4'} dependencies: assertion-error: 1.1.0 check-error: 1.0.2 - deep-eql: 3.0.1 + deep-eql: 4.1.3 get-func-name: 2.0.0 loupe: 2.3.4 pathval: 1.1.1 @@ -2634,9 +2701,9 @@ packages: dev: false optional: true - /deep-eql/3.0.1: - resolution: {integrity: sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==} - engines: {node: '>=0.12'} + /deep-eql/4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} dependencies: type-detect: 4.0.8 dev: true @@ -2863,13 +2930,13 @@ packages: dev: false optional: true - /esbuild-plugin-copy/2.0.1_esbuild@0.16.4: + /esbuild-plugin-copy/2.0.1_esbuild@0.16.7: resolution: {integrity: sha512-/mvriqGv2QAyrkui3REZaLEjwqESBKWZQQJtOZEausI8C4QMChREXGASNzmWpTlHo/v+ipLW73QCiNemBKggMw==} peerDependencies: esbuild: '>= 0.14.0' dependencies: chalk: 4.1.2 - esbuild: 0.16.4 + esbuild: 0.16.7 fs-extra: 10.1.0 globby: 11.1.0 dev: true @@ -2883,34 +2950,34 @@ packages: yargs: 17.6.2 dev: true - /esbuild/0.16.4: - resolution: {integrity: sha512-qQrPMQpPTWf8jHugLWHoGqZjApyx3OEm76dlTXobHwh/EBbavbRdjXdYi/GWr43GyN0sfpap14GPkb05NH3ROA==} + /esbuild/0.16.7: + resolution: {integrity: sha512-P6OBFYFSQOGzfApqCeYKqfKRRbCIRsdppTXFo4aAvtiW3o8TTyiIplBvHJI171saPAiy3WlawJHCveJVIOIx1A==} engines: {node: '>=12'} hasBin: true requiresBuild: true optionalDependencies: - '@esbuild/android-arm': 0.16.4 - '@esbuild/android-arm64': 0.16.4 - '@esbuild/android-x64': 0.16.4 - '@esbuild/darwin-arm64': 0.16.4 - '@esbuild/darwin-x64': 0.16.4 - '@esbuild/freebsd-arm64': 0.16.4 - '@esbuild/freebsd-x64': 0.16.4 - '@esbuild/linux-arm': 0.16.4 - '@esbuild/linux-arm64': 0.16.4 - '@esbuild/linux-ia32': 0.16.4 - '@esbuild/linux-loong64': 0.16.4 - '@esbuild/linux-mips64el': 0.16.4 - '@esbuild/linux-ppc64': 0.16.4 - '@esbuild/linux-riscv64': 0.16.4 - '@esbuild/linux-s390x': 0.16.4 - '@esbuild/linux-x64': 0.16.4 - '@esbuild/netbsd-x64': 0.16.4 - '@esbuild/openbsd-x64': 0.16.4 - '@esbuild/sunos-x64': 0.16.4 - '@esbuild/win32-arm64': 0.16.4 - '@esbuild/win32-ia32': 0.16.4 - '@esbuild/win32-x64': 0.16.4 + '@esbuild/android-arm': 0.16.7 + '@esbuild/android-arm64': 0.16.7 + '@esbuild/android-x64': 0.16.7 + '@esbuild/darwin-arm64': 0.16.7 + '@esbuild/darwin-x64': 0.16.7 + '@esbuild/freebsd-arm64': 0.16.7 + '@esbuild/freebsd-x64': 0.16.7 + '@esbuild/linux-arm': 0.16.7 + '@esbuild/linux-arm64': 0.16.7 + '@esbuild/linux-ia32': 0.16.7 + '@esbuild/linux-loong64': 0.16.7 + '@esbuild/linux-mips64el': 0.16.7 + '@esbuild/linux-ppc64': 0.16.7 + '@esbuild/linux-riscv64': 0.16.7 + '@esbuild/linux-s390x': 0.16.7 + '@esbuild/linux-x64': 0.16.7 + '@esbuild/netbsd-x64': 0.16.7 + '@esbuild/openbsd-x64': 0.16.7 + '@esbuild/sunos-x64': 0.16.7 + '@esbuild/win32-arm64': 0.16.7 + '@esbuild/win32-ia32': 0.16.7 + '@esbuild/win32-x64': 0.16.7 /escalade/3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} @@ -5051,8 +5118,8 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 - /postcss/8.4.19: - resolution: {integrity: sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==} + /postcss/8.4.20: + resolution: {integrity: sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==} engines: {node: ^10 || ^12 || >=14} dependencies: nanoid: 3.3.4 @@ -5988,7 +6055,6 @@ packages: /tslib/2.4.0: resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} - dev: false /tsscmp/1.0.6: resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} @@ -6187,8 +6253,8 @@ packages: dev: false optional: true - /vite/4.0.0_@types+node@18.11.13: - resolution: {integrity: sha512-ynad+4kYs8Jcnn8J7SacS9vAbk7eMy0xWg6E7bAhS1s79TK+D7tVFGXVZ55S7RNLRROU1rxoKlvZ/qjaB41DGA==} + /vite/4.0.1_@types+node@18.11.15: + resolution: {integrity: sha512-kZQPzbDau35iWOhy3CpkrRC7It+HIHtulAzBhMqzGHKRf/4+vmh8rPDDdv98SWQrFWo6//3ozwsRmwQIPZsK9g==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true peerDependencies: @@ -6212,27 +6278,27 @@ packages: terser: optional: true dependencies: - '@types/node': 18.11.13 - esbuild: 0.16.4 - postcss: 8.4.19 + '@types/node': 18.11.15 + esbuild: 0.16.7 + postcss: 8.4.20 resolve: 1.22.1 rollup: 3.7.1 optionalDependencies: fsevents: 2.3.2 - /vitepress/1.0.0-alpha.31_@types+node@18.11.13: + /vitepress/1.0.0-alpha.31_@types+node@18.11.15: resolution: {integrity: sha512-FWFXLs7WLbFbemxjBWo2S2+qUZCIoeLLyAKfVUpIu3LUB8oQ8cyIANRGO6f6zsM51u2bvJU9Sm+V6Z0WjOWS2Q==} hasBin: true requiresBuild: true dependencies: '@docsearch/css': 3.3.0 '@docsearch/js': 3.3.0 - '@vitejs/plugin-vue': 4.0.0_vite@4.0.0+vue@3.2.45 + '@vitejs/plugin-vue': 4.0.0_vite@4.0.1+vue@3.2.45 '@vue/devtools-api': 6.4.5 '@vueuse/core': 9.6.0_vue@3.2.45 body-scroll-lock: 4.0.0-beta.0 shiki: 0.11.1 - vite: 4.0.0_@types+node@18.11.13 + vite: 4.0.1_@types+node@18.11.15 vue: 3.2.45 transitivePeerDependencies: - '@algolia/client-search' @@ -6249,8 +6315,8 @@ packages: dev: false optional: true - /vitest/0.25.7: - resolution: {integrity: sha512-lJ+Ue+v8kHl2JzjaKHJ9u5Yo/loU7zrWK2/Whn8OKQjtq5G7nkeWfXuq3elZaC8xKdkdIuWiiIicaNBG1F5yzg==} + /vitest/0.25.8: + resolution: {integrity: sha512-X75TApG2wZTJn299E/TIYevr4E9/nBo1sUtZzn0Ci5oK8qnpZAZyhwg0qCeMSakGIWtc6oRwcQFyFfW14aOFWg==} engines: {node: '>=v14.16.0'} hasBin: true peerDependencies: @@ -6271,12 +6337,12 @@ packages: jsdom: optional: true dependencies: - '@types/chai': 4.3.3 + '@types/chai': 4.3.4 '@types/chai-subset': 1.3.3 - '@types/node': 18.11.13 + '@types/node': 18.11.15 acorn: 8.8.1 acorn-walk: 8.2.0 - chai: 4.3.6 + chai: 4.3.7 debug: 4.3.4 local-pkg: 0.4.2 source-map: 0.6.1 @@ -6284,7 +6350,7 @@ packages: tinybench: 2.3.1 tinypool: 0.3.0 tinyspy: 1.0.2 - vite: 4.0.0_@types+node@18.11.13 + vite: 4.0.1_@types+node@18.11.15 transitivePeerDependencies: - less - sass diff --git a/tsconfig.build.json b/tsconfig.build.json index c85438142..1163d9c64 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -29,6 +29,12 @@ { "path": "./examples/svelte-tsc/tsconfig.build.json" }, + { + "path": "./examples/vscode-svelte/tsconfig.build.json" + }, + { + "path": "./examples/vscode-angular/tsconfig.build.json" + }, // IDE / tests { "path": "./tsconfig.json" diff --git a/vue-language-tools/vue-language-core/src/generators/template.ts b/vue-language-tools/vue-language-core/src/generators/template.ts index fb686f96b..659a8806a 100644 --- a/vue-language-tools/vue-language-core/src/generators/template.ts +++ b/vue-language-tools/vue-language-core/src/generators/template.ts @@ -1394,7 +1394,7 @@ export function generate( prop.loc.start.offset, capabilitiesSet.diagnosticOnly, ]); - codeGen.push(`(await import('./__VLS_types.js')).directiveFunction(__VLS_ctx.`); + codeGen.push(`(await import('./__VLS_types.js')).directiveFunction((__VLS_ctx).`); codeGen.push([ camelize('v-' + prop.name), 'template', diff --git a/vue-language-tools/vue-language-core/src/sourceFile.ts b/vue-language-tools/vue-language-core/src/sourceFile.ts index a018e53a1..cfc3bcedd 100644 --- a/vue-language-tools/vue-language-core/src/sourceFile.ts +++ b/vue-language-tools/vue-language-core/src/sourceFile.ts @@ -349,6 +349,37 @@ export class VueSourceFile implements SourceFile { return blocks; }); + get kind(): EmbeddedFileKind { + return EmbeddedFileKind.TextFile; + } + + get capabilities(): DocumentCapabilities { + return { + diagnostic: true, + foldingRange: true, + documentFormatting: true, + documentSymbol: true, + codeAction: true, + inlayHint: true, + }; + } + + get mappings(): Mapping[] { + return [{ + sourceRange: [0, this._snapshot.value.getLength()], + generatedRange: [0, this._snapshot.value.getLength()], + data: { + hover: true, + references: true, + definition: true, + rename: true, + completion: true, + diagnostic: true, + semanticTokens: true, + }, + }]; + } + get text() { return this._snapshot.value.getText(0, this._snapshot.value.getLength()); } diff --git a/vue-language-tools/vue-language-core/src/utils/transform.ts b/vue-language-tools/vue-language-core/src/utils/transform.ts index 9b293d396..dc555dd02 100644 --- a/vue-language-tools/vue-language-core/src/utils/transform.ts +++ b/vue-language-tools/vue-language-core/src/utils/transform.ts @@ -75,7 +75,7 @@ export function walkInterpolationFragment( } } else { - cb('__VLS_ctx.', undefined); + cb('(__VLS_ctx).', undefined); if (ctxVars[i + 1].isShorthand) { cb(code.substring(ctxVars[i].offset, ctxVars[i + 1].offset + ctxVars[i + 1].text.length), ctxVars[i].offset); cb(': ', undefined); @@ -100,7 +100,7 @@ export function walkInterpolationFragment( } else { cb('', ctxVars[ctxVars.length - 1].offset, true); - cb('__VLS_ctx.', undefined); + cb('(__VLS_ctx).', undefined); cb(code.substring(ctxVars[ctxVars.length - 1].offset), ctxVars[ctxVars.length - 1].offset); } } diff --git a/vue-language-tools/vue-language-service/src/helpers.ts b/vue-language-tools/vue-language-service/src/helpers.ts index 38db8c24c..f3771e96a 100644 --- a/vue-language-tools/vue-language-service/src/helpers.ts +++ b/vue-language-tools/vue-language-service/src/helpers.ts @@ -186,7 +186,7 @@ function getComponentsType( let file: embedded.SourceFile | undefined; let tsSourceFile: ts.SourceFile | undefined; - embedded.forEachEmbeddeds(sourceFile.embeddeds, embedded => { + embedded.forEachEmbeddeds(sourceFile, embedded => { if (embedded.fileName === sourceFile.tsFileName) { file = embedded; } diff --git a/vue-language-tools/vue-language-service/src/plugins/vue-autoinsert-parentheses.ts b/vue-language-tools/vue-language-service/src/plugins/vue-autoinsert-parentheses.ts index 516e50604..eb2b93ce1 100644 --- a/vue-language-tools/vue-language-service/src/plugins/vue-autoinsert-parentheses.ts +++ b/vue-language-tools/vue-language-service/src/plugins/vue-autoinsert-parentheses.ts @@ -33,7 +33,7 @@ export default function (options: { let templateFormatScript: EmbeddedFile | undefined; - embedded.forEachEmbeddeds(vueDocument.file.embeddeds, embedded => { + embedded.forEachEmbeddeds(vueDocument.file, embedded => { if (embedded.fileName.endsWith('.__VLS_template_format.ts')) { templateFormatScript = embedded; } diff --git a/vue-language-tools/vue-language-service/src/plugins/vue-twoslash-queries.ts b/vue-language-tools/vue-language-service/src/plugins/vue-twoslash-queries.ts index 78d927224..fef323dc2 100644 --- a/vue-language-tools/vue-language-service/src/plugins/vue-twoslash-queries.ts +++ b/vue-language-tools/vue-language-service/src/plugins/vue-twoslash-queries.ts @@ -33,7 +33,7 @@ export default function (options: { })]); } - forEachEmbeddeds(vueFile.embeddeds, (embedded) => { + forEachEmbeddeds(vueFile, (embedded) => { if (embedded.kind === EmbeddedFileKind.TypeScriptHostFile) { const sourceMap = vueDocument.getSourceMap(embedded); for (const [pointerPosition, hoverOffset] of hoverOffsets) { From f7b1fbd0cfc55da1ab613f9e9f9b7b16c226eef4 Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Fri, 16 Dec 2022 19:53:59 +0800 Subject: [PATCH 2/7] feat(angular): support type-check for component props --- .../angular-language-core/src/modules/html.ts | 60 +++++++++++++++---- .../angular-language-core/src/modules/ts.ts | 51 +++++++++++++++- 2 files changed, 98 insertions(+), 13 deletions(-) diff --git a/examples/angular-language-core/src/modules/html.ts b/examples/angular-language-core/src/modules/html.ts index e37b02bd8..83b305e5f 100644 --- a/examples/angular-language-core/src/modules/html.ts +++ b/examples/angular-language-core/src/modules/html.ts @@ -103,7 +103,8 @@ function generate( const ngTemplates: TmplAstTemplate[] = []; codegen.text += 'export { };\n'; - codegen.text += `declare const __ctx: __Components['${fileName}'];\n`; + codegen.text += `declare const __ctx: __Templates2Components['${fileName}'];\n`; + codegen.text += `declare const __components: __Selectors2Components;\n`; const visitor: Parameters[0] = { visit(node) { @@ -113,12 +114,51 @@ function generate( visitElement(element) { console.log('visitElement'); codegen.text += `{\n`; + const isComponent = element.name.indexOf('-') >= 0; + if (isComponent) { + codegen.text += '__components'; + codegen.addPropertyAccess( + element.startSourceSpan.start.offset + '<'.length, + element.startSourceSpan.start.offset + '<'.length + element.name.length, + ); + codegen.text += ` = {\n`; + } + else { + codegen.text += `const { } = {\n`; + } + if (element.name === 'my-app') + console.log(JSON.stringify(element)); for (const input of element.inputs) { - codegen.text += '('; - // console.log(input.sourceSpan.start.offset, input.sourceSpan.end.offset); - addInterpolationFragment(input.value.sourceSpan.start, input.value.sourceSpan.end); - codegen.text += `);\n`; + codegen.addObjectKey(input.keySpan.start.offset, input.keySpan.end.offset); + codegen.text += ': ('; + if (input.valueSpan) { + addInterpolationFragment(input.valueSpan.start.offset, input.valueSpan.end.offset); + codegen.text += `)`; + codegen.addSourceText(input.valueSpan.end.offset, input.valueSpan.end.offset); // error message mapping + } + else { + codegen.text += `)`; + codegen.addSourceText(input.keySpan.end.offset, input.keySpan.end.offset); // error message mapping + } + codegen.text += `,\n`; } + for (const attr of element.attributes) { + if (attr.keySpan) { + codegen.addObjectKey(attr.keySpan.start.offset, attr.keySpan.end.offset); + codegen.text += ': "'; + if (attr.valueSpan) { + codegen.addSourceText(attr.valueSpan.start.offset, attr.valueSpan.end.offset); + codegen.text += `"`; + codegen.addSourceText(attr.valueSpan.end.offset, attr.valueSpan.end.offset); // error message mapping + } + else { + codegen.text += `"`; + codegen.addSourceText(attr.keySpan.end.offset, attr.keySpan.end.offset); // error message mapping + } + codegen.text += `,\n`; + } + } + codegen.text += `};\n`; for (const child of element.children) { child.visit(visitor); } @@ -148,9 +188,10 @@ function generate( codegen.text += `}\n`; } if (attr.name === 'ngIfElse' && attr.valueSpan) { - codegen.text += '(__templates).'; - const templateBlock = codegen.addSourceText(attr.valueSpan.start.offset, attr.valueSpan.end.offset); + codegen.text += '(__templates)'; + codegen.addPropertyAccess(attr.valueSpan.start.offset, attr.valueSpan.end.offset); codegen.text += ';\n'; + const templateBlock = fileText.substring(attr.valueSpan.start.offset, attr.valueSpan.end.offset); templateBlocksConditions[templateBlock] ??= []; templateBlocksConditions[templateBlock].push([ ...conditions, @@ -180,8 +221,6 @@ function generate( } localVars[binding]--; codegen.text += `}\n`; - - // console.log(JSON.stringify(template)); } }, visitContent(content) { @@ -236,9 +275,10 @@ function generate( codegen.text += 'var __templates = {\n'; for (const template of ngTemplates) { for (const reference of template.references) { - const templateBlock = codegen.addSourceText(reference.keySpan.start.offset, reference.keySpan.end.offset); + codegen.addObjectKey(reference.keySpan.start.offset, reference.keySpan.end.offset); codegen.text += ': (() => {\n'; let ifBlockOpen = false; + const templateBlock = fileText.substring(reference.keySpan.start.offset, reference.keySpan.end.offset); if (templateBlocksConditions[templateBlock]) { ifBlockOpen = true; codegen.text += `if (`; diff --git a/examples/angular-language-core/src/modules/ts.ts b/examples/angular-language-core/src/modules/ts.ts index 30adc2e45..62ca640e4 100644 --- a/examples/angular-language-core/src/modules/ts.ts +++ b/examples/angular-language-core/src/modules/ts.ts @@ -53,6 +53,7 @@ export function createTsLanguageModule( const classComponents: { templateUrl?: string, + selectorNode?: ts.StringLiteral, urlNodes: ts.Node[], decoratorName: string, className: string, @@ -75,6 +76,10 @@ export function createTsLanguageModule( decoratorName, urlNodes: [], }; + const selectorProp = decorator.expression.arguments[0].properties.find((prop) => prop.name?.getText(ast) === 'selector'); + if (selectorProp && ts.isPropertyAssignment(selectorProp) && ts.isStringLiteral(selectorProp.initializer)) { + classComponent.selectorNode = selectorProp.initializer; + } const templateUrlProp = decorator.expression.arguments[0].properties.find((prop) => prop.name?.getText(ast) === 'templateUrl'); if (templateUrlProp && ts.isPropertyAssignment(templateUrlProp) && ts.isStringLiteral(templateUrlProp.initializer)) { const templateUrl = path.resolve(path.dirname(ast.fileName), templateUrlProp.initializer.text); @@ -109,16 +114,25 @@ export function createTsLanguageModule( } } const classComponentsWithTemplateUrl = classComponents.filter(component => !!component.templateUrl); + codegen.text += `declare global {\n`; if (classComponentsWithTemplateUrl.length) { codegen.text += `type __WithComponent

= C1 extends import('@angular/core').Component ? { [k in P]: C2 } : {};\n`; - codegen.text += `declare global {\n`; - codegen.text += `interface __Components extends\n`; + codegen.text += `interface __Templates2Components extends\n`; codegen.text += classComponentsWithTemplateUrl.map((component) => { return `__WithComponent<'${component.templateUrl}', ${component.decoratorName}, ${component.className}>`; }).join(',\n'); codegen.text += `\n{ }\n`; - codegen.text += `}\n`; } + const classComponentsWithSelector = classComponents.filter(component => !!component.selectorNode); + if (classComponentsWithSelector.length) { + codegen.text += `type __WithComponent2

= C1 extends import('@angular/core').Component ? P : {};\n`; + for (const classComponentWithSelector of classComponentsWithSelector) { + codegen.text += `interface __Selectors2Components extends __WithComponent2<{ `; + codegen.addSourceText(classComponentWithSelector.selectorNode!.getStart(ast), classComponentWithSelector.selectorNode!.getEnd()); + codegen.text += `: ${classComponentWithSelector.className} }, ${classComponentWithSelector.decoratorName}> { }\n`; + } + } + codegen.text += `}\n`; } return codegen; @@ -137,6 +151,8 @@ const fullCap: PositionCapabilities = { export class Codegen { + static validTsVar = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/; + constructor(public sourceCode: string) { } public text = ''; @@ -152,4 +168,33 @@ export class Codegen { this.text += addText; return addText; } + + public addPropertyAccess(start: number, end: number, data: PositionCapabilities = fullCap) { + if (Codegen.validTsVar.test(this.sourceCode.substring(start, end))) { + this.text += `.`; + this.addSourceText(start, end, data); + } + else { + this.text += `[`; + this.addSourceTextWithQuotes(start, end, data); + this.text += `]`; + } + } + + public addObjectKey(start: number, end: number, data: PositionCapabilities = fullCap) { + if (Codegen.validTsVar.test(this.sourceCode.substring(start, end))) { + this.addSourceText(start, end, data); + } + else { + this.addSourceTextWithQuotes(start, end, data); + } + } + + public addSourceTextWithQuotes(start: number, end: number, data: PositionCapabilities = fullCap) { + this.addSourceText(start, start, data); + this.text += `'`; + this.addSourceText(start, end, data); + this.text += `'`; + this.addSourceText(end, end, data); + } } From 1d2cc778264840f158bc4b83a79552dd0b419869 Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Fri, 16 Dec 2022 21:24:34 +0800 Subject: [PATCH 3/7] feat(angular): support type-check for component events --- .../angular-language-core/src/modules/html.ts | 17 ++++++++++++++--- examples/vscode-angular/package.json | 5 ----- examples/vscode-angular/src/client.ts | 11 +++++++++-- .../syntaxes/angular-directives.json | 11 ++++++++++- .../vscode-vue-language-features/src/common.ts | 11 +++++++++-- .../src/features/tsVersion.ts | 14 ++++---------- 6 files changed, 46 insertions(+), 23 deletions(-) diff --git a/examples/angular-language-core/src/modules/html.ts b/examples/angular-language-core/src/modules/html.ts index 83b305e5f..65967dce3 100644 --- a/examples/angular-language-core/src/modules/html.ts +++ b/examples/angular-language-core/src/modules/html.ts @@ -126,8 +126,6 @@ function generate( else { codegen.text += `const { } = {\n`; } - if (element.name === 'my-app') - console.log(JSON.stringify(element)); for (const input of element.inputs) { codegen.addObjectKey(input.keySpan.start.offset, input.keySpan.end.offset); codegen.text += ': ('; @@ -162,6 +160,18 @@ function generate( for (const child of element.children) { child.visit(visitor); } + if (isComponent) { + for (const output of element.outputs) { + codegen.text += `__components['${element.name}']`; + codegen.addPropertyAccess(output.keySpan.start.offset, output.keySpan.end.offset); + codegen.text += `.subscribe($event => {\n`; + localVars.$event ??= 0; + localVars.$event++; + addInterpolationFragment(output.handlerSpan.start.offset, output.handlerSpan.end.offset); + localVars.$event--; + codegen.text += '};\n'; + } + } codegen.text += `}\n`; }, visitTemplate(template) { @@ -215,7 +225,8 @@ function generate( codegen.text += ` of `; addInterpolationFragment(forOfSource.start.offset, forOfSource.end.offset); codegen.text += `) {\n`; - localVars[binding] = localVars[binding] ? localVars[binding] + 1 : 1; + localVars[binding] ??= 0; + localVars[binding]++; for (const child of template.children) { child.visit(visitor); } diff --git a/examples/vscode-angular/package.json b/examples/vscode-angular/package.json index 4c99cce59..0bb3e9822 100644 --- a/examples/vscode-angular/package.json +++ b/examples/vscode-angular/package.json @@ -60,11 +60,6 @@ "category": "Volar-Angular (Debug)" } ], - "configurationDefaults": { - "[html]": { - "editor.semanticHighlighting.enabled": true - } - }, "configuration": { "type": "object", "title": "Volar-Angular", diff --git a/examples/vscode-angular/src/client.ts b/examples/vscode-angular/src/client.ts index 1a5b395fc..cf503e61b 100644 --- a/examples/vscode-angular/src/client.ts +++ b/examples/vscode-angular/src/client.ts @@ -2,7 +2,11 @@ import { LanguageServerInitializationOptions } from '@volar/language-server'; import * as path from 'typesafe-path'; import * as vscode from 'vscode'; import * as lsp from 'vscode-languageclient/node'; -import { registerShowVirtualFiles, registerTsConfig } from '@volar/vscode-language-client'; +import { + registerShowVirtualFiles, + registerTsConfig, + registerTsVersion, +} from '@volar/vscode-language-client'; let client: lsp.BaseLanguageClient; @@ -47,8 +51,11 @@ export async function activate(context: vscode.ExtensionContext) { ); await client.start(); + const isSupportDoc = (document: vscode.TextDocument) => documentSelector.some(selector => selector.language === document.languageId); + registerShowVirtualFiles('volar-angular.action.showVirtualFiles', context, client); - registerTsConfig('volar-angular.action.showTsConfig', context, client, document => documentSelector.some(selector => selector.language === document.languageId)); + registerTsConfig('volar-angular.action.showTsConfig', context, client, isSupportDoc); + registerTsVersion('volar-angular.action.showTsVersion', context, client, isSupportDoc, text => text + ' (angular)'); } export function deactivate(): Thenable | undefined { diff --git a/examples/vscode-angular/syntaxes/angular-directives.json b/examples/vscode-angular/syntaxes/angular-directives.json index d914de9a5..9286ddf43 100644 --- a/examples/vscode-angular/syntaxes/angular-directives.json +++ b/examples/vscode-angular/syntaxes/angular-directives.json @@ -9,7 +9,7 @@ ], "repository": { "directives": { - "begin": "((\\*?)(\\[)((ngIf)|(ngFor)|([^\\]]*))(\\]))|((\\*)((ngIf)|(ngFor)|([\\w-]*)))", + "begin": "((\\*?)(\\[)((ngIf)|(ngFor)|([^\\]]*))(\\]))|((\\*)((ngIf)|(ngFor)|([\\w-]*)))|((\\()(([^\\)]*))(\\)))", "beginCaptures": { "2": { "name": "punctuation.separator.key-value.html.angular" @@ -40,6 +40,15 @@ }, "14": { "name": "entity.other.attribute-name.html.angular" + }, + "16": { + "name": "punctuation.separator.key-value.html.angular" + }, + "17": { + "name": "entity.other.attribute-name.html.angular" + }, + "18": { + "name": "punctuation.separator.key-value.html.angular" } }, "end": "(?=\\s*+[^=\\s])", diff --git a/extensions/vscode-vue-language-features/src/common.ts b/extensions/vscode-vue-language-features/src/common.ts index 9db8cc15e..f32cd454c 100644 --- a/extensions/vscode-vue-language-features/src/common.ts +++ b/extensions/vscode-vue-language-features/src/common.ts @@ -132,8 +132,15 @@ async function doActivate(context: vscode.ExtensionContext, createLc: CreateLang && ['javascript', 'typescript', 'javascriptreact', 'typescriptreact'].includes(document.languageId) ); }, - () => takeOverModeEnabled(), - () => noProjectReferences(), + text => { + if (takeOverModeEnabled()) { + text += ' (vue)'; + } + if (noProjectReferences()) { + text += ' (noProjectReferences)'; + } + return text; + }, ); for (const client of clients) { diff --git a/packages/vscode-language-client/src/features/tsVersion.ts b/packages/vscode-language-client/src/features/tsVersion.ts index 73f59bea3..fe850502c 100644 --- a/packages/vscode-language-client/src/features/tsVersion.ts +++ b/packages/vscode-language-client/src/features/tsVersion.ts @@ -12,8 +12,7 @@ export async function register( context: vscode.ExtensionContext, client: BaseLanguageClient, shouldStatusBarShow: (document: vscode.TextDocument) => boolean, - takeOverModeEnabled: () => boolean, - noProjectReferences: () => boolean, + resolveStatusText: (text: string) => string, ) { const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right); @@ -52,9 +51,9 @@ export async function register( } : undefined, }, { - takeover: takeOverModeEnabled() ? { + takeover: { label: 'What is Takeover Mode?', - } : undefined, + }, } ]); @@ -92,12 +91,7 @@ export async function register( else { const tsVersion = await getTsVersion(getTsdk(context).uri); statusBar.text = tsVersion ?? 'x.x.x'; - if (takeOverModeEnabled()) { - statusBar.text += ' (takeover)'; - } - if (noProjectReferences()) { - statusBar.text += ' (noProjectReferences)'; - } + statusBar.text = resolveStatusText(statusBar.text); statusBar.show(); } } From 006075ebe6a776ea9da5703b9106a7f41a2cc366 Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Fri, 16 Dec 2022 21:41:28 +0800 Subject: [PATCH 4/7] fix(angular): avoid incorrect required props checking --- .../angular-language-core/src/modules/html.ts | 85 ++++++------------- .../angular-language-core/src/modules/ts.ts | 2 +- 2 files changed, 29 insertions(+), 58 deletions(-) diff --git a/examples/angular-language-core/src/modules/html.ts b/examples/angular-language-core/src/modules/html.ts index 65967dce3..a6e707070 100644 --- a/examples/angular-language-core/src/modules/html.ts +++ b/examples/angular-language-core/src/modules/html.ts @@ -22,7 +22,6 @@ export class HTMLTemplateFile implements SourceFile { public snapshot: ts.IScriptSnapshot, ) { - console.log('createSourceFile', fileName); const generated = generate(ts, fileName, snapshot.getText(0, snapshot.getLength())); this.text = snapshot.getText(0, snapshot.getLength()); // TODO: use empty string with snapshot @@ -56,7 +55,6 @@ export class HTMLTemplateFile implements SourceFile { } update(snapshot: ts.IScriptSnapshot) { - console.log('updateSourceFile', this.fileName); const generated = generate(this.ts, this.fileName, snapshot.getText(0, snapshot.getLength())); this.text = snapshot.getText(0, snapshot.getLength()); this.mappings = [ @@ -102,80 +100,62 @@ function generate( const conditions: string[] = []; const ngTemplates: TmplAstTemplate[] = []; + let elementIndex = 0; + codegen.text += 'export { };\n'; codegen.text += `declare const __ctx: __Templates2Components['${fileName}'];\n`; - codegen.text += `declare const __components: __Selectors2Components;\n`; + codegen.text += `declare const __components: __Selectors2Components & HTMLElementTagNameMap;\n`; const visitor: Parameters[0] = { visit(node) { - console.log('visit'); node.visit(visitor); }, visitElement(element) { - console.log('visitElement'); codegen.text += `{\n`; - const isComponent = element.name.indexOf('-') >= 0; - if (isComponent) { - codegen.text += '__components'; - codegen.addPropertyAccess( - element.startSourceSpan.start.offset + '<'.length, - element.startSourceSpan.start.offset + '<'.length + element.name.length, - ); - codegen.text += ` = {\n`; - } - else { - codegen.text += `const { } = {\n`; - } + // const isComponent = element.name.indexOf('-') >= 0; + const index = elementIndex++; + codegen.text += `const __element_${index} = __components`; + codegen.addPropertyAccess( + element.startSourceSpan.start.offset + '<'.length, + element.startSourceSpan.start.offset + '<'.length + element.name.length, + ); + codegen.text += `;\n`; for (const input of element.inputs) { - codegen.addObjectKey(input.keySpan.start.offset, input.keySpan.end.offset); - codegen.text += ': ('; + codegen.text += `__element_${index}`; + codegen.addPropertyAccess(input.keySpan.start.offset, input.keySpan.end.offset); + codegen.text += ' = ('; if (input.valueSpan) { addInterpolationFragment(input.valueSpan.start.offset, input.valueSpan.end.offset); - codegen.text += `)`; - codegen.addSourceText(input.valueSpan.end.offset, input.valueSpan.end.offset); // error message mapping - } - else { - codegen.text += `)`; - codegen.addSourceText(input.keySpan.end.offset, input.keySpan.end.offset); // error message mapping } - codegen.text += `,\n`; + codegen.text += `);\n`; } for (const attr of element.attributes) { if (attr.keySpan) { - codegen.addObjectKey(attr.keySpan.start.offset, attr.keySpan.end.offset); - codegen.text += ': "'; + codegen.text += `__element_${index}`; + codegen.addPropertyAccess(attr.keySpan.start.offset, attr.keySpan.end.offset); + codegen.text += ' = "'; if (attr.valueSpan) { codegen.addSourceText(attr.valueSpan.start.offset, attr.valueSpan.end.offset); - codegen.text += `"`; - codegen.addSourceText(attr.valueSpan.end.offset, attr.valueSpan.end.offset); // error message mapping } - else { - codegen.text += `"`; - codegen.addSourceText(attr.keySpan.end.offset, attr.keySpan.end.offset); // error message mapping - } - codegen.text += `,\n`; + codegen.text += `";\n`; } } - codegen.text += `};\n`; for (const child of element.children) { child.visit(visitor); } - if (isComponent) { - for (const output of element.outputs) { - codegen.text += `__components['${element.name}']`; - codegen.addPropertyAccess(output.keySpan.start.offset, output.keySpan.end.offset); - codegen.text += `.subscribe($event => {\n`; - localVars.$event ??= 0; - localVars.$event++; - addInterpolationFragment(output.handlerSpan.start.offset, output.handlerSpan.end.offset); - localVars.$event--; - codegen.text += '};\n'; - } + for (const output of element.outputs) { + codegen.text += `__element_${index}`; + codegen.addPropertyAccess(output.keySpan.start.offset, output.keySpan.end.offset); + codegen.text += `.subscribe($event => { `; + localVars.$event ??= 0; + localVars.$event++; + addInterpolationFragment(output.handlerSpan.start.offset, output.handlerSpan.end.offset); + localVars.$event--; + codegen.text += ' });\n'; } codegen.text += `}\n`; }, visitTemplate(template) { - console.log('visitTemplate', template.tagName); if (template.tagName === 'ng-template') { ngTemplates.push(template); @@ -235,36 +215,28 @@ function generate( } }, visitContent(content) { - console.log('visitContent'); content.visit(visitor); }, visitVariable(variable) { - console.log('visitVariable'); variable.visit(visitor); }, visitReference(reference) { - console.log('visitReference'); reference.visit(visitor); }, visitTextAttribute(attribute) { - console.log('visitTextAttribute'); attribute.visit(visitor); }, visitBoundAttribute(attribute) { - console.log('visitBoundAttribute'); attribute.visit(visitor); }, visitBoundEvent(event) { - console.log('visitBoundEvent'); event.visit(visitor); }, visitText(_text) { - console.log('visitText'); // text.visit(visitor); }, visitBoundText(text) { const content = fileText.substring(text.value.sourceSpan.start, text.value.sourceSpan.end); - console.log('visitBoundText'); const interpolations = content.matchAll(/{{[\s\S]*?}}/g); for (const interpolation of interpolations) { const start = text.value.sourceSpan.start + interpolation.index! + '{{'.length; @@ -274,7 +246,6 @@ function generate( } }, visitIcu(icu) { - console.log('visitIcu'); icu.visit(visitor); }, }; diff --git a/examples/angular-language-core/src/modules/ts.ts b/examples/angular-language-core/src/modules/ts.ts index 62ca640e4..444ed9bd2 100644 --- a/examples/angular-language-core/src/modules/ts.ts +++ b/examples/angular-language-core/src/modules/ts.ts @@ -36,7 +36,7 @@ export function createTsLanguageModule( const text = snapshot.getText(0, snapshot.getLength()); const change = snapshot.getChangeRange(sourceFile.snapshot); - // incremental update is important for performance + // incremental update for better performance sourceFile.ast = change ? sourceFile.ast.update(text, change) : ts.createSourceFile(sourceFile.fileName, text, ts.ScriptTarget.Latest); From efdc61e830738baf8f639593b5a70f662282ac07 Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Fri, 16 Dec 2022 22:01:22 +0800 Subject: [PATCH 5/7] chore: fix languageId of TextDocument --- packages/language-service/src/documents.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/language-service/src/documents.ts b/packages/language-service/src/documents.ts index 17cf2eccd..299a20efb 100644 --- a/packages/language-service/src/documents.ts +++ b/packages/language-service/src/documents.ts @@ -252,7 +252,7 @@ export function parseSourceFileDocument(sourceFile: SourceFile) { // computed const document = computed(() => TextDocument.create( shared.getUriByPath(sourceFile.fileName), - sourceFile.fileName.endsWith('.md') ? 'markdown' : 'vue', + shared.syntaxToLanguageId(sourceFile.fileName.slice(sourceFile.fileName.lastIndexOf('.') + 1)), documentVersion++, sourceFile.text, )); From 8a3e15879a2c0a755cfac4dfbdb2d1d79bbe6657 Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Fri, 16 Dec 2022 23:27:21 +0800 Subject: [PATCH 6/7] doc(angular): readme --- examples/vscode-angular/README.md | 38 ++++++++++++++++++++++++++++ examples/vscode-angular/package.json | 2 +- examples/vscode-svelte/package.json | 2 +- 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 examples/vscode-angular/README.md diff --git a/examples/vscode-angular/README.md b/examples/vscode-angular/README.md new file mode 100644 index 000000000..d4be4b885 --- /dev/null +++ b/examples/vscode-angular/README.md @@ -0,0 +1,38 @@ +# Angular Language Server (Example) + +> An example Angular Language Server implement via [Volar](https://github.com/johnsoncodehk/volar) framework. + +⚠️ This is a quick implementation for demonstration purposes only, functionality reliability and correctness are not guaranteed, and there are no plan to maintenance this plugin. + +## Features + +- Template AST error diagnosis +- Directives, Interpolations Syntax Highlighting +- Intellisense Support for Template Directives and Interpolations +- Typed Prop Support +- Typed Event Support +- Type Narrowing Support for `*ngIf` if else block + +## Usage + +We have only one performance-optimized available setup due to this is for demonstration purposes only. Please make sure you follow each step. + +1. Disable "TypeScript and JavaScript Language Features" to [takeover language support for .ts](https://vuejs.org/guide/typescript/overview.html#volar-takeover-mode) + + 1. Search `@builtin typescript-language-features` in Extensions sidebar + + 2. Right click and select "Disable (Workspace)" + +2. Disable "Angular Language Service" to avoid conflict + + 1. Search `Angular.ng-template` in Extensions sidebar + + 2. Right click and select "Disable (Workspace)" + +3. Disable "Vue Language Features (Volar)" to avoid it's takeover mode active + + 1. Search `Vue.volar` in Extensions sidebar + + 2. Right click and select "Disable (Workspace)" + +4. Reload VSCode diff --git a/examples/vscode-angular/package.json b/examples/vscode-angular/package.json index 0bb3e9822..3fe604498 100644 --- a/examples/vscode-angular/package.json +++ b/examples/vscode-angular/package.json @@ -1,6 +1,6 @@ { "private": true, - "name": "example-vscode-angular", + "name": "vscode-angular-example", "version": "1.0.13", "repository": { "type": "git", diff --git a/examples/vscode-svelte/package.json b/examples/vscode-svelte/package.json index 7727508fe..7ce609fae 100644 --- a/examples/vscode-svelte/package.json +++ b/examples/vscode-svelte/package.json @@ -1,6 +1,6 @@ { "private": true, - "name": "example-vscode-svelte", + "name": "vscode-svelte-example", "version": "1.0.13", "repository": { "type": "git", From 4f3f550c2dc9bb615a9100708f00de0b80c08c56 Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Sat, 17 Dec 2022 01:29:24 +0800 Subject: [PATCH 7/7] feat(angular): bundle by esbuild --- examples/angular-language-core/package.json | 3 +- examples/vscode-angular/.vscodeignore | 2 ++ examples/vscode-angular/README.md | 26 ++++++++++------ examples/vscode-angular/client.js | 3 ++ examples/vscode-angular/package.json | 19 +++++++----- examples/vscode-angular/scripts/build.js | 31 +++++++++++++++++++ examples/vscode-angular/server.js | 3 ++ examples/vscode-angular/src/client.ts | 30 +++++++++++++++--- examples/vscode-svelte/package.json | 2 +- .../language-server/src/registerFeatures.ts | 11 +++---- pnpm-lock.yaml | 24 ++++++-------- 11 files changed, 108 insertions(+), 46 deletions(-) create mode 100644 examples/vscode-angular/client.js create mode 100644 examples/vscode-angular/scripts/build.js create mode 100644 examples/vscode-angular/server.js diff --git a/examples/angular-language-core/package.json b/examples/angular-language-core/package.json index 5ce7866e9..45dc12068 100644 --- a/examples/angular-language-core/package.json +++ b/examples/angular-language-core/package.json @@ -19,7 +19,6 @@ "vscode-uri": "^3.0.6" }, "devDependencies": { - "@angular/compiler": "^15.0.4", - "@typescript-eslint/types": "^5.46.1" + "@angular/compiler": "^15.0.4" } } diff --git a/examples/vscode-angular/.vscodeignore b/examples/vscode-angular/.vscodeignore index 8a5911c6e..dc83992c1 100644 --- a/examples/vscode-angular/.vscodeignore +++ b/examples/vscode-angular/.vscodeignore @@ -1,3 +1,5 @@ +out +scripts src tsconfig.build.json tsconfig.build.tsbuildinfo diff --git a/examples/vscode-angular/README.md b/examples/vscode-angular/README.md index d4be4b885..afe4f7d25 100644 --- a/examples/vscode-angular/README.md +++ b/examples/vscode-angular/README.md @@ -1,21 +1,25 @@ -# Angular Language Server (Example) +# Volar language server example for Angular -> An example Angular Language Server implement via [Volar](https://github.com/johnsoncodehk/volar) framework. +⚠️⚠️⚠️⚠️⚠️ -⚠️ This is a quick implementation for demonstration purposes only, functionality reliability and correctness are not guaranteed, and there are no plan to maintenance this plugin. +Don't use it in the projects you work on. + +It's not a production-ready tool, this is a quick implementation for demonstration purposes only, functionality reliability and correctness are not guaranteed, and there are no plan to maintenance this plugin. + +⚠️⚠️⚠️⚠️⚠️ ## Features - Template AST error diagnosis - Directives, Interpolations Syntax Highlighting - Intellisense Support for Template Directives and Interpolations -- Typed Prop Support -- Typed Event Support +- Component props and event types Support - Type Narrowing Support for `*ngIf` if else block +- Only 500 KB and Fast ## Usage -We have only one performance-optimized available setup due to this is for demonstration purposes only. Please make sure you follow each step. +We have only one stubborn way to setup (that has the best performance) because this is for demonstration purposes only. Please make sure you follow each step. 1. Disable "TypeScript and JavaScript Language Features" to [takeover language support for .ts](https://vuejs.org/guide/typescript/overview.html#volar-takeover-mode) @@ -29,10 +33,12 @@ We have only one performance-optimized available setup due to this is for demons 2. Right click and select "Disable (Workspace)" -3. Disable "Vue Language Features (Volar)" to avoid it's takeover mode active +3. Create / Open `.vscode/settings.json` in workspace and put following setting to avoid "Vue Language Features (Volar)" auto active with takeover mode - 1. Search `Vue.volar` in Extensions sidebar - - 2. Right click and select "Disable (Workspace)" + ```json + { + "volar.takeOverMode.enabled": false + } + ``` 4. Reload VSCode diff --git a/examples/vscode-angular/client.js b/examples/vscode-angular/client.js new file mode 100644 index 000000000..a83481c85 --- /dev/null +++ b/examples/vscode-angular/client.js @@ -0,0 +1,3 @@ +let modulePath = './dist/node/client'; +try { modulePath = require.resolve('./out/client'); } catch { } +module.exports = require(modulePath); diff --git a/examples/vscode-angular/package.json b/examples/vscode-angular/package.json index 3fe604498..bc302a05e 100644 --- a/examples/vscode-angular/package.json +++ b/examples/vscode-angular/package.json @@ -1,14 +1,14 @@ { "private": true, - "name": "vscode-angular-example", + "name": "vscode-angular", "version": "1.0.13", "repository": { "type": "git", "url": "https://github.com/johnsoncodehk/volar.git", "directory": "examples/vscode-angular" }, - "displayName": "Angular (Volar Example)", - "description": "Angular (Volar Example)", + "displayName": "Angular (Volar)", + "description": "Angular (Volar)", "author": "johnsoncodehk", "publisher": "johnsoncodehk", "engines": { @@ -18,7 +18,7 @@ "onLanguage:html", "onLanguage:typescript" ], - "main": "out/client", + "main": "client", "contributes": { "languages": [ { @@ -79,16 +79,19 @@ } }, "scripts": { - "release": "vsce publish" + "prebuild": "cd ../.. && npm run build", + "build": "node scripts/build -- --minify", + "pack": "npm run build && vsce package --pre-release", + "release": "npm run build && vsce publish --pre-release" }, "devDependencies": { "@types/vscode": "1.67.0", - "vsce": "latest" - }, - "dependencies": { "@volar-examples/angular-language-server": "1.0.13", + "@volar/language-server": "1.0.13", "@volar/vscode-language-client": "1.0.13", + "esbuild": "latest", "typesafe-path": "^0.2.2", + "vsce": "latest", "vscode-languageclient": "^8.0.2" } } diff --git a/examples/vscode-angular/scripts/build.js b/examples/vscode-angular/scripts/build.js new file mode 100644 index 000000000..f37b53b00 --- /dev/null +++ b/examples/vscode-angular/scripts/build.js @@ -0,0 +1,31 @@ +require('esbuild').build({ + entryPoints: { + client: './out/client.js', + server: './node_modules/@volar-examples/angular-language-server/bin/angular-language-server.js', + }, + bundle: true, + metafile: process.argv.includes('--metafile'), + outdir: './dist/node', + external: [ + 'vscode', + ], + format: 'cjs', + platform: 'node', + tsconfig: '../../tsconfig.build.json', + define: { 'process.env.NODE_ENV': '"production"' }, + minify: process.argv.includes('--minify'), + watch: process.argv.includes('--watch'), + plugins: [ + { + name: 'umd2esm', + setup(build) { + build.onResolve({ filter: /^(vscode-.*|estree-walker|jsonc-parser)/ }, args => { + const pathUmdMay = require.resolve(args.path, { paths: [args.resolveDir] }) + // Call twice the replace is to solve the problem of the path in Windows + const pathEsm = pathUmdMay.replace('/umd/', '/esm/').replace('\\umd\\', '\\esm\\') + return { path: pathEsm } + }) + }, + }, + ], +}).catch(() => process.exit(1)) diff --git a/examples/vscode-angular/server.js b/examples/vscode-angular/server.js new file mode 100644 index 000000000..81afbfc21 --- /dev/null +++ b/examples/vscode-angular/server.js @@ -0,0 +1,3 @@ +let modulePath = './dist/node/server'; +try { modulePath = require.resolve('@volar/angular-language-server/bin/angular-language-server'); } catch { } +module.exports = require(modulePath); diff --git a/examples/vscode-angular/src/client.ts b/examples/vscode-angular/src/client.ts index cf503e61b..3430f6b04 100644 --- a/examples/vscode-angular/src/client.ts +++ b/examples/vscode-angular/src/client.ts @@ -1,4 +1,4 @@ -import { LanguageServerInitializationOptions } from '@volar/language-server'; +import type { LanguageServerInitializationOptions } from '@volar/language-server'; import * as path from 'typesafe-path'; import * as vscode from 'vscode'; import * as lsp from 'vscode-languageclient/node'; @@ -7,11 +7,29 @@ import { registerTsConfig, registerTsVersion, } from '@volar/vscode-language-client'; +import * as os from 'os'; +import * as fs from 'fs'; let client: lsp.BaseLanguageClient; export async function activate(context: vscode.ExtensionContext) { + const cancellationPipeName = path.join(os.tmpdir() as path.OsPath, `vscode-${context.extension.id}-cancellation-pipe.tmp` as path.PosixPath); + const isSupportDoc = (document: vscode.TextDocument) => documentSelector.some(selector => selector.language === document.languageId); + let cancellationPipeUpdateKey: string | undefined; + + vscode.workspace.onDidChangeTextDocument((e) => { + let key = e.document.uri.toString() + '|' + e.document.version; + if (cancellationPipeUpdateKey === undefined) { + cancellationPipeUpdateKey = key; + return; + } + if (isSupportDoc(e.document) && cancellationPipeUpdateKey !== key) { + cancellationPipeUpdateKey = key; + fs.writeFileSync(cancellationPipeName, ''); + } + }); + const documentSelector: lsp.DocumentFilter[] = [ { language: 'html' }, { language: 'typescript' }, @@ -23,8 +41,12 @@ export async function activate(context: vscode.ExtensionContext) { 'extensions/node_modules/typescript/lib' as path.PosixPath, ), }, + cancellationPipeName, + noProjectReferences: true, + // @ts-expect-error + __noPluginCommands: true, }; - const serverModule = vscode.Uri.joinPath(context.extensionUri, 'node_modules', '@volar-examples', 'angular-language-server', 'bin', 'angular-language-server.js'); + const serverModule = vscode.Uri.joinPath(context.extensionUri, 'server.js'); const runOptions = { execArgv: [] }; const debugOptions = { execArgv: ['--nolazy', '--inspect=' + 6009] }; const serverOptions: lsp.ServerOptions = { @@ -45,14 +67,12 @@ export async function activate(context: vscode.ExtensionContext) { }; client = new lsp.LanguageClient( 'volar-angular-language-server', - 'Angular Language Server (Volar)', + 'Angular (Volar Example)', serverOptions, clientOptions, ); await client.start(); - const isSupportDoc = (document: vscode.TextDocument) => documentSelector.some(selector => selector.language === document.languageId); - registerShowVirtualFiles('volar-angular.action.showVirtualFiles', context, client); registerTsConfig('volar-angular.action.showTsConfig', context, client, isSupportDoc); registerTsVersion('volar-angular.action.showTsVersion', context, client, isSupportDoc, text => text + ' (angular)'); diff --git a/examples/vscode-svelte/package.json b/examples/vscode-svelte/package.json index 7ce609fae..6bd3535ea 100644 --- a/examples/vscode-svelte/package.json +++ b/examples/vscode-svelte/package.json @@ -1,6 +1,6 @@ { "private": true, - "name": "vscode-svelte-example", + "name": "vscode-svelte", "version": "1.0.13", "repository": { "type": "git", diff --git a/packages/language-server/src/registerFeatures.ts b/packages/language-server/src/registerFeatures.ts index 3fbe888a4..e1cc1df4d 100644 --- a/packages/language-server/src/registerFeatures.ts +++ b/packages/language-server/src/registerFeatures.ts @@ -170,12 +170,11 @@ export function setupSemanticCapabilities( server.codeLensProvider = { resolveProvider: true, }; - server.executeCommandProvider = { - commands: [ - ...server.executeCommandProvider?.commands ?? [], - embedded.executePluginCommand, - ] - }; + server.executeCommandProvider = { commands: [...server.executeCommandProvider?.commands ?? []] }; + // @ts-expect-error + if (!initOptions.__noPluginCommands) { + server.executeCommandProvider.commands.push(embedded.executePluginCommand); + } } if (!initOptions.respectClientCapabilities || params.textDocument?.semanticTokens) { server.semanticTokensProvider = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c22315a7..f65ac48bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,7 +15,7 @@ importers: optionalDependencies: '@lerna-lite/cli': 1.13.0 '@vscode/test-web': 0.0.33 - vitepress: 1.0.0-alpha.31_@types+node@18.11.15 + vitepress: 1.0.0-alpha.32_@types+node@18.11.15 vue: 3.2.45 devDependencies: '@types/node': 18.11.15 @@ -27,7 +27,6 @@ importers: specifiers: '@angular-eslint/bundled-angular-compiler': ^15.1.0 '@angular/compiler': ^15.0.4 - '@typescript-eslint/types': ^5.46.1 '@volar/language-core': 1.0.13 vscode-languageserver-textdocument: ^1.0.7 vscode-uri: ^3.0.6 @@ -38,7 +37,6 @@ importers: vscode-uri: 3.0.6 devDependencies: '@angular/compiler': 15.0.4 - '@typescript-eslint/types': 5.46.1 examples/angular-language-server: specifiers: @@ -98,18 +96,21 @@ importers: specifiers: '@types/vscode': 1.67.0 '@volar-examples/angular-language-server': 1.0.13 + '@volar/language-server': 1.0.13 '@volar/vscode-language-client': 1.0.13 + esbuild: latest typesafe-path: ^0.2.2 vsce: latest vscode-languageclient: ^8.0.2 - dependencies: + devDependencies: + '@types/vscode': 1.67.0 '@volar-examples/angular-language-server': link:../angular-language-server + '@volar/language-server': link:../../packages/language-server '@volar/vscode-language-client': link:../../packages/vscode-language-client + esbuild: 0.16.7 typesafe-path: 0.2.2 - vscode-languageclient: 8.0.2 - devDependencies: - '@types/vscode': 1.67.0 vsce: 2.15.0 + vscode-languageclient: 8.0.2 examples/vscode-svelte: specifiers: @@ -1594,11 +1595,6 @@ packages: '@types/node': 18.8.0 dev: true - /@typescript-eslint/types/5.46.1: - resolution: {integrity: sha512-Z5pvlCaZgU+93ryiYUwGwLl9AQVB/PQ1TsJ9NZ/gHzZjN7g9IAn6RSDkpCV8hqTwAiaj6fmCcKSQeBPlIpW28w==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - /@vitejs/plugin-vue/4.0.0_vite@4.0.1+vue@3.2.45: resolution: {integrity: sha512-e0X4jErIxAB5oLtDqbHvHpJe/uWNkdpYV83AOG2xo2tEVSzCzewgJMtREZM30wXnM5ls90hxiOtAuVU6H5JgbA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -6286,8 +6282,8 @@ packages: optionalDependencies: fsevents: 2.3.2 - /vitepress/1.0.0-alpha.31_@types+node@18.11.15: - resolution: {integrity: sha512-FWFXLs7WLbFbemxjBWo2S2+qUZCIoeLLyAKfVUpIu3LUB8oQ8cyIANRGO6f6zsM51u2bvJU9Sm+V6Z0WjOWS2Q==} + /vitepress/1.0.0-alpha.32_@types+node@18.11.15: + resolution: {integrity: sha512-Q45N1cLdRr8MAu/+wAKKow7CNQD4sNBlSNsW1UxYfkvSgwPN/dlEmpEkQl/uOpE3C1kv3jvvEJVY0RAIaJFWYQ==} hasBin: true requiresBuild: true dependencies: