diff --git a/manifest.json b/manifest.json index 545909f..858bad1 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "mv-obcc", "name": "mv-SenceAI", - "version": "0.4.0", + "version": "0.4.1", "minAppVersion": "1.7.2", "description": "Connects local vaults to Claude Code with page tracking, session-aware terminals, MCP tools, editable diffs, and optional upstream compatibility.", "author": "MV", diff --git a/package-lock.json b/package-lock.json index f7885d6..1af1862 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mv-obcc", - "version": "0.4.0", + "version": "0.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mv-obcc", - "version": "0.4.0", + "version": "0.4.1", "dependencies": { "@codemirror/merge": "^6.12.1", "ws": "^8.18.3" diff --git a/package.json b/package.json index 40843ed..8fec0f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mv-obcc", - "version": "0.4.0", + "version": "0.4.1", "description": "A focused Claude Code IDE bridge for Obsidian.", "main": "main.js", "type": "module", diff --git a/src/inline-completion/inline-hotkey-format.ts b/src/inline-completion/inline-hotkey-format.ts index dad8e80..069a0fc 100644 --- a/src/inline-completion/inline-hotkey-format.ts +++ b/src/inline-completion/inline-hotkey-format.ts @@ -25,28 +25,100 @@ const NAMED_KEYS: Record = { Tab: "Tab", }; +export interface ParsedInlineHotkey { + key: string; + mod: boolean; + ctrl: boolean; + meta: boolean; + alt: boolean; + shift: boolean; +} + +function splitHotkey(value: string): string[] { + return value.trim().split(/-(?!$)/).filter(Boolean); +} + +function codeToBaseKey(code: string): string | null { + if (/^Key[A-Z]$/.test(code)) return code.slice(3).toLowerCase(); + if (/^Digit[0-9]$/.test(code)) return code.slice(5); + return null; +} + function normalizeBaseKey(key: string): string | null { if (!key || PURE_MODIFIER_KEYS.has(key)) return null; const named = NAMED_KEYS[key]; if (named) return named; - if (/^F\d{1,2}$/.test(key)) return key; + if (/^F\d{1,2}$/i.test(key)) return key.toUpperCase(); if (key.length === 1) { return key.toLowerCase(); } return key; } +function normalizeEventBaseKey(event: KeyboardEvent): string | null { + return codeToBaseKey(event.code) ?? normalizeBaseKey(event.key); +} + +function normalizeStoredBaseKey(key: string): string | null { + return normalizeBaseKey(key); +} + +function modifierDisplayLabel(modifier: string): string | null { + if (/^mod$/i.test(modifier)) return "Ctrl/Cmd"; + if (/^a(lt)?$/i.test(modifier) || /^option$/i.test(modifier)) { + return "Alt/Option"; + } + if (/^cmd$/i.test(modifier)) return "Cmd"; + if (/^meta$/i.test(modifier)) return "Meta"; + if (/^(c|ctrl|control)$/i.test(modifier)) return "Ctrl"; + if (/^s(hift)?$/i.test(modifier)) return "Shift"; + return null; +} + +export function parseInlineHotkey(value: string): ParsedInlineHotkey | null { + const parts = splitHotkey(value); + const key = parts.at(-1); + const normalizedKey = key ? normalizeStoredBaseKey(key) : null; + if (!normalizedKey) return null; + + const parsed: ParsedInlineHotkey = { + key: normalizedKey, + mod: false, + ctrl: false, + meta: false, + alt: false, + shift: false, + }; + + for (const modifier of parts.slice(0, -1)) { + if (/^mod$/i.test(modifier)) parsed.mod = true; + else if (/^a(lt)?$/i.test(modifier) || /^option$/i.test(modifier)) { + parsed.alt = true; + } else if (/^cmd$/i.test(modifier) || /^meta$/i.test(modifier)) { + parsed.meta = true; + } else if (/^(c|ctrl|control)$/i.test(modifier)) { + parsed.ctrl = true; + } else if (/^s(hift)?$/i.test(modifier)) { + parsed.shift = true; + } else { + return null; + } + } + + return parsed; +} + export function eventToCodeMirrorKey( event: KeyboardEvent, isMacLike: boolean, ): string | null { - const baseKey = normalizeBaseKey(event.key); + const baseKey = normalizeEventBaseKey(event); if (!baseKey) return null; const modifiers: string[] = []; const hasMod = isMacLike ? event.metaKey : event.ctrlKey; if (hasMod) { - modifiers.push(isMacLike ? "Cmd" : "Ctrl"); + modifiers.push("Mod"); } if (event.altKey) { modifiers.push("Alt"); @@ -63,5 +135,38 @@ export function eventToCodeMirrorKey( export function formatInlineHotkeyLabel(value: string): string { const trimmed = value.trim(); - return trimmed || "未绑定"; + if (!trimmed) return "未绑定"; + + const parts = splitHotkey(trimmed); + if (parts.length === 0) return "未绑定"; + const key = parts.at(-1); + if (!key) return trimmed; + + const labels: string[] = []; + for (const modifier of parts.slice(0, -1)) { + labels.push(modifierDisplayLabel(modifier) ?? modifier); + } + labels.push(normalizeStoredBaseKey(key) ?? key); + return labels.join("-"); +} + +export function matchInlineHotkey( + value: string, + event: KeyboardEvent, + isMacLike: boolean, +): boolean { + const parsed = parseInlineHotkey(value); + if (!parsed) return false; + + const eventKey = normalizeEventBaseKey(event); + if (!eventKey || eventKey !== parsed.key) return false; + + const wantMeta = parsed.meta || (parsed.mod && isMacLike); + const wantCtrl = parsed.ctrl || (parsed.mod && !isMacLike); + return ( + event.metaKey === wantMeta && + event.ctrlKey === wantCtrl && + event.altKey === parsed.alt && + event.shiftKey === parsed.shift + ); } diff --git a/src/inline-completion/inline-suggestion-keymap.ts b/src/inline-completion/inline-suggestion-keymap.ts index 8de5a8f..2678746 100644 --- a/src/inline-completion/inline-suggestion-keymap.ts +++ b/src/inline-completion/inline-suggestion-keymap.ts @@ -10,6 +10,10 @@ import { hasSuggestion, readSuggestion, } from "./inline-suggestion-state"; +import { + matchInlineHotkey, + parseInlineHotkey, +} from "./inline-hotkey-format"; /** * Per-view keymap for inline completion, registered at the highest precedence @@ -36,6 +40,17 @@ function normalizeKey(raw: string): string { return (raw ?? "").trim(); } +function isMacLikePlatform(): boolean { + const nav = + typeof activeWindow !== "undefined" ? activeWindow.navigator : navigator; + return nav.platform.toLowerCase().includes("mac"); +} + +interface InlineHotkeyAction { + key: string; + run: (view: EditorView) => boolean; +} + /** * Build the keymap extension from current settings + handlers. Entries with * empty bindings are skipped. Every handler first checks that a suggestion is @@ -45,12 +60,14 @@ function normalizeKey(raw: string): string { export function buildKeymapExtension( bindingsConfig: InlineCompletionKeymap, handlers: KeymapHandlers, + isMacLike = isMacLikePlatform(), ): Extension { const bindings: KeyBinding[] = []; + const actions: InlineHotkeyAction[] = []; const accept = normalizeKey(bindingsConfig.accept); - if (accept) { - bindings.push({ + if (accept && parseInlineHotkey(accept)) { + actions.push({ key: accept, run: (v) => { if (!hasSuggestion(v)) return false; @@ -61,8 +78,8 @@ export function buildKeymapExtension( } const reject = normalizeKey(bindingsConfig.reject); - if (reject) { - bindings.push({ + if (reject && parseInlineHotkey(reject)) { + actions.push({ key: reject, run: (v) => { if (!hasSuggestion(v)) return false; @@ -75,8 +92,8 @@ export function buildKeymapExtension( } const cancel = normalizeKey(bindingsConfig.cancel); - if (cancel) { - bindings.push({ + if (cancel && parseInlineHotkey(cancel)) { + actions.push({ key: cancel, run: (v) => { if (!hasSuggestion(v)) return false; @@ -87,12 +104,32 @@ export function buildKeymapExtension( } const request = normalizeKey(bindingsConfig.request); - if (request) { - bindings.push({ + if (request && parseInlineHotkey(request)) { + actions.push({ key: request, run: (v) => handlers.onRequest(v), }); } + for (const action of actions) { + bindings.push({ + key: action.key, + run: action.run, + }); + } + + if (actions.length > 0) { + bindings.push({ + any: (view, event) => { + for (const action of actions) { + if (matchInlineHotkey(action.key, event, isMacLike)) { + return action.run(view); + } + } + return false; + }, + }); + } + return Prec.highest(keymap.of(bindings)); } diff --git a/versions.json b/versions.json index 925088a..4b3f192 100644 --- a/versions.json +++ b/versions.json @@ -16,5 +16,6 @@ "0.3.6": "1.7.2", "0.3.7": "1.7.2", "0.3.8": "1.7.2", - "0.4.0": "1.7.2" + "0.4.0": "1.7.2", + "0.4.1": "1.7.2" }