2022-10-10 12:50:21 +00:00
|
|
|
import { Hook, Manifest } from "../../plugos/types.ts";
|
|
|
|
import { System } from "../../plugos/system.ts";
|
2022-10-12 09:47:13 +00:00
|
|
|
import { Completion, CompletionContext, CompletionResult } from "../deps.ts";
|
2022-10-10 12:50:21 +00:00
|
|
|
import { safeRun } from "../../common/util.ts";
|
2023-07-14 14:56:20 +00:00
|
|
|
import { Client } from "../client.ts";
|
2022-10-10 12:50:21 +00:00
|
|
|
import { syntaxTree } from "../deps.ts";
|
2023-11-06 08:14:16 +00:00
|
|
|
import { SlashCompletion } from "$sb/app_event.ts";
|
2022-03-29 09:21:32 +00:00
|
|
|
|
|
|
|
export type SlashCommandDef = {
|
|
|
|
name: string;
|
2022-07-04 13:07:27 +00:00
|
|
|
description?: string;
|
2022-11-18 15:04:37 +00:00
|
|
|
boost?: number;
|
2022-03-29 09:21:32 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
export type AppSlashCommand = {
|
|
|
|
slashCommand: SlashCommandDef;
|
|
|
|
run: () => Promise<void>;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type SlashCommandHookT = {
|
|
|
|
slashCommand?: SlashCommandDef;
|
|
|
|
};
|
|
|
|
|
2022-12-19 12:14:26 +00:00
|
|
|
const slashCommandRegexp = /([^\w:]|^)\/[\w\-]*/;
|
2022-08-02 11:22:10 +00:00
|
|
|
|
2022-03-29 09:21:32 +00:00
|
|
|
export class SlashCommandHook implements Hook<SlashCommandHookT> {
|
|
|
|
slashCommands = new Map<string, AppSlashCommand>();
|
2023-07-14 14:56:20 +00:00
|
|
|
private editor: Client;
|
2022-03-29 09:21:32 +00:00
|
|
|
|
2023-07-14 14:56:20 +00:00
|
|
|
constructor(editor: Client) {
|
2022-03-29 09:21:32 +00:00
|
|
|
this.editor = editor;
|
|
|
|
}
|
|
|
|
|
|
|
|
buildAllCommands(system: System<SlashCommandHookT>) {
|
|
|
|
this.slashCommands.clear();
|
2022-10-15 17:02:56 +00:00
|
|
|
for (const plug of system.loadedPlugs.values()) {
|
2022-10-10 12:50:21 +00:00
|
|
|
for (
|
|
|
|
const [name, functionDef] of Object.entries(
|
|
|
|
plug.manifest!.functions,
|
|
|
|
)
|
|
|
|
) {
|
2022-03-29 09:21:32 +00:00
|
|
|
if (!functionDef.slashCommand) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const cmd = functionDef.slashCommand;
|
|
|
|
this.slashCommands.set(cmd.name, {
|
|
|
|
slashCommand: cmd,
|
|
|
|
run: () => {
|
2022-07-04 13:07:27 +00:00
|
|
|
return plug.invoke(name, [cmd]);
|
2022-03-29 09:21:32 +00:00
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Completer for CodeMirror
|
2023-11-06 08:14:16 +00:00
|
|
|
public async slashCommandCompleter(
|
2022-10-10 12:50:21 +00:00
|
|
|
ctx: CompletionContext,
|
2023-11-06 08:14:16 +00:00
|
|
|
): Promise<CompletionResult | null> {
|
2022-10-15 17:02:56 +00:00
|
|
|
const prefix = ctx.matchBefore(slashCommandRegexp);
|
2022-03-29 09:21:32 +00:00
|
|
|
if (!prefix) {
|
|
|
|
return null;
|
|
|
|
}
|
2022-08-03 10:24:35 +00:00
|
|
|
const prefixText = prefix.text;
|
2022-10-15 17:02:56 +00:00
|
|
|
const options: Completion[] = [];
|
2022-07-04 13:07:27 +00:00
|
|
|
|
|
|
|
// No slash commands in comment blocks (queries and such)
|
2022-10-15 17:02:56 +00:00
|
|
|
const currentNode = syntaxTree(ctx.state).resolveInner(ctx.pos);
|
2022-07-04 13:07:27 +00:00
|
|
|
if (currentNode.type.name === "CommentBlock") {
|
|
|
|
return null;
|
|
|
|
}
|
2023-11-06 08:14:16 +00:00
|
|
|
|
2022-10-15 17:02:56 +00:00
|
|
|
for (const def of this.slashCommands.values()) {
|
2022-03-29 09:21:32 +00:00
|
|
|
options.push({
|
|
|
|
label: def.slashCommand.name,
|
2022-07-04 13:07:27 +00:00
|
|
|
detail: def.slashCommand.description,
|
2022-11-18 15:04:37 +00:00
|
|
|
boost: def.slashCommand.boost,
|
2022-03-29 09:21:32 +00:00
|
|
|
apply: () => {
|
|
|
|
// Delete slash command part
|
2023-07-27 09:41:44 +00:00
|
|
|
this.editor.editorView.dispatch({
|
2022-03-29 09:21:32 +00:00
|
|
|
changes: {
|
2022-08-03 10:24:35 +00:00
|
|
|
from: prefix!.from + prefixText.indexOf("/"),
|
2022-03-29 09:21:32 +00:00
|
|
|
to: ctx.pos,
|
|
|
|
insert: "",
|
|
|
|
},
|
|
|
|
});
|
|
|
|
// Replace with whatever the completion is
|
|
|
|
safeRun(async () => {
|
|
|
|
await def.run();
|
2022-07-04 13:07:27 +00:00
|
|
|
this.editor.focus();
|
2022-03-29 09:21:32 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
2023-11-06 08:14:16 +00:00
|
|
|
|
|
|
|
const slashCompletions: SlashCompletion[] | null = await this.editor
|
|
|
|
.completeWithEvent(
|
|
|
|
ctx,
|
|
|
|
"slash:complete",
|
|
|
|
) as any;
|
|
|
|
|
|
|
|
if (slashCompletions) {
|
|
|
|
for (const slashCompletion of slashCompletions) {
|
|
|
|
options.push({
|
|
|
|
label: slashCompletion.label,
|
|
|
|
detail: slashCompletion.detail,
|
|
|
|
apply: () => {
|
|
|
|
// Delete slash command part
|
|
|
|
this.editor.editorView.dispatch({
|
|
|
|
changes: {
|
|
|
|
from: prefix!.from + prefixText.indexOf("/"),
|
|
|
|
to: ctx.pos,
|
|
|
|
insert: "",
|
|
|
|
},
|
|
|
|
});
|
|
|
|
// Replace with whatever the completion is
|
|
|
|
safeRun(async () => {
|
2024-01-20 18:16:07 +00:00
|
|
|
await this.editor.system.system.invokeFunction(
|
|
|
|
slashCompletion.invoke,
|
|
|
|
[slashCompletion],
|
2023-11-06 08:14:16 +00:00
|
|
|
);
|
|
|
|
this.editor.focus();
|
|
|
|
});
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-29 09:21:32 +00:00
|
|
|
return {
|
|
|
|
// + 1 because of the '/'
|
2022-08-03 10:24:35 +00:00
|
|
|
from: prefix.from + prefixText.indexOf("/") + 1,
|
2022-03-29 09:21:32 +00:00
|
|
|
options: options,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
apply(system: System<SlashCommandHookT>): void {
|
|
|
|
this.buildAllCommands(system);
|
|
|
|
system.on({
|
|
|
|
plugLoaded: () => {
|
|
|
|
this.buildAllCommands(system);
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
validateManifest(manifest: Manifest<SlashCommandHookT>): string[] {
|
2022-10-15 17:02:56 +00:00
|
|
|
const errors = [];
|
2022-03-29 09:21:32 +00:00
|
|
|
for (const [name, functionDef] of Object.entries(manifest.functions)) {
|
|
|
|
if (!functionDef.slashCommand) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const cmd = functionDef.slashCommand;
|
|
|
|
if (!cmd.name) {
|
|
|
|
errors.push(`Function ${name} has a command but no name`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
}
|