Skip to content

Commit

Permalink
Separate list building from list showing/picking
Browse files Browse the repository at this point in the history
  • Loading branch information
TwitchBronBron committed Oct 30, 2023
1 parent 9702b2f commit 956e6c5
Show file tree
Hide file tree
Showing 3 changed files with 255 additions and 45 deletions.
149 changes: 144 additions & 5 deletions src/DebugConfigurationProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { assert, expect } from 'chai';
import * as path from 'path';
import { createSandbox } from 'sinon';
import type { WorkspaceFolder } from 'vscode';
import { QuickPickItemKind } from 'vscode';
import Uri from 'vscode-uri';
import type { BrightScriptLaunchConfiguration } from './DebugConfigurationProvider';
import { BrightScriptDebugConfigurationProvider } from './DebugConfigurationProvider';
import { vscode } from './mockVscode.spec';
import { standardizePath as s } from 'brighterscript';
import * as fsExtra from 'fs-extra';
import type { ActiveDeviceManager } from './ActiveDeviceManager';
import type { RokuDeviceDetails } from './ActiveDeviceManager';
import { ActiveDeviceManager } from './ActiveDeviceManager';

const sinon = createSandbox();
const Module = require('module');
Expand Down Expand Up @@ -49,10 +51,16 @@ describe('BrightScriptConfigurationProvider', () => {
index: 0
};

let activeDeviceManager = {
getActiveDevices: () => []
} as any as ActiveDeviceManager;
configProvider = new BrightScriptDebugConfigurationProvider(<any>context, activeDeviceManager, null, vscode.window.createOutputChannel('Extension'));
//prevent the 'start' method from actually running
sinon.stub(ActiveDeviceManager.prototype as any, 'start').callsFake(() => { });
let activeDeviceManager = new ActiveDeviceManager();

configProvider = new BrightScriptDebugConfigurationProvider(
<any>context,
activeDeviceManager,
null,
vscode.window.createOutputChannel('Extension')
);
});

afterEach(() => {
Expand Down Expand Up @@ -322,4 +330,135 @@ describe('BrightScriptConfigurationProvider', () => {
expect(config.rootDir).to.eql('./somePath/123');
});
});

describe('createHostQuickPickList', () => {
const devices: Array<RokuDeviceDetails> = [{
deviceInfo: {
'user-device-name': 'roku1',
'serial-number': 'alpha',
'model-number': 'model1'
},
id: '1',
ip: '1.1.1.1',
location: '???'
}, {
deviceInfo: {
'user-device-name': 'roku2',
'serial-number': 'beta',
'model-number': 'model2'
},
id: '2',
ip: '1.1.1.2',
location: '???'
}, {
deviceInfo: {
'user-device-name': 'roku3',
'serial-number': 'charlie',
'model-number': 'model3'
},
id: '3',
ip: '1.1.1.3',
location: '???'
}];
function label(device: RokuDeviceDetails) {
return `${device.ip} | ${device.deviceInfo['user-device-name']} - ${device.deviceInfo['serial-number']} - ${device.deviceInfo['model-number']}`;
}

it('includes "manual', () => {
expect(
configProvider['createHostQuickPickList']([], undefined, '')
).to.eql([{
label: 'Enter manually',
device: {
id: Number.MAX_SAFE_INTEGER
}
}]);
});

it('includes separators for devices and manual options', () => {
expect(
configProvider['createHostQuickPickList']([devices[0]], undefined, '')
).to.eql([
{
kind: QuickPickItemKind.Separator,
label: 'devices'
},
{
label: '1.1.1.1 | roku1 - alpha - model1',
device: devices[0]
},
{
kind: QuickPickItemKind.Separator,
label: ' '
}, {
label: 'Enter manually',
device: {
id: Number.MAX_SAFE_INTEGER
}
}]
);
});

it('moves active device to the top', () => {
expect(
configProvider['createHostQuickPickList']([devices[0], devices[1], devices[2]], devices[1], '').map(x => x.label)
).to.eql([
'last used',
label(devices[1]),
'other devices',
label(devices[0]),
label(devices[2]),
' ',
'Enter manually'
]);
});

it('includes the spinner text when "last used" and "other devices" separators are both present', () => {
expect(
configProvider['createHostQuickPickList'](devices, devices[1], ' (searching ...)').map(x => x.label)
).to.eql([
'last used',
label(devices[1]),
'other devices',
label(devices[0]),
label(devices[2]),
'(searching ...)',
'Enter manually'
]);
});

it('includes the spinner text if "devices" separator is present', () => {
expect(
configProvider['createHostQuickPickList'](devices, null, ' (searching ...)').map(x => x.label)
).to.eql([
'devices',
label(devices[0]),
label(devices[1]),
label(devices[2]),
'(searching ...)',
'Enter manually'
]);
});

it('includes the spinner text if only "last used" separator is present', () => {
expect(
configProvider['createHostQuickPickList']([devices[0]], devices[0], ' (searching ...)').map(x => x.label)
).to.eql([
'last used',
label(devices[0]),
'(searching ...)',
'Enter manually'
]);
});

it('includes the spinner text when no other device entries are present', () => {
expect(
configProvider['createHostQuickPickList']([], null, ' (searching ...)').map(x => x.label)
).to.eql([
'(searching ...)',
'Enter manually'
]);
});

});
});
107 changes: 68 additions & 39 deletions src/DebugConfigurationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,49 +459,18 @@ export class BrightScriptDebugConfigurationProvider implements DebugConfiguratio
const refreshListDebounced = debounce(() => refreshList(true), 400);

