diff --git a/.chronus/changes/no-lsp-error-without-workspace-2024-11-19-18-43-58.md b/.chronus/changes/no-lsp-error-without-workspace-2024-11-19-18-43-58.md new file mode 100644 index 0000000000..c3efaca050 --- /dev/null +++ b/.chronus/changes/no-lsp-error-without-workspace-2024-11-19-18-43-58.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - typespec-vscode +--- + +Do not start TypeSpec Language Server when there is no workspace opened \ No newline at end of file diff --git a/.chronus/changes/no-lsp-error-without-workspace-2024-11-24-15-20-12.md b/.chronus/changes/no-lsp-error-without-workspace-2024-11-24-15-20-12.md new file mode 100644 index 0000000000..2727522f5a --- /dev/null +++ b/.chronus/changes/no-lsp-error-without-workspace-2024-11-24-15-20-12.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@typespec/compiler" +--- + +Use inspect instead of Json.stringify when logging object in server log \ No newline at end of file diff --git a/packages/compiler/src/server/server.ts b/packages/compiler/src/server/server.ts index 0d1b61238e..ab12d52a5c 100644 --- a/packages/compiler/src/server/server.ts +++ b/packages/compiler/src/server/server.ts @@ -3,6 +3,7 @@ import { mkdir, writeFile } from "fs/promises"; import inspector from "inspector"; import { join } from "path"; import { fileURLToPath } from "url"; +import { inspect } from "util"; import { TextDocument } from "vscode-languageserver-textdocument"; import { ApplyWorkspaceEditParams, @@ -50,8 +51,7 @@ function main() { let detail: string | undefined = undefined; let fullMessage = message; if (log.detail) { - detail = - typeof log.detail === "string" ? log.detail : JSON.stringify(log.detail, undefined, 2); + detail = typeof log.detail === "string" ? log.detail : inspect(log.detail, undefined, 2); fullMessage = `${message}:\n${detail}`; } diff --git a/packages/typespec-vscode/src/extension.ts b/packages/typespec-vscode/src/extension.ts index 6495b6bbd0..7a2f3a4e34 100644 --- a/packages/typespec-vscode/src/extension.ts +++ b/packages/typespec-vscode/src/extension.ts @@ -50,13 +50,13 @@ export async function activate(context: ExtensionContext) { async (args: RestartServerCommandArgs | undefined): Promise => { return vscode.window.withProgress( { - title: "Restarting TypeSpec language service...", + title: args?.notificationMessage ?? "Restarting TypeSpec language service...", location: vscode.ProgressLocation.Notification, }, async () => { if (args?.forceRecreate === true) { logger.info("Forcing to recreate TypeSpec LSP server..."); - return await recreateLSPClient(context, args?.popupRecreateLspError); + return await recreateLSPClient(context); } if (client && client.state === State.Running) { await client.restart(); @@ -65,7 +65,7 @@ export async function activate(context: ExtensionContext) { logger.info( "TypeSpec LSP server is not running which is not expected, try to recreate and start...", ); - return recreateLSPClient(context, args?.popupRecreateLspError); + return recreateLSPClient(context); } }, ); @@ -97,26 +97,33 @@ export async function activate(context: ExtensionContext) { }), ); - return await vscode.window.withProgress( - { - title: "Launching TypeSpec language service...", - location: vscode.ProgressLocation.Notification, - }, - async () => { - await recreateLSPClient(context); - }, - ); + // Only try to start language server when some workspace has been opened + // because the LanguageClient class will popup error notification in vscode directly if failing to start + // which will be confusing to user if no workspace is opened (i.e. in Create TypeSpec project scenario) + if ((vscode.workspace.workspaceFolders?.length ?? 0) > 0) { + return await vscode.window.withProgress( + { + title: "Launching TypeSpec language service...", + location: vscode.ProgressLocation.Notification, + }, + async () => { + await recreateLSPClient(context); + }, + ); + } else { + logger.info("No workspace opened, Skip starting TypeSpec language service."); + } } export async function deactivate() { await client?.stop(); } -async function recreateLSPClient(context: ExtensionContext, showPopupWhenError?: boolean) { +async function recreateLSPClient(context: ExtensionContext) { logger.info("Recreating TypeSpec LSP server..."); const oldClient = client; client = await TspLanguageClient.create(context, outputChannel); await oldClient?.stop(); - await client.start(showPopupWhenError ?? (vscode.workspace.workspaceFolders?.length ?? 0) > 0); + await client.start(); return client; } diff --git a/packages/typespec-vscode/src/tsp-language-client.ts b/packages/typespec-vscode/src/tsp-language-client.ts index f0d6c6f571..43b24433fb 100644 --- a/packages/typespec-vscode/src/tsp-language-client.ts +++ b/packages/typespec-vscode/src/tsp-language-client.ts @@ -142,9 +142,10 @@ export class TspLanguageClient { } } - async start(showPopupWhenError: boolean): Promise { + async start(): Promise { try { if (this.client.needsStart()) { + // please be aware that this method would popup error notification in vscode directly await this.client.start(); logger.info("TypeSpec server started"); } else { @@ -162,13 +163,13 @@ export class TspLanguageClient { " - TypeSpec server path is configured with https://github.com/microsoft/typespec#installing-vs-code-extension.", ].join("\n"), [], - { showOutput: false, showPopup: showPopupWhenError }, + { showOutput: false, showPopup: true }, ); logger.error("Error detail", [e]); } else { logger.error("Unexpected error when starting TypeSpec server", [e], { showOutput: false, - showPopup: showPopupWhenError, + showPopup: true, }); } } diff --git a/packages/typespec-vscode/src/types.ts b/packages/typespec-vscode/src/types.ts index c21dba7e6b..99af0a7fa0 100644 --- a/packages/typespec-vscode/src/types.ts +++ b/packages/typespec-vscode/src/types.ts @@ -19,6 +19,10 @@ export interface InstallGlobalCliCommandArgs { confirm: boolean; confirmTitle?: string; confirmPlaceholder?: string; + /** + * set to true to disable popup notification and show output channel when running the command + */ + silentMode?: boolean; } export interface RestartServerCommandArgs { @@ -26,5 +30,25 @@ export interface RestartServerCommandArgs { * whether to recreate TspLanguageClient instead of just restarting it */ forceRecreate: boolean; - popupRecreateLspError: boolean; + notificationMessage?: string; } + +export const enum ResultCode { + Success = "success", + Fail = "fail", + Cancelled = "cancelled", + Timeout = "timeout", +} + +interface SuccessResult { + code: ResultCode.Success; + value: T; + details?: any; +} + +interface UnsuccessResult { + code: ResultCode.Fail | ResultCode.Cancelled | ResultCode.Timeout; + details?: any; +} + +export type Result = SuccessResult | UnsuccessResult; diff --git a/packages/typespec-vscode/src/utils.ts b/packages/typespec-vscode/src/utils.ts index f5b72bae7d..e1d34a6c2c 100644 --- a/packages/typespec-vscode/src/utils.ts +++ b/packages/typespec-vscode/src/utils.ts @@ -6,6 +6,7 @@ import { CancellationToken } from "vscode"; import { Executable } from "vscode-languageclient/node.js"; import logger from "./log/logger.js"; import { isUrl } from "./path-utils.js"; +import { ResultCode } from "./types.js"; export async function isFile(path: string) { try { @@ -278,8 +279,8 @@ export function spawnExecution( } /** - * if the operation is cancelled, the promise will be rejected with reason==="cancelled" - * if the operation is timeout, the promise will be rejected with reason==="timeout" + * if the operation is cancelled, the promise will be rejected with {@link ResultCode.Cancelled} + * if the operation is timeout, the promise will be rejected with {@link ResultCode.Timeout} * * @param action * @param token @@ -293,10 +294,10 @@ export function createPromiseWithCancelAndTimeout( ) { return new Promise((resolve, reject) => { token.onCancellationRequested(() => { - reject("cancelled"); + reject(ResultCode.Cancelled); }); setTimeout(() => { - reject("timeout"); + reject(ResultCode.Timeout); }, timeoutInMs); action.then(resolve, reject); }); diff --git a/packages/typespec-vscode/src/vscode-cmd/create-tsp-project.ts b/packages/typespec-vscode/src/vscode-cmd/create-tsp-project.ts index deb3327e44..31438c1a71 100644 --- a/packages/typespec-vscode/src/vscode-cmd/create-tsp-project.ts +++ b/packages/typespec-vscode/src/vscode-cmd/create-tsp-project.ts @@ -6,7 +6,7 @@ import type { import { TIMEOUT } from "dns"; import { readdir } from "fs/promises"; import * as semver from "semver"; -import vscode, { OpenDialogOptions, QuickPickItem } from "vscode"; +import vscode, { OpenDialogOptions, QuickPickItem, window } from "vscode"; import { State } from "vscode-languageclient"; import logger from "../log/logger.js"; import { getBaseFileName, getDirectoryPath, joinPaths } from "../path-utils.js"; @@ -15,6 +15,8 @@ import { CommandName, InstallGlobalCliCommandArgs, RestartServerCommandArgs, + Result, + ResultCode, SettingName, } from "../types.js"; import { @@ -22,6 +24,7 @@ import { ExecOutput, isFile, isWhitespaceStringOrUndefined, + spawnExecution, tryParseJson, tryReadFileOrUrl, } from "../utils.js"; @@ -71,13 +74,27 @@ export async function createTypeSpecProject(client: TspLanguageClient | undefine const folderName = getBaseFileName(selectedRootFolder); if (!client || client.state !== State.Running) { - const r = await InstallCompilerAndRestartLSPClient(); - if (r === undefined) { + const r = await CheckCompilerAndStartLSPClient(selectedRootFolder); + if (r.code === ResultCode.Cancelled) { logger.info("Creating TypeSpec Project cancelled when installing Compiler/CLI"); return; - } else { - client = r; } + if ( + r.code !== ResultCode.Success || + r.value === undefined || + r.value.state !== State.Running + ) { + logger.error( + "Unexpected Error when checking Compiler/CLI. Please check the previous log for details.", + [], + { + showOutput: true, + showPopup: true, + }, + ); + return; + } + client = r.value; } const isSupport = await isCompilerSupport(client); @@ -221,12 +238,12 @@ async function tspInstall( ); return result; } catch (e) { - if (e === "cancelled") { + if (e === ResultCode.Cancelled) { logger.info( "Installation of TypeSpec project dependencies by 'tsp install' is cancelled by user", ); return undefined; - } else if (e === "timeout") { + } else if (e === ResultCode.Timeout) { logger.error( `Installation of TypeSpec project dependencies by 'tsp install' is timeout after ${TIMEOUT}ms`, ); @@ -275,9 +292,9 @@ async function initProject( logger.info("Creating TypeSpec project completed. "); return true; } catch (e) { - if (e === "cancelled") { + if (e === ResultCode.Cancelled) { logger.info("Creating TypeSpec project cancelled by user."); - } else if (e === "timeout") { + } else if (e === ResultCode.Timeout) { logger.error(`Creating TypeSpec project timed out (${TIMEOUT}ms).`); } else { logger.error("Error when creating TypeSpec project", [e], { @@ -513,7 +530,8 @@ async function isCompilerSupport(client: TspLanguageClient): Promise { client.initializeResult?.customCapacities?.initProject !== true ) { logger.error( - `Create project feature is not supported by the current TypeSpec Compiler (ver ${client.initializeResult?.serverInfo?.version ?? "<= 0.63.0"}). Please upgrade TypeSpec Compiler and try again.`, + `Create project feature is not supported by the current TypeSpec Compiler (ver ${client.initializeResult?.serverInfo?.version ?? "< 0.64.0"}). ` + + `Please Upgrade TypeSpec Compiler, Restart TypeSpec server (by vscode command 'TypeSpec:Restart TypeSpec server') or restart vscode, and try again.`, [], { showOutput: true, @@ -554,42 +572,60 @@ async function loadInitTemplates( .getConfiguration() .get(SettingName.InitTemplatesUrls); if (settings) { - for (const item of settings) { - const { content, url } = (await tryReadFileOrUrl(item.url)) ?? { - content: undefined, - url: item.url, - }; - if (!content) { - logger.error(`Failed to read template from ${item.url}. The url will be skipped`, [], { - showOutput: true, - showPopup: false, - }); - continue; - } else { - const json = tryParseJson(content); - if (!json) { - logger.error( - `Failed to parse templates content from ${item.url}. The url will be skipped`, - [], - { showOutput: true, showPopup: false }, - ); + const loadFromConfig = async () => { + for (const item of settings) { + const { content, url } = (await tryReadFileOrUrl(item.url)) ?? { + content: undefined, + url: item.url, + }; + if (!content) { + logger.warning(`Failed to read template from ${item.url}. The url will be skipped`, [], { + showOutput: false, + showPopup: true, + }); continue; } else { - for (const [key, value] of Object.entries(json)) { - if (value !== undefined) { - const info: InitTemplateInfo = { - source: item.name, - sourceType: "config", - baseUrl: getDirectoryPath(url), - name: key, - template: value as InitProjectTemplate, - }; - templateInfoMap.get(item.name)?.push(info) ?? templateInfoMap.set(item.name, [info]); + const json = tryParseJson(content); + if (!json) { + logger.warning( + `Failed to parse templates content from ${item.url}. The url will be skipped`, + [], + { showOutput: false, showPopup: true }, + ); + continue; + } else { + for (const [key, value] of Object.entries(json)) { + if (value !== undefined) { + const info: InitTemplateInfo = { + source: item.name, + sourceType: "config", + baseUrl: getDirectoryPath(url), + name: key, + template: value as InitProjectTemplate, + }; + templateInfoMap.get(item.name)?.push(info) ?? + templateInfoMap.set(item.name, [info]); + } } } } } - } + }; + // this may take long time if the network is slow or broken + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Loading init templates from config...", + cancellable: true, + }, + async (_progress, token) => { + await createPromiseWithCancelAndTimeout( + loadFromConfig(), + token, + 5 * 60 * 1000, // 5 minutes as timeout + ); + }, + ); } logger.info(`${templateInfoMap.size} templates loaded.`); return templateInfoMap; @@ -639,28 +675,77 @@ async function checkProjectRootFolderEmpty(selectedFolder: string): Promise { - const igcArgs: InstallGlobalCliCommandArgs = { - confirm: true, - confirmTitle: "No TypeSpec Compiler/CLI found which is needed to create TypeSpec project.", - confirmPlaceholder: - "No TypeSpec Compiler/CLI found which is needed to create TypeSpec project.", - }; - const result = await vscode.commands.executeCommand( - CommandName.InstallGlobalCompilerCli, - igcArgs, - ); - if (!result) { - return undefined; +async function CheckCompilerAndStartLSPClient(folder: string): Promise> { + // language server may not be started because no workspace is opened or failed to start for some reason + // so before trying to start it, let's try to check whether global compiler is available first + // to avoid unnecessary error notification when starting LSP which would be confusing (we can't avoid it which + // is from base LanguageClient class...). + const r = await IsGlobalCompilerAvailable(folder); + if (r.code !== ResultCode.Success) { + return { code: r.code, details: r.details }; + } + if (!r.value) { + const igcArgs: InstallGlobalCliCommandArgs = { + confirm: true, + confirmTitle: "No TypeSpec Compiler/CLI found which is needed to create TypeSpec project.", + confirmPlaceholder: + "No TypeSpec Compiler/CLI found which is needed to create TypeSpec project.", + silentMode: true, + }; + const result = await vscode.commands.executeCommand>( + CommandName.InstallGlobalCompilerCli, + igcArgs, + ); + if (result.code !== ResultCode.Success) { + return { code: result.code, details: result.details }; + } } - logger.info("Try to restart lsp client after installing compiler."); + logger.info("Try to restart lsp client."); const rsArgs: RestartServerCommandArgs = { forceRecreate: false, - popupRecreateLspError: true, + notificationMessage: "Launching TypeSpec language service...", }; const newClient = await vscode.commands.executeCommand( CommandName.RestartServer, rsArgs, ); - return newClient; + return { code: ResultCode.Success, value: newClient }; +} + +async function IsGlobalCompilerAvailable(folder: string): Promise> { + const TIMEOUT = 120000; // set timeout to 2 minutes which should be enough for checking compiler + return await window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Checking TypeSpec compiler...", + cancellable: true, + }, + async (_progress, token) => { + let output; + try { + output = await createPromiseWithCancelAndTimeout( + // it's possible for the execution to fail, so don't log to output channel by default to avoid potential confusing + spawnExecution("tsp", ["--version"], folder), + token, + TIMEOUT, + ); + logger.debug("Global compiler is available by checking 'tsp --version'"); + return { code: ResultCode.Success, value: true }; + } catch (e) { + if (e === ResultCode.Cancelled) { + logger.info("Checking compiler is cancelled by user."); + return { code: ResultCode.Cancelled }; + } else if (e === ResultCode.Timeout) { + logger.debug(`Checking compiler is timeout after ${TIMEOUT}ms.`); + return { code: ResultCode.Timeout }; + } else { + logger.debug( + "Global compiler is not available by check 'tsp --version' command which reported error", + [e, output], + ); + return { code: ResultCode.Success, value: false }; + } + } + }, + ); } diff --git a/packages/typespec-vscode/src/vscode-cmd/install-tsp-compiler.ts b/packages/typespec-vscode/src/vscode-cmd/install-tsp-compiler.ts index d22b3a1182..b7c82cab60 100644 --- a/packages/typespec-vscode/src/vscode-cmd/install-tsp-compiler.ts +++ b/packages/typespec-vscode/src/vscode-cmd/install-tsp-compiler.ts @@ -1,6 +1,6 @@ import vscode, { QuickPickItem } from "vscode"; import logger from "../log/logger.js"; -import { InstallGlobalCliCommandArgs } from "../types.js"; +import { InstallGlobalCliCommandArgs, Result, ResultCode } from "../types.js"; import { createPromiseWithCancelAndTimeout, spawnExecutionAndLogToOutput } from "../utils.js"; const COMPILER_REQUIREMENT = @@ -8,7 +8,9 @@ const COMPILER_REQUIREMENT = export async function installCompilerGlobally( args: InstallGlobalCliCommandArgs | undefined, -): Promise { +): Promise> { + const showOutput = args?.silentMode !== true; + const showPopup = args?.silentMode !== true; // confirm with end user by default if (args?.confirm !== false) { const yes: QuickPickItem = { @@ -24,14 +26,14 @@ export async function installCompilerGlobally( }); if (confirm !== yes) { logger.info("User cancelled the installation of TypeSpec Compiler/CLI"); - return false; + return { code: ResultCode.Cancelled }; } else { logger.info("User confirmed the installation of TypeSpec Compiler/CLI"); } } else { logger.info("Installing TypeSpec Compiler/CLI with confirmation disabled explicitly..."); } - return await vscode.window.withProgress( + return await vscode.window.withProgress>( { title: "Installing TypeSpec Compiler/CLI...", location: vscode.ProgressLocation.Notification, @@ -40,7 +42,7 @@ export async function installCompilerGlobally( async (_progress, token) => { const TIMEOUT = 300000; // set timeout to 5 minutes which should be enough for installing compiler try { - const output = await createPromiseWithCancelAndTimeout( + await createPromiseWithCancelAndTimeout( spawnExecutionAndLogToOutput( "npm", ["install", "-g", "@typespec/compiler"], @@ -49,27 +51,35 @@ export async function installCompilerGlobally( token, TIMEOUT, ); - if (output.exitCode !== 0) { + + logger.info("TypeSpec Compiler/CLI installed successfully", [], { + showOutput: false, + showPopup, + }); + return { code: ResultCode.Success, value: undefined }; + } catch (e: any) { + if (e === ResultCode.Cancelled) { + return { code: ResultCode.Cancelled }; + } else if (e === ResultCode.Timeout) { + logger.error(`Installation of TypeSpec Compiler/CLI is timeout after ${TIMEOUT}ms`, [e], { + showOutput, + showPopup, + }); + return { code: ResultCode.Timeout }; + } else { logger.error( - "Failed to install TypeSpec CLI. Please check the previous log for details", - [output], - { showOutput: true, showPopup: true }, + `Installing TypeSpec Compiler/CLI failed. Please make sure the pre-requisites below has been installed properly. And you may check the previous log for more detail.\n` + + COMPILER_REQUIREMENT + + "\n" + + `More detail about typespec compiler: https://typespec.io/docs/\n` + + "More detail about nodejs: https://nodejs.org/en/download/package-manager\n", + [e], + { + showOutput, + showPopup, + }, ); - return false; - } else { - logger.info("TypeSpec CLI installed successfully"); - return true; - } - } catch (e) { - if (e === "cancelled") { - logger.info("Installation of TypeSpec Compiler/CLI is cancelled by user"); - return false; - } else if (e === "timeout") { - logger.error(`Installation of TypeSpec Compiler/CLI is timeout after ${TIMEOUT}ms`); - return false; - } else { - logger.error("Unexpected error when installing TypeSpec Compiler/CLI", [e]); - return false; + return { code: ResultCode.Fail, details: e }; } } },