mirror of
https://github.com/aitingtingya/mv-obcc.git
synced 2026-07-22 07:47:28 +00:00
release: v0.4.1
This commit is contained in:
parent
c5dfcaefbb
commit
3aa4f8b2dc
6 changed files with 160 additions and 17 deletions
|
|
@ -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",
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -25,28 +25,100 @@ const NAMED_KEYS: Record<string, string> = {
|
|||
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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue