Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion editors/vscode/l10n/bundle.l10n.zh-cn.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"Vide output channel has not been initialized.": "Vide 输出通道尚未初始化。",
"Vide Qihe output channel has not been initialized.": "Vide Qihe 输出通道尚未初始化。",
"Vide language server is starting.": "Vide 语言服务器正在启动。",
"Vide": "Vide",
"Vide language server is running.": "Vide 语言服务器正在运行。",
Expand Down
1 change: 1 addition & 0 deletions editors/vscode/scripts/package/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export function stageDistFilesForTarget(context: PackageContext, plan: PackagePl
const distDir = path.join(context.vscodeDir, 'dist');
if (plan.targetSpec.kind === 'web') {
fs.rmSync(path.join(distDir, 'extension.js'), { force: true });
fs.rmSync(path.join(distDir, 'node'), { recursive: true, force: true });
return;
}

Expand Down
160 changes: 15 additions & 145 deletions editors/vscode/src/browser/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
type MessageTransports,
} from "vscode-languageclient/browser";

import { videInitializationOptions } from "../../../../packages/vide-extension-shared/src/config/initialization-options";
import type {
LspTraceEntry,
WorkerRequest,
Expand All @@ -20,12 +19,11 @@ import {
BROWSER_WORKSPACE_FOLDER_NAME,
type BrowserWorkspaceSnapshot,
} from "./workspaceSnapshot";
import { createVideClientOptionsCore } from "../common/clientOptionsCore";
import { createVideDocumentSelector } from "../common/documentSelector";
import { createProvideExpandedRenameEdits } from "../common/renameMiddleware";

const CLIENT_DISPOSED_MESSAGE = "Vide browser client has been disposed.";
const RENAME_EXPANSION_INFO_REQUEST =
"vide.server.renameExpansionInfo";
const EXPANDED_RENAME_REQUEST = "vide.server.expandedRename";
const RENAME_CONFLICT_INFO_REQUEST = "vide.server.renameConflictInfo";

export class VideBrowserClient {
private readonly worker: Worker;
Expand Down Expand Up @@ -125,19 +123,23 @@ export class VideBrowserClient {
}

private clientOptions(): LanguageClientOptions {
const provideRenameEdits = createProvideExpandedRenameEdits(
() => this.requireLanguageClient(),
(message) => this.onLog(message, "warn"),
);
const coreOptions = createVideClientOptionsCore({
documentSelector: createVideDocumentSelector(),
configuration: vscode.workspace.getConfiguration("vide"),
provideRenameEdits,
});

return {
documentSelector: [
{ language: "verilog" },
{ language: "systemverilog" },
],
...coreOptions,
workspaceFolder: {
index: 0,
name: BROWSER_WORKSPACE_FOLDER_NAME,
uri: vscode.Uri.parse(this.snapshot.rootUri),
},
initializationOptions: videInitializationOptions(
vscode.workspace.getConfiguration("vide"),
),
diagnosticPullOptions: {
onChange: false,
onSave: false,
Expand All @@ -151,96 +153,10 @@ export class VideBrowserClient {
closed: () => ({ action: CloseAction.DoNotRestart }),
},
middleware: {
...coreOptions.middleware,
handleDiagnostics: (uri, diagnostics, next) => {
next(uri, diagnostics);
},
provideRenameEdits: async (document, position, newName, token, next) => {
const languageClient = this.requireLanguageClient();
const textDocumentPosition = {
textDocument:
languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document),
position: languageClient.code2ProtocolConverter.asPosition(position),
};
const standardRename = async () => {
if (
!(await confirmRenameCollision(
languageClient,
textDocumentPosition,
newName,
false,
token,
))
) {
return emptyRenameEdit();
}
return await next(document, position, newName, token);
};

let info: RenameExpansionInfo | undefined;
try {
info = await languageClient.sendRequest<RenameExpansionInfo>(
"workspace/executeCommand",
{
command: RENAME_EXPANSION_INFO_REQUEST,
arguments: [{ textDocumentPosition }],
},
token,
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.onLog(`Falling back to standard rename: ${message}`, "warn");
}

if (!info || info.additionalSymbols === 0) {
return await standardRename();
}

const recursiveAction = vscode.l10n.t(
"Rename Connected Ports/Signals",
);
const localAction = vscode.l10n.t("Only This Symbol");
const selected = await vscode.window.showInformationMessage(
vscode.l10n.t(
"Rename {0} connected port/signal symbol(s) as well?",
info.additionalSymbols,
),
recursiveAction,
localAction,
);

if (selected === localAction) {
return await standardRename();
}

if (selected !== recursiveAction) {
return emptyRenameEdit();
}

if (
!(await confirmRenameCollision(
languageClient,
textDocumentPosition,
newName,
true,
token,
))
) {
return emptyRenameEdit();
}

const edit = await languageClient.sendRequest(
"workspace/executeCommand",
{
command: EXPANDED_RENAME_REQUEST,
arguments: [{ textDocumentPosition, newName }],
},
token,
);
return await languageClient.protocol2CodeConverter.asWorkspaceEdit(
edit as never,
token,
);
},
workspace: {
configuration: () => [],
},
Expand Down Expand Up @@ -298,52 +214,6 @@ export class VideBrowserClient {
}
}

type RenameExpansionInfo = {
additionalSymbols: number;
};

type RenameConflictInfo = {
conflicts: number;
};

function emptyRenameEdit(): vscode.WorkspaceEdit {
return new vscode.WorkspaceEdit();
}

async function confirmRenameCollision(
languageClient: VideLanguageClient,
textDocumentPosition: unknown,
newName: string,
recursive: boolean,
token: vscode.CancellationToken,
): Promise<boolean> {
const info = await languageClient.sendRequest<RenameConflictInfo>(
"workspace/executeCommand",
{
command: RENAME_CONFLICT_INFO_REQUEST,
arguments: [{ textDocumentPosition, newName, recursive }],
},
token,
);

if (info.conflicts === 0) {
return true;
}

const continueAction = vscode.l10n.t("Continue Rename");
const cancelAction = vscode.l10n.t("Cancel");
const selected = await vscode.window.showWarningMessage(
vscode.l10n.t(
'Renaming to "{0}" may collide with {1} existing symbol(s).',
newName,
info.conflicts,
),
continueAction,
cancelAction,
);
return selected === continueAction;
}

class VideLanguageClient extends BaseLanguageClient {
constructor(
clientOptions: LanguageClientOptions,
Expand Down
141 changes: 141 additions & 0 deletions editors/vscode/src/browser/clientController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import * as vscode from "vscode";

import type { ServerStatus } from "../status";
import { projectStatusNotification } from "../videStatus";
import { VideBrowserClient } from "./client";
import { buildBrowserWorkspaceSnapshot } from "./workspaceSnapshot";

type Logger = (message: string) => void;

export class BrowserClientController {
private client: VideBrowserClient | undefined;
private restartChain: Promise<void> = Promise.resolve();
private workspaceRestartTimer: ReturnType<typeof setTimeout> | undefined;

constructor(
private readonly context: vscode.ExtensionContext,
private readonly options: {
log: Logger;
updateServerStatus: (status: ServerStatus, detail?: string) => void;
showLanguageServerErrorMessage: (message: string) => Promise<void>;
handleProjectStatusNotification: (params: unknown) => void;
},
) {}

getClient(): VideBrowserClient | undefined {
return this.client;
}

hasClient(): boolean {
return this.client !== undefined;
}

initializeServerInfo(): { name?: string; version?: string } | undefined {
return this.client?.initializeServerInfo();
}

queueRestart(reason: string): Promise<void> {
this.restartChain = this.restartChain
.catch(() => undefined)
.then(async () => {
this.options.log(`[INFO] Restarting browser language client: ${reason}`);
await this.stopClient();
await this.startClient();
});
return this.restartChain;
}

scheduleRestart(reason: string): void {
if (this.workspaceRestartTimer) {
clearTimeout(this.workspaceRestartTimer);
}
this.workspaceRestartTimer = setTimeout(() => {
this.workspaceRestartTimer = undefined;
void this.queueRestart(reason);
}, 250);
}

async stop(): Promise<void> {
this.clearScheduledRestart();
await this.stopClient();
}

private async startClient(): Promise<void> {
this.options.updateServerStatus("starting");
this.options.log("[INFO] Building browser workspace snapshot...");

let startedClient: VideBrowserClient | undefined;
try {
const snapshot = await buildBrowserWorkspaceSnapshot(this.options.log);
const browserClient = new VideBrowserClient(this.context, snapshot);
startedClient = browserClient;
this.client = browserClient;

browserClient.onStatus = (status) => {
if (!this.isActiveClient(browserClient)) {
return;
}
this.options.updateServerStatus(status.ready ? "ready" : "error", status.detail);
};
browserClient.onServerCapabilities = () => undefined;
browserClient.onLog = (message, level) => {
if (!this.isActiveClient(browserClient)) {
return;
}
this.options.log(`[${level.toUpperCase()}] ${message}`);
};
browserClient.onTrace = (entry) => {
if (!this.isActiveClient(browserClient)) {
return;
}
this.options.log(`[TRACE] ${entry.direction} ${entry.method} ${entry.detail}`);
};

browserClient.start();
browserClient.onNotification(projectStatusNotification, (params) => {
if (!this.isActiveClient(browserClient)) {
return;
}
this.options.handleProjectStatusNotification(params);
});
this.options.log("[INFO] Browser language client booted.");
} catch (error) {
if (!startedClient || this.isActiveClient(startedClient)) {
this.client = undefined;
}
const message =
error instanceof Error
? error.message
: "Failed to start the Vide browser extension.";
this.options.log(`[ERROR] ${message}`);
this.options.updateServerStatus("error", message);
await this.options.showLanguageServerErrorMessage(
vscode.l10n.t("Failed to start Vide Language Server: {0}", message),
);
}
}

private async stopClient(): Promise<void> {
if (!this.client) {
this.options.updateServerStatus("stopped");
return;
}

this.options.updateServerStatus("stopping");
this.client.dispose();
this.client = undefined;
this.options.updateServerStatus("stopped");
}

private isActiveClient(browserClient: VideBrowserClient): boolean {
return this.client === browserClient;
}

private clearScheduledRestart(): void {
if (!this.workspaceRestartTimer) {
return;
}
clearTimeout(this.workspaceRestartTimer);
this.workspaceRestartTimer = undefined;
}
}
13 changes: 13 additions & 0 deletions editors/vscode/src/browser/configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type * as vscode from "vscode";

import { USER_CONFIG_SETTINGS } from "../generated/configuration";

const browserRestartConfigurationKeys = USER_CONFIG_SETTINGS.map(
(setting) => setting.vscodeKey,
);

export function affectsBrowserClientConfiguration(
event: Pick<vscode.ConfigurationChangeEvent, "affectsConfiguration">,
): boolean {
return browserRestartConfigurationKeys.some((key) => event.affectsConfiguration(key));
}
Loading
Loading