Skip to content

Commit

Permalink
Add support for loading lsp from npm or url
Browse files Browse the repository at this point in the history
  • Loading branch information
TwitchBronBron committed Jul 30, 2024
1 parent 4bd3d8d commit 83eeef0
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 58 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
"chalk": "^4.1.2",
"changelog-parser": "^2.8.0",
"coveralls-next": "^4.2.0",
"dayjs": "^1.11.7",
"dayjs": "^1.11.12",
"deferred": "^0.7.11",
"eslint": "^8.10.0",
"eslint-plugin-github": "^4.3.5",
Expand Down
4 changes: 2 additions & 2 deletions src/ActiveDeviceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,8 @@ class RokuFinder extends EventEmitter {
}

private readonly client: Client;
private intervalId: NodeJS.Timer | null = null;
private timeoutId: NodeJS.Timer | null = null;
private intervalId: NodeJS.Timeout | null = null;
private timeoutId: NodeJS.Timeout | null = null;
private running = false;

public start(timeout: number) {
Expand Down
4 changes: 2 additions & 2 deletions src/LanguageServerManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ describe('LanguageServerManager', () => {
//disable starting so we can manually test
sinon.stub(languageServerManager, 'syncVersionAndTryRun').callsFake(() => Promise.resolve());

await languageServerManager.init(languageServerManager['context'], languageServerManager['definitionRepository'], languageServerManager['logger']);
await languageServerManager.init(languageServerManager['context'], languageServerManager['definitionRepository']);

languageServerManager['lspRunTracker'].debounceDelay = 100;

Expand Down Expand Up @@ -325,7 +325,7 @@ describe('LanguageServerManager', () => {
});
});

describe.only('ensureBscVersionInstalled', function() {
describe('ensureBscVersionInstalled', function() {
//these tests take a long time (due to running `npm install`)
this.timeout(20_000);

Expand Down
133 changes: 85 additions & 48 deletions src/LanguageServerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { LanguageServerInfoCommand, languageServerInfoCommand } from './commands
import * as fsExtra from 'fs-extra';
import { EventEmitter } from 'eventemitter3';
import * as childProcess from 'child_process';
import * as semver from 'semver';

/**
* 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.
Expand Down Expand Up @@ -90,13 +91,10 @@ export class LanguageServerManager {
return this.definitionRepository.provider;
}

private logger: Logger;
public async init(
context: vscode.ExtensionContext,
definitionRepository: DefinitionRepository,
logger: Logger
definitionRepository: DefinitionRepository
) {
this.logger = logger;
this.context = context;
this.definitionRepository = definitionRepository;

Expand Down Expand Up @@ -391,7 +389,6 @@ export class LanguageServerManager {
* and if different, re-launch the specific version of the language server'
*/
public async syncVersionAndTryRun() {
await this.ensureBscVersionInstalled('0.67.3');
const bsdkPath = await this.getBsdkPath();

//if the path to bsc is different, spin down the old server and start a new one
Expand Down Expand Up @@ -421,27 +418,34 @@ export class LanguageServerManager {
}

/**
* Get the full path to the brighterscript module where the LanguageServer should be run
* Get the full path to the brighterscript module where the LanguageServer should be run.
* If `brightscript.bsdk` is a version number or a URL, install that version in global storage and return that path.
* if it's a relative path, resolve it to the workspace folder and return that path.
*/
private async getBsdkPath() {
//if there's a bsdk entry in the workspace settings, assume the path is relative to the workspace
if (this.workspaceConfigIncludesBsdkKey()) {
let bsdk = vscode.workspace.getConfiguration('brightscript', vscode.workspace.workspaceFile).get<string>('bsdk');
return bsdk === 'embedded'
? this.embeddedBscInfo.path
: path.resolve(path.dirname(vscode.workspace.workspaceFile.fsPath), bsdk);
if (bsdk === 'embedded') {
return this.embeddedBscInfo.path;
}
let bscPath = await this.ensureBscVersionInstalled(bsdk);
return path.resolve(path.dirname(vscode.workspace.workspaceFile.fsPath), bscPath);
}

const folderResults = new Set<string>();
//look for a bsdk entry in each of the workspace folders
for (const folder of vscode.workspace.workspaceFolders) {
const bsdk = vscode.workspace.getConfiguration('brightscript', folder).get<string>('bsdk');
if (bsdk) {
folderResults.add(
bsdk === 'embedded'
? this.embeddedBscInfo.path
: path.resolve(folder.uri.fsPath, bsdk)
);
if (bsdk === 'embedded') {
folderResults.add(this.embeddedBscInfo.path);
} else {
let bscPath = await this.ensureBscVersionInstalled(bsdk);
folderResults.add(
path.resolve(folder.uri.fsPath, bscPath)
);
}
}
}
const values = [...folderResults.values()];
Expand Down Expand Up @@ -473,42 +477,75 @@ export class LanguageServerManager {
* @param retryCount the number of times we should retry before giving up
* @returns full path to the root of where the brighterscript module is installed
*/
private async ensureBscVersionInstalled(version: string, retryCount = 1) {
console.log('Ensuring bsc version is installed', version);
const bscNpmDir = s`${this.context.globalStorageUri.fsPath}/packages/brighterscript-${version}`;
if (await fsExtra.pathExists(bscNpmDir) === false) {
//write a simple package.json file referencing the version of brighterscript we want
await fsExtra.outputJson(`${bscNpmDir}/package.json`, {
name: 'vscode-brighterscript-host',
private: true,
version: '1.0.0',
dependencies: {
'brighterscript': version
}
});
await new Promise<void>((resolve, reject) => {
const process = childProcess.exec(`npm install`, {
cwd: bscNpmDir
});
process.on('error', (err) => {
console.error(err);
reject(err);
});
process.on('close', (code) => {
if (code === 0) {
resolve();
private async ensureBscVersionInstalled(bsdkEntry: string, retryCount = 1, showProgress = true): Promise<string> {
let folderName: string;
let packageJsonEntry: string;
//if this is a URL
if (/^(http|https):\/\//.test(bsdkEntry)) {
folderName = `brighterscript-${btoa(bsdkEntry.trim())}`.substring(0, 30);
packageJsonEntry = bsdkEntry.trim();

//this is a valid semantic version
} else if (semver.valid(bsdkEntry)) {
folderName = `brighterscript-${bsdkEntry}`;
packageJsonEntry = bsdkEntry;

//assume this is a folder path, return as-is
} else {
return bsdkEntry;
}

let bscPath: string;

const action = async () => {

console.log('Ensuring bsc version is installed', packageJsonEntry);
const bscNpmDir = s`${this.context.globalStorageUri.fsPath}/packages/${folderName}`;
if (await fsExtra.pathExists(bscNpmDir) === false) {
//write a simple package.json file referencing the version of brighterscript we want
await fsExtra.outputJson(`${bscNpmDir}/package.json`, {
name: 'vscode-brighterscript-host',
private: true,
version: '1.0.0',
dependencies: {
'brighterscript': packageJsonEntry
}
});
});
}
const bscPath = s`${bscNpmDir}/node_modules/brighterscript`;

//if the module is invalid, try again
if (await fsExtra.pathExists(`${bscPath}/dist/index.js`) === false && retryCount > 0) {
console.log(`Failed to load brighterscript module at ${bscNpmDir}. Deleting directory and trying again`);
//remove the dir and try again
await fsExtra.remove(bscNpmDir);
return this.ensureBscVersionInstalled(version, retryCount - 1);
await new Promise<void>((resolve, reject) => {
const process = childProcess.exec(`npm install`, {
cwd: bscNpmDir
});
process.on('error', (err) => {
console.error(err);
reject(err);
});
process.on('close', (code) => {
if (code === 0) {
resolve();
}
});
});
}
bscPath = s`${bscNpmDir}/node_modules/brighterscript`;

//if the module is invalid, try again
if (await fsExtra.pathExists(`${bscPath}/dist/index.js`) === false && retryCount > 0) {
console.log(`Failed to load brighterscript module at ${bscNpmDir}. Deleting directory and trying again`);
//remove the dir and try again
await fsExtra.remove(bscNpmDir);
return this.ensureBscVersionInstalled(bsdkEntry, retryCount - 1, false);
}
};

//show a progress spinner if configured to do so
if (showProgress) {
await vscode.window.withProgress({
title: 'Installing brighterscript language server ' + packageJsonEntry,
location: vscode.ProgressLocation.Notification,
cancellable: false
}, action);
} else {
await action();
}

return bscPath;
Expand Down
15 changes: 15 additions & 0 deletions src/commands/LanguageServerInfoCommand.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ describe('LanguageServerInfoCommand', () => {
fsExtra.removeSync(tempDir);
});

describe('getBscVersionsFromNpm', () => {
it('returns a list of versions', async () => {
const results = await command['getBscVersionsFromNpm']();
// `results` is entire list of all bsc versions, live from npm. so we obviously can't make a test that ensure they're all correct.
// so just check that certain values are sorted correctly
expect(results.map(x => x.version).filter(x => x.startsWith('0.64'))).to.eql([
'0.64.4',
'0.64.3',
'0.64.2',
'0.64.1',
'0.64.0'
]);
});
});

describe('discoverBrighterScriptVersions', () => {
function writePackage(version: string) {
fsExtra.outputJsonSync(s`${tempDir}/package.json`, {
Expand Down
66 changes: 65 additions & 1 deletion src/commands/LanguageServerInfoCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { LANGUAGE_SERVER_NAME, languageServerManager } from '../LanguageServerMa
import * as path from 'path';
import * as resolve from 'resolve';
import * as fsExtra from 'fs-extra';
import * as childProcess from 'child_process';
import { firstBy } from 'thenby';
import * as dayjs from 'dayjs';
import * as relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);

export class LanguageServerInfoCommand {
public static commandName = 'extension.brightscript.languageServer.info';
Expand Down Expand Up @@ -73,9 +78,45 @@ export class LanguageServerInfoCommand {
});
}
}

return versions;
}

private async getBscVersionsFromNpm() {
const versions = await new Promise((resolve, reject) => {
const process = childProcess.exec(`npm view brighterscript time --json`);

process.stdout.on('data', (data) => {
try {
const versions = JSON.parse(data);
delete versions.created;
delete versions.modified;
resolve(versions);
} catch (error) {
reject(error);
}
});

process.stderr.on('data', (error) => {
reject(error);
});

process.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Process exited with code ${code}`));
}
});
});
return Object.entries(versions)
.map(x => {
return {
version: x[0],
date: x[1]
};
})
.sort(firstBy(x => x.date, -1));
}

/**
* If this changes the user/folder/workspace settings, that will trigger a reload of the language server so there's no need to
* call the reload manually
Expand All @@ -84,7 +125,30 @@ export class LanguageServerInfoCommand {
const versions = this.discoverBrighterScriptVersions(
vscode.workspace.workspaceFolders.map(x => this.getWorkspaceOrFolderPath(x.uri.fsPath))
);
let selection = await vscode.window.showQuickPick(versions, { placeHolder: `Select the BrighterScript version used for BrightScript and BrighterScript language features` });

//start the request right now, we will leverage it later
const versionsFromNpmPromise = this.getBscVersionsFromNpm();

//get the full list of versions from npm
versions.push({
label: '$(package) Install from npm',
description: '',
detail: '',
command: async () => {
let versionsFromNpm = (await versionsFromNpmPromise).map(x => ({
label: x.version,
detail: x.version,
description: dayjs(x.date).fromNow(true) + ' ago'
}));
return await vscode.window.showQuickPick(versionsFromNpm, { placeHolder: `Select the BrighterScript version used for BrightScript and BrighterScript language features` }) as any;
}
} as any);

let selection = await vscode.window.showQuickPick(versions, { placeHolder: `Select the BrighterScript version used for BrightScript and BrighterScript language features` }) as any;

//if the selection has a command, run it before continuing;
selection = await selection?.command() ?? selection;

if (selection) {
const config = vscode.workspace.getConfiguration('brightscript');
//quickly clear the setting, then set it again so we are guaranteed to trigger a change event
Expand Down
6 changes: 6 additions & 0 deletions src/mockVscode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export let vscode = {
CodeAction: class { },
Diagnostic: class { },
CallHierarchyItem: class { },
ProgressLocation: {
Notification: 1
},
QuickPickItemKind: QuickPickItemKind,
StatusBarAlignment: {
Left: 1,
Expand Down Expand Up @@ -161,6 +164,9 @@ export let vscode = {
onDidCloseTextDocument: () => { }
},
window: {
withProgress: (options, action) => {
return action();
},
showInputBox: () => { },
createStatusBarItem: () => {
return {
Expand Down

0 comments on commit 83eeef0

Please sign in to comment.