Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vscode): Add Language Status Item #179

Merged
merged 1 commit into from
Nov 21, 2024
Merged
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
4 changes: 4 additions & 0 deletions editor-extensions/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
{
"command": "grain.restart",
"title": "Grain: Restart Language Server"
},
{
"command": "grain.openOutput",
"title": "Grain: Show Extension Output"
}
],
"languages": [
Expand Down
55 changes: 55 additions & 0 deletions editor-extensions/vscode/src/GrainErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { LanguageStatusSeverity, type LanguageStatusItem } from "vscode";
import {
ErrorHandler,
ErrorAction,
CloseAction,
type BaseLanguageClient,
type ErrorHandlerResult,
type Message,
type CloseHandlerResult,
} from "vscode-languageclient";

// Derived from: https://github.com/microsoft/vscode-languageserver-node/blob/a561f1342ba94ad7f550cb15446f65432f5e1367/client/src/common/client.ts#L439
export default class GrainErrorHandler implements ErrorHandler {
private readonly restarts: number[];

constructor(
private name: string,
private languageStatusItem: LanguageStatusItem,
private maxRestartCount: number
) {
this.restarts = [];
}

public error(
_error: Error,
_message: Message,
count: number
): ErrorHandlerResult {
if (count && count <= 3) {
return { action: ErrorAction.Continue };
}
return { action: ErrorAction.Shutdown };
}

public closed(): CloseHandlerResult {
this.restarts.push(Date.now());
if (this.restarts.length <= this.maxRestartCount) {
return { action: CloseAction.Restart };
} else {
const diff = this.restarts[this.restarts.length - 1] - this.restarts[0];
if (diff <= 3 * 60 * 1000) {
this.languageStatusItem.severity = LanguageStatusSeverity.Error;
return {
action: CloseAction.DoNotRestart,
message: `The ${this.name} server crashed ${
this.maxRestartCount + 1
} times in the last 3 minutes. The server will not be restarted. See the output for more information.`,
};
} else {
this.restarts.shift();
return { action: CloseAction.Restart };
}
}
}
}
58 changes: 51 additions & 7 deletions editor-extensions/vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
ConfigurationChangeEvent,
Uri,
window,
type LanguageStatusItem,
} from "vscode";

import {
Expand All @@ -28,18 +29,20 @@ import {
import which from "which";

import { GrainDocCompletionProvider } from "./GrainDocCompletionProvider";
import GrainErrorHandler from "./GrainErrorHandler";

let extensionName = "Grain Language Server";

let languageId = "grain";

let outputChannel = window.createOutputChannel(extensionName, languageId);

let fileClients: Map<string, LanguageClient> = new Map();
let workspaceClients: Map<string, LanguageClient> = new Map();

let activeMutex: Set<string> = new Set();

let grainStatusBarItem: LanguageStatusItem | null = null;
let grainRestartStatusItem: LanguageStatusItem | null = null;

function mutex(key: string, fn: (...args: unknown[]) => Promise<void>) {
return (...args) => {
if (activeMutex.has(key)) return;
Expand Down Expand Up @@ -141,7 +144,7 @@ function getLspCommand(uri: Uri) {
async function startFileClient(uri: Uri) {
let [command, args] = getLspCommand(uri);

let clientOptions = {
let clientOptions: LanguageClientOptions = {
documentSelector: [
{
scheme: uri.scheme,
Expand All @@ -150,6 +153,10 @@ async function startFileClient(uri: Uri) {
},
],
outputChannel,
errorHandler:
grainStatusBarItem != null
? new GrainErrorHandler(extensionName, grainStatusBarItem, 5)
: undefined,
};

let serverOptions: ServerOptions = {
Expand Down Expand Up @@ -261,12 +268,14 @@ async function removeWorkspaceClient(workspaceFolder: WorkspaceFolder) {
}

async function restartAllClients() {
if (grainRestartStatusItem != null) grainRestartStatusItem.busy = true;
for (let client of fileClients.values()) {
await client.restart();
}
for (let client of workspaceClients.values()) {
await client.restart();
}
if (grainRestartStatusItem != null) grainRestartStatusItem.busy = false;
}

async function didOpenTextDocument(
Expand Down Expand Up @@ -352,19 +361,54 @@ async function didChangeWorkspaceFolders(event: WorkspaceFoldersChangeEvent) {
}
}

function createStatusBarEntries(context: ExtensionContext) {
// Language status bar
const statusScope = { language: languageId };

// Main extension entry
grainStatusBarItem = languages.createLanguageStatusItem("grain", statusScope);
grainStatusBarItem.busy = false;
grainStatusBarItem.name = "grain";
grainStatusBarItem.text = "grain";
grainStatusBarItem.command = {
title: "Open Grain Output",
command: "grain.openOutput",
};
context.subscriptions.push(grainStatusBarItem);

// Restart command entry
grainRestartStatusItem = languages.createLanguageStatusItem(
"grain.restart",
statusScope
);
grainStatusBarItem.busy = false;
grainRestartStatusItem.name = "grain.restart";
grainRestartStatusItem.text = "grain";
grainRestartStatusItem.command = {
title: "Restart Extension",
command: "grain.restart",
};
context.subscriptions.push(grainRestartStatusItem);
}

export async function activate(context: ExtensionContext): Promise<void> {
let didOpenTextDocument$ =
const didOpenTextDocument$ =
workspace.onDidOpenTextDocument(didOpenTextDocument);
let didChangeWorkspaceFolders$ = workspace.onDidChangeWorkspaceFolders(
const didChangeWorkspaceFolders$ = workspace.onDidChangeWorkspaceFolders(
didChangeWorkspaceFolders
);
let restart$ = commands.registerCommand("grain.restart", restartAllClients);
const restart$ = commands.registerCommand("grain.restart", restartAllClients);
const output$ = commands.registerCommand("grain.openOutput", () => {
outputChannel.show();
});

context.subscriptions.push(
didOpenTextDocument$,
didChangeWorkspaceFolders$,
restart$
restart$,
output$
);
createStatusBarEntries(context);

for (let doc of workspace.textDocuments) {
const disposable = await didOpenTextDocument(doc);
Expand Down
Loading