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

rokucommunity / vscode-brightscript-language / 30096694707

24 Jul 2026 01:23PM UTC coverage: 59.949% (+0.04%) from 59.908%
30096694707

Pull #802

github

web-flow
Merge 333fa7808 into e465804e3
Pull Request #802: Route extension logs through the BrightScript Extension output channel

2592 of 4745 branches covered (54.63%)

Branch coverage included in aggregate %.

19 of 23 new or added lines in 3 files covered. (82.61%)

732 existing lines in 18 files now uncovered.

4036 of 6311 relevant lines covered (63.95%)

43.79 hits per line

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

48.29
/src/viewProviders/BaseWebviewViewProvider.ts
1
import * as vscode from 'vscode';
1✔
2
import * as path from 'path';
1✔
3
import * as fsExtra from 'fs-extra';
1✔
4
import type { RequestType } from 'roku-test-automation';
5
import type { AsyncSubscription, Event } from '@parcel/watcher';
6
import type { ChannelPublishedEvent } from 'roku-debug';
7
import { vscodeContextManager } from '../managers/VscodeContextManager';
1✔
8
import type { WebviewViewProviderManager } from '../managers/WebviewViewProviderManager';
9
import { ViewProviderEvent } from './ViewProviderEvent';
1✔
10
import { ViewProviderCommand } from './ViewProviderCommand';
1✔
11
import type { VscodeCommand } from '../commands/VscodeCommand';
12
import type { RtaManager } from '../managers/RtaManager';
13
import type { BrightScriptCommands } from '../BrightScriptCommands';
14

