Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ jobs:
run: |
go build -C "$GITHUB_WORKSPACE/../let-go" -o "$GITHUB_WORKSPACE/../lg" .

# lg goes on PATH too: the jack-in tests start a real `lg -n` server
# the same way the editor does.
- name: Run test suite
run: |
"$GITHUB_WORKSPACE/../lg" test/run.lg
PATH="$GITHUB_WORKSPACE/..:$PATH" "$GITHUB_WORKSPACE/../lg" test/run.lg

# Full AOT native build (build lg, lower legmacs.*, bundle, go build) -- the
# same path release.yml ships. Guards against a build break landing on main.
Expand Down
2 changes: 1 addition & 1 deletion .let-go-version
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
# May be a commit SHA (reproducible) or a branch name like `main` (tracks tip).
# legmacs's AOT build needs the gogen lowering + scripts/lg-compile tooling,
# which live on main; bump this SHA when you want newer let-go fixes.
a6657612002bd70b277617368985639992d1c11b
36d57e3693075f9e63e4aff49b220331ff3fea12
50 changes: 44 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,12 @@ lg test/run.lg
Run a single test namespace (faster iteration than the full suite):

```sh
lg -e "(require '[test :refer [run-tests]] '[test.region-test]) (run-tests)"
lg -e "(require '[test :refer [run-tests]] '[test.region-test]) (run-tests 'test.region-test)"
```

(`run-tests` with no arguments tests only `*ns*`, like Clojure's, so name
the namespace -- a bare `(run-tests)` reports 0 tests and success.)

There's no separate build/lint step — `lg` interprets `.lg` source directly.
`test/run.lg` is a hand-maintained list of every test namespace; adding a
new `test/*_test.lg` file means adding its namespace to that `:require`
Expand Down Expand Up @@ -254,11 +257,46 @@ form at point, highlights it, snapshots its source, and evaluates it through
`bufs/spawn-task`. Completion verifies that the source span is unchanged
before replacing it. Ordinary values use `pr-str`; functions that need
source-aware application return `replacement-result` with a registered
result-kind handler. This async/result seam is also the intended boundary
for a future nREPL backend: `register-eval-backend!` installs an async-safe
`(fn [request] -> value)` and `use-eval-backend!` selects it. Requests carry
the source plus filename/column/namespace context; backends never touch live
buffer state.
result-kind handler. The same seam carries every other eval command: all four build one
request (`:op` `:eval`/`:load`, `:source`, `:forms`, `:state`, `:backend`,
and filename/directory/line/column/namespace `:context`) and hand it to
the buffer's backend. Which backend is `letgo/eval-backend-for`: the
first registered resolver that claims the buffer, else the global
`current-eval-backend` -- resolvers run every frame (the lighter), so they
must stay string checks, no filesystem. With `:in-process`,
`C-x C-e`/`C-j`/`C-c C-e` still eval synchronously on the main thread (so
editor-scripting forms take effect before the next frame); any other
backend makes them `spawn-task` jobs too. Remote backends return
`printed-result` (text the server printed, plus captured output) since
the value never existed in this VM. Completion
(`legmacs.modes.letgo-complete`, TAB/M-TAB) routes the same way through a
per-backend `completers` registry, and so do eldoc, doc and `M-.`
(`legmacs.modes.letgo-lookup`, a `lookups` registry of `(fn [request] ->
info-map)`). let-go-mode's single `:after-command` now runs
paren-matching and then `letgo/after-command-hooks` (eldoc is one), each
in a try so a failing hook can't break editing. eldoc runs every key, so
it scans only back to the enclosing top-level form (a `(` in column 0)
and reads the ns form from the buffer's first 80 lines; remote answers
come from a short-TTL cache, fetched by a quiet `spawn-task`. Goto uses
a `:visit` buffer-command (show buffer by `:id`/`:filename`/`:name`, or
open `:state`, at `:row`/`:col`) and a shared `:xref-stack` for `M-,`.

