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]: https://github.com/microsoft/vscode-remote-release/issues/3665
This commit is contained in:
Elena Tanasoiu
2023-03-17 17:16:17 +00:00
parent 5b2093df8f
commit 4fa3c459a1
3 changed files with 147 additions and 1 deletions

View File

@@ -70,6 +70,7 @@ import {
showInformationMessageWithAction,
tmpDir,
tmpDirDisposal,
prepareCodeTour,
} from "./helpers";
import {
asError,
@@ -344,6 +345,8 @@ export async function activate(
codeQlExtension.variantAnalysisManager,
);
await prepareCodeTour();
return codeQlExtension;
}

View File

@@ -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,36 @@ 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;
// We need this path to check that the file exists on windows
const tutorialWorkspacePath = join(
currentFolder,
"tutorial.code-workspace",
);
const toursFolderPath = join(currentFolder, ".tours");
if (
existsSync(tutorialWorkspacePath) &&
existsSync(toursFolderPath) &&
!isCodespacesTemplate()
) {
const tutorialWorkspaceUri = Uri.parse(
join(
workspace.workspaceFolders[0].uri.fsPath,
"tutorial.code-workspace",
),
);
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.

View File

@@ -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,103 @@ 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
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 with no tour", () => {
it("should not open the tutorial workspace", async () => {
// 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");
});
});
});