From 881e4b680f6644d9f5ee24580aa24ed169e9e72b Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Mon, 25 Nov 2024 17:14:06 +0800 Subject: [PATCH 01/25] Add a intellisense for the extends path in tspconfig.yaml --- .../src/completion-provider.ts | 71 +++++++++++++++++++ packages/typespec-vscode/src/extension.ts | 3 + 2 files changed, 74 insertions(+) create mode 100644 packages/typespec-vscode/src/completion-provider.ts diff --git a/packages/typespec-vscode/src/completion-provider.ts b/packages/typespec-vscode/src/completion-provider.ts new file mode 100644 index 0000000000..e9b9881e91 --- /dev/null +++ b/packages/typespec-vscode/src/completion-provider.ts @@ -0,0 +1,71 @@ +import * as fs from "fs"; +import * as path from "path"; +import vscode from "vscode"; + +export function createExtendsCompletionItemProvider() { + return vscode.languages.registerCompletionItemProvider( + "yaml", + { + provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) { + // get all text until the `position` and check if it reads `extends:.` + // and if so then complete if `log`, `warn`, and `error` + const linePrefix = document.lineAt(position).text.slice(0, position.character); + const result: vscode.CompletionItem[] = []; + + if (linePrefix.includes("extends:")) { + const curActiveFile = vscode.window.activeTextEditor?.document.fileName; + if (!curActiveFile) { + return result; + } + + if (vscode.workspace.workspaceFolders) { + const pos = curActiveFile.lastIndexOf("\\"); + const fileName = curActiveFile.slice(pos + 1, curActiveFile.length); + const yamlFiles = findFilesWithSameExtension( + vscode.workspace.workspaceFolders[0].uri.fsPath, + fileName, + ); + + yamlFiles.forEach((file) => { + result.push({ + label: file, + kind: vscode.CompletionItemKind.Value, + }); + }); + } + } + return result; + }, + }, + ":", + ); +} + +function findFilesWithSameExtension(rootPath: string, fileName: string): string[] { + const fileExtension = path.extname(fileName); + const result: string[] = []; + const exclude = [".vs", ".vscode", "tsp-output", "node_modules", ".gitignore", "main.tsp"]; + + function searchDirectory(currentDir: string) { + const files = fs.readdirSync(currentDir); + + for (const file of files) { + if (exclude.includes(file)) { + continue; + } + + const fullPath = path.join(currentDir, file); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + searchDirectory(fullPath); + } else if (file !== fileName && path.extname(file) === fileExtension) { + const newRelativePath = path.relative(rootPath, fullPath); + result.push(" ./" + newRelativePath.replace("\\", "/")); + } + } + } + + searchDirectory(rootPath); + return result; +} diff --git a/packages/typespec-vscode/src/extension.ts b/packages/typespec-vscode/src/extension.ts index 3647a203f6..33e7eb262b 100644 --- a/packages/typespec-vscode/src/extension.ts +++ b/packages/typespec-vscode/src/extension.ts @@ -1,4 +1,5 @@ import vscode, { commands, ExtensionContext } from "vscode"; +import { createExtendsCompletionItemProvider } from "./completion-provider.js"; import { SettingName } from "./const.js"; import { ExtensionLogListener } from "./log/extension-log-listener.js"; import logger from "./log/logger.js"; @@ -17,6 +18,8 @@ logger.registerLogListener("extension-log", new ExtensionLogListener(outputChann export async function activate(context: ExtensionContext) { context.subscriptions.push(createTaskProvider()); + context.subscriptions.push(createExtendsCompletionItemProvider()); + context.subscriptions.push( commands.registerCommand("typespec.showOutputChannel", () => { outputChannel.show(true /*preserveFocus*/); From dd4314b2a5b21e9135ca04fdbae380262fec9117 Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Mon, 25 Nov 2024 17:38:31 +0800 Subject: [PATCH 02/25] fix warning --- packages/typespec-vscode/src/completion-provider.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/typespec-vscode/src/completion-provider.ts b/packages/typespec-vscode/src/completion-provider.ts index e9b9881e91..a9c0dafd88 100644 --- a/packages/typespec-vscode/src/completion-provider.ts +++ b/packages/typespec-vscode/src/completion-provider.ts @@ -1,6 +1,7 @@ import * as fs from "fs"; import * as path from "path"; import vscode from "vscode"; +import { normalizeSlash } from "./utils.js"; export function createExtendsCompletionItemProvider() { return vscode.languages.registerCompletionItemProvider( @@ -61,7 +62,7 @@ function findFilesWithSameExtension(rootPath: string, fileName: string): string[ searchDirectory(fullPath); } else if (file !== fileName && path.extname(file) === fileExtension) { const newRelativePath = path.relative(rootPath, fullPath); - result.push(" ./" + newRelativePath.replace("\\", "/")); + result.push(" ./" + normalizeSlash(newRelativePath)); } } } From a43f7e98e76f348961333ebdeac3730c8c86b0fd Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Tue, 26 Nov 2024 16:06:53 +0800 Subject: [PATCH 03/25] Some autocompletes in linters, extends autocomplete updates --- .../compiler/src/server/tspconfig/completion.ts | 16 ++++++++++++++++ .../typespec-vscode/src/completion-provider.ts | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/server/tspconfig/completion.ts b/packages/compiler/src/server/tspconfig/completion.ts index 691fa605f8..8470103a11 100644 --- a/packages/compiler/src/server/tspconfig/completion.ts +++ b/packages/compiler/src/server/tspconfig/completion.ts @@ -80,6 +80,22 @@ export async function provideTspconfigCompletionItems( itemsFromEmitter.push(...more); } return [...itemsFromBuiltIn, ...itemsFromEmitter]; + } + if ( + nodePath.length === CONFIG_PATH_LENGTH_FOR_EMITTER_LIST && + nodePath[0] === "linter" && + targetType === "key" + ) { + const items: CompletionItem[] = []; + const schema = TypeSpecConfigJsonSchema; + + const more = resolveCompleteItems(schema.properties?.linter, { + ...target, + path: nodePath.slice(CONFIG_PATH_LENGTH_FOR_EMITTER_LIST), + }); + items.push(...more); + + return items; } else { const schema = TypeSpecConfigJsonSchema; return schema ? resolveCompleteItems(schema, target) : []; diff --git a/packages/typespec-vscode/src/completion-provider.ts b/packages/typespec-vscode/src/completion-provider.ts index a9c0dafd88..5be1fe0637 100644 --- a/packages/typespec-vscode/src/completion-provider.ts +++ b/packages/typespec-vscode/src/completion-provider.ts @@ -13,7 +13,7 @@ export function createExtendsCompletionItemProvider() { const linePrefix = document.lineAt(position).text.slice(0, position.character); const result: vscode.CompletionItem[] = []; - if (linePrefix.includes("extends:")) { + if (linePrefix.startsWith("extends:")) { const curActiveFile = vscode.window.activeTextEditor?.document.fileName; if (!curActiveFile) { return result; From 47e65f8876879fe6b155f6d770c51f5423b55a88 Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Wed, 27 Nov 2024 10:32:00 +0800 Subject: [PATCH 04/25] fix emitter option auto complete while inside "" will add extra "" --- packages/compiler/src/server/tspconfig/completion.ts | 3 ++- packages/compiler/src/server/yaml-resolver.ts | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/server/tspconfig/completion.ts b/packages/compiler/src/server/tspconfig/completion.ts index 8470103a11..5e54a63ff9 100644 --- a/packages/compiler/src/server/tspconfig/completion.ts +++ b/packages/compiler/src/server/tspconfig/completion.ts @@ -44,11 +44,12 @@ export async function provideTspconfigCompletionItems( const items: CompletionItem[] = []; for (const [name, pkg] of Object.entries(emitters)) { if (!siblings.includes(name)) { + // If there are already double quotes, no double quotes will be inserted. const item: CompletionItem = { label: name, kind: CompletionItemKind.Field, documentation: (await pkg.getPackageJsonData())?.description ?? `Emitter from ${name}`, - insertText: `"${name}"`, + insertText: nodePath[1] === '""' ? `${name}` : `"${name}"`, }; items.push(item); } diff --git a/packages/compiler/src/server/yaml-resolver.ts b/packages/compiler/src/server/yaml-resolver.ts index 07877ee792..59dfc4604d 100644 --- a/packages/compiler/src/server/yaml-resolver.ts +++ b/packages/compiler/src/server/yaml-resolver.ts @@ -250,7 +250,13 @@ function createYamlPathFromVisitScalarNode( if (key === "value" && newline && newline.offset < offset) { // if the scalar node is marked as value but separated by newline from the key, it's more likely that the user is inputting the first property of an object // so build the target as an object key - path.push(n.source ?? ""); + // If the value of type is QUOTE_DOUBLE, you need to include "" + if (n.source !== undefined && n.source.length === 0 && n.type === "QUOTE_DOUBLE") { + path.push('""'); + } else { + path.push(n.source ?? ""); + } + return { path, type: "key", From 18025ccfaf1eda4f358b843d82adf2e00dae9a75 Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Thu, 28 Nov 2024 14:04:34 +0800 Subject: [PATCH 05/25] fix emitter option auto complete debug; revert vscode side code --- .../src/server/tspconfig/completion.ts | 55 ++++++++------ packages/compiler/src/server/yaml-resolver.ts | 19 +++-- .../src/Microsoft.TypeSpec.VS.csproj | 36 +++++----- .../src/completion-provider.ts | 72 ------------------- packages/typespec-vscode/src/extension.ts | 3 - 5 files changed, 63 insertions(+), 122 deletions(-) delete mode 100644 packages/typespec-vscode/src/completion-provider.ts diff --git a/packages/compiler/src/server/tspconfig/completion.ts b/packages/compiler/src/server/tspconfig/completion.ts index 5e54a63ff9..a990d52717 100644 --- a/packages/compiler/src/server/tspconfig/completion.ts +++ b/packages/compiler/src/server/tspconfig/completion.ts @@ -1,5 +1,11 @@ import { TextDocument } from "vscode-languageserver-textdocument"; -import { CompletionItem, CompletionItemKind, Position } from "vscode-languageserver/node.js"; +import { + CompletionItem, + CompletionItemKind, + Position, + Range, + TextEdit, +} from "vscode-languageserver/node.js"; import { emitterOptionsSchema, TypeSpecConfigJsonSchema } from "../../config/config-schema.js"; import { JSONSchemaType, ServerLog } from "../../index.js"; import { distinctArray } from "../../utils/misc.js"; @@ -23,14 +29,19 @@ export async function provideTspconfigCompletionItems( if (target === undefined) { return []; } - const items = resolveTspConfigCompleteItems(await fileService.getPath(tspConfigDoc), target); + const items = resolveTspConfigCompleteItems( + await fileService.getPath(tspConfigDoc), + target, + tspConfigPosition, + ); return items; async function resolveTspConfigCompleteItems( tspConfigFile: string, target: YamlScalarTarget, + tspConfigPosition: Position, ): Promise { - const { path: nodePath, type: targetType, siblings } = target; + const { path: nodePath, type: targetType, siblings, sourceQuotation, source } = target; const CONFIG_PATH_LENGTH_FOR_EMITTER_LIST = 2; if ( (nodePath.length === CONFIG_PATH_LENGTH_FOR_EMITTER_LIST && @@ -44,12 +55,30 @@ export async function provideTspconfigCompletionItems( const items: CompletionItem[] = []; for (const [name, pkg] of Object.entries(emitters)) { if (!siblings.includes(name)) { - // If there are already double quotes, no double quotes will be inserted. + // Generate new text + let newText: string = ""; + if (sourceQuotation === "QUOTE_SINGLE") { + newText = `${name}'`; + } else if (sourceQuotation === "QUOTE_DOUBLE") { + newText = `${name}"`; + } else { + newText = `"${name}"`; + } + + // The position of the new text + const edit = TextEdit.replace( + Range.create( + Position.create(tspConfigPosition.line, tspConfigPosition.character - source.length), + Position.create(tspConfigPosition.line, tspConfigPosition.character + newText.length), + ), + newText, + ); + const item: CompletionItem = { label: name, kind: CompletionItemKind.Field, documentation: (await pkg.getPackageJsonData())?.description ?? `Emitter from ${name}`, - insertText: nodePath[1] === '""' ? `${name}` : `"${name}"`, + textEdit: edit, }; items.push(item); } @@ -81,22 +110,6 @@ export async function provideTspconfigCompletionItems( itemsFromEmitter.push(...more); } return [...itemsFromBuiltIn, ...itemsFromEmitter]; - } - if ( - nodePath.length === CONFIG_PATH_LENGTH_FOR_EMITTER_LIST && - nodePath[0] === "linter" && - targetType === "key" - ) { - const items: CompletionItem[] = []; - const schema = TypeSpecConfigJsonSchema; - - const more = resolveCompleteItems(schema.properties?.linter, { - ...target, - path: nodePath.slice(CONFIG_PATH_LENGTH_FOR_EMITTER_LIST), - }); - items.push(...more); - - return items; } else { const schema = TypeSpecConfigJsonSchema; return schema ? resolveCompleteItems(schema, target) : []; diff --git a/packages/compiler/src/server/yaml-resolver.ts b/packages/compiler/src/server/yaml-resolver.ts index 59dfc4604d..93930f0526 100644 --- a/packages/compiler/src/server/yaml-resolver.ts +++ b/packages/compiler/src/server/yaml-resolver.ts @@ -35,6 +35,11 @@ export interface YamlScalarTarget { * The siblings of the target node */ siblings: string[]; + + /** + * The input quotes (double quotes or single quotes) + */ + sourceQuotation: string; } interface YamlVisitScalarNode { @@ -86,6 +91,7 @@ export function resolveYamlScalarTarget( type: "key", source: "", siblings: rootProperties, + sourceQuotation: "", }; } for (let i = position.line - 1; i >= 0; i--) { @@ -132,6 +138,7 @@ export function resolveYamlScalarTarget( type: "key", source: "", siblings: [...yp.siblings, yp.source], + sourceQuotation: "", }; } break; @@ -175,6 +182,7 @@ export function resolveYamlScalarTarget( siblings: isMap(last.value) ? (last.value?.items.map((item) => (item.key as any).source ?? "") ?? []) : [], + sourceQuotation: "", }; } break; @@ -237,6 +245,7 @@ function createYamlPathFromVisitScalarNode( type: key === null ? "key" : "value", source: n.source ?? "", siblings: [], + sourceQuotation: n.type ?? "", }; } else if (isPair(last)) { if (nodePath.length < 2) { @@ -250,18 +259,14 @@ function createYamlPathFromVisitScalarNode( if (key === "value" && newline && newline.offset < offset) { // if the scalar node is marked as value but separated by newline from the key, it's more likely that the user is inputting the first property of an object // so build the target as an object key - // If the value of type is QUOTE_DOUBLE, you need to include "" - if (n.source !== undefined && n.source.length === 0 && n.type === "QUOTE_DOUBLE") { - path.push('""'); - } else { - path.push(n.source ?? ""); - } + path.push(n.source ?? ""); return { path, type: "key", source: n.source ?? "", siblings: [], + sourceQuotation: n.type ?? "", }; } else { const parent = nodePath[nodePath.length - 2]; @@ -273,6 +278,7 @@ function createYamlPathFromVisitScalarNode( type: key === "key" ? "key" : "value", source: n.source ?? "", siblings: targetSiblings, + sourceQuotation: n.type ?? "", }; } } else if (isSeq(last)) { @@ -283,6 +289,7 @@ function createYamlPathFromVisitScalarNode( siblings: last.items .filter((i) => i !== n) .map((item) => (isScalar(item) ? (item.source ?? "") : "")), + sourceQuotation: n.type ?? "", }; } else { log({ diff --git a/packages/typespec-vs/src/Microsoft.TypeSpec.VS.csproj b/packages/typespec-vs/src/Microsoft.TypeSpec.VS.csproj index 5d2bb081a1..1138ca3670 100644 --- a/packages/typespec-vs/src/Microsoft.TypeSpec.VS.csproj +++ b/packages/typespec-vs/src/Microsoft.TypeSpec.VS.csproj @@ -17,15 +17,19 @@ $(MSBuildThisFileDirectory)..\$(AssemblyName).vsix + + NU1902;NU1903 + + + NU1902;NU1903 + - + - + @@ -42,10 +46,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -56,26 +58,20 @@ - + - - + + - - + + - + diff --git a/packages/typespec-vscode/src/completion-provider.ts b/packages/typespec-vscode/src/completion-provider.ts deleted file mode 100644 index 5be1fe0637..0000000000 --- a/packages/typespec-vscode/src/completion-provider.ts +++ /dev/null @@ -1,72 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import vscode from "vscode"; -import { normalizeSlash } from "./utils.js"; - -export function createExtendsCompletionItemProvider() { - return vscode.languages.registerCompletionItemProvider( - "yaml", - { - provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) { - // get all text until the `position` and check if it reads `extends:.` - // and if so then complete if `log`, `warn`, and `error` - const linePrefix = document.lineAt(position).text.slice(0, position.character); - const result: vscode.CompletionItem[] = []; - - if (linePrefix.startsWith("extends:")) { - const curActiveFile = vscode.window.activeTextEditor?.document.fileName; - if (!curActiveFile) { - return result; - } - - if (vscode.workspace.workspaceFolders) { - const pos = curActiveFile.lastIndexOf("\\"); - const fileName = curActiveFile.slice(pos + 1, curActiveFile.length); - const yamlFiles = findFilesWithSameExtension( - vscode.workspace.workspaceFolders[0].uri.fsPath, - fileName, - ); - - yamlFiles.forEach((file) => { - result.push({ - label: file, - kind: vscode.CompletionItemKind.Value, - }); - }); - } - } - return result; - }, - }, - ":", - ); -} - -function findFilesWithSameExtension(rootPath: string, fileName: string): string[] { - const fileExtension = path.extname(fileName); - const result: string[] = []; - const exclude = [".vs", ".vscode", "tsp-output", "node_modules", ".gitignore", "main.tsp"]; - - function searchDirectory(currentDir: string) { - const files = fs.readdirSync(currentDir); - - for (const file of files) { - if (exclude.includes(file)) { - continue; - } - - const fullPath = path.join(currentDir, file); - const stat = fs.statSync(fullPath); - - if (stat.isDirectory()) { - searchDirectory(fullPath); - } else if (file !== fileName && path.extname(file) === fileExtension) { - const newRelativePath = path.relative(rootPath, fullPath); - result.push(" ./" + normalizeSlash(newRelativePath)); - } - } - } - - searchDirectory(rootPath); - return result; -} diff --git a/packages/typespec-vscode/src/extension.ts b/packages/typespec-vscode/src/extension.ts index 33e7eb262b..3647a203f6 100644 --- a/packages/typespec-vscode/src/extension.ts +++ b/packages/typespec-vscode/src/extension.ts @@ -1,5 +1,4 @@ import vscode, { commands, ExtensionContext } from "vscode"; -import { createExtendsCompletionItemProvider } from "./completion-provider.js"; import { SettingName } from "./const.js"; import { ExtensionLogListener } from "./log/extension-log-listener.js"; import logger from "./log/logger.js"; @@ -18,8 +17,6 @@ logger.registerLogListener("extension-log", new ExtensionLogListener(outputChann export async function activate(context: ExtensionContext) { context.subscriptions.push(createTaskProvider()); - context.subscriptions.push(createExtendsCompletionItemProvider()); - context.subscriptions.push( commands.registerCommand("typespec.showOutputChannel", () => { outputChannel.show(true /*preserveFocus*/); From 51084bc253222203605422bdb17fbd28d6f2c010 Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Mon, 2 Dec 2024 13:21:55 +0800 Subject: [PATCH 06/25] linter complete rule --- .../{emitter-provider.ts => lib-provider.ts} | 55 ++++-- packages/compiler/src/server/serverlib.ts | 6 +- .../src/server/tspconfig/completion.ts | 175 +++++++++++++++--- packages/compiler/src/server/yaml-resolver.ts | 44 ++++- .../src/Microsoft.TypeSpec.VS.csproj | 8 +- 5 files changed, 229 insertions(+), 59 deletions(-) rename packages/compiler/src/server/{emitter-provider.ts => lib-provider.ts} (56%) diff --git a/packages/compiler/src/server/emitter-provider.ts b/packages/compiler/src/server/lib-provider.ts similarity index 56% rename from packages/compiler/src/server/emitter-provider.ts rename to packages/compiler/src/server/lib-provider.ts index 59d54e05da..92387419a9 100644 --- a/packages/compiler/src/server/emitter-provider.ts +++ b/packages/compiler/src/server/lib-provider.ts @@ -1,16 +1,26 @@ import { joinPaths } from "../core/path-utils.js"; import { NpmPackage, NpmPackageProvider } from "./npm-package-provider.js"; -export class EmitterProvider { +export class LibraryProvider { private isEmitterPackageCache = new Map(); + private isLinterPackageCache = new Map(); + private isGetEmitter: boolean = false; constructor(private npmPackageProvider: NpmPackageProvider) {} + /** + * Set whether to get the emitter library or the linter library + * @param isGetEmitter true if you want to get the emitter library, false if you want to get the linter library + */ + setIsGetEmitterVal(isGetEmitter: boolean): void { + this.isGetEmitter = isGetEmitter; + } + /** * - * @param startFolder folder starts to search for package.json with emitters defined as dependencies + * @param startFolder folder starts to search for package.json with emitters/linters defined as dependencies * @returns */ - async listEmitters(startFolder: string): Promise> { + async listLibraries(startFolder: string): Promise> { const packageJsonFolder = await this.npmPackageProvider.getPackageJsonFolder(startFolder); if (!packageJsonFolder) return {}; @@ -18,39 +28,43 @@ export class EmitterProvider { const data = await pkg?.getPackageJsonData(); if (!data) return {}; - const emitters: Record = {}; + const libs: Record = {}; const allDep = { ...(data.dependencies ?? {}), ...(data.devDependencies ?? {}), }; for (const dep of Object.keys(allDep)) { - const depPkg = await this.getEmitterFromDep(packageJsonFolder, dep); + const depPkg = await this.getLibraryFromDep(packageJsonFolder, dep); if (depPkg) { - emitters[dep] = depPkg; + libs[dep] = depPkg; } } - return emitters; + return libs; } /** * - * @param startFolder folder starts to search for package.json with emitters defined as dependencies + * @param startFolder folder starts to search for package.json with emitters/linter defined as dependencies * @param emitterName * @returns */ - async getEmitter(startFolder: string, emitterName: string): Promise { + async getLibrary(startFolder: string, emitterName: string): Promise { const packageJsonFolder = await this.npmPackageProvider.getPackageJsonFolder(startFolder); if (!packageJsonFolder) { return undefined; } - return this.getEmitterFromDep(packageJsonFolder, emitterName); + return this.getLibraryFromDep(packageJsonFolder, emitterName); } private async isEmitter(depName: string, pkg: NpmPackage) { - if (this.isEmitterPackageCache.has(depName)) { + if (this.isGetEmitter && this.isEmitterPackageCache.has(depName)) { return this.isEmitterPackageCache.get(depName); } + if (!this.isGetEmitter && this.isLinterPackageCache.has(depName)) { + return this.isLinterPackageCache.get(depName); + } + const data = await pkg.getPackageJsonData(); // don't add to cache when failing to load package.json which is unexpected if (!data) return false; @@ -61,16 +75,27 @@ export class EmitterProvider { const exports = await pkg.getModuleExports(); // don't add to cache when failing to load exports which is unexpected if (!exports) return false; - const isEmitter = exports.$onEmit !== undefined; - this.isEmitterPackageCache.set(depName, isEmitter); + const isEmitter = this.isGetEmitter + ? exports.$onEmit !== undefined + : exports.$linter !== undefined; + if (this.isGetEmitter) { + this.isEmitterPackageCache.set(depName, isEmitter); + } else { + this.isLinterPackageCache.set(depName, isEmitter); + } return isEmitter; } else { - this.isEmitterPackageCache.set(depName, false); + if (this.isGetEmitter) { + this.isEmitterPackageCache.set(depName, false); + } else { + this.isLinterPackageCache.set(depName, false); + } + return false; } } - private async getEmitterFromDep(packageJsonFolder: string, depName: string) { + private async getLibraryFromDep(packageJsonFolder: string, depName: string) { const depFolder = joinPaths(packageJsonFolder, "node_modules", depName); const depPkg = await this.npmPackageProvider.get(depFolder); if (depPkg && (await this.isEmitter(depName, depPkg))) { diff --git a/packages/compiler/src/server/serverlib.ts b/packages/compiler/src/server/serverlib.ts index 0e883c04b7..80cba55526 100644 --- a/packages/compiler/src/server/serverlib.ts +++ b/packages/compiler/src/server/serverlib.ts @@ -80,9 +80,9 @@ import { createCompileService } from "./compile-service.js"; import { resolveCompletion } from "./completion.js"; import { Commands } from "./constants.js"; import { convertDiagnosticToLsp } from "./diagnostics.js"; -import { EmitterProvider } from "./emitter-provider.js"; import { createFileService } from "./file-service.js"; import { createFileSystemCache } from "./file-system-cache.js"; +import { LibraryProvider } from "./lib-provider.js"; import { NpmPackageProvider } from "./npm-package-provider.js"; import { getSymbolStructure } from "./symbol-structure.js"; import { provideTspconfigCompletionItems } from "./tspconfig/completion.js"; @@ -114,7 +114,7 @@ export function createServer(host: ServerHost): Server { }); const compilerHost = createCompilerHost(); const npmPackageProvider = new NpmPackageProvider(compilerHost); - const emitterProvider = new EmitterProvider(npmPackageProvider); + const libProvider = new LibraryProvider(npmPackageProvider); const compileService = createCompileService({ fileService, @@ -703,7 +703,7 @@ export function createServer(host: ServerHost): Server { if (doc) { const items = await provideTspconfigCompletionItems(doc, params.position, { fileService, - emitterProvider, + libProvider: libProvider, log, }); return CompletionList.create(items); diff --git a/packages/compiler/src/server/tspconfig/completion.ts b/packages/compiler/src/server/tspconfig/completion.ts index a990d52717..ba3b6bfa96 100644 --- a/packages/compiler/src/server/tspconfig/completion.ts +++ b/packages/compiler/src/server/tspconfig/completion.ts @@ -9,8 +9,8 @@ import { import { emitterOptionsSchema, TypeSpecConfigJsonSchema } from "../../config/config-schema.js"; import { JSONSchemaType, ServerLog } from "../../index.js"; import { distinctArray } from "../../utils/misc.js"; -import { EmitterProvider } from "../emitter-provider.js"; import { FileService } from "../file-service.js"; +import { LibraryProvider } from "../lib-provider.js"; import { resolveYamlScalarTarget, YamlScalarTarget } from "../yaml-resolver.js"; type ObjectJSONSchemaType = JSONSchemaType; @@ -20,11 +20,11 @@ export async function provideTspconfigCompletionItems( tspConfigPosition: Position, context: { fileService: FileService; - emitterProvider: EmitterProvider; + libProvider: LibraryProvider; log: (log: ServerLog) => void; }, ): Promise { - const { fileService, emitterProvider, log } = context; + const { fileService, libProvider, log } = context; const target = resolveYamlScalarTarget(tspConfigDoc, tspConfigPosition, log); if (target === undefined) { return []; @@ -41,7 +41,14 @@ export async function provideTspconfigCompletionItems( target: YamlScalarTarget, tspConfigPosition: Position, ): Promise { - const { path: nodePath, type: targetType, siblings, sourceQuotation, source } = target; + const { + path: nodePath, + type: targetType, + siblings, + sourceQuoteType, + source, + siblingsChildren, + } = target; const CONFIG_PATH_LENGTH_FOR_EMITTER_LIST = 2; if ( (nodePath.length === CONFIG_PATH_LENGTH_FOR_EMITTER_LIST && @@ -51,34 +58,19 @@ export async function provideTspconfigCompletionItems( nodePath[0] === "emit" && targetType === "arr-item") ) { - const emitters = await emitterProvider.listEmitters(tspConfigFile); + libProvider.setIsGetEmitterVal(true); + const libs = await libProvider.listLibraries(tspConfigFile); const items: CompletionItem[] = []; - for (const [name, pkg] of Object.entries(emitters)) { + for (const [name, pkg] of Object.entries(libs)) { if (!siblings.includes(name)) { // Generate new text - let newText: string = ""; - if (sourceQuotation === "QUOTE_SINGLE") { - newText = `${name}'`; - } else if (sourceQuotation === "QUOTE_DOUBLE") { - newText = `${name}"`; - } else { - newText = `"${name}"`; - } - - // The position of the new text - const edit = TextEdit.replace( - Range.create( - Position.create(tspConfigPosition.line, tspConfigPosition.character - source.length), - Position.create(tspConfigPosition.line, tspConfigPosition.character + newText.length), - ), - newText, - ); + const newText = getNewTextValue(sourceQuoteType, name); const item: CompletionItem = { label: name, kind: CompletionItemKind.Field, documentation: (await pkg.getPackageJsonData())?.description ?? `Emitter from ${name}`, - textEdit: edit, + textEdit: getNewTextAndPosition(newText, source, tspConfigPosition), }; items.push(item); } @@ -86,7 +78,8 @@ export async function provideTspconfigCompletionItems( return items; } else if (nodePath.length > CONFIG_PATH_LENGTH_FOR_EMITTER_LIST && nodePath[0] === "options") { const emitterName = nodePath[CONFIG_PATH_LENGTH_FOR_EMITTER_LIST - 1]; - const emitter = await emitterProvider.getEmitter(tspConfigFile, emitterName); + libProvider.setIsGetEmitterVal(true); + const emitter = await libProvider.getLibrary(tspConfigFile, emitterName); if (!emitter) { return []; } @@ -110,6 +103,79 @@ export async function provideTspconfigCompletionItems( itemsFromEmitter.push(...more); } return [...itemsFromBuiltIn, ...itemsFromEmitter]; + } else if (nodePath.length > CONFIG_PATH_LENGTH_FOR_EMITTER_LIST && nodePath[0] === "linter") { + const linterName = nodePath[CONFIG_PATH_LENGTH_FOR_EMITTER_LIST - 1]; + libProvider.setIsGetEmitterVal(false); + if (linterName === "extends") { + const linters = await libProvider.listLibraries(tspConfigFile); + const items: CompletionItem[] = []; + for (const [name, pkg] of Object.entries(linters)) { + if (!siblings.includes(name)) { + // If a ruleSet exists for the linter, add it to the end of the library name. + const exports = await pkg.getModuleExports(); + let additionalContent: string = ""; + if (exports?.$linter?.ruleSets !== undefined) { + additionalContent = Object.keys(exports?.$linter?.ruleSets)[0]; + } + + const labelName = + additionalContent.length === 0 ? name : `${name}/${additionalContent}`; + const newText = getNewTextValue(sourceQuoteType, labelName); + + const item: CompletionItem = { + label: labelName, + kind: CompletionItemKind.Field, + documentation: + (await pkg.getPackageJsonData())?.description ?? `Linters from ${name}`, + textEdit: getNewTextAndPosition(newText, source, tspConfigPosition), + }; + items.push(item); + } + } + return items; + } else { + const itemsFromLintter = []; + const libNames: string[] = []; + if (siblingsChildren && siblingsChildren.length > 0) { + // Filter duplicate library names + for (const extendsValue of siblingsChildren) { + const arrLine = extendsValue.split("/"); + let libName: string = ""; + if (arrLine.length >= 2) { + libName = arrLine[0] + "/" + arrLine[1]; + } else { + continue; + } + if (!libNames.includes(libName)) { + libNames.push(libName); + } + } + + // Get rules in each library + for (const name of libNames) { + const linter = await libProvider.getLibrary(tspConfigFile, name); + if (!linter) { + return []; + } + + const exports = await linter.getModuleExports(); + + if (exports?.$linter?.rules !== undefined) { + const more = resolveCompleteItems( + exports?.$linter?.rules, + { + ...target, + path: nodePath.slice(CONFIG_PATH_LENGTH_FOR_EMITTER_LIST), + }, + name, + ); + itemsFromLintter.push(...more); + } + } + } + + return [...itemsFromLintter]; + } } else { const schema = TypeSpecConfigJsonSchema; return schema ? resolveCompleteItems(schema, target) : []; @@ -184,8 +250,9 @@ export async function provideTspconfigCompletionItems( function resolveCompleteItems( schema: ObjectJSONSchemaType, target: YamlScalarTarget, + libName?: string, ): CompletionItem[] { - const { path: nodePath, type: targetType } = target; + const { path: nodePath, type: targetType, source, sourceQuoteType } = target; // if the target is a key which means it's pointing to an object property, we should remove the last element of the path to get it's parent object for its schema const path = targetType === "key" ? nodePath.slice(0, -1) : nodePath; const foundSchemas = findSchemaByPath(schema, path, 0); @@ -209,6 +276,23 @@ export async function provideTspconfigCompletionItems( return item; }); result.push(...props); + } else if (cur.type === undefined) { + // lint rule + for (const key of Object.keys(cur ?? {})) { + const labelName = `${libName}/${cur[key].name}`; + if (target.siblingsChildren.includes(labelName)) { + continue; + } + + const newText = getNewTextValue(sourceQuoteType, labelName); + const item: CompletionItem = { + label: labelName, + kind: CompletionItemKind.Field, + documentation: cur[key].description, + textEdit: getNewTextAndPosition(newText, source, tspConfigPosition), + }; + result.push(item); + } } } if (targetType === "value" || targetType === "arr-item") { @@ -241,3 +325,42 @@ export async function provideTspconfigCompletionItems( return distinctArray(result, (t) => t.label); } } + +/** + * Get the new text value + * @param sourceQuoteType input quote type(single, double, none) + * @param formatText format text value + * @returns + */ +function getNewTextValue(sourceQuoteType: string, formatText: string): string { + let newText: string = ""; + if (sourceQuoteType === "QUOTE_SINGLE") { + newText = `${formatText}'`; + } else if (sourceQuoteType === "QUOTE_DOUBLE") { + newText = `${formatText}"`; + } else { + newText = `"${formatText}"`; + } + return newText; +} + +/** + * Get the position of the new text + * @param newText the new text + * @param source source text + * @param tspConfigPosition original position + * @returns + */ +function getNewTextAndPosition( + newText: string, + source: string, + tspConfigPosition: Position, +): TextEdit { + return TextEdit.replace( + Range.create( + Position.create(tspConfigPosition.line, tspConfigPosition.character - source.length), + Position.create(tspConfigPosition.line, tspConfigPosition.character + newText.length), + ), + newText, + ); +} diff --git a/packages/compiler/src/server/yaml-resolver.ts b/packages/compiler/src/server/yaml-resolver.ts index 93930f0526..590da3079f 100644 --- a/packages/compiler/src/server/yaml-resolver.ts +++ b/packages/compiler/src/server/yaml-resolver.ts @@ -36,10 +36,15 @@ export interface YamlScalarTarget { */ siblings: string[]; + /** + * The children of the siblings of the target node + */ + siblingsChildren: string[]; + /** * The input quotes (double quotes or single quotes) */ - sourceQuotation: string; + sourceQuoteType: string; } interface YamlVisitScalarNode { @@ -90,8 +95,9 @@ export function resolveYamlScalarTarget( path: [""], type: "key", source: "", + sourceQuoteType: "", siblings: rootProperties, - sourceQuotation: "", + siblingsChildren: [], }; } for (let i = position.line - 1; i >= 0; i--) { @@ -137,8 +143,9 @@ export function resolveYamlScalarTarget( path: [...yp.path.slice(0, yp.path.length - 1), ""], type: "key", source: "", + sourceQuoteType: "", siblings: [...yp.siblings, yp.source], - sourceQuotation: "", + siblingsChildren: yp.siblingsChildren, }; } break; @@ -179,10 +186,11 @@ export function resolveYamlScalarTarget( path: [...yp.path, ""], type: "key", source: "", + sourceQuoteType: "", siblings: isMap(last.value) ? (last.value?.items.map((item) => (item.key as any).source ?? "") ?? []) : [], - sourceQuotation: "", + siblingsChildren: yp.siblingsChildren, }; } break; @@ -244,8 +252,9 @@ function createYamlPathFromVisitScalarNode( path: [], type: key === null ? "key" : "value", source: n.source ?? "", + sourceQuoteType: n.type ?? "", siblings: [], - sourceQuotation: n.type ?? "", + siblingsChildren: [], }; } else if (isPair(last)) { if (nodePath.length < 2) { @@ -265,20 +274,38 @@ function createYamlPathFromVisitScalarNode( path, type: "key", source: n.source ?? "", + sourceQuoteType: n.type ?? "", siblings: [], - sourceQuotation: n.type ?? "", + siblingsChildren: [], }; } else { const parent = nodePath[nodePath.length - 2]; const targetSiblings = isMap(parent) ? parent.items.filter((item) => item !== last).map((item) => (item.key as any).source ?? "") : []; + + // This function mainly provides support for linter + const targetSiblingChildren: string[] = []; + if (isMap(parent)) { + for (const p of parent.items) { + if (p !== last && isPair(p)) { + (p.value as any)?.items.forEach((i: any) => { + if (i.key !== undefined) { + targetSiblingChildren.push((i.key as any).source ?? ""); + } else { + targetSiblingChildren.push((i as any).source ?? ""); + } + }); + } + } + } return { path: path, type: key === "key" ? "key" : "value", source: n.source ?? "", siblings: targetSiblings, - sourceQuotation: n.type ?? "", + siblingsChildren: targetSiblingChildren, + sourceQuoteType: n.type ?? "", }; } } else if (isSeq(last)) { @@ -286,10 +313,11 @@ function createYamlPathFromVisitScalarNode( path: path, type: "arr-item", source: n.source ?? "", + sourceQuoteType: n.type ?? "", siblings: last.items .filter((i) => i !== n) .map((item) => (isScalar(item) ? (item.source ?? "") : "")), - sourceQuotation: n.type ?? "", + siblingsChildren: [], }; } else { log({ diff --git a/packages/typespec-vs/src/Microsoft.TypeSpec.VS.csproj b/packages/typespec-vs/src/Microsoft.TypeSpec.VS.csproj index 1138ca3670..c7847b41c6 100644 --- a/packages/typespec-vs/src/Microsoft.TypeSpec.VS.csproj +++ b/packages/typespec-vs/src/Microsoft.TypeSpec.VS.csproj @@ -16,13 +16,7 @@ $(MSBuildThisFileDirectory)..\$(AssemblyName).vsix - - - NU1902;NU1903 - - - NU1902;NU1903 - + From 7344ef2661a1b84c6f195c3b743265352add4dc2 Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Mon, 2 Dec 2024 16:43:39 +0800 Subject: [PATCH 07/25] extends autocomplete --- .../src/server/tspconfig/completion.ts | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/packages/compiler/src/server/tspconfig/completion.ts b/packages/compiler/src/server/tspconfig/completion.ts index ba3b6bfa96..049f6a70c2 100644 --- a/packages/compiler/src/server/tspconfig/completion.ts +++ b/packages/compiler/src/server/tspconfig/completion.ts @@ -1,3 +1,4 @@ +import * as fs from "fs"; import { TextDocument } from "vscode-languageserver-textdocument"; import { CompletionItem, @@ -7,6 +8,13 @@ import { TextEdit, } from "vscode-languageserver/node.js"; import { emitterOptionsSchema, TypeSpecConfigJsonSchema } from "../../config/config-schema.js"; +import { + getAnyExtensionFromPath, + getBaseFileName, + getDirectoryPath, + getRelativePathFromDirectory, + joinPaths, +} from "../../core/path-utils.js"; import { JSONSchemaType, ServerLog } from "../../index.js"; import { distinctArray } from "../../utils/misc.js"; import { FileService } from "../file-service.js"; @@ -176,6 +184,21 @@ export async function provideTspconfigCompletionItems( return [...itemsFromLintter]; } + } else if (nodePath.length < CONFIG_PATH_LENGTH_FOR_EMITTER_LIST && nodePath[0] === "extends") { + const currentFolder = getDirectoryPath(tspConfigFile); + const extName = getAnyExtensionFromPath(tspConfigFile); + const newFolderPath = joinPaths(currentFolder, source); + const configFile = getBaseFileName(tspConfigFile); + + const relativeFiles = findFilesWithSameExtension( + newFolderPath, + extName, + configFile, + tspConfigFile, + ); + + const schema = TypeSpecConfigJsonSchema; + return schema ? resolveCompleteItems(schema, target, "", relativeFiles) : []; } else { const schema = TypeSpecConfigJsonSchema; return schema ? resolveCompleteItems(schema, target) : []; @@ -251,6 +274,7 @@ export async function provideTspconfigCompletionItems( schema: ObjectJSONSchemaType, target: YamlScalarTarget, libName?: string, + relativeFiles?: string[], ): CompletionItem[] { const { path: nodePath, type: targetType, source, sourceQuoteType } = target; // if the target is a key which means it's pointing to an object property, we should remove the last element of the path to get it's parent object for its schema @@ -318,6 +342,18 @@ export async function provideTspconfigCompletionItems( return item; }); result.push(...enums); + } else if (cur.type === "string") { + // extends + if (relativeFiles && relativeFiles.length > 0) { + for (const file of relativeFiles) { + const item: CompletionItem = { + label: file, + kind: CompletionItemKind.Field, + textEdit: getNewTextAndPosition(file, source, tspConfigPosition), + }; + result.push(item); + } + } } } }); @@ -364,3 +400,42 @@ function getNewTextAndPosition( newText, ); } + +/** + * Find a set of relative paths to files with the same suffix + * @param rootPath The root path of the current configuration file + * @param fileExtension File extension + * @param configFile Configuration file name + * @param tspConfigFile Full path of configuration file (including file name) + * @returns + */ +function findFilesWithSameExtension( + rootPath: string, + fileExtension: string, + configFile: string, + tspConfigFile: string, +): string[] { + const exclude = ["node_modules", "tsp-output"]; + const files: string[] = []; + const filesInDir = fs.readdirSync(rootPath); + for (const file of filesInDir) { + const ext = getAnyExtensionFromPath(file); + if ( + (ext && ext.length > 0 && ext !== fileExtension) || + exclude.includes(file) || + getBaseFileName(file) === configFile + ) { + continue; + } + + const filePath = joinPaths(rootPath, file); + const stat = fs.statSync(filePath); + if (stat.isDirectory()) { + files.push(...findFilesWithSameExtension(filePath, fileExtension, configFile, tspConfigFile)); + } else if (stat.isFile() && file.endsWith(fileExtension)) { + const relativePath = getRelativePathFromDirectory(tspConfigFile, filePath, false); + files.push(relativePath.slice(1, relativePath.length)); + } + } + return files; +} From 82f2ac788d4f981c8a92f763f3e9d345b1120638 Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Mon, 2 Dec 2024 17:46:54 +0800 Subject: [PATCH 08/25] imports autocomplete --- .../src/server/tspconfig/completion.ts | 24 +++++++++++++++++++ packages/compiler/src/server/yaml-resolver.ts | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/server/tspconfig/completion.ts b/packages/compiler/src/server/tspconfig/completion.ts index 049f6a70c2..013893cffa 100644 --- a/packages/compiler/src/server/tspconfig/completion.ts +++ b/packages/compiler/src/server/tspconfig/completion.ts @@ -197,6 +197,23 @@ export async function provideTspconfigCompletionItems( tspConfigFile, ); + const schema = TypeSpecConfigJsonSchema; + return schema ? resolveCompleteItems(schema, target, "", relativeFiles) : []; + } else if ( + nodePath.length >= CONFIG_PATH_LENGTH_FOR_EMITTER_LIST && + nodePath[0] === "imports" + ) { + const currentFolder = getDirectoryPath(tspConfigFile); + const newFolderPath = joinPaths(currentFolder, source); + const configFile = getBaseFileName(tspConfigFile); + + const relativeFiles = findFilesWithSameExtension( + newFolderPath, + ".tsp", + configFile, + tspConfigFile, + ); + const schema = TypeSpecConfigJsonSchema; return schema ? resolveCompleteItems(schema, target, "", relativeFiles) : []; } else { @@ -346,6 +363,10 @@ export async function provideTspconfigCompletionItems( // extends if (relativeFiles && relativeFiles.length > 0) { for (const file of relativeFiles) { + if (target.siblings.includes(file)) { + continue; + } + const item: CompletionItem = { label: file, kind: CompletionItemKind.Field, @@ -416,6 +437,9 @@ function findFilesWithSameExtension( tspConfigFile: string, ): string[] { const exclude = ["node_modules", "tsp-output"]; + if (fileExtension === ".tsp") { + exclude.push("main.tsp"); + } const files: string[] = []; const filesInDir = fs.readdirSync(rootPath); for (const file of filesInDir) { diff --git a/packages/compiler/src/server/yaml-resolver.ts b/packages/compiler/src/server/yaml-resolver.ts index 590da3079f..138749993e 100644 --- a/packages/compiler/src/server/yaml-resolver.ts +++ b/packages/compiler/src/server/yaml-resolver.ts @@ -289,7 +289,7 @@ function createYamlPathFromVisitScalarNode( if (isMap(parent)) { for (const p of parent.items) { if (p !== last && isPair(p)) { - (p.value as any)?.items.forEach((i: any) => { + (p.value as any)?.items?.forEach((i: any) => { if (i.key !== undefined) { targetSiblingChildren.push((i.key as any).source ?? ""); } else { From 81df10c1e29ab771babea135c079e87a6a4d91b9 Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Tue, 3 Dec 2024 10:15:04 +0800 Subject: [PATCH 09/25] add more info to distinguish required or optional parameter for emitter's options --- .../src/server/tspconfig/completion.ts | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/packages/compiler/src/server/tspconfig/completion.ts b/packages/compiler/src/server/tspconfig/completion.ts index 013893cffa..68df798efe 100644 --- a/packages/compiler/src/server/tspconfig/completion.ts +++ b/packages/compiler/src/server/tspconfig/completion.ts @@ -118,27 +118,26 @@ export async function provideTspconfigCompletionItems( const linters = await libProvider.listLibraries(tspConfigFile); const items: CompletionItem[] = []; for (const [name, pkg] of Object.entries(linters)) { - if (!siblings.includes(name)) { - // If a ruleSet exists for the linter, add it to the end of the library name. - const exports = await pkg.getModuleExports(); - let additionalContent: string = ""; - if (exports?.$linter?.ruleSets !== undefined) { - additionalContent = Object.keys(exports?.$linter?.ruleSets)[0]; - } - - const labelName = - additionalContent.length === 0 ? name : `${name}/${additionalContent}`; - const newText = getNewTextValue(sourceQuoteType, labelName); + // If a ruleSet exists for the linter, add it to the end of the library name. + const exports = await pkg.getModuleExports(); + let additionalContent: string = ""; + if (exports?.$linter?.ruleSets !== undefined) { + additionalContent = Object.keys(exports?.$linter?.ruleSets)[0]; + } - const item: CompletionItem = { - label: labelName, - kind: CompletionItemKind.Field, - documentation: - (await pkg.getPackageJsonData())?.description ?? `Linters from ${name}`, - textEdit: getNewTextAndPosition(newText, source, tspConfigPosition), - }; - items.push(item); + const labelName = additionalContent.length === 0 ? name : `${name}/${additionalContent}`; + if (siblings.includes(labelName)) { + continue; } + + const newText = getNewTextValue(sourceQuoteType, labelName); + const item: CompletionItem = { + label: labelName, + kind: CompletionItemKind.Field, + documentation: (await pkg.getPackageJsonData())?.description ?? `Linters from ${name}`, + textEdit: getNewTextAndPosition(newText, source, tspConfigPosition), + }; + items.push(item); } return items; } else { @@ -312,7 +311,12 @@ export async function provideTspconfigCompletionItems( const item: CompletionItem = { label: key, kind: CompletionItemKind.Field, - documentation: cur.properties[key].description, + documentation: + cur.properties[key].description !== undefined + ? cur.required?.includes(key) + ? "[required]\n" + cur.properties[key].description + : "[optional]\n" + cur.properties[key].description + : "", }; return item; }); From da937402b3f63f926a37f202d7bc63bd8ec5dbdd Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Wed, 4 Dec 2024 11:35:46 +0800 Subject: [PATCH 10/25] completion for known variables/parameters/env interpolation --- .../src/server/tspconfig/completion.ts | 40 ++++++++++++ packages/compiler/src/server/yaml-resolver.ts | 62 ++++++++++++++++++- 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/server/tspconfig/completion.ts b/packages/compiler/src/server/tspconfig/completion.ts index 68df798efe..4ef7a69eae 100644 --- a/packages/compiler/src/server/tspconfig/completion.ts +++ b/packages/compiler/src/server/tspconfig/completion.ts @@ -363,6 +363,46 @@ export async function provideTspconfigCompletionItems( return item; }); result.push(...enums); + } else if ( + cur.type === "string" && + sourceQuoteType === "QUOTE_DOUBLE" && + /{(env\.)?}|{[^{}]*}/g.test(source) + ) { + // Variable interpolation + // environment-variables + if (/{env\.}(?!\S)|{env\.}/g.test(source)) { + for (const env of target.envs) { + if (!nodePath.includes(env)) { + result.push({ + label: env, + kind: CompletionItemKind.Value, + documentation: cur.description, + }); + } + } + } else { + // built-in variables + result.push( + ...["cwd", "project-root"].map((value) => { + const item: CompletionItem = { + label: value, + kind: CompletionItemKind.Value, + documentation: cur.description, + }; + return item; + }), + ); + // parameters + for (const param of target.parameters) { + if (!nodePath.includes(param)) { + result.push({ + label: param, + kind: CompletionItemKind.Value, + documentation: cur.description, + }); + } + } + } } else if (cur.type === "string") { // extends if (relativeFiles && relativeFiles.length > 0) { diff --git a/packages/compiler/src/server/yaml-resolver.ts b/packages/compiler/src/server/yaml-resolver.ts index 138749993e..dbeb22b7e6 100644 --- a/packages/compiler/src/server/yaml-resolver.ts +++ b/packages/compiler/src/server/yaml-resolver.ts @@ -45,6 +45,16 @@ export interface YamlScalarTarget { * The input quotes (double quotes or single quotes) */ sourceQuoteType: string; + + /** + * The parameters of the config file + */ + parameters: string[]; + + /** + * The environment variables of the config file + */ + envs: string[]; } interface YamlVisitScalarNode { @@ -98,6 +108,8 @@ export function resolveYamlScalarTarget( sourceQuoteType: "", siblings: rootProperties, siblingsChildren: [], + parameters: [], + envs: [], }; } for (let i = position.line - 1; i >= 0; i--) { @@ -146,6 +158,8 @@ export function resolveYamlScalarTarget( sourceQuoteType: "", siblings: [...yp.siblings, yp.source], siblingsChildren: yp.siblingsChildren, + parameters: yp.parameters, + envs: yp.envs, }; } break; @@ -191,6 +205,8 @@ export function resolveYamlScalarTarget( ? (last.value?.items.map((item) => (item.key as any).source ?? "") ?? []) : [], siblingsChildren: yp.siblingsChildren, + parameters: yp.parameters, + envs: yp.envs, }; } break; @@ -224,6 +240,35 @@ function createYamlPathFromVisitScalarNode( }); return undefined; } + + // fix params and environment variables if exists in the config file + const configParams: string[] = []; + const configEnvs: string[] = []; + for (let i = 0; i < nodePath.length; i++) { + const seg = nodePath[i]; + if (isMap(seg)) { + const findItems = seg.items.filter( + (item) => + (item.key).source === "environment-variables" || + (item.key).source === "parameters", + ); + findItems.forEach((item) => { + if (item.value !== null && isMap(item.value)) { + item.value.items.forEach((i) => { + if (isPair(i)) { + if ((item.key as any).source === "environment-variables") { + configEnvs.push((i.key as any).source ?? ""); + } else if ((item.key as any).source === "parameters") { + configParams.push((i.key as any).source ?? ""); + } + } + }); + } + }); + break; + } + } + const path: string[] = []; for (let i = 0; i < nodePath.length; i++) { @@ -255,6 +300,8 @@ function createYamlPathFromVisitScalarNode( sourceQuoteType: n.type ?? "", siblings: [], siblingsChildren: [], + parameters: configParams, + envs: configEnvs, }; } else if (isPair(last)) { if (nodePath.length < 2) { @@ -277,9 +324,18 @@ function createYamlPathFromVisitScalarNode( sourceQuoteType: n.type ?? "", siblings: [], siblingsChildren: [], + parameters: configParams, + envs: configEnvs, }; } else { - const parent = nodePath[nodePath.length - 2]; + let parent: any; + for (const p of nodePath) { + if (isPair(p) && path.length > 0 && (p.key as any).source === path[0]) { + parent = p.value; + break; + } + } + const targetSiblings = isMap(parent) ? parent.items.filter((item) => item !== last).map((item) => (item.key as any).source ?? "") : []; @@ -306,6 +362,8 @@ function createYamlPathFromVisitScalarNode( siblings: targetSiblings, siblingsChildren: targetSiblingChildren, sourceQuoteType: n.type ?? "", + parameters: configParams, + envs: configEnvs, }; } } else if (isSeq(last)) { @@ -318,6 +376,8 @@ function createYamlPathFromVisitScalarNode( .filter((i) => i !== n) .map((item) => (isScalar(item) ? (item.source ?? "") : "")), siblingsChildren: [], + parameters: configParams, + envs: configEnvs, }; } else { log({ From 54726b8105df398ecf23e17d6484e6ff738f73ce Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Thu, 5 Dec 2024 09:27:16 +0800 Subject: [PATCH 11/25] update extends/imports path autocompletion and required or optional parameter for emitter's options, revert typespec-vs .csproj file --- .../src/server/tspconfig/completion.ts | 108 +++++++++--------- packages/compiler/src/server/yaml-resolver.ts | 16 +-- .../src/Microsoft.TypeSpec.VS.csproj | 32 ++++-- 3 files changed, 80 insertions(+), 76 deletions(-) diff --git a/packages/compiler/src/server/tspconfig/completion.ts b/packages/compiler/src/server/tspconfig/completion.ts index 4ef7a69eae..2d6d92049f 100644 --- a/packages/compiler/src/server/tspconfig/completion.ts +++ b/packages/compiler/src/server/tspconfig/completion.ts @@ -41,6 +41,7 @@ export async function provideTspconfigCompletionItems( await fileService.getPath(tspConfigDoc), target, tspConfigPosition, + log, ); return items; @@ -48,16 +49,20 @@ export async function provideTspconfigCompletionItems( tspConfigFile: string, target: YamlScalarTarget, tspConfigPosition: Position, + log: (log: ServerLog) => void, ): Promise { const { path: nodePath, type: targetType, siblings, - sourceQuoteType, + sourceType, source, siblingsChildren, } = target; const CONFIG_PATH_LENGTH_FOR_EMITTER_LIST = 2; + const CONFIG_PATH_LENGTH_FOR_LINTER_LIST = 2; + const CONFIG_PATH_LENGTH_FOR_EXTENDS = 1; + const CONFIG_PATH_LENGTH_FOR_IMPORTS = 2; if ( (nodePath.length === CONFIG_PATH_LENGTH_FOR_EMITTER_LIST && nodePath[0] === "options" && @@ -72,7 +77,7 @@ export async function provideTspconfigCompletionItems( for (const [name, pkg] of Object.entries(libs)) { if (!siblings.includes(name)) { // Generate new text - const newText = getNewTextValue(sourceQuoteType, name); + const newText = getNewTextValue(sourceType, name); const item: CompletionItem = { label: name, @@ -111,8 +116,8 @@ export async function provideTspconfigCompletionItems( itemsFromEmitter.push(...more); } return [...itemsFromBuiltIn, ...itemsFromEmitter]; - } else if (nodePath.length > CONFIG_PATH_LENGTH_FOR_EMITTER_LIST && nodePath[0] === "linter") { - const linterName = nodePath[CONFIG_PATH_LENGTH_FOR_EMITTER_LIST - 1]; + } else if (nodePath.length > CONFIG_PATH_LENGTH_FOR_LINTER_LIST && nodePath[0] === "linter") { + const linterName = nodePath[CONFIG_PATH_LENGTH_FOR_LINTER_LIST - 1]; libProvider.setIsGetEmitterVal(false); if (linterName === "extends") { const linters = await libProvider.listLibraries(tspConfigFile); @@ -130,7 +135,7 @@ export async function provideTspconfigCompletionItems( continue; } - const newText = getNewTextValue(sourceQuoteType, labelName); + const newText = getNewTextValue(sourceType, labelName); const item: CompletionItem = { label: labelName, kind: CompletionItemKind.Field, @@ -183,35 +188,21 @@ export async function provideTspconfigCompletionItems( return [...itemsFromLintter]; } - } else if (nodePath.length < CONFIG_PATH_LENGTH_FOR_EMITTER_LIST && nodePath[0] === "extends") { + } else if (nodePath.length === CONFIG_PATH_LENGTH_FOR_EXTENDS && nodePath[0] === "extends") { const currentFolder = getDirectoryPath(tspConfigFile); const extName = getAnyExtensionFromPath(tspConfigFile); const newFolderPath = joinPaths(currentFolder, source); const configFile = getBaseFileName(tspConfigFile); - const relativeFiles = findFilesWithSameExtension( - newFolderPath, - extName, - configFile, - tspConfigFile, - ); + const relativeFiles = findFilesWithSameExtension(newFolderPath, extName, log, configFile); const schema = TypeSpecConfigJsonSchema; return schema ? resolveCompleteItems(schema, target, "", relativeFiles) : []; - } else if ( - nodePath.length >= CONFIG_PATH_LENGTH_FOR_EMITTER_LIST && - nodePath[0] === "imports" - ) { + } else if (nodePath.length >= CONFIG_PATH_LENGTH_FOR_IMPORTS && nodePath[0] === "imports") { const currentFolder = getDirectoryPath(tspConfigFile); const newFolderPath = joinPaths(currentFolder, source); - const configFile = getBaseFileName(tspConfigFile); - const relativeFiles = findFilesWithSameExtension( - newFolderPath, - ".tsp", - configFile, - tspConfigFile, - ); + const relativeFiles = findFilesWithSameExtension(newFolderPath, ".tsp", log); const schema = TypeSpecConfigJsonSchema; return schema ? resolveCompleteItems(schema, target, "", relativeFiles) : []; @@ -292,7 +283,7 @@ export async function provideTspconfigCompletionItems( libName?: string, relativeFiles?: string[], ): CompletionItem[] { - const { path: nodePath, type: targetType, source, sourceQuoteType } = target; + const { path: nodePath, type: targetType, source, sourceType } = target; // if the target is a key which means it's pointing to an object property, we should remove the last element of the path to get it's parent object for its schema const path = targetType === "key" ? nodePath.slice(0, -1) : nodePath; const foundSchemas = findSchemaByPath(schema, path, 0); @@ -311,12 +302,9 @@ export async function provideTspconfigCompletionItems( const item: CompletionItem = { label: key, kind: CompletionItemKind.Field, - documentation: - cur.properties[key].description !== undefined - ? cur.required?.includes(key) - ? "[required]\n" + cur.properties[key].description - : "[optional]\n" + cur.properties[key].description - : "", + documentation: cur.required?.includes(key) + ? "[required]\n" + (cur.properties[key].description ?? "") + : "[optional]\n" + (cur.properties[key].description ?? ""), }; return item; }); @@ -329,7 +317,7 @@ export async function provideTspconfigCompletionItems( continue; } - const newText = getNewTextValue(sourceQuoteType, labelName); + const newText = getNewTextValue(sourceType, labelName); const item: CompletionItem = { label: labelName, kind: CompletionItemKind.Field, @@ -365,7 +353,7 @@ export async function provideTspconfigCompletionItems( result.push(...enums); } else if ( cur.type === "string" && - sourceQuoteType === "QUOTE_DOUBLE" && + sourceType === "QUOTE_DOUBLE" && /{(env\.)?}|{[^{}]*}/g.test(source) ) { // Variable interpolation @@ -414,7 +402,6 @@ export async function provideTspconfigCompletionItems( const item: CompletionItem = { label: file, kind: CompletionItemKind.Field, - textEdit: getNewTextAndPosition(file, source, tspConfigPosition), }; result.push(item); } @@ -429,15 +416,15 @@ export async function provideTspconfigCompletionItems( /** * Get the new text value - * @param sourceQuoteType input quote type(single, double, none) + * @param sourceType input quote type(single, double, none) * @param formatText format text value * @returns */ -function getNewTextValue(sourceQuoteType: string, formatText: string): string { +function getNewTextValue(sourceType: string, formatText: string): string { let newText: string = ""; - if (sourceQuoteType === "QUOTE_SINGLE") { + if (sourceType === "QUOTE_SINGLE") { newText = `${formatText}'`; - } else if (sourceQuoteType === "QUOTE_DOUBLE") { + } else if (sourceType === "QUOTE_DOUBLE") { newText = `${formatText}"`; } else { newText = `"${formatText}"`; @@ -470,40 +457,47 @@ function getNewTextAndPosition( * Find a set of relative paths to files with the same suffix * @param rootPath The root path of the current configuration file * @param fileExtension File extension + * @param log log function * @param configFile Configuration file name - * @param tspConfigFile Full path of configuration file (including file name) * @returns */ function findFilesWithSameExtension( rootPath: string, fileExtension: string, - configFile: string, - tspConfigFile: string, + log: (log: ServerLog) => void, + configFile: string = "", ): string[] { const exclude = ["node_modules", "tsp-output"]; if (fileExtension === ".tsp") { exclude.push("main.tsp"); } + const files: string[] = []; - const filesInDir = fs.readdirSync(rootPath); - for (const file of filesInDir) { - const ext = getAnyExtensionFromPath(file); - if ( - (ext && ext.length > 0 && ext !== fileExtension) || - exclude.includes(file) || - getBaseFileName(file) === configFile - ) { - continue; - } + try { + // When reading the content under the path, an error may be reported if the path is incorrect. + const filesInDir = fs.readdirSync(rootPath); + for (const file of filesInDir) { + const ext = getAnyExtensionFromPath(file); + if ( + (ext && ext.length > 0 && ext !== fileExtension) || + exclude.includes(file) || + (configFile !== "" && getBaseFileName(file) === configFile) + ) { + continue; + } - const filePath = joinPaths(rootPath, file); - const stat = fs.statSync(filePath); - if (stat.isDirectory()) { - files.push(...findFilesWithSameExtension(filePath, fileExtension, configFile, tspConfigFile)); - } else if (stat.isFile() && file.endsWith(fileExtension)) { - const relativePath = getRelativePathFromDirectory(tspConfigFile, filePath, false); - files.push(relativePath.slice(1, relativePath.length)); + const filePath = joinPaths(rootPath, file); + const stat = fs.statSync(filePath); + if (stat.isDirectory() || (stat.isFile() && file.endsWith(fileExtension))) { + const relativePath = getRelativePathFromDirectory(rootPath, filePath, false); + files.push(relativePath); + } } + } catch (error) { + log({ + level: "error", + message: `input path error: ${(error as Error).message}`, + }); } return files; } diff --git a/packages/compiler/src/server/yaml-resolver.ts b/packages/compiler/src/server/yaml-resolver.ts index dbeb22b7e6..07e8fe0e49 100644 --- a/packages/compiler/src/server/yaml-resolver.ts +++ b/packages/compiler/src/server/yaml-resolver.ts @@ -44,7 +44,7 @@ export interface YamlScalarTarget { /** * The input quotes (double quotes or single quotes) */ - sourceQuoteType: string; + sourceType: string; /** * The parameters of the config file @@ -105,7 +105,7 @@ export function resolveYamlScalarTarget( path: [""], type: "key", source: "", - sourceQuoteType: "", + sourceType: "", siblings: rootProperties, siblingsChildren: [], parameters: [], @@ -155,7 +155,7 @@ export function resolveYamlScalarTarget( path: [...yp.path.slice(0, yp.path.length - 1), ""], type: "key", source: "", - sourceQuoteType: "", + sourceType: "", siblings: [...yp.siblings, yp.source], siblingsChildren: yp.siblingsChildren, parameters: yp.parameters, @@ -200,7 +200,7 @@ export function resolveYamlScalarTarget( path: [...yp.path, ""], type: "key", source: "", - sourceQuoteType: "", + sourceType: "", siblings: isMap(last.value) ? (last.value?.items.map((item) => (item.key as any).source ?? "") ?? []) : [], @@ -297,7 +297,7 @@ function createYamlPathFromVisitScalarNode( path: [], type: key === null ? "key" : "value", source: n.source ?? "", - sourceQuoteType: n.type ?? "", + sourceType: n.type ?? "", siblings: [], siblingsChildren: [], parameters: configParams, @@ -321,7 +321,7 @@ function createYamlPathFromVisitScalarNode( path, type: "key", source: n.source ?? "", - sourceQuoteType: n.type ?? "", + sourceType: n.type ?? "", siblings: [], siblingsChildren: [], parameters: configParams, @@ -361,7 +361,7 @@ function createYamlPathFromVisitScalarNode( source: n.source ?? "", siblings: targetSiblings, siblingsChildren: targetSiblingChildren, - sourceQuoteType: n.type ?? "", + sourceType: n.type ?? "", parameters: configParams, envs: configEnvs, }; @@ -371,7 +371,7 @@ function createYamlPathFromVisitScalarNode( path: path, type: "arr-item", source: n.source ?? "", - sourceQuoteType: n.type ?? "", + sourceType: n.type ?? "", siblings: last.items .filter((i) => i !== n) .map((item) => (isScalar(item) ? (item.source ?? "") : "")), diff --git a/packages/typespec-vs/src/Microsoft.TypeSpec.VS.csproj b/packages/typespec-vs/src/Microsoft.TypeSpec.VS.csproj index c7847b41c6..5d2bb081a1 100644 --- a/packages/typespec-vs/src/Microsoft.TypeSpec.VS.csproj +++ b/packages/typespec-vs/src/Microsoft.TypeSpec.VS.csproj @@ -16,14 +16,16 @@ $(MSBuildThisFileDirectory)..\$(AssemblyName).vsix - + - + - + @@ -40,8 +42,10 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -52,20 +56,26 @@ - + - - + + - - + + - + From 759ec59257d206a4c76789c7bee35a6b88160387 Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Thu, 5 Dec 2024 10:56:38 +0800 Subject: [PATCH 12/25] update linter rules/ruleSets --- packages/compiler/src/server/lib-provider.ts | 53 +++----- packages/compiler/src/server/serverlib.ts | 12 +- .../src/server/tspconfig/completion.ts | 120 +++++++----------- packages/compiler/src/server/yaml-resolver.ts | 39 +----- 4 files changed, 71 insertions(+), 153 deletions(-) diff --git a/packages/compiler/src/server/lib-provider.ts b/packages/compiler/src/server/lib-provider.ts index 92387419a9..f27dbe3c4f 100644 --- a/packages/compiler/src/server/lib-provider.ts +++ b/packages/compiler/src/server/lib-provider.ts @@ -2,18 +2,11 @@ import { joinPaths } from "../core/path-utils.js"; import { NpmPackage, NpmPackageProvider } from "./npm-package-provider.js"; export class LibraryProvider { - private isEmitterPackageCache = new Map(); - private isLinterPackageCache = new Map(); - private isGetEmitter: boolean = false; - constructor(private npmPackageProvider: NpmPackageProvider) {} - - /** - * Set whether to get the emitter library or the linter library - * @param isGetEmitter true if you want to get the emitter library, false if you want to get the linter library - */ - setIsGetEmitterVal(isGetEmitter: boolean): void { - this.isGetEmitter = isGetEmitter; - } + private isLibPackageCache = new Map(); + constructor( + private npmPackageProvider: NpmPackageProvider, + private filter: (obj: Record) => boolean, + ) {} /** * @@ -45,24 +38,20 @@ export class LibraryProvider { /** * * @param startFolder folder starts to search for package.json with emitters/linter defined as dependencies - * @param emitterName + * @param depName * @returns */ - async getLibrary(startFolder: string, emitterName: string): Promise { + async getLibrary(startFolder: string, depName: string): Promise { const packageJsonFolder = await this.npmPackageProvider.getPackageJsonFolder(startFolder); if (!packageJsonFolder) { return undefined; } - return this.getLibraryFromDep(packageJsonFolder, emitterName); + return this.getLibraryFromDep(packageJsonFolder, depName); } - private async isEmitter(depName: string, pkg: NpmPackage) { - if (this.isGetEmitter && this.isEmitterPackageCache.has(depName)) { - return this.isEmitterPackageCache.get(depName); - } - - if (!this.isGetEmitter && this.isLinterPackageCache.has(depName)) { - return this.isLinterPackageCache.get(depName); + private async isSpecifyLibType(depName: string, pkg: NpmPackage) { + if (this.isLibPackageCache.has(depName)) { + return this.isLibPackageCache.get(depName); } const data = await pkg.getPackageJsonData(); @@ -75,22 +64,12 @@ export class LibraryProvider { const exports = await pkg.getModuleExports(); // don't add to cache when failing to load exports which is unexpected if (!exports) return false; - const isEmitter = this.isGetEmitter - ? exports.$onEmit !== undefined - : exports.$linter !== undefined; - if (this.isGetEmitter) { - this.isEmitterPackageCache.set(depName, isEmitter); - } else { - this.isLinterPackageCache.set(depName, isEmitter); - } + + const isEmitter = this.filter(exports); + this.isLibPackageCache.set(depName, isEmitter); return isEmitter; } else { - if (this.isGetEmitter) { - this.isEmitterPackageCache.set(depName, false); - } else { - this.isLinterPackageCache.set(depName, false); - } - + this.isLibPackageCache.set(depName, false); return false; } } @@ -98,7 +77,7 @@ export class LibraryProvider { private async getLibraryFromDep(packageJsonFolder: string, depName: string) { const depFolder = joinPaths(packageJsonFolder, "node_modules", depName); const depPkg = await this.npmPackageProvider.get(depFolder); - if (depPkg && (await this.isEmitter(depName, depPkg))) { + if (depPkg && (await this.isSpecifyLibType(depName, depPkg))) { return depPkg; } return undefined; diff --git a/packages/compiler/src/server/serverlib.ts b/packages/compiler/src/server/serverlib.ts index 80cba55526..c942f39c68 100644 --- a/packages/compiler/src/server/serverlib.ts +++ b/packages/compiler/src/server/serverlib.ts @@ -114,7 +114,14 @@ export function createServer(host: ServerHost): Server { }); const compilerHost = createCompilerHost(); const npmPackageProvider = new NpmPackageProvider(compilerHost); - const libProvider = new LibraryProvider(npmPackageProvider); + const emitterProvider = new LibraryProvider( + npmPackageProvider, + (exports) => exports.$onEmit !== undefined, + ); + const linterProvider = new LibraryProvider( + npmPackageProvider, + (exports) => exports.$linter !== undefined, + ); const compileService = createCompileService({ fileService, @@ -703,7 +710,8 @@ export function createServer(host: ServerHost): Server { if (doc) { const items = await provideTspconfigCompletionItems(doc, params.position, { fileService, - libProvider: libProvider, + emitterProvider, + linterProvider, log, }); return CompletionList.create(items); diff --git a/packages/compiler/src/server/tspconfig/completion.ts b/packages/compiler/src/server/tspconfig/completion.ts index 2d6d92049f..52fe2aeb4b 100644 --- a/packages/compiler/src/server/tspconfig/completion.ts +++ b/packages/compiler/src/server/tspconfig/completion.ts @@ -28,11 +28,12 @@ export async function provideTspconfigCompletionItems( tspConfigPosition: Position, context: { fileService: FileService; - libProvider: LibraryProvider; + emitterProvider: LibraryProvider; + linterProvider: LibraryProvider; log: (log: ServerLog) => void; }, ): Promise { - const { fileService, libProvider, log } = context; + const { fileService, emitterProvider, linterProvider, log } = context; const target = resolveYamlScalarTarget(tspConfigDoc, tspConfigPosition, log); if (target === undefined) { return []; @@ -51,18 +52,12 @@ export async function provideTspconfigCompletionItems( tspConfigPosition: Position, log: (log: ServerLog) => void, ): Promise { - const { - path: nodePath, - type: targetType, - siblings, - sourceType, - source, - siblingsChildren, - } = target; + const { path: nodePath, type: targetType, siblings, sourceType, source } = target; const CONFIG_PATH_LENGTH_FOR_EMITTER_LIST = 2; const CONFIG_PATH_LENGTH_FOR_LINTER_LIST = 2; const CONFIG_PATH_LENGTH_FOR_EXTENDS = 1; const CONFIG_PATH_LENGTH_FOR_IMPORTS = 2; + if ( (nodePath.length === CONFIG_PATH_LENGTH_FOR_EMITTER_LIST && nodePath[0] === "options" && @@ -71,8 +66,7 @@ export async function provideTspconfigCompletionItems( nodePath[0] === "emit" && targetType === "arr-item") ) { - libProvider.setIsGetEmitterVal(true); - const libs = await libProvider.listLibraries(tspConfigFile); + const libs = await emitterProvider.listLibraries(tspConfigFile); const items: CompletionItem[] = []; for (const [name, pkg] of Object.entries(libs)) { if (!siblings.includes(name)) { @@ -91,15 +85,13 @@ export async function provideTspconfigCompletionItems( return items; } else if (nodePath.length > CONFIG_PATH_LENGTH_FOR_EMITTER_LIST && nodePath[0] === "options") { const emitterName = nodePath[CONFIG_PATH_LENGTH_FOR_EMITTER_LIST - 1]; - libProvider.setIsGetEmitterVal(true); - const emitter = await libProvider.getLibrary(tspConfigFile, emitterName); + const emitter = await emitterProvider.getLibrary(tspConfigFile, emitterName); if (!emitter) { return []; } - const exports = await emitter.getModuleExports(); + const exports = await emitter.getModuleExports(); const builtInEmitterSchema = emitterOptionsSchema; - const itemsFromBuiltIn = builtInEmitterSchema ? resolveCompleteItems(builtInEmitterSchema, { ...target, @@ -118,11 +110,11 @@ export async function provideTspconfigCompletionItems( return [...itemsFromBuiltIn, ...itemsFromEmitter]; } else if (nodePath.length > CONFIG_PATH_LENGTH_FOR_LINTER_LIST && nodePath[0] === "linter") { const linterName = nodePath[CONFIG_PATH_LENGTH_FOR_LINTER_LIST - 1]; - libProvider.setIsGetEmitterVal(false); - if (linterName === "extends") { - const linters = await libProvider.listLibraries(tspConfigFile); - const items: CompletionItem[] = []; - for (const [name, pkg] of Object.entries(linters)) { + const items: CompletionItem[] = []; + const linters = await linterProvider.listLibraries(tspConfigFile); + + for (const [name, pkg] of Object.entries(linters)) { + if (linterName === "extends") { // If a ruleSet exists for the linter, add it to the end of the library name. const exports = await pkg.getModuleExports(); let additionalContent: string = ""; @@ -143,51 +135,29 @@ export async function provideTspconfigCompletionItems( textEdit: getNewTextAndPosition(newText, source, tspConfigPosition), }; items.push(item); - } - return items; - } else { - const itemsFromLintter = []; - const libNames: string[] = []; - if (siblingsChildren && siblingsChildren.length > 0) { - // Filter duplicate library names - for (const extendsValue of siblingsChildren) { - const arrLine = extendsValue.split("/"); - let libName: string = ""; - if (arrLine.length >= 2) { - libName = arrLine[0] + "/" + arrLine[1]; - } else { - continue; - } - if (!libNames.includes(libName)) { - libNames.push(libName); - } + } else { + // enable/disable rules + const linter = await linterProvider.getLibrary(tspConfigFile, name); + if (!linter) { + return []; } - // Get rules in each library - for (const name of libNames) { - const linter = await libProvider.getLibrary(tspConfigFile, name); - if (!linter) { - return []; - } - - const exports = await linter.getModuleExports(); + const exports = await linter.getModuleExports(); - if (exports?.$linter?.rules !== undefined) { - const more = resolveCompleteItems( - exports?.$linter?.rules, - { - ...target, - path: nodePath.slice(CONFIG_PATH_LENGTH_FOR_EMITTER_LIST), - }, - name, - ); - itemsFromLintter.push(...more); - } + if (exports?.$linter?.rules !== undefined) { + const more = resolveCompleteItems( + exports?.$linter?.rules, + { + ...target, + path: nodePath.slice(CONFIG_PATH_LENGTH_FOR_LINTER_LIST), + }, + name, + ); + items.push(...more); } } - - return [...itemsFromLintter]; } + return items; } else if (nodePath.length === CONFIG_PATH_LENGTH_FOR_EXTENDS && nodePath[0] === "extends") { const currentFolder = getDirectoryPath(tspConfigFile); const extName = getAnyExtensionFromPath(tspConfigFile); @@ -280,8 +250,8 @@ export async function provideTspconfigCompletionItems( function resolveCompleteItems( schema: ObjectJSONSchemaType, target: YamlScalarTarget, - libName?: string, - relativeFiles?: string[], + libName: string = "", + relativeFiles: string[] = [], ): CompletionItem[] { const { path: nodePath, type: targetType, source, sourceType } = target; // if the target is a key which means it's pointing to an object property, we should remove the last element of the path to get it's parent object for its schema @@ -311,20 +281,18 @@ export async function provideTspconfigCompletionItems( result.push(...props); } else if (cur.type === undefined) { // lint rule - for (const key of Object.keys(cur ?? {})) { - const labelName = `${libName}/${cur[key].name}`; - if (target.siblingsChildren.includes(labelName)) { - continue; + if (libName !== "") { + for (const key of Object.keys(cur ?? {})) { + const labelName = `${libName}/${cur[key].name}`; + const newText = getNewTextValue(sourceType, labelName); + const item: CompletionItem = { + label: labelName, + kind: CompletionItemKind.Field, + documentation: cur[key].description, + textEdit: getNewTextAndPosition(newText, source, tspConfigPosition), + }; + result.push(item); } - - const newText = getNewTextValue(sourceType, labelName); - const item: CompletionItem = { - label: labelName, - kind: CompletionItemKind.Field, - documentation: cur[key].description, - textEdit: getNewTextAndPosition(newText, source, tspConfigPosition), - }; - result.push(item); } } } @@ -393,7 +361,7 @@ export async function provideTspconfigCompletionItems( } } else if (cur.type === "string") { // extends - if (relativeFiles && relativeFiles.length > 0) { + if (relativeFiles.length > 0) { for (const file of relativeFiles) { if (target.siblings.includes(file)) { continue; diff --git a/packages/compiler/src/server/yaml-resolver.ts b/packages/compiler/src/server/yaml-resolver.ts index 07e8fe0e49..5dff9fdb7a 100644 --- a/packages/compiler/src/server/yaml-resolver.ts +++ b/packages/compiler/src/server/yaml-resolver.ts @@ -35,22 +35,14 @@ export interface YamlScalarTarget { * The siblings of the target node */ siblings: string[]; - - /** - * The children of the siblings of the target node - */ - siblingsChildren: string[]; - /** * The input quotes (double quotes or single quotes) */ sourceType: string; - /** * The parameters of the config file */ parameters: string[]; - /** * The environment variables of the config file */ @@ -107,7 +99,6 @@ export function resolveYamlScalarTarget( source: "", sourceType: "", siblings: rootProperties, - siblingsChildren: [], parameters: [], envs: [], }; @@ -157,7 +148,6 @@ export function resolveYamlScalarTarget( source: "", sourceType: "", siblings: [...yp.siblings, yp.source], - siblingsChildren: yp.siblingsChildren, parameters: yp.parameters, envs: yp.envs, }; @@ -204,7 +194,6 @@ export function resolveYamlScalarTarget( siblings: isMap(last.value) ? (last.value?.items.map((item) => (item.key as any).source ?? "") ?? []) : [], - siblingsChildren: yp.siblingsChildren, parameters: yp.parameters, envs: yp.envs, }; @@ -299,7 +288,6 @@ function createYamlPathFromVisitScalarNode( source: n.source ?? "", sourceType: n.type ?? "", siblings: [], - siblingsChildren: [], parameters: configParams, envs: configEnvs, }; @@ -323,44 +311,20 @@ function createYamlPathFromVisitScalarNode( source: n.source ?? "", sourceType: n.type ?? "", siblings: [], - siblingsChildren: [], parameters: configParams, envs: configEnvs, }; } else { - let parent: any; - for (const p of nodePath) { - if (isPair(p) && path.length > 0 && (p.key as any).source === path[0]) { - parent = p.value; - break; - } - } - + const parent = nodePath.length >= 2 ? nodePath[nodePath.length - 2] : undefined; const targetSiblings = isMap(parent) ? parent.items.filter((item) => item !== last).map((item) => (item.key as any).source ?? "") : []; - // This function mainly provides support for linter - const targetSiblingChildren: string[] = []; - if (isMap(parent)) { - for (const p of parent.items) { - if (p !== last && isPair(p)) { - (p.value as any)?.items?.forEach((i: any) => { - if (i.key !== undefined) { - targetSiblingChildren.push((i.key as any).source ?? ""); - } else { - targetSiblingChildren.push((i as any).source ?? ""); - } - }); - } - } - } return { path: path, type: key === "key" ? "key" : "value", source: n.source ?? "", siblings: targetSiblings, - siblingsChildren: targetSiblingChildren, sourceType: n.type ?? "", parameters: configParams, envs: configEnvs, @@ -375,7 +339,6 @@ function createYamlPathFromVisitScalarNode( siblings: last.items .filter((i) => i !== n) .map((item) => (isScalar(item) ? (item.source ?? "") : "")), - siblingsChildren: [], parameters: configParams, envs: configEnvs, }; From 6f249c7f6bbbe9ee5e7367e8d203977ddc7c7779 Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Mon, 9 Dec 2024 14:56:34 +0800 Subject: [PATCH 13/25] Partially updated --- packages/compiler/src/server/lib-provider.ts | 26 +- .../src/server/tspconfig/completion.ts | 244 ++++++++++-------- 2 files changed, 152 insertions(+), 118 deletions(-) diff --git a/packages/compiler/src/server/lib-provider.ts b/packages/compiler/src/server/lib-provider.ts index f27dbe3c4f..ab0f1e2f7c 100644 --- a/packages/compiler/src/server/lib-provider.ts +++ b/packages/compiler/src/server/lib-provider.ts @@ -4,20 +4,21 @@ import { NpmPackage, NpmPackageProvider } from "./npm-package-provider.js"; export class LibraryProvider { private isLibPackageCache = new Map(); constructor( - private npmPackageProvider: NpmPackageProvider, + private libPackageFilterResultCache: NpmPackageProvider, private filter: (obj: Record) => boolean, ) {} /** * - * @param startFolder folder starts to search for package.json with emitters/linters defined as dependencies + * @param startFolder folder starts to search for package.json with library defined as dependencies * @returns */ async listLibraries(startFolder: string): Promise> { - const packageJsonFolder = await this.npmPackageProvider.getPackageJsonFolder(startFolder); + const packageJsonFolder = + await this.libPackageFilterResultCache.getPackageJsonFolder(startFolder); if (!packageJsonFolder) return {}; - const pkg = await this.npmPackageProvider.get(packageJsonFolder); + const pkg = await this.libPackageFilterResultCache.get(packageJsonFolder); const data = await pkg?.getPackageJsonData(); if (!data) return {}; @@ -37,19 +38,20 @@ export class LibraryProvider { /** * - * @param startFolder folder starts to search for package.json with emitters/linter defined as dependencies - * @param depName + * @param startFolder folder starts to search for package.json with library defined as dependencies + * @param libName * @returns */ - async getLibrary(startFolder: string, depName: string): Promise { - const packageJsonFolder = await this.npmPackageProvider.getPackageJsonFolder(startFolder); + async getLibrary(startFolder: string, libName: string): Promise { + const packageJsonFolder = + await this.libPackageFilterResultCache.getPackageJsonFolder(startFolder); if (!packageJsonFolder) { return undefined; } - return this.getLibraryFromDep(packageJsonFolder, depName); + return this.getLibraryFromDep(packageJsonFolder, libName); } - private async isSpecifyLibType(depName: string, pkg: NpmPackage) { + private async getLibFilterResult(depName: string, pkg: NpmPackage) { if (this.isLibPackageCache.has(depName)) { return this.isLibPackageCache.get(depName); } @@ -76,8 +78,8 @@ export class LibraryProvider { private async getLibraryFromDep(packageJsonFolder: string, depName: string) { const depFolder = joinPaths(packageJsonFolder, "node_modules", depName); - const depPkg = await this.npmPackageProvider.get(depFolder); - if (depPkg && (await this.isSpecifyLibType(depName, depPkg))) { + const depPkg = await this.libPackageFilterResultCache.get(depFolder); + if (depPkg && (await this.getLibFilterResult(depName, depPkg))) { return depPkg; } return undefined; diff --git a/packages/compiler/src/server/tspconfig/completion.ts b/packages/compiler/src/server/tspconfig/completion.ts index 52fe2aeb4b..23b27feebf 100644 --- a/packages/compiler/src/server/tspconfig/completion.ts +++ b/packages/compiler/src/server/tspconfig/completion.ts @@ -15,7 +15,12 @@ import { getRelativePathFromDirectory, joinPaths, } from "../../core/path-utils.js"; -import { JSONSchemaType, ServerLog } from "../../index.js"; +import { + DiagnosticMessages, + JSONSchemaType, + LinterRuleDefinition, + ServerLog, +} from "../../index.js"; import { distinctArray } from "../../utils/misc.js"; import { FileService } from "../file-service.js"; import { LibraryProvider } from "../lib-provider.js"; @@ -66,20 +71,19 @@ export async function provideTspconfigCompletionItems( nodePath[0] === "emit" && targetType === "arr-item") ) { - const libs = await emitterProvider.listLibraries(tspConfigFile); + const emitters = await emitterProvider.listLibraries(tspConfigFile); const items: CompletionItem[] = []; - for (const [name, pkg] of Object.entries(libs)) { + for (const [name, pkg] of Object.entries(emitters)) { if (!siblings.includes(name)) { - // Generate new text - const newText = getNewTextValue(sourceType, name); - - const item: CompletionItem = { - label: name, - kind: CompletionItemKind.Field, - documentation: (await pkg.getPackageJsonData())?.description ?? `Emitter from ${name}`, - textEdit: getNewTextAndPosition(newText, source, tspConfigPosition), - }; - items.push(item); + items.push( + getCommonCompetionItem( + name, + sourceType, + source, + (await pkg.getPackageJsonData())?.description ?? `Emitter from ${name}`, + tspConfigPosition, + ), + ); } } return items; @@ -109,51 +113,70 @@ export async function provideTspconfigCompletionItems( } return [...itemsFromBuiltIn, ...itemsFromEmitter]; } else if (nodePath.length > CONFIG_PATH_LENGTH_FOR_LINTER_LIST && nodePath[0] === "linter") { - const linterName = nodePath[CONFIG_PATH_LENGTH_FOR_LINTER_LIST - 1]; + const extendKeyWord = nodePath[CONFIG_PATH_LENGTH_FOR_LINTER_LIST - 1]; const items: CompletionItem[] = []; const linters = await linterProvider.listLibraries(tspConfigFile); - for (const [name, pkg] of Object.entries(linters)) { - if (linterName === "extends") { + if (extendKeyWord === "extends") { + for (const [name, pkg] of Object.entries(linters)) { // If a ruleSet exists for the linter, add it to the end of the library name. const exports = await pkg.getModuleExports(); - let additionalContent: string = ""; if (exports?.$linter?.ruleSets !== undefined) { - additionalContent = Object.keys(exports?.$linter?.ruleSets)[0]; - } + // Below ruleSets are objects rather than arrays + for (const [ruleSet] of Object.entries(exports?.$linter?.ruleSets)) { + const labelName = `${name}/${ruleSet}`; + if (siblings.includes(labelName)) { + continue; + } - const labelName = additionalContent.length === 0 ? name : `${name}/${additionalContent}`; - if (siblings.includes(labelName)) { + items.push( + getCommonCompetionItem( + labelName, + sourceType, + source, + (await pkg.getPackageJsonData())?.description ?? `Linters from ${labelName}`, + tspConfigPosition, + ), + ); + } continue; } - const newText = getNewTextValue(sourceType, labelName); - const item: CompletionItem = { - label: labelName, - kind: CompletionItemKind.Field, - documentation: (await pkg.getPackageJsonData())?.description ?? `Linters from ${name}`, - textEdit: getNewTextAndPosition(newText, source, tspConfigPosition), - }; - items.push(item); - } else { - // enable/disable rules - const linter = await linterProvider.getLibrary(tspConfigFile, name); - if (!linter) { - return []; + // If there is no corresponding ruleSet in the library, add the library name directly. + if (siblings.includes(name)) { + continue; } - const exports = await linter.getModuleExports(); + items.push( + getCommonCompetionItem( + name, + sourceType, + source, + (await pkg.getPackageJsonData())?.description ?? `Linters from ${name}`, + tspConfigPosition, + ), + ); + } + } else { + // enable/disable rules + for (const [name, pkg] of Object.entries(linters)) { + const exports = await pkg.getModuleExports(); if (exports?.$linter?.rules !== undefined) { - const more = resolveCompleteItems( + for (const [, rule] of Object.entries>( exports?.$linter?.rules, - { - ...target, - path: nodePath.slice(CONFIG_PATH_LENGTH_FOR_LINTER_LIST), - }, - name, - ); - items.push(...more); + )) { + const labelName = `${name}/${rule.name}`; + items.push( + getCommonCompetionItem( + labelName, + sourceType, + source, + rule.description, + tspConfigPosition, + ), + ); + } } } } @@ -165,17 +188,13 @@ export async function provideTspconfigCompletionItems( const configFile = getBaseFileName(tspConfigFile); const relativeFiles = findFilesWithSameExtension(newFolderPath, extName, log, configFile); - - const schema = TypeSpecConfigJsonSchema; - return schema ? resolveCompleteItems(schema, target, "", relativeFiles) : []; + return getFilePathCompletionItems(relativeFiles, siblings); } else if (nodePath.length >= CONFIG_PATH_LENGTH_FOR_IMPORTS && nodePath[0] === "imports") { const currentFolder = getDirectoryPath(tspConfigFile); const newFolderPath = joinPaths(currentFolder, source); const relativeFiles = findFilesWithSameExtension(newFolderPath, ".tsp", log); - - const schema = TypeSpecConfigJsonSchema; - return schema ? resolveCompleteItems(schema, target, "", relativeFiles) : []; + return getFilePathCompletionItems(relativeFiles, siblings); } else { const schema = TypeSpecConfigJsonSchema; return schema ? resolveCompleteItems(schema, target) : []; @@ -250,8 +269,6 @@ export async function provideTspconfigCompletionItems( function resolveCompleteItems( schema: ObjectJSONSchemaType, target: YamlScalarTarget, - libName: string = "", - relativeFiles: string[] = [], ): CompletionItem[] { const { path: nodePath, type: targetType, source, sourceType } = target; // if the target is a key which means it's pointing to an object property, we should remove the last element of the path to get it's parent object for its schema @@ -279,21 +296,6 @@ export async function provideTspconfigCompletionItems( return item; }); result.push(...props); - } else if (cur.type === undefined) { - // lint rule - if (libName !== "") { - for (const key of Object.keys(cur ?? {})) { - const labelName = `${libName}/${cur[key].name}`; - const newText = getNewTextValue(sourceType, labelName); - const item: CompletionItem = { - label: labelName, - kind: CompletionItemKind.Field, - documentation: cur[key].description, - textEdit: getNewTextAndPosition(newText, source, tspConfigPosition), - }; - result.push(item); - } - } } } if (targetType === "value" || targetType === "arr-item") { @@ -359,21 +361,6 @@ export async function provideTspconfigCompletionItems( } } } - } else if (cur.type === "string") { - // extends - if (relativeFiles.length > 0) { - for (const file of relativeFiles) { - if (target.siblings.includes(file)) { - continue; - } - - const item: CompletionItem = { - label: file, - kind: CompletionItemKind.Field, - }; - result.push(item); - } - } } } }); @@ -384,15 +371,15 @@ export async function provideTspconfigCompletionItems( /** * Get the new text value - * @param sourceType input quote type(single, double, none) + * @param sourceQuoteType input quote type(single, double, none) * @param formatText format text value - * @returns + * @returns new text value */ -function getNewTextValue(sourceType: string, formatText: string): string { +function getCompletionItemInsertedValue(sourceQuoteType: string, formatText: string): string { let newText: string = ""; - if (sourceType === "QUOTE_SINGLE") { + if (sourceQuoteType === "QUOTE_SINGLE") { newText = `${formatText}'`; - } else if (sourceType === "QUOTE_DOUBLE") { + } else if (sourceQuoteType === "QUOTE_DOUBLE") { newText = `${formatText}"`; } else { newText = `"${formatText}"`; @@ -404,8 +391,8 @@ function getNewTextValue(sourceType: string, formatText: string): string { * Get the position of the new text * @param newText the new text * @param source source text - * @param tspConfigPosition original position - * @returns + * @param tspConfigPosition original position, see {@link Position} + * @returns TextEdit object */ function getNewTextAndPosition( newText: string, @@ -427,7 +414,7 @@ function getNewTextAndPosition( * @param fileExtension File extension * @param log log function * @param configFile Configuration file name - * @returns + * @returns Relative path array */ function findFilesWithSameExtension( rootPath: string, @@ -435,7 +422,7 @@ function findFilesWithSameExtension( log: (log: ServerLog) => void, configFile: string = "", ): string[] { - const exclude = ["node_modules", "tsp-output"]; + const exclude = ["node_modules", "tsp-output", ".vs", ".vscode"]; if (fileExtension === ".tsp") { exclude.push("main.tsp"); } @@ -443,24 +430,18 @@ function findFilesWithSameExtension( const files: string[] = []; try { // When reading the content under the path, an error may be reported if the path is incorrect. - const filesInDir = fs.readdirSync(rootPath); - for (const file of filesInDir) { - const ext = getAnyExtensionFromPath(file); - if ( - (ext && ext.length > 0 && ext !== fileExtension) || - exclude.includes(file) || - (configFile !== "" && getBaseFileName(file) === configFile) - ) { - continue; - } - - const filePath = joinPaths(rootPath, file); - const stat = fs.statSync(filePath); - if (stat.isDirectory() || (stat.isFile() && file.endsWith(fileExtension))) { + fs.readdirSync(rootPath, { withFileTypes: true }) + .filter( + (d) => + (d.isDirectory() || (d.isFile() && d.name.endsWith(fileExtension))) && + !exclude.includes(d.name) && + getBaseFileName(d.name) !== configFile, + ) + .map((d) => { + const filePath = joinPaths(rootPath, d.name); const relativePath = getRelativePathFromDirectory(rootPath, filePath, false); files.push(relativePath); - } - } + }); } catch (error) { log({ level: "error", @@ -469,3 +450,54 @@ function findFilesWithSameExtension( } return files; } + +/** + * Get the CompletionItem object array of the relative path of the file + * @param relativeFiles File relative path array + * @param siblings Sibling node array + * @returns CompletionItem object array + */ +function getFilePathCompletionItems(relativeFiles: string[], siblings: string[]): CompletionItem[] { + const items: CompletionItem[] = []; + if (relativeFiles.length > 0) { + for (const file of relativeFiles) { + if (siblings.includes(file)) { + continue; + } + + const item: CompletionItem = { + label: file, + kind: CompletionItemKind.File, + }; + items.push(item); + } + } + return items; +} + +/** + * Get the common CompletionItem object + * @param labelName The value of the label attribute of the CompletionItem object + * @param sourceType Input quote type + * @param source Entered text + * @param description The value of the documentation attribute of the CompletionItem object + * @param tspConfigPosition Input location object, see {@link Position} + * @returns CompletionItem object + */ +function getCommonCompetionItem( + labelName: string, + sourceType: string, + source: string, + description: string, + tspConfigPosition: Position, +): CompletionItem { + // Generate new text + const newText = getCompletionItemInsertedValue(sourceType, labelName); + + return { + label: labelName, + kind: CompletionItemKind.Field, + documentation: description, + textEdit: getNewTextAndPosition(newText, source, tspConfigPosition), + }; +} From 849973b803e0b6c1603fb7f487210d97fca4e7a9 Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Mon, 9 Dec 2024 16:44:16 +0800 Subject: [PATCH 14/25] Update variable interpolation --- .../src/server/tspconfig/completion.ts | 151 +++++++++++++----- packages/compiler/src/server/yaml-resolver.ts | 71 +++----- 2 files changed, 129 insertions(+), 93 deletions(-) diff --git a/packages/compiler/src/server/tspconfig/completion.ts b/packages/compiler/src/server/tspconfig/completion.ts index 23b27feebf..c98c00f023 100644 --- a/packages/compiler/src/server/tspconfig/completion.ts +++ b/packages/compiler/src/server/tspconfig/completion.ts @@ -7,6 +7,7 @@ import { Range, TextEdit, } from "vscode-languageserver/node.js"; +import { isMap, isPair } from "yaml"; import { emitterOptionsSchema, TypeSpecConfigJsonSchema } from "../../config/config-schema.js"; import { getAnyExtensionFromPath, @@ -24,7 +25,12 @@ import { import { distinctArray } from "../../utils/misc.js"; import { FileService } from "../file-service.js"; import { LibraryProvider } from "../lib-provider.js"; -import { resolveYamlScalarTarget, YamlScalarTarget } from "../yaml-resolver.js"; +import { + getYamlDocScalarNode, + resolveYamlScalarTarget, + YamlScalarTarget, + YamlVisitScalarNode, +} from "../yaml-resolver.js"; type ObjectJSONSchemaType = JSONSchemaType; @@ -43,6 +49,34 @@ export async function provideTspconfigCompletionItems( if (target === undefined) { return []; } + + // Variable interpolation + const yamlDocNodes = getYamlDocScalarNode(tspConfigDoc); + if (yamlDocNodes === undefined) { + return []; + } + + if (target.sourceType === "QUOTE_DOUBLE" && /{(env\.)?}|{[^{}]*}/g.test(target.source)) { + // environment-variables + if (/{env\.}(?!\S)|{env\.}/g.test(target.source)) { + // environment-variables + return getEnvsCompletionItem(yamlDocNodes); + } else { + // parameters and built-in variables + return [ + ...getParametersCompletionItems(yamlDocNodes), + ...["cwd", "project-root"].map((value) => { + const item: CompletionItem = { + label: value, + kind: CompletionItemKind.Value, + documentation: "Built-in variables", + }; + return item; + }), + ]; + } + } + const items = resolveTspConfigCompleteItems( await fileService.getPath(tspConfigDoc), target, @@ -270,7 +304,7 @@ export async function provideTspconfigCompletionItems( schema: ObjectJSONSchemaType, target: YamlScalarTarget, ): CompletionItem[] { - const { path: nodePath, type: targetType, source, sourceType } = target; + const { path: nodePath, type: targetType } = target; // if the target is a key which means it's pointing to an object property, we should remove the last element of the path to get it's parent object for its schema const path = targetType === "key" ? nodePath.slice(0, -1) : nodePath; const foundSchemas = findSchemaByPath(schema, path, 0); @@ -321,46 +355,6 @@ export async function provideTspconfigCompletionItems( return item; }); result.push(...enums); - } else if ( - cur.type === "string" && - sourceType === "QUOTE_DOUBLE" && - /{(env\.)?}|{[^{}]*}/g.test(source) - ) { - // Variable interpolation - // environment-variables - if (/{env\.}(?!\S)|{env\.}/g.test(source)) { - for (const env of target.envs) { - if (!nodePath.includes(env)) { - result.push({ - label: env, - kind: CompletionItemKind.Value, - documentation: cur.description, - }); - } - } - } else { - // built-in variables - result.push( - ...["cwd", "project-root"].map((value) => { - const item: CompletionItem = { - label: value, - kind: CompletionItemKind.Value, - documentation: cur.description, - }; - return item; - }), - ); - // parameters - for (const param of target.parameters) { - if (!nodePath.includes(param)) { - result.push({ - label: param, - kind: CompletionItemKind.Value, - documentation: cur.description, - }); - } - } - } } } }); @@ -501,3 +495,76 @@ function getCommonCompetionItem( textEdit: getNewTextAndPosition(newText, source, tspConfigPosition), }; } + +/** + * Get the CompletionItem array of custom variables + * @param yamlDocNodes The contents of the YAML configuration file + * @returns CompletionItem array of custom variables + */ +function getParametersCompletionItems(yamlDocNodes: YamlVisitScalarNode): CompletionItem[] { + const { path: yamlNode } = yamlDocNodes; + const configParams: string[] = []; + for (let i = 0; i < yamlNode.length; i++) { + const seg = yamlNode[i]; + if (isMap(seg)) { + seg.items + .filter((item) => (item.key).source === "parameters") + .map((item) => { + if (item.value !== null && isMap(item.value)) { + item.value.items.forEach((i) => { + if (isPair(i) && (item.key as any).source === "parameters") { + configParams.push((i.key as any).source ?? ""); + } + }); + } + }); + break; + } + } + const result: CompletionItem[] = []; + configParams.map((param) => { + result.push({ + label: param, + kind: CompletionItemKind.Value, + documentation: "Custom paramters variables", + }); + }); + return result; +} + +/** + * Get the CompletionItem array of custom environment variables + * @param yamlDocNodes The contents of the YAML configuration file + * @returns CompletionItem array of custom environment variables + */ +function getEnvsCompletionItem(yamlDocNodes: YamlVisitScalarNode): CompletionItem[] { + const { path: yamlNode } = yamlDocNodes; + const configEnvs: string[] = []; + for (let i = 0; i < yamlNode.length; i++) { + const seg = yamlNode[i]; + if (isMap(seg)) { + seg.items + .filter((item) => (item.key).source === "environment-variables") + .map((item) => { + if (item.value !== null && isMap(item.value)) { + item.value.items.forEach((i) => { + if (isPair(i) && (item.key as any).source === "environment-variables") { + configEnvs.push((i.key as any).source ?? ""); + } + }); + } + }); + break; + } + } + + const result: CompletionItem[] = []; + configEnvs.map((envLabel) => { + result.push({ + label: envLabel, + kind: CompletionItemKind.Value, + documentation: "Custom environment variables", + }); + }); + return result; +} diff --git a/packages/compiler/src/server/yaml-resolver.ts b/packages/compiler/src/server/yaml-resolver.ts index 5dff9fdb7a..1274822f65 100644 --- a/packages/compiler/src/server/yaml-resolver.ts +++ b/packages/compiler/src/server/yaml-resolver.ts @@ -39,22 +39,33 @@ export interface YamlScalarTarget { * The input quotes (double quotes or single quotes) */ sourceType: string; - /** - * The parameters of the config file - */ - parameters: string[]; - /** - * The environment variables of the config file - */ - envs: string[]; } -interface YamlVisitScalarNode { +export interface YamlVisitScalarNode { key: number | "key" | "value" | null; n: Scalar; path: readonly YamlNodePathSegment[]; } +export function getYamlDocScalarNode(document: TextDocument): YamlVisitScalarNode | undefined { + const content = document.getText(); + const yamlDoc = parseDocument(content, { + keepSourceTokens: true, + }); + + let found = undefined; + visit(yamlDoc, { + Node: (key, n, path) => { + if (isScalar(n)) { + found = { key, n, path }; + return visit.BREAK; + } + return undefined; + }, + }); + return found; +} + export function resolveYamlScalarTarget( document: TextDocument, position: Position, @@ -99,8 +110,6 @@ export function resolveYamlScalarTarget( source: "", sourceType: "", siblings: rootProperties, - parameters: [], - envs: [], }; } for (let i = position.line - 1; i >= 0; i--) { @@ -148,8 +157,6 @@ export function resolveYamlScalarTarget( source: "", sourceType: "", siblings: [...yp.siblings, yp.source], - parameters: yp.parameters, - envs: yp.envs, }; } break; @@ -194,8 +201,6 @@ export function resolveYamlScalarTarget( siblings: isMap(last.value) ? (last.value?.items.map((item) => (item.key as any).source ?? "") ?? []) : [], - parameters: yp.parameters, - envs: yp.envs, }; } break; @@ -230,34 +235,6 @@ function createYamlPathFromVisitScalarNode( return undefined; } - // fix params and environment variables if exists in the config file - const configParams: string[] = []; - const configEnvs: string[] = []; - for (let i = 0; i < nodePath.length; i++) { - const seg = nodePath[i]; - if (isMap(seg)) { - const findItems = seg.items.filter( - (item) => - (item.key).source === "environment-variables" || - (item.key).source === "parameters", - ); - findItems.forEach((item) => { - if (item.value !== null && isMap(item.value)) { - item.value.items.forEach((i) => { - if (isPair(i)) { - if ((item.key as any).source === "environment-variables") { - configEnvs.push((i.key as any).source ?? ""); - } else if ((item.key as any).source === "parameters") { - configParams.push((i.key as any).source ?? ""); - } - } - }); - } - }); - break; - } - } - const path: string[] = []; for (let i = 0; i < nodePath.length; i++) { @@ -288,8 +265,6 @@ function createYamlPathFromVisitScalarNode( source: n.source ?? "", sourceType: n.type ?? "", siblings: [], - parameters: configParams, - envs: configEnvs, }; } else if (isPair(last)) { if (nodePath.length < 2) { @@ -311,8 +286,6 @@ function createYamlPathFromVisitScalarNode( source: n.source ?? "", sourceType: n.type ?? "", siblings: [], - parameters: configParams, - envs: configEnvs, }; } else { const parent = nodePath.length >= 2 ? nodePath[nodePath.length - 2] : undefined; @@ -326,8 +299,6 @@ function createYamlPathFromVisitScalarNode( source: n.source ?? "", siblings: targetSiblings, sourceType: n.type ?? "", - parameters: configParams, - envs: configEnvs, }; } } else if (isSeq(last)) { @@ -339,8 +310,6 @@ function createYamlPathFromVisitScalarNode( siblings: last.items .filter((i) => i !== n) .map((item) => (isScalar(item) ? (item.source ?? "") : "")), - parameters: configParams, - envs: configEnvs, }; } else { log({ From 9d30083c35b744331ff6336c628507414dd7d4ec Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Wed, 11 Dec 2024 17:43:06 +0800 Subject: [PATCH 15/25] updated --- packages/compiler/src/server/lib-provider.ts | 26 +- .../src/server/tspconfig/completion.ts | 421 +++++++++--------- packages/compiler/src/server/yaml-resolver.ts | 66 +-- 3 files changed, 269 insertions(+), 244 deletions(-) diff --git a/packages/compiler/src/server/lib-provider.ts b/packages/compiler/src/server/lib-provider.ts index ab0f1e2f7c..bc4d2e9b1b 100644 --- a/packages/compiler/src/server/lib-provider.ts +++ b/packages/compiler/src/server/lib-provider.ts @@ -2,9 +2,9 @@ import { joinPaths } from "../core/path-utils.js"; import { NpmPackage, NpmPackageProvider } from "./npm-package-provider.js"; export class LibraryProvider { - private isLibPackageCache = new Map(); + private libPackageFilterResultCache = new Map(); constructor( - private libPackageFilterResultCache: NpmPackageProvider, + private npmPackageProvider: NpmPackageProvider, private filter: (obj: Record) => boolean, ) {} @@ -14,11 +14,10 @@ export class LibraryProvider { * @returns */ async listLibraries(startFolder: string): Promise> { - const packageJsonFolder = - await this.libPackageFilterResultCache.getPackageJsonFolder(startFolder); + const packageJsonFolder = await this.npmPackageProvider.getPackageJsonFolder(startFolder); if (!packageJsonFolder) return {}; - const pkg = await this.libPackageFilterResultCache.get(packageJsonFolder); + const pkg = await this.npmPackageProvider.get(packageJsonFolder); const data = await pkg?.getPackageJsonData(); if (!data) return {}; @@ -43,8 +42,7 @@ export class LibraryProvider { * @returns */ async getLibrary(startFolder: string, libName: string): Promise { - const packageJsonFolder = - await this.libPackageFilterResultCache.getPackageJsonFolder(startFolder); + const packageJsonFolder = await this.npmPackageProvider.getPackageJsonFolder(startFolder); if (!packageJsonFolder) { return undefined; } @@ -52,8 +50,8 @@ export class LibraryProvider { } private async getLibFilterResult(depName: string, pkg: NpmPackage) { - if (this.isLibPackageCache.has(depName)) { - return this.isLibPackageCache.get(depName); + if (this.libPackageFilterResultCache.has(depName)) { + return this.libPackageFilterResultCache.get(depName); } const data = await pkg.getPackageJsonData(); @@ -67,18 +65,18 @@ export class LibraryProvider { // don't add to cache when failing to load exports which is unexpected if (!exports) return false; - const isEmitter = this.filter(exports); - this.isLibPackageCache.set(depName, isEmitter); - return isEmitter; + const filterResult = this.filter(exports); + this.libPackageFilterResultCache.set(depName, filterResult); + return filterResult; } else { - this.isLibPackageCache.set(depName, false); + this.libPackageFilterResultCache.set(depName, false); return false; } } private async getLibraryFromDep(packageJsonFolder: string, depName: string) { const depFolder = joinPaths(packageJsonFolder, "node_modules", depName); - const depPkg = await this.libPackageFilterResultCache.get(depFolder); + const depPkg = await this.npmPackageProvider.get(depFolder); if (depPkg && (await this.getLibFilterResult(depName, depPkg))) { return depPkg; } diff --git a/packages/compiler/src/server/tspconfig/completion.ts b/packages/compiler/src/server/tspconfig/completion.ts index c98c00f023..92cb77219d 100644 --- a/packages/compiler/src/server/tspconfig/completion.ts +++ b/packages/compiler/src/server/tspconfig/completion.ts @@ -1,4 +1,5 @@ -import * as fs from "fs"; +import * as fs from "fs/promises"; +import * as sysPath from "path"; import { TextDocument } from "vscode-languageserver-textdocument"; import { CompletionItem, @@ -7,15 +8,9 @@ import { Range, TextEdit, } from "vscode-languageserver/node.js"; -import { isMap, isPair } from "yaml"; +import { Document, isMap, isPair, Node } from "yaml"; import { emitterOptionsSchema, TypeSpecConfigJsonSchema } from "../../config/config-schema.js"; -import { - getAnyExtensionFromPath, - getBaseFileName, - getDirectoryPath, - getRelativePathFromDirectory, - joinPaths, -} from "../../core/path-utils.js"; +import { getDirectoryPath, joinPaths } from "../../core/path-utils.js"; import { DiagnosticMessages, JSONSchemaType, @@ -25,12 +20,7 @@ import { import { distinctArray } from "../../utils/misc.js"; import { FileService } from "../file-service.js"; import { LibraryProvider } from "../lib-provider.js"; -import { - getYamlDocScalarNode, - resolveYamlScalarTarget, - YamlScalarTarget, - YamlVisitScalarNode, -} from "../yaml-resolver.js"; +import { resolveYamlScalarTarget, YamlScalarTarget } from "../yaml-resolver.js"; type ObjectJSONSchemaType = JSONSchemaType; @@ -51,32 +41,19 @@ export async function provideTspconfigCompletionItems( } // Variable interpolation - const yamlDocNodes = getYamlDocScalarNode(tspConfigDoc); - if (yamlDocNodes === undefined) { - return []; - } - - if (target.sourceType === "QUOTE_DOUBLE" && /{(env\.)?}|{[^{}]*}/g.test(target.source)) { - // environment-variables - if (/{env\.}(?!\S)|{env\.}/g.test(target.source)) { - // environment-variables - return getEnvsCompletionItem(yamlDocNodes); - } else { - // parameters and built-in variables - return [ - ...getParametersCompletionItems(yamlDocNodes), - ...["cwd", "project-root"].map((value) => { - const item: CompletionItem = { - label: value, - kind: CompletionItemKind.Value, - documentation: "Built-in variables", - }; - return item; - }), - ]; - } + const content = tspConfigDoc.getText(); + const lines = content.split("\n"); + const targetLine = lines[tspConfigPosition.line]; + const variableInterpolationItems = resolveVariableInterpolationCompleteItems( + target, + tspConfigPosition, + targetLine, + ); + if (variableInterpolationItems.length > 0) { + return variableInterpolationItems; } + const pos = tspConfigDoc.offsetAt(tspConfigPosition); const items = resolveTspConfigCompleteItems( await fileService.getPath(tspConfigDoc), target, @@ -109,15 +86,18 @@ export async function provideTspconfigCompletionItems( const items: CompletionItem[] = []; for (const [name, pkg] of Object.entries(emitters)) { if (!siblings.includes(name)) { - items.push( - getCommonCompetionItem( - name, - sourceType, - source, - (await pkg.getPackageJsonData())?.description ?? `Emitter from ${name}`, - tspConfigPosition, - ), + const item = createContainingQuatedValCompetionItem( + name, + sourceType, + source, + (await pkg.getPackageJsonData())?.description ?? `Emitter from ${name}`, + tspConfigPosition, + target.nodePostionRange, + pos, ); + if (item !== undefined) { + items.push(item); + } } } return items; @@ -159,39 +139,41 @@ export async function provideTspconfigCompletionItems( // Below ruleSets are objects rather than arrays for (const [ruleSet] of Object.entries(exports?.$linter?.ruleSets)) { const labelName = `${name}/${ruleSet}`; - if (siblings.includes(labelName)) { - continue; - } - - items.push( - getCommonCompetionItem( + if (!siblings.includes(labelName)) { + const item = createContainingQuatedValCompetionItem( labelName, sourceType, source, (await pkg.getPackageJsonData())?.description ?? `Linters from ${labelName}`, tspConfigPosition, - ), - ); + target.nodePostionRange, + pos, + ); + if (item !== undefined) { + items.push(item); + } + } } continue; } // If there is no corresponding ruleSet in the library, add the library name directly. - if (siblings.includes(name)) { - continue; - } - - items.push( - getCommonCompetionItem( + if (!siblings.includes(name)) { + const item = createContainingQuatedValCompetionItem( name, sourceType, source, (await pkg.getPackageJsonData())?.description ?? `Linters from ${name}`, tspConfigPosition, - ), - ); + target.nodePostionRange, + pos, + ); + if (item !== undefined) { + items.push(item); + } + } } - } else { + } else if (extendKeyWord === "enable" || extendKeyWord === "disable") { // enable/disable rules for (const [name, pkg] of Object.entries(linters)) { const exports = await pkg.getModuleExports(); @@ -201,33 +183,43 @@ export async function provideTspconfigCompletionItems( exports?.$linter?.rules, )) { const labelName = `${name}/${rule.name}`; - items.push( - getCommonCompetionItem( - labelName, - sourceType, - source, - rule.description, - tspConfigPosition, - ), + const item = createContainingQuatedValCompetionItem( + labelName, + sourceType, + source, + rule.description, + tspConfigPosition, + target.nodePostionRange, + pos, ); + if (item !== undefined) { + items.push(item); + } } } } + } else { + log({ + level: "warning", + message: "Unknown linter keyword", + }); } return items; } else if (nodePath.length === CONFIG_PATH_LENGTH_FOR_EXTENDS && nodePath[0] === "extends") { const currentFolder = getDirectoryPath(tspConfigFile); - const extName = getAnyExtensionFromPath(tspConfigFile); const newFolderPath = joinPaths(currentFolder, source); - const configFile = getBaseFileName(tspConfigFile); - const relativeFiles = findFilesWithSameExtension(newFolderPath, extName, log, configFile); + const relativeFiles = await findFilesOrDirsWithSameExtension( + newFolderPath, + ".yaml", + sysPath.resolve(tspConfigFile), + ); return getFilePathCompletionItems(relativeFiles, siblings); } else if (nodePath.length >= CONFIG_PATH_LENGTH_FOR_IMPORTS && nodePath[0] === "imports") { const currentFolder = getDirectoryPath(tspConfigFile); const newFolderPath = joinPaths(currentFolder, source); - const relativeFiles = findFilesWithSameExtension(newFolderPath, ".tsp", log); + const relativeFiles = await findFilesOrDirsWithSameExtension(newFolderPath, ".tsp"); return getFilePathCompletionItems(relativeFiles, siblings); } else { const schema = TypeSpecConfigJsonSchema; @@ -382,65 +374,105 @@ function getCompletionItemInsertedValue(sourceQuoteType: string, formatText: str } /** - * Get the position of the new text + * Get the full completion item value and edit position * @param newText the new text * @param source source text + * @param nodePosRange current node position range, [startPos, endPos, nodeEndPos] + * @param curPos current cursor position * @param tspConfigPosition original position, see {@link Position} - * @returns TextEdit object + * @returns TextEdit object or undefined */ -function getNewTextAndPosition( +function getFullCompletionItemValAndEditPosition( newText: string, source: string, + nodePosRange: number[], + curPos: number, + tspConfigPosition: Position, +): TextEdit | undefined { + const [startPos, endPos] = nodePosRange; + + if (curPos >= startPos && curPos <= endPos) { + return TextEdit.replace( + Range.create( + Position.create(tspConfigPosition.line, tspConfigPosition.character - source.length), + Position.create(tspConfigPosition.line, tspConfigPosition.character + (endPos - curPos)), + ), + newText, + ); + } + return undefined; +} + +/** + * Get the common CompletionItem object, which value is included in quotes + * @param labelName The value of the label attribute of the CompletionItem object + * @param sourceType Input quote type + * @param source Entered text + * @param description The value of the documentation attribute of the CompletionItem object + * @param tspConfigPosition Input location object, see {@link Position} + * @param nodePosRange current node position range, [startPos, endPos, nodeEndPos] + * @param curPos current cursor position + * @returns CompletionItem object or undefined + */ +function createContainingQuatedValCompetionItem( + labelName: string, + sourceType: string, + source: string, + description: string, tspConfigPosition: Position, -): TextEdit { - return TextEdit.replace( - Range.create( - Position.create(tspConfigPosition.line, tspConfigPosition.character - source.length), - Position.create(tspConfigPosition.line, tspConfigPosition.character + newText.length), - ), + nodePosRange: number[], + curPos: number, +): CompletionItem | undefined { + // Generate new text + const newText = getCompletionItemInsertedValue(sourceType, labelName); + const textEdit = getFullCompletionItemValAndEditPosition( newText, + source, + nodePosRange, + curPos, + tspConfigPosition, ); + if (textEdit !== undefined) { + return { + label: labelName, + kind: CompletionItemKind.Field, + documentation: description, + textEdit: textEdit, + }; + } + return undefined; } /** - * Find a set of relative paths to files with the same suffix - * @param rootPath The root path of the current configuration file - * @param fileExtension File extension - * @param log log function - * @param configFile Configuration file name - * @returns Relative path array + * Find a set of dirs/files with the same suffix + * @param rootPath The absolute input path or relative path of the current configuration file + * @param fileExtension File extension, such as ".yaml" or ".tsp" + * @param configFile absolute path of the current configuration file + * @returns dir/file array */ -function findFilesWithSameExtension( +async function findFilesOrDirsWithSameExtension( rootPath: string, fileExtension: string, - log: (log: ServerLog) => void, configFile: string = "", -): string[] { +): Promise { const exclude = ["node_modules", "tsp-output", ".vs", ".vscode"]; - if (fileExtension === ".tsp") { - exclude.push("main.tsp"); - } const files: string[] = []; try { // When reading the content under the path, an error may be reported if the path is incorrect. - fs.readdirSync(rootPath, { withFileTypes: true }) + // Exclude the current configuration file, compare in absolute paths + (await fs.readdir(rootPath, { withFileTypes: true })) .filter( (d) => (d.isDirectory() || (d.isFile() && d.name.endsWith(fileExtension))) && !exclude.includes(d.name) && - getBaseFileName(d.name) !== configFile, + sysPath.resolve(d.name) !== configFile, ) .map((d) => { - const filePath = joinPaths(rootPath, d.name); - const relativePath = getRelativePathFromDirectory(rootPath, filePath, false); - files.push(relativePath); + files.push(d.name); }); - } catch (error) { - log({ - level: "error", - message: `input path error: ${(error as Error).message}`, - }); + } catch { + return files; } return files; } @@ -455,116 +487,107 @@ function getFilePathCompletionItems(relativeFiles: string[], siblings: string[]) const items: CompletionItem[] = []; if (relativeFiles.length > 0) { for (const file of relativeFiles) { - if (siblings.includes(file)) { - continue; + if (!siblings.includes(file)) { + const item: CompletionItem = { + label: file, + kind: CompletionItemKind.File, + }; + items.push(item); } - - const item: CompletionItem = { - label: file, - kind: CompletionItemKind.File, - }; - items.push(item); } } return items; } /** - * Get the common CompletionItem object - * @param labelName The value of the label attribute of the CompletionItem object - * @param sourceType Input quote type - * @param source Entered text - * @param description The value of the documentation attribute of the CompletionItem object - * @param tspConfigPosition Input location object, see {@link Position} - * @returns CompletionItem object + * resolve variable interpolation completion items + * @param target YamlScalarTarget object, contains the information of the target node + * @param tspConfigPosition current cursor position in the configuration file + * @param targetLine current line content of the target node + * @returns variable interpolation completion items */ -function getCommonCompetionItem( - labelName: string, - sourceType: string, - source: string, - description: string, +function resolveVariableInterpolationCompleteItems( + target: YamlScalarTarget, tspConfigPosition: Position, -): CompletionItem { - // Generate new text - const newText = getCompletionItemInsertedValue(sourceType, labelName); + targetLine: string, +): CompletionItem[] { + const yamlDocNodes = target.yamlDoc; - return { - label: labelName, - kind: CompletionItemKind.Field, - documentation: description, - textEdit: getNewTextAndPosition(newText, source, tspConfigPosition), - }; -} + if (target.sourceType === "QUOTE_DOUBLE" || target.sourceType === "QUOTE_SINGLE") { + const targetPos = targetLine.lastIndexOf("{"); + const curText = targetLine.substring(targetPos, tspConfigPosition.character); -/** - * Get the CompletionItem array of custom variables - * @param yamlDocNodes The contents of the YAML configuration file - * @returns CompletionItem array of custom variables - */ -function getParametersCompletionItems(yamlDocNodes: YamlVisitScalarNode): CompletionItem[] { - const { path: yamlNode } = yamlDocNodes; - const configParams: string[] = []; - for (let i = 0; i < yamlNode.length; i++) { - const seg = yamlNode[i]; - if (isMap(seg)) { - seg.items - .filter((item) => (item.key).source === "parameters") - .map((item) => { - if (item.value !== null && isMap(item.value)) { - item.value.items.forEach((i) => { - if (isPair(i) && (item.key as any).source === "parameters") { - configParams.push((i.key as any).source ?? ""); - } - }); - } - }); - break; + if (/{[^}]*env\.[^}]*$/.test(curText)) { + // environment-variables + return getCompletionItemsByFilter( + yamlDocNodes, + "environment-variables", + "Custom environment variables", + ); + } else if (/{[^}]*$/.test(curText)) { + // parameters and built-in variables + const result = [ + ...getCompletionItemsByFilter(yamlDocNodes, "parameters", "Custom paramters variables"), + ...["cwd", "project-root"].map((value) => { + const item: CompletionItem = { + label: value, + kind: CompletionItemKind.Value, + documentation: "Built-in variables", + }; + return item; + }), + ]; + + // if the current path is options, add output-dir and emitter-name + if (target.path.length > 2 && target.path[0] === "options") { + result.push( + ...["output-dir", "emitter-name"].map((value) => { + const item: CompletionItem = { + label: value, + kind: CompletionItemKind.Value, + documentation: "Built-in variables", + }; + return item; + }), + ); + } + return result; } } - const result: CompletionItem[] = []; - configParams.map((param) => { - result.push({ - label: param, - kind: CompletionItemKind.Value, - documentation: "Custom paramters variables", - }); - }); - return result; + + return []; } /** - * Get the CompletionItem array of custom environment variables - * @param yamlDocNodes The contents of the YAML configuration file - * @returns CompletionItem array of custom environment variables + * Get the corresponding CompletionItem array based on the filter name + * @param yamlDoc The contents of the YAML configuration file + * @param filterName The filter name,such as "parameters" or "environment-variables" + * @param description The description of the CompletionItem object + * @returns CompletionItem object array */ -function getEnvsCompletionItem(yamlDocNodes: YamlVisitScalarNode): CompletionItem[] { - const { path: yamlNode } = yamlDocNodes; - const configEnvs: string[] = []; - for (let i = 0; i < yamlNode.length; i++) { - const seg = yamlNode[i]; - if (isMap(seg)) { - seg.items - .filter((item) => (item.key).source === "environment-variables") - .map((item) => { - if (item.value !== null && isMap(item.value)) { - item.value.items.forEach((i) => { - if (isPair(i) && (item.key as any).source === "environment-variables") { - configEnvs.push((i.key as any).source ?? ""); - } - }); - } - }); - break; - } +function getCompletionItemsByFilter( + yamlDoc: Document, + filterName: string, + description: string, +): CompletionItem[] { + const result: CompletionItem[] = []; + if (isMap(yamlDoc.contents)) { + yamlDoc.contents.items + .filter((item) => (item.key).source === filterName) + .map((item) => { + if (item.value !== null && isMap(item.value)) { + item.value.items.forEach((i) => { + if (isPair(i)) { + result.push({ + label: (i.key as any).source ?? "", + kind: CompletionItemKind.Value, + documentation: description, + }); + } + }); + } + }); } - const result: CompletionItem[] = []; - configEnvs.map((envLabel) => { - result.push({ - label: envLabel, - kind: CompletionItemKind.Value, - documentation: "Custom environment variables", - }); - }); return result; } diff --git a/packages/compiler/src/server/yaml-resolver.ts b/packages/compiler/src/server/yaml-resolver.ts index 1274822f65..f7522a6d33 100644 --- a/packages/compiler/src/server/yaml-resolver.ts +++ b/packages/compiler/src/server/yaml-resolver.ts @@ -38,34 +38,23 @@ export interface YamlScalarTarget { /** * The input quotes (double quotes or single quotes) */ - sourceType: string; + sourceType: Scalar.Type; + /** + * The parsed yaml document + */ + yamlDoc: Document; + /** + * The position range of the node in the document + */ + nodePostionRange: number[]; } -export interface YamlVisitScalarNode { +interface YamlVisitScalarNode { key: number | "key" | "value" | null; n: Scalar; path: readonly YamlNodePathSegment[]; } -export function getYamlDocScalarNode(document: TextDocument): YamlVisitScalarNode | undefined { - const content = document.getText(); - const yamlDoc = parseDocument(content, { - keepSourceTokens: true, - }); - - let found = undefined; - visit(yamlDoc, { - Node: (key, n, path) => { - if (isScalar(n)) { - found = { key, n, path }; - return visit.BREAK; - } - return undefined; - }, - }); - return found; -} - export function resolveYamlScalarTarget( document: TextDocument, position: Position, @@ -108,8 +97,10 @@ export function resolveYamlScalarTarget( path: [""], type: "key", source: "", - sourceType: "", + sourceType: "PLAIN", siblings: rootProperties, + yamlDoc, + nodePostionRange: [], }; } for (let i = position.line - 1; i >= 0; i--) { @@ -138,7 +129,7 @@ export function resolveYamlScalarTarget( }); return undefined; } - const yp = createYamlPathFromVisitScalarNode(found, pos, log); + const yp = createYamlPathFromVisitScalarNode(found, pos, log, yamlDoc); if (!yp || yp.path.length === 0) { log({ level: "debug", @@ -155,8 +146,10 @@ export function resolveYamlScalarTarget( path: [...yp.path.slice(0, yp.path.length - 1), ""], type: "key", source: "", - sourceType: "", + sourceType: "PLAIN", siblings: [...yp.siblings, yp.source], + yamlDoc, + nodePostionRange: yp.nodePostionRange ?? [], }; } break; @@ -184,7 +177,7 @@ export function resolveYamlScalarTarget( isMap(last.value) || (isScalar(last.value) && isWhitespaceStringOrUndefined(last.value.source))) ) { - const yp = createYamlPathFromVisitScalarNode(found, pos, log); + const yp = createYamlPathFromVisitScalarNode(found, pos, log, yamlDoc); if (!yp || yp.path.length === 0) { log({ level: "debug", @@ -197,10 +190,12 @@ export function resolveYamlScalarTarget( path: [...yp.path, ""], type: "key", source: "", - sourceType: "", + sourceType: "PLAIN", siblings: isMap(last.value) ? (last.value?.items.map((item) => (item.key as any).source ?? "") ?? []) : [], + yamlDoc, + nodePostionRange: yp.nodePostionRange ?? [], }; } break; @@ -216,7 +211,7 @@ export function resolveYamlScalarTarget( }); return undefined; } - const yp = createYamlPathFromVisitScalarNode(found, pos, log); + const yp = createYamlPathFromVisitScalarNode(found, pos, log, yamlDoc); return yp; } } @@ -225,6 +220,7 @@ function createYamlPathFromVisitScalarNode( info: YamlVisitScalarNode, offset: number, log: (log: ServerLog) => void, + yamlDoc: Document, ): YamlScalarTarget | undefined { const { key, n, path: nodePath } = info; if (nodePath.length === 0) { @@ -263,8 +259,10 @@ function createYamlPathFromVisitScalarNode( path: [], type: key === null ? "key" : "value", source: n.source ?? "", - sourceType: n.type ?? "", + sourceType: n.type ?? "PLAIN", siblings: [], + yamlDoc, + nodePostionRange: n.range ?? [], }; } else if (isPair(last)) { if (nodePath.length < 2) { @@ -284,8 +282,10 @@ function createYamlPathFromVisitScalarNode( path, type: "key", source: n.source ?? "", - sourceType: n.type ?? "", + sourceType: n.type ?? "PLAIN", siblings: [], + yamlDoc, + nodePostionRange: n.range ?? [], }; } else { const parent = nodePath.length >= 2 ? nodePath[nodePath.length - 2] : undefined; @@ -298,7 +298,9 @@ function createYamlPathFromVisitScalarNode( type: key === "key" ? "key" : "value", source: n.source ?? "", siblings: targetSiblings, - sourceType: n.type ?? "", + sourceType: n.type ?? "PLAIN", + yamlDoc, + nodePostionRange: n.range ?? [], }; } } else if (isSeq(last)) { @@ -306,10 +308,12 @@ function createYamlPathFromVisitScalarNode( path: path, type: "arr-item", source: n.source ?? "", - sourceType: n.type ?? "", + sourceType: n.type ?? "PLAIN", siblings: last.items .filter((i) => i !== n) .map((item) => (isScalar(item) ? (item.source ?? "") : "")), + yamlDoc, + nodePostionRange: n.range ?? [], }; } else { log({ From 3780210dfb2c3baef0d411f7f3da47cbe9486e1c Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Mon, 16 Dec 2024 17:32:18 +0800 Subject: [PATCH 16/25] updated and add test --- packages/compiler/src/server/lib-provider.ts | 2 +- packages/compiler/src/server/serverlib.ts | 1 + .../src/server/tspconfig/completion.ts | 283 +++++++----------- packages/compiler/src/server/yaml-resolver.ts | 29 +- .../test/server/completion.tspconfig.test.ts | 242 ++++++++++++++- .../compiler/test/server/workspace/.gitignore | 4 + .../test/server/workspace/demo_tsp/test1.tsp | 0 .../test/server/workspace/demo_tsp/test3.tsp | 0 .../workspace/demo_tsp/tspconfigtest1.yaml | 0 .../test/server/workspace/demo_yaml/test2.tsp | 0 .../workspace/demo_yaml/tspconfigtest2.yaml | 0 .../node_modules/fake-emitter/lib/index.js | 6 +- .../fake-linter-no-schema/lib/index.js | 5 + .../fake-linter-no-schema/package.json | 33 ++ .../node_modules/fake-linter/lib/index.js | 24 ++ .../node_modules/fake-linter/package.json | 33 ++ .../test/server/workspace/package.json | 4 +- .../test/server/workspace/tspconfigtest0.yaml | 0 18 files changed, 469 insertions(+), 197 deletions(-) create mode 100644 packages/compiler/test/server/workspace/demo_tsp/test1.tsp create mode 100644 packages/compiler/test/server/workspace/demo_tsp/test3.tsp create mode 100644 packages/compiler/test/server/workspace/demo_tsp/tspconfigtest1.yaml create mode 100644 packages/compiler/test/server/workspace/demo_yaml/test2.tsp create mode 100644 packages/compiler/test/server/workspace/demo_yaml/tspconfigtest2.yaml create mode 100644 packages/compiler/test/server/workspace/node_modules/fake-linter-no-schema/lib/index.js create mode 100644 packages/compiler/test/server/workspace/node_modules/fake-linter-no-schema/package.json create mode 100644 packages/compiler/test/server/workspace/node_modules/fake-linter/lib/index.js create mode 100644 packages/compiler/test/server/workspace/node_modules/fake-linter/package.json create mode 100644 packages/compiler/test/server/workspace/tspconfigtest0.yaml diff --git a/packages/compiler/src/server/lib-provider.ts b/packages/compiler/src/server/lib-provider.ts index bc4d2e9b1b..746fa83dc4 100644 --- a/packages/compiler/src/server/lib-provider.ts +++ b/packages/compiler/src/server/lib-provider.ts @@ -5,7 +5,7 @@ export class LibraryProvider { private libPackageFilterResultCache = new Map(); constructor( private npmPackageProvider: NpmPackageProvider, - private filter: (obj: Record) => boolean, + private filter: (libExports: Record) => boolean, ) {} /** diff --git a/packages/compiler/src/server/serverlib.ts b/packages/compiler/src/server/serverlib.ts index c942f39c68..b071526ee9 100644 --- a/packages/compiler/src/server/serverlib.ts +++ b/packages/compiler/src/server/serverlib.ts @@ -710,6 +710,7 @@ export function createServer(host: ServerHost): Server { if (doc) { const items = await provideTspconfigCompletionItems(doc, params.position, { fileService, + compilerHost, emitterProvider, linterProvider, log, diff --git a/packages/compiler/src/server/tspconfig/completion.ts b/packages/compiler/src/server/tspconfig/completion.ts index 92cb77219d..a41c7818ae 100644 --- a/packages/compiler/src/server/tspconfig/completion.ts +++ b/packages/compiler/src/server/tspconfig/completion.ts @@ -1,5 +1,3 @@ -import * as fs from "fs/promises"; -import * as sysPath from "path"; import { TextDocument } from "vscode-languageserver-textdocument"; import { CompletionItem, @@ -10,8 +8,14 @@ import { } from "vscode-languageserver/node.js"; import { Document, isMap, isPair, Node } from "yaml"; import { emitterOptionsSchema, TypeSpecConfigJsonSchema } from "../../config/config-schema.js"; -import { getDirectoryPath, joinPaths } from "../../core/path-utils.js"; import { + getDirectoryPath, + getNormalizedAbsolutePath, + joinPaths, + normalizeSlashes, +} from "../../core/path-utils.js"; +import { + CompilerHost, DiagnosticMessages, JSONSchemaType, LinterRuleDefinition, @@ -29,31 +33,28 @@ export async function provideTspconfigCompletionItems( tspConfigPosition: Position, context: { fileService: FileService; + compilerHost: CompilerHost; emitterProvider: LibraryProvider; linterProvider: LibraryProvider; log: (log: ServerLog) => void; }, ): Promise { - const { fileService, emitterProvider, linterProvider, log } = context; + const { fileService, compilerHost, emitterProvider, linterProvider, log } = context; const target = resolveYamlScalarTarget(tspConfigDoc, tspConfigPosition, log); if (target === undefined) { return []; } // Variable interpolation - const content = tspConfigDoc.getText(); - const lines = content.split("\n"); - const targetLine = lines[tspConfigPosition.line]; const variableInterpolationItems = resolveVariableInterpolationCompleteItems( - target, - tspConfigPosition, - targetLine, + target.yamlDoc, + target.path, + tspConfigDoc.getText().slice(target.TextRange[0], target.curPos), ); if (variableInterpolationItems.length > 0) { return variableInterpolationItems; } - const pos = tspConfigDoc.offsetAt(tspConfigPosition); const items = resolveTspConfigCompleteItems( await fileService.getPath(tspConfigDoc), target, @@ -68,7 +69,7 @@ export async function provideTspconfigCompletionItems( tspConfigPosition: Position, log: (log: ServerLog) => void, ): Promise { - const { path: nodePath, type: targetType, siblings, sourceType, source } = target; + const { path: nodePath, type: targetType, siblings, source } = target; const CONFIG_PATH_LENGTH_FOR_EMITTER_LIST = 2; const CONFIG_PATH_LENGTH_FOR_LINTER_LIST = 2; const CONFIG_PATH_LENGTH_FOR_EXTENDS = 1; @@ -88,16 +89,11 @@ export async function provideTspconfigCompletionItems( if (!siblings.includes(name)) { const item = createContainingQuatedValCompetionItem( name, - sourceType, - source, (await pkg.getPackageJsonData())?.description ?? `Emitter from ${name}`, tspConfigPosition, - target.nodePostionRange, - pos, + target, ); - if (item !== undefined) { - items.push(item); - } + items.push(item); } } return items; @@ -142,16 +138,11 @@ export async function provideTspconfigCompletionItems( if (!siblings.includes(labelName)) { const item = createContainingQuatedValCompetionItem( labelName, - sourceType, - source, (await pkg.getPackageJsonData())?.description ?? `Linters from ${labelName}`, tspConfigPosition, - target.nodePostionRange, - pos, + target, ); - if (item !== undefined) { - items.push(item); - } + items.push(item); } } continue; @@ -161,16 +152,11 @@ export async function provideTspconfigCompletionItems( if (!siblings.includes(name)) { const item = createContainingQuatedValCompetionItem( name, - sourceType, - source, (await pkg.getPackageJsonData())?.description ?? `Linters from ${name}`, tspConfigPosition, - target.nodePostionRange, - pos, + target, ); - if (item !== undefined) { - items.push(item); - } + items.push(item); } } } else if (extendKeyWord === "enable" || extendKeyWord === "disable") { @@ -185,16 +171,11 @@ export async function provideTspconfigCompletionItems( const labelName = `${name}/${rule.name}`; const item = createContainingQuatedValCompetionItem( labelName, - sourceType, - source, rule.description, tspConfigPosition, - target.nodePostionRange, - pos, + target, ); - if (item !== undefined) { - items.push(item); - } + items.push(item); } } } @@ -209,17 +190,25 @@ export async function provideTspconfigCompletionItems( const currentFolder = getDirectoryPath(tspConfigFile); const newFolderPath = joinPaths(currentFolder, source); + // Exclude the current yaml configuration file const relativeFiles = await findFilesOrDirsWithSameExtension( + compilerHost, newFolderPath, ".yaml", - sysPath.resolve(tspConfigFile), + [normalizeSlashes(tspConfigFile)], ); return getFilePathCompletionItems(relativeFiles, siblings); } else if (nodePath.length >= CONFIG_PATH_LENGTH_FOR_IMPORTS && nodePath[0] === "imports") { const currentFolder = getDirectoryPath(tspConfigFile); const newFolderPath = joinPaths(currentFolder, source); - const relativeFiles = await findFilesOrDirsWithSameExtension(newFolderPath, ".tsp"); + // Exclude main.tsp files that are the same as the current yaml configuration directory + const relativeFiles = await findFilesOrDirsWithSameExtension( + compilerHost, + newFolderPath, + ".tsp", + [joinPaths(currentFolder, "main.tsp")], + ); return getFilePathCompletionItems(relativeFiles, siblings); } else { const schema = TypeSpecConfigJsonSchema; @@ -356,121 +345,75 @@ export async function provideTspconfigCompletionItems( } /** - * Get the new text value - * @param sourceQuoteType input quote type(single, double, none) - * @param formatText format text value - * @returns new text value - */ -function getCompletionItemInsertedValue(sourceQuoteType: string, formatText: string): string { - let newText: string = ""; - if (sourceQuoteType === "QUOTE_SINGLE") { - newText = `${formatText}'`; - } else if (sourceQuoteType === "QUOTE_DOUBLE") { - newText = `${formatText}"`; - } else { - newText = `"${formatText}"`; - } - return newText; -} - -/** - * Get the full completion item value and edit position - * @param newText the new text - * @param source source text - * @param nodePosRange current node position range, [startPos, endPos, nodeEndPos] - * @param curPos current cursor position - * @param tspConfigPosition original position, see {@link Position} - * @returns TextEdit object or undefined - */ -function getFullCompletionItemValAndEditPosition( - newText: string, - source: string, - nodePosRange: number[], - curPos: number, - tspConfigPosition: Position, -): TextEdit | undefined { - const [startPos, endPos] = nodePosRange; - - if (curPos >= startPos && curPos <= endPos) { - return TextEdit.replace( - Range.create( - Position.create(tspConfigPosition.line, tspConfigPosition.character - source.length), - Position.create(tspConfigPosition.line, tspConfigPosition.character + (endPos - curPos)), - ), - newText, - ); - } - return undefined; -} - -/** - * Get the common CompletionItem object, which value is included in quotes + * Create the common CompletionItem object, which value is included in quotes * @param labelName The value of the label attribute of the CompletionItem object - * @param sourceType Input quote type - * @param source Entered text * @param description The value of the documentation attribute of the CompletionItem object - * @param tspConfigPosition Input location object, see {@link Position} - * @param nodePosRange current node position range, [startPos, endPos, nodeEndPos] - * @param curPos current cursor position - * @returns CompletionItem object or undefined + * @param tspConfigPosition Input current line location object, see {@link Position} + * @param target The target object of the current configuration file, see {@link YamlScalarTarget} + * @returns CompletionItem object */ function createContainingQuatedValCompetionItem( labelName: string, - sourceType: string, - source: string, description: string, tspConfigPosition: Position, - nodePosRange: number[], - curPos: number, -): CompletionItem | undefined { - // Generate new text - const newText = getCompletionItemInsertedValue(sourceType, labelName); - const textEdit = getFullCompletionItemValAndEditPosition( - newText, - source, - nodePosRange, - curPos, - tspConfigPosition, - ); - if (textEdit !== undefined) { + target: YamlScalarTarget, +): CompletionItem { + const [startPos, endPos] = target.TextRange; + + if (target.curPos >= startPos && target.curPos <= endPos) { return { label: labelName, kind: CompletionItemKind.Field, documentation: description, - textEdit: textEdit, + textEdit: TextEdit.replace( + Range.create( + Position.create( + tspConfigPosition.line, + tspConfigPosition.character - target.source.length, + ), + Position.create(tspConfigPosition.line, tspConfigPosition.character), + ), + target.sourceType === "QUOTE_SINGLE" || target.sourceType === "QUOTE_DOUBLE" + ? labelName + : `"${labelName}"`, + ), }; + } else { + return { label: "" }; } - return undefined; } /** * Find a set of dirs/files with the same suffix + * @param compilerHost CompilerHost object for file operations, see {@link CompilerHost} * @param rootPath The absolute input path or relative path of the current configuration file - * @param fileExtension File extension, such as ".yaml" or ".tsp" - * @param configFile absolute path of the current configuration file - * @returns dir/file array + * @param fileExtension File extension + * @param excludeFiles exclude files array, default is [] + * @returns dirs/files array */ async function findFilesOrDirsWithSameExtension( + compilerHost: CompilerHost, rootPath: string, - fileExtension: string, - configFile: string = "", + fileExtension: ".yaml" | ".tsp", + excludeFiles: string[] = [], ): Promise { const exclude = ["node_modules", "tsp-output", ".vs", ".vscode"]; const files: string[] = []; try { // When reading the content under the path, an error may be reported if the path is incorrect. - // Exclude the current configuration file, compare in absolute paths - (await fs.readdir(rootPath, { withFileTypes: true })) - .filter( - (d) => - (d.isDirectory() || (d.isFile() && d.name.endsWith(fileExtension))) && - !exclude.includes(d.name) && - sysPath.resolve(d.name) !== configFile, - ) - .map((d) => { - files.push(d.name); - }); + const dirs = await compilerHost.readDir(rootPath); + for (const d of dirs) { + if ( + ((await compilerHost.stat(joinPaths(rootPath, d))).isDirectory() || + ((await compilerHost.stat(joinPaths(rootPath, d))).isFile() && + d.endsWith(fileExtension))) && + !exclude.includes(d) && + !excludeFiles.includes(getNormalizedAbsolutePath(d, rootPath)) + ) { + files.push(d); + } + } } catch { return files; } @@ -501,34 +444,42 @@ function getFilePathCompletionItems(relativeFiles: string[], siblings: string[]) /** * resolve variable interpolation completion items - * @param target YamlScalarTarget object, contains the information of the target node - * @param tspConfigPosition current cursor position in the configuration file - * @param targetLine current line content of the target node + * @param yamlDocNodes tsp config yaml document nodes , see {@link Document} + * @param path current path of the target node, such as ["linter", "extends","- ┆"] + * @param curText current editing text, from startPos to curPos * @returns variable interpolation completion items */ function resolveVariableInterpolationCompleteItems( - target: YamlScalarTarget, - tspConfigPosition: Position, - targetLine: string, + yamlDocNodes: Document, + path: string[], + curText: string, ): CompletionItem[] { - const yamlDocNodes = target.yamlDoc; - - if (target.sourceType === "QUOTE_DOUBLE" || target.sourceType === "QUOTE_SINGLE") { - const targetPos = targetLine.lastIndexOf("{"); - const curText = targetLine.substring(targetPos, tspConfigPosition.character); - - if (/{[^}]*env\.[^}]*$/.test(curText)) { - // environment-variables - return getCompletionItemsByFilter( - yamlDocNodes, - "environment-variables", - "Custom environment variables", - ); - } else if (/{[^}]*$/.test(curText)) { - // parameters and built-in variables - const result = [ - ...getCompletionItemsByFilter(yamlDocNodes, "parameters", "Custom paramters variables"), - ...["cwd", "project-root"].map((value) => { + if (/{ *env\./g.test(curText)) { + // environment-variables + return getVariableCompletionItem( + yamlDocNodes, + "environment-variables", + "Custom environment variables", + ); + } else if (/{[^}]*$/.test(curText)) { + // parameters and built-in variables + const result = [ + ...getVariableCompletionItem(yamlDocNodes, "parameters", "Custom paramters variables"), + ...["cwd", "project-root"].map((value) => { + const item: CompletionItem = { + label: value, + kind: CompletionItemKind.Value, + documentation: "Built-in variables", + }; + return item; + }), + ]; + + // if the current path is options, add output-dir and emitter-name + const CONFIG_PATH_LENGTH_FOR_OPTIONS = 2; + if (path.length > CONFIG_PATH_LENGTH_FOR_OPTIONS && path[0] === "options") { + result.push( + ...["output-dir", "emitter-name"].map((value) => { const item: CompletionItem = { label: value, kind: CompletionItemKind.Value, @@ -536,23 +487,9 @@ function resolveVariableInterpolationCompleteItems( }; return item; }), - ]; - - // if the current path is options, add output-dir and emitter-name - if (target.path.length > 2 && target.path[0] === "options") { - result.push( - ...["output-dir", "emitter-name"].map((value) => { - const item: CompletionItem = { - label: value, - kind: CompletionItemKind.Value, - documentation: "Built-in variables", - }; - return item; - }), - ); - } - return result; + ); } + return result; } return []; @@ -561,13 +498,13 @@ function resolveVariableInterpolationCompleteItems( /** * Get the corresponding CompletionItem array based on the filter name * @param yamlDoc The contents of the YAML configuration file - * @param filterName The filter name,such as "parameters" or "environment-variables" + * @param filterName The filter name * @param description The description of the CompletionItem object * @returns CompletionItem object array */ -function getCompletionItemsByFilter( +function getVariableCompletionItem( yamlDoc: Document, - filterName: string, + filterName: "parameters" | "environment-variables", description: string, ): CompletionItem[] { const result: CompletionItem[] = []; diff --git a/packages/compiler/src/server/yaml-resolver.ts b/packages/compiler/src/server/yaml-resolver.ts index f7522a6d33..f666810ef5 100644 --- a/packages/compiler/src/server/yaml-resolver.ts +++ b/packages/compiler/src/server/yaml-resolver.ts @@ -44,9 +44,13 @@ export interface YamlScalarTarget { */ yamlDoc: Document; /** - * The position range of the node in the document + * The position range of the text in the document, such as [startPos, endPos, _endNodePos] */ - nodePostionRange: number[]; + TextRange: number[]; + /** + * The cursor current position + */ + curPos: number; } interface YamlVisitScalarNode { @@ -100,7 +104,8 @@ export function resolveYamlScalarTarget( sourceType: "PLAIN", siblings: rootProperties, yamlDoc, - nodePostionRange: [], + TextRange: [], + curPos: pos, }; } for (let i = position.line - 1; i >= 0; i--) { @@ -149,7 +154,8 @@ export function resolveYamlScalarTarget( sourceType: "PLAIN", siblings: [...yp.siblings, yp.source], yamlDoc, - nodePostionRange: yp.nodePostionRange ?? [], + TextRange: yp.TextRange ?? [], + curPos: pos, }; } break; @@ -195,7 +201,8 @@ export function resolveYamlScalarTarget( ? (last.value?.items.map((item) => (item.key as any).source ?? "") ?? []) : [], yamlDoc, - nodePostionRange: yp.nodePostionRange ?? [], + TextRange: yp.TextRange ?? [], + curPos: pos, }; } break; @@ -262,7 +269,8 @@ function createYamlPathFromVisitScalarNode( sourceType: n.type ?? "PLAIN", siblings: [], yamlDoc, - nodePostionRange: n.range ?? [], + TextRange: n.range ?? [], + curPos: offset, }; } else if (isPair(last)) { if (nodePath.length < 2) { @@ -285,7 +293,8 @@ function createYamlPathFromVisitScalarNode( sourceType: n.type ?? "PLAIN", siblings: [], yamlDoc, - nodePostionRange: n.range ?? [], + TextRange: n.range ?? [], + curPos: offset, }; } else { const parent = nodePath.length >= 2 ? nodePath[nodePath.length - 2] : undefined; @@ -300,7 +309,8 @@ function createYamlPathFromVisitScalarNode( siblings: targetSiblings, sourceType: n.type ?? "PLAIN", yamlDoc, - nodePostionRange: n.range ?? [], + TextRange: n.range ?? [], + curPos: offset, }; } } else if (isSeq(last)) { @@ -313,7 +323,8 @@ function createYamlPathFromVisitScalarNode( .filter((i) => i !== n) .map((item) => (isScalar(item) ? (item.source ?? "") : "")), yamlDoc, - nodePostionRange: n.range ?? [], + TextRange: n.range ?? [], + curPos: offset, }; } else { log({ diff --git a/packages/compiler/test/server/completion.tspconfig.test.ts b/packages/compiler/test/server/completion.tspconfig.test.ts index 2f3a12dd6f..ad9c910d07 100644 --- a/packages/compiler/test/server/completion.tspconfig.test.ts +++ b/packages/compiler/test/server/completion.tspconfig.test.ts @@ -71,12 +71,28 @@ describe("Test completion items for options and emitters", () => { it.each([ { config: `emit:\n - ┆`, + expected: ['"fake-emitter"', '"fake-emitter-no-schema"'], + }, + { + config: `emit:\n - "┆"`, expected: ["fake-emitter", "fake-emitter-no-schema"], }, { - config: `options:\n\n fak┆`, + config: `emit:\n - "┆`, + expected: ["fake-emitter", "fake-emitter-no-schema"], + }, + { + config: `emit:\n - '┆`, + expected: ["fake-emitter", "fake-emitter-no-schema"], + }, + { + config: `emit:\n - '┆'`, expected: ["fake-emitter", "fake-emitter-no-schema"], }, + { + config: `options:\n\n fak┆`, + expected: ['"fake-emitter"', '"fake-emitter-no-schema"'], + }, ])("#%# Test emitters: $config", async ({ config, expected }) => { await checkCompletionItems(config, true, expected); }); @@ -167,7 +183,27 @@ describe("Test completion items for emitters options", () => { expected: ["a", "b", "c"], }, ])("#%# Test emitter options: $config", async ({ config, expected }) => { - await checkCompletionItems(config, true, expected, "./subfolder/tspconfig.yaml"); + await checkCompletionItems(config, true, expected, false, "./subfolder/tspconfig.yaml"); + }); +}); + +describe("Test whether the completion items description of the emitters options is optional or required", () => { + it.each([ + { + config: `options:\n fake-emitter:\n ┆`, + expected: [ + "[required]\nThe name of the target to emit to.", //"target-name", + "[optional]\nWhether the target is valid.", //"is-valid", + "[required]\n", //"type", + "[optional]\n", //"emitter-output-dir", + "[optional]\n", //"options", + "[optional]\n", //"options-b", + "[optional]\n", //"options-arr-obj", + "[optional]\n", //"options-arr-boolean", + ], + }, + ])("#%# Test emitter options: $config", async ({ config, expected }) => { + await checkCompletionItems(config, true, expected, true, "./subfolder/tspconfig.yaml"); }); }); @@ -209,23 +245,43 @@ describe("Test completion items for linter", () => { expected: ["extends", "disable"], }, { - config: `linter:\n enable:\n linter-one: ┆`, - expected: ["true", "false"], + config: `linter:\n extends:\n - "┆`, + expected: ["fake-linter-no-schema", "fake-linter/recommended"], + }, + { + config: `linter:\n extends:\n - "fake-linter/recommended"\n - "┆`, + expected: ["fake-linter-no-schema"], }, { - config: `linter:\n enable:\n linter-one┆:`, + config: `linter:\n extends:\n - "fake-linter/recommended"\n enable:\n "┆`, + expected: ["fake-linter/casing", "fake-linter/no-model-doc", "fake-linter/testing"], + }, + { + config: `linter:\n extends:\n - "fake-linter/recommended"\n disable:\n "┆`, + expected: ["fake-linter/casing", "fake-linter/no-model-doc", "fake-linter/testing"], + }, + { + config: `linter:\n extends:\n - "fake-linter/recommended"\n - "fake-linter-no-schema"\n enable:\n "┆`, + expected: ["fake-linter/casing", "fake-linter/no-model-doc", "fake-linter/testing"], + }, + { + config: `linter:\n extends:\n - "fake-linter/recommended"\n enable:\n "fake-linter/casing": true\n disable:\n "┆`, + expected: ["fake-linter/casing", "fake-linter/no-model-doc", "fake-linter/testing"], + }, + { + config: `linter:\n extends:\n - "fake-linter-no-schema" - "fake-linter/recommended"┆`, expected: [], }, { - config: `linter:\n disable:\n linter-one: true\n ┆`, + config: `linter:\n extends:\n enable: "fak┆e"`, expected: [], }, { - config: `linter:\n disable:\n linter-one: true\n linter-two: ┆`, + config: `linter:\n extends:\n - "fake-linter-no-schema" - "fake"┆`, expected: [], }, - ])("#%# Test linter: $config", async ({ config, expected }) => { - await checkCompletionItems(config, true, expected); + ])("#%# Test emitter options: $config", async ({ config, expected }) => { + await checkCompletionItems(config, true, expected, false, "./subfolder/tspconfig.yaml"); }); }); @@ -252,6 +308,169 @@ describe("Test completion items for additionalProperties", () => { }); }); +describe("Test completion items that use parameters and environment variables and built-in variables", () => { + it.each([ + { + config: `environment-variables:\n BASE_DIR:\n default: "{cwd}"\n test-env:\n default: ""\noutput-dir: "{env.┆}"`, + expected: ["BASE_DIR", "test-env"], + }, + { + config: `environment-variables:\n BASE_DIR:\n default: "{cwd}"\n test-env:\n default: ""\noutput-dir: "{ env.┆}"`, + expected: ["BASE_DIR", "test-env"], + }, + { + config: `environment-variables:\n BASE_DIR:\n default: "{cwd}"\n test-env:\n default: ""\noutput-dir: "outdir/{env.┆}"`, + expected: ["BASE_DIR", "test-env"], + }, + { + config: `environment-variables:\n BASE_DIR:\n default: "{cwd}"\n test-env:\n default: ""\noutput-dir: "outdir/{env.┆}/myDir"`, + expected: ["BASE_DIR", "test-env"], + }, + { + config: `environment-variables:\n BASE_DIR:\n default: "{cwd}"\n test-env:\n default: ""\noutput-dir: "{}env.┆}"`, + expected: [], + }, + { + config: `environment-variables:\n BASE_DIR:\n default: "{cwd}"\n test-env:\n default: ""\noutput-dir: "{ abcenv.┆}"`, + expected: ["cwd", "project-root"], + }, + { + config: `environment-variables:\n BASE_DIR:\n default: "{cwd}"\n test-env:\n default: ""\noutput-dir: ┆"{ env.}"`, + expected: [], + }, + { + config: `parameters:\n base-dir:\n default: "{cwd}"\n test-param: default: ""\noutput-dir: "{┆}"`, + expected: ["cwd", "project-root", "base-dir", "test-param"], + }, + { + config: `parameters:\n base-dir:\n default: "{cwd}"\n test-param: default: ""\noutput-dir: "{cw┆}"`, + expected: ["cwd", "project-root", "base-dir", "test-param"], + }, + { + config: `environment-variables:\n BASE_DIR:\n default: "{cwd}"\n test-env:\n default: ""\nparameters:\n base-dir:\n default: "{cwd}"\n test-param: default: ""\noutput-dir: "{env.┆}"`, + expected: ["BASE_DIR", "test-env"], + }, + { + config: `parameters:\n base-dir:\n default: "{cwd}"\n test-param: default: ""\noutput-dir: "test/{cw┆}"`, + expected: ["cwd", "project-root", "base-dir", "test-param"], + }, + { + config: `parameters:\n base-dir:\n default: "{cwd}"\n test-param: default: ""\noutput-dir: "outDir/{cw┆}/myDir"`, + expected: ["cwd", "project-root", "base-dir", "test-param"], + }, + { + config: `parameters:\n base-dir:\n default: "{cwd}"\n test-param: default: ""\noutput-dir: "{env.┆}"`, + expected: [], + }, + { + config: `parameters:\n base-dir:\n default: "{cwd}"\n test-param: default: ""\noutput-dir: "{}┆"`, + expected: [], + }, + { + config: `parameters:\n base-dir:\n default: "{cwd}"\n test-param: default: ""\noutput-dir: ┆"{}"`, + expected: [], + }, + ])("#%# Test addProp: $config", async ({ config, expected }) => { + await checkCompletionItems(config, false, expected); + }); +}); + +describe("Test completion items for extends", () => { + it.each([ + { + config: `extends: "┆`, + expected: ["tspconfigtest0.yaml", "demo_yaml", "demo_tsp"], + }, + { + config: `extends: "./┆`, + expected: ["tspconfigtest0.yaml", "demo_yaml", "demo_tsp"], + }, + { + config: `extends: "./demo_yaml┆"`, + expected: ["tspconfigtest2.yaml"], + }, + { + config: `extends: "Z:/test/workspace┆"`, + expected: ["tspconfigtest0.yaml", "demo_yaml", "demo_tsp"], + }, + { + config: `extends: \n┆`, + expected: [ + "emit", + "environment-variables", + "imports", + "linter", + "options", + "output-dir", + "parameters", + "trace", + "warn-as-error", + ], + }, + { + config: `extends: "./demo┆"`, + expected: [], + }, + { + config: `extends: "./tspconfigtest0.yaml"┆`, + expected: [], + }, + { + config: `extends: "./┆demo"`, + expected: [], + }, + { + config: `extends: "Z:/test/workspace/demo┆"`, + expected: [], + }, + ])("#%# Test addProp: $config", async ({ config, expected }) => { + await checkCompletionItems(config, true, expected); + }); +}); + +describe("Test completion items for imports", () => { + it.each([ + { + config: `imports:\n - "./┆`, + expected: ["demo_yaml", "demo_tsp"], + }, + { + config: `imports:\n - "./demo_tsp"\n - "./┆`, + expected: ["demo_yaml", "demo_tsp"], + }, + { + config: `imports:\n - "./demo_tsp/┆`, + expected: ["test1.tsp", "test3.tsp"], + }, + { + config: `imports:\n - "./demo_tsp/test1.tsp"\n - "./demo_tsp/┆`, + expected: ["test1.tsp", "test3.tsp"], + }, + { + config: `imports:\n - "Z:/test/workspace/┆`, + expected: ["demo_yaml", "demo_tsp"], + }, + { + config: `imports:\n - "┆./demo"`, + expected: [], + }, + { + config: `imports:\n - "Z:/test/workspace/demo┆`, + expected: [], + }, + { + config: `imports:\n - "./demo_tsp/test┆`, + expected: [], + }, + { + config: `imports:\n "./┆"`, + expected: [], + }, + ])("#%# Test addProp: $config", async ({ config, expected }) => { + await checkCompletionItems(config, true, expected); + }); +}); + describe("Test completion items with comments", () => { it.each([ { @@ -428,11 +647,14 @@ async function checkCompletionItems( configWithPosition: string, includeWorkspace: boolean, expected: string[], + isTestDesc: boolean = false, tspconfigPathUnderWorkspace: string = "./tspconfig.yaml", ) { const items = (await complete(configWithPosition, includeWorkspace, tspconfigPathUnderWorkspace)) .items; - expect(items.map((i) => i.label).sort()).toEqual(expected.sort()); + isTestDesc + ? expect(items.map((i) => i.documentation ?? "").sort()).toEqual(expected.sort()) + : expect(items.map((i) => i.textEdit?.newText ?? i.label).sort()).toEqual(expected.sort()); } async function complete( diff --git a/packages/compiler/test/server/workspace/.gitignore b/packages/compiler/test/server/workspace/.gitignore index d994bdc059..8f513e6006 100644 --- a/packages/compiler/test/server/workspace/.gitignore +++ b/packages/compiler/test/server/workspace/.gitignore @@ -1,3 +1,7 @@ # not ignore here which is for testing !node_modules/ +!demo_tsp/ +!demo_yaml/ !**/package.json +!tspconfigtest0.yaml + \ No newline at end of file diff --git a/packages/compiler/test/server/workspace/demo_tsp/test1.tsp b/packages/compiler/test/server/workspace/demo_tsp/test1.tsp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/compiler/test/server/workspace/demo_tsp/test3.tsp b/packages/compiler/test/server/workspace/demo_tsp/test3.tsp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/compiler/test/server/workspace/demo_tsp/tspconfigtest1.yaml b/packages/compiler/test/server/workspace/demo_tsp/tspconfigtest1.yaml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/compiler/test/server/workspace/demo_yaml/test2.tsp b/packages/compiler/test/server/workspace/demo_yaml/test2.tsp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/compiler/test/server/workspace/demo_yaml/tspconfigtest2.yaml b/packages/compiler/test/server/workspace/demo_yaml/tspconfigtest2.yaml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/compiler/test/server/workspace/node_modules/fake-emitter/lib/index.js b/packages/compiler/test/server/workspace/node_modules/fake-emitter/lib/index.js index 99190288ed..993eca2d5f 100644 --- a/packages/compiler/test/server/workspace/node_modules/fake-emitter/lib/index.js +++ b/packages/compiler/test/server/workspace/node_modules/fake-emitter/lib/index.js @@ -3,8 +3,8 @@ const EmitterOptionsSchema = { type: "object", additionalProperties: false, properties: { - "target-name": { type: "string", nullable: true }, - "is-valid": { type: "boolean", nullable: true }, + "target-name": { type: "string", nullable: true, description: "The name of the target to emit to." }, + "is-valid": { type: "boolean", nullable: true, description: "Whether the target is valid." }, "type": { type: "string", nullable: true, enum: ["a", "b", "c"] }, "options": { type: "object", @@ -61,7 +61,7 @@ const EmitterOptionsSchema = { } } }, - required: [], + required: ["target-name","type"], }; export const $lib = { diff --git a/packages/compiler/test/server/workspace/node_modules/fake-linter-no-schema/lib/index.js b/packages/compiler/test/server/workspace/node_modules/fake-linter-no-schema/lib/index.js new file mode 100644 index 0000000000..7f03c37a5d --- /dev/null +++ b/packages/compiler/test/server/workspace/node_modules/fake-linter-no-schema/lib/index.js @@ -0,0 +1,5 @@ +import { defineLinter } from "@typespec/compiler"; + +export const $linter = defineLinter({ + rules: [] +}); diff --git a/packages/compiler/test/server/workspace/node_modules/fake-linter-no-schema/package.json b/packages/compiler/test/server/workspace/node_modules/fake-linter-no-schema/package.json new file mode 100644 index 0000000000..f2a60f8b9c --- /dev/null +++ b/packages/compiler/test/server/workspace/node_modules/fake-linter-no-schema/package.json @@ -0,0 +1,33 @@ +{ + "name": "fake-linter-no-schema", + "version": "0.1.2", + "main": "lib/index.js", + "dependencies": { + "@readme/openapi-parser": "~2.6.0", + "yaml": "~2.4.5" + }, + "peerDependencies": { + "@typespec/compiler": "~0.60.0", + "@typespec/http": "~0.60.0", + "@typespec/openapi": "~0.60.0", + "@typespec/versioning": "~0.60.0" + }, + "devDependencies": { + "@types/node": "~18.11.19", + "@types/yargs": "~17.0.32", + "@vitest/coverage-v8": "^2.0.4", + "@vitest/ui": "^2.0.4", + "c8": "^10.1.2", + "cross-env": "~7.0.3", + "rimraf": "~6.0.1", + "typescript": "~5.5.4", + "vitest": "^2.0.4", + "@typespec/compiler": "~0.60.0", + "@typespec/http": "~0.60.0", + "@typespec/library-linter": "~0.60.0", + "@typespec/openapi": "~0.60.0", + "@typespec/rest": "~0.60.0", + "@typespec/tspd": "~0.46.0", + "@typespec/versioning": "~0.60.0" + } +} diff --git a/packages/compiler/test/server/workspace/node_modules/fake-linter/lib/index.js b/packages/compiler/test/server/workspace/node_modules/fake-linter/lib/index.js new file mode 100644 index 0000000000..a2d335b063 --- /dev/null +++ b/packages/compiler/test/server/workspace/node_modules/fake-linter/lib/index.js @@ -0,0 +1,24 @@ +import { createRule ,defineLinter } from "@typespec/compiler"; + + +export const requiredDocRule = createRule({ + name: "no-model-doc", + severity: "warning", +}); +export const casingRule = createRule({ + name: "casing", + severity: "warning", +}); +export const testingRule = createRule({ + name: "testing", + severity: "warning", +}); + +export const $linter = defineLinter({ + rules: [requiredDocRule,casingRule,testingRule], + ruleSets: { + recommended: { + enable: {[`fake-linter/${testingRule.name}`]: true} + } + } +}); diff --git a/packages/compiler/test/server/workspace/node_modules/fake-linter/package.json b/packages/compiler/test/server/workspace/node_modules/fake-linter/package.json new file mode 100644 index 0000000000..48e6636c53 --- /dev/null +++ b/packages/compiler/test/server/workspace/node_modules/fake-linter/package.json @@ -0,0 +1,33 @@ +{ + "name": "fake-linter", + "version": "0.0.2", + "main": "lib/index.js", + "dependencies": { + "@readme/openapi-parser": "~2.6.0", + "yaml": "~2.4.5" + }, + "peerDependencies": { + "@typespec/compiler": "~0.60.0", + "@typespec/http": "~0.60.0", + "@typespec/openapi": "~0.60.0", + "@typespec/versioning": "~0.60.0" + }, + "devDependencies": { + "@types/node": "~18.11.19", + "@types/yargs": "~17.0.32", + "@vitest/coverage-v8": "^2.0.4", + "@vitest/ui": "^2.0.4", + "c8": "^10.1.2", + "cross-env": "~7.0.3", + "rimraf": "~6.0.1", + "typescript": "~5.5.4", + "vitest": "^2.0.4", + "@typespec/compiler": "~0.60.0", + "@typespec/http": "~0.60.0", + "@typespec/library-linter": "~0.60.0", + "@typespec/openapi": "~0.60.0", + "@typespec/rest": "~0.60.0", + "@typespec/tspd": "~0.46.0", + "@typespec/versioning": "~0.60.0" + } +} diff --git a/packages/compiler/test/server/workspace/package.json b/packages/compiler/test/server/workspace/package.json index 81d5d56bc4..a117767823 100644 --- a/packages/compiler/test/server/workspace/package.json +++ b/packages/compiler/test/server/workspace/package.json @@ -9,10 +9,12 @@ "author": "", "license": "ISC", "devDependencies": { - "fake-emitter-no-schema": "0.1.2" + "fake-emitter-no-schema": "0.1.2", + "fake-linter-no-schema": "0.1.2" }, "dependencies": { "fake-emitter": "0.0.2", + "fake-linter": "0.0.2", "fake-yaml": "0.0.1", "not-exist": "0.0.1" } diff --git a/packages/compiler/test/server/workspace/tspconfigtest0.yaml b/packages/compiler/test/server/workspace/tspconfigtest0.yaml new file mode 100644 index 0000000000..e69de29bb2 From bfb5b1482d42a7960165406cc68dcf82a19bb57f Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Tue, 17 Dec 2024 16:39:25 +0800 Subject: [PATCH 17/25] updated --- .../src/server/tspconfig/completion.ts | 9 +++++---- packages/compiler/src/server/yaml-resolver.ts | 20 ++++++++++--------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/compiler/src/server/tspconfig/completion.ts b/packages/compiler/src/server/tspconfig/completion.ts index a41c7818ae..617cde65ab 100644 --- a/packages/compiler/src/server/tspconfig/completion.ts +++ b/packages/compiler/src/server/tspconfig/completion.ts @@ -49,7 +49,7 @@ export async function provideTspconfigCompletionItems( const variableInterpolationItems = resolveVariableInterpolationCompleteItems( target.yamlDoc, target.path, - tspConfigDoc.getText().slice(target.TextRange[0], target.curPos), + tspConfigDoc.getText().slice(target.nodePostionRange.pos, target.curPos), ); if (variableInterpolationItems.length > 0) { return variableInterpolationItems; @@ -358,9 +358,10 @@ function createContainingQuatedValCompetionItem( tspConfigPosition: Position, target: YamlScalarTarget, ): CompletionItem { - const [startPos, endPos] = target.TextRange; - - if (target.curPos >= startPos && target.curPos <= endPos) { + if ( + target.curPos >= target.nodePostionRange.pos && + target.curPos <= target.nodePostionRange.end + ) { return { label: labelName, kind: CompletionItemKind.Field, diff --git a/packages/compiler/src/server/yaml-resolver.ts b/packages/compiler/src/server/yaml-resolver.ts index f666810ef5..f65134481b 100644 --- a/packages/compiler/src/server/yaml-resolver.ts +++ b/packages/compiler/src/server/yaml-resolver.ts @@ -12,6 +12,7 @@ import { Scalar, visit, } from "yaml"; +import { TextRange } from "../core/index.js"; import { firstNonWhitespaceCharacterIndex, isWhitespaceStringOrUndefined } from "../utils/misc.js"; import { ServerLog } from "./types.js"; @@ -44,9 +45,9 @@ export interface YamlScalarTarget { */ yamlDoc: Document; /** - * The position range of the text in the document, such as [startPos, endPos, _endNodePos] + * The position range of the text in the document, such as [startPos, endPos], see {@link TextRange} */ - TextRange: number[]; + nodePostionRange: TextRange; /** * The cursor current position */ @@ -104,7 +105,7 @@ export function resolveYamlScalarTarget( sourceType: "PLAIN", siblings: rootProperties, yamlDoc, - TextRange: [], + nodePostionRange: { pos: 0, end: 0 }, curPos: pos, }; } @@ -154,7 +155,7 @@ export function resolveYamlScalarTarget( sourceType: "PLAIN", siblings: [...yp.siblings, yp.source], yamlDoc, - TextRange: yp.TextRange ?? [], + nodePostionRange: yp.nodePostionRange, curPos: pos, }; } @@ -201,7 +202,7 @@ export function resolveYamlScalarTarget( ? (last.value?.items.map((item) => (item.key as any).source ?? "") ?? []) : [], yamlDoc, - TextRange: yp.TextRange ?? [], + nodePostionRange: yp.nodePostionRange, curPos: pos, }; } @@ -260,6 +261,7 @@ function createYamlPathFromVisitScalarNode( } const last = nodePath[nodePath.length - 1]; + const [startPos, endPos] = n.range ?? []; if (isDocument(last)) { // we are at the root and the content is a pure text (otherwise there should be a Map under document node first) return { @@ -269,7 +271,7 @@ function createYamlPathFromVisitScalarNode( sourceType: n.type ?? "PLAIN", siblings: [], yamlDoc, - TextRange: n.range ?? [], + nodePostionRange: { pos: startPos ?? 0, end: endPos ?? 0 }, curPos: offset, }; } else if (isPair(last)) { @@ -293,7 +295,7 @@ function createYamlPathFromVisitScalarNode( sourceType: n.type ?? "PLAIN", siblings: [], yamlDoc, - TextRange: n.range ?? [], + nodePostionRange: { pos: startPos ?? 0, end: endPos ?? 0 }, curPos: offset, }; } else { @@ -309,7 +311,7 @@ function createYamlPathFromVisitScalarNode( siblings: targetSiblings, sourceType: n.type ?? "PLAIN", yamlDoc, - TextRange: n.range ?? [], + nodePostionRange: { pos: startPos ?? 0, end: endPos ?? 0 }, curPos: offset, }; } @@ -323,7 +325,7 @@ function createYamlPathFromVisitScalarNode( .filter((i) => i !== n) .map((item) => (isScalar(item) ? (item.source ?? "") : "")), yamlDoc, - TextRange: n.range ?? [], + nodePostionRange: { pos: startPos ?? 0, end: endPos ?? 0 }, curPos: offset, }; } else { From 92a2540fabc4fb986f76ffe8c8f84c758783f299 Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Thu, 19 Dec 2024 10:28:42 +0800 Subject: [PATCH 18/25] updated and add some testing --- .../src/server/tspconfig/completion.ts | 115 ++++++++++-------- packages/compiler/src/server/yaml-resolver.ts | 53 ++++---- .../test/server/completion.tspconfig.test.ts | 20 ++- 3 files changed, 107 insertions(+), 81 deletions(-) diff --git a/packages/compiler/src/server/tspconfig/completion.ts b/packages/compiler/src/server/tspconfig/completion.ts index 617cde65ab..295ec26065 100644 --- a/packages/compiler/src/server/tspconfig/completion.ts +++ b/packages/compiler/src/server/tspconfig/completion.ts @@ -49,7 +49,7 @@ export async function provideTspconfigCompletionItems( const variableInterpolationItems = resolveVariableInterpolationCompleteItems( target.yamlDoc, target.path, - tspConfigDoc.getText().slice(target.nodePostionRange.pos, target.curPos), + tspConfigDoc.getText().slice(target.sourceRange?.pos, target.cursorPosition), ); if (variableInterpolationItems.length > 0) { return variableInterpolationItems; @@ -93,7 +93,9 @@ export async function provideTspconfigCompletionItems( tspConfigPosition, target, ); - items.push(item); + if (item) { + items.push(item); + } } } return items; @@ -142,10 +144,11 @@ export async function provideTspconfigCompletionItems( tspConfigPosition, target, ); - items.push(item); + if (item) { + items.push(item); + } } } - continue; } // If there is no corresponding ruleSet in the library, add the library name directly. @@ -156,7 +159,9 @@ export async function provideTspconfigCompletionItems( tspConfigPosition, target, ); - items.push(item); + if (item) { + items.push(item); + } } } } else if (extendKeyWord === "enable" || extendKeyWord === "disable") { @@ -175,14 +180,16 @@ export async function provideTspconfigCompletionItems( tspConfigPosition, target, ); - items.push(item); + if (item) { + items.push(item); + } } } } } else { log({ level: "warning", - message: "Unknown linter keyword", + message: "Unknown linter keyword, it should be 'extends', 'enable' or 'disable'", }); } return items; @@ -197,7 +204,7 @@ export async function provideTspconfigCompletionItems( ".yaml", [normalizeSlashes(tspConfigFile)], ); - return getFilePathCompletionItems(relativeFiles, siblings); + return getFilePathCompletionItems(relativeFiles, siblings, source); } else if (nodePath.length >= CONFIG_PATH_LENGTH_FOR_IMPORTS && nodePath[0] === "imports") { const currentFolder = getDirectoryPath(tspConfigFile); const newFolderPath = joinPaths(currentFolder, source); @@ -209,7 +216,7 @@ export async function provideTspconfigCompletionItems( ".tsp", [joinPaths(currentFolder, "main.tsp")], ); - return getFilePathCompletionItems(relativeFiles, siblings); + return getFilePathCompletionItems(relativeFiles, siblings, source); } else { const schema = TypeSpecConfigJsonSchema; return schema ? resolveCompleteItems(schema, target) : []; @@ -357,21 +364,24 @@ function createContainingQuatedValCompetionItem( description: string, tspConfigPosition: Position, target: YamlScalarTarget, -): CompletionItem { +): CompletionItem | undefined { if ( - target.curPos >= target.nodePostionRange.pos && - target.curPos <= target.nodePostionRange.end + target.sourceRange && + target.cursorPosition >= target.sourceRange.pos && + target.cursorPosition <= target.sourceRange.end ) { + // If it is a quoted string, the relative position needs to be reduced by 1 + const relativePos = + target.sourceType === "QUOTE_SINGLE" || target.sourceType === "QUOTE_DOUBLE" + ? target.cursorPosition - target.sourceRange.pos - 1 + : target.cursorPosition - target.sourceRange.pos; return { label: labelName, kind: CompletionItemKind.Field, documentation: description, textEdit: TextEdit.replace( Range.create( - Position.create( - tspConfigPosition.line, - tspConfigPosition.character - target.source.length, - ), + Position.create(tspConfigPosition.line, tspConfigPosition.character - relativePos), Position.create(tspConfigPosition.line, tspConfigPosition.character), ), target.sourceType === "QUOTE_SINGLE" || target.sourceType === "QUOTE_DOUBLE" @@ -379,9 +389,9 @@ function createContainingQuatedValCompetionItem( : `"${labelName}"`, ), }; - } else { - return { label: "" }; } + + return undefined; } /** @@ -405,14 +415,16 @@ async function findFilesOrDirsWithSameExtension( // When reading the content under the path, an error may be reported if the path is incorrect. const dirs = await compilerHost.readDir(rootPath); for (const d of dirs) { - if ( - ((await compilerHost.stat(joinPaths(rootPath, d))).isDirectory() || - ((await compilerHost.stat(joinPaths(rootPath, d))).isFile() && - d.endsWith(fileExtension))) && - !exclude.includes(d) && - !excludeFiles.includes(getNormalizedAbsolutePath(d, rootPath)) - ) { - files.push(d); + if (!exclude.includes(d) && !excludeFiles.includes(getNormalizedAbsolutePath(d, rootPath))) { + try { + const stat = await compilerHost.stat(joinPaths(rootPath, d)); + if (stat.isDirectory() || (stat.isFile() && d.endsWith(fileExtension))) { + files.push(d); + } + } catch { + // If the path is incorrect, the error is ignored + continue; + } } } } catch { @@ -425,22 +437,22 @@ async function findFilesOrDirsWithSameExtension( * Get the CompletionItem object array of the relative path of the file * @param relativeFiles File relative path array * @param siblings Sibling node array + * @param source The input source of the current node * @returns CompletionItem object array */ -function getFilePathCompletionItems(relativeFiles: string[], siblings: string[]): CompletionItem[] { - const items: CompletionItem[] = []; - if (relativeFiles.length > 0) { - for (const file of relativeFiles) { - if (!siblings.includes(file)) { - const item: CompletionItem = { - label: file, - kind: CompletionItemKind.File, - }; - items.push(item); - } - } - } - return items; +function getFilePathCompletionItems( + relativeFiles: string[], + siblings: string[], + source: string, +): CompletionItem[] { + return relativeFiles + .filter((file) => !siblings.includes(joinPaths(source, file))) + .map((file) => { + return { + label: file, + kind: CompletionItemKind.File, + }; + }); } /** @@ -455,7 +467,7 @@ function resolveVariableInterpolationCompleteItems( path: string[], curText: string, ): CompletionItem[] { - if (/{ *env\./g.test(curText)) { + if (/{\s*env\.[^}]*$/.test(curText)) { // environment-variables return getVariableCompletionItem( yamlDocNodes, @@ -510,21 +522,18 @@ function getVariableCompletionItem( ): CompletionItem[] { const result: CompletionItem[] = []; if (isMap(yamlDoc.contents)) { - yamlDoc.contents.items - .filter((item) => (item.key).source === filterName) - .map((item) => { - if (item.value !== null && isMap(item.value)) { - item.value.items.forEach((i) => { - if (isPair(i)) { - result.push({ - label: (i.key as any).source ?? "", - kind: CompletionItemKind.Value, - documentation: description, - }); - } + const yamlMap = yamlDoc.contents.items.find((item) => (item.key).source === filterName); + if (yamlMap && yamlMap.value !== null && isMap(yamlMap.value)) { + yamlMap.value.items.forEach((i) => { + if (isPair(i)) { + result.push({ + label: (i.key as any).source ?? "", + kind: CompletionItemKind.Value, + documentation: description, }); } }); + } } return result; diff --git a/packages/compiler/src/server/yaml-resolver.ts b/packages/compiler/src/server/yaml-resolver.ts index f65134481b..5d53c72a7b 100644 --- a/packages/compiler/src/server/yaml-resolver.ts +++ b/packages/compiler/src/server/yaml-resolver.ts @@ -32,26 +32,26 @@ export interface YamlScalarTarget { * actual value of target in the doc */ source: string; - /** - * The siblings of the target node - */ - siblings: string[]; /** * The input quotes (double quotes or single quotes) */ sourceType: Scalar.Type; /** - * The parsed yaml document + * The position range of the text in the document, such as [startPos, endPos], see {@link TextRange} */ - yamlDoc: Document; + sourceRange: TextRange | undefined; /** - * The position range of the text in the document, such as [startPos, endPos], see {@link TextRange} + * The siblings of the target node + */ + siblings: string[]; + /** + * The parsed yaml document */ - nodePostionRange: TextRange; + yamlDoc: Document; /** * The cursor current position */ - curPos: number; + cursorPosition: number; } interface YamlVisitScalarNode { @@ -105,8 +105,8 @@ export function resolveYamlScalarTarget( sourceType: "PLAIN", siblings: rootProperties, yamlDoc, - nodePostionRange: { pos: 0, end: 0 }, - curPos: pos, + sourceRange: undefined, + cursorPosition: pos, }; } for (let i = position.line - 1; i >= 0; i--) { @@ -155,8 +155,8 @@ export function resolveYamlScalarTarget( sourceType: "PLAIN", siblings: [...yp.siblings, yp.source], yamlDoc, - nodePostionRange: yp.nodePostionRange, - curPos: pos, + sourceRange: yp.sourceRange, + cursorPosition: pos, }; } break; @@ -202,8 +202,8 @@ export function resolveYamlScalarTarget( ? (last.value?.items.map((item) => (item.key as any).source ?? "") ?? []) : [], yamlDoc, - nodePostionRange: yp.nodePostionRange, - curPos: pos, + sourceRange: yp.sourceRange, + cursorPosition: pos, }; } break; @@ -261,7 +261,12 @@ function createYamlPathFromVisitScalarNode( } const last = nodePath[nodePath.length - 1]; - const [startPos, endPos] = n.range ?? []; + let curSourceRange = undefined; + if (n.range) { + const [startPos, endPos] = n.range; + curSourceRange = { pos: startPos, end: endPos }; + } + if (isDocument(last)) { // we are at the root and the content is a pure text (otherwise there should be a Map under document node first) return { @@ -271,8 +276,8 @@ function createYamlPathFromVisitScalarNode( sourceType: n.type ?? "PLAIN", siblings: [], yamlDoc, - nodePostionRange: { pos: startPos ?? 0, end: endPos ?? 0 }, - curPos: offset, + sourceRange: curSourceRange, + cursorPosition: offset, }; } else if (isPair(last)) { if (nodePath.length < 2) { @@ -295,8 +300,8 @@ function createYamlPathFromVisitScalarNode( sourceType: n.type ?? "PLAIN", siblings: [], yamlDoc, - nodePostionRange: { pos: startPos ?? 0, end: endPos ?? 0 }, - curPos: offset, + sourceRange: curSourceRange, + cursorPosition: offset, }; } else { const parent = nodePath.length >= 2 ? nodePath[nodePath.length - 2] : undefined; @@ -311,8 +316,8 @@ function createYamlPathFromVisitScalarNode( siblings: targetSiblings, sourceType: n.type ?? "PLAIN", yamlDoc, - nodePostionRange: { pos: startPos ?? 0, end: endPos ?? 0 }, - curPos: offset, + sourceRange: curSourceRange, + cursorPosition: offset, }; } } else if (isSeq(last)) { @@ -325,8 +330,8 @@ function createYamlPathFromVisitScalarNode( .filter((i) => i !== n) .map((item) => (isScalar(item) ? (item.source ?? "") : "")), yamlDoc, - nodePostionRange: { pos: startPos ?? 0, end: endPos ?? 0 }, - curPos: offset, + sourceRange: curSourceRange, + cursorPosition: offset, }; } else { log({ diff --git a/packages/compiler/test/server/completion.tspconfig.test.ts b/packages/compiler/test/server/completion.tspconfig.test.ts index ad9c910d07..bb41870478 100644 --- a/packages/compiler/test/server/completion.tspconfig.test.ts +++ b/packages/compiler/test/server/completion.tspconfig.test.ts @@ -246,11 +246,15 @@ describe("Test completion items for linter", () => { }, { config: `linter:\n extends:\n - "┆`, - expected: ["fake-linter-no-schema", "fake-linter/recommended"], + expected: ["fake-linter-no-schema", "fake-linter/recommended", "fake-linter"], }, { config: `linter:\n extends:\n - "fake-linter/recommended"\n - "┆`, - expected: ["fake-linter-no-schema"], + expected: ["fake-linter-no-schema", "fake-linter"], + }, + { + config: `linter:\n extends:\n - "fake-linter"\n enable:\n "┆`, + expected: ["fake-linter/casing", "fake-linter/no-model-doc", "fake-linter/testing"], }, { config: `linter:\n extends:\n - "fake-linter/recommended"\n enable:\n "┆`, @@ -330,6 +334,10 @@ describe("Test completion items that use parameters and environment variables an config: `environment-variables:\n BASE_DIR:\n default: "{cwd}"\n test-env:\n default: ""\noutput-dir: "{}env.┆}"`, expected: [], }, + { + config: `environment-variables:\n BASE_DIR:\n default: "{cwd}"\n test-env:\n default: ""\noutput-dir: "outdir/{env.}/my┆Dir"`, + expected: [], + }, { config: `environment-variables:\n BASE_DIR:\n default: "{cwd}"\n test-env:\n default: ""\noutput-dir: "{ abcenv.┆}"`, expected: ["cwd", "project-root"], @@ -358,6 +366,10 @@ describe("Test completion items that use parameters and environment variables an config: `parameters:\n base-dir:\n default: "{cwd}"\n test-param: default: ""\noutput-dir: "outDir/{cw┆}/myDir"`, expected: ["cwd", "project-root", "base-dir", "test-param"], }, + { + config: `parameters:\n base-dir:\n default: "{cwd}"\n test-param: default: ""\noptions:\n emitter-sub-folder:\n sub-folder: "{cw┆}/myDir"`, + expected: ["cwd", "project-root", "base-dir", "test-param", "output-dir", "emitter-name"], + }, { config: `parameters:\n base-dir:\n default: "{cwd}"\n test-param: default: ""\noutput-dir: "{env.┆}"`, expected: [], @@ -436,7 +448,7 @@ describe("Test completion items for imports", () => { }, { config: `imports:\n - "./demo_tsp"\n - "./┆`, - expected: ["demo_yaml", "demo_tsp"], + expected: ["demo_yaml"], }, { config: `imports:\n - "./demo_tsp/┆`, @@ -444,7 +456,7 @@ describe("Test completion items for imports", () => { }, { config: `imports:\n - "./demo_tsp/test1.tsp"\n - "./demo_tsp/┆`, - expected: ["test1.tsp", "test3.tsp"], + expected: ["test3.tsp"], }, { config: `imports:\n - "Z:/test/workspace/┆`, From 5b8eba74ce6073d1188816c46bf12f195bca72f6 Mon Sep 17 00:00:00 2001 From: Zhonglei Ma Date: Thu, 19 Dec 2024 14:42:34 +0800 Subject: [PATCH 19/25] Create improvements-for-the-intellisense-of-tspconfig.yaml-2024-11-19-2-35-2.md --- ...ellisense-of-tspconfig.yaml-2024-11-19-2-35-2.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .chronus/changes/improvements-for-the-intellisense-of-tspconfig.yaml-2024-11-19-2-35-2.md diff --git a/.chronus/changes/improvements-for-the-intellisense-of-tspconfig.yaml-2024-11-19-2-35-2.md b/.chronus/changes/improvements-for-the-intellisense-of-tspconfig.yaml-2024-11-19-2-35-2.md new file mode 100644 index 0000000000..83035b7ba5 --- /dev/null +++ b/.chronus/changes/improvements-for-the-intellisense-of-tspconfig.yaml-2024-11-19-2-35-2.md @@ -0,0 +1,13 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@typespec/compiler" +--- + +Improvements for the intellisense of tspconfig.yaml +- Supports auto-completion of the extends and imports paths +- The rule or ruleSets of the linter can be auto-completed +- Emitter optoins autocomplete intelligently handles quotation mark display +- Autocomplete of variable interpolation +- The parameters of emitter's options distinguish whether they are required or optional From facf5e558869b2899afd29996e7e91abad811747 Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Mon, 23 Dec 2024 10:19:03 +0800 Subject: [PATCH 20/25] update code and change logs --- ...nse-of-tspconfig.yaml-2024-11-19-2-35-1.md | 10 ++++++ ...nse-of-tspconfig.yaml-2024-11-19-2-35-2.md | 8 ++--- .../src/server/tspconfig/completion.ts | 35 +++++++++++-------- 3 files changed, 32 insertions(+), 21 deletions(-) create mode 100644 .chronus/changes/improvements-for-the-intellisense-of-tspconfig.yaml-2024-11-19-2-35-1.md diff --git a/.chronus/changes/improvements-for-the-intellisense-of-tspconfig.yaml-2024-11-19-2-35-1.md b/.chronus/changes/improvements-for-the-intellisense-of-tspconfig.yaml-2024-11-19-2-35-1.md new file mode 100644 index 0000000000..f4866cc4b1 --- /dev/null +++ b/.chronus/changes/improvements-for-the-intellisense-of-tspconfig.yaml-2024-11-19-2-35-1.md @@ -0,0 +1,10 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@typespec/compiler" +--- + +Improvements for the intellisense of tspconfig.yaml +- Support the auto completion for extends, imports, rule, rule sets and variables in tspconfig.yaml +- Show required/optional information in the details of emitter's options completion item in tspconfig.yaml diff --git a/.chronus/changes/improvements-for-the-intellisense-of-tspconfig.yaml-2024-11-19-2-35-2.md b/.chronus/changes/improvements-for-the-intellisense-of-tspconfig.yaml-2024-11-19-2-35-2.md index 83035b7ba5..b2ccd9aa71 100644 --- a/.chronus/changes/improvements-for-the-intellisense-of-tspconfig.yaml-2024-11-19-2-35-2.md +++ b/.chronus/changes/improvements-for-the-intellisense-of-tspconfig.yaml-2024-11-19-2-35-2.md @@ -5,9 +5,5 @@ packages: - "@typespec/compiler" --- -Improvements for the intellisense of tspconfig.yaml -- Supports auto-completion of the extends and imports paths -- The rule or ruleSets of the linter can be auto-completed -- Emitter optoins autocomplete intelligently handles quotation mark display -- Autocomplete of variable interpolation -- The parameters of emitter's options distinguish whether they are required or optional +Fix bug in tspconfig.yaml +- Fix the issue that emitter option auto complete while inside "" will add extra ""` diff --git a/packages/compiler/src/server/tspconfig/completion.ts b/packages/compiler/src/server/tspconfig/completion.ts index 295ec26065..d76109662d 100644 --- a/packages/compiler/src/server/tspconfig/completion.ts +++ b/packages/compiler/src/server/tspconfig/completion.ts @@ -46,13 +46,15 @@ export async function provideTspconfigCompletionItems( } // Variable interpolation - const variableInterpolationItems = resolveVariableInterpolationCompleteItems( - target.yamlDoc, - target.path, - tspConfigDoc.getText().slice(target.sourceRange?.pos, target.cursorPosition), - ); - if (variableInterpolationItems.length > 0) { - return variableInterpolationItems; + if (target.sourceRange) { + const variableInterpolationItems = resolveVariableInterpolationCompleteItems( + target.yamlDoc, + target.path, + tspConfigDoc.getText().slice(target.sourceRange.pos, target.cursorPosition), + ); + if (variableInterpolationItems.length > 0) { + return variableInterpolationItems; + } } const items = resolveTspConfigCompleteItems( @@ -87,7 +89,7 @@ export async function provideTspconfigCompletionItems( const items: CompletionItem[] = []; for (const [name, pkg] of Object.entries(emitters)) { if (!siblings.includes(name)) { - const item = createContainingQuatedValCompetionItem( + const item = createCompletionItemWithQuote( name, (await pkg.getPackageJsonData())?.description ?? `Emitter from ${name}`, tspConfigPosition, @@ -138,7 +140,7 @@ export async function provideTspconfigCompletionItems( for (const [ruleSet] of Object.entries(exports?.$linter?.ruleSets)) { const labelName = `${name}/${ruleSet}`; if (!siblings.includes(labelName)) { - const item = createContainingQuatedValCompetionItem( + const item = createCompletionItemWithQuote( labelName, (await pkg.getPackageJsonData())?.description ?? `Linters from ${labelName}`, tspConfigPosition, @@ -151,9 +153,9 @@ export async function provideTspconfigCompletionItems( } } - // If there is no corresponding ruleSet in the library, add the library name directly. + // Add the library name directly. if (!siblings.includes(name)) { - const item = createContainingQuatedValCompetionItem( + const item = createCompletionItemWithQuote( name, (await pkg.getPackageJsonData())?.description ?? `Linters from ${name}`, tspConfigPosition, @@ -174,7 +176,7 @@ export async function provideTspconfigCompletionItems( exports?.$linter?.rules, )) { const labelName = `${name}/${rule.name}`; - const item = createContainingQuatedValCompetionItem( + const item = createCompletionItemWithQuote( labelName, rule.description, tspConfigPosition, @@ -359,7 +361,7 @@ export async function provideTspconfigCompletionItems( * @param target The target object of the current configuration file, see {@link YamlScalarTarget} * @returns CompletionItem object */ -function createContainingQuatedValCompetionItem( +function createCompletionItemWithQuote( labelName: string, description: string, tspConfigPosition: Position, @@ -371,7 +373,7 @@ function createContainingQuatedValCompetionItem( target.cursorPosition <= target.sourceRange.end ) { // If it is a quoted string, the relative position needs to be reduced by 1 - const relativePos = + const lenRelativeToStartPos = target.sourceType === "QUOTE_SINGLE" || target.sourceType === "QUOTE_DOUBLE" ? target.cursorPosition - target.sourceRange.pos - 1 : target.cursorPosition - target.sourceRange.pos; @@ -381,7 +383,10 @@ function createContainingQuatedValCompetionItem( documentation: description, textEdit: TextEdit.replace( Range.create( - Position.create(tspConfigPosition.line, tspConfigPosition.character - relativePos), + Position.create( + tspConfigPosition.line, + tspConfigPosition.character - lenRelativeToStartPos, + ), Position.create(tspConfigPosition.line, tspConfigPosition.character), ), target.sourceType === "QUOTE_SINGLE" || target.sourceType === "QUOTE_DOUBLE" From 3bf6351a6c0e76804827e23de359f2242a83818f Mon Sep 17 00:00:00 2001 From: Zhonglei Ma Date: Tue, 24 Dec 2024 09:14:06 +0800 Subject: [PATCH 21/25] Update improvements-for-the-intellisense-of-tspconfig.yaml-2024-11-19-2-35-2.md --- ...-for-the-intellisense-of-tspconfig.yaml-2024-11-19-2-35-2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.chronus/changes/improvements-for-the-intellisense-of-tspconfig.yaml-2024-11-19-2-35-2.md b/.chronus/changes/improvements-for-the-intellisense-of-tspconfig.yaml-2024-11-19-2-35-2.md index b2ccd9aa71..04d558cae3 100644 --- a/.chronus/changes/improvements-for-the-intellisense-of-tspconfig.yaml-2024-11-19-2-35-2.md +++ b/.chronus/changes/improvements-for-the-intellisense-of-tspconfig.yaml-2024-11-19-2-35-2.md @@ -6,4 +6,4 @@ packages: --- Fix bug in tspconfig.yaml -- Fix the issue that emitter option auto complete while inside "" will add extra ""` +- Fix the issue that extra " will be added when auto completing emitter options inside "" From 26e1f3e45db5b39673b5811811fbad287242af6d Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Tue, 24 Dec 2024 11:37:01 +0800 Subject: [PATCH 22/25] updated --- packages/compiler/src/server/tspconfig/completion.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/server/tspconfig/completion.ts b/packages/compiler/src/server/tspconfig/completion.ts index d76109662d..f68166ba58 100644 --- a/packages/compiler/src/server/tspconfig/completion.ts +++ b/packages/compiler/src/server/tspconfig/completion.ts @@ -366,7 +366,7 @@ function createCompletionItemWithQuote( description: string, tspConfigPosition: Position, target: YamlScalarTarget, -): CompletionItem | undefined { +): CompletionItem { if ( target.sourceRange && target.cursorPosition >= target.sourceRange.pos && @@ -396,7 +396,12 @@ function createCompletionItemWithQuote( }; } - return undefined; + return { + label: labelName, + kind: CompletionItemKind.Field, + documentation: description, + insertText: `"${labelName}"`, + }; } /** From 81c3ced34b5b2aff875dd1a1799b9a370c25d947 Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Tue, 24 Dec 2024 16:04:09 +0800 Subject: [PATCH 23/25] updated --- .../compiler/src/server/tspconfig/completion.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/compiler/src/server/tspconfig/completion.ts b/packages/compiler/src/server/tspconfig/completion.ts index f68166ba58..e1cb5acef9 100644 --- a/packages/compiler/src/server/tspconfig/completion.ts +++ b/packages/compiler/src/server/tspconfig/completion.ts @@ -95,9 +95,7 @@ export async function provideTspconfigCompletionItems( tspConfigPosition, target, ); - if (item) { - items.push(item); - } + items.push(item); } } return items; @@ -146,9 +144,7 @@ export async function provideTspconfigCompletionItems( tspConfigPosition, target, ); - if (item) { - items.push(item); - } + items.push(item); } } } @@ -161,9 +157,7 @@ export async function provideTspconfigCompletionItems( tspConfigPosition, target, ); - if (item) { - items.push(item); - } + items.push(item); } } } else if (extendKeyWord === "enable" || extendKeyWord === "disable") { @@ -182,9 +176,7 @@ export async function provideTspconfigCompletionItems( tspConfigPosition, target, ); - if (item) { - items.push(item); - } + items.push(item); } } } From 82faddf64be7aabd2e781761e9e71c0892ad6f47 Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Fri, 27 Dec 2024 13:28:47 +0800 Subject: [PATCH 24/25] updated test --- .../compiler/test/server/completion.tspconfig.test.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/compiler/test/server/completion.tspconfig.test.ts b/packages/compiler/test/server/completion.tspconfig.test.ts index bb41870478..5a9a5f70e7 100644 --- a/packages/compiler/test/server/completion.tspconfig.test.ts +++ b/packages/compiler/test/server/completion.tspconfig.test.ts @@ -2,6 +2,7 @@ import { join } from "path"; import { describe, expect, it } from "vitest"; import { CompletionList } from "vscode-languageserver/node.js"; import { createTestServerHost, extractCursor } from "../../src/testing/test-server-host.js"; +import { resolveVirtualPath } from "../../src/testing/test-utils.js"; const rootOptions = [ "extends", @@ -388,6 +389,7 @@ describe("Test completion items that use parameters and environment variables an }); describe("Test completion items for extends", () => { + const path = resolveVirtualPath("Z:/test/workspace"); it.each([ { config: `extends: "┆`, @@ -402,7 +404,7 @@ describe("Test completion items for extends", () => { expected: ["tspconfigtest2.yaml"], }, { - config: `extends: "Z:/test/workspace┆"`, + config: `extends: "${path}┆"`, expected: ["tspconfigtest0.yaml", "demo_yaml", "demo_tsp"], }, { @@ -432,7 +434,7 @@ describe("Test completion items for extends", () => { expected: [], }, { - config: `extends: "Z:/test/workspace/demo┆"`, + config: `extends: "${path}/demo┆"`, expected: [], }, ])("#%# Test addProp: $config", async ({ config, expected }) => { @@ -441,6 +443,7 @@ describe("Test completion items for extends", () => { }); describe("Test completion items for imports", () => { + const path = resolveVirtualPath("Z:/test/workspace/"); it.each([ { config: `imports:\n - "./┆`, @@ -459,7 +462,7 @@ describe("Test completion items for imports", () => { expected: ["test3.tsp"], }, { - config: `imports:\n - "Z:/test/workspace/┆`, + config: `imports:\n - "${path}┆`, expected: ["demo_yaml", "demo_tsp"], }, { @@ -467,7 +470,7 @@ describe("Test completion items for imports", () => { expected: [], }, { - config: `imports:\n - "Z:/test/workspace/demo┆`, + config: `imports:\n - "${path}demo┆`, expected: [], }, { From 09b87962aa68c8935218e0fb85f63dff44742974 Mon Sep 17 00:00:00 2001 From: "Zhonglei Ma (WICRESOFT NORTH AMERICA LTD)" Date: Mon, 30 Dec 2024 15:48:33 +0800 Subject: [PATCH 25/25] test update for pipeline --- packages/compiler/test/server/completion.tspconfig.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compiler/test/server/completion.tspconfig.test.ts b/packages/compiler/test/server/completion.tspconfig.test.ts index 5a9a5f70e7..f883747209 100644 --- a/packages/compiler/test/server/completion.tspconfig.test.ts +++ b/packages/compiler/test/server/completion.tspconfig.test.ts @@ -389,7 +389,7 @@ describe("Test completion items that use parameters and environment variables an }); describe("Test completion items for extends", () => { - const path = resolveVirtualPath("Z:/test/workspace"); + const path = resolveVirtualPath("workspace"); it.each([ { config: `extends: "┆`, @@ -443,7 +443,7 @@ describe("Test completion items for extends", () => { }); describe("Test completion items for imports", () => { - const path = resolveVirtualPath("Z:/test/workspace/"); + const path = resolveVirtualPath("workspace/"); it.each([ { config: `imports:\n - "./┆`,