`legmacs.nrepl` is the nREPL backend: one connection per project root
(buffers with a `:filename` under it resolve to it; fileless buffers like
`*scratch*` never do), one blocking bencode round trip per request,
serialized by a channel-token lock, inside the job's future -- so no
long-lived reader goroutine exists for `loop-wait` to keep polling.
`legmacs.jack-in` starts servers from data launch configs. let-go
interop can't reach `cmd.Process`, so a server runs under `sh -c` that
writes a pid file, redirects output to a log (an unread pipe would fill
and block it) and `exec`s the command; the editor holds its stdin pipe
open (`lg -n` exits on EOF) and stops it with SIGTERM + closing stdin.
Liveness is `ps -o stat=`, not `kill -0`: an exited child is a zombie
until Waited, and `kill -0` says a zombie is alive. `bufs/add-exit-hook!`
hooks run after `main.lg` restores the terminal; both namespaces use it
so no server outlives the editor. The editor picks the port
(`os/free-port`) rather than reading `.nrepl-port`, which Babashka
doesn't write for port 0. Backends never touch live buffer state.

`legmacs.vibe/vibe` uses that protocol rather than owning a special key
command. Under the dynamic `letgo/*eval-replace-context*` it derives its
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ started:
form (or select several), evaluate it without blocking the editor, and
replace the source with its result; the active form is highlighted while
it runs
- CIDER-style nREPL: `C-c M-j` starts and connects the project's server
-- lgx, Clojure CLI (with cider-nrepl), Babashka, or plain `lg` -- and
that project's buffers then evaluate and complete there; the server
stops when you disconnect or quit
- symbol completion on `TAB`/`M-TAB`, eldoc arglists as you type, doc
lookup (`C-c C-d d`) and goto-definition (`M-.`/`M-,`, into jars for
Clojure libraries), all from the editor's own VM or the connected server
- a dedicated `*repl*` buffer (`C-c C-z`), for when you want an actual
running transcript instead of building one by hand with `C-j`
- `(vibe "...")`: write what you want where you want it, then use the same
Expand Down
17 changes: 13 additions & 4 deletions build/go.mod
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
module legmacs/build

go 1.26
go 1.27

require github.com/nooga/let-go v0.0.0-00010101000000-000000000000

require (
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/ebitengine/gomobile v0.0.0-20260820040257-d11f821a26a6 // indirect
github.com/ebitengine/hideconsole v1.0.0 // indirect
github.com/ebitengine/purego v0.11.0 // indirect
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20260823155953-d41da22a9587 // indirect
github.com/hajimehoshi/ebiten/v2 v2.10.2 // indirect
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/zeebo/bencode v1.0.0 // indirect
github.com/zeebo/xxh3 v1.1.0 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/term v0.41.0 // indirect
golang.org/x/image v0.46.0 // indirect
golang.org/x/sync v0.23.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.org/x/term v0.46.0 // indirect
golang.org/x/text v0.42.0 // indirect
)

replace github.com/nooga/let-go => ../../let-go
42 changes: 28 additions & 14 deletions build/go.sum
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/ebitengine/gomobile v0.0.0-20260820040257-d11f821a26a6 h1:Tnc3YtzxhgsvNdNrER9wWkGJbyjOwyUuzjUY5rZK72k=
github.com/ebitengine/gomobile v0.0.0-20260820040257-d11f821a26a6/go.mod h1:gwnFEwdzWZpNehgwkeK4756Ez58f58bXz6bgEAq+xqk=
github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE=
github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A=
github.com/ebitengine/purego v0.11.0 h1:jhp/D+Nyv7UUW8HAcmcjt2N2rYrYi9m3SL21k0Ua/NI=
github.com/ebitengine/purego v0.11.0/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ=
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 h1:IO5P06Pcj9K04d+l4nrf3c2U56+dAotIFG6u4P1wAHI=
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20260823155953-d41da22a9587 h1:yzPGEmWIlLQvQ0HvNHpRzLwyJ3pAmVXpa6pGclnH9Ks=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20260823155953-d41da22a9587/go.mod h1:SyRD8YfuKk+ZXlDqYiqe1qMSqjNgtHzBTG810KUagMc=
github.com/hajimehoshi/ebiten/v2 v2.10.2 h1:uNMOpfb/pW7GQRMsSn/pxwrk9rNcgSaIdjKwFZS11LM=
github.com/hajimehoshi/ebiten/v2 v2.10.2/go.mod h1:47QNgyS/y2ZRkjVUvlGLx8a+F7MSjcn8/GsjcCZ9Rc8=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/bencode v1.0.0 h1:zgop0Wu1nu4IexAZeCZ5qbsjU4O1vMrfCrVgUjbHVuA=
github.com/zeebo/bencode v1.0.0/go.mod h1:Ct7CkrWIQuLWAy9M3atFHYq4kG9Ao/SsY5cdtCXmp9Y=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
Loading
Loading