Merge pull request #2188 from github/elena/force-workspace-in-codespace

Codespace: Open tutorial workspace on extension start
This commit is contained in:
Elena Tanasoiu
2023-03-22 08:30:31 +00:00
committed by GitHub
3 changed files with 177 additions and 1 deletions

View File

@@ -69,6 +69,7 @@ import {
showInformationMessageWithAction,
tmpDir,
tmpDirDisposal,
prepareCodeTour,
} from "./helpers";
import {
asError,
@@ -527,6 +528,14 @@ async function installOrUpdateThenTryActivate(
): Promise<CodeQLExtensionInterface | Record<string, never>> {
await installOrUpdateDistribution(ctx, distributionManager, config);
try {
await prepareCodeTour();
} catch (e: unknown) {
void extLogger.log(
`Could not open tutorial workspace automatically: ${getErrorMessage(e)}`,
);
}
// Display the warnings even if the extension has already activated.
const distributionResult =
await getDistributionDisplayingDistributionWarnings(distributionManager);

View File

@@ -16,6 +16,7 @@ import {
window as Window,
workspace,
env,
commands,
} from "vscode";
import { CodeQLCliServer, QlpacksInfo } from "./cli";
import { UserCancellationException } from "./commandRunner";
@@ -25,6 +26,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 +268,51 @@ 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 tutorialWorkspacePath = join(
currentFolder,
"tutorial.code-workspace",
);
const toursFolderPath = join(currentFolder, ".tours");
/** We're opening the tutorial workspace, if we detect 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 doesn't exist (it's only set if the user has already opened
* the tutorial workspace so it's a good indicator that the user is in the folder but has ignored
* the prompt to open the workspace)
*/
if (
(await pathExists(tutorialWorkspacePath)) &&
(await pathExists(toursFolderPath)) &&
!isCodespacesTemplate()
) {
const answer = await showBinaryChoiceDialog(
"We've detected you're in the CodeQL Tour repo. We will need to open the workspace file to continue. Reload?",
);
if (!answer) {
return;
}
const tutorialWorkspaceUri = Uri.parse(tutorialWorkspacePath);
void extLogger.log(
`In prepareCodeTour() method, going to open the tutorial workspace file: ${tutorialWorkspacePath}`,
);
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,113 @@ describe("isFolderAlreadyInWorkspace", () => {
expect(isFolderAlreadyInWorkspace("/third/path")).toBe(false);
});
});
describe("prepareCodeTour", () => {
let dir: tmp.DirResult;
let showInformationMessageSpy: jest.SpiedFunction<
typeof window.showInformationMessage
>;
beforeEach(() => {
dir = tmp.dirSync();
const mockWorkspaceFolders = [
{
uri: Uri.file(dir.name),
name: "test",
index: 0,
},
] as WorkspaceFolder[];
jest
.spyOn(workspace, "workspaceFolders", "get")
.mockReturnValue(mockWorkspaceFolders);
showInformationMessageSpy = jest
.spyOn(window, "showInformationMessage")
.mockResolvedValue({ title: "Yes" });
});
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(showInformationMessageSpy).toHaveBeenCalled();
expect(commandSpy).toHaveBeenCalledWith(
"vscode.openFolder",
expect.objectContaining({
path: Uri.parse(tutorialWorkspacePath).fsPath,
}),
);
});
});
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(true);
// 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).not.toHaveBeenCalled();
});
});
});
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 commandSpy = jest.spyOn(commands, "executeCommand");
commandSpy.mockImplementation(() => Promise.resolve());
await prepareCodeTour();
expect(commandSpy).not.toHaveBeenCalled();
});
});
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 commandSpy = jest.spyOn(commands, "executeCommand");
commandSpy.mockImplementation(() => Promise.resolve());
await prepareCodeTour();
expect(commandSpy).not.toHaveBeenCalled();
});
});
});