Skip to content

Commit 4fa3c45

Browse files
committed
Open tutorial workspace on extension start
When opening https://github.com/github/codespaces-codeql/ in a codespace, it's easy to miss the prompt that tells you to open the tutorial.code-workspace file. In fact people actively dismiss the alert to get it out of the way. If you miss that prompt, you end up with a single-rooted workspace, which causes various other problems. While there is an open issue to allow VS Code to open a default workspace [1], there doesn't seem to have been any progress on it in the last two years. So we're taking matters into our own hands and forcing the extension to open the tutorial workspace, if it detects it. This will only happen if the following three conditions are met: - the .tours folder exists - the tutorial.code-workspace file exists - the CODESPACES_TEMPLATE setting hasn't been set NB: the `CODESPACES_TEMPLATE` setting can only be found if the tutorial.code-workspace has already been opened. So it's a good indicator that we're in the folder, but the user has ignored the prompt. [1]: microsoft/vscode-remote-release#3665
1 parent 5b2093d commit 4fa3c45

3 files changed

Lines changed: 147 additions & 1 deletion

File tree

extensions/ql-vscode/src/extension.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import {
7070
showInformationMessageWithAction,
7171
tmpDir,
7272
tmpDirDisposal,
73+
prepareCodeTour,
7374
} from "./helpers";
7475
import {
7576
asError,
@@ -344,6 +345,8 @@ export async function activate(
344345
codeQlExtension.variantAnalysisManager,
345346
);
346347

348+
await prepareCodeTour();
349+
347350
return codeQlExtension;
348351
}
349352

extensions/ql-vscode/src/helpers.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ensureDir,
66
writeFile,
77
opendir,
8+
existsSync,
89
} from "fs-extra";
910
import { promise as glob } from "glob-promise";
1011
import { load } from "js-yaml";
@@ -16,6 +17,7 @@ import {
1617
window as Window,
1718
workspace,
1819
env,
20+
commands,
1921
} from "vscode";
2022
import { CodeQLCliServer, QlpacksInfo } from "./cli";
2123
import { UserCancellationException } from "./commandRunner";
@@ -25,6 +27,7 @@ import { telemetryListener } from "./telemetry";
2527
import { RedactableError } from "./pure/errors";
2628
import { getQlPackPath } from "./pure/ql";
2729
import { dbSchemeToLanguage } from "./common/query-language";
30+
import { isCodespacesTemplate } from "./config";
2831

2932
// Shared temporary folder for the extension.
3033
export const tmpDir = dirSync({
@@ -266,6 +269,36 @@ export function isFolderAlreadyInWorkspace(folderName: string) {
266269
);
267270
}
268271

272+
/** Check if the current workspace is the CodeTour and open the workspace folder.
273+
* Without this, we can't run the code tour correctly.
274+
**/
275+
export async function prepareCodeTour(): Promise<void> {
276+
if (workspace.workspaceFolders?.length) {
277+
const currentFolder = workspace.workspaceFolders[0].uri.fsPath;
278+
279+
// We need this path to check that the file exists on windows
280+
const tutorialWorkspacePath = join(
281+
currentFolder,
282+
"tutorial.code-workspace",
283+
);
284+
const toursFolderPath = join(currentFolder, ".tours");
285+
286+
if (
287+
existsSync(tutorialWorkspacePath) &&
288+
existsSync(toursFolderPath) &&
289+
!isCodespacesTemplate()
290+
) {
291+
const tutorialWorkspaceUri = Uri.parse(
292+
join(
293+
workspace.workspaceFolders[0].uri.fsPath,
294+
"tutorial.code-workspace",
295+
),
296+
);
297+
await commands.executeCommand("vscode.openFolder", tutorialWorkspaceUri);
298+
}
299+
}
300+
}
301+
269302
/**
270303
* Provides a utility method to invoke a function only if a minimum time interval has elapsed since
271304
* the last invocation of that function.

extensions/ql-vscode/test/vscode-tests/no-workspace/helpers.test.ts

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
commands,
23
EnvironmentVariableCollection,
34
EnvironmentVariableMutator,
45
Event,
@@ -15,7 +16,14 @@ import {
1516
import { dump } from "js-yaml";
1617
import * as tmp from "tmp";
1718
import { join } from "path";
18-
import { writeFileSync, mkdirSync, ensureDirSync, symlinkSync } from "fs-extra";
19+
import {
20+
writeFileSync,
21+
mkdirSync,
22+
ensureDirSync,
23+
symlinkSync,
24+
writeFile,
25+
mkdir,
26+
} from "fs-extra";
1927
import { DirResult } from "tmp";
2028

2129
import {
@@ -24,13 +32,15 @@ import {
2432
isFolderAlreadyInWorkspace,
2533
isLikelyDatabaseRoot,
2634
isLikelyDbLanguageFolder,
35+
prepareCodeTour,
2736
showBinaryChoiceDialog,
2837
showBinaryChoiceWithUrlDialog,
2938
showInformationMessageWithAction,
3039
walkDirectory,
3140
} from "../../../src/helpers";
3241
import { reportStreamProgress } from "../../../src/commandRunner";
3342
import { QueryLanguage } from "../../../src/common/query-language";
43+
import { Setting } from "../../../src/config";
3444

3545
describe("helpers", () => {
3646
describe("Invocation rate limiter", () => {
@@ -559,3 +569,103 @@ describe("isFolderAlreadyInWorkspace", () => {
559569
expect(isFolderAlreadyInWorkspace("/third/path")).toBe(false);
560570
});
561571
});
572+
573+
describe("prepareCodeTour", () => {
574+
let dir: tmp.DirResult;
575+
576+
beforeEach(() => {
577+
dir = tmp.dirSync();
578+
579+
const mockWorkspaceFolders = [
580+
{
581+
uri: Uri.file(dir.name),
582+
name: "test",
583+
index: 0,
584+
},
585+
] as WorkspaceFolder[];
586+
587+
jest
588+
.spyOn(workspace, "workspaceFolders", "get")
589+
.mockReturnValue(mockWorkspaceFolders);
590+
});
591+
592+
afterEach(() => {
593+
dir.removeCallback();
594+
});
595+
596+
describe("if we're in the tour repo", () => {
597+
describe("if the workspace is not already open", () => {
598+
it("should open the tutorial workspace", async () => {
599+
// set up directory to have a 'tutorial.code-workspace' file
600+
const tutorialWorkspacePath = join(dir.name, "tutorial.code-workspace");
601+
await writeFile(tutorialWorkspacePath, "{}");
602+
603+
// set up a .tours directory to indicate we're in the tour codespace
604+
const tourDirPath = join(dir.name, ".tours");
605+
await mkdir(tourDirPath);
606+
607+
// spy that we open the workspace file by calling the 'vscode.openFolder' command
608+
const commandSpy = jest.spyOn(commands, "executeCommand");
609+
commandSpy.mockImplementation(() => Promise.resolve());
610+
611+
await prepareCodeTour();
612+
613+
expect(commandSpy).toHaveBeenCalledWith(
614+
"vscode.openFolder",
615+
expect.anything(),
616+
);
617+
});
618+
});
619+
620+
describe("if the workspace is already open", () => {
621+
it("should not open the tutorial workspace", async () => {
622+
// Set isCodespaceTemplate to true to indicate the workspace has already been opened
623+
jest.spyOn(Setting.prototype, "getValue").mockReturnValue(false);
624+
625+
// set up directory to have a 'tutorial.code-workspace' file
626+
const tutorialWorkspacePath = join(dir.name, "tutorial.code-workspace");
627+
await writeFile(tutorialWorkspacePath, "{}");
628+
629+
// set up a .tours directory to indicate we're in the tour codespace
630+
const tourDirPath = join(dir.name, ".tours");
631+
await mkdir(tourDirPath);
632+
633+
// spy that we open the workspace file by calling the 'vscode.openFolder' command
634+
const openFileSpy = jest.spyOn(commands, "executeCommand");
635+
openFileSpy.mockImplementation(() => Promise.resolve());
636+
637+
await prepareCodeTour();
638+
639+
expect(openFileSpy).not.toHaveBeenCalledWith("vscode.openFolder");
640+
});
641+
});
642+
});
643+
644+
describe("if we're in a different tour repo", () => {
645+
it("should not open the tutorial workspace", async () => {
646+
// set up a .tours directory
647+
const tourDirPath = join(dir.name, ".tours");
648+
await mkdir(tourDirPath);
649+
650+
// spy that we open the workspace file by calling the 'vscode.openFolder' command
651+
const openFileSpy = jest.spyOn(commands, "executeCommand");
652+
openFileSpy.mockImplementation(() => Promise.resolve());
653+
654+
await prepareCodeTour();
655+
656+
expect(openFileSpy).not.toHaveBeenCalledWith("vscode.openFolder");
657+
});
658+
});
659+
660+
describe("if we're in a different repo with no tour", () => {
661+
it("should not open the tutorial workspace", async () => {
662+
// spy that we open the workspace file by calling the 'vscode.openFolder' command
663+
const openFileSpy = jest.spyOn(commands, "executeCommand");
664+
openFileSpy.mockImplementation(() => Promise.resolve());
665+
666+
await prepareCodeTour();
667+
668+
expect(openFileSpy).not.toHaveBeenCalledWith("vscode.openFolder");
669+
});
670+
});
671+
});

0 commit comments

Comments
 (0)