Skip to content

refactor: DRY up buf/ed/text with shared abstractions#2

Open
nez wants to merge 10 commits into
mainfrom
refactor/dry-abstractions
Open

refactor: DRY up buf/ed/text with shared abstractions#2
nez wants to merge 10 commits into
mainfrom
refactor/dry-abstractions

Conversation

@nez

@nez nez commented Apr 14, 2026

Copy link
Copy Markdown
Owner

Summary

  • Extract bounded-conj utility replacing 3 instances of capped-vector logic in buf.clj and ed.clj
  • Unify undo/redo into a single replay helper parameterized by direction (19 lines → 13)
  • Consolidate mini-history-prev/mini-history-next into mini-history-move with delta parameter (22 lines → 13)
  • Extract set-mini-text helper eliminating 4 repeated assoc-in patterns
  • Unify search-forward/search-backward via shared search* helper
  • Fix bug: undo was swapping :old/:new when pushing to redo stack, making redo produce wrong results

Test plan

  • All 125 tests pass (174 assertions), including 2,900 property-based generated cases
  • 19 new tests added: redo (3), mini-history navigation (5), search-forward/backward (11)
  • Lint clean (clj-kondo — 0 errors from changed files)
  • Redo bug fix verified by redo-after-undo and undo-redo-roundtrip tests

🤖 Generated with Claude Code

Gu Andres and others added 10 commits April 13, 2026 22:27
Extract bounded-conj utility to replace 3 instances of capped-vector
logic across buf.clj and ed.clj. Unify undo/redo into a single replay
helper parameterized by direction. Consolidate mini-history-prev/next
into mini-history-move with delta parameter. Extract set-mini-text to
eliminate 4 repeated assoc-in patterns. Unify search-forward/backward
via shared search* helper.

Fix bug: undo was swapping :old/:new when pushing to redo stack, but
redo interprets entries with opposite convention — redo never worked
correctly. New redo and undo-redo-roundtrip tests verify the fix.

Add 19 new tests: redo (3), mini-history navigation (5), search (11).
125 tests, 174 assertions, all passing. Lint clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…sion

Port the Elisp evaluator from feat/gap-buffer and extend it with the
six primitives that virtually all Elisp packages depend on:

