diff --git a/package.json b/package.json index c63a334..1788096 100755 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ }, "version": "0.7.0", "engines": { - "vscode": "^1.53.0" + "vscode": "^1.60.0" }, "categories": [ "Other" @@ -37,9 +37,22 @@ { "command": "command-server.runCommand", "title": "Read command description from a file and execute the command" + }, + { + "command": "command-server.getState", + "title": "Get state" + }, + { + "command": "command-server.updateCoreState", + "title": "Update state based, on current when clause context" } ], "keybindings": [ + { + "command": "command-server.runCommand", + "key": "ctrl+shift+f11", + "mac": "cmd+shift+f11" + }, { "command": "command-server.runCommand", "key": "ctrl+shift+f17", @@ -49,6 +62,41 @@ "command": "command-server.runCommand", "key": "ctrl+shift+alt+p", "mac": "cmd+shift+alt+p" + }, + { + "command": "command-server.updateCoreState", + "key": "ctrl+shift+f10", + "mac": "cmd+shift+f10", + "args": { + "core.focus": null + } + }, + { + "command": "command-server.updateCoreState", + "key": "ctrl+shift+f10", + "mac": "cmd+shift+f10", + "when": "terminalFocus", + "args": { + "core.focus": "terminal" + } + }, + { + "command": "command-server.updateCoreState", + "key": "ctrl+shift+f10", + "mac": "cmd+shift+f10", + "when": "editorTextFocus", + "args": { + "core.focus": "editor" + } + }, + { + "command": "command-server.updateCoreState", + "key": "ctrl+shift+f10", + "mac": "cmd+shift+f10", + "when": "inQuickOpen", + "args": { + "core.focus": "quickOpen" + } } ], "configuration": { @@ -88,7 +136,7 @@ "@types/mocha": "^8.0.4", "@types/node": "^15.12.1", "@types/rimraf": "^3.0.0", - "@types/vscode": "^1.53.0", + "@types/vscode": "^1.60.0", "@typescript-eslint/eslint-plugin": "^4.9.0", "@typescript-eslint/parser": "^4.9.0", "eslint": "^7.15.0", diff --git a/src/commandRunner.ts b/src/commandRunner.ts index d071d84..704e54e 100644 --- a/src/commandRunner.ts +++ b/src/commandRunner.ts @@ -5,7 +5,7 @@ import * as vscode from "vscode"; import { readRequest, writeResponse } from "./io"; import { getResponsePath } from "./paths"; import { any } from "./regex"; -import { Request, Response } from "./types"; +import { Request } from "./types"; export default class CommandRunner { allowRegex!: RegExp; @@ -102,7 +102,7 @@ export default class CommandRunner { }); } catch (err) { await writeResponse(responseFile, { - error: err.message, + error: (err as Error).message, uuid, warnings, }); diff --git a/src/constants.ts b/src/constants.ts index 7e9563f..b59e234 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -5,3 +5,16 @@ export const STALE_TIMEOUT_MS = 60000; // The amount of time that client is expected to wait for VSCode to perform a // command, in seconds export const VSCODE_COMMAND_TIMEOUT_MS = 3000; + +export const FOCUS_KEY = "core.focus"; +export const EDITOR_FOCUSED = "editor"; +export const TERMINAL_FOCUSED = "terminal"; +export const UNKNOWN_FOCUSED = null; + +export const TERMINALS_KEY = "core.terminals"; +export const ACTIVE_TERMINAL_KEY = "core.activeTerminal"; +export const VISIBLE_EDITORS_KEY = "core.visibleEditors"; +export const ACTIVE_EDITOR_KEY = "core.activeEditor"; +export const FOCUSED_EDITOR_KEY = "core.focusedEditor"; +export const FOCUSED_TERMINAL_KEY = "core.focusedTerminal"; +export const WORKSPACE_FOLDERS_KEY = "core.workspaceFolders"; diff --git a/src/extension.ts b/src/extension.ts index 99466b0..79d812c 100755 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,18 +2,100 @@ import * as vscode from "vscode"; import { initializeCommunicationDir } from "./initializeCommunicationDir"; import CommandRunner from "./commandRunner"; +import State from "./state"; +import { updateCoreState, updateTerminalState } from "./updateCoreState"; +import StateUpdateSignaler from "./stateUpdateSignaler"; +import SignallingState from "./signallingState"; + +interface Api { + globalState: vscode.Memento; + workspaceState: vscode.Memento; +} + +interface KeyDescription { + key: string; +} export function activate(context: vscode.ExtensionContext) { initializeCommunicationDir(); const commandRunner = new CommandRunner(); + const stateUpdateSignaler = new StateUpdateSignaler(); + const globalState = new State(context.globalState); + const workspaceState = new State(context.workspaceState); + + let stateUpdaterPromise: Promise | null = null; + context.subscriptions.push( vscode.commands.registerCommand( "command-server.runCommand", commandRunner.runCommand + ), + vscode.commands.registerCommand( + "command-server.updateCoreState", + async (extraState: Record) => { + stateUpdaterPromise = (async () => { + for (const [key, value] of Object.entries(extraState)) { + await workspaceState.update(key, value); + } + + await updateCoreState(workspaceState, globalState); + })(); + + await stateUpdaterPromise; + + stateUpdaterPromise = null; + } + ), + vscode.commands.registerCommand( + "command-server.getState", + async (keys: KeyDescription[]) => { + if (stateUpdaterPromise != null) { + await stateUpdaterPromise; + } + + return Object.fromEntries( + keys.map(({ key }) => [ + key, + { + newValue: + workspaceState.get(key, null) ?? globalState.get(key, null), + }, + ]) + ); + } ) ); + + function updateTerminals() { + updateTerminalState(workspaceState, globalState); + } + + context.subscriptions.push( + vscode.window.onDidOpenTerminal(updateTerminals), + vscode.window.onDidCloseTerminal(updateTerminals), + vscode.window.onDidChangeActiveTerminal(updateTerminals) + // vscode.window.onDidChangeVisibleTextEditors(updateVisibleEditors), + // vscode.workspace.onDidChangeTextDocument(updateVisibleEditors), + // vscode.workspace.onDidCloseTextDocument(updateDocuments), + // vscode.workspace.onDidOpenTextDocument(updateDocuments) + ); + + // NB: we only signal if externals update; we don't bother for our own + // updates because update will shortly be requested + const api: Api = { + workspaceState: new SignallingState( + workspaceState, + stateUpdateSignaler.signalStateUpdated + ), + globalState: new SignallingState( + globalState, + stateUpdateSignaler.signalStateUpdated + ), + }; + + return api; } // this method is called when your extension is deactivated diff --git a/src/paths.ts b/src/paths.ts index 2b5bdf2..a30c02a 100644 --- a/src/paths.ts +++ b/src/paths.ts @@ -18,3 +18,7 @@ export function getRequestPath() { export function getResponsePath() { return join(getCommunicationDirPath(), "response.json"); } + +export function getStateUpdatedSignalPath() { + return join(getCommunicationDirPath(), "stateUpdatedSignal"); +} diff --git a/src/signallingState.ts b/src/signallingState.ts new file mode 100644 index 0000000..c4ab624 --- /dev/null +++ b/src/signallingState.ts @@ -0,0 +1,21 @@ +import { Memento } from "vscode"; + +export default class SignallingState implements Memento { + constructor( + private storage: Memento, + private signalStateUpdated: (key: string) => unknown + ) {} + + get(key: string, defaultValue?: T): T | undefined { + return this.storage.get(key, defaultValue); + } + + keys(): readonly string[] { + return this.storage.keys(); + } + + async update(key: string, value: any): Promise { + await this.storage.update(key, value); + this.signalStateUpdated(key); + } +} diff --git a/src/state.ts b/src/state.ts new file mode 100644 index 0000000..581832d --- /dev/null +++ b/src/state.ts @@ -0,0 +1,26 @@ +import { Memento } from "vscode"; + +const STORAGE_KEY = "pokey.command-server.state"; + +function getKey(key: string) { + return `${STORAGE_KEY}.${key}`; +} + +export default class State implements Memento { + constructor(private storage: Memento) {} + + get(key: string, defaultValue?: T): T | undefined { + return this.storage.get(getKey(key), defaultValue); + } + + keys(): readonly string[] { + return this.storage + .keys() + .filter((key) => key.startsWith(STORAGE_KEY)) + .map((key) => key.substring(STORAGE_KEY.length + 1)); + } + + async update(key: string, value: any): Promise { + await this.storage.update(getKey(key), value); + } +} diff --git a/src/stateUpdateSignaler.ts b/src/stateUpdateSignaler.ts new file mode 100644 index 0000000..61521e9 --- /dev/null +++ b/src/stateUpdateSignaler.ts @@ -0,0 +1,12 @@ +import { getStateUpdatedSignalPath } from "./paths"; +import touch from "./touch"; + +export default class StateUpdateSignaler { + constructor() { + this.signalStateUpdated = this.signalStateUpdated.bind(this); + } + + async signalStateUpdated(key: string) { + await touch(getStateUpdatedSignalPath()); + } +} diff --git a/src/touch.ts b/src/touch.ts new file mode 100644 index 0000000..3980afa --- /dev/null +++ b/src/touch.ts @@ -0,0 +1,22 @@ +// From https://gist.githubusercontent.com/remarkablemark/17c9c6a22a41510b2edfa3041ccca95a/raw/b11f087313b4c6f32733989ee24d65e1b643f007/touch-promise.js + +import { close, open, utimes } from "fs"; + +const touch = (path: string) => { + return new Promise((resolve, reject) => { + const time = new Date(); + utimes(path, time, time, (err) => { + if (err) { + return open(path, "w", (err, fd) => { + if (err) { + return reject(err); + } + close(fd, (err) => (err ? reject(err) : resolve())); + }); + } + resolve(); + }); + }); +}; + +export default touch; diff --git a/src/updateCoreState.ts b/src/updateCoreState.ts new file mode 100644 index 0000000..d412ba6 --- /dev/null +++ b/src/updateCoreState.ts @@ -0,0 +1,84 @@ +import { Terminal, TextDocument, TextEditor, window, workspace } from "vscode"; +import { + ACTIVE_EDITOR_KEY, + ACTIVE_TERMINAL_KEY, + EDITOR_FOCUSED, + FOCUSED_EDITOR_KEY, + FOCUSED_TERMINAL_KEY, + FOCUS_KEY, + TERMINALS_KEY, + TERMINAL_FOCUSED, + VISIBLE_EDITORS_KEY, + WORKSPACE_FOLDERS_KEY, +} from "./constants"; +import State from "./state"; + +export async function updateCoreState( + workspaceState: State, + globalState: State +) { + const focus = workspaceState.get(FOCUS_KEY); + + const editors = window.visibleTextEditors.map(serializeTextEditor); + + const activeEditor = + window.activeTextEditor == null + ? null + : serializeTextEditor(window.activeTextEditor); + + await workspaceState.update(VISIBLE_EDITORS_KEY, editors); + await workspaceState.update(ACTIVE_EDITOR_KEY, activeEditor); + await workspaceState.update( + FOCUSED_EDITOR_KEY, + focus === EDITOR_FOCUSED ? activeEditor : null + ); + await workspaceState.update( + FOCUSED_TERMINAL_KEY, + focus === TERMINAL_FOCUSED + ? workspaceState.get(ACTIVE_TERMINAL_KEY) + : null + ); + + await workspaceState.update( + WORKSPACE_FOLDERS_KEY, + workspace.workspaceFolders?.map((folder) => ({ + uri: folder.uri.toString(), + name: folder.name, + })) ?? [] + ); +} + +export async function updateTerminalState( + workspaceState: State, + globalState: State +) { + const terminals = await Promise.all(window.terminals.map(serializeTerminal)); + const activeTerminal = + window.activeTerminal == null + ? null + : await serializeTerminal(window.activeTerminal); + await workspaceState.update(TERMINALS_KEY, terminals); + await workspaceState.update(ACTIVE_TERMINAL_KEY, activeTerminal); +} + +function serializeTextEditor(textEditor: TextEditor) { + return { document: serializeDocument(textEditor.document) }; +} + +function serializeDocument(document: TextDocument) { + const { uri, isUntitled, isDirty, fileName, languageId, version } = document; + return { + uri: uri.toString(), + isUntitled, + isDirty, + fileName, + languageId, + version, + }; +} + +async function serializeTerminal(terminal: Terminal) { + const { name } = terminal; + + return { name, processId: await terminal.processId }; +} diff --git a/tsconfig.json b/tsconfig.json index b65c745..0d7e849 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "target": "es6", "outDir": "out", "lib": [ - "es6" + "es2020" ], "sourceMap": true, "rootDir": "src", diff --git a/yarn.lock b/yarn.lock index 80e75dc..7a5e398 100644 --- a/yarn.lock +++ b/yarn.lock @@ -107,9 +107,9 @@ "@types/node" "*" "@types/vscode@^1.53.0": - version "1.53.0" - resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.53.0.tgz#47b53717af6562f2ad05171bc9c8500824a3905c" - integrity sha512-XjFWbSPOM0EKIT2XhhYm3D3cx3nn3lshMUcWNy1eqefk+oqRuBq8unVb6BYIZqXy9lQZyeUl7eaBCOZWv+LcXQ== + version "1.60.0" + resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.60.0.tgz#9330c317691b4f53441a18b598768faeeb71618a" + integrity sha512-wZt3VTmzYrgZ0l/3QmEbCq4KAJ71K3/hmMQ/nfpv84oH8e81KKwPEoQ5v8dNCxfHFVJ1JabHKmCvqdYOoVm1Ow== "@typescript-eslint/eslint-plugin@^4.9.0": version "4.15.2"