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

rokucommunity / vscode-brightscript-language / 26235216720

21 May 2026 03:18PM UTC coverage: 56.569% (+1.1%) from 55.501%
26235216720

Pull #790

github

web-flow
Merge 73903c9d3 into b9f6aae1a
Pull Request #790: Add filter dropdown to the Devices view

2291 of 4457 branches covered (51.4%)

Branch coverage included in aggregate %.

62 of 65 new or added lines in 3 files covered. (95.38%)

486 existing lines in 6 files now uncovered.

3612 of 5978 relevant lines covered (60.42%)

39.42 hits per line

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

28.02
/src/managers/UserInputManager.ts
1
import { Deferred } from 'brighterscript';
1✔
2
import type {
3
    Disposable,
4
    QuickPickItem
5
} from 'vscode';
6
import * as vscode from 'vscode';
1✔
7
import type { DeviceManager, RokuDevice } from '../deviceDiscovery/DeviceManager';
8
import { icons } from '../icons';
1✔
9
import { vscodeContextManager } from './VscodeContextManager';
1✔
10
import { util } from '../util';
1✔
11

12
/**
13
 * An id to represent the "Enter manually" option in the host picker
14
 */
15
export const manualHostItemId = `${Number.MAX_SAFE_INTEGER}`;
1✔
16
const manualLabel = 'Enter manually';
1✔
17
/**
18
 * An id to represent the "Scan for devices" option in the host picker
19
 */
20
export const scanForDevicesItemId = `${Number.MAX_SAFE_INTEGER - 1}`;
1✔
21
const scanForDevicesLabel = 'Scan for devices';
1✔
22