const refreshList = (updateSpinnerText = false) => {
const devices = this.activeDeviceManager.getActiveDevices();
let itemsRefreshed: Array<QuickPickItem & { device?: RokuDeviceDetails }> = devices.map(device => ({
label: `${device.ip} | ${device.deviceInfo['user-device-name']} - ${device.deviceInfo['serial-number']} - ${device.deviceInfo['model-number']}`,
device: device
}));

const devicesLabel: QuickPickItem = {
label: this.activeDeviceManager.lastUsedDevice ? 'other devices' : 'devices',
kind: vscode.QuickPickItemKind.Separator
};
itemsRefreshed.unshift(devicesLabel);

//move the the most recently used device to the top
if (this.activeDeviceManager.lastUsedDevice) {
const idx = itemsRefreshed.findIndex(x => x.device?.id === this.activeDeviceManager.lastUsedDevice?.id);
const [item] = itemsRefreshed.splice(idx, 1);
itemsRefreshed.unshift(item);

itemsRefreshed.unshift({
label: 'last used',
kind: vscode.QuickPickItemKind.Separator
});
}

const { activeItems } = quickPick;
let spinnerText = '';
if (this.activeDeviceManager.timeSinceLastDiscoveredDevice < discoveryTime) {
devicesLabel.label += ` (searching ${generateSpinnerText(updateSpinnerText)})`;
spinnerText = ` (searching ${generateSpinnerText(updateSpinnerText)})`;
refreshListDebounced();
}

// allow user to manually type an IP address
itemsRefreshed.push(
{ label: ' ', kind: vscode.QuickPickItemKind.Separator },
{ label: manualLabel, device: { id: Number.MAX_SAFE_INTEGER } } as any
quickPick.items = this.createHostQuickPickList(
this.activeDeviceManager.getActiveDevices(),
this.activeDeviceManager.lastUsedDevice,
spinnerText
);

//find the active item from our list (if there is one)
const activeItem = itemsRefreshed.find(x => {
return x.device?.id === ((quickPick.activeItems?.[0] as any)?.device as RokuDeviceDetails)?.id;
});
quickPick.items = itemsRefreshed;
if (activeItem) {
quickPick.activeItems = [activeItem];
}
quickPick.activeItems = activeItems;
quickPick.show();
};

Expand Down Expand Up @@ -543,6 +512,66 @@ export class BrightScriptDebugConfigurationProvider implements DebugConfiguratio
return result;
}

