CI / config (push) Canceled after 0s
CI / test-frontend (push) Canceled after 0s
CI / test-rust (push) Canceled after 0s
CI / test-e2e (push) Canceled after 0s
CI / test-e2e-release (push) Canceled after 0s
CI / build (push) Canceled after 0s
CI / release (push) Canceled after 0s
CI / docker (push) Canceled after 0s
CI / docker-website (push) Canceled after 0s
Add a persistent system-content switch, translate the extension manager, and apply theme changes in place without reloading the browser UI. Co-Authored-By: Codex <noreply@anthropic.com>
1552 lines
48 KiB
TypeScript
1552 lines
48 KiB
TypeScript
import {
|
|
acceptCompletion,
|
|
closeCompletion,
|
|
moveCompletionSelection,
|
|
startCompletion,
|
|
} from "@codemirror/autocomplete";
|
|
import {
|
|
cursorCharLeft,
|
|
cursorCharRight,
|
|
cursorDocEnd,
|
|
cursorDocStart,
|
|
cursorGroupLeft,
|
|
cursorGroupRight,
|
|
cursorLineBoundaryLeft,
|
|
cursorLineBoundaryRight,
|
|
cursorLineDown,
|
|
cursorLineEnd,
|
|
cursorLineStart,
|
|
cursorLineUp,
|
|
cursorPageDown,
|
|
cursorPageUp,
|
|
deleteCharBackward,
|
|
deleteCharForward,
|
|
deleteGroupBackward,
|
|
deleteGroupForward,
|
|
deleteLine,
|
|
deleteLineBoundaryBackward,
|
|
deleteLineBoundaryForward,
|
|
indentLess,
|
|
indentMore,
|
|
insertNewline,
|
|
insertNewlineAndIndent,
|
|
moveLineDown,
|
|
moveLineUp,
|
|
redo,
|
|
selectAll,
|
|
selectCharLeft,
|
|
selectCharRight,
|
|
selectDocEnd,
|
|
selectDocStart,
|
|
selectGroupLeft,
|
|
selectGroupRight,
|
|
selectLineBoundaryLeft,
|
|
selectLineBoundaryRight,
|
|
selectLineDown,
|
|
selectLineEnd,
|
|
selectLineStart,
|
|
selectLineUp,
|
|
selectPageDown,
|
|
selectPageUp,
|
|
toggleComment,
|
|
transposeChars,
|
|
undo,
|
|
} from "@codemirror/commands";
|
|
import { insertNewlineContinueMarkup } from "@codemirror/lang-markdown";
|
|
import {
|
|
foldAll,
|
|
foldCode,
|
|
toggleFold,
|
|
unfoldAll,
|
|
unfoldCode,
|
|
} from "@codemirror/language";
|
|
import { forceLinting } from "@codemirror/lint";
|
|
import { openSearchPanel } from "@codemirror/search";
|
|
import type { Transaction } from "@codemirror/state";
|
|
import { EditorView } from "@codemirror/view";
|
|
import {
|
|
coerceAndValidateRef,
|
|
type Ref,
|
|
} from "@silverbulletmd/silverbullet/lib/ref";
|
|
import type {
|
|
FilterOption,
|
|
NotificationAction,
|
|
NotificationType,
|
|
UploadFile,
|
|
} from "@silverbulletmd/silverbullet/type/client";
|
|
import type { VimConfig } from "@silverbulletmd/silverbullet/type/config";
|
|
import type { PageMeta } from "@silverbulletmd/silverbullet/type/index";
|
|
import { updateBakedSections } from "../../baked_sections/bake.ts";
|
|
import type { Client } from "../../client.ts";
|
|
import { refreshLintEffect } from "../../codemirror/lint.ts";
|
|
import { getVimModule } from "../../vim_loader.ts";
|
|
import type { SysCallMapping } from "../system.ts";
|
|
|
|
export function editorSyscalls(client: Client): SysCallMapping {
|
|
const syscalls: SysCallMapping = {
|
|
"editor.getCurrentPage": {
|
|
callback: (): string => {
|
|
return client.currentName();
|
|
},
|
|
description:
|
|
"Returns the name of the page or document currently open in the editor.",
|
|
returns: [{ type: "string", description: "The current page name." }],
|
|
},
|
|
"editor.getCurrentPageMeta": {
|
|
callback: (): Promise<PageMeta | undefined> => {
|
|
const name = client.currentName();
|
|
return client.objectIndex.getObjectByRef(name, "page", name);
|
|
},
|
|
description:
|
|
"Returns metadata for the page or document currently open in the editor.",
|
|
returns: [
|
|
{
|
|
type: "PageMeta | undefined",
|
|
description: "The current page metadata, if indexed.",
|
|
},
|
|
],
|
|
},
|
|
"editor.getCurrentPath": {
|
|
callback: (_ctx): string => {
|
|
return client.currentPath();
|
|
},
|
|
description:
|
|
"Returns the path of the page or document currently open in the editor.",
|
|
returns: [{ type: "string", description: "The current page path." }],
|
|
},
|
|
"editor.getCurrentEditor": {
|
|
callback: (): string => {
|
|
return client.contentManager.documentEditor?.name || "page";
|
|
},
|
|
description:
|
|
"Returns the name of the currently active editor implementation.",
|
|
returns: [
|
|
{
|
|
type: "string",
|
|
description: "The editor name, or `page` for the page editor.",
|
|
},
|
|
],
|
|
},
|
|
"editor.getRecentlyOpenedPages": {
|
|
callback: (): PageMeta[] => {
|
|
return client.ui.viewState.allPages.sort(
|
|
(a, b) => (b.lastOpened || 0) - (a.lastOpened || 0),
|
|
);
|
|
},
|
|
description:
|
|
"Returns page metadata ordered from most to least recently opened.",
|
|
returns: [{ type: "PageMeta[]", description: "Recently opened pages." }],
|
|
},
|
|
"editor.getText": {
|
|
callback: () => {
|
|
return client.editorView.state.sliceDoc();
|
|
},
|
|
description:
|
|
"Returns the full text of the currently open page or document.",
|
|
returns: [{ type: "string", description: "The editor contents." }],
|
|
},
|
|
"editor.getCurrentLine": {
|
|
callback: (): {
|
|
from: number;
|
|
to: number;
|
|
text: string;
|
|
textWithCursor: string;
|
|
} => {
|
|
const pos = client.editorView.state.selection.main.from;
|
|
const line = client.editorView.state.doc.lineAt(pos);
|
|
return {
|
|
...line,
|
|
textWithCursor:
|
|
line.text.slice(0, pos - line.from) +
|
|
"|^|" +
|
|
line.text.slice(pos - line.from),
|
|
};
|
|
},
|
|
description:
|
|
"Returns the current line's range and text, including a `|^|` cursor marker variant.",
|
|
returns: [
|
|
{
|
|
type: "{ from: number; to: number; text: string; textWithCursor: string }",
|
|
description: "The line containing the main selection head.",
|
|
},
|
|
],
|
|
},
|
|
"editor.setText": {
|
|
callback: (_ctx, newText: string, shouldIsolateHistory = false) => {
|
|
client.contentManager.setEditorText(newText, shouldIsolateHistory);
|
|
},
|
|
description:
|
|
"Updates the editor text with a minimal diff while preserving the cursor when possible.",
|
|
parameters: [
|
|
{
|
|
name: "newText",
|
|
type: "string",
|
|
description: "The complete replacement text.",
|
|
},
|
|
{
|
|
name: "shouldIsolateHistory",
|
|
type: "boolean",
|
|
description: "Whether to isolate the change in undo history.",
|
|
optional: true,
|
|
},
|
|
],
|
|
},
|
|
"editor.getCursor": {
|
|
callback: (): number => {
|
|
return client.editorView.state.selection.main.from;
|
|
},
|
|
description:
|
|
"Returns the cursor position as a character offset from the start of the document.",
|
|
returns: [{ type: "number", description: "The cursor offset." }],
|
|
},
|
|
"editor.getSelection": {
|
|
callback: (): { from: number; to: number; text: string } => {
|
|
const selection = client.editorView.state.selection.main;
|
|
const text = client.editorView.state.sliceDoc(
|
|
selection.from,
|
|
selection.to,
|
|
);
|
|
return {
|
|
from: selection.from,
|
|
to: selection.to,
|
|
text,
|
|
};
|
|
},
|
|
description: "Returns the current selection range and selected text.",
|
|
returns: [
|
|
{
|
|
type: "{ from: number; to: number; text: string }",
|
|
description: "The main editor selection.",
|
|
},
|
|
],
|
|
},
|
|
"editor.save": {
|
|
callback: () => {
|
|
return client.save(true);
|
|
},
|
|
description: "Forces the current page or document to be saved.",
|
|
},
|
|
"editor.navigate": {
|
|
callback: async (
|
|
_ctx,
|
|
ref: Ref | string,
|
|
replaceState = false,
|
|
newWindow = false,
|
|
) => {
|
|
await client.navigate(
|
|
coerceAndValidateRef(ref),
|
|
replaceState,
|
|
newWindow,
|
|
);
|
|
},
|
|
description:
|
|
"Navigates to a page reference without restoring its remembered cursor and scroll position.",
|
|
parameters: [
|
|
{
|
|
name: "ref",
|
|
type: "Ref | string",
|
|
description: "The page reference to navigate to.",
|
|
},
|
|
{
|
|
name: "replaceState",
|
|
type: "boolean",
|
|
description: "Whether to replace the current browser history state.",
|
|
optional: true,
|
|
},
|
|
{
|
|
name: "newWindow",
|
|
type: "boolean",
|
|
description: "Whether to open the reference in a new window.",
|
|
optional: true,
|
|
},
|
|
],
|
|
examples: [{ code: 'editor.navigate("CHANGELOG@123")' }],
|
|
},
|
|
"editor.open": {
|
|
callback: async (
|
|
_ctx,
|
|
ref: Ref | string,
|
|
replaceState = false,
|
|
newWindow = false,
|
|
) => {
|
|
await client.open(coerceAndValidateRef(ref), replaceState, newWindow);
|
|
},
|
|
description:
|
|
"Opens a page reference and restores its remembered cursor and scroll position when possible.",
|
|
parameters: [
|
|
{
|
|
name: "ref",
|
|
type: "Ref | string",
|
|
description: "The page reference to open.",
|
|
},
|
|
{
|
|
name: "replaceState",
|
|
type: "boolean",
|
|
description: "Whether to replace the current browser history state.",
|
|
optional: true,
|
|
},
|
|
{
|
|
name: "newWindow",
|
|
type: "boolean",
|
|
description: "Whether to open the reference in a new window.",
|
|
optional: true,
|
|
},
|
|
],
|
|
examples: [{ code: 'editor.open("CHANGELOG")' }],
|
|
},
|
|
"editor.reloadPage": {
|
|
callback: async () => {
|
|
await client.reloadEditor();
|
|
},
|
|
description: "Force reloads the current page in the editor.",
|
|
},
|
|
"editor.forceLint": {
|
|
callback: () => {
|
|
// `forceLinting` alone is a no-op on unchanged content after the
|
|
// linter's internal "scheduled" flag has been cleared; the effect
|
|
// re-arms it via `needsRefresh`.
|
|
client.editorView.dispatch({
|
|
effects: refreshLintEffect.of(null),
|
|
});
|
|
forceLinting(client.editorView);
|
|
},
|
|
description:
|
|
"Forces editor linting to run, including when the content has not changed.",
|
|
},
|
|
"editor.reloadUI": {
|
|
callback: () => {
|
|
location.reload();
|
|
},
|
|
description: "Force reloads the browser UI.",
|
|
},
|
|
"editor.rebuildEditorState": {
|
|
callback: () => {
|
|
client.rebuildEditorState();
|
|
},
|
|
description:
|
|
"Rebuilds the CodeMirror editor state from the current client configuration.",
|
|
},
|
|
"editor.reloadConfigAndCommands": {
|
|
callback: async () => {
|
|
await client.clientSystem.system.localSyscall("system.loadScripts", []);
|
|
await client.clientSystem.system.localSyscall(
|
|
"system.loadSpaceStyles",
|
|
[],
|
|
);
|
|
client.rebuildEditorState();
|
|
},
|
|
description:
|
|
"Reloads space scripts and styles, then rebuilds the editor state.",
|
|
},
|
|
"editor.invokeCommand": {
|
|
callback: (_ctx, name: string, args?: string[]) => {
|
|
return client.runCommandByName(name, args);
|
|
},
|
|
description: "Invokes a client command by name.",
|
|
parameters: [
|
|
{ name: "name", type: "string", description: "The command name." },
|
|
{
|
|
name: "args",
|
|
type: "string[]",
|
|
description: "Arguments passed to the command.",
|
|
optional: true,
|
|
},
|
|
],
|
|
},
|
|
// Re-evaluate every baked section on the current page and rewrite each body
|
|
// with its latest output (same as the "Baked Sections: Update" command),
|
|
// exposed for programmatic use.
|
|
"editor.updateBakedSections": {
|
|
callback: (): Promise<void> => {
|
|
return updateBakedSections(client);
|
|
},
|
|
description:
|
|
"Re-evaluates every baked section on the current page and replaces each body with its latest output.",
|
|
},
|
|
"editor.openUrl": {
|
|
callback: (_ctx, url: string, existingWindow = false) => {
|
|
client.openUrl(url, existingWindow);
|
|
},
|
|
description: "Opens a URL in the browser.",
|
|
parameters: [
|
|
{ name: "url", type: "string", description: "The URL to open." },
|
|
{
|
|
name: "existingWindow",
|
|
type: "boolean",
|
|
description: "Whether to reuse an existing window.",
|
|
optional: true,
|
|
},
|
|
],
|
|
},
|
|
"editor.newWindow": {
|
|
callback: () => {
|
|
globalThis.open(
|
|
location.href,
|
|
`rnd${Math.random()}`,
|
|
`width=${globalThis.innerWidth},heigh=${globalThis.innerHeight}`,
|
|
);
|
|
},
|
|
description:
|
|
"Opens the current SilverBullet URL in a new browser window.",
|
|
},
|
|
"editor.goHistory": {
|
|
callback: (_ctx, delta: number) => {
|
|
globalThis.history.go(delta);
|
|
},
|
|
description: "Moves backward or forward through browser history.",
|
|
parameters: [
|
|
{
|
|
name: "delta",
|
|
type: "number",
|
|
description:
|
|
"The relative history offset; negative moves backward and positive moves forward.",
|
|
},
|
|
],
|
|
},
|
|
"editor.downloadFile": {
|
|
callback: (_ctx, filename: string, dataUrl: string) => {
|
|
const link = document.createElement("a");
|
|
link.href = dataUrl;
|
|
link.download = filename;
|
|
link.click();
|
|
},
|
|
description:
|
|
"Triggers a browser download of a data URL under the given filename.",
|
|
parameters: [
|
|
{
|
|
name: "filename",
|
|
type: "string",
|
|
description: "The downloaded filename.",
|
|
},
|
|
{
|
|
name: "dataUrl",
|
|
type: "string",
|
|
description: "The data URL to download.",
|
|
},
|
|
],
|
|
examples: [
|
|
{
|
|
code: 'editor.downloadFile("test.txt", "data:text/plain;base64,SGVsbG8=")',
|
|
},
|
|
],
|
|
},
|
|
"editor.uploadFile": {
|
|
callback: (
|
|
_ctx,
|
|
accept?: string,
|
|
capture?: string,
|
|
): Promise<UploadFile> => {
|
|
return new Promise<UploadFile>((resolve, reject) => {
|
|
const input = document.createElement("input");
|
|
input.type = "file";
|
|
if (accept) {
|
|
input.accept = accept;
|
|
}
|
|
if (capture) {
|
|
input.capture = capture;
|
|
}
|
|
|
|
input.onchange = () => {
|
|
const file = input.files?.item(0);
|
|
if (!file) {
|
|
reject(new Error("No file found"));
|
|
} else {
|
|
const reader = new FileReader();
|
|
reader.readAsArrayBuffer(file);
|
|
reader.onloadend = async (evt) => {
|
|
if (evt.target?.readyState === FileReader.DONE) {
|
|
resolve({
|
|
name: file.name,
|
|
contentType: file.type,
|
|
content: new Uint8Array(await file.arrayBuffer()),
|
|
});
|
|
}
|
|
};
|
|
reader.onabort = (e) => {
|
|
reject(e);
|
|
};
|
|
reader.onerror = (e) => {
|
|
reject(e);
|
|
};
|
|
}
|
|
};
|
|
input.onabort = (e) => {
|
|
reject(e);
|
|
};
|
|
|
|
input.style.display = "none";
|
|
document.body.appendChild(input);
|
|
input.click();
|
|
setTimeout(() => document.body.removeChild(input), 1000);
|
|
});
|
|
},
|
|
description:
|
|
"Opens the browser's native file picker and returns the selected file's bytes and metadata.",
|
|
parameters: [
|
|
{
|
|
name: "accept",
|
|
type: "string",
|
|
description: "Accepted file types for the file input.",
|
|
optional: true,
|
|
},
|
|
{
|
|
name: "capture",
|
|
type: "string",
|
|
description: "The media capture mode for the file input.",
|
|
optional: true,
|
|
},
|
|
],
|
|
returns: [
|
|
{
|
|
type: "UploadFile",
|
|
description: "The selected file's name, content type, and bytes.",
|
|
},
|
|
],
|
|
examples: [
|
|
{
|
|
code: 'local file = editor.uploadFile(".txt")\nprint(file.name)',
|
|
},
|
|
],
|
|
},
|
|
"editor.flashNotification": {
|
|
callback: (
|
|
_ctx,
|
|
message: string,
|
|
type: NotificationType = "info",
|
|
options?: { timeout?: number; actions?: NotificationAction[] },
|
|
) => {
|
|
client.ui.flashNotification(message, type, options);
|
|
},
|
|
description: "Shows a flash notification in the editor UI.",
|
|
parameters: [
|
|
{
|
|
name: "message",
|
|
type: "string",
|
|
description: "The message to display.",
|
|
},
|
|
{
|
|
name: "type",
|
|
type: "NotificationType",
|
|
description:
|
|
'The notification severity: "info", "error", or "warning".',
|
|
optional: true,
|
|
},
|
|
{
|
|
name: "options",
|
|
type: "{ timeout?: number; actions?: NotificationAction[] }",
|
|
description:
|
|
"Optional timeout and action buttons. A timeout of 0 keeps the notification visible until dismissed.",
|
|
optional: true,
|
|
},
|
|
],
|
|
examples: [
|
|
{
|
|
code: 'editor.flashNotification("Update available", "warning", {\n timeout = 0,\n actions = {{\n name = "Reload",\n run = function() editor.reloadUI() end\n }}\n})',
|
|
},
|
|
],
|
|
},
|
|
"editor.filterBox": {
|
|
callback: (
|
|
_ctx,
|
|
label: string,
|
|
options: FilterOption[],
|
|
helpText = "",
|
|
placeHolder = "",
|
|
): Promise<FilterOption | undefined> => {
|
|
return client.ui.filterBox(label, options, helpText, placeHolder);
|
|
},
|
|
description:
|
|
"Shows a filterable option picker similar to the page navigator.",
|
|
parameters: [
|
|
{
|
|
name: "label",
|
|
type: "string",
|
|
description: "The label shown beside the filter input.",
|
|
},
|
|
{
|
|
name: "options",
|
|
type: "FilterOption[]",
|
|
description: "The available options.",
|
|
},
|
|
{
|
|
name: "helpText",
|
|
type: "string",
|
|
description: "Help text shown below the picker.",
|
|
optional: true,
|
|
},
|
|
{
|
|
name: "placeHolder",
|
|
type: "string",
|
|
description: "Placeholder text for the filter input.",
|
|
optional: true,
|
|
},
|
|
],
|
|
returns: [
|
|
{
|
|
type: "FilterOption | undefined",
|
|
description: "The selected option, or undefined if dismissed.",
|
|
},
|
|
],
|
|
examples: [
|
|
{
|
|
code: 'local result = editor.filterBox("Select:", {\n {name = "Option 1", value = "1"},\n {name = "Option 2", value = "2", description = "More details"}\n})',
|
|
},
|
|
],
|
|
},
|
|
"editor.showPanel": {
|
|
callback: (
|
|
_ctx,
|
|
id: string,
|
|
mode: number,
|
|
html: HTMLElement | HTMLElement[] | string,
|
|
script: string,
|
|
) => {
|
|
client.ui.viewDispatch({
|
|
type: "show-panel",
|
|
id: id as any,
|
|
config: { html, script, mode },
|
|
});
|
|
setTimeout(() => {
|
|
// Dummy dispatch to rerender the editor and toggle the panel
|
|
client.editorView.dispatch({});
|
|
});
|
|
},
|
|
description: "Shows an HTML panel in a specified editor UI location.",
|
|
parameters: [
|
|
{
|
|
name: "id",
|
|
type: "string",
|
|
description: "The panel location identifier.",
|
|
},
|
|
{
|
|
name: "mode",
|
|
type: "number",
|
|
description: "The panel display mode or size.",
|
|
},
|
|
{
|
|
name: "html",
|
|
type: "HTMLElement | HTMLElement[] | string",
|
|
description: "The panel content.",
|
|
},
|
|
{
|
|
name: "script",
|
|
type: "string",
|
|
description: "A script associated with the panel content.",
|
|
},
|
|
],
|
|
},
|
|
"editor.focus": {
|
|
callback: () => {
|
|
client.focus();
|
|
},
|
|
description: "Returns focus to the main editor.",
|
|
},
|
|
"editor.hidePanel": {
|
|
callback: (_ctx, id: string) => {
|
|
client.ui.viewDispatch({
|
|
type: "hide-panel",
|
|
id: id as any,
|
|
});
|
|
setTimeout(() => {
|
|
// Dummy dispatch to rerender the editor and toggle the panel
|
|
client.editorView.dispatch({});
|
|
});
|
|
},
|
|
description: "Hides the panel at a specified editor UI location.",
|
|
parameters: [
|
|
{
|
|
name: "id",
|
|
type: "string",
|
|
description: "The panel location identifier.",
|
|
},
|
|
],
|
|
},
|
|
"editor.showProgress": {
|
|
callback: (
|
|
_ctx,
|
|
progressType: "sync" | "index",
|
|
progressPercentage?: number,
|
|
) => {
|
|
client.ui.showProgress(progressType, progressPercentage);
|
|
},
|
|
description:
|
|
"Shows, updates, or hides a sync or indexing progress indicator.",
|
|
parameters: [
|
|
{
|
|
name: "progressType",
|
|
type: "sync | index",
|
|
description: "The operation represented by the indicator.",
|
|
},
|
|
{
|
|
name: "progressPercentage",
|
|
type: "number",
|
|
description:
|
|
"Completion percentage, or undefined to hide the indicator.",
|
|
optional: true,
|
|
},
|
|
],
|
|
},
|
|
"editor.insertAtPos": {
|
|
callback: (
|
|
_ctx,
|
|
text: string,
|
|
pos: number,
|
|
cursorPlaceHolder = false,
|
|
) => {
|
|
let cursorPlaceholderPos = -1;
|
|
if (cursorPlaceHolder) {
|
|
cursorPlaceholderPos = text.indexOf("|^|");
|
|
if (cursorPlaceholderPos !== -1) {
|
|
text =
|
|
text.slice(0, cursorPlaceholderPos) +
|
|
text.slice(cursorPlaceholderPos + 3);
|
|
} else {
|
|
cursorPlaceHolder = false;
|
|
}
|
|
}
|
|
client.editorView.dispatch({
|
|
changes: {
|
|
insert: text,
|
|
from: pos,
|
|
},
|
|
});
|
|
if (cursorPlaceHolder) {
|
|
const cursorPos = pos + cursorPlaceholderPos;
|
|
client.editorView.dispatch({
|
|
selection: {
|
|
anchor: cursorPos,
|
|
},
|
|
effects: [EditorView.scrollIntoView(cursorPos)],
|
|
});
|
|
}
|
|
},
|
|
description:
|
|
"Inserts text at a character offset, optionally placing the cursor at a `|^|` marker.",
|
|
parameters: [
|
|
{ name: "text", type: "string", description: "The text to insert." },
|
|
{
|
|
name: "pos",
|
|
type: "number",
|
|
description: "The character offset at which to insert.",
|
|
},
|
|
{
|
|
name: "cursorPlaceHolder",
|
|
type: "boolean",
|
|
description:
|
|
"Whether to remove `|^|` and move the cursor to its position.",
|
|
optional: true,
|
|
},
|
|
],
|
|
},
|
|
"editor.replaceRange": {
|
|
callback: (
|
|
_ctx,
|
|
from: number,
|
|
to: number,
|
|
text: string,
|
|
cursorPlaceHolder = false,
|
|
) => {
|
|
let cursorPlaceholderPos = -1;
|
|
if (cursorPlaceHolder) {
|
|
cursorPlaceholderPos = text.indexOf("|^|");
|
|
text =
|
|
text.slice(0, cursorPlaceholderPos) +
|
|
text.slice(cursorPlaceholderPos + 3);
|
|
}
|
|
client.editorView.dispatch({
|
|
changes: {
|
|
insert: text,
|
|
from: from,
|
|
to: to,
|
|
},
|
|
});
|
|
if (cursorPlaceHolder) {
|
|
const cursorPos = from + cursorPlaceholderPos;
|
|
client.editorView.dispatch({
|
|
selection: {
|
|
anchor: cursorPos,
|
|
},
|
|
effects: [EditorView.scrollIntoView(cursorPos)],
|
|
});
|
|
}
|
|
},
|
|
description:
|
|
"Replaces a text range, optionally placing the cursor at a `|^|` marker in the replacement.",
|
|
parameters: [
|
|
{
|
|
name: "from",
|
|
type: "number",
|
|
description: "The start offset of the range.",
|
|
},
|
|
{
|
|
name: "to",
|
|
type: "number",
|
|
description: "The end offset of the range.",
|
|
},
|
|
{
|
|
name: "text",
|
|
type: "string",
|
|
description: "The replacement text.",
|
|
},
|
|
{
|
|
name: "cursorPlaceHolder",
|
|
type: "boolean",
|
|
description:
|
|
"Whether to remove `|^|` and move the cursor to its position.",
|
|
optional: true,
|
|
},
|
|
],
|
|
},
|
|
"editor.moveCursor": {
|
|
callback: (_ctx, pos: number, center = false) => {
|
|
// Always scroll into view so callers don't strand the cursor
|
|
// off-screen; `nearest` (default) is a no-op when already visible.
|
|
client.editorView.dispatch({
|
|
selection: { anchor: pos },
|
|
effects: [
|
|
EditorView.scrollIntoView(pos, center ? { y: "center" } : {}),
|
|
],
|
|
});
|
|
client.editorView.focus();
|
|
},
|
|
description:
|
|
"Moves and focuses the cursor at a character offset, scrolling it into view.",
|
|
parameters: [
|
|
{
|
|
name: "pos",
|
|
type: "number",
|
|
description: "The character offset to move to.",
|
|
},
|
|
{
|
|
name: "center",
|
|
type: "boolean",
|
|
description: "Whether to vertically center the cursor.",
|
|
optional: true,
|
|
},
|
|
],
|
|
},
|
|
"editor.moveCursorToLine": {
|
|
callback: (_ctx, line: number, column = 1, center = false) => {
|
|
// CodeMirror already keeps information about lines
|
|
const cmLine = client.editorView.state.doc.line(line);
|
|
// How much to move inside the line, column number starts from 1
|
|
const offset = Math.max(0, Math.min(cmLine.length, column - 1));
|
|
// Just reuse the implementation above
|
|
const moveCursor = syscalls["editor.moveCursor"];
|
|
const moveCursorCallback =
|
|
typeof moveCursor === "function" ? moveCursor : moveCursor.callback;
|
|
moveCursorCallback(_ctx, cmLine.from + offset, center);
|
|
},
|
|
description:
|
|
"Moves the cursor to a one-based line and column, clamping the column to the line length.",
|
|
parameters: [
|
|
{
|
|
name: "line",
|
|
type: "number",
|
|
description: "The one-based line number.",
|
|
},
|
|
{
|
|
name: "column",
|
|
type: "number",
|
|
description: "The one-based column number.",
|
|
optional: true,
|
|
},
|
|
{
|
|
name: "center",
|
|
type: "boolean",
|
|
description: "Whether to vertically center the cursor.",
|
|
optional: true,
|
|
},
|
|
],
|
|
},
|
|
"editor.setSelection": {
|
|
callback: (_ctx, from: number, to: number) => {
|
|
client.editorView.dispatch({
|
|
selection: {
|
|
anchor: from,
|
|
head: to,
|
|
},
|
|
});
|
|
},
|
|
description: "Sets the main editor selection to a character range.",
|
|
parameters: [
|
|
{
|
|
name: "from",
|
|
type: "number",
|
|
description: "The selection anchor offset.",
|
|
},
|
|
{
|
|
name: "to",
|
|
type: "number",
|
|
description: "The selection head offset.",
|
|
},
|
|
],
|
|
},
|
|
|
|
"editor.insertAtCursor": {
|
|
callback: (
|
|
_ctx,
|
|
text: string,
|
|
scrollIntoView = false,
|
|
cursorPlaceHolder = false,
|
|
) => {
|
|
const editorView = client.editorView;
|
|
const from = editorView.state.selection.main.from;
|
|
const cursorPlaceholderPos = text.indexOf("|^|");
|
|
if (cursorPlaceHolder && cursorPlaceholderPos !== -1) {
|
|
text =
|
|
text.slice(0, cursorPlaceholderPos) +
|
|
text.slice(cursorPlaceholderPos + 3);
|
|
} else {
|
|
cursorPlaceHolder = false;
|
|
}
|
|
editorView.dispatch({
|
|
changes: {
|
|
insert: text,
|
|
from: from,
|
|
},
|
|
selection: {
|
|
anchor: cursorPlaceHolder
|
|
? from + cursorPlaceholderPos
|
|
: from + text.length,
|
|
},
|
|
scrollIntoView,
|
|
});
|
|
},
|
|
description:
|
|
"Inserts text at the cursor and moves the cursor after it or to an optional `|^|` marker.",
|
|
parameters: [
|
|
{ name: "text", type: "string", description: "The text to insert." },
|
|
{
|
|
name: "scrollIntoView",
|
|
type: "boolean",
|
|
description: "Whether to scroll the new cursor position into view.",
|
|
optional: true,
|
|
},
|
|
{
|
|
name: "cursorPlaceHolder",
|
|
type: "boolean",
|
|
description:
|
|
"Whether to remove `|^|` and move the cursor to its position.",
|
|
optional: true,
|
|
},
|
|
],
|
|
},
|
|
"editor.dispatch": {
|
|
callback: (_ctx, change: Transaction) => {
|
|
client.editorView.dispatch(change);
|
|
},
|
|
description: "Dispatches a CodeMirror transaction to the editor view.",
|
|
parameters: [
|
|
{
|
|
name: "change",
|
|
type: "Transaction",
|
|
description: "The CodeMirror transaction to dispatch.",
|
|
},
|
|
],
|
|
},
|
|
"editor.prompt": {
|
|
callback: (
|
|
_ctx,
|
|
message: string,
|
|
defaultValue = "",
|
|
): Promise<string | undefined> => {
|
|
return client.ui.prompt(message, defaultValue);
|
|
},
|
|
description: "Prompts the user for text input.",
|
|
parameters: [
|
|
{
|
|
name: "message",
|
|
type: "string",
|
|
description: "The prompt message.",
|
|
},
|
|
{
|
|
name: "defaultValue",
|
|
type: "string",
|
|
description: "The initial input value.",
|
|
optional: true,
|
|
},
|
|
],
|
|
returns: [
|
|
{
|
|
type: "string | undefined",
|
|
description: "The entered text, or undefined if dismissed.",
|
|
},
|
|
],
|
|
},
|
|
"editor.confirm": {
|
|
callback: (
|
|
_ctx,
|
|
message: string,
|
|
options?: { destructive?: boolean },
|
|
): Promise<boolean> => {
|
|
return client.ui.confirm(message, options);
|
|
},
|
|
description: "Prompts the user to confirm or cancel an action.",
|
|
parameters: [
|
|
{
|
|
name: "message",
|
|
type: "string",
|
|
description: "The confirmation message.",
|
|
},
|
|
{
|
|
name: "options",
|
|
type: "{ destructive?: boolean }",
|
|
description: "Optional dialog styling settings.",
|
|
optional: true,
|
|
},
|
|
],
|
|
returns: [
|
|
{ type: "boolean", description: "Whether the user confirmed." },
|
|
],
|
|
},
|
|
"editor.alert": {
|
|
callback: (_ctx, message: string) => {
|
|
alert(message);
|
|
},
|
|
description: "Shows a browser alert dialog.",
|
|
parameters: [
|
|
{
|
|
name: "message",
|
|
type: "string",
|
|
description: "The alert message.",
|
|
},
|
|
],
|
|
},
|
|
"editor.getUiOption": {
|
|
callback: (_ctx, key: string): any => {
|
|
return (client.ui.viewState.uiOptions as any)[key];
|
|
},
|
|
description: "Returns the current value of an editor UI option.",
|
|
parameters: [
|
|
{ name: "key", type: "string", description: "The UI option key." },
|
|
],
|
|
returns: [{ type: "any", description: "The option value." }],
|
|
},
|
|
"editor.setUiOption": {
|
|
callback: (_ctx, key: string, value: any) => {
|
|
client.ui.viewDispatch({
|
|
type: "set-ui-option",
|
|
key,
|
|
value,
|
|
});
|
|
if (key !== "darkMode") {
|
|
void client.reloadEditor();
|
|
}
|
|
},
|
|
description:
|
|
"Sets an editor UI option. Theme changes rebuild the editor state in place; other options reload the editor.",
|
|
parameters: [
|
|
{ name: "key", type: "string", description: "The UI option key." },
|
|
{ name: "value", type: "any", description: "The option value." },
|
|
],
|
|
},
|
|
"editor.vimEx": {
|
|
callback: (_ctx, exCommand: string) => {
|
|
const vimMod = getVimModule();
|
|
if (!vimMod) {
|
|
throw new Error("Vim module not loaded.");
|
|
}
|
|
const cm = vimMod.getCM(client.editorView);
|
|
if (cm?.state.vim) {
|
|
return vimMod.Vim.handleEx(cm as any, exCommand);
|
|
} else {
|
|
throw new Error("Vim mode not active or not initialized.");
|
|
}
|
|
},
|
|
description: "Executes a Vim Ex command in the active Vim-mode editor.",
|
|
parameters: [
|
|
{
|
|
name: "exCommand",
|
|
type: "string",
|
|
description: "The Ex command to execute.",
|
|
},
|
|
],
|
|
},
|
|
"editor.configureVimMode": {
|
|
callback: () => {
|
|
const vimMod = getVimModule();
|
|
if (!vimMod) {
|
|
throw new Error("Vim module not loaded.");
|
|
}
|
|
const { Vim } = vimMod;
|
|
// Override the default "o" binding to be more intelligent and follow the markdown editor's behavior
|
|
Vim.mapCommand("o", "action", "newline-continue-markup", {}, {});
|
|
Vim.mapCommand("O", "action", "back-newline-continue-markup", {}, {});
|
|
Vim.unmap("<C-q>", undefined as any);
|
|
Vim.defineAction("newline-continue-markup", (cm) => {
|
|
// Append at end of line
|
|
Vim.handleKey(cm, "A", "+input");
|
|
// Insert newline continuing markup where appropriate
|
|
insertNewlineContinueMarkup(client.editorView) ||
|
|
insertNewlineAndIndent(client.editorView);
|
|
});
|
|
Vim.defineAction("back-newline-continue-markup", (cm) => {
|
|
// Determine current line
|
|
const pos = client.editorView.state.selection.main.from;
|
|
const line = client.editorView.state.doc.lineAt(pos).number;
|
|
if (line === 1) {
|
|
// We're on the top line
|
|
// Go to 0:0
|
|
Vim.handleKey(cm, "0", "+input");
|
|
// Insert a newline
|
|
insertNewline(client.editorView);
|
|
// Go up to the new line
|
|
Vim.handleKey(cm, "k", "+input");
|
|
// Into insert mode
|
|
Vim.handleKey(cm, "i", "+input");
|
|
} else {
|
|
// We're elsewhere in the document
|
|
// Go up
|
|
Vim.handleKey(cm, "k", "+input");
|
|
// Append mode at the end of the line
|
|
Vim.handleKey(cm, "A", "+input");
|
|
// Insert a newline using the continue markup thing
|
|
insertNewlineContinueMarkup(client.editorView) ||
|
|
insertNewlineAndIndent(client.editorView);
|
|
}
|
|
});
|
|
|
|
// Load the config if any
|
|
const config = client.config.get<VimConfig>("vim", {});
|
|
if (config) {
|
|
config.unmap?.forEach((binding) => {
|
|
if (typeof binding === "string") {
|
|
console.log(`Unmapping ${binding}`);
|
|
// @ts-expect-error: unmap expects a string for the mode, this is problematic with Ex mappings which requires undefined or false
|
|
Vim.unmap(binding, undefined);
|
|
} else if (binding.key) {
|
|
console.log(
|
|
`Unmapping ${binding.key} in ${binding.mode ?? "normal"}`,
|
|
);
|
|
Vim.unmap(binding.key, binding.mode ?? "normal");
|
|
}
|
|
});
|
|
config.map?.forEach(({ map, to, mode }) => {
|
|
console.log(`Mapping ${map} to ${to} for ${mode ?? "normal"}`);
|
|
Vim.map(map, to, mode ?? "normal");
|
|
});
|
|
config.noremap?.forEach(({ map, to, mode }) => {
|
|
console.log(`Noremapping ${map} to ${to} for ${mode ?? "normal"}`);
|
|
Vim.noremap(map, to, mode ?? "normal");
|
|
});
|
|
config.commands?.forEach(({ ex, command }) => {
|
|
console.log(`Mapping command '${command}' to Ex ${ex}`);
|
|
Vim.defineEx(ex, "", (_, params) =>
|
|
client.runCommandByName(command, params.args),
|
|
);
|
|
});
|
|
} else {
|
|
console.log("No vim config found");
|
|
}
|
|
},
|
|
description:
|
|
"Configures CodeMirror Vim mode from the current SilverBullet Vim settings.",
|
|
},
|
|
"editor.openPageNavigator": {
|
|
callback: (_ctx, mode: "page" | "meta" | "document" | "all" = "page") => {
|
|
client.startPageNavigate(mode);
|
|
},
|
|
description: "Opens the page navigator in the requested browsing mode.",
|
|
parameters: [
|
|
{
|
|
name: "mode",
|
|
type: "page | meta | document | all",
|
|
description: "The navigator mode.",
|
|
optional: true,
|
|
},
|
|
],
|
|
},
|
|
"editor.openCommandPalette": {
|
|
callback: () => {
|
|
void client.startCommandPalette();
|
|
},
|
|
description: "Opens the command palette.",
|
|
},
|
|
"editor.deleteLine": {
|
|
callback: () => {
|
|
deleteLine(client.editorView);
|
|
},
|
|
description:
|
|
"Deletes the current line or the lines touched by the selection.",
|
|
},
|
|
"editor.selectAll": {
|
|
callback: () => {
|
|
return selectAll({
|
|
state: client.editorView.state,
|
|
dispatch: client.editorView.dispatch,
|
|
});
|
|
},
|
|
description: "Selects the entire editor document.",
|
|
},
|
|
"editor.indentMore": {
|
|
callback: () => {
|
|
return indentMore({
|
|
state: client.editorView.state,
|
|
dispatch: client.editorView.dispatch,
|
|
});
|
|
},
|
|
description: "Increases indentation for the current line or selection.",
|
|
},
|
|
"editor.indentLess": {
|
|
callback: () => {
|
|
return indentLess({
|
|
state: client.editorView.state,
|
|
dispatch: client.editorView.dispatch,
|
|
});
|
|
},
|
|
description: "Decreases indentation for the current line or selection.",
|
|
},
|
|
"editor.toggleComment": {
|
|
callback: () => {
|
|
return toggleComment({
|
|
state: client.editorView.state,
|
|
dispatch: client.editorView.dispatch,
|
|
});
|
|
},
|
|
description: "Comments or uncomments the current line or selection.",
|
|
},
|
|
"editor.moveLineUp": {
|
|
callback: () => {
|
|
return moveLineUp({
|
|
state: client.editorView.state,
|
|
dispatch: client.editorView.dispatch,
|
|
});
|
|
},
|
|
description: "Moves the current line or selected lines upward.",
|
|
},
|
|
"editor.moveLineDown": {
|
|
callback: () => {
|
|
return moveLineDown({
|
|
state: client.editorView.state,
|
|
dispatch: client.editorView.dispatch,
|
|
});
|
|
},
|
|
description: "Moves the current line or selected lines downward.",
|
|
},
|
|
// Folding
|
|
"editor.fold": {
|
|
callback: () => {
|
|
foldCode(client.editorView);
|
|
},
|
|
description: "Folds the code or markup region at the cursor.",
|
|
},
|
|
"editor.unfold": {
|
|
callback: () => {
|
|
unfoldCode(client.editorView);
|
|
},
|
|
description: "Unfolds the folded region at the cursor.",
|
|
},
|
|
"editor.toggleFold": {
|
|
callback: () => {
|
|
toggleFold(client.editorView);
|
|
},
|
|
description: "Toggles folding for the region at the cursor.",
|
|
},
|
|
"editor.foldAll": {
|
|
callback: () => {
|
|
foldAll(client.editorView);
|
|
},
|
|
description: "Folds all foldable regions in the editor.",
|
|
},
|
|
"editor.unfoldAll": {
|
|
callback: () => {
|
|
unfoldAll(client.editorView);
|
|
},
|
|
description: "Unfolds all folded regions in the editor.",
|
|
},
|
|
"editor.undo": {
|
|
callback: () => {
|
|
return undo(client.editorView);
|
|
},
|
|
description: "Undoes the most recent editor change.",
|
|
},
|
|
"editor.redo": {
|
|
callback: () => {
|
|
return redo(client.editorView);
|
|
},
|
|
description: "Redoes the most recently undone editor change.",
|
|
},
|
|
// Cursor motion syscalls
|
|
"editor.cursorCharLeft": {
|
|
callback: () => cursorCharLeft(client.editorView),
|
|
description:
|
|
"Moves the cursor one character left, respecting bidirectional text.",
|
|
},
|
|
"editor.cursorCharRight": {
|
|
callback: () => cursorCharRight(client.editorView),
|
|
description:
|
|
"Moves the cursor one character right, respecting bidirectional text.",
|
|
},
|
|
"editor.cursorGroupLeft": {
|
|
callback: () => cursorGroupLeft(client.editorView),
|
|
description: "Moves the cursor left by one character group or word.",
|
|
},
|
|
"editor.cursorGroupRight": {
|
|
callback: () => cursorGroupRight(client.editorView),
|
|
description: "Moves the cursor right by one character group or word.",
|
|
},
|
|
"editor.cursorLineBoundaryLeft": {
|
|
callback: () => cursorLineBoundaryLeft(client.editorView),
|
|
description:
|
|
"Moves the cursor to the left visual boundary of the current line.",
|
|
},
|
|
"editor.cursorLineBoundaryRight": {
|
|
callback: () => cursorLineBoundaryRight(client.editorView),
|
|
description:
|
|
"Moves the cursor to the right visual boundary of the current line.",
|
|
},
|
|
"editor.cursorLineStart": {
|
|
callback: () => cursorLineStart(client.editorView),
|
|
description: "Moves the cursor to the start of the current logical line.",
|
|
},
|
|
"editor.cursorLineEnd": {
|
|
callback: () => cursorLineEnd(client.editorView),
|
|
description: "Moves the cursor to the end of the current logical line.",
|
|
},
|
|
"editor.cursorDocStart": {
|
|
callback: () => cursorDocStart(client.editorView),
|
|
description: "Moves the cursor to the start of the document.",
|
|
},
|
|
"editor.cursorDocEnd": {
|
|
callback: () => cursorDocEnd(client.editorView),
|
|
description: "Moves the cursor to the end of the document.",
|
|
},
|
|
// Cursor motions that also navigate the completion popup if it's open
|
|
"editor.cursorLineUp": {
|
|
callback: () => {
|
|
const view = client.editorView;
|
|
if (moveCompletionSelection(false)(view)) return true;
|
|
return cursorLineUp(view);
|
|
},
|
|
description:
|
|
"Moves completion selection up when open, otherwise moves the cursor up one visual line.",
|
|
},
|
|
"editor.cursorLineDown": {
|
|
callback: () => {
|
|
const view = client.editorView;
|
|
if (moveCompletionSelection(true)(view)) return true;
|
|
return cursorLineDown(view);
|
|
},
|
|
description:
|
|
"Moves completion selection down when open, otherwise moves the cursor down one visual line.",
|
|
},
|
|
"editor.cursorPageUp": {
|
|
callback: () => {
|
|
const view = client.editorView;
|
|
if (moveCompletionSelection(false, "page")(view)) return true;
|
|
return cursorPageUp(view);
|
|
},
|
|
description:
|
|
"Moves completion selection up one page when open, otherwise moves the cursor up one viewport page.",
|
|
},
|
|
"editor.cursorPageDown": {
|
|
callback: () => {
|
|
const view = client.editorView;
|
|
if (moveCompletionSelection(true, "page")(view)) return true;
|
|
return cursorPageDown(view);
|
|
},
|
|
description:
|
|
"Moves completion selection down one page when open, otherwise moves the cursor down one viewport page.",
|
|
},
|
|
// Selection-extending motions
|
|
"editor.selectCharLeft": {
|
|
callback: () => selectCharLeft(client.editorView),
|
|
description:
|
|
"Extends the selection one character left, respecting bidirectional text.",
|
|
},
|
|
"editor.selectCharRight": {
|
|
callback: () => selectCharRight(client.editorView),
|
|
description:
|
|
"Extends the selection one character right, respecting bidirectional text.",
|
|
},
|
|
"editor.selectGroupLeft": {
|
|
callback: () => selectGroupLeft(client.editorView),
|
|
description: "Extends the selection left by one character group or word.",
|
|
},
|
|
"editor.selectGroupRight": {
|
|
callback: () => selectGroupRight(client.editorView),
|
|
description:
|
|
"Extends the selection right by one character group or word.",
|
|
},
|
|
"editor.selectLineBoundaryLeft": {
|
|
callback: () => selectLineBoundaryLeft(client.editorView),
|
|
description:
|
|
"Extends the selection to the left visual boundary of the current line.",
|
|
},
|
|
"editor.selectLineBoundaryRight": {
|
|
callback: () => selectLineBoundaryRight(client.editorView),
|
|
description:
|
|
"Extends the selection to the right visual boundary of the current line.",
|
|
},
|
|
"editor.selectLineStart": {
|
|
callback: () => selectLineStart(client.editorView),
|
|
description:
|
|
"Extends the selection to the start of the current logical line.",
|
|
},
|
|
"editor.selectLineEnd": {
|
|
callback: () => selectLineEnd(client.editorView),
|
|
description:
|
|
"Extends the selection to the end of the current logical line.",
|
|
},
|
|
"editor.selectDocStart": {
|
|
callback: () => selectDocStart(client.editorView),
|
|
description: "Extends the selection to the start of the document.",
|
|
},
|
|
"editor.selectDocEnd": {
|
|
callback: () => selectDocEnd(client.editorView),
|
|
description: "Extends the selection to the end of the document.",
|
|
},
|
|
"editor.selectLineUp": {
|
|
callback: () => selectLineUp(client.editorView),
|
|
description: "Extends the selection upward by one visual line.",
|
|
},
|
|
"editor.selectLineDown": {
|
|
callback: () => selectLineDown(client.editorView),
|
|
description: "Extends the selection downward by one visual line.",
|
|
},
|
|
"editor.selectPageUp": {
|
|
callback: () => selectPageUp(client.editorView),
|
|
description: "Extends the selection upward by one viewport page.",
|
|
},
|
|
"editor.selectPageDown": {
|
|
callback: () => selectPageDown(client.editorView),
|
|
description: "Extends the selection downward by one viewport page.",
|
|
},
|
|
// Delete
|
|
"editor.deleteCharBackward": {
|
|
callback: () => deleteCharBackward(client.editorView),
|
|
description: "Deletes the selection or the character before the cursor.",
|
|
},
|
|
"editor.deleteCharForward": {
|
|
callback: () => deleteCharForward(client.editorView),
|
|
description: "Deletes the selection or the character after the cursor.",
|
|
},
|
|
"editor.deleteGroupBackward": {
|
|
callback: () => deleteGroupBackward(client.editorView),
|
|
description:
|
|
"Deletes the selection or the character group before the cursor.",
|
|
},
|
|
"editor.deleteGroupForward": {
|
|
callback: () => deleteGroupForward(client.editorView),
|
|
description:
|
|
"Deletes the selection or the character group after the cursor.",
|
|
},
|
|
"editor.deleteLineBoundaryBackward": {
|
|
callback: () => deleteLineBoundaryBackward(client.editorView),
|
|
description:
|
|
"Deletes the selection or text back to the current line boundary.",
|
|
},
|
|
"editor.deleteLineBoundaryForward": {
|
|
callback: () => deleteLineBoundaryForward(client.editorView),
|
|
description:
|
|
"Deletes the selection or text forward to the current line boundary.",
|
|
},
|
|
"editor.transposeChars": {
|
|
callback: () => transposeChars(client.editorView),
|
|
description: "Transposes the characters around the cursor.",
|
|
},
|
|
// Enter: accept completion if popup is open, else newline-and-indent
|
|
"editor.insertNewline": {
|
|
callback: () => {
|
|
const view = client.editorView;
|
|
if (acceptCompletion(view)) return true;
|
|
return insertNewlineAndIndent(view);
|
|
},
|
|
description:
|
|
"Accepts the active completion, or inserts a newline with appropriate indentation.",
|
|
},
|
|
// Completion popup control
|
|
"editor.acceptCompletion": {
|
|
callback: () => acceptCompletion(client.editorView),
|
|
description:
|
|
"Accepts the currently selected completion when the completion popup is active.",
|
|
returns: [
|
|
{
|
|
type: "boolean",
|
|
description: "Whether an active completion was accepted.",
|
|
},
|
|
],
|
|
},
|
|
"editor.startCompletion": {
|
|
callback: () => startCompletion(client.editorView),
|
|
description: "Explicitly starts editor completion at the cursor.",
|
|
},
|
|
"editor.closeCompletion": {
|
|
callback: () => closeCompletion(client.editorView),
|
|
description: "Closes the active editor completion popup.",
|
|
},
|
|
"editor.openSearchPanel": {
|
|
callback: () => {
|
|
openSearchPanel(client.editorView);
|
|
},
|
|
description: "Opens the editor's native search panel.",
|
|
},
|
|
"editor.copyToClipboard": {
|
|
callback: async (_ctx, data: string | Blob) => {
|
|
try {
|
|
if (typeof data === "string") {
|
|
await navigator.clipboard.writeText(data);
|
|
} else {
|
|
await navigator.clipboard.write([
|
|
new ClipboardItem({ [data.type]: data }),
|
|
]);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
client.ui.flashNotification(`Could not copy to clipboard: ${e}`);
|
|
}
|
|
},
|
|
description:
|
|
"Copies text or binary Blob data to the system clipboard. Clipboard access requires a secure HTTPS context.",
|
|
parameters: [
|
|
{
|
|
name: "data",
|
|
type: "string | Blob",
|
|
description: "The text or Blob to copy.",
|
|
},
|
|
],
|
|
examples: [{ code: 'editor.copyToClipboard("Copied text")' }],
|
|
},
|
|
"editor.sendMessage": {
|
|
callback: (_ctx, type: string, data: any) => {
|
|
if (!client.contentManager.isDocumentEditor()) return;
|
|
|
|
client.contentManager.documentEditor.sendPublicMessage({
|
|
type,
|
|
data,
|
|
});
|
|
},
|
|
description:
|
|
"Sends a public message to the active document editor, if one is open.",
|
|
parameters: [
|
|
{ name: "type", type: "string", description: "The message type." },
|
|
{
|
|
name: "data",
|
|
type: "any",
|
|
description: "Data attached to the message.",
|
|
optional: true,
|
|
},
|
|
],
|
|
},
|
|
|
|
"editor.isMobile": {
|
|
callback: () => {
|
|
const mouseDetected = globalThis.matchMedia("(pointer:fine)").matches;
|
|
return !mouseDetected;
|
|
},
|
|
description:
|
|
"Checks whether the current device lacks a fine pointer and should be treated as mobile.",
|
|
returns: [
|
|
{
|
|
type: "boolean",
|
|
description:
|
|
"Whether the editor is running in a mobile-style pointer environment.",
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
return syscalls;
|
|
}
|