diff --git a/crate/src/lib.rs b/crate/src/lib.rs index 73e6a64..8b21155 100644 --- a/crate/src/lib.rs +++ b/crate/src/lib.rs @@ -893,10 +893,34 @@ impl ParsedDocument { } } + let is_multi_expr = matches!(&parse_result.root.kind, + NodeKind::Module(exprs) if exprs.items.len() > 1); + for bind_idx in bind_sites { if used_binds.contains(&bind_idx) { continue; } let cps_id = CpsId(bind_idx); - if resolved.bind_scope.get(cps_id) == &Some(root_scope_id) { continue; } + + // Skip non-Ident bind sites (synthetic CPS nodes from lambdas, + // pattern matching, etc.) + let is_ident = cps.origin.get(cps_id) + .and_then(|ast_id| *ast_index.get(ast_id)) + .is_some_and(|node| matches!(&node.kind, NodeKind::Ident(_))); + if !is_ident { continue; } + + // Skip module-level bindings (they are exports). + // Single-expr module: bindings are directly in root scope. + // Multi-expr module: bindings are in the synthetic module fn + // scope, whose parent_scope is root. + if let Some(scope) = resolved.bind_scope.get(cps_id) { + if *scope == root_scope_id { continue; } + if is_multi_expr { + let parent_is_root = resolved.parent_scope.try_get(*scope) + .and_then(|p| *p) + .is_some_and(|parent| parent == root_scope_id); + if parent_is_root { continue; } + } + } + if let Some(ast_id) = *cps.origin.get(cps_id) { if let Some(node) = *ast_index.get(ast_id) { let line = node.loc.start.line.saturating_sub(1); @@ -905,7 +929,7 @@ impl ParsedDocument { let end_col = node.loc.end.col; let name = match &node.kind { NodeKind::Ident(s) => s.replace('\\', "\\\\").replace('"', "\\\""), - _ => "?".to_string(), + _ => continue, }; diag_entries.push(format!( r#"{{"line":{line},"col":{col},"endLine":{end_line},"endCol":{end_col},"message":"unused binding '{name}'","source":"name_res","severity":"warning"}}"# diff --git a/src/ast-panel.ts b/src/ast-panel.ts index f5be5dc..c162be8 100644 --- a/src/ast-panel.ts +++ b/src/ast-panel.ts @@ -105,12 +105,6 @@ export class AstPanel { this.renderNode(root, this.container, 0) this.rebuildFlat() - - // Sync with current cursor position after update - const pos = this.editor.getPosition() - if (pos) { - this.highlightAtPosition(pos.lineNumber - 1, pos.column - 1) - } } // Render one node as a group (row + children wrapper) and recurse. diff --git a/src/main.ts b/src/main.ts index 7a2dffa..c039153 100644 --- a/src/main.ts +++ b/src/main.ts @@ -32,6 +32,8 @@ import 'monaco-editor/esm/vs/editor/contrib/wordHighlighter/browser/highlightDec import 'monaco-editor/esm/vs/editor/contrib/caretOperations/browser/caretOperations.js' // transpose chars import 'monaco-editor/esm/vs/editor/contrib/cursorUndo/browser/cursorUndo.js' // cursor stack undo import 'monaco-editor/esm/vs/editor/contrib/hover/browser/hoverContribution.js' // hover tooltips (diagnostics, symbols) +import 'monaco-editor/esm/vs/editor/contrib/gotoSymbol/browser/goToCommands.js' // go-to-definition (F12 / Cmd+click) +import 'monaco-editor/esm/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.js' // Ctrl/Cmd+click inline import { compile } from './compiler.js' import { run } from './wasi-shim.js' import { FinkTokenizer, type LexToken } from './tokenizer.js' @@ -59,6 +61,11 @@ let lastSemanticTokens: Uint32Array = new Uint32Array(0) let lastDiagnostics: string = '[]' let lastParseMs = 0 +// Last ParsedDocument kept alive for cursor-time queries (go-to-def, references). +// Freed and replaced on every successful re-parse. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let lastDoc: any = null + // Forward declaration — defined after the editor is created (needs DOM + editor). let applyDiagnosticToggles: () => void = () => {} @@ -105,7 +112,9 @@ function _reparse(src: string, _modelVersion: number): void { lastDiagnostics = doc.get_diagnostics() cpsJson = doc.get_cps() cpsLiftedJson = doc.get_cps_lifted() - doc.free() + // Keep doc alive for cursor-time queries (go-to-def, references). + if (lastDoc) try { lastDoc.free() } catch (_) { /* already freed */ } + lastDoc = doc } catch (e) { console.warn('[fink] CPS analysis crashed (name resolution disabled):', e) try { doc.free() } catch (_) { /* handle already poisoned */ } @@ -235,6 +244,27 @@ monaco.languages.registerDocumentSemanticTokensProvider('fink', { releaseDocumentSemanticTokens() {}, }) +// Definition provider — delegates to the WASM ParsedDocument kept alive across +// re-parses. Requires run_analysis() to have succeeded (name resolution). +monaco.languages.registerDefinitionProvider('fink', { + provideDefinition(model, position) { + if (!lastDoc) return undefined + // Monaco positions are 1-based; WASM API is 0-based. + const data: Uint32Array = lastDoc.get_definition( + position.lineNumber - 1, + position.column - 1, + ) + if (data.length !== 4) return undefined + return { + uri: model.uri, + range: new monaco.Range( + data[0] + 1, data[1] + 1, + data[2] + 1, data[3] + 1, + ), + } + }, +}) + // --------------------------------------------------------------------------- // Theme — reads colors from CSS variables (set by embedding page or dev wrapper) // --------------------------------------------------------------------------- @@ -382,6 +412,18 @@ for (const tab of document.querySelectorAll('.fink-tab')) { if (!SYNC_TABS.has(activeTab)) { // Passive tab (e.g. Output) — clear all decorations and stop syncing. clearAllDecorations() + } else { + // Sync tab activated — sync to current cursor position. + clearAllDecorations() + const pos = editor.getPosition() + if (pos) { + const line = pos.lineNumber - 1 + const col = pos.column - 1 + if (activeTab === 'fink-tokens') tokensPanel?.highlightAtPosition(line, col) + if (activeTab === 'fink-ast') astPanel?.highlightAtPosition(line, col) + if (activeTab === 'fink-cps') cpsPanel?.syncFromSource(line, col) + if (activeTab === 'fink-cps-lifted') cpsPanelLifted?.syncFromSource(line, col) + } } }) } diff --git a/src/tokens-panel.ts b/src/tokens-panel.ts index c0b6a4a..07bab5b 100644 --- a/src/tokens-panel.ts +++ b/src/tokens-panel.ts @@ -96,12 +96,6 @@ export class TokensPanel { this.container.innerHTML = '' this.container.appendChild(frag) - - // Sync with current cursor position - const pos = this.editor.getPosition() - if (pos) { - this.highlightAtPosition(pos.lineNumber - 1, pos.column - 1) - } } clearEditorHighlight(): void {