Skip to content

Commit 29e2716

Browse files
Refactor LSP Initialization to avoid blocking on update prompt (#69)
* draft of rearranging active order * add Hole initialization * make sure refactoring does include all comments * add comments indicating the wrongful `getEffektVersion` * Introduce new OutputChannel for the Main extension and log instead of showing the user more messages * extract fetching the Effekt version to `getEffektVersion` * Move the npm search of the Effekt executable into `locateEffektExecutable` * Remove unneccessary comments * Revert changes regarding npmRoot executable search * Finalize Revert * whitespace :) * more whitescpace fixes * WIP * Refactor and Rearrange Extension activation to handle cases of no effekt exe, old effekt exe and current effekt exe without blocking lsp * Manually fix whitespaces / formatting * Minor stylistic fixes --------- Co-authored-by: Tim Süberkrüb <dev@timsueberkrueb.io>
1 parent a6872d0 commit 29e2716

3 files changed

Lines changed: 160 additions & 94 deletions

File tree

src/effektLanguageClient.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { LanguageClient } from 'vscode-languageclient/node';
2+
3+
/*
4+
* Overrides the `registerFeature` method to disable the built-in inlay hints feature.
5+
*
6+
* By default the LanguageClient provides inlay hints automatically, which does not allow
7+
* for filtering Inlay Hints based on their 'data'-field. We use the 'data'-field to allow
8+
* the user to select which inlay hints the extension should show.
9+
*
10+
* By doing this, we retain full control over how inlay hints are displayed, allowing us to
11+
* implement custom logic.
12+
*
13+
* Note: This approach relies on identifying the inlay hints feature by its constructor name
14+
* (`InlayHintsFeature`). If the LSP implementation changes, this logic may need to be updated.
15+
*/
16+
export class EffektLanguageClient extends LanguageClient {
17+
public registerFeature(feature: any) {
18+
if (feature.constructor.name === 'InlayHintsFeature') {
19+
return;
20+
}
21+
super.registerFeature(feature);
22+
}
23+
}

src/effektManager.ts

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { compare as compareVersion } from 'compare-versions';
55
import { URL } from 'url';
66
import * as path from 'path';
77
import * as fs from 'fs/promises';
8+
import { EffektLanguageClient } from './effektLanguageClient';
89

