lsp: support OpenGOAL and improve the lsp local story

This commit is contained in:
Tyler Wilding 2023-05-21 17:27:29 -04:00
parent 1995dfb878
commit 27110517db
No known key found for this signature in database
GPG key ID: C500E15300515B67

View file

@ -1,65 +1,98 @@
import * as vscode from "vscode"; import * as vscode from "vscode";
import * as path from "path"; import * as path from "path";
import { import {
BaseLanguageClient,
ClientCapabilities,
FeatureState,
LanguageClient, LanguageClient,
LanguageClientOptions, LanguageClientOptions,
ServerOptions, ServerOptions,
StaticFeature,
TransportKind, TransportKind,
WorkDoneProgress,
WorkDoneProgressCreateRequest,
} from "vscode-languageclient/node"; } from "vscode-languageclient/node";
import { getConfig } from "../config/config"; import { getConfig } from "../config/config";
import { downloadLsp } from "./download"; import { downloadLsp } from "./download";
import { getLatestVersion, getLspPath, getVersionFromMetaFile } from "./util"; import { getLatestVersion, getLspPath, getVersionFromMetaFile } from "./util";
import * as fs from "fs";
import { disposeAll } from "../vendor/vscode-pdfviewer/disposable";
let extensionContext: vscode.ExtensionContext; let extensionContext: vscode.ExtensionContext;
let opengoalLspPath: string | undefined; let opengoalLspPath: string | undefined;
let activeClient: LanguageClient | undefined; let activeClient: LanguageClient | undefined;
const lspStatusItem = vscode.window.createStatusBarItem( type LspStatus =
vscode.StatusBarAlignment.Left,
0
);
export type LspStatus =
| "stopped" | "stopped"
| "starting" | "starting"
| "started" | "started"
| "downloading" | "downloading"
| "error"; | "error"
export let lspStatus: LspStatus = "stopped"; | "serverProgressBegin"
| "serverProgressEnd";
function updateStatus(status: LspStatus, extraInfo?: string) { // TODO - rust analyzer's context menu on hover is nice
lspStatus = status; class LSPStatusItem {
switch (status) { private currentStatus: LspStatus = "stopped";
case "stopped":
lspStatusItem.text = "$(circle-outline) OpenGOAL LSP Stopped"; constructor(private readonly statusItem: vscode.StatusBarItem) {}
lspStatusItem.tooltip = "Launch LSP";
lspStatusItem.command = "opengoal.lsp.start"; public updateStatus(status: LspStatus, extraInfo?: string) {
break; this.currentStatus = status;
case "starting": switch (this.currentStatus) {
lspStatusItem.text = "$(loading~spin) OpenGOAL LSP Starting"; case "stopped":
lspStatusItem.tooltip = "LSP Starting"; this.statusItem.text = "$(circle-outline) OpenGOAL LSP Stopped";
lspStatusItem.command = undefined; this.statusItem.tooltip = "Launch LSP";
break; this.statusItem.command = "opengoal.lsp.start";
case "started": break;
lspStatusItem.text = "$(circle-filled) OpenGOAL LSP Ready"; case "starting":
lspStatusItem.tooltip = `LSP Active - ${extraInfo}`; this.statusItem.text = "$(loading~spin) OpenGOAL LSP Starting";
lspStatusItem.command = "opengoal.lsp.showLspStartedMenu"; this.statusItem.tooltip = "LSP Starting";
break; this.statusItem.command = undefined;
case "downloading": break;
lspStatusItem.text = `$(sync~spin) OpenGOAL LSP Downloading - ${extraInfo}`; case "started":
lspStatusItem.tooltip = `Downloading version - ${extraInfo}`; this.statusItem.text = "$(circle-filled) OpenGOAL LSP Ready";
lspStatusItem.command = undefined; this.statusItem.tooltip = `LSP Active - ${extraInfo}`;
break; this.statusItem.command = "opengoal.lsp.showLspStartedMenu";
case "error": break;
lspStatusItem.text = "$(error) OpenGOAL LSP Error"; case "downloading":
lspStatusItem.tooltip = "LSP not running due to an error"; this.statusItem.text = `$(sync~spin) OpenGOAL LSP Downloading - ${extraInfo}`;
lspStatusItem.command = undefined; this.statusItem.tooltip = `Downloading version - ${extraInfo}`;
break; this.statusItem.command = undefined;
default: break;
break; case "error":
this.statusItem.text = "$(error) OpenGOAL LSP Error";
this.statusItem.tooltip = "LSP not running due to an error";
this.statusItem.command = undefined;
break;
case "serverProgressBegin":
this.statusItem.text = `$(loading~spin) ${extraInfo}`;
this.statusItem.tooltip = extraInfo;
this.statusItem.command = "opengoal.lsp.showLspStartedMenu";
break;
case "serverProgressEnd":
this.statusItem.text = `$(circle-filled) ${extraInfo}`;
this.statusItem.tooltip = extraInfo;
this.statusItem.command = "opengoal.lsp.showLspStartedMenu";
break;
default:
break;
}
}
public hide() {
this.statusItem.hide();
}
public show() {
this.statusItem.show();
} }
} }
const statusItem = new LSPStatusItem(
vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 0)
);
async function ensureServerDownloaded(): Promise<string | undefined> { async function ensureServerDownloaded(): Promise<string | undefined> {
const installedVersion = getVersionFromMetaFile( const installedVersion = getVersionFromMetaFile(
extensionContext.extensionPath extensionContext.extensionPath
@ -94,15 +127,15 @@ async function ensureServerDownloaded(): Promise<string | undefined> {
} }
// Install the LSP and update the version metadata file // Install the LSP and update the version metadata file
updateStatus("downloading", versionToDownload); statusItem.updateStatus("downloading", versionToDownload);
const newLspPath = await downloadLsp( const newLspPath = await downloadLsp(
extensionContext.extensionPath, extensionContext.extensionPath,
versionToDownload versionToDownload
); );
if (newLspPath === undefined) { if (newLspPath === undefined) {
updateStatus("error"); statusItem.updateStatus("error");
} else { } else {
updateStatus("stopped"); statusItem.updateStatus("stopped");
} }
return newLspPath; return newLspPath;
} }
@ -113,7 +146,13 @@ async function maybeDownloadLspServer(): Promise<void> {
userConfiguredOpengoalLspPath !== "" && userConfiguredOpengoalLspPath !== "" &&
userConfiguredOpengoalLspPath !== undefined userConfiguredOpengoalLspPath !== undefined
) { ) {
opengoalLspPath = userConfiguredOpengoalLspPath; // Copy the binary to the extension directory so it doesn't block future compilations
const lspPath = path.join(
extensionContext.extensionPath,
`opengoal-lsp-local.bin`
);
fs.copyFileSync(userConfiguredOpengoalLspPath, lspPath);
opengoalLspPath = lspPath;
} else { } else {
opengoalLspPath = await ensureServerDownloaded(); opengoalLspPath = await ensureServerDownloaded();
} }
@ -146,6 +185,7 @@ function createClient(lspPath: string): LanguageClient {
], ],
synchronize: { synchronize: {
fileEvents: [ fileEvents: [
vscode.workspace.createFileSystemWatcher("**/*.gc"),
vscode.workspace.createFileSystemWatcher("**/*_ir2.asm"), vscode.workspace.createFileSystemWatcher("**/*_ir2.asm"),
vscode.workspace.createFileSystemWatcher("**/all-types.gc"), vscode.workspace.createFileSystemWatcher("**/all-types.gc"),
], ],
@ -167,7 +207,7 @@ function createClient(lspPath: string): LanguageClient {
} }
async function stopClient() { async function stopClient() {
updateStatus("stopped"); statusItem.updateStatus("stopped");
if (activeClient !== undefined) { if (activeClient !== undefined) {
console.log("Stopping opengoal-lsp"); console.log("Stopping opengoal-lsp");
return await activeClient return await activeClient
@ -181,29 +221,69 @@ async function stopClient() {
} }
} }
class StatusBarFeature implements StaticFeature {
private requestHandlers: vscode.Disposable[] = [];
public fillClientCapabilities(capabilities: ClientCapabilities): void {
if (!capabilities.window) {
capabilities.window = {};
}
capabilities.window.workDoneProgress = true;
}
constructor(private readonly client: BaseLanguageClient) {}
public getState(): FeatureState {
return { kind: "static" };
}
public dispose(): void {
// nothing to dispose here
}
public initialize(): void {
this.requestHandlers.push(
this.client.onRequest(WorkDoneProgressCreateRequest.type, ({ token }) => {
this.client.onProgress(WorkDoneProgress.type, token, (progress) => {
if (progress.kind === "begin") {
statusItem.updateStatus("serverProgressBegin", progress.title);
}
if (progress.kind === "report") {
// do nothing right now, goalc provides no feedback on it's status
}
if (progress.kind === "end") {
statusItem.updateStatus("serverProgressEnd", progress.message);
disposeAll(this.requestHandlers);
}
});
})
);
}
}
async function startClient(): Promise<void> { async function startClient(): Promise<void> {
if (opengoalLspPath === undefined) { if (opengoalLspPath === undefined) {
return; return;
} }
const client = createClient(opengoalLspPath); const client = createClient(opengoalLspPath);
client.registerFeature(new StatusBarFeature(client));
console.log("Starting opengoal-lsp at", opengoalLspPath); console.log("Starting opengoal-lsp at", opengoalLspPath);
// TODO - some form of startup test would be nice // TODO - some form of startup test would be nice
try { try {
updateStatus("starting"); statusItem.updateStatus("starting");
await client.start(); await client.start();
activeClient = client; activeClient = client;
updateStatus("started", path.basename(opengoalLspPath)); statusItem.updateStatus("started", path.basename(opengoalLspPath));
} catch (error) { } catch (error) {
console.error("opengoal-lsp:", error); console.error("opengoal-lsp:", error);
updateStatus("error"); statusItem.updateStatus("error");
lspStatusItem.hide(); statusItem.hide();
await stopClient(); await stopClient();
} }
} }
export async function startClientCommand() { export async function startClientCommand() {
lspStatusItem.show(); statusItem.show();
await maybeDownloadLspServer(); await maybeDownloadLspServer();
if (opengoalLspPath !== undefined) { if (opengoalLspPath !== undefined) {
await startClient(); await startClient();
@ -211,7 +291,10 @@ export async function startClientCommand() {
} }
async function restartClient() { async function restartClient() {
console.log("Stopping opengoal-lsp - restart");
await stopClient(); await stopClient();
await new Promise((f) => setTimeout(f, 2000));
console.log("Starting opengoal-lsp - restart");
await startClientCommand(); await startClientCommand();
} }
@ -276,8 +359,8 @@ export async function activate(
registerLifeCycleCommands(context); registerLifeCycleCommands(context);
// TODO - add info and open log file options // TODO - add info and open log file options
// registerDiagnosticsCommands(context); // registerDiagnosticsCommands(context);
updateStatus("stopped"); statusItem.updateStatus("stopped");
lspStatusItem.show(); statusItem.show();
const config = getConfig(); const config = getConfig();
if (config.launchLspOnStartup) { if (config.launchLspOnStartup) {
await startClientCommand(); await startClientCommand();