Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,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>
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
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
Loading