diff --git a/racket-edit.el b/racket-edit.el index 3e8da82a..ea111512 100644 --- a/racket-edit.el +++ b/racket-edit.el @@ -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 @@ -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. @@ -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\". @@ -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 @@ -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 @@ -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. @@ -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 diff --git a/racket/command-server.rkt b/racket/command-server.rkt index 42b6cbce..ea4eab7c 100644 --- a/racket/command-server.rkt +++ b/racket/command-server.rkt @@ -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)] diff --git a/racket/commands/requires.rkt b/racket/commands/requires.rkt index 6b4113fa..b58c90be 100644 --- a/racket/commands/requires.rkt +++ b/racket/commands/requires.rkt @@ -4,15 +4,19 @@ #lang at-exp racket/base (require (only-in racket/list - append* add-between + append* + argmin cartesian-product) racket/match racket/set syntax/parse/pre (only-in syntax/stx stx-map) - "../util.rkt") + (only-in "../syntax.rkt" + file->syntax) + (only-in "../util.rkt" + safe-dynamic-require)) (provide requires/tidy requires/trim @@ -22,60 +26,18 @@ (require rackunit racket/format)) -(define (requires/tidy reqs) - ;; (-> (listof require-form?) string?) - (require-pretty-format - (tidy reqs))) +(define (requires/tidy path-str) + (tidy-file path-str)) -(module+ test - (check-equal? - (requires/tidy '((require z/z) - (require (prefix-in b: b)) - (require (combine-in)) ;=> no-op - (require (combine-in (only-in "a.rkt"))) ;=> just "a.rkt" - (require (combine-in (rename-in "b.rkt"))) ;=> just "b.rkt" - (require (combine-in (except-in "c.rkt"))) ;=> just "c.rkt" - (require (combine-in z y x "w.rkt" "x.rkt")) ;sort - (require a) - (require c d e))) - @~a{(require a - (prefix-in b: - b) - c - d - e - (combine-in x - y - z - "w.rkt" - "x.rkt") - z/z - "a.rkt" - "b.rkt" - "c.rkt") - - })) - -;; Note: Why pass in a list of the existing require forms -- why not -;; just use the "keep" list from show-requires? Because the keep list -;; only states the module name, not the original form. Therefore if -;; the original require has a subform like `(only-in mod f)` (or -;; rename-in, except-in, &c), we won't know how to preserve that -;; unless we're given it. That's why our strategy must be to look for -;; things to drop, as opposed to things to keep. -(define (requires/trim path-str reqs) - ;; (-> path-string? (listof require-form?) string?) - (require-pretty-format - (tidy reqs - #:drops (drops (analyze path-str))))) - -(define (requires/base path-str reqs) - ;; (-> path-string? (listof require-form?) string?) +(define (requires/trim path-str) + (tidy-file path-str + #:drops (drops (analyze path-str)))) + +(define (requires/base path-str) (define a (analyze path-str)) - (require-pretty-format - (tidy reqs - #:adds (adds/racket->racket/base a) - #:drops (drops a)))) + (tidy-file path-str + #:adds (adds/racket->racket/base a) + #:drops (drops a))) ;;; analysis by macro-debugger/analysis/check-requires @@ -130,7 +92,7 @@ #:when v) v)) -;;; tidy +;;; tidy-module (define current-phase-level (make-parameter 0)) (define current-drops (make-parameter null)) ; (listof (cons mod level)) @@ -575,7 +537,86 @@ (only-in "mod.rkt" oi) "z.rkt"))) -;;; pretty +;; Per each module (file root-module or submodule) +(define current-module-old-requires (make-parameter null)) + +;; Per entire file root-module +(struct delete (pos span) #:transparent) +(struct insert (pos str) #:transparent) +(define current-file-changes (make-parameter null)) + +(define-syntax-class require-statement + #:datum-literals (require) + (pattern (require _:phased-require-spec ...) + #:do + [(current-module-old-requires + (cons this-syntax + (current-module-old-requires))) + (current-file-changes + (cons (delete (syntax-position this-syntax) + (syntax-span this-syntax)) + (current-file-changes)))])) + +(define-syntax-class module-level + (pattern _:require-statement) + (pattern _:submodule-statement) + (pattern _)) + +(define-syntax-class submodule-statement + #:datum-literals (module) + (pattern (module _mod _lang ml ...) + #:do + [(parameterize ([current-module-old-requires null]) + (syntax-parse #'(ml ...) + [(_:module-level ...) + (olds->new)]))])) + +(define-syntax-class root-module + #:datum-literals (module #%module-begin) + (pattern (module _mod _lang (#%module-begin _:module-level ...)))) + +(define (tidy-file path-str #:adds [adds null] #:drops [drops null]) + (define stx (file->syntax path-str)) + (syntax-parse stx + [_:root-module + (olds->new #:adds adds #:drops drops) + ;; Sort in reverse position, with deletes sorting before inserts + ;; at the same position. + (define changes (sort (current-file-changes) + #:key (match-lambda + [(insert pos _) pos] + [(delete pos _) (+ pos 0.5)]) + >)) + ;; Return a list of changes, where delete/insert at same position + ;; are consolidated into (list 'replace pos span str). There + ;; should be no other inserts. The remaining deletes are + ;; converted to (list 'delete pos span). + (let loop ([changes changes]) + (match changes + [(list) (list)] + [(list* (delete pos span) (insert pos str) more) + (cons (list 'replace pos span str) (loop more))] + [(cons (delete pos span) more) + (cons (list 'delete pos span) (loop more))]))])) + +(define (olds->new #:adds [adds null] #:drops [drops null]) + (unless (null? (current-module-old-requires)) + (current-file-changes + (cons (insert (syntax-position (argmin syntax-position + (current-module-old-requires))) + (require-pretty-format + (tidy (sort (current-module-old-requires) + #:key syntax-position + <) + #:adds adds + #:drops drops))) + (current-file-changes))))) + +(module+ tidy-file-example + (define path-str "/home/greg/src/racket/examples/module.rkt") + (tidy-file path-str)) + +;;; format (define (require-pretty-format x) (define out (open-output-string)) @@ -586,15 +627,11 @@ (module+ test (check-equal? (require-pretty-format '(require a)) - @~a{(require a) - - }) + @~a{(require a)}) (check-equal? (require-pretty-format '(require a b)) @~a{(require a - b) - - }) + b)}) (check-equal? (require-pretty-format '(require (for-syntax a b) (for-meta 2 c d) e f)) @~a{(require (for-syntax a @@ -603,9 +640,7 @@ c d) e - f) - - }) + f)}) (check-equal? (require-pretty-format `(require (only-in m a b) (except-in m a b))) @~a{(require (only-in m @@ -613,9 +648,7 @@ b) (except-in m a - b)) - - })) + b))})) ;; Pretty print a require form. ;; @@ -665,9 +698,7 @@ (maybe-indent) (write mod-path)] [(list* (? symbol? head) this more) - (prn-form head this more) - (when (eq? head 'require) - (newline))] + (prn-form head this more)] [(text s) (maybe-indent) (display s)] @@ -688,6 +719,4 @@ [c d]) (rename-in (submod m sub) [y z] - [p q])))) - - })) + [p q]))))}))