910
interface InstallationResult {
1011
success: boolean;
@@ -18,6 +19,13 @@ interface EffektExecutableInfo {
1819
version: string;
1920
}
2021

22+
export class EffektExecutableNotFoundError extends Error {
23+
constructor(message: string = "Effekt executable not found") {
24+
super(message);
25+
this.name = "EffektExecutableNotFoundError";
26+
}
27+
}
28+
2129
/**
2230
* Manages Effekt installation, updates, and status within VS Code.
2331
*/
@@ -83,6 +91,15 @@ export class EffektManager {
8391
throw new Error("Unable to determine Effekt version");
8492
}
8593

94+
public async getEffektVersion(): Promise<string> {
95+
if (!this.effektVersion) {
96+
const effektPath = await this.locateEffektExecutable();
97+
const currentVersion = await this.fetchEffektVersion(effektPath.path);
98+
this.effektVersion = currentVersion;
99+
}
100+
return this.effektVersion || '';
101+
}
102+
86103
/**
87104
* Executes a shell command and returns the output.
88105
* @param command The command to execute.
@@ -161,16 +178,15 @@ export class EffektManager {
161178
}
162179
}
163180

164-
throw new Error('Effekt executable not found');
181+
throw new EffektExecutableNotFoundError('Effekt executable not found');
165182
}
166183

167184
/**
168185
* Installs or updates Effekt.
169-
* @param version The version to install or update to.
170186
* @param action The action being performed ('install' or 'update').
171187
* @returns A promise that resolves with the installed/updated version or an empty string.
172188
*/
173-
private async installOrUpdateEffekt(version: string, action: 'install' | 'update'): Promise<string> {
189+
private async installOrUpdateEffekt(action: 'install' | 'update', client?: EffektLanguageClient): Promise<string> {
174190
if (!(await this.checkJava())) {
175191
this.logMessage('INFO', 'Java is not installed.');
176192
return '';
@@ -186,6 +202,8 @@ export class EffektManager {
186202
cancellable: false
187203
}, async (progress) => {
188204
try {
205+
// The client is optional as when installing Effekt, there is no LSP initialized yet
206+
await client?.stop();
189207
progress.report({ increment: 0, message: 'Preparing...' });
190208
await this.runNpmInstall();
191209
progress.report({ increment: 50, message: 'Verifying installation...' });
@@ -194,10 +212,14 @@ export class EffektManager {
194212
progress.report({ increment: 100, message: 'Completed' });
195213

196214
this.handleInstallationResult(verificationResult, action);
215+
216+
await client?.start();
197217
return verificationResult.success ? verificationResult.version || '' : '';
198218
} catch (error) {
199219
this.showErrorWithLogs(`Failed to ${action} Effekt: ${error}`);
200220
this.updateStatusBar();
221+
222+
await client?.start();
201223
return '';
202224
}
203225
});
@@ -269,20 +291,20 @@ export class EffektManager {
269291
const baseMessage = `Effekt has been ${action === 'update' ? 'updated' : 'installed'} to version ${result.version}.`;
270292

271293
if (result.executable && !result.executable.includes(path.sep)) {
272-
// Effekt is in PATH
294+
// Effekt is in PATH
273295
const isUpdate = action === 'update';
274-
296+
275297
const options = isUpdate ? ['View Release Notes', 'Close'] : ['View Language Introduction', 'Close'];
276298
const changelogResponse = await vscode.window.showInformationMessage(baseMessage, ...options);
277-
299+
278300
if (changelogResponse === 'View Release Notes') {
279301
const changelogUrl = `https://github.com/effekt-lang/effekt/releases/tag/v${result.version}`;
280302
vscode.env.openExternal(vscode.Uri.parse(changelogUrl));
281303
} else if (changelogResponse === 'View Language Introduction') {
282304
const introUrl = 'https://effekt-lang.org/docs/introduction';
283305
vscode.env.openExternal(vscode.Uri.parse(introUrl));
284306
}
285-
307+
286308
} else {
287309
// Effekt is not in PATH
288310
const fullMessage = `${baseMessage}\n${result.message}\nConsider adding it to your PATH for easier access.`;
@@ -306,29 +328,24 @@ export class EffektManager {
306328
* Checks for Effekt updates and offers to install/update if necessary.
307329
* @returns A promise that resolves with the current Effekt version.
308330
*/
309-
public async checkForUpdatesAndInstall(): Promise<string> {
331+
public async checkForUpdatesAndInstall(client?: EffektLanguageClient): Promise<string> {
310332
try {
311-
const effektPath = await this.locateEffektExecutable();
312-
if (!this.effektVersion) {
313-
const currentVersion = await this.fetchEffektVersion(effektPath.path);
314-
this.effektVersion = currentVersion;
315-
}
316-
333+
let currentVersion = await this.getEffektVersion();
317334
const latestVersion = await this.getLatestNPMVersion(this.effektNPMPackage);
318335

319336
// check if the latest version strictly newer than the current version
320-
if (!this.effektVersion || compareVersion(latestVersion, this.effektVersion, '>')) {
321-
return this.promptForAction(latestVersion, 'update');
337+
if (!currentVersion || compareVersion(latestVersion, currentVersion, '>')) {
338+
return this.promptForAction(latestVersion, 'update', client);
322339
} else {
323-
vscode.window.showInformationMessage(`Effekt is up-to-date (version ${this.effektVersion}).`);
340+
vscode.window.showInformationMessage(`Effekt is up-to-date (version ${currentVersion}).`);
324341
}
325342

326343
this.updateStatusBar();
327344
return this.effektVersion || '';
328345
} catch (error) {
329346
if (error instanceof Error && error.message.includes('Effekt executable not found')) {
330347
const latestVersion = await this.getLatestNPMVersion(this.effektNPMPackage);
331-
return this.promptForAction(latestVersion, 'install');
348+
return this.promptForAction(latestVersion, 'install', client);
332349
} else {
333350
this.showErrorWithLogs(`Failed to check Effekt: ${error}`);
334351
return '';
@@ -342,14 +359,14 @@ export class EffektManager {
342359
* @param action The action to perform ('install' or 'update').
343360
* @returns A promise that resolves with the installed/updated version or an empty string.
344361
*/
345-
private async promptForAction(version: string, action: 'install' | 'update'): Promise<string> {
362+
private async promptForAction(version: string, action: 'install' | 'update', client?: EffektLanguageClient): Promise<string> {
346363
const message = action === 'update'
347364
? `A new version of Effekt is available (${version}). Would you like to update?`
348365
: `Effekt ${version} is available. Would you like to install it?`;
349366

350367
const response = await vscode.window.showInformationMessage(message, 'Yes', 'No');
351368
if (response === 'Yes') {
352-
return this.installOrUpdateEffekt(version, action);
369+
return this.installOrUpdateEffekt(action, client);
353370
}
354371
this.updateStatusBar();
355372
return this.effektVersion || '';

0 commit comments

Comments
 (0)