Skip to content

Commit

Permalink
add the ability to define mappings
Browse files Browse the repository at this point in the history
dlants committed Dec 16, 2024
1 parent 9af1882 commit 3dcaf39
Showing 8 changed files with 148 additions and 15 deletions.
24 changes: 14 additions & 10 deletions rplugin/node/magenta/src/chat/chat.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,8 @@ import {
} from "./message.ts";
import { ToolModel } from "../tools/toolManager.ts";
import { Dispatch, Update } from "../tea/tea.ts";
import { d, View } from "../tea/view.ts";
import { d, View, withBindings } from "../tea/view.ts";
import { context } from "../context.ts";

export type Role = "user" | "assistant";

@@ -173,15 +174,18 @@ export const view: View<{ model: Model; dispatch: Dispatch<Msg> }> = ({
model,
dispatch,
}) => {
return d`# Chat\n${model.messages.map(
(m, idx) =>
d`${messageView({
model: m,
dispatch: (msg) => {
dispatch({ type: "message-msg", msg, idx });
},
})}\n`,
)}`;
return withBindings(
d`# Chat\n${model.messages.map(
(m, idx) =>
d`${messageView({
model: m,
dispatch: (msg) => {
dispatch({ type: "message-msg", msg, idx });
},
})}\n`,
)}`,
{ Enter: () => context.logger.debug("hello, binding") },
);
};

export function getMessages(model: Model): Anthropic.MessageParam[] {
3 changes: 2 additions & 1 deletion rplugin/node/magenta/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Neovim } from "neovim";
import { Neovim, NvimPlugin } from "neovim";
import { Logger } from "./logger.ts";

export type Context = {
plugin: NvimPlugin;
nvim: Neovim;
logger: Logger;
};
17 changes: 16 additions & 1 deletion rplugin/node/magenta/src/magenta.ts
Original file line number Diff line number Diff line change
@@ -7,11 +7,13 @@ import { App, createApp } from "./tea/tea.ts";
import * as ToolManager from "./tools/toolManager.ts";
import { d } from "./tea/view.ts";
import { setContext, context } from "./context.ts";
import { BindingKey } from "./tea/mappings.ts";

