Skip to content

Commit

Permalink
Do not start TypeSpec Language Server when there is no workspace open…
Browse files Browse the repository at this point in the history
…ed (#5413)

Do not start TypeSpec Language Server when there is no workspace opened
in vscode and update the Create TypeSpec project scenario to make sure
there is not error notification popup complaining about LSP when
checking compiler which would be confusing.

fixes: #5418
  • Loading branch information
RodgeFu authored Jan 7, 2025
1 parent 5ee9275 commit 6e352d2
Show file tree
Hide file tree
Showing 7 changed files with 300 additions and 111 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- typespec-vscode
---

Do not start TypeSpec Language Server when there is no workspace opened
64 changes: 49 additions & 15 deletions packages/typespec-vscode/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import vscode, { commands, ExtensionContext } from "vscode";
import vscode, { commands, ExtensionContext, TabInputText } from "vscode";
import { State } from "vscode-languageclient";
import { createCodeActionProvider } from "./code-action-provider.js";
import { ExtensionLogListener } from "./log/extension-log-listener.js";
Expand Down Expand Up @@ -50,13 +50,13 @@ export async function activate(context: ExtensionContext) {
async (args: RestartServerCommandArgs | undefined): Promise<TspLanguageClient> => {
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();
Expand All @@ -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);
}
},
);
Expand Down Expand Up @@ -97,26 +97,60 @@ 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 ||
// still need to check opened files when there is no workspace opened
vscode.window.tabGroups.all
.flatMap((tg) => tg.tabs)
.findIndex((t) => {
if (!t.input || !(t.input instanceof TabInputText) || !t.input.uri) {
return false;
}
// When an untitled file being renamed to .tsp file, our extension will be activated
// before the file info being refreshed properly, so need to check the untitled file too here.
// untitled file has the scheme "untitled"
if (t.input.uri.scheme === "untitled") {
return true;
}
// only handle .tsp file, not tspconfig.yaml file because
// vscode won't activate our extension if tspconfig.yaml is opened without workspace because we are using "workspaceContains:..." activation event now.
// In order to cover "tspconfig.yaml" file, we would need to hook on "onStartupFinish" or "*" activation event
// and check whether we should do real job in onDidOpenTextDocument event ourselves.
// Considering
// - it's not a good idea to start our extension whenever vscode is started
// - the increasement of complaxity to handle activation ourselves
// - purely open a tspconfig.yaml file without other .tsp file as well as without workspace is a related corner case
// - user can easily workaround this by calling "Restart TypeSpec Server" command
// We won't handle this case for now and may revisit this if we get more feedbacks from users.
return t.input.uri.fsPath.endsWith(".tsp");
}) >= 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;
}
7 changes: 4 additions & 3 deletions packages/typespec-vscode/src/tsp-language-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,10 @@ export class TspLanguageClient {
}
}

async start(showPopupWhenError: boolean): Promise<void> {
async start(): Promise<void> {
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 {
Expand All @@ -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,
});
}
}
Expand Down
26 changes: 25 additions & 1 deletion packages/typespec-vscode/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,36 @@ 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 {
/**
* 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<T> {
code: ResultCode.Success;
value: T;
details?: any;
}

interface UnsuccessResult {
code: ResultCode.Fail | ResultCode.Cancelled | ResultCode.Timeout;
details?: any;
}

export type Result<T> = SuccessResult<T> | UnsuccessResult;
9 changes: 5 additions & 4 deletions packages/typespec-vscode/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -293,10 +294,10 @@ export function createPromiseWithCancelAndTimeout<T>(
) {
return new Promise<T>((resolve, reject) => {
token.onCancellationRequested(() => {
reject("cancelled");
reject(ResultCode.Cancelled);
});
setTimeout(() => {
reject("timeout");
reject(ResultCode.Timeout);
}, timeoutInMs);
action.then(resolve, reject);
});
Expand Down
Loading

0 comments on commit 6e352d2

Please sign in to comment.