private createHostLabel(device: RokuDeviceDetails) {
return `${device.ip} | ${device.deviceInfo['user-device-name']} - ${device.deviceInfo['serial-number']} - ${device.deviceInfo['model-number']}`;
}

/**
* Generate the item list for the `this.promptForHost()` call
*/
private createHostQuickPickList(devices: RokuDeviceDetails[], lastUsedDevice: RokuDeviceDetails, spinnerText: string) {
//the collection of items we will eventually return
let items: Array<QuickPickItem & { device?: RokuDeviceDetails }> = [];

//yank the last used device out of the list so we can think about the remaining list more easily
lastUsedDevice = devices.find(x => x.id === lastUsedDevice?.id);
//remove the lastUsedDevice from the devices list so we can more easily reason with the rest of the list
devices = devices.filter(x => x.id !== lastUsedDevice?.id);

// Ensure the most recently used device is at the top of the list
if (lastUsedDevice) {
//add a separator for "last used"
items.push({
label: 'last used',
kind: vscode.QuickPickItemKind.Separator
});

//add the device
items.push({
label: this.createHostLabel(lastUsedDevice),
device: lastUsedDevice
});
}

//add all other devices
if (devices.length > 0) {
items.push({
label: lastUsedDevice ? 'other devices' : 'devices',
kind: vscode.QuickPickItemKind.Separator
});

//add each device
for (const device of devices) {
//add the device
items.push({
label: this.createHostLabel(device),
device: device
});
}
}

//include a divider between devices and "manual" option (only if we have devices)
if (spinnerText || lastUsedDevice || devices.length) {
items.push({ label: spinnerText.trim() || ' ', kind: vscode.QuickPickItemKind.Separator });
}

// allow user to manually type an IP address
items.push(
{ label: 'Enter manually', device: { id: Number.MAX_SAFE_INTEGER } } as any
);
return items;
}

/**
* Validates the password parameter in the config and opens an input ui if set to ${promptForPassword}
* @param config current config object
Expand Down
44 changes: 43 additions & 1 deletion src/mockVscode.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import type { Command, Range, TreeDataProvider, TreeItemCollapsibleState, Uri, WorkspaceFolder, ConfigurationScope, ExtensionContext, WorkspaceConfiguration, OutputChannel } from 'vscode';
import { EventEmitter } from 'eventemitter3';
import type { Command, Range, TreeDataProvider, TreeItemCollapsibleState, Uri, WorkspaceFolder, ConfigurationScope, ExtensionContext, WorkspaceConfiguration, OutputChannel, QuickPickItem } from 'vscode';

//copied from vscode to help with unit tests
enum QuickPickItemKind {
Separator = -1,
Default = 0
}

afterEach(() => {
delete vscode.workspace.workspaceFile;
Expand All @@ -20,6 +27,7 @@ export let vscode = {
CodeAction: class { },
Diagnostic: class { },
CallHierarchyItem: class { },
QuickPickItemKind: QuickPickItemKind,
StatusBarAlignment: {
Left: 1,
Right: 2
Expand Down Expand Up @@ -144,13 +152,47 @@ export let vscode = {
onDidCloseTextDocument: () => { }
},
window: {
showInputBox: () => { },
createStatusBarItem: () => {
return {
clear: () => { },
text: '',
show: () => { }
};
},
createQuickPick: () => {
class QuickPick {
private emitter = new EventEmitter();

public placeholder = '';

public items: QuickPickItem[];
public keepScrollPosition = false;

public show() { }

public onDidAccept(cb) {
this.emitter.on('didAccept', cb);
}

public onDidHide(cb) {
this.emitter.on('didHide', cb);
}

public hide() {
this.emitter.emit('didHide');
}

public onDidChangeSelection(cb) {
this.emitter.on('didChangeSelection', cb);
}

public dispose() {
this.emitter.removeAllListeners();
}
}
return new QuickPick();
},
createOutputChannel: function(name?: string) {
return {
name: name,
Expand Down

0 comments on commit 956e6c5

Please sign in to comment.