Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
},
"pnpm": {
"patchedDependencies": {
"@docusaurus/theme-search-algolia": "patches/@[email protected]",
"@types/[email protected]": "patches/@[email protected]",
"[email protected]": "patches/[email protected]"
},
Expand Down
4 changes: 4 additions & 0 deletions packages/common/src/cursorlessCommandIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const cursorlessCommandIds = [
"cursorless.recordScopeTests.saveActiveDocument",
"cursorless.showCheatsheet",
"cursorless.showDocumentation",
"cursorless.showInstallationDependencies",
"cursorless.showQuickPick",
"cursorless.takeSnapshot",
"cursorless.toggleDecorations",
Expand Down Expand Up @@ -89,6 +90,9 @@ export const cursorlessCommandDescriptions: Record<
"Bulk save scope tests for the active document",
),
["cursorless.showDocumentation"]: new VisibleCommand("Show documentation"),
["cursorless.showInstallationDependencies"]: new VisibleCommand(
"Show installation dependencies",
),
["cursorless.showScopeVisualizer"]: new VisibleCommand(
"Show the scope visualizer",
),
Expand Down
1 change: 1 addition & 0 deletions packages/cursorless-neovim/src/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export async function registerCommands(
// Other commands
["cursorless.showQuickPick"]: dummyCommandHandler,
["cursorless.showDocumentation"]: dummyCommandHandler,
["cursorless.showInstallationDependencies"]: dummyCommandHandler,
["cursorless.private.logQuickActions"]: dummyCommandHandler,

// Hats
Expand Down
5 changes: 5 additions & 0 deletions packages/cursorless-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@
"command": "cursorless.showDocumentation",
"title": "Cursorless: Show documentation"
},
{
"command": "cursorless.showInstallationDependencies",
"title": "Cursorless: Show installation dependencies"
},
{
"command": "cursorless.showScopeVisualizer",
"title": "Cursorless: Show the scope visualizer"
Expand Down Expand Up @@ -1275,6 +1279,7 @@
"@cursorless/node-common": "workspace:*",
"@cursorless/test-case-recorder": "workspace:*",
"@cursorless/vscode-common": "workspace:*",
"glob": "^11.0.0",
"itertools": "^2.3.2",
"lodash-es": "^4.17.21",
"nearley": "2.20.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
const vscode = acquireVsCodeApi();

const root = document.getElementById("root");

const header = document.createElement("h2");
header.textContent = "Cursorless extension is now running!";

const description = document.createElement("p");
const aDocs = link(
"https://www.cursorless.org/docs/user/installation",
"Click here to learn how to install Cursorless",
);
description.innerHTML = `<p>Lets check if all dependencies are installed.</p>${aDocs}`;

const messages = document.createElement("div");

root.append(header, description, messages);

const keyboardUserMessage =
"<p><i>If you're using Cursorless by keyboard you can ignore this message.</i></p>";

window.addEventListener("message", (event) => {
const { dontShow, dependencies } = event.data;
const children = [];

if (!dependencies.talon) {
const a = link("https://talonvoice.com", "talonvoice.com");
children.push(
getChild(
"Talon not installed",
`Cursorless requires Talon to function by voice.</br>You can download Talon from ${a}.${keyboardUserMessage}`,
),
);
} else if (!dependencies.cursorlessTalon) {
const a = link(
"https://github.com/cursorless-dev/cursorless-talon",
"github.com/cursorless-dev/cursorless-talon",
);
children.push(
getChild(
"Cursorless Talon scripts missing",
`Cursorless requires Talon user scripts to function by voice.</br>The scripts are available at ${a}.`,
),
);
}

if (!dependencies.commandServer) {
const a = link(
"https://marketplace.visualstudio.com/items?itemName=pokey.command-server",
"vscode marketplace",
);
children.push(
getChild(
"Command server extension not installed",
`Cursorless requires the command server extension to function by voice.</br>The extension is available at the ${a}.${keyboardUserMessage}`,
),
);
}

if (children.length === 0) {
children.push(getChild("All dependencies are installed!"));
}

const dontShowCheckbox = getDontShowCheckbox(dontShow);

messages.replaceChildren(...children, dontShowCheckbox);
});

function link(href, text) {
return `<a href="${href}">${text}</a>`;
}

function getChild(title, body) {
const child = document.createElement("div");
const titleElement = document.createElement("h4");
titleElement.textContent = title;
child.append(titleElement);
if (body != null) {
const bodyElement = document.createElement("p");
bodyElement.innerHTML = body;
child.append(bodyElement);
}
return child;
}

function getDontShowCheckbox(dontShow) {
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = dontShow;
checkbox.onchange = () => {
const command = { type: "dontShow", checked: checkbox.checked };
vscode.postMessage(command);
};
const label = document.createElement("label");
label.style.marginTop = "1rem";
const text = document.createTextNode("Don't show again");
label.append(checkbox, text);
return label;
}
141 changes: 141 additions & 0 deletions packages/cursorless-vscode/src/InstallationDependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { COMMAND_SERVER_EXTENSION_ID } from "@cursorless/vscode-common";
import { globSync } from "glob";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import * as vscode from "vscode";

const STATE_KEY = "dontShowInstallationDependencies";

export class InstallationDependencies {
private panel: vscode.WebviewPanel | undefined;

constructor(private extensionContext: vscode.ExtensionContext) {
this.show = this.show.bind(this);
this.maybeShow = this.maybeShow.bind(this);
}

/**
* Shows the installation dependencies webview.
*/
show() {
this.createWebview();
}

/**
* Shows the installation dependencies webview if there are missing dependencies.
*/
maybeShow() {
const state = this.getState();
if (!state.dontShow && hasMissingDependencies(state.dependencies)) {
this.createWebview();
}
}

private getState() {
return {
dontShow: !!this.extensionContext.globalState.get<boolean>(STATE_KEY),
dependencies: getDependencies(),
};
}

private createWebview() {
if (this.panel != null) {
this.panel.reveal();
return;
}

this.panel = vscode.window.createWebviewPanel(
"cursorless.installationDependencies",
"Cursorless dependencies",
{
viewColumn: vscode.ViewColumn.Active,
},
{
enableScripts: true,
},
);

const jsUri = this.panel.webview.asWebviewUri(
vscode.Uri.joinPath(
this.extensionContext.extensionUri,
"resources",
"installationDependencies.js",
),
);

this.panel.webview.html = getWebviewContent(this.panel.webview, jsUri);

const updateWebview = () => {
this.panel?.webview.postMessage(this.getState());
};

this.panel.onDidChangeViewState(updateWebview);

this.panel.webview.onDidReceiveMessage((message) => {
if (message.type === "dontShow") {
const checked = message.checked;
this.extensionContext.globalState.update(STATE_KEY, checked);
} else {
console.error(`Unknown message: ${message}`);
}
});

const interval = setInterval(updateWebview, 5000);

this.panel.onDidDispose(() => {
clearInterval(interval);
this.panel = undefined;
});

this.panel.webview.postMessage(this.getState());
}
}

function getDependencies(): Record<string, boolean> {
return {
talon: talonHomeExists(),
cursorlessTalon: cursorlessTalonExists(),
commandServer: commandServerInstalled(),
};
}

function hasMissingDependencies(dependencies: Record<string, boolean>) {
return Object.values(dependencies).some((value) => !value);
}

function talonHomeExists() {
return fs.existsSync(getTalonHomePath());
}

function cursorlessTalonExists() {
const talonUserPath = path.join(getTalonHomePath(), "user");
const files = globSync("*/src/cursorless.talon", { cwd: talonUserPath });
return files.length > 0;
}

function commandServerInstalled() {
const extension = vscode.extensions.getExtension(COMMAND_SERVER_EXTENSION_ID);
return extension != null;
}

function getTalonHomePath() {
return os.platform() === "win32"
? `${os.homedir()}\\AppData\\Roaming\\talon`
: `${os.homedir()}/.talon`;
}

function getWebviewContent(webview: vscode.Webview, jsUri: vscode.Uri) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Security-Policy" content="script-src ${webview.cspSource};" />
</head>

<body>
<div id="root"></div>
<script src="${jsUri}"></script>
</body>
</html>`;
}
6 changes: 6 additions & 0 deletions packages/cursorless-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import * as crypto from "crypto";
import * as os from "node:os";
import * as path from "node:path";
import * as vscode from "vscode";
import { InstallationDependencies } from "./InstallationDependencies";
import { ReleaseNotes } from "./ReleaseNotes";
import { ScopeTreeProvider } from "./ScopeTreeProvider";
import type {
Expand Down Expand Up @@ -165,6 +166,8 @@ export async function activate(
commandServerApi != null,
);

const installationDependencies = new InstallationDependencies(context);

context.subscriptions.push(storedTargetHighlighter(vscodeIDE, storedTargets));

const vscodeTutorial = createTutorial(
Expand All @@ -189,11 +192,14 @@ export async function activate(
keyboardCommands,
hats,
vscodeTutorial,
installationDependencies,
storedTargets,
);

void new ReleaseNotes(vscodeApi, context, normalizedIde.messages).maybeShow();

installationDependencies.maybeShow();

return {
testHelpers:
normalizedIde.runMode === "test"
Expand Down
3 changes: 3 additions & 0 deletions packages/cursorless-vscode/src/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
TestCaseRecorder,
} from "@cursorless/test-case-recorder";
import * as vscode from "vscode";
import type { InstallationDependencies } from "./InstallationDependencies";
import type { ScopeVisualizer } from "./ScopeVisualizerCommandApi";
import type { VscodeTutorial } from "./VscodeTutorial";
import { showDocumentation, showQuickPick } from "./commands";
Expand All @@ -36,6 +37,7 @@ export function registerCommands(
keyboardCommands: KeyboardCommands,
hats: VscodeHats,
tutorial: VscodeTutorial,
installationDependencies: InstallationDependencies,
storedTargets: StoredTargetMap,
): void {
const runCommandWrapper = async (run: () => Promise<unknown>) => {
Expand Down Expand Up @@ -82,6 +84,7 @@ export function registerCommands(
// Other commands
["cursorless.showQuickPick"]: showQuickPick,
["cursorless.showDocumentation"]: showDocumentation,
["cursorless.showInstallationDependencies"]: installationDependencies.show,

["cursorless.private.logQuickActions"]: logQuickActions,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ export const assets: Asset[] = [
source: "resources/font_measurements.js",
destination: "resources/font_measurements.js",
},
{
source: "resources/installationDependencies.js",
destination: "resources/installationDependencies.js",
},
{ source: "../../schemas", destination: "schemas" },
{
source: "../../third-party-licenses.csv",
Expand Down
4 changes: 3 additions & 1 deletion packages/vscode-common/src/getExtensionApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export async function getExtensionApiStrict<T>(extensionId: string) {
}

export const EXTENSION_ID = "pokey.cursorless";
export const COMMAND_SERVER_EXTENSION_ID = "pokey.command-server";

export const getCursorlessApi = () =>
getExtensionApiStrict<CursorlessApi>(EXTENSION_ID);

Expand All @@ -49,4 +51,4 @@ export const getParseTreeApi = () =>
* @returns Command server API or null if not installed
*/
export const getCommandServerApi = () =>
getExtensionApi<CommandServerApi>("pokey.command-server");
getExtensionApi<CommandServerApi>(COMMAND_SERVER_EXTENSION_ID);
Loading
Loading