Skip to content

Unify CLI and editor semantic navigation - #107

Merged
JesseHerrick merged 5 commits into
mainfrom
unify-semantic-navigation
Sep 25, 2026
Merged

JesseHerrick merged 5 commits into
mainfrom
unify-semantic-navigation

Conversation

@JesseHerrick

@JesseHerrick JesseHerrick commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Summary

  • extract protocol-neutral name lookup and reference operations from the LSP handlers
  • route CLI daemon methods through the shared language service for use-chain, generated-symbol, delegate, and bare-call accuracy
  • support targeted reindexing of deleted paths and validate project marker types
  • require a real Dexter database instead of an empty .dexter directory

Testing

  • make test
  • make lint
  • go test -race ./internal/lsp ./internal/daemon ./internal/workspace

Note

Medium Risk
Touches core navigation, daemon contract version 2 (forces daemon replacement), and reference resolution on large codebases; behavior changes are intentional but wide-reaching for CLI and editor parity.

Overview
CLI lookup and references now use the same semantic engine as the editor — shared LookupName / ReferenceNames on the headless LSP service cover use chains, injected aliases, generated BEAM symbols, delegates, and bare in-module calls; duplicate hits on one line are deduped. lookup --strict maps to ExactModule so generated-only modules do not fall back to a parent’s source. Go-to-definition and find-references in the LSP delegate into the same helpers.

Daemon wire protocol bumps ContractVersion to 2 — lookup/reference payloads dedupe file paths via a file table so large reference lists stay under the 16 MiB line cap; oversize responses fail only that call, and oversized workspace/changed notifications downgrade to a full change.

Reindex and workspace guards — reindexing a deleted file or directory prunes the index; mistyped paths report “nothing there” via Store.HasPath / ReindexResult.Missing. Project detection requires a real mix.exs, .git, or .dexter/dexter.db, not an empty .dexter folder; deleted reindex targets resolve the workspace from the nearest existing ancestor.

Performance and monorepo BEAMs — find-references through injected aliases tokenizes only files that already have candidate refs (large monorepo fix). Generated-function lookup scans other compiled Mix projects in the workspace and picks the newest BEAM when a path-dep library has no local _build.

Debug logging — session debug lines go to the editor as window/logMessage; DEXTER_DEBUG applies when the headless server is constructed (CLI/daemon without initialize).

Reviewed by Cursor Bugbot for commit cd71354. Bugbot is set up for automated code reviews on this repo. Configure here.

JesseHerrick and others added 5 commits September 22, 2026 20:35
injectedAliasRefs collected use sites by tokenizing every file that uses
an injector before checking whether it held a candidate reference. On a
large umbrella that is nearly every file: a references call for
Tiger.Repo.get tokenized ~19,900 files and allocated ~2.4 GB to keep the
~200 that matter. Fetch the candidate references first and read use sites
only in those files; consumerColumns never consulted any other file, so
results are unchanged.

scanRefStream also walked every token from the start of the file for each
line it checked. Both token streams are in byte order, so binary search to
the line's first token instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A hot references result such as Tiger.Factory.insert (98k locations)
encoded to ~19 MB, past the 16 MiB line limit. The daemon wrote it anyway,
so the client's reader gave up on the connection and every other call
multiplexed on it failed too.

Send lookup and references locations as a file table plus entries that
index into it, in their original order. Each path is sent once, which
brings that result to ~4 MB. The Go API of LookupResult and
ReferencesResult is unchanged; decoding rejects a file index outside the
table.

writeJSONLine now refuses a line the reader would reject before writing
anything, so the stream stays in step. An oversized control result fails
only its own call, and a workspace/changed notification with too many
paths is sent as a full change, as the runtime already does when a
subscriber drops changes.

The encoding is a breaking wire change, so ContractVersion is bumped to 2.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A deleted reindex target resolved its workspace from the missing path
itself, so outside a project the error named the deleted file as the
workspace, and -y tried to start a daemon in a directory that does not
exist. The search now starts from the nearest existing ancestor.

A mistyped reindex path printed "Reindexed" like a real one. The daemon
now checks, before pruning, whether a missing target was indexed as a
file or a directory, and the CLI says when there was nothing there. It
still exits 0: the index already matches the disk, and the watcher may
have pruned a deleted file first.

lookup --strict could answer a generated function with the defmodule
line of a lexical parent, which is a fallback in a different module.
The new ExactModule option, set only for strict lookups, keeps generated
results to the module itself; editors keep the parent fallback that
BEAM-only modules such as route helpers rely on.

The daemon's headless language service never receives initialize, so
DEXTER_DEBUG had no effect for CLI and MCP calls. It is now also read
when the server is constructed.

A new test keeps references through an injected alias from growing with
the files that use the injector without naming the target (0.5 MB for
400 such files, against 77 MB before the candidate-first change).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A library that another Mix project compiles as a path dependency has no
_build of its own: tiger builds libs/* inside apps/tiger. Generated
functions for its modules, and for modules generated beneath them, were
looked up in a build that does not exist. A module without source also
took the workspace root as its build, so EORWeb.Router.Helpers was never
found.

Modules without source now take their nearest lexical parent's source as
the hint that places them. When the hinted build has no BEAM, or the
workspace holds several builds, the builds found in the workspace are
asked for the owning project's application (read from its mix.exs), and
the most recently compiled BEAM wins. A single build keeps the existing
path unchanged.

With the workspace daemon, debug lines only reached the daemon's log.
Each editor session now forwards its own lines to its editor as
window/logMessage through a bounded queue that drops lines rather than
block a request on a slow editor, and reports how many it dropped.

README and CHANGELOG describe where logs go and the CLI's semantic
navigation; the issue checklist names `dexter version`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@JesseHerrick
JesseHerrick merged commit cd099f3 into main Sep 25, 2026
5 checks passed
@JesseHerrick
JesseHerrick deleted the unify-semantic-navigation branch September 25, 2026 02:26
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