class Magenta {
private anthropicClient: AnthropicClient;
private sidebar: Sidebar;
private chat: App<Chat.Msg, Chat.Model>;
private chatRoot: { onKey(key: BindingKey): void } | undefined;
private toolManager: App<ToolManager.Msg, ToolManager.Model>;

constructor() {
@@ -75,12 +77,14 @@ class Magenta {
case "toggle": {
const buffers = await this.sidebar.toggle();
if (buffers) {
await this.chat.mount({
this.chatRoot = await this.chat.mount({
buffer: buffers.displayBuffer,
startPos: { row: 0, col: 0 },
endPos: { row: 0, col: 0 },
});
context.logger.trace(`Chat rendered.`);
} else {
// TODO: maybe set this.chatRoot to undefined?
}

break;
@@ -146,6 +150,12 @@ class Magenta {
}
}
}

onKey(key: BindingKey) {
if (this.chatRoot) {
this.chatRoot.onKey(key);
}
}
}

let init: { magenta: Magenta; logger: Logger } | undefined = undefined;
@@ -156,6 +166,7 @@ module.exports = (plugin: NvimPlugin) => {
if (!init) {
const logger = new Logger(plugin.nvim, { level: "trace" });
setContext({
plugin,
nvim: plugin.nvim,
logger,
});
@@ -187,4 +198,8 @@ module.exports = (plugin: NvimPlugin) => {
nargs: "1",
},
);

context.plugin.registerFunction("MagentaOnEnter", () => {
init?.magenta.onKey("Enter");
});
};
49 changes: 49 additions & 0 deletions rplugin/node/magenta/src/tea/mappings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { MountedVDOM, MountPoint, Position } from "./view.ts";
import { context } from "../context.ts";
import { assertUnreachable } from "../utils/assertUnreachable.ts";

export type BindingKey = "Enter";
export type Bindings = Partial<{
[key in BindingKey]: () => void;
}>;

export function getBindings(
mountedNode: MountedVDOM,
cursor: Position,
): Bindings | undefined {
if (
compare(cursor, mountedNode.startPos) > 0 ||
compare(cursor, mountedNode.endPos) < 0
) {
return undefined;
}

switch (mountedNode.type) {
case "string":
return mountedNode.bindings;
case "node":
case "array": {
// most specific binding wins
for (const child of mountedNode.children) {
const childBindings = getBindings(child, cursor);
if (childBindings) {
return childBindings;
}
}
return mountedNode.bindings;
}
default:
assertUnreachable(mountedNode);
}
}

/** returns a positive number if pos2 is greater than pos1, 0 if equal, -1 if pos2 is less than pos1
*/
function compare(pos1: Position, pos2: Position): number {
const rowDiff = pos2.row - pos1.row;
if (rowDiff != 0) {
return rowDiff;
}

return pos2.col - pos1.col;
}
10 changes: 10 additions & 0 deletions rplugin/node/magenta/src/tea/render.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Line } from "../chat/part.ts";
import { assertUnreachable } from "../utils/assertUnreachable.ts";
import { Bindings } from "./mappings.ts";
import { calculatePosition, replaceBetweenPositions } from "./util.ts";
import { MountedVDOM, MountPoint, VDOMNode } from "./view.ts";

@@ -16,19 +17,22 @@ export async function render({
content: string;
start: number;
end: number;
bindings?: Bindings;
}
| {
type: "node";
template: TemplateStringsArray;
children: NodePosition[];
start: number;
end: number;
bindings?: Bindings;
}
| {
type: "array";
children: NodePosition[];
start: number;
end: number;
bindings?: Bindings;
};

// First pass: build the complete string and create tree structure with positions
@@ -44,6 +48,7 @@ export async function render({
content: node.content,
start,
end: content.length,
bindings: node.bindings,
};
}
case "node": {
@@ -55,6 +60,7 @@ export async function render({
children,
start,
end: content.length,
bindings: node.bindings,
};
}
case "array": {
@@ -65,6 +71,7 @@ export async function render({
children,
start,
end: content.length,
bindings: node.bindings,
};
}
default: {
@@ -92,6 +99,7 @@ export async function render({
content: node.content,
startPos,
endPos,
bindings: node.bindings,
};
case "node":
return {
@@ -100,13 +108,15 @@ export async function render({
children: node.children.map(assignPositions),
startPos,
endPos,
bindings: node.bindings,
};
case "array":
return {
type: "array",
children: node.children.map(assignPositions),
startPos,
endPos,
bindings: node.bindings,
};
default:
assertUnreachable(node);
30 changes: 29 additions & 1 deletion rplugin/node/magenta/src/tea/tea.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { d, MountedView, MountPoint, mountView, VDOMNode } from "./view.ts";
import { context } from "../context.ts";
import { BindingKey, getBindings } from "./mappings.ts";

export type Dispatch<Msg> = (msg: Msg) => void;

@@ -40,7 +41,9 @@ type AppState<Model> =
};

export type App<Msg, Model> = {
mount(mount: MountPoint): Promise<void>;
mount(mount: MountPoint): Promise<{
onKey(key: BindingKey): void;
}>;
unmount(): void;
dispatch: Dispatch<Msg>;
getState(): AppState<Model>;
@@ -184,6 +187,31 @@ export function createApp<Model, Msg, SubscriptionType extends string>({
mount,
props: { currentState, dispatch },
});

await context.nvim.call("nvim_buf_set_keymap", [
mount.buffer.id,
"n",
"<CR>",
":call MagentaOnEnter()",
{ noremap: true, silent: true },
]);

return {
async onKey(key: BindingKey) {
const window = await context.nvim.window;
const [row, col] = await window.cursor;
if (root) {
const bindings = getBindings(root._getMountedNode(), { row, col });
if (bindings && bindings[key]) {
bindings[key]();
}
} else {
context.logger.debug(
`Got onKey event ${key}, but root is no longer mounted.`,
);
}
},
};
},
unmount() {
if (root) {
8 changes: 7 additions & 1 deletion rplugin/node/magenta/src/tea/update.ts
Original file line number Diff line number Diff line change
@@ -151,7 +151,11 @@ nextRoot: ${JSON.stringify(nextRoot, null, 2)}`);
switch (current.type) {
case "string":
if (current.content == (next as StringVDOMNode).content) {
return updateNodePos(current as unknown as CurrentMountedVDOM);
const updatedNode = updateNodePos(
current as unknown as CurrentMountedVDOM,
);
updatedNode.bindings = next.bindings;
return updatedNode;
} else {
return await replaceNode(
current as unknown as CurrentMountedVDOM,
@@ -185,6 +189,7 @@ nextRoot: ${JSON.stringify(nextRoot, null, 2)}`);
endPos: nextChildren.length
? nextChildren[nextChildren.length - 1].endPos
: updatePos(current.endPos as CurrentPosition),
bindings: next.bindings,
};
return nextMountedNode;
} else {
@@ -251,6 +256,7 @@ nextRoot: ${JSON.stringify(nextRoot, null, 2)}`);
children: nextChildren,
startPos,
endPos: nextChildrenEndPos,
bindings: next.bindings,
};
return nextMountedNode;
}
22 changes: 21 additions & 1 deletion rplugin/node/magenta/src/tea/view.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Buffer } from "neovim";
import { render } from "./render.ts";
import { update } from "./update.ts";
import { Bindings } from "./mappings.ts";

export type Position = {
row: number;
@@ -14,15 +15,21 @@ export interface MountPoint {
}

export type View<P> = (props: P) => VDOMNode;
export type StringVDOMNode = { type: "string"; content: string };
export type StringVDOMNode = {
type: "string";
content: string;
bindings?: Bindings;
};
export type ComponentVDOMNode = {
type: "node";
children: VDOMNode[];
template: TemplateStringsArray;
bindings?: Bindings;
};
export type ArrayVDOMNode = {
type: "array";
children: VDOMNode[];
bindings?: Bindings;
};

export type VDOMNode = StringVDOMNode | ComponentVDOMNode | ArrayVDOMNode;
@@ -32,6 +39,7 @@ export type MountedStringNode = {
content: string;
startPos: Position;
endPos: Position;
bindings?: Bindings;
};

export type MountedComponentNode = {
@@ -40,13 +48,15 @@ export type MountedComponentNode = {
children: MountedVDOM[];
startPos: Position;
endPos: Position;
bindings?: Bindings;
};

export type MountedArrayNode = {
type: "array";
children: MountedVDOM[];
startPos: Position;
endPos: Position;
bindings?: Bindings;
};

export type MountedVDOM =
@@ -71,6 +81,7 @@ export async function mountView<P>({
props: P;
}): Promise<MountedView<P>> {
let mountedNode = await render({ vdom: view(props), mount });

return {
async render(props) {
const next = view(props);
@@ -110,3 +121,12 @@ export function d(

return { type: "node", children: children, template: template };
}

/** Replace the bindings for this node
*/
export function withBindings(node: VDOMNode, bindings: Bindings) {
return {
...node,
bindings,
};
}

0 comments on commit 3dcaf39

Please sign in to comment.