• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / vscode-brightscript-language / #2719

24 Aug 2022 12:51PM UTC coverage: 41.691% (+0.02%) from 41.675%
#2719

push

TwitchBronBron
2.35.0

477 of 1427 branches covered (33.43%)

Branch coverage included in aggregate %.

1126 of 2418 relevant lines covered (46.57%)

7.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

53.11
/src/LanguageServerManager.ts
1
import type {
2
    LanguageClientOptions,
3
    ServerOptions,
4
    ExecuteCommandParams
5
} from 'vscode-languageclient/node';
6
import {
1✔
7
    LanguageClient,
8
    TransportKind
9
} from 'vscode-languageclient/node';
10
import * as vscode from 'vscode';
1✔
11
import * as path from 'path';
1✔
12
import type { Disposable } from 'vscode';
13
import {
1✔
14
    window,
15
    workspace
16
} from 'vscode';
17
import { CustomCommands, Deferred } from 'brighterscript';
1✔
18
import type { CodeWithSourceMap } from 'source-map';
19
import BrightScriptDefinitionProvider from './BrightScriptDefinitionProvider';
1✔
20
import { BrightScriptWorkspaceSymbolProvider, SymbolInformationRepository } from './SymbolInformationRepository';
1✔
21
import { BrightScriptDocumentSymbolProvider } from './BrightScriptDocumentSymbolProvider';
1✔
22
import { BrightScriptReferenceProvider } from './BrightScriptReferenceProvider';
1✔
23
import BrightScriptSignatureHelpProvider from './BrightScriptSignatureHelpProvider';
1✔
24
import type { DefinitionRepository } from './DefinitionRepository';
25
import { util } from './util';
1✔
26
import { LanguageServerInfoCommand, languageServerInfoCommand } from './commands/LanguageServerInfoCommand';
1✔
27
import * as fsExtra from 'fs-extra';
1✔
28

29
export class LanguageServerManager {
1✔
30
    constructor() {
31
        this.deferred = new Deferred();
15✔
32
        this.embeddedBscInfo = {
15✔
33
            path: require.resolve('brighterscript').replace(/[\\\/]dist[\\\/]index.js/i, ''),
34
            // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
35
            version: require('brighterscript/package.json').version
36
        };
37
        //default to the embedded bsc version
38
        this.selectedBscInfo = this.embeddedBscInfo;
15✔
39
    }
40

41
    public embeddedBscInfo: BscInfo;
42
    public selectedBscInfo: BscInfo;
43

44
    private context: vscode.ExtensionContext;
45
    private definitionRepository: DefinitionRepository;
46
    private get declarationProvider() {
47
        return this.definitionRepository.provider;
12✔
48
    }
49

50
    public async init(
51
        context: vscode.ExtensionContext,
52
        definitionRepository: DefinitionRepository
53

54
    ) {
55
        this.context = context;
×
56
        this.definitionRepository = definitionRepository;
×
57

58
        //dynamically enable or disable the language server based on user settings
59
        vscode.workspace.onDidChangeConfiguration(async (configuration) => {
×
60
            await this.syncVersionAndTryRun();
×
61
        });
62
        await this.syncVersionAndTryRun();
×
63
    }
64

65
    private deferred: Deferred<any>;
66

67
    /**
68
     * Returns a promise that resolves once the language server is ready to be interacted with
69
     */
70
    private async ready() {
71
        if (this.isLanguageServerEnabledInSettings() === false) {
×
72
            throw new Error('Language server is disabled in user settings');
×
73
        } else {
74
            return this.deferred.promise;
×
75
        }
76
    }
77

78
    private refreshDeferred() {
79
        let newDeferred = new Deferred<any>();
3✔
80
        //chain any pending promises to this new deferred
81
        if (!this.deferred.isCompleted) {
3✔
82
            this.deferred.resolve(newDeferred.promise);
2✔
83
        }
84
        this.deferred = newDeferred;
3✔
85
    }
86

87
    private client: LanguageClient;
88
    private languageServerStatusBar: vscode.StatusBarItem;
89

90
    private clientDispose: Disposable;
91

92
    private async enableLanguageServer() {
93
        try {
2✔
94

95
            //if we already have a language server, nothing more needs to be done
96
            if (this.client) {
2✔
97
                return await this.ready();
1✔
98
            }
99
            this.refreshDeferred();
1✔
100

101
            //create the statusbar
102
            this.languageServerStatusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right);
1✔
103
            this.languageServerStatusBar.command = LanguageServerInfoCommand.commandName;
1✔
104
            this.updateStatusbar('running');
1✔
105
            this.languageServerStatusBar.show();
1✔
106

107
            //disable the simple providers (the language server will handle all of these)
108
            this.disableSimpleProviders();
1✔
109

110
            // The server is implemented in node
111
            let serverModule = this.context.asAbsolutePath(
1✔
112
                path.join('dist', 'LanguageServerRunner.js')
113
            );
114

115
            //give the runner the specific version of bsc to run
116
            const args = [
1✔
117
                this.selectedBscInfo.path,
118
                (this.context.extensionMode === vscode.ExtensionMode.Development).toString()
119
            ];
120
            // If the extension is launched in debug mode then the debug server options are used
121
            // Otherwise the run options are used
122
            let serverOptions: ServerOptions = {
×
123
                run: {
124
                    module: serverModule,
125
                    transport: TransportKind.ipc,
126
                    args: args
127
                },
128
                debug: {
129
                    module: serverModule,
130
                    transport: TransportKind.ipc,
131
                    args: args,
132
                    // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging
133
                    options: { execArgv: ['--nolazy', '--inspect=6009'] }
134
                }
135
            };
136

137
            // Options to control the language client
138
            let clientOptions: LanguageClientOptions = {
×
139
                // Register the server for various types of documents
140
                documentSelector: [
141
                    { scheme: 'file', language: 'brightscript' },
142
                    { scheme: 'file', language: 'brighterscript' },
143
                    { scheme: 'file', language: 'xml' }
144
                ],
145
                synchronize: {
146
                    // Notify the server about file changes to every filetype it cares about
147
                    fileEvents: workspace.createFileSystemWatcher('**/*')
148
                }
149
            };
150

151
            // Create the language client and start the client.
152
            this.client = new LanguageClient(
×
153
                'brighterScriptLanguageServer',
154
                'BrighterScript Language Server',
155
                serverOptions,
156
                clientOptions
157
            );
158
            // Start the client. This will also launch the server
159
            this.clientDispose = this.client.start();
×
160
            await this.client.onReady();
×
161

162
            this.client.onNotification('critical-failure', (message) => {
×
163
                void window.showErrorMessage(message);
×
164
            });
165

166
            //update the statusbar with build statuses
167
            this.client.onNotification('build-status', (message) => {
×
168
                if (message === 'building') {
×
169
                    this.updateStatusbar('running');
×
170

171
                } else if (message === 'success') {
×
172
                    this.updateStatusbar('running');
×
173

174
                } else if (message === 'critical-error') {
×
175
                    this.updateStatusbar('encountered a critical runtime error', '#FF0000');
×
176
                }
177
            });
178
            this.deferred.resolve(true);
×
179
        } catch (e) {
180
            console.error(e);
2✔
181
            void this.client?.stop?.();
2!
182
            delete this.client;
2✔
183

184
            this.refreshDeferred();
2✔
185

186
            this.deferred.reject(e);
2✔
187
        }
188
        return this.ready();
2✔
189
    }
190

191
    private updateStatusbar(tooltip: string, color?: string) {
192
        this.languageServerStatusBar.text = `$(flame)bsc-${this.selectedBscInfo.version}`;
1✔
193
        this.languageServerStatusBar.tooltip = 'BrightScript Language Server: ' + tooltip;
1✔
194
        this.languageServerStatusBar.color = color;
1✔
195
    }
196

197
    /**
198
     * Stop and then start the language server.
199
     * This is a noop if the language server is currently disabled
200
     */
201
    public async restart() {
202
        await this.disableLanguageServer();
×
203
        await util.delay(1);
×
204
        await this.syncVersionAndTryRun();
×
205
    }
206

207
    private async disableLanguageServer() {
208
        if (this.client) {
×
209
            await this.client.stop();
×
210
            this.languageServerStatusBar.dispose();
×
211
            this.languageServerStatusBar = undefined;
×
212
            this.clientDispose?.dispose();
×
213
            this.client = undefined;
×
214
            //delay slightly to let things catch up
215
            await util.delay(100);
×
216
            this.deferred = new Deferred();
×
217
        }
218
        //enable the simple providers (since there is no language server)
219
        this.enableSimpleProviders();
×
220
    }
221

222
    private simpleSubscriptions = [] as Disposable[];
15✔
223

224
    /**
225
     * Enable the simple providers (which means the language server is disabled).
226
     * These were the original providers created by George. Most of this functionality has been moved into the language server
227
     * However, if the language server is disabled, we want to at least fall back to these.
228
     */
229
    private enableSimpleProviders() {
230
        if (this.simpleSubscriptions.length === 0) {
4!
231
            //register the definition provider
232
            const definitionProvider = new BrightScriptDefinitionProvider(this.definitionRepository);
4✔
233
            const symbolInformationRepository = new SymbolInformationRepository(this.declarationProvider);
4✔
234
            const selector = { scheme: 'file', pattern: '**/*.{brs,bs}' };
4✔
235

236
            this.simpleSubscriptions.push(
4✔
237
                vscode.languages.registerDefinitionProvider(selector, definitionProvider),
238
                vscode.languages.registerDocumentSymbolProvider(selector, new BrightScriptDocumentSymbolProvider(this.declarationProvider)),
239
                vscode.languages.registerWorkspaceSymbolProvider(new BrightScriptWorkspaceSymbolProvider(this.declarationProvider, symbolInformationRepository)),
240
                vscode.languages.registerReferenceProvider(selector, new BrightScriptReferenceProvider()),
241
                vscode.languages.registerSignatureHelpProvider(selector, new BrightScriptSignatureHelpProvider(this.definitionRepository), '(', ',')
242
            );
243

244
            this.context.subscriptions.push(...this.simpleSubscriptions);
4✔
245
        }
246
    }
