Skip to content

Commit

Permalink
Change require tidying to work on full file syntax
Browse files Browse the repository at this point in the history
Now require tidying always happens for all modules.

The trim-requires and base-requires commands still work only on the
file's root module, because the analysis only works for that module.

As a result of this change, the front end no longer needs to gather and
supply a list of require forms. Instead it always just supplies a
pathname. The back end commands return a list of changes -- deletions
and replacements -- for the front end to make to the file/buffer.

This seems like a cleaner and more appropriate division of labor.
  • Loading branch information
greghendershott committed Jan 7, 2025
1 parent 86caa37 commit 9e357bb
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 191 deletions.
191 changes: 77 additions & 114 deletions racket-edit.el
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,7 @@
;;; requires

(defun racket-tidy-requires ()
"Make a single, sorted \"require\" form.
The scope of this command is the innermost module around point --
whether an explicit submodule form or the outermost module for a
file that has a \"#lang\" line.
Merge all require forms within that module to one form.
"Make a single, sorted \"require\" form for each module.
Use a single require-spec for each phase-level, sorted in this
order: for-syntax, for-template, for-label, for-meta, and
Expand All @@ -82,30 +76,19 @@ to m and elide (combine-in).
See also: `racket-trim-requires' and `racket-base-requires'."
(interactive)
(racket--assert-sexp-edit-mode)
(racket--tidy-requires '() #'ignore))

(defun racket--tidy-requires (add callback)
(pcase (append (racket--module-requires 'find) add)
(`() (user-error "The module has no requires; nothing to do"))
(reqs (racket--cmd/async
nil
`(requires/tidy ,reqs)
(lambda (result)
(pcase result
("" nil)
(new
(pcase (racket--module-requires 'kill)
(`()
(goto-char (racket--inside-innermost-module))
(forward-line 1))
(pos (goto-char pos)))
(let ((pt (point)))
(insert new)
(when (eq (char-before pt) ?\n)
(newline))
(indent-region pt (1+ (point)))
(goto-char pt))))
(funcall callback result))))))
(racket--tidy-requires))

(defun racket--tidy-requires (&optional callback)
"Helper for both the `racket-tidy-requires' and
`racket-add-require-for-identifier' commands."
(racket--save-if-changed)
(racket--cmd/async
nil
`(requires/tidy ,(racket--buffer-file-name))
(lambda (changes)
(racket--require-changes changes)
(when callback
(funcall callback changes)))))

(defun racket-trim-requires ()
"Like `racket-tidy-requires' but also delete unnecessary requires.
Expand All @@ -120,28 +103,20 @@ The analysis:
errors.
- Only works for requires at the top level of a source file using
#lang -- not for requires inside submodule forms. Furthermore, it
is not smart about module+ or module* forms -- it might delete
outer requires that are actually needed by such submodules.
#lang -- not for requires inside submodule forms. Furthermore,
the analysis is not smart about module+ or module* forms -- it
might delete outer requires that are actually needed by such
submodules.
See also: `racket-base-requires'."
(interactive)
(racket--assert-edit-mode)
(when (racket--submodule-y-or-n-p)
(racket--save-if-changed)
(pcase (racket--module-requires 'find t)
(`nil (user-error "The file module has no requires; nothing to do"))
(reqs (racket--cmd/async
nil
`(requires/trim
,(racket--buffer-file-name)
,reqs)
(lambda (result)
(pcase result
(`nil (user-error "Syntax error in source file"))
("" (goto-char (racket--module-requires 'kill t)))
(new (goto-char (racket--module-requires 'kill t))
(insert (concat new "\n"))))))))))
(racket--cmd/async
nil
`(requires/trim ,(racket--buffer-file-name))
#'racket--require-changes)))

(defun racket-base-requires ()
"Change from \"#lang racket\" to \"#lang racket/base\".
Expand Down Expand Up @@ -169,22 +144,44 @@ typed/racket/base\"."
(user-error "File does not use use #lang racket. Cannot change."))
(when (racket--submodule-y-or-n-p)
(racket--save-if-changed)
(let ((reqs (racket--module-requires 'find t)))
(racket--cmd/async
nil
`(requires/base
,(racket--buffer-file-name)
,reqs)
(lambda (result)
(pcase result
(`nil (user-error "Syntax error in source file"))
(new (goto-char (point-min))
(re-search-forward "^#lang.*? racket$")
(insert "/base")
(goto-char (or (racket--module-requires 'kill t)
(progn (insert "\n\n") (point))))
(unless (string= "" new)
(insert (concat new "\n"))))))))))
(racket--cmd/async
nil
`(requires/base ,(racket--buffer-file-name))
(lambda (changes)
(racket--require-changes changes)
(goto-char (point-min))
(re-search-forward "^#lang.*? racket$")
(insert "/base")))))

(defun racket--require-changes (changes)
"Process response from back end tidy/trim/base commands.
Each change is either a deletion or a replacement.
The changes are sorted from greater to smaller positions -- so
that by working backwards through the buffer, we need not worry
about shifting positions of later items.
The biggest wrinkle here is that, for esthetics, we want to
remove surrounding whitepsace when deleting. Otherwise, for
replacing, it suffices to make the change and re-indent."
(dolist (change changes)
(pcase change
(`(delete ,pos ,span)
(delete-region pos (+ pos span))
(save-excursion
(goto-char pos)
(if (save-excursion
(forward-line 0)
(looking-at "[ \t]*)" t))
(delete-indentation) ;i.e. join-line
(delete-blank-lines))))
(`(replace ,pos ,span ,str)
(delete-region pos (+ pos span))
(save-excursion
(goto-char pos)
(insert str)
(indent-region pos (point)))))))

(defun racket--submodule-y-or-n-p ()
(save-excursion
Expand All @@ -201,40 +198,6 @@ typed/racket/base\"."
(re-search-forward re)
t)))

(defun racket--module-requires (what &optional outermost-p)
"Identify all require forms and do WHAT.
When WHAT is \"find\", return the require forms.
When WHAT is \"kill\", kill the require forms and return the
position where the first one had started.
OUTERMOST-P says which module's requires: true means the
outermost file module, nil means the innermost module around
point."
(save-excursion
(goto-char (if outermost-p
(point-min)
(racket--inside-innermost-module)))
(let ((first-beg nil)
(requires nil))
(while
(condition-case _
(let ((end (progn (forward-sexp 1) (point)))
(beg (progn (forward-sexp -1) (point))))
(unless (equal end (point-max))
(when (prog1 (racket--looking-at-require-form)
(goto-char end))
(unless first-beg (setq first-beg beg))
(push (read (buffer-substring-no-properties beg end))
requires)
(when (eq 'kill what)
(delete-region beg end)
(delete-blank-lines)))
t))
(scan-error nil)))
(if (eq 'kill what) first-beg requires))))

(defun racket--inside-innermost-module ()
"Position of the start of the inside of the innermost module
around point. This could be \"(point-min)\" if point is within no
Expand All @@ -246,16 +209,13 @@ module form, meaning the outermost, file module."
(while (not (racket--looking-at-module-form))
(backward-up-list))
(down-list)
(forward-line 1)
(re-search-forward "[ \t]*" nil t)
(point))
(scan-error (point-min)))))

(defun racket--looking-at-require-form ()
;; Assumes you navigated to point using a method that ignores
;; strings and comments, preferably `forward-sexp'.
(and (eq ?\( (char-syntax (char-after)))
(save-excursion
(down-list 1)
(looking-at-p "require"))))
(scan-error
(goto-char (point-min))
(or (re-search-forward "^#lang .+?\n$" nil t)
(point-min))))))

(defun racket-add-require-for-identifier ()
"Add a require for an identifier.
Expand Down Expand Up @@ -285,16 +245,19 @@ identifiers that are exported but not documented."
(racket--assert-sexp-edit-mode)
(when-let (result (racket--describe-search-completing-read))
(pcase-let* ((`(,term ,_path ,_anchor ,lib) result)
(req `(require ,(intern lib))))
(req (format "(require %s)" lib)))
(unless (equal (racket--thing-at-point 'symbol) term)
(insert term))
(let ((pt (copy-marker (point))))
(racket--tidy-requires
(list req)
(lambda (result)
(goto-char pt)
(when result
(message "Added \"%s\" and did racket-tidy-requires" req))))))))
(save-excursion
(goto-char (racket--inside-innermost-module))
(insert req)
(newline-and-indent)
(let ((pt (copy-marker (point))))
(racket--tidy-requires
(lambda (result)
(goto-char pt)
(when result
(message "Added %S and did racket-tidy-requires" req)))))))))

;;; align

Expand Down
6 changes: 3 additions & 3 deletions racket/command-server.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,9 @@
[`(macro-stepper ,path ,str ,pol) (macro-stepper path str pol)]
[`(macro-stepper/next ,what) (macro-stepper/next what)]
[`(module-names) (module-names)]
[`(requires/tidy ,reqs) (requires/tidy reqs)]
[`(requires/trim ,path-str ,reqs) (requires/trim path-str reqs)]
[`(requires/base ,path-str ,reqs) (requires/base path-str reqs)]
[`(requires/tidy ,path-str) (requires/tidy path-str)]
[`(requires/trim ,path-str) (requires/trim path-str)]
[`(requires/base ,path-str) (requires/base path-str)]
[`(doc-search ,prefix) (doc-search prefix)]
[`(hash-lang . ,more) (apply hash-lang more)]
[`(pkg-list) (package-list)]
Expand Down
Loading

0 comments on commit 9e357bb

Please sign in to comment.