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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user