diff --git a/src/conf/base/right-click.ts b/src/conf/base/right-click.ts index 8f474e4c..46559cc0 100644 --- a/src/conf/base/right-click.ts +++ b/src/conf/base/right-click.ts @@ -1,4 +1,3 @@ -import { AvailableActions } from "@conf/plugins"; export const RightClickIndexes = { textobjects: 1, @@ -17,106 +16,3 @@ export type RightClickGroups = { default: 1000; }; -interface RightClickMenuItemBase { - title: string; - keys?: string | string[]; -} - -interface RightClickMenuActionItem extends RightClickMenuItemBase { - actionId: AvailableActions; - alwaysInMenu?: boolean; -} - -interface RightClickMenuSubMenuItem extends RightClickMenuItemBase { - children: RightClickMenuItem[]; -} - -interface RightClickMenuGroup { - items: RightClickMenuItem[]; -} - -type RightClickMenuItem = - | RightClickMenuActionItem - | RightClickMenuSubMenuItem - | RightClickMenuGroup; - -const CppToolkitGroup: RightClickMenuGroup = { - items: [ - { - title: "CppToolkit", - children: [ - { - title: "Insert header", - actionId: "cpptoolkit.insert-header", - keys: "i", - }, - { - title: "Generate function implementation", - actionId: "cpptoolkit.gen-def", - keys: "d", - }, - { - title: "Move value", - actionId: "cpptoolkit.move-value", - keys: "m", - }, - { - title: "Forward value", - actionId: "cpptoolkit.forward-value", - keys: "f", - }, - ], - }, - ], -}; - -const RustToolkitGroup: RightClickMenuGroup = { - items: [ - { - title: "Open cargo.toml", - actionId: "rust-tools.open-cargo-toml", - }, - { - title: "Open parent module", - actionId: "rust-tools.open-parent-module", - }, - ], -}; - -const FormatFileItem: RightClickMenuActionItem = { - title: "Format file", - actionId: "conform.format", - keys: "c", -}; - -const CopilotGroup: RightClickMenuGroup = { - items: [ - { - title: "Copilot", - children: [ - { - title: "Copilot status", - actionId: "copilot.status", - keys: "s", - }, - { - title: "Copilot auth", - actionId: "copilot.auth", - keys: "a", - }, - { - title: "Copilot panel", - actionId: "copilot.show-panel", - keys: "p", - }, - ], - }, - ], -}; - -export const RightClickMenu: RightClickMenuItem[] = [ - FormatFileItem, - CppToolkitGroup, - RustToolkitGroup, - CopilotGroup, -]; diff --git a/src/conf/plugins/index.ts b/src/conf/plugins/index.ts index 55fb4f72..a453a562 100644 --- a/src/conf/plugins/index.ts +++ b/src/conf/plugins/index.ts @@ -7,7 +7,7 @@ import { plugins as uiPlugins } from "./ui"; import { plugins as otherPlugins } from "./other"; import { plugins as codingPlugins } from "./coding"; import { plugins as treesitterPlugins } from "./treesitter"; -import { Plugin, PluginActionIds } from "@core/model"; +import { Action, Plugin, PluginActionIds } from "@core/model"; import { TupleToUnion } from "@core/type_traits"; export const AllPlugins = [ @@ -25,6 +25,42 @@ export const LazySpecs = [...AllPlugins, ...AllLspServers] .flat() .map((p) => p.asLazySpec()); +export class ActionRegistry { + private static instance?: ActionRegistry; + + private _actions: Map> = new Map(); + + private constructor() { + AllPlugins.flat().forEach((plug) => { + plug.actions.forEach((action) => { + this.add(action); + }); + }); + } + + static getInstance() { + if (!ActionRegistry.instance) { + ActionRegistry.instance = new ActionRegistry(); + } + return ActionRegistry.instance; + } + + private add(action: Action) { + if (this._actions.has(action.id)) { + throw new Error(`Action ${action.id} already exists`); + } + this._actions.set(action.id, action); + } + + public get(id: string) { + return this._actions.get(id); + } + + public get actions() { + return this._actions.values(); + } +} + type RemoveReadonlyFromTuple = T extends readonly [ infer A, ...infer Rest, diff --git a/src/conf/ui/right-click.ts b/src/conf/ui/right-click.ts new file mode 100644 index 00000000..59c581e5 --- /dev/null +++ b/src/conf/ui/right-click.ts @@ -0,0 +1,173 @@ +import { ActionRegistry, AvailableActions } from "@conf/plugins"; +import { ContextMenu } from "@core/components/context-menu"; +import { MenuItem } from "@core/components/menu-item"; +import { MenuText } from "@core/components/menu-text"; +import { Cache } from "@core/model"; +import { VimBuffer, isNil } from "@core/vim"; + +interface RightClickMenuItemBase { + title: string; + keys?: string | string[]; +} + +interface RightClickMenuActionItem extends RightClickMenuItemBase { + actionId: AvailableActions; + alwaysInMenu?: boolean; +} + +interface RightClickMenuSubMenuItem extends RightClickMenuItemBase { + children: RightClickMenuItem[]; +} + +interface RightClickMenuGroup { + items: RightClickMenuItem[]; +} + +type RightClickMenuItem = + | RightClickMenuActionItem + | RightClickMenuSubMenuItem + | RightClickMenuGroup; + +function normalizeKeys(keys?: string | string[]): string[] { + if (isNil(keys)) { + return []; + } + if (typeof keys === "string") { + return [keys]; + } else { + return keys; + } +} + +function intoMenuItem(buffer: VimBuffer, item: RightClickMenuItem): MenuItem[] { + if ("actionId" in item) { + return [ + new MenuItem( + item.title, + () => { + let action = ActionRegistry.getInstance().get(item.actionId); + if (action === undefined) { + throw new Error(`Action ${item.actionId} not found`); + } + action.execute(); + }, + { + keys: normalizeKeys(item.keys), + alwaysInMenu: item.alwaysInMenu, + enabled: () => { + let action = ActionRegistry.getInstance().get(item.actionId); + if (action === undefined) { + throw new Error(`Action ${item.actionId} not found`); + } + return action.enabled(buffer); + }, + } + ), + ]; + } else if ("children" in item) { + return [ + new MenuItem(item.title, () => {}, { + children: item.children.map((v) => intoMenuItem(buffer, v)).flat(), + keys: normalizeKeys(item.keys), + }), + ]; + } else { + return [ + new MenuItem("---", () => {}), + ...item.items.map((v) => intoMenuItem(buffer, v)).flat(), + ]; + } +} + +const CppToolkitGroup: RightClickMenuGroup = { + items: [ + { + title: "CppToolkit", + children: [ + { + title: "Insert header", + actionId: "cpptoolkit.insert-header", + keys: "i", + }, + { + title: "Generate function implementation", + actionId: "cpptoolkit.gen-def", + keys: "d", + }, + { + title: "Move value", + actionId: "cpptoolkit.move-value", + keys: "m", + }, + { + title: "Forward value", + actionId: "cpptoolkit.forward-value", + keys: "f", + }, + ], + }, + ], +}; + +const RustToolkitGroup: RightClickMenuGroup = { + items: [ + { + title: "Open cargo.toml", + actionId: "rust-tools.open-cargo-toml", + }, + { + title: "Open parent module", + actionId: "rust-tools.open-parent-module", + }, + ], +}; + +const FormatFileItem: RightClickMenuActionItem = { + title: "Format file", + actionId: "conform.format", + keys: "c", +}; + +const CopilotGroup: RightClickMenuGroup = { + items: [ + { + title: "Copilot", + children: [ + { + title: "Copilot status", + actionId: "copilot.status", + keys: "s", + }, + { + title: "Copilot auth", + actionId: "copilot.auth", + keys: "a", + }, + { + title: "Copilot panel", + actionId: "copilot.show-panel", + keys: "p", + }, + ], + }, + ], +}; + +export const rightClickMenu: RightClickMenuItem[] = [ + FormatFileItem, + CppToolkitGroup, + RustToolkitGroup, + CopilotGroup, +]; + +const _rightClickMenuCache = new Cache(); + +export function mountRightClickMenu(buffer: VimBuffer, opt?: any): void { + let items = _rightClickMenuCache.ensure(buffer.asCacheKey(), () => { + return rightClickMenu.map((v) => intoMenuItem(buffer, v)).flat(); + }); + let menu = new ContextMenu(items); + vim.schedule(() => { + menu.asNuiMenu(opt ?? {}).mount(); + }); +} diff --git a/src/core/collections/right-click.ts b/src/core/collections/right-click.ts deleted file mode 100644 index 23d4c30d..00000000 --- a/src/core/collections/right-click.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** @noSelfInFile */ - -import { VimBuffer, ifNil, isNil } from "@core/vim"; -import { Collection } from "./collection"; -import { ContextMenu } from "@core/components/context-menu"; -import { MenuItem } from "@core/components/menu-item"; -import { ExcludeNil } from "@core/type_traits"; -import { RightClickPathElement, RightClickPathPart } from "@core/components"; -import { uniqueArray } from "@core/utils"; -import { Command, invokeCommand } from "@core/model"; - -type PathElement = ExcludeNil>; - -export class MenuItemPathMap { - public next: Map = new Map(); - public items: { item: MenuItem; index: number }[] = []; - public readonly title: string; - public index: number; - public keys: string[]; - public readonly depth: number; - - constructor(opts: { - title?: string; - index?: number; - depth?: number; - keys?: string[]; - }) { - this.title = ifNil(opts.title, ""); - this.index = ifNil(opts.index, 0); - this.depth = ifNil(opts.depth, 0); - this.keys = ifNil(opts.keys, []); - } - - private _updateInfo(ele: RightClickPathElement) { - this.index = Math.max(this.index, ele.index ?? 0); - this.keys = uniqueArray([...this.keys, ...(ele.keys ?? [])]); - } - - public push(path: RightClickPathElement[], item: MenuItem, index: number) { - if (path.length === this.depth) { - this.items.push({ item, index }); - } else { - let next = this.next.get(path[this.depth].title); - if (isNil(next)) { - next = new MenuItemPathMap({ - title: path[this.depth].title, - index: path[this.depth].index, - depth: this.depth + 1, - keys: path[this.depth].keys, - }); - this.next.set(path[this.depth].title, next); - } - next._updateInfo(path[this.depth]); - next.push(path, item, index); - } - } - - public complete(): MenuItem[] { - let ret = [...this.items]; - - for (let [k, v] of this.next) { - let ele = new MenuItem(k, () => {}, { - children: v.complete(), - keys: v.keys, - }); - ret.push({ - index: v.index, - item: ele, - }); - } - - return ret.sort((a, b) => a.index - b.index).map((item) => item.item); - } -} - -export class RightClickPaletteCollection extends Collection { - mount(buffer: VimBuffer, opt?: any): boolean { - let items = this.getMenuItems(buffer); - if (items.length > 0) { - let menu = new ContextMenu(items); - vim.schedule(() => { - menu.asNuiMenu(opt ?? {}).mount(); - }); - } - return items.length > 0; - } - - getMenuItems(buffer: VimBuffer) { - return this._cache.ensure(buffer.asCacheKey(), () => - this._getMenuItems(buffer) - ); - } - - _getMenuItems(buffer: VimBuffer) { - let commands = this.getCommands(buffer); - let pathMap = new MenuItemPathMap({}); - for (let cmd of commands) { - if (isNil(cmd.rightClick)) { - continue; - } - let info = commandToItemInfo(cmd); - pathMap.push(info.path, info.item, info.index); - } - return pathMap.complete(); - } -} - -type ItemInfo = { - item: MenuItem; - path: PathElement[]; - index: number; -}; - -function commandToItemInfo(cmd: Command): ItemInfo { - let callback = () => invokeCommand(cmd); - if (cmd.rightClick === true) { - return { - item: new MenuItem(cmd.name, callback), - path: [], - index: 0, - }; - } - return { - item: new MenuItem(ifNil(cmd.rightClick?.title, cmd.name), callback, { - keys: cmd.rightClick?.keys, - description: cmd.description, - }), - path: ifNil(cmd.rightClick?.path, []).map((v) => { - return normPathElement(v); - }), - index: ifNil(cmd.rightClick?.index, 0), - }; -} - -function normPathElement(e: RightClickPathPart): PathElement { - if (typeof e === "string") { - return { - title: e, - index: 0, - keys: [], - }; - } - return { - title: e.title, - index: ifNil(e.index, 0), - keys: e.keys ?? [], - }; -} diff --git a/src/core/components/menu-item.ts b/src/core/components/menu-item.ts index 5067dd22..38351a4a 100644 --- a/src/core/components/menu-item.ts +++ b/src/core/components/menu-item.ts @@ -28,11 +28,13 @@ function removeBuiltinKeys(keys: string[]) { * A menu item component. */ export class MenuItem { - public text: MenuText; + public readonly text: MenuText; public children: MenuItem[]; public description?: string; public callback: (this: void) => void; public parent?: NuiMenu; + public _enabled: boolean | (() => boolean); + public readonly alwaysInMenu: boolean; private _keys: string[]; @@ -43,6 +45,8 @@ export class MenuItem { children?: MenuItem[]; description?: string; keys?: string[]; + enabled?: boolean | (() => boolean); + alwaysInMenu?: boolean; } ) { this.text = typeof text === "string" ? new MenuText(text) : text; @@ -53,6 +57,8 @@ export class MenuItem { this._keys = removeBuiltinKeys( uniqueArray([...this.text.keys, ...(options?.keys ?? [])]) ); + this._enabled = options?.enabled ?? true; + this.alwaysInMenu = options?.alwaysInMenu ?? false; } /** @@ -71,6 +77,13 @@ export class MenuItem { return this.text.length; } + public get enabled(): boolean { + if (typeof this._enabled === "function") { + return this._enabled(); + } + return this._enabled; + } + public getPartLength() { if (this.text.isSeparator()) { return { @@ -107,9 +120,7 @@ export class MenuItem { childrenLength: number; }): NuiTreeNode { if (this.text.isSeparator()) { - return NuiMenuMod.separator(undefined, { - char: "-", - }); + return NuiMenuMod.separator(); } let line = this.text.asNuiLine(); @@ -120,6 +131,8 @@ export class MenuItem { let hint = this.keys.join("|"); line.append(hint, "@variable.builtin"); fillSpaces(line, hint.length, opts.keysLength); + } else { + fillSpaces(line, 0, opts.keysLength); } if (this.children.length > 0) { diff --git a/src/core/model/action.ts b/src/core/model/action.ts index a0976321..25f6c75c 100644 --- a/src/core/model/action.ts +++ b/src/core/model/action.ts @@ -275,24 +275,3 @@ export class Action { return ret; } } - -export class ActionRegistry { - private _actions: Map> = new Map(); - - constructor() {} - - public add(action: Action) { - if (this._actions.has(action.id)) { - throw new Error(`Action ${action.id} already exists`); - } - this._actions.set(action.id, action); - } - - public get(id: string) { - return this._actions.get(id); - } - - public get actions() { - return this._actions.values(); - } -} diff --git a/src/core/utils/fn.ts b/src/core/utils/fn.ts new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/core/utils/fn.ts @@ -0,0 +1 @@ + diff --git a/src/index.ts b/src/index.ts index 4bd88c78..3064bf98 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ +import { mountRightClickMenu } from "@conf/ui/right-click"; import { AllPlugins } from "./conf/plugins"; -import { RightClickPaletteCollection } from "@core/collections/right-click"; import { Command } from "@core/model"; import { VimBuffer, hideCursor } from "@core/vim"; @@ -14,17 +14,9 @@ export function getAllCommands(): Command[] { return result; } -const rightClickCollection = (() => { - let collection = new RightClickPaletteCollection(); - for (let cmd of getAllCommands()) { - collection.push(cmd); - } - return collection; -})(); - export function onRightClick(opts: any) { let bufnr = vim.api.nvim_get_current_buf(); let buffer = new VimBuffer(bufnr); hideCursor(); - rightClickCollection.mount(buffer, opts); + mountRightClickMenu(buffer, opts); }