Skip to content

feat(navigation): gf navigation for file references and include expressions#18

Merged
StanAngeloff merged 5 commits intodevelopfrom
feature/includeexpr
Mar 12, 2026
Merged

feat(navigation): gf navigation for file references and include expressions#18
StanAngeloff merged 5 commits intodevelopfrom
feature/includeexpr

Conversation

@StanAngeloff
Copy link
Collaborator

@StanAngeloff StanAngeloff commented Mar 9, 2026

Summary

Adds gf / <C-w>f navigation support to .chat buffers. When the cursor is on an @./file reference or a {{ include('path') }} expression, pressing gf opens the referenced file, and <C-w>f opens it in a split.

  • Resolve @./file references to absolute paths via the existing include() machinery
  • Resolve {{ include('path') }} expressions by evaluating with a capturing stub
  • Frontmatter variables are available in expressions (e.g. {{ include(my_var) }})
  • Falls back to Vim's default v:fname behavior when cursor is not on an include expression

Architecture

New infrastructure (shared with future LSP hover plan)

end_col on flemma.ast.Position — The AST position type previously had start_col but not end_col. Both expression segments ({{ expr }}) and file reference segments (@./file) now track their ending column. For file references, end_col excludes trailing punctuation (e.g. the comma in @./file.txt, more text). This is needed for precise cursor-to-segment matching.

find_segment_at_position(doc, lnum, col) in ast/query.lua — A new query function that finds the segment (and its parent message) at a given 1-indexed cursor position. The algorithm handles mixed segment types on the same line: column-aware segments (expressions, file refs) get precise matching, while column-less segments (plain text) serve as fallbacks when no column-specific match is found. This prevents text segments from shadowing expression segments on the same line.

Navigation module additions

resolve_include_path(bufnr) — The core resolution function. It:

  1. Gets the cursor position and finds the segment under it via find_segment_at_position
  2. Bails early if the segment isn't an expression
  3. Evaluates frontmatter to populate the environment with user variables
  4. Installs a capturing include() stub that records the resolved path without performing any I/O (no file reads, no MIME detection, no circular-include checks)
  5. Evaluates the expression via pcall(eval.eval_expression, ...) — if it calls include(), we capture the path; if it doesn't (e.g. {{ 2 + 2 }}), we return nil
  6. Returns the absolute resolved path, or nil

resolve_include_path_expr() — A thin wrapper called by Vim's includeexpr option. Returns the resolved path or falls back to vim.v.fname.

Wiring

apply_chat_buffer_settings in ui/init.lua sets includeexpr to v:lua.require("flemma.navigation").resolve_include_path_expr(). The require happens lazily at gf-press time (not at buffer setup), so navigation.lua is not added to ui/init.lua's top-level requires.

Logging

Debug and trace logging throughout resolve_include_path covering: cursor position, segment lookup, expression code, frontmatter diagnostics, include() capture details, eval failures, and resolved path. Follows the project convention of "navigation: ..." prefixed messages.

Files changed

File Change
lua/flemma/ast/nodes.lua Add end_col? integer to Position type
lua/flemma/ast/query.lua Add find_segment_at_position() with fallback pattern
lua/flemma/parser.lua Compute end_col for expression and file-reference segments
lua/flemma/navigation.lua Add resolve_include_path, resolve_include_path_expr, logging
lua/flemma/ui/init.lua Wire includeexpr buffer option
tests/flemma/ast_spec.lua 9 new specs for end_col and find_segment_at_position
tests/flemma/includeexpr_spec.lua 6 new integration specs for path resolution
tests/fixtures/include_target.txt Simple fixture for integration tests
.changeset/includeexpr-navigation.md Minor changeset

Test plan

  • make qa passes (luacheck, LuaLS types, inline-requires lint, full test suite)
  • Manual: open a .chat file, place cursor on @./some-file.txt, press gf — file opens
  • Manual: place cursor on {{ include('./some-file.txt') }}, press gf — file opens
  • Manual: place cursor on plain text, press gf — default Vim behavior (no error)
  • Manual: frontmatter defines a variable, expression uses it in include(var), gf resolves correctly

🤖 Generated with Claude Code

@StanAngeloff StanAngeloff force-pushed the feature/includeexpr branch from b87b8d4 to b378946 Compare March 9, 2026 22:27
StanAngeloff and others added 5 commits March 12, 2026 19:50
…xpressions

Enable gf / <C-w>f navigation in .chat buffers. Cursor on @./file or
{{ include('path') }} resolves the target path using the expression
evaluator with a capturing include() stub — no I/O, just path resolution.

Infrastructure changes:
- Add end_col to ast.Position and compute it in the parser for expression
  and file-reference segments
- Add find_segment_at_position() to ast/query with a fallback pattern
  that handles column-less text segments alongside column-aware expressions
- Wire includeexpr buffer option via v:lua in apply_chat_buffer_settings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tion

Instruments resolve_include_path with trace-level logging for cursor
position, segment lookup, and include() capture flow. Debug-level logs
for the resolved path and eval failures. Helps diagnose gf navigation
issues without stepping through code.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When an include result is assigned to a frontmatter variable and
referenced in an expression (e.g., {{ mod }} where mod = include(file)),
gf navigation now resolves the path by inspecting the eval result for
a SOURCE_PATH symbol rather than relying solely on capturing the
include() call.

Introduces flemma.symbols — centralized well-known table keys (analogous
to JavaScript's Symbol) for cross-module metadata tagging. Emittable
include parts are tagged with symbols.SOURCE_PATH at construction time,
making the originating file path available to any downstream consumer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Navigation was overriding env.include with a capturing stub that
duplicated path resolution logic and crashed on nil arguments.
Instead, evaluate the expression with the real include() and check
the result for symbols.SOURCE_PATH — simpler, correct, and no
behaviour divergence from normal evaluation.

Also adds a type guard to eval.lua's include() so passing a
non-string argument produces a clear diagnostic instead of crashing
on :sub().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add {, }, (, ) to buffer-local isfname so Neovim's gf extracts a
candidate covering the full {{ ... }} expression at any cursor
position — without these, gf on delimiters or whitespace within
the expression would not trigger includeexpr.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@StanAngeloff StanAngeloff merged commit f430fa1 into develop Mar 12, 2026
@StanAngeloff StanAngeloff deleted the feature/includeexpr branch March 12, 2026 18:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant