refactor: implement base/modifier separation for variable reactivity

Introduce a distinction between base values (set via `/set` or
declarations) and active modifiers (applied via tags). This ensures
tag transitions and numeric modifiers are calculated correctly during
cascades.

- Refactor `VariableView` to use a flat list with hover popups for
  declarations and active modifiers.
- Update `command-dispatcher` to use `getCombined` for accurate
  value lookups and `setBase` for variable updates.
- Update `var-reactivity` to manage `baseValues`, `activeMods`, and
  `sourceActivations` separately.
- Export new utility functions for accessing combined values,
  modifiers, and declaration expressions.
This commit is contained in:
2026-07-13 10:10:47 +08:00
parent 411f4d79ff
commit c524dd5867
4 changed files with 375 additions and 435 deletions
+93 -212
View File
@@ -1,96 +1,34 @@
/**
* VariableView — table view of all variable key-value pairs and active tags.
* VariableView — flat list of all variable key-value pairs.
*
* Shows variables grouped by type (declared, plain, tag-typed) and an
* active tags section showing which variable activates each tag and
* what modifier effects are in play.
* Hovering a value shows a popup with:
* - Declaration expression (if any), styled distinctly
* - Active tag modifiers: #tag +N (from $source)
*/
import { Component, For, createMemo, Show } from "solid-js";
import { Component, For, createMemo, Show, createSignal } from "solid-js";
import { useJournalStream } from "../stores/journalStream";
import { useJournalCompletions } from "./completions";
import { computeActiveTags } from "./var-reactivity";
import { extractDependencies } from "./var-reactivity";
import type { VarDeclaration } from "./declare-parser";
import { getMods, getDeclExpr } from "./var-reactivity";
export const VariableView: Component = () => {
const stream = useJournalStream();
const comp = useJournalCompletions();
const declarations = createMemo(() => comp.data.declarations);
const tagModifiers = createMemo(() => comp.data.tagModifiers);
const values = createMemo(() => stream.variables);
/** Build a map: $key → VarDeclaration */
const declMap = createMemo(() => {
const map = new Map<string, VarDeclaration>();
for (const d of declarations()) {
map.set(d.key, d);
}
return map;
});
/** Active tags */
const activeTags = createMemo(() => computeActiveTags(values()));
/** Tag modifier index: #tag → [{target, expression}] */
const tagModMap = createMemo(() => {
const map = new Map<string, Array<{ target: string; expression: string }>>();
for (const tm of tagModifiers()) {
let list = map.get(tm.tag);
if (!list) {
list = [];
map.set(tm.tag, list);
}
list.push({ target: tm.target, expression: tm.expression });
}
return map;
});
/** Which variable activates each tag */
const tagActivators = createMemo(() => {
const map = new Map<string, string>();
/** Flat list of all variables */
const items = createMemo(() => {
const vars = values();
for (const [key, val] of Object.entries(vars)) {
if (val.startsWith("#")) {
if (!map.has(val)) map.set(val, key);
}
}
return map;
});
/** Group variables: declared, plain (set directly), tag-typed */
const groups = createMemo(() => {
const vars = values();
const declKeys = new Set(declarations().map((d) => d.key));
const declared: Array<{ key: string; value: string; expr: string; deps: string[] }> = [];
const plain: Array<{ key: string; value: string }> = [];
const tagVars: Array<{ key: string; value: string }> = [];
for (const [key, val] of Object.entries(vars)) {
if (val.startsWith("#")) {
tagVars.push({ key, value: val });
} else if (declKeys.has(key)) {
const decl = declMap().get(key)!;
declared.push({
key,
value: val,
expr: decl.expression,
deps: extractDependencies(decl.expression),
});
} else {
plain.push({ key, value: val });
}
}
return { declared, plain, tagVars };
return Object.entries(vars).map(([key, value]) => ({
key,
value,
isTag: value.startsWith("#"),
}));
});
const hasAnyData = () =>
declarations().length > 0 ||
Object.keys(values()).length > 0 ||
activeTags().length > 0;
comp.data.declarations.length > 0 || Object.keys(values()).length > 0;
return (
<div class="h-full overflow-y-auto bg-gray-50">
@@ -102,142 +40,85 @@ export const VariableView: Component = () => {
</p>
}
>
{/* Active Tags */}
<Show when={activeTags().length > 0}>
<div class="mb-3">
<div class="sticky top-0 bg-gray-100 px-3 py-1.5 border-b border-gray-200">
<span class="text-xs font-semibold text-gray-600">激活标签</span>
</div>
<div class="divide-y divide-gray-100">
<For each={activeTags()}>
{(tag) => {
const activator = () => tagActivators().get(tag);
const mods = () => tagModMap().get(tag) ?? [];
return (
<div class="px-3 py-1.5">
<div class="flex items-center gap-1.5">
<span class="text-sm font-mono text-purple-700 font-semibold">
{tag}
</span>
<Show when={activator()}>
<span class="text-[10px] text-gray-400">
← {activator()}
</span>
</Show>
</div>
<Show when={mods().length > 0}>
<div class="mt-1 space-y-0.5">
<For each={mods()}>
{(mod) => {
const currentVal = () => values()[mod.target];
return (
<div class="text-[10px] text-gray-500 ml-2 flex items-center gap-1">
<span class="font-mono">{mod.target}</span>
<span>+{mod.expression}</span>
<Show when={currentVal() !== undefined}>
<span class="text-gray-400">
= {currentVal()}
</span>
</Show>
</div>
);
}}
</For>
</div>
</Show>
</div>
);
}}
</For>
</div>
</div>
</Show>
{/* Declared Variables */}
<Show when={groups().declared.length > 0}>
<div class="mb-3">
<div class="sticky top-0 bg-gray-100 px-3 py-1.5 border-b border-gray-200">
<span class="text-xs font-semibold text-gray-600">
声明变量
</span>
</div>
<div class="divide-y divide-gray-100">
<For each={groups().declared}>
{(item) => (
<div class="flex items-center px-3 py-1.5 hover:bg-white/50 transition-colors">
<div class="flex-1 min-w-0 flex items-center gap-1.5">
<span class="text-sm font-mono text-gray-800">
{item.key}
</span>
<span class="text-[10px] text-gray-400 bg-gray-100 px-1 rounded shrink-0">
{item.expr}
</span>
</div>
<div class="flex items-center gap-2 shrink-0">
<span class="text-sm font-mono text-gray-700 font-semibold">
{item.value}
</span>
<Show when={item.deps.length > 0}>
<span class="text-[10px] text-gray-400">
← {item.deps.join(", ")}
</span>
</Show>
</div>
</div>
)}
</For>
</div>
</div>
</Show>
{/* Plain Variables */}
<Show when={groups().plain.length > 0}>
<div class="mb-3">
<div class="sticky top-0 bg-gray-100 px-3 py-1.5 border-b border-gray-200">
<span class="text-xs font-semibold text-gray-600">
直接设置
</span>
</div>
<div class="divide-y divide-gray-100">
<For each={groups().plain}>
{(item) => (
<div class="flex items-center px-3 py-1.5 hover:bg-white/50 transition-colors">
<span class="flex-1 text-sm font-mono text-gray-800">
{item.key}
</span>
<span class="text-sm font-mono text-gray-700 font-semibold">
{item.value}
</span>
</div>
)}
</For>
</div>
</div>
</Show>
{/* Tag Variables */}
<Show when={groups().tagVars.length > 0}>
<div class="mb-3">
<div class="sticky top-0 bg-gray-100 px-3 py-1.5 border-b border-gray-200">
<span class="text-xs font-semibold text-gray-600">标签变量</span>
</div>
<div class="divide-y divide-gray-100">
<For each={groups().tagVars}>
{(item) => (
<div class="flex items-center px-3 py-1.5 hover:bg-white/50 transition-colors">
<span class="flex-1 text-sm font-mono text-gray-800">
{item.key}
</span>
<span class="text-sm font-mono text-purple-700 font-semibold">
{item.value}
</span>
</div>
)}
</For>
</div>
</div>
</Show>
<div class="divide-y divide-gray-100">
<For each={items()}>
{(item) => (
<VariableRow
key={item.key}
value={item.value}
isTag={item.isTag}
/>
)}
</For>
</div>
</Show>
</div>
);
};
};
// ---------------------------------------------------------------------------
// VariableRow — single row with hover popup
// ---------------------------------------------------------------------------
const VariableRow: Component<{
key: string;
value: string;
isTag: boolean;
}> = (props) => {
const [hovered, setHovered] = createSignal(false);
const declExpr = createMemo(() => getDeclExpr(props.key));
const mods = createMemo(() => getMods(props.key));
const hasPopup = () => !!declExpr() || mods().length > 0;
return (
<div
class="relative flex items-center px-3 py-1.5 hover:bg-white/50 transition-colors"
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<span class="flex-1 text-sm font-mono text-gray-800">{props.key}</span>
<span
class={props.isTag
? "text-sm font-mono text-purple-700 font-semibold"
: "text-sm font-mono text-gray-700 font-semibold"
}
>
{props.value}
</span>
{/* Hover popup */}
<Show when={hasPopup() && hovered()}>
<div class="absolute right-0 top-full mt-1 z-50 bg-white border border-gray-200 rounded-md shadow-lg p-2 min-w-[180px] text-xs">
{/* Declaration expression */}
<Show when={declExpr()}>
<div class="mb-1">
<span class="text-gray-400">expr: </span>
<span class="font-mono text-gray-500 bg-gray-50 px-1 rounded">
{declExpr()}
</span>
</div>
</Show>
{/* Tag modifiers */}
<Show when={mods().length > 0}>
<div class={declExpr() ? "border-t border-gray-100 pt-1 mt-1" : ""}>
<For each={mods()}>
{(mod) => (
<div class="flex items-center gap-1 text-gray-500">
<span class="font-mono text-purple-600">{mod.tag}</span>
<span>+{mod.value}</span>
<span class="text-gray-400">(from {mod.source})</span>
</div>
)}
</For>
</div>
</Show>
</div>
</Show>
</div>
);
};
export default VariableView;