Files
plainleaf/client/client_system.ts
T
rktjmpandGitHub 8754b685ba Ensure command keybindings work reliably in any vim-mode (#1821)
When the vim extension is enabled and in "normal" mode, some default
bindings collide with Silverbullets commands (eg: `ctrl-p` - Share,
`ctrl-o` - Open Document, `ctrl-e` - Export, `ctrl-g h` - Home). These
vim bindings are not always functional (they may appear to do nothing
depending on vims state). This gives the impression that Silverbullet is
bugged and wont trigger commands. In "insert" mode, most bindings are
passed through correctly with some exceptions (eg: `ctrl-o`).

By splitting the command keybindings into their own keymap extension and
loading it before the vim extension, we can ensure Silverbullet commands
always trigger.

This does mean some vim bindings are overshadowed, but if a binding
triggers a Silverbullet command, its generally obvious that *something*
happened and the user should be able to recognise why the vim binding
failed and then rebind the command or vim mapping.
2026-02-06 16:12:02 +01:00

273 lines
9.0 KiB
TypeScript

import { PlugNamespaceHook } from "./plugos/hooks/plug_namespace.ts";
import type { SilverBulletHooks } from "@silverbulletmd/silverbullet/type/manifest";
import type { EventHook } from "./plugos/hooks/event.ts";
import { createWorkerSandboxFromLocalPath } from "./plugos/sandboxes/web_worker_sandbox.ts";
import assetSyscalls from "./plugos/syscalls/asset.ts";
import { System } from "./plugos/system.ts";
import type { Client } from "./client.ts";
import { CodeWidgetHook } from "./plugos/hooks/code_widget.ts";
import { CommandHook } from "./plugos/hooks/command.ts";
import { SlashCommandHook } from "./plugos/hooks/slash_command.ts";
import { SyscallHook } from "./plugos/hooks/syscall.ts";
import { clientStoreSyscalls } from "./plugos/syscalls/clientStore.ts";
import { editorSyscalls } from "./plugos/syscalls/editor.ts";
import { sandboxFetchSyscalls } from "./plugos/syscalls/fetch.ts";
import { markdownSyscalls } from "./plugos/syscalls/markdown.ts";
import { shellSyscalls } from "./plugos/syscalls/shell.ts";
import {
spaceReadSyscalls,
spaceWriteSyscalls,
} from "./plugos/syscalls/space.ts";
import { syncSyscalls } from "./plugos/syscalls/sync.ts";
import { systemSyscalls } from "./plugos/syscalls/system.ts";
import type { Space } from "./space.ts";
import { MQHook } from "./plugos/hooks/mq.ts";
import { mqSyscalls } from "./plugos/syscalls/mq.ts";
import {
dataStoreReadSyscalls,
dataStoreWriteSyscalls,
} from "./plugos/syscalls/datastore.ts";
import type { DataStore } from "./data/datastore.ts";
import { languageSyscalls } from "./plugos/syscalls/language.ts";
import { codeWidgetSyscalls } from "./plugos/syscalls/code_widget.ts";
import { clientCodeWidgetSyscalls } from "./plugos/syscalls/client_code_widget.ts";
import { KVPrimitivesManifestCache } from "./plugos/manifest_cache.ts";
import { createCommandKeyBindings } from "./codemirror/editor_state.ts";
import type { DataStoreMQ } from "./data/mq.datastore.ts";
import { jsonschemaSyscalls } from "./plugos/syscalls/jsonschema.ts";
import { luaSyscalls } from "./plugos/syscalls/lua.ts";
import { indexSyscalls } from "./plugos/syscalls/index.ts";
import { configSyscalls } from "./plugos/syscalls/config.ts";
import { eventSyscalls } from "./plugos/syscalls/event.ts";
import { DocumentEditorHook } from "./plugos/hooks/document_editor.ts";
import type { Command } from "./types/command.ts";
import { SpaceLuaEnvironment } from "./space_lua.ts";
import { builtinPlugPaths } from "../plugs/builtin_plugs.ts";
import { ServiceRegistry } from "./service_registry.ts";
import { serviceRegistrySyscalls } from "./plugos/syscalls/service_registry.ts";
import type { ObjectIndex } from "./data/object_index.ts";
const mqTimeout = 10000; // 10s
const mqTimeoutRetry = 3;
/**
* Handles the extension-related mechanisms of the client by wrapping a PlugOS System object as well as Space Lua environments
*/
export class ClientSystem {
// PlugOS system
system!: System<SilverBulletHooks>;
// ... and hooks
commandHook!: CommandHook;
slashCommandHook!: SlashCommandHook;
namespaceHook!: PlugNamespaceHook;
codeWidgetHook!: CodeWidgetHook;
documentEditorHook!: DocumentEditorHook;
mqHook!: MQHook;
serviceRegistry!: ServiceRegistry;
// Space Lua
spaceLuaEnv: SpaceLuaEnvironment;
readonly scriptCommands = new Map<string, Command>();
scriptsLoaded: boolean = false;
// Known files (for UI)
readonly allKnownFiles = new Set<string>();
public knownFilesLoaded: boolean = false;
constructor(
private client: Client,
protected mq: DataStoreMQ,
public ds: DataStore,
public eventHook: EventHook,
private objectIndex: ObjectIndex,
public readOnlyMode: boolean,
) {
this.system = new System(undefined, {
manifestCache: new KVPrimitivesManifestCache<SilverBulletHooks>(
ds.kv,
"manifest",
),
});
this.spaceLuaEnv = new SpaceLuaEnvironment(this.system, objectIndex);
this.serviceRegistry = new ServiceRegistry(this.eventHook, client.config);
setInterval(() => {
mq.requeueTimeouts(mqTimeout, mqTimeoutRetry, true).catch(console.error);
}, 20000); // Look to requeue every 20s
this.system.addHook(this.eventHook);
// Plug page namespace hook
this.namespaceHook = new PlugNamespaceHook();
this.system.addHook(this.namespaceHook);
// Code widget hook
this.codeWidgetHook = new CodeWidgetHook();
this.system.addHook(this.codeWidgetHook);
// Document editor hook
this.documentEditorHook = new DocumentEditorHook();
this.system.addHook(this.documentEditorHook);
// Command hook
this.commandHook = new CommandHook(
this.readOnlyMode,
this.scriptCommands,
);
this.commandHook.on({
commandsUpdated: (commandMap) => {
this.client.ui?.viewDispatch({
type: "update-commands",
commands: commandMap,
});
// Replace the key mapping compartment (keybindings)
this.client.editorView.dispatch({
effects: this.client.commandKeyHandlerCompartment?.reconfigure(
createCommandKeyBindings(this.client),
),
});
},
});
this.slashCommandHook = new SlashCommandHook(this.client);
// MQ hook
this.mqHook = new MQHook(this.system, this.mq, this.client.config);
this.system.addHook(this.mqHook);
// Syscall hook
this.system.addHook(new SyscallHook());
this.eventHook.addLocalListener("editor:reloadState", async () => {
await this.reloadState();
});
}
init() {
// Init is called after the editor is initialized, so we can safely add the command hook
this.system.addHook(this.commandHook);
this.system.addHook(this.slashCommandHook);
// Syscalls available to all plugs
this.system.registerSyscalls(
[],
eventSyscalls(this.eventHook, this.client),
editorSyscalls(this.client),
spaceReadSyscalls(this.client),
systemSyscalls(client, this.readOnlyMode),
markdownSyscalls(client),
assetSyscalls(this.system),
codeWidgetSyscalls(this.codeWidgetHook),
clientCodeWidgetSyscalls(),
languageSyscalls(),
jsonschemaSyscalls(),
indexSyscalls(this.objectIndex, this.client),
//commandSyscalls(client),
luaSyscalls(this),
mqSyscalls(this.mq),
serviceRegistrySyscalls(this.serviceRegistry),
dataStoreReadSyscalls(this.ds, this),
dataStoreWriteSyscalls(this.ds),
syncSyscalls(this.client),
clientStoreSyscalls(this.ds),
configSyscalls(this.client.config),
);
if (!this.readOnlyMode) {
// Write syscalls
this.system.registerSyscalls(
[],
spaceWriteSyscalls(this.client),
);
// Syscalls that require some additional permissions
this.system.registerSyscalls(
["fetch"],
sandboxFetchSyscalls(this.client),
);
this.system.registerSyscalls(
["shell"],
shellSyscalls(this.client),
);
}
}
async loadLuaScripts() {
if (this.client.bootConfig.disableSpaceLua) {
console.info("Space Lua scripts are disabled, skipping loading scripts");
return;
}
if (!await this.objectIndex.hasFullIndexCompleted()) {
console.info(
"Not loading space scripts, since full indexing has not completed yet",
);
return;
}
this.client.config.clear();
try {
await this.spaceLuaEnv.reload();
} catch (e: any) {
console.error("Error loading Lua script:", e.message);
}
// Reset the space script commands
this.scriptCommands.clear();
for (
const [name, command] of Object.entries(
this.client.config.get<Record<string, Command>>("commands", {}),
)
) {
this.scriptCommands.set(name, command);
}
// Make scripted (slash) commands available
this.commandHook.throttledBuildAllCommandsAndEmit();
this.slashCommandHook.throttledBuildAllCommands();
this.mqHook.throttledReloadQueues();
this.scriptsLoaded = true;
}
async reloadPlugsFromSpace(space: Space) {
console.log("(Re)loading plugs");
await this.system.unloadAll();
let allPlugs = await space.listPlugs();
// console.log("All plugs", allPlugs);
if (this.client.bootConfig.disablePlugs) {
// Only keep builtin plugs
allPlugs = allPlugs.filter(({ name }) => builtinPlugPaths.includes(name));
console.warn("Not loading custom plugs as `disablePlugs` has been set");
}
await Promise.all(allPlugs.map((fileMeta) =>
this.system.loadPlug(
createWorkerSandboxFromLocalPath(fileMeta.name),
fileMeta.name,
fileMeta.lastModified,
).catch((e) =>
console.error(
`Could not load plug ${fileMeta.name} error: ${e.message}`,
)
)
));
}
localSyscall(name: string, args: any[]) {
return this.system.localSyscall(name, args);
}
public async reloadState() {
console.log(
"Now loading space scripts, custom styles and rebuilding editor state",
);
await this.loadLuaScripts();
await this.client.loadCustomStyles();
this.client.rebuildEditorState();
}
}