Skip to content

Commit

Permalink
Prompt to restart lsp if it has stopped (#575)
Browse files Browse the repository at this point in the history
  • Loading branch information
TwitchBronBron authored Jun 1, 2024
1 parent 0f22f67 commit 92b7cb3
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 12 deletions.
11 changes: 10 additions & 1 deletion src/LanguageServerManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Module.prototype.require = function hijacked(file) {

const tempDir = s`${process.cwd()}/.tmp`;

describe('extension', () => {
describe('LanguageServerManager', () => {
let languageServerManager: LanguageServerManager;

beforeEach(() => {
Expand All @@ -55,6 +55,15 @@ describe('extension', () => {
fsExtra.removeSync(tempDir);
});

describe('updateStatusbar', () => {
it('does not crash when undefined', () => {
delete languageServerManager['statusbarItem'];
//the test passes if these don't throw
languageServerManager['updateStatusbar'](true);
languageServerManager['updateStatusbar'](false);
});
});

it('registers referenceProvider', () => {
let spy = sinon.spy(vscode.languages, 'registerReferenceProvider');
expect(spy.calledOnce).to.be.false;
Expand Down
85 changes: 74 additions & 11 deletions src/LanguageServerManager.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type {
LanguageClientOptions,
ServerOptions,
ExecuteCommandParams
ExecuteCommandParams,
StateChangeEvent
} from 'vscode-languageclient/node';
import {
LanguageClient,
State,
TransportKind
} from 'vscode-languageclient/node';
import * as vscode from 'vscode';
Expand All @@ -26,6 +28,42 @@ import type { DefinitionRepository } from './DefinitionRepository';
import { util } from './util';
import { LanguageServerInfoCommand, languageServerInfoCommand } from './commands/LanguageServerInfoCommand';
import * as fsExtra from 'fs-extra';
import { EventEmitter } from 'eventemitter3';

/**
* Tracks the running/stopped state of the language server. When the lsp crashes, vscode will restart it. After the 5th crash, they'll leave it permanently crashed.
* There seems to be no time limit on adding up to the 5, so even after a few days, vscode may still terminate the language server.
* This class track when the language server is stopped and then not started back up again after a period of time.
* For example, 20 seconds after after the final failure, this event fires so that we can show a "wanna restart it" popup.
*/
class LspRunTracker {

public constructor(
public debounceDelay: number
) {
}

public setState(state: State) {
//if language server is running, clear any timers
if (state === State.Starting || state === State.Running) {
clearTimeout(this.timeoutHandle);
} else {
this.timeoutHandle = setTimeout(() => {
clearTimeout(this.timeoutHandle);
this.emitter.emit('stopped');
}, this.debounceDelay);
}
}
private timeoutHandle: NodeJS.Timeout;

private emitter = new EventEmitter();
public on(event: 'stopped', listener: () => any) {
this.emitter.on(event, listener);
return () => {
this.emitter.off(event, listener);
};
}
}

export const LANGUAGE_SERVER_NAME = 'BrighterScript Language Server';

Expand Down Expand Up @@ -53,11 +91,26 @@ export class LanguageServerManager {
public async init(
context: vscode.ExtensionContext,
definitionRepository: DefinitionRepository

) {
this.context = context;
this.definitionRepository = definitionRepository;

//if the lsp is permanently stopped by vscode, ask the user if they want to restart it again.
this.lspRunTracker.on('stopped', async () => {
//stop the statusbar spinner
this.updateStatusbar(false);
if (this.isLanguageServerEnabledInSettings()) {
const response = await vscode.window.showErrorMessage('The BrighterScript language server unexpectedly shut down. Do you want to restart it?', {
modal: true
}, { title: 'Yes' }, { title: 'No ', isCloseAffordance: true });
if (response.title === 'Yes') {
await this.restart();
}
} else {
await this.disableLanguageServer();
}
});

//dynamically enable or disable the language server based on user settings
vscode.workspace.onDidChangeConfiguration(async (configuration) => {
await this.syncVersionAndTryRun();
Expand Down Expand Up @@ -88,7 +141,9 @@ export class LanguageServerManager {
}

private client: LanguageClient;
private languageServerStatusBar: vscode.StatusBarItem;
private statusbarItem: vscode.StatusBarItem;

private lspRunTracker = new LspRunTracker(20_000);

private clientDispose: Disposable;

Expand All @@ -101,14 +156,14 @@ export class LanguageServerManager {
}
this.refreshDeferred();

//create the statusbar
this.languageServerStatusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right);
this.languageServerStatusBar.command = LanguageServerInfoCommand.commandName;
//create the statusbar item
this.statusbarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right);
this.statusbarItem.command = LanguageServerInfoCommand.commandName;

//enable the statusbar loading anmation. the language server will disable once it finishes loading
this.updateStatusbar(false);

this.languageServerStatusBar.show();
this.statusbarItem.show();

//disable the simple providers (the language server will handle all of these)
this.disableSimpleProviders();
Expand Down Expand Up @@ -164,6 +219,10 @@ export class LanguageServerManager {
// Start the client. This will also launch the server
this.clientDispose = this.client.start();
await this.client.onReady();
this.client.onDidChangeState((event: StateChangeEvent) => {
console.log(new Date().toLocaleTimeString(), 'onDidChangeState', State[event.newState]);
this.lspRunTracker.setState(event.newState);
});

this.client.onNotification('critical-failure', (message) => {
void window.showErrorMessage(message);
Expand Down Expand Up @@ -216,9 +275,13 @@ export class LanguageServerManager {
* Enable/disable the loading spinner on the statusbar item
*/
private updateStatusbar(isLoading: boolean) {
//do nothing if we don't have a statusbar
if (!this.statusbarItem) {
return;
}
const icon = isLoading ? '$(sync~spin)' : '$(flame)';
this.languageServerStatusBar.text = `${icon} bsc-${this.selectedBscInfo.version}`;
this.languageServerStatusBar.tooltip = `BrightScript Language Server: running`;
this.statusbarItem.text = `${icon} bsc-${this.selectedBscInfo.version}`;
this.statusbarItem.tooltip = `BrightScript Language Server: running`;
}

/**
Expand All @@ -234,8 +297,8 @@ export class LanguageServerManager {
private async disableLanguageServer() {
if (this.client) {
await this.client.stop();
this.languageServerStatusBar.dispose();
this.languageServerStatusBar = undefined;
this.statusbarItem.dispose();
this.statusbarItem = undefined;
this.clientDispose?.dispose();
this.client = undefined;
//delay slightly to let things catch up
Expand Down

0 comments on commit 92b7cb3

Please sign in to comment.