[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[nongnu] elpa/haskell-tng-mode 387aa18 203/385: more efficient layout al
From: |
ELPA Syncer |
Subject: |
[nongnu] elpa/haskell-tng-mode 387aa18 203/385: more efficient layout algorithm |
Date: |
Tue, 5 Oct 2021 23:59:31 -0400 (EDT) |
branch: elpa/haskell-tng-mode
commit 387aa18c79e17247c78f46f67d85d62082d84e26
Author: Tseen She <ts33n.sh3@gmail.com>
Commit: Tseen She <ts33n.sh3@gmail.com>
more efficient layout algorithm
---
haskell-tng-layout.el | 177 +++++++++++++++++++++++++++-------------
haskell-tng-lexer.el | 5 --
test/haskell-tng-layout-test.el | 43 ++++++++--
3 files changed, 153 insertions(+), 72 deletions(-)
diff --git a/haskell-tng-layout.el b/haskell-tng-layout.el
index 08fe7ce..1f49b78 100644
--- a/haskell-tng-layout.el
+++ b/haskell-tng-layout.el
@@ -19,26 +19,25 @@
;; Notes on caching
;;
-;; Small brain is to parse the entire buffer, invalidated on any change.
+;; Small brain parses the entire buffer, invalidated by any change.
;;
-;; Big brain would store a record of the region that has been edited and
reparse
-;; only the layouts that have changed. The invalidation may be a simple case of
-;; dismissing everything (including CLOSE parts) after any point that has been
-;; edited or trying to track insertions.
+;; Big brain parses toplevel regions of interest, invalidated by any changes.
+;; This is what we do.
;;
;; Galaxy brain caching would use properties and put dirty markers on inserted
-;; or deleted regions. Also this could give lightning fast lookup at point on
-;; cache hits.
-;;
-;; Anything more complicated that small brain needs improved testing.
+;; or deleted regions. Would give fast lookup at point.
(require 'dash)
(require 'haskell-tng-util)
-;; Easiest cache... full buffer parse with full invalidation on any insertion.
+;; A alist of lists of (OPEN . (CLOSE . SEPS)) positions, keyed by (START .
END)
+;;
+;; Regions are exclusive of START and inclusive of END, and do not overlap.
This
+;; is because START would only ever contain CLOSE or SEP, not OPEN.
;;
-;; A list of (OPEN . (CLOSE . SEPS)) positions, one per inferred block.
+;; Instead of a list, may also be t, indicating that there is no relevant
layout
+;; in a region.
(defvar-local haskell-tng--layout-cache nil)
(defun haskell-tng--layout-cache-invalidation (_beg _end _pre-length)
@@ -56,61 +55,124 @@ the layout engine."
Haskell2010 Layout rules.
Designed to be called repeatedly, managing its own caching."
- (unless haskell-tng--layout-cache
- (haskell-tng--layout-rebuild-cache-full))
-
- (let ((pos (point))
- opens breaks closes)
- (dolist (block haskell-tng--layout-cache)
- (let ((open (car block))
- (close (cadr block))
- (lines (cddr block)))
- (when (and (<= open pos) (<= pos close))
- (when (= open pos)
- (push "{" opens))
- (when (= close pos)
- (push "}" closes))
- (dolist (line lines)
- (when (= line pos)
- (push ";" breaks))))))
- (append opens closes breaks)))
+ (when-let (cache (haskell-tng--layout-at-point))
+ (let ((pos (point))
+ opens breaks closes)
+ (dolist (block cache)
+ (pcase block
+ (`(,open . (,close . ,seps))
+ (when (and open (= open pos))
+ (push "{" opens))
+ (when (and close (= close pos))
+ (push "}" closes))
+ (dolist (sep seps)
+ (when (= sep pos)
+ (push ";" breaks))))))
+ (append opens closes breaks))))
(defun haskell-tng--layout-has-virtual-at-point ()
"t if there is a virtual at POINT"
;; avoids a measured performance hit (append indentation)
- (unless haskell-tng--layout-cache
- (haskell-tng--layout-rebuild-cache-full))
- (--any (member (point) it)
- haskell-tng--layout-cache))
-
-(defun haskell-tng--layout-rebuild-cache-full ()
- (let (case-fold-search
- cache)
- (save-excursion
- (goto-char 0)
- (while (not (eobp))
- (when-let (wldo (haskell-tng--layout-next-wldo))
- (push wldo cache))))
- (setq haskell-tng--layout-cache (reverse cache))))
-
-(defun haskell-tng--layout-next-wldo ()
+ (when-let (cache (haskell-tng--layout-at-point))
+ (--any (member (point) it) cache)))
+
+(defun haskell-tng--layout-at-point ()
+ "Returns the relevant virtual tokens for the current point,
+using a cache if available."
+ (when-let
+ (layout (or
+ (cdr (--find
+ (and (< (caar it) (point))
+ (<= (point) (cdar it)))
+ haskell-tng--layout-cache))
+ (haskell-tng--layout-rebuild-cache-at-point)))
+ (unless (eq layout t) layout)))
+
+(defun haskell-tng--layout-rebuild-cache-at-point ()
+ (let ((toplevel (rx bol (or word-start "("))))
+ (if (and (looking-at toplevel) (not (bobp)))
+ ;; min is exclusive, so go back one.
+ (save-excursion
+ (forward-char -1)
+ (haskell-tng--layout-rebuild-cache-at-point))
+ (let* ((min
+ (save-excursion
+ (end-of-line 1)
+ (or (re-search-backward toplevel nil t) 0)))
+ (max
+ (save-excursion
+ (end-of-line 1)
+ (or (and (re-search-forward toplevel nil t)
+ (match-beginning 0))
+ (point-max))))
+ (module
+ (save-excursion
+ (goto-char min)
+ (looking-at (rx word-start "module" word-end))))
+ (before-module
+ (save-excursion
+ (goto-char max)
+ (looking-at (rx word-start "module" word-end))))
+ case-fold-search
+ cache)
+
+ ;; `module ... where { ... }' special cases:
+ ;;
+ ;; 1. before module, nothing
+ ;; 2. after module, only an open
+ ;; 3. eob, extra close
+ ;; 4. everywhere else, extra sep
+ (when module
+ (push `(,max nil) cache))
+ (when (not (or module before-module))
+ (if (eq max (point-max))
+ (push `(nil ,max) cache)
+ (push `(nil nil ,max) cache))
+ (save-excursion
+ (goto-char min)
+ (while (< (point) max)
+ (when-let (wldo (haskell-tng--layout-next-wldo max))
+ (push wldo cache)))))
+
+ ;; TODO remove this sanity check when we are happy
+ ;; a sanity check that all points are within the bounds
+ (cl-flet ((good (type p)
+ (when (and p (or (<= p min) (< max p)))
+ (message "BUG: LAYOUT %S at %S" type p))))
+ (dolist (block cache)
+ (pcase block
+ (`(,open . (,close . ,seps))
+ (good 'OPEN open)
+ (good 'CLOSE close)
+ (dolist (sep seps)
+ (good 'SEP sep))))))
+
+ (let ((key (cons min max))
+ (value (or (reverse cache) t)))
+ (push (cons key value) haskell-tng--layout-cache)
+ value)))))
+
+(defun haskell-tng--layout-next-wldo (limit)
(catch 'wldo
- (while (not (eobp))
- (forward-comment (point-max))
+ (while (< (point) limit)
+ (forward-comment limit)
(cond
((looking-at (rx symbol-start
(| "\\case" ;; LambdaCase
"where" "let" "do" "of")
word-end))
(goto-char (match-end 0))
- (forward-comment (point-max))
+ (forward-comment limit)
(when (not (looking-at "{"))
- (throw 'wldo (haskell-tng--layout-wldo))))
+ (throw 'wldo (haskell-tng--layout-wldo
+ (min (or (haskell-tng--util-paren-close) (point-max))
+ limit)))))
(t (skip-syntax-forward "^-"))))))
-(defun haskell-tng--layout-wldo ()
- "A list holding virtual `{', then `}', then virtual `;' in order.
+(defun haskell-tng--layout-wldo (limit)
+ "A list holding virtual `{', then `}', then virtual `;' in
+order between point and LIMIT.
Assumes that point is at the beginning of the first token after a
WLDO that is using the offside rule."
@@ -118,21 +180,20 @@ WLDO that is using the offside rule."
(let* ((open (point))
seps
(level (current-column))
- (limit (or (haskell-tng--util-paren-close) (point-max)))
(close (catch 'closed
- (while (not (eobp))
+ (while (< (point) limit)
(forward-line)
- (forward-comment (point-max))
- (when (and (= (current-column) level)
- (not (eobp))
+ (forward-comment limit)
+ (when (and (< (point) limit)
+ (= (current-column) level)
(not (looking-at
(rx bol (or "," ")" "]" "}")))))
(push (point) seps))
- (when (< limit (point))
+ (when (<= limit (point))
(throw 'closed limit))
(when (< (current-column) level)
(throw 'closed (point))))
- (point-max))))
+ limit)))
`(,open . (,close . ,(reverse seps))))))
(provide 'haskell-tng-layout)
diff --git a/haskell-tng-lexer.el b/haskell-tng-lexer.el
index d21701a..5f6eda0 100644
--- a/haskell-tng-lexer.el
+++ b/haskell-tng-lexer.el
@@ -84,9 +84,6 @@ the lexer."
(forward-comment (point-max))
- ;; TODO: performance. Only request virtuals when they make sense...
- ;; e.g. on newlines, or following a WLDO (assuming a comment-aware
- ;; lookback is fast).
(setq haskell-tng--lexer-state
(unless haskell-tng--lexer-state
(haskell-tng--layout-virtuals-at-point)))
@@ -159,8 +156,6 @@ the lexer."
(setq haskell-tng--lexer-state
(unless haskell-tng--lexer-state
- ;; TODO semicolon cannot be used as a separator and a line
end
- ;; in the grammar rules, so should we emit multiple tokens?
(haskell-tng--layout-virtuals-at-point)))
(if haskell-tng--lexer-state
diff --git a/test/haskell-tng-layout-test.el b/test/haskell-tng-layout-test.el
index 7c6f485..c1a94a3 100644
--- a/test/haskell-tng-layout-test.el
+++ b/test/haskell-tng-layout-test.el
@@ -13,28 +13,34 @@
(ert-deftest haskell-tng-layout-file-tests:layout ()
;; the Haskell2010 test case
- (should (have-expected-layout (testdata "src/layout.hs"))))
+ (should (have-expected-layout (testdata "src/layout.hs")))
+ (should (have-expected-layout-reverse (testdata "src/layout.hs"))))
(ert-deftest haskell-tng-layout-file-tests:indentation ()
- (should (have-expected-layout (testdata "src/indentation.hs"))))
+ (should (have-expected-layout (testdata "src/indentation.hs")))
+ (should (have-expected-layout-reverse (testdata "src/indentation.hs"))))
(ert-deftest haskell-tng-layout-file-tests:medley ()
- (should (have-expected-layout (testdata "src/medley.hs"))))
+ (should (have-expected-layout (testdata "src/medley.hs")))
+ (should (have-expected-layout-reverse (testdata "src/medley.hs"))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Testing utilities
-(defun haskell-tng--layout-test-parse-to-string ()
- (goto-char 0)
+(defun haskell-tng--layout-test-parse-to-string (&optional reverse)
+ (if reverse
+ (goto-char (point-max))
+ (goto-char 0))
(let (tokens exit)
(while (not exit)
(when-let (virtuals (haskell-tng--layout-virtuals-at-point))
(push (s-join "" virtuals) tokens))
- (if (eobp)
+ (if (or (and (not reverse) (eobp))
+ (and reverse (bobp)))
(setq exit t)
- (push (string (char-after)) tokens)
- (forward-char)))
- (s-join "" (reverse tokens))))
+ (push (string (if reverse (char-before) (char-after))) tokens)
+ (forward-char (if reverse -1 1))))
+ (s-join "" (if reverse tokens (reverse tokens)))))
(defun have-expected-layout (file)
(haskell-tng--testutils-assert-file-contents
@@ -42,6 +48,25 @@
#'haskell-tng-mode
#'haskell-tng--layout-test-parse-to-string
"layout"))
+(defun have-expected-layout-reverse (file)
+ (haskell-tng--testutils-assert-file-contents
+ file
+ #'haskell-tng-mode
+ (lambda () (haskell-tng--layout-test-parse-to-string t))
+ "layout"))
+
+(ert-deftest haskell-tng-layout-cache-ordering-tests ()
+ (with-temp-buffer
+ (insert-file-contents (testdata "src/layout.hs"))
+ (haskell-tng-mode)
+
+ ;; Differs from the regular layout test because the cache is empty. We
could
+ ;; do a more intensive version of this by randomly sampling the points.
+ (goto-char 94)
+ (should
+ (equal
+ (haskell-tng--layout-virtuals-at-point)
+ '("{")))))
(ert-deftest haskell-tng-layout-cache-invalidation-tests ()
(with-temp-buffer
- [nongnu] elpa/haskell-tng-mode c211672 174/385: indentation for hanging type definition, (continued)
- [nongnu] elpa/haskell-tng-mode c211672 174/385: indentation for hanging type definition, ELPA Syncer, 2021/10/06
- [nongnu] elpa/haskell-tng-mode ef7f335 171/385: note stack compatibility, ELPA Syncer, 2021/10/06
- [nongnu] elpa/haskell-tng-mode 86bcd98 175/385: constraint indentation, ELPA Syncer, 2021/10/06
- [nongnu] elpa/haskell-tng-mode e1c0f0a 179/385: allow indentation option to be tested, ELPA Syncer, 2021/10/06
- [nongnu] elpa/haskell-tng-mode 1cceb64 182/385: cleaner tests, ELPA Syncer, 2021/10/06
- [nongnu] elpa/haskell-tng-mode b1122e4 188/385: allow users to use unsupported build tools, ELPA Syncer, 2021/10/06
- [nongnu] elpa/haskell-tng-mode bd4ff6f 192/385: standardise on naming convention:, ELPA Syncer, 2021/10/06
- [nongnu] elpa/haskell-tng-mode 3b5ac7f 190/385: package-lint, ELPA Syncer, 2021/10/06
- [nongnu] elpa/haskell-tng-mode a227f7e 200/385: blank lines end a run of type definitions, ELPA Syncer, 2021/10/06
- [nongnu] elpa/haskell-tng-mode e4ead98 201/385: thots on prettify-symbol, ELPA Syncer, 2021/10/06
- [nongnu] elpa/haskell-tng-mode 387aa18 203/385: more efficient layout algorithm,
ELPA Syncer <=
- [nongnu] elpa/haskell-tng-mode bfc3841 193/385: remove bind-key dependency, ELPA Syncer, 2021/10/06
- [nongnu] elpa/haskell-tng-mode 6acba95 206/385: better insertion indentation on toplevels, ELPA Syncer, 2021/10/06
- [nongnu] elpa/haskell-tng-mode 9bfc6bf 215/385: really basic imenu, ELPA Syncer, 2021/10/06
- [nongnu] elpa/haskell-tng-mode f648cf9 212/385: fallback to current directory (e.g. for cabal scripts), ELPA Syncer, 2021/10/06
- [nongnu] elpa/haskell-tng-mode 5bdf688 211/385: better in-line indentation of parens, ELPA Syncer, 2021/10/06
- [nongnu] elpa/haskell-tng-mode 1cdf901 120/385: better escape syntax detection, ELPA Syncer, 2021/10/06
- [nongnu] elpa/haskell-tng-mode c40c6e6 125/385: fix explicit export fontification bug, ELPA Syncer, 2021/10/06
- [nongnu] elpa/haskell-tng-mode 82312a2 126/385: working compilation matchers, ELPA Syncer, 2021/10/06
- [nongnu] elpa/haskell-tng-mode 794c80b 131/385: better indentation alts, ELPA Syncer, 2021/10/06
- [nongnu] elpa/haskell-tng-mode d43fd73 141/385: more efficient indentation 'insert test, ELPA Syncer, 2021/10/06