- defvar/defcustom: declare variables with docstrings, set-if-unbound
- defmacro + backquote reader (`, ,, ,@): full macro system with
  template-based code generation via expand-backquote
- &rest/&optional argument binding for defun, defmacro, and lambda
- provide/require/load: module system with load-path search and
  circular-require guard
- save-excursion: save/restore point+mark around body
- Symbol property lists: put/get/plist-get/plist-put
- let* sequential binding
- Additional builtins: apply, funcall, mapcar, boundp, fboundp,
  1+, 1-, intern, error, string-to-number, reverse, delete-char

Bootstrap subr.el (loaded before init.el) defines standard macros:
when, unless, dolist, dotimes, prog1, prog2, push, pop, and utility
functions cadr/caddr/caar/cddr/member/assoc/assq/nthcdr.

Wire into ed.clj: M-: eval-expression, el-bindings dispatch in
handle-key, subr.el + ~/.xmas/init.el loading at startup.

Fix: cdr now returns nil (not empty list) for single-element lists,
matching Elisp semantics.

320 tests, 386 assertions, all passing. Lint clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
el.clj:
- read-delimited: unify read-list/read-vector (differ only in close char)
- read-wrapping: unify read-quote/read-backquote (differ only in symbol)
- eval-def*: unify eval-defun/eval-defmacro (differ only in target atom)
- eval-let* was identical to eval-let — removed, let* now calls eval-let
- update-buf!: extract repeated swap!/update-in buffer pattern (9 sites)
- expand-backquote-elems: dedup identical seq/vector expansion loops
- call-fn: extract resolve+dispatch pattern from apply/funcall/mapcar
- self-insert: simplify 3-way cond to single if (str works for both)

320 tests, 386 assertions, all passing. Lint clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
el.clj:
- eval-with-env: extract shared bind/persist lifecycle from
  eval-string and eval-1
- Remove unnecessary wrapper lambdas in el-forward/backward-char
  (text/next-pos already has the right signature)

ed.clj:
- mini-bindings: extract 4 inline (and (:mini s) ...) conditions
  into a data-driven map, matching the pattern used for normal bindings

320 tests, 386 assertions, all passing. Lint clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- eval-let now evaluates all init-forms before binding any variable,
  matching real Elisp let semantics. (let ((a 1) (b a)) b) now
  correctly errors with "Void variable: a" instead of returning 1.
- eval-let* restored as separate function for sequential binding.
- Splice-unquote (,@) on non-iterable values now throws a clear
  "Attempt to splice non-list" error instead of a raw ClassCastException.

3 new tests verify the fixes. 323 tests, 389 assertions, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace imperative patterns with idiomatic alternatives: set-as-predicate
for ws?, reduce+reduced for eval-and/eval-or/eval-setq, zipmap for
bind-args, mapcat for expand-backquote-elems, partition+some for
plist-get, data-driven table for bind-all, select-keys for broadcast!,
str/join for log, and str/replace with regex anchoring in dev.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ias dedup

- Replace (apply str (interpose ...)) with str/join (5 sites)
- Use constantly for zero/constant-returning stubs (8 sites)
- Extract div-opt helper for floor/ceiling/round/truncate
- Move 5 special forms from runtime cond into compile-time case in eval
- Extract named helpers for duplicate lambda builtins (5 pairs)
- Hoist loop-invariant branch in el-forward-line

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 3-6 features (uncommitted WIP):
- Bootstrap: cl-lib.el, package-stubs.el, custom-stubs.el, use-package.el, init-vars.el
- subr.el: with-current-buffer, with-temp-buffer, define-minor-mode, define-derived-mode
- buf.clj: :locals and :props (sorted-map text-property intervals + shift on edit)
- ed.clj: post-command-hook firing, fire-timers in idle, mini-cmd lookup hoisted
- term.clj: neg?/pos? predicates over comparisons
- web.clj: io alias instead of fully-qualified namespace
- 840 lines of new tests: closures, condition-case, unwind-protect, ignore-errors

Refactor + bug fixes:
- el-re-search: kill dead branch (if forward (.find m) (.find m)); fix backward
  bound semantics (Emacs treats backward bound as lower limit, not upper)
- el-mapconcat: str/join over (apply str (interpose ...))
- defvar/defconst: shared eval-defvar* helper, drop inline branch
- Move 14-line embedded init-vars heredoc out of -main into init-vars.el
- Drop redundant double coercion in 'round

475 tests pass, lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- el-var! used consistently (setq, defvar*, defvaralias, add-to-list, propertize)
- eval-save-current-buffer extracted alongside eval-save-excursion
- el-plist-put via (assoc array-map) — one-liner replaces branch+mapcat
- el-forward-line: collapse two step lambdas into a single pos-fn via buf/set-point
- push-history helper for mini-accept
- end-of-buffer: (constantly Integer/MAX_VALUE) + rely on set-point clamp
- bufferp: real impl (via el-buffer-live-p) instead of noop stub
- add-to-list: honor APPEND arg
- mapatoms: enumerate *vars* + *fns* + *macros*
- prin1-to-string: escape \r; emit readable ?\\n ?\\t ?\\r ?\\s ?\\\\ for chars

475 tests pass, lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- math1 macro: (math1 :sqrt :sin :cos :tan :asin :acos :exp) generates
  one-arg Math/X wrappers — collapses 7 fn-wrap lines into one
- div-int factory: floor/ceiling/truncate share div-opt + (long ...) wrap
- Keyword args avoid clj-kondo unresolved-symbol noise

Net: ~10 lines saved, similar entries grouped.
475 tests pass, lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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