Skip to content

Commit d2e3e0b

Browse files
authored
feat(vscode): implemented quickPickProject (#1025)
1 parent 7dd6b2a commit d2e3e0b

File tree

3 files changed

+337
-3
lines changed

3 files changed

+337
-3
lines changed

extensions/vscode/src/test/suite/utils/dart-frog-application.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ suite("quickPickApplication", () => {
265265
});
266266

267267
suite("returns", () => {
268-
test("returns undefined when dismissed", async () => {
268+
test("undefined when dismissed", async () => {
269269
const application = quickPickApplication({}, [
270270
application1,
271271
application2,
@@ -280,7 +280,7 @@ suite("quickPickApplication", () => {
280280
assert.strictEqual(selection, undefined);
281281
});
282282

283-
test("returns application when selected", async () => {
283+
test("application when selected", async () => {
284284
const application = quickPickApplication({}, [
285285
application1,
286286
application2,

extensions/vscode/src/test/suite/utils/dart-frog-project.test.ts

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,272 @@ suite("resolveDartFrogProjectPathFromActiveTextEditor", () => {
647647
});
648648
});
649649

650+
suite("quickPickProject", () => {
651+
let vscodeStub: any;
652+
let quickPickProject: any;
653+
let quickPick: any;
654+
655+
const projectPath1 = "home/project1";
656+
const projectPath2 = "home/project2";
657+
658+
beforeEach(() => {
659+
vscodeStub = {
660+
window: {
661+
createQuickPick: sinon.stub(),
662+
},
663+
};
664+
665+
quickPickProject = proxyquire("../../../utils/dart-frog-project", {
666+
vscode: vscodeStub,
667+
}).quickPickProject;
668+
669+
quickPick = sinon.stub();
670+
vscodeStub.window.createQuickPick.returns(quickPick);
671+
quickPick.show = sinon.stub();
672+
quickPick.dispose = sinon.stub();
673+
quickPick.onDidChangeSelection = sinon.stub();
674+
});
675+
676+
afterEach(() => {
677+
sinon.restore();
678+
});
679+
680+
suite("placeholder", () => {
681+
test("is defined by default", async () => {
682+
const project = quickPickProject({}, [projectPath1, projectPath2]);
683+
684+
const onDidChangeSelection =
685+
quickPick.onDidChangeSelection.getCall(0).args[0];
686+
onDidChangeSelection([]);
687+
688+
await project;
689+
690+
assert.strictEqual(quickPick.placeholder, "Select a Dart Frog project");
691+
});
692+
693+
test("can be overridden", async () => {
694+
const placeHolder = "placeholder";
695+
const project = quickPickProject(
696+
{
697+
placeHolder,
698+
},
699+
[projectPath1, projectPath2]
700+
);
701+
702+
const onDidChangeSelection =
703+
quickPick.onDidChangeSelection.getCall(0).args[0];
704+
onDidChangeSelection([]);
705+
706+
await project;
707+
708+
assert.strictEqual(quickPick.placeholder, placeHolder);
709+
});
710+
});
711+
712+
suite("ignoreFocusOut", () => {
713+
test("is true by default", async () => {
714+
const project = quickPickProject({}, [projectPath1, projectPath2]);
715+
716+
const onDidChangeSelection =
717+
quickPick.onDidChangeSelection.getCall(0).args[0];
718+
onDidChangeSelection([]);
719+
720+
await project;
721+
722+
assert.strictEqual(quickPick.ignoreFocusOut, true);
723+
});
724+
725+
test("can be overridden", async () => {
726+
const ignoreFocusOut = false;
727+
const project = quickPickProject(
728+
{
729+
ignoreFocusOut,
730+
},
731+
[projectPath1, projectPath2]
732+
);
733+
734+
const onDidChangeSelection =
735+
quickPick.onDidChangeSelection.getCall(0).args[0];
736+
onDidChangeSelection([]);
737+
738+
await project;
739+
740+
assert.strictEqual(quickPick.ignoreFocusOut, ignoreFocusOut);
741+
});
742+
});
743+
744+
suite("canSelectMany", () => {
745+
test("is false by default", async () => {
746+
const project = quickPickProject({}, [projectPath1, projectPath2]);
747+
748+
const onDidChangeSelection =
749+
quickPick.onDidChangeSelection.getCall(0).args[0];
750+
onDidChangeSelection([]);
751+
752+
await project;
753+
754+
assert.strictEqual(quickPick.canSelectMany, false);
755+
});
756+
757+
test("can be overridden", async () => {
758+
const canPickMany = true;
759+
const project = quickPickProject(
760+
{
761+
canPickMany,
762+
},
763+
[projectPath1, projectPath2]
764+
);
765+
766+
const onDidChangeSelection =
767+
quickPick.onDidChangeSelection.getCall(0).args[0];
768+
onDidChangeSelection([]);
769+
770+
await project;
771+
772+
assert.strictEqual(quickPick.canSelectMany, canPickMany);
773+
});
774+
});
775+
776+
test("busy is false by default", async () => {
777+
const project = quickPickProject({}, [projectPath1, projectPath2]);
778+
779+
const onDidChangeSelection =
780+
quickPick.onDidChangeSelection.getCall(0).args[0];
781+
onDidChangeSelection([]);
782+
783+
await project;
784+
785+
assert.strictEqual(quickPick.busy, false);
786+
});
787+
788+
test("shows appropiate items for resolved projects", async () => {
789+
const project = quickPickProject({}, [projectPath1, projectPath2]);
790+
791+
const onDidChangeSelection =
792+
quickPick.onDidChangeSelection.getCall(0).args[0];
793+
onDidChangeSelection([]);
794+
795+
await project;
796+
797+
const items = quickPick.items;
798+
799+
sinon.assert.match(items[0], {
800+
label: `$(dart-frog) project1`,
801+
description: projectPath1,
802+
projectPath: projectPath1,
803+
});
804+
sinon.assert.match(items[1], {
805+
label: `$(dart-frog) project2`,
806+
description: projectPath2,
807+
projectPath: projectPath2,
808+
});
809+
});
810+
811+
test("shows the quick pick", async () => {
812+
const project = quickPickProject({}, [projectPath1, projectPath2]);
813+
814+
const onDidChangeSelection =
815+
quickPick.onDidChangeSelection.getCall(0).args[0];
816+
onDidChangeSelection([]);
817+
818+
await project;
819+
820+
sinon.assert.calledOnce(quickPick.show);
821+
});
822+
823+
suite("onDidSelectItem", () => {
824+
test("is called when an item is selected", async () => {
825+
const onDidSelectItem = sinon.stub();
826+
const project = quickPickProject(
827+
{
828+
onDidSelectItem,
829+
},
830+
[projectPath1, projectPath2]
831+
);
832+
833+
const onDidChangeSelection =
834+
quickPick.onDidChangeSelection.getCall(0).args[0];
835+
onDidChangeSelection([{ projectPath: projectPath1 }]);
836+
837+
await project;
838+
839+
sinon.assert.calledOnceWithExactly(onDidSelectItem, {
840+
projectPath: projectPath1,
841+
});
842+
});
843+
844+
test("is not called when an item is dismissed", async () => {
845+
const onDidSelectItem = sinon.stub();
846+
const project = quickPickProject(
847+
{
848+
onDidSelectItem,
849+
},
850+
[projectPath1, projectPath2]
851+
);
852+
853+
const onDidChangeSelection =
854+
quickPick.onDidChangeSelection.getCall(0).args[0];
855+
onDidChangeSelection(undefined);
856+
857+
await project;
858+
859+
sinon.assert.notCalled(onDidSelectItem);
860+
});
861+
});
862+
863+
suite("dispose", () => {
864+
test("is called when an item is selected", async () => {
865+
const project = quickPickProject({}, [projectPath1, projectPath2]);
866+
867+
const onDidChangeSelection =
868+
quickPick.onDidChangeSelection.getCall(0).args[0];
869+
onDidChangeSelection([{ projectPath: projectPath1 }]);
870+
871+
await project;
872+
873+
sinon.assert.calledOnce(quickPick.dispose);
874+
});
875+
876+
test("is called when an item is dismissed", async () => {
877+
const project = quickPickProject({}, [projectPath1, projectPath2]);
878+
879+
const onDidChangeSelection =
880+
quickPick.onDidChangeSelection.getCall(0).args[0];
881+
onDidChangeSelection(undefined);
882+
883+
await project;
884+
885+
sinon.assert.calledOnce(quickPick.dispose);
886+
});
887+
});
888+
889+
suite("returns", () => {
890+
test("undefined when dismissed", async () => {
891+
const project = quickPickProject({}, [projectPath1, projectPath2]);
892+
893+
const onDidChangeSelection =
894+
quickPick.onDidChangeSelection.getCall(0).args[0];
895+
onDidChangeSelection(undefined);
896+
897+
const selection = await project;
898+
899+
assert.strictEqual(selection, undefined);
900+
});
901+
902+
test("application when selected", async () => {
903+
const project = quickPickProject({}, [projectPath1, projectPath2]);
904+
905+
const onDidChangeSelection =
906+
quickPick.onDidChangeSelection.getCall(0).args[0];
907+
onDidChangeSelection([{ projectPath: projectPath1 }]);
908+
909+
const selection = await project;
910+
911+
assert.strictEqual(selection, projectPath1);
912+
});
913+
});
914+
});
915+
650916
/**
651917
* Example of a pubspec.yaml file that depends on Dart Frog.
652918
*

extensions/vscode/src/utils/dart-frog-project.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@
55

66
const fs = require("fs");
77
const path = require("node:path");
8-
import { window, workspace } from "vscode";
8+
import {
9+
QuickInputButton,
10+
QuickPickItem,
11+
QuickPickItemKind,
12+
QuickPickOptions,
13+
window,
14+
workspace,
15+
} from "vscode";
916

1017
/**
1118
* Normalizes a file path to Dart Frog route path from the root of the
@@ -247,3 +254,64 @@ export function resolveDartFrogProjectPathFromActiveTextEditor(
247254

248255
return undefined;
249256
}
257+
258+
/**
259+
* Prompts the user to select a Dart Frog project from a list of resolved Dart
260+
* Frog projects.
261+
*
262+
* @param options The options for the {@link QuickPick}.
263+
* @param projectPaths The resolved project paths.
264+
* @returns The selected project path or `undefined` if the user cancelled the
265+
* selection.
266+
*/
267+
export async function quickPickProject(
268+
options: QuickPickOptions,
269+
projectPaths: string[]
270+
) {
271+
const quickPick = window.createQuickPick<PickableDartFrogProject>();
272+
quickPick.placeholder = options.placeHolder ?? "Select a Dart Frog project";
273+
quickPick.busy = false;
274+
quickPick.ignoreFocusOut = options.ignoreFocusOut ?? true;
275+
quickPick.canSelectMany = options.canPickMany ?? false;
276+
quickPick.items = projectPaths.map(
277+
(projectPath) => new PickableDartFrogProject(projectPath)
278+
);
279+
quickPick.show();
280+
281+
return new Promise<string | undefined>((resolve) => {
282+
quickPick.onDidChangeSelection((value) => {
283+
quickPick.dispose();
284+
285+
const selection =
286+
!value || value.length === 0 ? undefined : value[0]!.projectPath;
287+
if (selection) {
288+
options.onDidSelectItem?.(value[0]);
289+
}
290+
291+
resolve(selection);
292+
});
293+
});
294+
}
295+
296+
/**
297+
* A {@link QuickPickItem} that represents a Dart Frog project.
298+
*
299+
* @see {@link quickPickApplication}
300+
*/
301+
class PickableDartFrogProject implements QuickPickItem {
302+
constructor(dartFrogProjectPath: string) {
303+
this.label = `$(dart-frog) ${path.basename(dartFrogProjectPath)}`;
304+
this.description = dartFrogProjectPath;
305+
this.projectPath = dartFrogProjectPath;
306+
}
307+
308+
public readonly projectPath: string;
309+
310+
label: string;
311+
kind?: QuickPickItemKind | undefined;
312+
description?: string | undefined;
313+
detail?: string | undefined;
314+
picked?: boolean | undefined;
315+
alwaysShow?: boolean | undefined;
316+
buttons?: readonly QuickInputButton[] | undefined;
317+
}

0 commit comments

Comments
 (0)