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

rokucommunity / vscode-brightscript-language / 30100866380

24 Jul 2026 02:24PM UTC coverage: 60.103% (+0.2%) from 59.908%
30100866380

Pull #802

github

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

2593 of 4745 branches covered (54.65%)

Branch coverage included in aggregate %.

64 of 102 new or added lines in 20 files covered. (62.75%)

4073 of 6346 relevant lines covered (64.18%)

43.71 hits per line

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

48.79
/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
import { createLogger } from '../logging';
1✔
15

16
const logger = createLogger('BaseWebviewViewProvider');
1✔
17

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

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

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

46
    public dispose() {
47
        void this.outDirWatcher?.unsubscribe();
1,031✔
48
    }
49

50
    public setWebviewViewProviderManager(manager: WebviewViewProviderManager) {
51
        this.webviewViewProviderManager = manager;
112✔
52
    }
53

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

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

62
    public onChannelPublishedEvent(e: ChannelPublishedEvent) {
63
        // Can be overwritten in a child to notify on channel publish
64
    }
65

66
    public createCommandMessage(command: VscodeCommand | ViewProviderCommand, context = {}) {
×
67
        const message = {
×
68
            command: command,
69
            context: context
70
        };
71
        return message;
×
72
    }
73

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

82
    public createResponseMessage(incomingMessage, response = undefined, error = undefined) {
×
83
        const message = {
×
84
            ...incomingMessage,
85
            response: response,
86
            error: error
87
        };
88

89
        return message;
×
90
    }
91

92
    public postOrQueueMessage(message) {
93
        if (this.viewReady) {
10!
94
            this.postMessage(message);
×
95
        } else {
96
            this.queuedMessages.push(message);
10✔
97
        }
98
    }
99

100
    protected postMessage(message) {
101
        this.view?.webview.postMessage(message).then(null, (reason) => {
×
NEW
102
            logger.log('postMessage failed: ', reason);
×
103
        });
104

105
        this.panel?.webview.postMessage(message).then(null, (reason) => {
×
NEW
106
            logger.log('postMessage failed: ', reason);
×
107
        });
108
    }
109

110
    private postQueuedMessages() {
111
        for (const queuedMessage of this.queuedMessages) {
×
112
            this.postMessage(queuedMessage);
×
113
        }
114
    }
115

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

120
    private setupViewMessageObserver(webview: vscode.Webview) {
121
        webview.onDidReceiveMessage(async (message) => {
1✔
122
            try {
1✔
123
                const command = message.command;
1✔
124

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

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

176
            this.postOrQueueMessage(message);
×
177
        });
178
    }
179

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

184
    protected onViewReady() { }
185

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

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

201
            if (watcher && !this.outDirWatcher) {
2!
202
                // When in dev mode, spin up a watcher to auto-reload the webview whenever the files have changed.
203
                this.outDirWatcher = await watcher.subscribe(this.webviewBasePath, (err, events: Event[]) => {
2✔
204
                    //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
205
                    if (
×
206
                        events.find(x => (x.type === 'create' || x.type === 'update') && x.path?.toLowerCase()?.endsWith('index.html'))
×
207
                    ) {
208
                        this.view.webview.html = '';
×
209
                        this.view.webview.html = this.getIndexHtml();
×
210
                    }
211
                });
212
            }
213
        } catch (e) {
NEW
214
            logger.error(e);
×
215
        }
216
        return this.getIndexHtml();
2✔
217
    }
218

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

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

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

270
        webview.options = {
1✔
271
            // Allow scripts in the webview
272
            enableScripts: true,
273

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

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

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

311
            this.setupViewMessageObserver(this.panel.webview);
×
312

313
            const html = await this.getHtmlForWebview();
×
314
            this.panel.webview.html = html;
×
315
        }
316
    }
317

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

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

328
        return null;
×
329
    }
330
}
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