15
export abstract class BaseWebviewViewProvider implements vscode.WebviewViewProvider, vscode.Disposable {
1✔
16
    constructor(
17
        protected extensionContext: vscode.ExtensionContext,
1,144✔
18
        protected dependencies: {
1,144✔
19
            rtaManager: RtaManager;
20
            brightscriptCommands: BrightScriptCommands;
21
        }
22
    ) {
23
        this.webviewBasePath = path.join(extensionContext.extensionPath, 'dist', 'webviews');
1,144✔
24
        extensionContext.subscriptions.push(this);
1,144✔
25
        this.extensionContext = extensionContext;
1,144✔
26
    }
27

28
    /**
29
     * The ID of the view. This should be the same as the id in the `views` contribution in package.json, and the same name
30
     * as the view in the webviews client-side code
31
     */
32
    public readonly abstract id: string;
33

34
    protected panel?: vscode.WebviewPanel;
35
    protected view?: vscode.WebviewView;
36
    protected webviewBasePath: string;
37
    private outDirWatcher: AsyncSubscription;
38
    private viewReady = false;
1,144✔
39
    private queuedMessages = [];
1,144✔
40
    private webviewViewProviderManager: WebviewViewProviderManager;
41
    private messageCommandCallbacks = {} as Record<ViewProviderCommand, (message) => Promise<boolean>>;
1,144✔
42

43
    public dispose() {
44
        void this.outDirWatcher?.unsubscribe();
1,031✔
45
    }
46

47
    public setWebviewViewProviderManager(manager: WebviewViewProviderManager) {
48
        this.webviewViewProviderManager = manager;
112✔
49
    }
50

51
    public onDidStartDebugSession(e: vscode.DebugSession) {
52
        // Can be overwritten in a child to notify on debug session start
53
    }
54

55
    public onDidTerminateDebugSession(e: vscode.DebugSession) {
56
        // Can be overwritten in a child to notify on debug session end
57
    }
58

59
    public onChannelPublishedEvent(e: ChannelPublishedEvent) {
60
        // Can be overwritten in a child to notify on channel publish
61
    }
62

63
    public createCommandMessage(command: VscodeCommand | ViewProviderCommand, context = {}) {
×
UNCOV
64
        const message = {
×
65
            command: command,
66
            context: context
67
        };
UNCOV
68
        return message;
×
69
    }
70

71
    public createEventMessage(event: ViewProviderEvent, context = {}) {
1✔
72
        const message = {
11✔
73
            event: event,
74
            context: context
75
        };
76
        return message;
11✔
77
    }
78

79
    public createResponseMessage(incomingMessage, response = undefined, error = undefined) {
×
UNCOV
80
        const message = {
×
81
            ...incomingMessage,
82
            response: response,
83
            error: error
84
        };
85

UNCOV
86
        return message;
×
87
    }
88

89
    public postOrQueueMessage(message) {
90
        if (this.viewReady) {
10!
UNCOV
91
            this.postMessage(message);
×
92
        } else {
93
            this.queuedMessages.push(message);
10✔
94
        }
95
    }
96

97
    protected postMessage(message) {
UNCOV
98
        this.view?.webview.postMessage(message).then(null, (reason) => {
×
UNCOV
99
            console.log('postMessage failed: ', reason);
×
100
        });
101

102
        this.panel?.webview.postMessage(message).then(null, (reason) => {
×
UNCOV
103
            console.log('postMessage failed: ', reason);
×
104
        });
105
    }
106

107
    private postQueuedMessages() {
UNCOV
108
        for (const queuedMessage of this.queuedMessages) {
×
UNCOV
109
            this.postMessage(queuedMessage);
×
110
        }
111
    }
112

113
    protected addMessageCommandCallback(command: ViewProviderCommand | VscodeCommand | RequestType, callback: (message) => Promise<boolean>) {
114
        this.messageCommandCallbacks[command] = callback;
53,883✔
115
    }
116

117
    private setupViewMessageObserver(webview: vscode.Webview) {
118
        webview.onDidReceiveMessage(async (message) => {
1✔
119
            try {
1✔
120
                const command = message.command;
1✔
121

122
                if (command === ViewProviderCommand.viewReady) {
1!
UNCOV
123
                    this.viewReady = true;
×
UNCOV
124
                    this.onViewReady();
×
UNCOV
125
                    this.postQueuedMessages();
×
126
                } else if (command === ViewProviderCommand.setVscodeContext) {
1!
127
                    const context = message.context;
×
128
                    await vscodeContextManager.set(context.key, context.value);
×
129
                } else if (command === ViewProviderCommand.getVscodeContext) {
1!
130
                    const context = message.context;
×
131
                    const value = vscodeContextManager.get(context.key);
×
UNCOV
132
                    this.postOrQueueMessage(this.createResponseMessage(message, {
×
133
                        value: value
134
                    }));
135
                } else if (command === ViewProviderCommand.sendMessageToWebviews) {
1!
UNCOV
136
                    const context = message.context;
×
UNCOV
137
                    this.webviewViewProviderManager.sendMessageToWebviews(context.viewIds, context.message);
×
138
                } else if (command === ViewProviderCommand.updateWorkspaceState) {
1!
139
                    const context = message.context;
×
140
                    await this.extensionContext.workspaceState.update(context.key, context.value);
×
UNCOV
141
                    this.postOrQueueMessage(this.createResponseMessage(message));
×
142
                } else if (command === ViewProviderCommand.getWorkspaceState) {
1!
143
                    const context = message.context;
×
144
                    const response = await this.extensionContext.workspaceState.get(context.key, context.defaultValue);
×
UNCOV
145
                    this.postOrQueueMessage(this.createResponseMessage(message, response));
×
146
                } else {
147
                    const callback = this.messageCommandCallbacks[command];
1✔
148
                    if (!callback || !await callback(message)) {
1!
UNCOV
149
                        console.warn('Did not handle message', message);
×
150
                    }
151
                }
152
            } catch (e) {
UNCOV
153
                this.postMessage({
×
154
                    ...message,
155
                    error: {
156
                        message: e.message,
157
                        stack: e.stack
158
                    }
159
                });
160
            }
161
        });
162
    }
163

164
    protected registerCommandWithWebViewNotifier(command: string, callback: (() => any) | undefined = undefined) {
98✔
165
        this.registerCommand(command, async () => {
112✔
UNCOV
166
            if (callback) {
×
UNCOV
167
                await callback();
×
168
            }
169
            const message = this.createEventMessage(ViewProviderEvent.onVscodeCommandReceived, {
×
170
                commandName: command
171
            });
172

UNCOV
173
            this.postOrQueueMessage(message);
×
174
        });
175
    }
176

177
    protected registerCommand(command: string, callback: (...args: any[]) => any) {
178
        this.extensionContext.subscriptions.push(vscode.commands.registerCommand(command, callback));
210✔
179
    }
180

181
    protected onViewReady() { }
182

183
    /** Adds ability to add additional script contents to the main webview html */
184
    protected additionalScriptContents(): string[] {
UNCOV
185
        return [];
×
186
    }
187

188
    protected async getHtmlForWebview() {
189
        try {
2✔
190
            let watcher;
191
            try {
2✔
192
                // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-extraneous-dependencies
193
                watcher = require('@parcel/watcher');
2✔
194
            } catch (e) {
195
                // Doing nothing if watcher does not exist
196
            }
197

198
            if (watcher && !this.outDirWatcher) {
2!
199
                // When in dev mode, spin up a watcher to auto-reload the webview whenever the files have changed.
200
                this.outDirWatcher = await watcher.subscribe(this.webviewBasePath, (err, events: Event[]) => {
2✔
201
                    //only refresh when the index.html page is changed. Since vite rewrites the file on every build, this is enough to know to reload the page
UNCOV
202
                    if (
×
UNCOV
203
                        events.find(x => (x.type === 'create' || x.type === 'update') && x.path?.toLowerCase()?.endsWith('index.html'))
×
204
                    ) {
205
                        this.view.webview.html = '';
×
206
                        this.view.webview.html = this.getIndexHtml();
×
207
                    }
208
                });
209
            }
210
        } catch (e) {
UNCOV
211
            console.error(e);
×
212
        }
213
        return this.getIndexHtml();
2✔
214
    }
215

216
    /**
217
    * Get a webview-supported URI for the given path
218
    */
219
    private asWebviewUri(...parts: string[]) {
220
        return this.view?.webview?.asWebviewUri?.(
2!
221
            vscode.Uri.file(
222
                path.join(...parts)
223
            )
224
        );
225
    }
226

227
    private getIndexHtml() {
228
        let html: string;
229
        try {
2✔
230
            html = fsExtra.readFileSync(this.webviewBasePath + '/index.html').toString();
2✔
231
        } catch (e) {
UNCOV
232
            console.error(e);
×
UNCOV
233
            html = '<h1>Error loading webview</h1>';
×
234
        }
235
        //the data that will be replaced in the index.html
236
        const data = {
2✔
237
            viewName: this.id,
238
            baseHref: `${this.asWebviewUri(this.webviewBasePath)}/`,
239
            additionalScriptContents: this.additionalScriptContents().join('\n                        ')
240
        };
241
        /**
242
         * replace placeholders in the html, in one of these formats:
243
         * <!--{{thing1}}-->
244
         * //{{thing2}}
245
         * {{thing3}}
246
         */
247
        html = html.replace(/(\/\/{{(\w+)}})|({{(\w+)}})|(<!--{{(\w+)}})/gm, (...match: string[]) => {
2✔
248
            const [, , key1, , key2, , key3] = match;
6✔
249
            return data[key1] ?? data[key2] ?? data[key3] ?? match[0];
6!
250
        });
251
        // remove leading slash for css/js urls so we can make them relative to the baseHref
252
        html = html.replace(/((?:href|src)\s*=\s*["'])(\/.*")/g, (...match: string[]) => {
2✔
253
            return match[1] + match[2]?.replace(/^\/+/, '');
4!
254
        });
255
        return html;
2✔
256
    }
257

258
    public async resolveWebviewView(
259
        view: vscode.WebviewView,
260
        _context: vscode.WebviewViewResolveContext,
261
        _token: vscode.CancellationToken
262
    ) {
263
        this.view = view;
1✔
264
        const webview = view.webview;
1✔
265
        this.setupViewMessageObserver(webview);
1✔
266

267
        webview.options = {
1✔
268
            // Allow scripts in the webview
269
            enableScripts: true,
270

271
            localResourceRoots: [
272
                vscode.Uri.file(this.webviewBasePath)
273
            ]
274
        };
275
        webview.html = await this.getHtmlForWebview();
1✔
276
    }
277

278
    protected async createOrRevealWebviewPanel() {
279
        // See if we need to make the panel or not
UNCOV
280
        let createPanel = false;
×
UNCOV
281
        if (!this.panel) {
×
UNCOV
282
            createPanel = true;
×
283
        } else {
284
            try {
×
285
                if (!this.panel.active) {
×
286
                    // If we still exist and aren't active then reveal the panel
287
                    this.panel.reveal();
×
288
                }
289
            } catch (e) {
290
                createPanel = true;
×
291
            }
292
        }
293

UNCOV
294
        if (createPanel) {
×
UNCOV
295
            this.panel = vscode.window.createWebviewPanel(
×
296
                this.id,
297
                await this.getViewNameById(this.id),
298
                vscode.ViewColumn.Active,
299
                {
300
                    // Enable javascript in the webview
301
                    enableScripts: true,
302
                    localResourceRoots: [
303
                        vscode.Uri.file(this.webviewBasePath)
304
                    ]
305
                }
306
            );
307

UNCOV
308
            this.setupViewMessageObserver(this.panel.webview);
×
309

UNCOV
310
            const html = await this.getHtmlForWebview();
×
311
            this.panel.webview.html = html;
×
312
        }
313
    }
314

315
    private async getViewNameById(viewId) {
UNCOV
316
        const packageJsonPath = path.join(this.extensionContext.extensionPath, 'package.json');
×
UNCOV
317
        const packageJson = JSON.parse(await fsExtra.readFile(packageJsonPath, 'utf8'));
×
318

319
        for (const view of [...packageJson.contributes.views.debug, ...packageJson.contributes.views['vscode-brightscript-language']]) {
×
320
            if (view.id === viewId) {
×
UNCOV
321
                return view.name;
×
322
            }
323
        }
324

UNCOV
325
        return null;
×
326
    }
327
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc