diff --git a/evil-commands.el b/evil-commands.el index e6c1c10f..eec70386 100644 --- a/evil-commands.el +++ b/evil-commands.el @@ -4228,45 +4228,109 @@ Default position is the beginning of the buffer." (message "%d lines --%s--" nlines perc)))) (defvar sort-fold-case) -(evil-define-operator evil-ex-sort (beg end &optional options reverse) +(evil-define-operator evil-ex-sort (beg end &optional args reverse) "The Ex sort command. -\[BEG,END]sort[!] [i][u] +\[BEG,END]sort[!] [/PATTERN/] [b][i][u][r][n][x][o] The following additional options are supported: * i ignore case * u remove duplicate lines + * r sort the contents of pattern + * n sort by the first decimal number + * x sort by the first hexadecimal number (with optional \"0x\" prefix) + * o sort by the first octal number + * b sort by the first binary number -The 'bang' argument means to sort in reverse order." +If a pattern is supplied without supplying the \"r\" option, sort +the contents of the lines after skipping the pattern. +If the pattern is empty, the last search pattern is used instead. + +The \"!\" argument means to sort in reverse order." :motion mark-whole-buffer :move-point nil (interactive "") - (let ((beg (copy-marker beg)) - (end (copy-marker end)) - sort-fold-case uniq) - (dolist (opt (append options nil)) - (cond - ((eq opt ?i) (setq sort-fold-case t)) - ((eq opt ?u) (setq uniq t)) - (t (user-error "Unsupported sort option: %c" opt)))) - (sort-lines reverse beg end) - (when uniq - (let (line prev-line) - (goto-char beg) - (while (and (< (point) end) (not (eobp))) - (setq line (buffer-substring-no-properties - (line-beginning-position) - (line-end-position))) - (if (and (stringp prev-line) - (eq t (compare-strings line nil nil - prev-line nil nil - sort-fold-case))) - (delete-region (progn (forward-line 0) (point)) - (progn (forward-line 1) (point))) - (setq prev-line line) - (forward-line 1))))) - (goto-char beg) - (set-marker beg nil) - (set-marker end nil))) + (unless args (setq args "")) + (let ((inhibit-field-text-motion t) + options sort-fold-case unique base sort-pat pat) + ;; Handle arguments like + ;; /[^,]*,/ n + ;; and + ;; nu + (if (or (zerop (length args)) (memq (aref args 0) '(?i ?n ?x ?o ?b ?u ?r))) + (setq options args) + (setq args (evil-delimited-arguments args 2) + ;; Use last search pattern when an empty pattern is provided + pat (if (string= (car args) "") + (evil-ex-pattern-regex evil-ex-search-pattern) + (car args)) + options (cadr args))) + (cl-loop + for opt across options do + (cond + ((eq opt ?i) (setq sort-fold-case t)) + ((eq opt ?b) (setq base 2)) + ((eq opt ?o) (setq base 8)) + ((eq opt ?n) (setq base 10)) + ((eq opt ?x) (setq base 16)) + ((eq opt ?r) (setq sort-pat t)) + ((eq opt ?u) (setq unique t)) + ((eq opt ? )) + (t (user-error "Invalid sort option `%c'" opt)))) + (evil-with-restriction beg end + (goto-char beg) + (let ((num-re + (cond + ((null base) nil) + ((= base 2) "[01]+") + ((= base 8) "[0-7]+") + ((= base 10) "-?[0-9]+") + ((= base 16) "\\(-\\)?\\(?:0x\\)?\\([0-9a-f]+\\)"))) + key-end) + (sort-subr + reverse + #'forward-line + #'end-of-line + (lambda () + ;; Find the boundary of the key to match on the line + (setq key-end (line-end-position)) + (and (> (length pat) 0) + ;; When matching a pattern and one doesn't exist on the line, + ;; skip the line + (re-search-forward pat key-end 'move) + sort-pat ; Otherwise go to the start of the key + (progn (setq key-end (point)) + (goto-char (match-beginning 0)))) + ;; Return the key for the line when sorting numbers, otherwise let + ;; `sort-subr' extract the key + (when base + (let ((case-fold-search t)) + (if (not (re-search-forward num-re key-end t)) + ;; When sorting numbers and a number doesn't exist on the + ;; line, place it above all the numeric lines + most-negative-fixnum + (let ((num (string-to-number + (buffer-substring + (match-beginning (if (= base 16) 2 0)) + (match-end 0)) + base))) + (if (and (= base 16) (match-beginning 1)) + (- num) + num)))))) + ;; Only called when sorting lexicographically + (lambda () (goto-char key-end)))) + (when unique + (goto-char (point-min)) + (let ((case-fold-search sort-fold-case) + prev-line-beg) + (while (not (eobp)) + (if (and prev-line-beg + (eq 0 (compare-buffer-substrings + nil prev-line-beg (1- (point)) + nil (point) (line-end-position)))) + (delete-region (point) (line-beginning-position 2)) + (setq prev-line-beg (point)) + (forward-line))))))) + (goto-char beg)) ;;; Window navigation diff --git a/evil-tests.el b/evil-tests.el index edf728d1..159e3a74 100644 --- a/evil-tests.el +++ b/evil-tests.el @@ -8781,7 +8781,40 @@ Source (evil-test-buffer "[z]zyy\ntest\ntEst\ntesT\nTEST\ntest\n" (":sort iu") - "[t]est\nzzyy\n"))) + "[t]est\nzzyy\n")) + (ert-info ("pattern sort") + (evil-test-buffer + "[t]e|z|t\nzz|a|y\n" + (":sort /|[a-z]|/ r") + ("zz|a|y\nte|z|t\n")) + (evil-test-buffer + "[a],b\nb,a\n" + (":sort /[^,]*,/") + "b,a\na,b\n") + (evil-test-buffer + "[a],b\nb,a\n" + ("/," [return] ":sort //") + "b,a\na,b\n")) + (ert-info ("numeric sort") + (ert-info ("decimal") + (evil-test-buffer + "27\n027\n2\na\n1\n" + (":sort n") + "a\n1\n2\n27\n027\n")) + (ert-info ("octal") + (evil-test-buffer + "9\n8\n" + (":sort o") + "9\n8\n") + (evil-test-buffer + "777\n776\n7239\n" + (":sort o") + "7239\n776\n777\n")) + (ert-info ("hexadecimal") + (evil-test-buffer + "0xae\n0xb\nae\nad\n" + (":sort x") + "0xb\nad\n0xae\nae\n")))) ;;; Command line window