23
export class UserInputManager {
1✔
24

25
    public constructor(
26
        private deviceManager: DeviceManager
74✔
27
    ) { }
28

29
    public async promptForHostManual(): Promise<string | undefined> {
UNCOV
30
        while (true) {
×
UNCOV
31
            const value = await vscode.window.showInputBox({
×
32
                placeHolder: 'Please enter the IP address of your Roku device',
33
                value: ''
34
            });
UNCOV
35
            if (!value) {
×
UNCOV
36
                return undefined;
×
37
            }
UNCOV
38
            const probed = await vscode.window.withProgress(
×
39
                { location: vscode.ProgressLocation.Notification, title: `Contacting ${value}...` },
40
                async () => {
41
                    return this.deviceManager.validateAndAddDevice(value);
×
42
                }
43
            );
UNCOV
44
            if (probed) {
×
UNCOV
45
                return probed.ip;
×
46
            }
47
            await vscode.window.showErrorMessage(`Unable to connect to a Roku at ${value}. Check the IP and confirm developer mode is enabled.`);
×
48
        }
49
    }
50

51
    /**
52
     * Prompt the user to pick a host from a list of devices
53
     */
54
    public async promptForHost(options?: { defaultValue?: string }) {
55

56
        const deferred = new Deferred<{ ip: string; manual?: boolean } | { ip?: string; manual: true }>();
1✔
57
        const disposables: Array<Disposable> = [];
1✔
58

59
        //create the quickpick item
60
        const quickPick = vscode.window.createQuickPick();
1✔
61
        disposables.push(quickPick);
1✔
62
        quickPick.placeholder = `Please Select a Roku or manually type an IP address`;
1✔
63
        quickPick.keepScrollPosition = true;
1✔
64

65
        // Track multiple busy sources (scan, health check) with a counter
66
        let busyCount = 0;
1✔
67
        const setBusy = (isBusy: boolean) => {
1✔
UNCOV
68
            busyCount += isBusy ? 1 : -1;
×
UNCOV
69
            busyCount = Math.max(0, busyCount); // Prevent negative
×
UNCOV
70
            quickPick.busy = busyCount > 0;
×
71
        };
72

73
        // Subscribe to scan events before triggering refresh so we catch the scan-started event
74
        this.deviceManager.on('scan-started', () => {
1✔
UNCOV
75
            setBusy(true);
×
76
        }, disposables);
77

78
        this.deviceManager.on('scan-ended', () => {
1✔
79
            setBusy(false);
×
80
        }, disposables);
81

82
        const scanTimeoutMs = 7_000;
1✔
83
        let scanTimeoutId: NodeJS.Timeout | null = null;
1✔
84
        let hasScanned = this.deviceManager.scan();
1✔
85
        this.deviceManager.on('scanNeeded-changed', () => {
1✔
86
            hasScanned = true;
×
UNCOV
87
            if (scanTimeoutId) {
×
UNCOV
88
                clearTimeout(scanTimeoutId);
×
UNCOV
89
                scanTimeoutId = null;
×
90
            }
UNCOV
91
            this.deviceManager.scan();
×
92
        }, disposables);
93
        scanTimeoutId = setTimeout(() => {
1✔
94
            if (hasScanned) {
1!
95
                return;
1✔
96
            }
97
            this.deviceManager.scan();
×
98
        }, scanTimeoutMs);
99

100
        function dispose() {
UNCOV
101
            for (const disposable of disposables) {
×
102
                disposable.dispose();
×
103
            }
104
        }
105

106
        //detect if the user types an IP address into the picker and presses enter.
107
        let selectedDevice: vscode.QuickPickItem | undefined;
108
        quickPick.onDidAccept(async () => {
1✔
UNCOV
109
            if (selectedDevice) {
×
UNCOV
110
                if (selectedDevice.kind !== vscode.QuickPickItemKind.Separator) {
×
UNCOV
111
                    if (selectedDevice.label === manualLabel) {
×
112
                        deferred.resolve({ manual: true });
×
113
                    } else if (selectedDevice.label === scanForDevicesLabel) {
×
UNCOV
114
                        this.deviceManager.refresh(true);
×
UNCOV
115
                        return;
×
116
                    } else {
UNCOV
117
                        const device = (selectedDevice as any).device as RokuDevice;
×
118
                        // if the selected device isn't healthy, show an error and keep the picker open so they can select a different device
UNCOV
119
                        setBusy(true);
×
120
                        const isHealthy = await this.deviceManager.healthCheckDevice(device, true, false);
×
121
                        setBusy(false);
×
122
                        if (!isHealthy) {
×
123
                            await vscode.window.showErrorMessage(`The selected device (${device.ip}) is not responding.`);
×
124
                            return;
×
125
                        }
126
                        this.deviceManager.setLastUsedDeviceIp(device.ip);
×
UNCOV
127
                        deferred.resolve(device);
×
128
                    }
UNCOV
129
                    quickPick.dispose();
×
130
                }
131
                selectedDevice = undefined;
×
132
                // If the user has typed a value, probe the IP before resolving so
133
                // the caller only ever receives a reachable device.
134
            } else if (quickPick.value) {
×
135
                const typedValue = quickPick.value;
×
UNCOV
136
                setBusy(true);
×
137
                const probed = await this.deviceManager.validateAndAddDevice(typedValue);
×
138
                setBusy(false);
×
UNCOV
139
                if (!probed) {
×
140
                    await vscode.window.showErrorMessage(`Unable to connect to a Roku at ${typedValue}. Check the IP and confirm developer mode is enabled.`);
×
UNCOV
141
                    return;
×
142
                }
UNCOV
143
                this.deviceManager.setLastUsedDeviceIp(probed.ip);
×
UNCOV
144
                deferred.resolve({ ip: probed.ip });
×
145
                quickPick.dispose();
×
146
            }
147
        });
148

149
        quickPick.onDidChangeSelection((selection) => {
1✔
150
            // only save the selectedDevice if the user explicitly clicks on an item
151
            // use the selected device in onDidAccept
152
            selectedDevice = selection[0];
×
153
        });
154

155
        let activeChangesSinceRefresh = 0;
1✔
156
        let activeItem: QuickPickItem;
157

158
        // remember the currently active item so we can maintain active selection when refreshing the list
159
        quickPick.onDidChangeActive((items) => {
1✔
160
            // reset our activeChanges tracker since users cannot cause items.length to be 0 (meaning a refresh has just happened)
UNCOV
161
            if (items.length === 0) {
×
UNCOV
162
                activeChangesSinceRefresh = 0;
×
163
                return;
×
164
            }
UNCOV
165
            if (activeChangesSinceRefresh > 0) {
×
UNCOV
166
                activeItem = items[0];
×
167
            }
UNCOV
168
            activeChangesSinceRefresh++;
×
169
        });
170

UNCOV
171
        const itemCache = new Map<string, QuickPickHostItem>();
×
172
        if (options?.defaultValue) {
×
173
            quickPick.value = options?.defaultValue;
×
174
        }
UNCOV
175
        quickPick.show();
×
176

177
        //set a timeout to automatically start scanning for devices after a short delay
UNCOV
178
        const SCAN_FOR_DEVICES = 'Scan for Devices';
×
179
        const CLEAR_DEVICE_LIST = 'Clear Device List';
×
UNCOV
180
        const ENABLE_DEVICE_DISCOVERY = 'Enable Device Discovery';
×
UNCOV
181
        const DISABLE_DEVICE_DISCOVERY = 'Disable Device Discovery';
×
182

183
        const refreshList = () => {
×
184
            const items = this.createHostQuickPickList(
×
185
                this.deviceManager.getDevicesForUI(),
186
                this.deviceManager.getLastUsedDeviceIp(),
187
                itemCache
188
            );
189
            quickPick.items = items;
×
190
            const discoveryEnabled = vscodeContextManager.get('brightscript.deviceDiscovery.enabled') === true;
×
191
            // Buttons render left-to-right; order is [toggleScanning, clearList, refresh] so right-to-left reads: refresh, clear list, toggle scanning
192
            quickPick.buttons = [
×
193
                {
194
                    iconPath: discoveryEnabled ? icons.radioTower : icons.radioTowerOff,
×
195
                    tooltip: discoveryEnabled ? DISABLE_DEVICE_DISCOVERY : ENABLE_DEVICE_DISCOVERY
×
196
                },
197
                {
198
                    iconPath: new vscode.ThemeIcon('clear-all'),
199
                    tooltip: CLEAR_DEVICE_LIST
200
                },
201
                {
202
                    iconPath: new vscode.ThemeIcon('refresh'),
203
                    tooltip: SCAN_FOR_DEVICES
204
                }
205
            ];
206

207
            // clear the activeItem if we can't find it in the list
208
            if (!quickPick.items.includes(activeItem)) {
×
209
                activeItem = undefined;
×
210
            }
211

212
            // if the user manually selected an item, re-focus that item now that we refreshed the list
UNCOV
213
            if (activeItem) {
×
UNCOV
214
                quickPick.activeItems = [activeItem];
×
215
            }
216
            // quickPick.show();
217
        };
218

219
        //anytime the device list changes, update the list
UNCOV
220
        this.deviceManager.on('devices-changed', refreshList, disposables);
×
221

222
        //anytime the deviceDiscovery.enabled setting changes, refresh the buttons so the toggle icon updates
UNCOV
223
        disposables.push(
×
224
            vscode.workspace.onDidChangeConfiguration(e => {
225
                if (e.affectsConfiguration('brightscript.deviceDiscovery.enabled')) {
×
226
                    refreshList();
×
227
                }
228
            })
229
        );
230

231
        quickPick.onDidHide(() => {
×
UNCOV
232
            dispose();
×
UNCOV
233
            deferred.reject(new Error('No host was selected'));
×
234
        });
235

UNCOV
236
        quickPick.onDidTriggerButton(button => {
×
237
            if (button.tooltip === SCAN_FOR_DEVICES) {
×
UNCOV
238
                this.deviceManager.refresh(true);
×
239
            } else if (button.tooltip === CLEAR_DEVICE_LIST) {
×
240
                this.deviceManager.clearCurrentDeviceList().catch(() => { });
×
UNCOV
241
                void util.showTimedNotification('Clearing device list');
×
UNCOV
242
            } else if (button.tooltip === ENABLE_DEVICE_DISCOVERY) {
×
243
                void util.setConfigurationValueAtUserOrClosestScope('brightscript.deviceDiscovery.enabled', true);
×
244
            } else if (button.tooltip === DISABLE_DEVICE_DISCOVERY) {
×
245
                void util.setConfigurationValueAtUserOrClosestScope('brightscript.deviceDiscovery.enabled', false);
×
246
            }
247
        });
248

249
        //run the list refresh once to show the popup
UNCOV
250
        refreshList();
×
UNCOV
251
        const result = await deferred.promise;
×
252
        dispose();
×
253
        if (result?.manual === true) {
×
254
            return this.promptForHostManual();
×
255
        } else {
256
            return result?.ip;
×
257
        }
258
    }
259

260
    /**
261
     * Generate the item list for the `this.promptForHost()` call
262
     */
263
    private createHostQuickPickList(
264
        devices: RokuDevice[],
265
        lastUsedDeviceIp: string | undefined,
266
        cache = new Map<string, QuickPickHostItem>()
7✔
267
    ) {
268
        //the collection of items we will eventually return
269
        let items: QuickPickHostItem[] = [];
7✔
270

271
        //find the lastUsedDevice from the devices list
272
        const lastUsedDevice = lastUsedDeviceIp ? devices.find(x => x.ip === lastUsedDeviceIp) : undefined;
7✔
273
        //remove the lastUsedDevice from the devices list so we can more easily reason with the rest of the list
274
        devices = devices.filter(x => x.ip !== lastUsedDeviceIp);
11✔
275

276
        // Ensure the most recently used device is at the top of the list
277
        if (lastUsedDevice) {
7✔
278
            //add a separator for "last used"
279
            items.push({
3✔
280
                label: 'last used',
281
                kind: vscode.QuickPickItemKind.Separator
282
            });
283

284
            //add the device
285
            items.push({
3✔
286
                label: this.deviceManager.getDeviceDisplayName(lastUsedDevice, true),
287
                device: lastUsedDevice,
288
                iconPath: this.deviceManager.getIconPath(lastUsedDevice)
289
            } as any);
290
        }
291

292
        //add all other devices
293
        if (devices.length > 0) {
7✔
294
            items.push({
4✔
295
                label: lastUsedDevice ? 'other devices' : 'devices',
4✔
296
                kind: vscode.QuickPickItemKind.Separator
297
            });
298

299
            //add each device
300
            for (const device of devices) {
4✔
301
                //add the device
302
                items.push({
8✔
303
                    label: this.deviceManager.getDeviceDisplayName(device, true),
304
                    device: device,
305
                    iconPath: this.deviceManager.getIconPath(device)
306
                });
307
            }
308
        }
309

310
        //include a divider between devices and "manual" option (only if we have devices)
311
        if (lastUsedDevice || devices.length) {
7✔
312
            items.push({ label: ' ', kind: vscode.QuickPickItemKind.Separator });
5✔
313
        }
314

315
        // allow user to manually type an IP address
316
        items.push(
7✔
317
            {
318
                label: manualLabel,
319
                device: { id: manualHostItemId },
320
                iconPath: new vscode.ThemeIcon('keyboard')
321
            } as any,
322
            {
323
                label: scanForDevicesLabel,
324
                device: { id: scanForDevicesItemId },
325
                iconPath: new vscode.ThemeIcon('radio-tower')
326
            } as any
327
        );
328

329
        // replace items with their cached versions if found (to maintain references)
330
        for (let i = 0; i < items.length; i++) {
7✔
331
            const item = items[i];
37✔
332
            if (cache.has(item.label)) {
37!
UNCOV
333
                items[i] = cache.get(item.label);
×
UNCOV
334
                items[i].device = item.device;
×
UNCOV
335
                items[i].iconPath = item.iconPath;
×
336
            } else {
337
                cache.set(item.label, item);
37✔
338
            }
339
        }
340

341
        return items;
7✔
342
    }
343
}
344

345
type QuickPickHostItem = QuickPickItem & { device?: RokuDevice; iconPath?: vscode.ThemeIcon | { light: vscode.Uri; dark: vscode.Uri } };
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