release: v0.4.1

This commit is contained in:
牟威霖 2026-06-17 18:57:38 +08:00
parent c5dfcaefbb
commit 3aa4f8b2dc
6 changed files with 160 additions and 17 deletions

View file

@ -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
View file

@ -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"

View file

@ -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",

View file

@ -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
);
}

View file

@ -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));
}

View file

@ -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"
}