Skip to content

Commit

Permalink
Open tutorial workspace on extension start
Browse files Browse the repository at this point in the history
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
  • Loading branch information
elenatanasoiu committed Mar 20, 2023
1 parent 42ce27b commit efb5bb9
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 1 deletion.
4 changes: 4 additions & 0 deletions extensions/ql-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
CliConfigListener,
DistributionConfigListener,
isCanary,
isCodespacesTemplate,
joinOrderWarningThreshold,
MAX_QUERIES,
QueryHistoryConfigListener,
Expand Down Expand Up @@ -70,6 +71,7 @@ import {
showInformationMessageWithAction,
tmpDir,
tmpDirDisposal,
prepareCodeTour,
} from "./helpers";
import {
asError,
Expand Down Expand Up @@ -344,6 +346,8 @@ export async function activate(
codeQlExtension.variantAnalysisManager,
);

await prepareCodeTour();

return codeQlExtension;
}

Expand Down
26 changes: 26 additions & 0 deletions extensions/ql-vscode/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ensureDir,
writeFile,
opendir,
existsSync,
} from "fs-extra";
import { promise as glob } from "glob-promise";
import { load } from "js-yaml";
Expand All @@ -16,6 +17,7 @@ import {
window as Window,
workspace,
env,
commands,
} from "vscode";
import { CodeQLCliServer, QlpacksInfo } from "./cli";
import { UserCancellationException } from "./commandRunner";
Expand All @@ -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({
Expand Down Expand Up @@ -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<void> {
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.
Expand Down
116 changes: 115 additions & 1 deletion extensions/ql-vscode/test/vscode-tests/no-workspace/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
commands,
EnvironmentVariableCollection,
EnvironmentVariableMutator,
Event,
Expand All @@ -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 {
Expand All @@ -24,13 +32,15 @@ import {
isFolderAlreadyInWorkspace,
isLikelyDatabaseRoot,
isLikelyDbLanguageFolder,
prepareCodeTour,
showBinaryChoiceDialog,
showBinaryChoiceWithUrlDialog,
showInformationMessageWithAction,
walkDirectory,
} 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", () => {
Expand Down Expand Up @@ -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");
});
});
});

0 comments on commit efb5bb9

Please sign in to comment.