247

248
    /**
249
     * Disable the simple subscriptions (which means we'll depend on the language server)
250
     */
251
    private disableSimpleProviders() {
252
        if (this.simpleSubscriptions.length > 0) {
1!
253
            for (const sub of this.simpleSubscriptions) {
×
254
                const idx = this.context.subscriptions.indexOf(sub);
×
255
                if (idx > -1) {
×
256
                    this.context.subscriptions.splice(idx, 1);
×
257
                    sub.dispose();
×
258
                }
259
            }
260
            this.simpleSubscriptions = [];
×
261
        }
262
    }
263

264
    public isLanguageServerEnabledInSettings() {
265
        let settings = vscode.workspace.getConfiguration('brightscript');
×
266
        let value = settings.enableLanguageServer === false ? false : true;
×
267
        return value;
×
268
    }
269

270
    public async getTranspiledFileContents(pathAbsolute: string) {
271
        //wait for the language server to be ready
272
        await this.ready();
×
273
        let result = await this.client.sendRequest('workspace/executeCommand', {
×
274
            command: CustomCommands.TranspileFile,
275
            arguments: [pathAbsolute]
276
        } as ExecuteCommandParams);
277
        return result as CodeWithSourceMap;
×
278
    }
279

280
    /**
281
     * Check user settings for which language server version to use,
282
     * and if different, re-launch the specific version of the language server'
283
     */
284
    public async syncVersionAndTryRun() {
285
        const bsdkPath = await this.getBsdkPath();
×
286

287
        //if the path to bsc is different, spin down the old server and start a new one
288
        if (bsdkPath !== this.selectedBscInfo.path) {
×
289
            await this.disableLanguageServer();
×
290
        }
291

292
        //try to load the package version.
293
        try {
×
294
            this.selectedBscInfo = {
×
295
                path: bsdkPath,
296
                // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
297
                version: require(`${bsdkPath}/package.json`).version
298
            };
299
        } catch (e) {
300
            console.error(e);
×
301
            //fall back to the embedded version, and show a popup
302
            await vscode.window.showErrorMessage(`Can't find language sever at "${bsdkPath}". Using embedded version v${this.embeddedBscInfo.version} instead.`);
×
303
            this.selectedBscInfo = this.embeddedBscInfo;
×
304
        }
305

306
        if (this.isLanguageServerEnabledInSettings()) {
×
307
            await this.enableLanguageServer();
×
308
        } else {
309
            await this.disableLanguageServer();
×
310
        }
311
    }
312

313
    /**
314
     * Get the full path to the brighterscript module where the LanguageServer should be run
315
     */
316
    private async getBsdkPath() {
317
        //if there's a bsdk entry in the workspace settings, assume the path is relative to the workspace
318
        if (this.workspaceConfigIncludesBsdkKey()) {
9✔
319
            let bsdk = vscode.workspace.getConfiguration('brightscript', vscode.workspace.workspaceFile).get<string>('bsdk');
2✔
320
            return bsdk === 'embedded'
2✔
321
                ? this.embeddedBscInfo.path
322
                : path.resolve(path.dirname(vscode.workspace.workspaceFile.fsPath), bsdk);
323
        }
324

325
        const folderResults = new Set<string>();
7✔
326
        //look for a bsdk entry in each of the workspace folders
327
        for (const folder of vscode.workspace.workspaceFolders) {
7✔
328
            const bsdk = vscode.workspace.getConfiguration('brightscript', folder).get<string>('bsdk');
16✔
329
            if (bsdk) {
16✔
330
                folderResults.add(
15✔
331
                    bsdk === 'embedded'
15✔
332
                        ? this.embeddedBscInfo.path
333
                        : path.resolve(folder.uri.fsPath, bsdk)
334
                );
335
            }
336
        }
337
        const values = [...folderResults.values()];
7✔
338
        //there's no bsdk configuration in folder settings.
339
        if (values.length === 0) {
7✔
340
            return this.embeddedBscInfo.path;
3✔
341

342
            //we have exactly one result. use it
343
        } else if (values.length === 1) {
4✔
344
            return values[0];
3✔
345
        } else {
346
            //there were multiple versions. make the user pick which to use
347
            return languageServerInfoCommand.selectBrighterScriptVersion();
1✔
348
        }
349
    }
350

351
    private workspaceConfigIncludesBsdkKey() {
352
        return vscode.workspace.workspaceFile &&
9✔
353
            fsExtra.pathExistsSync(vscode.workspace.workspaceFile.fsPath) &&
354
            /"brightscript.bsdk"/.exec(
355
                fsExtra.readFileSync(vscode.workspace.workspaceFile.fsPath
356
                ).toString()
357
            );
358
    }
359
}
360

361
export const languageServerManager = new LanguageServerManager();
1✔
362

363
interface BscInfo {
364
    /**
365
     * The full path to the brighterscript module
366
     */
367
    path: string;
368
    /**
369
     * The version of the brighterscript module
370
     */
371
    version: string;
372
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc