diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index a3502bb21ec..a31256fc174 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -36,6 +36,7 @@ import { CliConfigListener, DistributionConfigListener, isCanary, + isCodespacesTemplate, joinOrderWarningThreshold, MAX_QUERIES, QueryHistoryConfigListener, @@ -70,6 +71,7 @@ import { showInformationMessageWithAction, tmpDir, tmpDirDisposal, + prepareCodeTour, } from "./helpers"; import { asError, @@ -344,6 +346,8 @@ export async function activate( codeQlExtension.variantAnalysisManager, ); + await prepareCodeTour(); + return codeQlExtension; } diff --git a/extensions/ql-vscode/src/helpers.ts b/extensions/ql-vscode/src/helpers.ts index d3245a28786..7dccb2a83f5 100644 --- a/extensions/ql-vscode/src/helpers.ts +++ b/extensions/ql-vscode/src/helpers.ts @@ -5,6 +5,7 @@ import { ensureDir, writeFile, opendir, + existsSync, } from "fs-extra"; import { promise as glob } from "glob-promise"; import { load } from "js-yaml"; @@ -16,6 +17,7 @@ import { window as Window, workspace, env, + commands, } from "vscode"; import { CodeQLCliServer, QlpacksInfo } from "./cli"; import { UserCancellationException } from "./commandRunner"; @@ -25,6 +27,7 @@ import { telemetryListener } from "./telemetry"; import { RedactableError } from "./pure/errors"; import { getQlPackPath } from "./pure/ql"; import { dbSchemeToLanguage } from "./common/query-language"; +import { isCodespacesTemplate } from "./config"; // Shared temporary folder for the extension. export const tmpDir = dirSync({ @@ -266,6 +269,29 @@ export function isFolderAlreadyInWorkspace(folderName: string) { ); } +/** Check if the current workspace is the CodeTour and open the workspace folder. + * Without this, we can't run the code tour correctly. + **/ +export async function prepareCodeTour(): Promise { + if (workspace.workspaceFolders?.length) { + const currentFolder = workspace.workspaceFolders[0].uri.fsPath; + + const tutorialWorkspaceUri = Uri.parse( + join(workspace.workspaceFolders[0].uri.fsPath, "tutorial.code-workspace"), + ); + + const toursFolderPath = join(currentFolder, ".tours"); + + if ( + existsSync(tutorialWorkspaceUri.fsPath) && + existsSync(toursFolderPath) && + !isCodespacesTemplate() + ) { + await commands.executeCommand("vscode.openFolder", tutorialWorkspaceUri); + } + } +} + /** * Provides a utility method to invoke a function only if a minimum time interval has elapsed since * the last invocation of that function. diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/helpers.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/helpers.test.ts index cf2a022c212..91a2bcd0bcf 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/helpers.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/helpers.test.ts @@ -1,4 +1,5 @@ import { + commands, EnvironmentVariableCollection, EnvironmentVariableMutator, Event, @@ -15,7 +16,14 @@ import { import { dump } from "js-yaml"; import * as tmp from "tmp"; import { join } from "path"; -import { writeFileSync, mkdirSync, ensureDirSync, symlinkSync } from "fs-extra"; +import { + writeFileSync, + mkdirSync, + ensureDirSync, + symlinkSync, + writeFile, + mkdir, +} from "fs-extra"; import { DirResult } from "tmp"; import { @@ -24,6 +32,7 @@ import { isFolderAlreadyInWorkspace, isLikelyDatabaseRoot, isLikelyDbLanguageFolder, + prepareCodeTour, showBinaryChoiceDialog, showBinaryChoiceWithUrlDialog, showInformationMessageWithAction, @@ -31,6 +40,7 @@ import { } from "../../../src/helpers"; import { reportStreamProgress } from "../../../src/commandRunner"; import { QueryLanguage } from "../../../src/common/query-language"; +import { Setting } from "../../../src/config"; describe("helpers", () => { describe("Invocation rate limiter", () => { @@ -559,3 +569,107 @@ describe("isFolderAlreadyInWorkspace", () => { expect(isFolderAlreadyInWorkspace("/third/path")).toBe(false); }); }); + +describe("prepareCodeTour", () => { + let dir: tmp.DirResult; + + beforeEach(() => { + dir = tmp.dirSync(); + + const mockWorkspaceFolders = [ + { + uri: Uri.file(dir.name), + name: "test", + index: 0, + }, + ] as WorkspaceFolder[]; + + jest + .spyOn(workspace, "workspaceFolders", "get") + .mockReturnValue(mockWorkspaceFolders); + }); + + afterEach(() => { + dir.removeCallback(); + }); + + describe("if we're in the tour repo", () => { + describe("if the workspace is not already open", () => { + it("should open the tutorial workspace", async () => { + // set up directory to have a 'tutorial.code-workspace' file + const tutorialWorkspacePath = join(dir.name, "tutorial.code-workspace"); + await writeFile(tutorialWorkspacePath, "{}"); + + // set up a .tours directory to indicate we're in the tour codespace + const tourDirPath = join(dir.name, ".tours"); + await mkdir(tourDirPath); + + // spy that we open the workspace file by calling the 'vscode.openFolder' command + const commandSpy = jest.spyOn(commands, "executeCommand"); + commandSpy.mockImplementation(() => Promise.resolve()); + + await prepareCodeTour(); + + expect(commandSpy).toHaveBeenCalledWith( + "vscode.openFolder", + expect.anything(), + ); + }); + }); + + describe("if the workspace is already open", () => { + it("should not open the tutorial workspace", async () => { + // Set isCodespaceTemplate to true to indicate the workspace has already been opened + jest.spyOn(Setting.prototype, "getValue").mockReturnValue(false); + + // set up directory to have a 'tutorial.code-workspace' file + const tutorialWorkspacePath = join(dir.name, "tutorial.code-workspace"); + await writeFile(tutorialWorkspacePath, "{}"); + + // set up a .tours directory to indicate we're in the tour codespace + const tourDirPath = join(dir.name, ".tours"); + await mkdir(tourDirPath); + + // spy that we open the workspace file by calling the 'vscode.openFolder' command + const openFileSpy = jest.spyOn(commands, "executeCommand"); + openFileSpy.mockImplementation(() => Promise.resolve()); + + await prepareCodeTour(); + + expect(openFileSpy).not.toHaveBeenCalledWith("vscode.openFolder"); + }); + }); + }); + + describe("if we're in a different tour repo", () => { + it("should not open the tutorial workspace", async () => { + // set up a .tours directory to indicate we're in the tour codespace + const tourDirPath = join(dir.name, ".tours"); + await mkdir(tourDirPath); + + // spy that we open the workspace file by calling the 'vscode.openFolder' command + const openFileSpy = jest.spyOn(commands, "executeCommand"); + openFileSpy.mockImplementation(() => Promise.resolve()); + + await prepareCodeTour(); + + expect(openFileSpy).not.toHaveBeenCalledWith("vscode.openFolder"); + }); + }); + + describe("if we're in a different repo", () => { + it("should not open the tutorial workspace", async () => { + // set up a .tours directory to indicate we're in the tour codespace + const tourDirPath = join(dir.name, ".tours"); + await mkdir(tourDirPath); + + // spy that we open the workspace file by calling the 'vscode.openFolder' command + const openFileSpy = jest.spyOn(commands, "executeCommand"); + openFileSpy.mockImplementation(() => Promise.resolve()); + + await prepareCodeTour(); + + expect(openFileSpy).not.toHaveBeenCalledWith("vscode.openFolder"); + }); + }); +});