Skip to content

Commit

Permalink
Show webview about missing installation dependencies (#2720)
Browse files Browse the repository at this point in the history
If the user is missing some installation dependencies. eg talon,
cursorless-talon, command server, we now show a web view informing them

Fixes #1953
Fixes #528

<img width="689" alt="Screenshot 2025-01-11T15-11-03 - Visual Studio
Code"
src="https://github.com/user-attachments/assets/6692cd78-fe4b-437b-a67f-8e906a4ffa4e"
/>

## Checklist

- [/] I have added
[tests](https://www.cursorless.org/docs/contributing/test-case-recorder/)
- [/] I have updated the
[docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and
[cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet)
- [/] I have not broken the cheatsheet

---------

Co-authored-by: Phil Cohen <[email protected]>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Jan 18, 2025
1 parent 66d77a6 commit ca1a80e
Show file tree
Hide file tree
Showing 12 changed files with 422 additions and 172 deletions.
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
82 changes: 82 additions & 0 deletions packages/cursorless-vscode/resources/installationDependencies.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<!doctype html>
<html lang="en">
<head>
<meta http-equiv="Content-Security-Policy" content="META_CONTENT" />
<style>
.hide {
display: none;
}
</style>
</head>

<body>
<h2>Cursorless extension is now running!</h2>

<a href="https://www.cursorless.org/docs/user/installation">
Click here to learn how to install Cursorless
</a>

<p>Let's check if all dependencies are installed.</p>

<div id="msg-talon" class="hide">
<h4>Talon not installed</h4>
<p>
Cursorless requires Talon to function by voice.
<br />
You can download Talon from
<a href="https://talonvoice.com">talonvoice.com</a>
</p>
<p>
<i>
If you're using Cursorless by keyboard, you can ignore this message.
</i>
</p>
</div>

<div id="msg-cursorless-talon" class="hide">
<h4>Cursorless Talon scripts missing</h4>
<p>
Cursorless requires Talon user scripts to function by voice.
<br />
The installation steps for the scripts are available at
<a
href="https://www.cursorless.org/docs/user/installation/#installing-the-talon-side"
>
github.com/cursorless-dev/cursorless-talon
</a>
</p>
</div>

<div id="msg-command-server" class="hide">
<h4>Command server extension not installed</h4>
<p>
Cursorless requires the command server extension to function by voice.
<br />
The extension is available at the
<a
href="https://marketplace.visualstudio.com/items?itemName=pokey.command-server"
>
Visual Studio Marketplace
</a>
</p>
<p>
<i>
If you're using Cursorless by keyboard, you can ignore this message.
</i>
</p>
</div>

<div id="msg-all-installed" class="hide">
<h4>All dependencies are installed!</h4>
</div>

<div style="margin-top: 1rem">
<label>
<input id="input-dont-show" type="checkbox" />
Don't show again
</label>
</div>

<script src="SCRIPT_SOURCE"></script>
</body>
</html>
26 changes: 26 additions & 0 deletions packages/cursorless-vscode/resources/installationDependencies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const vscode = acquireVsCodeApi();
const msgTalon = document.getElementById("msg-talon");
const msgCursorlessTalon = document.getElementById("msg-cursorless-talon");
const msgCommandServer = document.getElementById("msg-command-server");
const msgAllInstalled = document.getElementById("msg-all-installed");
const inputDontShow = document.getElementById("input-dont-show");

inputDontShow.onchange = (e) => {
const command = { type: "dontShow", checked: e.target.checked };
vscode.postMessage(command);
};

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

hide(msgTalon, dependencies.talon);
// No need to show missing Cursorless Talon if Talon itself is missing
hide(msgCursorlessTalon, dependencies.cursorlessTalon || !dependencies.talon);
hide(msgCommandServer, dependencies.commandServer);
hide(msgAllInstalled, hasMissingDependencies);
inputDontShow.checked = dontShow;
});

function hide(element, doHide) {
element.className = doHide ? "hide" : "";
}
148 changes: 148 additions & 0 deletions packages/cursorless-vscode/src/InstallationDependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
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.hasMissingDependencies && !state.dontShow) {
this.createWebview();
}
}

private getState() {
const dependencies = getDependencies();
const hasMissingDependencies = Object.values(dependencies).some(
(value) => !value,
);
return {
dontShow: !!this.extensionContext.globalState.get<boolean>(STATE_KEY),
hasMissingDependencies,
dependencies,
};
}

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,
},
);

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

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());
}

private getWebviewContent() {
if (this.panel == null) {
throw new Error("Panel not created yet");
}
const htmlPath = this.getResourceUri("installationDependencies.html");
const jsUri = this.getResourceUri("installationDependencies.js");
const template = fs
.readFileSync(htmlPath.fsPath, "utf8")
.replace("META_CONTENT", `script-src ${this.panel.webview.cspSource};`)
.replace("SCRIPT_SOURCE", jsUri.toString());
return template;
}

private getResourceUri(name: string): vscode.Uri {
if (this.panel == null) {
throw new Error("Panel not created yet");
}
return this.panel.webview.asWebviewUri(
vscode.Uri.joinPath(
this.extensionContext.extensionUri,
"resources",
name,
),
);
}
}

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

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

function cursorlessTalonExists() {
const talonUserPath = path.join(getTalonHomePath(), "user");
const files = globSync("**/*/src/cursorless.talon", {
cwd: talonUserPath,
maxDepth: 3,
});
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`;
}
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
8 changes: 8 additions & 0 deletions packages/cursorless-vscode/src/scripts/populateDist/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ export const assets: Asset[] = [
source: "resources/font_measurements.js",
destination: "resources/font_measurements.js",
},
{
source: "resources/installationDependencies.html",
destination: "resources/installationDependencies.html",
},
{
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

0 comments on commit ca1a80e

Please sign in to comment.