Skip to content

Commit a8199e9

Browse files
committed
feat(visual-edits): contributors literais (HTML/Tx/VAR) sem expansão transitiva
Antes, ao clicar em um elemento, o dropdown listava TODAS as VARs transitivamente alcançáveis via o cadeia de concatenação ancestral — o que trazia dezenas de variáveis que sequer compunham aquele trecho. Agora o dropdown enumera apenas os blocos literais que produzem o trecho clicado, na ordem em que aparecem na fonte: - Fragmentos de string com tags HTML → badge </> - Fragmentos de texto puro (ex.: "Sem fornecedores") → badge Tx - Referências a VAR (cada ocorrência) → badge VAR (navega para a decl) Recursão entra em chamadas (IF, SWITCH, etc.) para capturar todos os varrefs e literais que contribuem para o resultado, sem expandir o corpo das VARs. "Trecho HTML final" passa a apontar para o range da concat-raiz completa, não só o primeiro literal.
1 parent 76bcca4 commit a8199e9

5 files changed

Lines changed: 242 additions & 158 deletions

File tree

src/App.tsx

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,7 @@ export default function App() {
6969
const [rendered, setRendered] = useState<ParseResult | null>(null);
7070
const [veMenu, setVeMenu] = useState<VisualEditsMenuState | null>(null);
7171
const renderedRef = useRef<ParseResult | null>(null);
72-
const codeRef = useRef(code);
7372
useEffect(() => { renderedRef.current = rendered; }, [rendered]);
74-
useEffect(() => { codeRef.current = code; }, [code]);
7573

7674
// Apply dark/light class
7775
useEffect(() => {
@@ -255,39 +253,27 @@ export default function App() {
255253
setPanelSplit(s);
256254
}, []);
257255

258-
// Calcula linha 1-based de um offset no documento atual.
259-
const lineOfOffset = useCallback((offset: number) => {
260-
const src = codeRef.current;
261-
let line = 1;
262-
for (let i = 0; i < offset && i < src.length; i++) {
263-
if (src[i] === '\n') line++;
264-
}
265-
return line;
266-
}, []);
267-
268256
// Recebe clique do Visual Edits no preview (coords já em window space).
269257
const onVisualEditsLocate = useCallback(
270258
(loc: string, screenX: number, screenY: number) => {
271259
const m = /^(\d+)-(\d+)$/.exec(loc);
272260
if (!m) return;
273-
const from = parseInt(m[1], 10);
274-
const to = parseInt(m[2], 10);
275-
const deps = renderedRef.current?.varDeps?.[loc] ?? [];
276-
if (deps.length === 0) {
277-
// Nada a desambiguar: mantém fluxo rápido, pula direto pro trecho.
278-
editorRef.current?.scrollAndSelect(from, to);
261+
const entry = renderedRef.current?.contributors?.[loc];
262+
if (!entry || entry.items.length === 0) {
263+
// Nada a desambiguar: pula direto pro literal clicado.
264+
editorRef.current?.scrollAndSelect(parseInt(m[1], 10), parseInt(m[2], 10));
279265
setVeMenu(null);
280266
return;
281267
}
282268
setVeMenu({
283269
x: screenX,
284270
y: screenY,
285-
clickedLoc: { start: from, end: to },
286-
clickedLine: lineOfOffset(from),
287-
deps,
271+
clickedLoc: entry.rootLoc,
272+
clickedLine: entry.rootLine,
273+
items: entry.items,
288274
});
289275
},
290-
[lineOfOffset],
276+
[],
291277
);
292278

293279
const onVeMenuSelect = useCallback((from: number, to: number) => {

src/components/VisualEditsMenu.tsx

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
22
import { MousePointerClick } from 'lucide-react';
3-
import type { VarDep } from '@/lib/daxParser/types';
3+
import type { Contributor } from '@/lib/daxParser/types';
44

55
export interface VisualEditsMenuState {
66
x: number;
77
y: number;
88
clickedLoc: { start: number; end: number };
99
clickedLine: number;
10-
deps: VarDep[];
10+
items: Contributor[];
1111
}
1212

1313
interface Props {
@@ -16,32 +16,61 @@ interface Props {
1616
onClose: () => void;
1717
}
1818

19-
const MENU_MIN_W = 260;
20-
const MENU_MAX_W = 420;
19+
const MENU_MIN_W = 280;
20+
const MENU_MAX_W = 460;
2121
const VIEWPORT_PAD = 8;
2222

23-
type BadgeKind = 'var' | 'html';
23+
type BadgeKind = 'var' | 'html' | 'text';
2424

2525
function TypeBadge({ kind }: { kind: BadgeKind }) {
26+
const base =
27+
'shrink-0 inline-flex items-center justify-center text-[9px] font-mono font-bold h-4 px-1 rounded border';
2628
if (kind === 'html') {
2729
return (
28-
<span className="shrink-0 inline-flex items-center justify-center text-[9px] font-mono font-bold h-4 px-1 rounded bg-orange-100 text-orange-600 dark:bg-orange-900/40 dark:text-orange-400 border border-orange-200 dark:border-orange-700">
30+
<span
31+
className={`${base} bg-orange-100 text-orange-600 border-orange-200 dark:bg-orange-900/40 dark:text-orange-400 dark:border-orange-700`}
32+
>
2933
{'</>'}
3034
</span>
3135
);
3236
}
37+
if (kind === 'text') {
38+
return (
39+
<span
40+
className={`${base} bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-900/40 dark:text-emerald-400 dark:border-emerald-700`}
41+
>
42+
Tx
43+
</span>
44+
);
45+
}
3346
return (
34-
<span className="shrink-0 inline-flex items-center justify-center text-[9px] font-mono font-bold h-4 px-1 rounded bg-violet-100 text-violet-600 dark:bg-violet-900/40 dark:text-violet-400 border border-violet-200 dark:border-violet-700">
47+
<span
48+
className={`${base} bg-violet-100 text-violet-600 border-violet-200 dark:bg-violet-900/40 dark:text-violet-400 dark:border-violet-700`}
49+
>
3550
VAR
3651
</span>
3752
);
3853
}
3954

55+
function labelOf(c: Contributor): string {
56+
if (c.kind === 'var') return c.name;
57+
return c.text;
58+
}
59+
60+
function tooltipOf(c: Contributor): string | undefined {
61+
if (c.kind === 'var') return c.snippet;
62+
return c.text;
63+
}
64+
65+
function locOf(c: Contributor): { start: number; end: number } {
66+
if (c.kind === 'var') return c.declLoc;
67+
return c.loc;
68+
}
69+
4070
export function VisualEditsMenu({ menu, onSelect, onClose }: Props) {
4171
const ref = useRef<HTMLDivElement | null>(null);
4272
const [adjusted, setAdjusted] = useState<{ left: number; top: number } | null>(null);
4373

44-
// Reposiciona dentro do viewport.
4574
useLayoutEffect(() => {
4675
if (!menu || !ref.current) {
4776
setAdjusted(null);
@@ -72,7 +101,6 @@ export function VisualEditsMenu({ menu, onSelect, onClose }: Props) {
72101
if (!ref.current.contains(e.target as Node)) onClose();
73102
}
74103
window.addEventListener('keydown', onKey);
75-
// Captura no próximo tick para não fechar imediatamente após o postMessage.
76104
const t = window.setTimeout(() => {
77105
window.addEventListener('mousedown', onClick, true);
78106
}, 0);
@@ -115,24 +143,27 @@ export function VisualEditsMenu({ menu, onSelect, onClose }: Props) {
115143
</span>
116144
</div>
117145
<div className="py-1">
118-
{menu.deps.length > 0 && (
146+
{menu.items.length > 0 && (
119147
<div className="max-h-72 overflow-y-auto">
120-
{menu.deps.map((d) => (
121-
<MenuItem
122-
key={d.name + d.declLoc.start}
123-
kind="var"
124-
label={d.name}
125-
line={d.line}
126-
tooltip={d.snippet}
127-
onClick={() => {
128-
onSelect(d.declLoc.start, d.declLoc.end);
129-
onClose();
130-
}}
131-
/>
132-
))}
148+
{menu.items.map((c, i) => {
149+
const loc = locOf(c);
150+
return (
151+
<MenuItem
152+
key={`${c.kind}-${loc.start}-${i}`}
153+
kind={c.kind}
154+
label={labelOf(c)}
155+
line={c.line}
156+
tooltip={tooltipOf(c)}
157+
onClick={() => {
158+
onSelect(loc.start, loc.end);
159+
onClose();
160+
}}
161+
/>
162+
);
163+
})}
133164
</div>
134165
)}
135-
{menu.deps.length > 0 && <div className="my-1 h-px bg-border" />}
166+
{menu.items.length > 0 && <div className="my-1 h-px bg-border" />}
136167
<MenuItem
137168
kind="html"
138169
label="Trecho HTML final"

src/lib/daxParser/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { evalNode, makeContext } from './evaluator';
44
import { ParseError } from './types';
55
import type { ParseResult } from './types';
66
import { processHtml } from './htmlProcessor';
7-
import { buildVarDepIndex } from './varDependencies';
7+
import { buildContributorIndex } from './varDependencies';
88

99
function posToLineCol(src: string, pos: number): { line: number; col: number } {
1010
let line = 1, col = 1;
@@ -41,8 +41,8 @@ export function parseDax(src: string): ParseResult {
4141
const isPureDax = !!html.trim() && !/<[a-z][\s\S]*?>/i.test(html);
4242
const rawValue = isPureDax ? html.trim() : undefined;
4343
html = processHtml(html);
44-
const varDeps = isPureDax ? undefined : buildVarDepIndex(ast, src);
45-
return { html, warnings, isPureDax, rawValue, measureName, varDeps };
44+
const contributors = isPureDax ? undefined : buildContributorIndex(ast, src);
45+
return { html, warnings, isPureDax, rawValue, measureName, contributors };
4646
} catch (e) {
4747
const msg = e instanceof Error ? e.message : String(e);
4848
if (e instanceof ParseError) {

src/lib/daxParser/types.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,32 @@ export type Node =
4343
| { kind: 'in'; value: Node; list: Node[]; loc?: SourceLoc }
4444
| { kind: 'let'; vars: { name: string; expr: Node; loc?: SourceLoc }[]; body: Node; loc?: SourceLoc };
4545

46-
// One VAR declaration referenced (directly or transitively) by an HTML element.
47-
export interface VarDep {
48-
name: string;
49-
declLoc: SourceLoc; // range covering "VAR _name = expr"
50-
snippet: string; // short preview of the declaration (~60 chars)
51-
line: number; // 1-based line of the VAR keyword
46+
// One concrete building block that composes a clicked HTML element.
47+
// Enumerated by walking the surrounding `&` concat chain (and recursing into
48+
// function-call args). Three flavors:
49+
// - 'html': string literal fragment that contains tags
50+
// - 'text': string literal fragment that is plain visible text
51+
// - 'var' : a varref in the chain (with its declaration location)
52+
export type Contributor =
53+
| { kind: 'html'; text: string; loc: SourceLoc; line: number }
54+
| { kind: 'text'; text: string; loc: SourceLoc; line: number }
55+
| {
56+
kind: 'var';
57+
name: string;
58+
refLoc: SourceLoc;
59+
declLoc: SourceLoc;
60+
line: number;
61+
snippet: string;
62+
};
63+
64+
export interface ContributorsEntry {
65+
rootLoc: SourceLoc; // full concat-chain range that produced this element
66+
rootLine: number; // 1-based line of rootLoc.start
67+
items: Contributor[]; // building blocks in source order
5268
}
5369

54-
// Map of data-dax-loc value ("start-end") -> ordered list of contributing VARs
55-
// (top-down by declaration position in the source).
56-
export type VarDepIndex = Record<string, VarDep[]>;
70+
// Map of data-dax-loc value ("start-end") -> contributors of that element.
71+
export type ContributorIndex = Record<string, ContributorsEntry>;
5772

5873
export interface ParseResult {
5974
html: string;
@@ -66,7 +81,7 @@ export interface ParseResult {
6681
isPureDax?: boolean;
6782
rawValue?: string;
6883
measureName?: string;
69-
varDeps?: VarDepIndex;
84+
contributors?: ContributorIndex;
7085
}
7186

7287
export class ParseError extends Error {

0 commit comments

Comments
 (0)