From 36fb4a48d4e84f72dfab50d8c05c89d2c2a5f7e3 Mon Sep 17 00:00:00 2001 From: mahmoodsheikh36 Date: Tue, 16 Jun 2026 22:09:38 +0300 Subject: [PATCH 1/9] support for invisibility and "virtual" items --- src/display/logical-line.lisp | 341 +++++++++++++++++++++++++-------- src/display/physical-line.lisp | 1 + 2 files changed, 257 insertions(+), 85 deletions(-) diff --git a/src/display/logical-line.lisp b/src/display/logical-line.lisp index 7545c6f50..7d3bd024a 100644 --- a/src/display/logical-line.lisp +++ b/src/display/logical-line.lisp @@ -2,9 +2,17 @@ (defvar *active-modes*) +(defstruct virtual-item + "a display-only string fragment injected at a character position within a logical line." + ;; 0-based position in the line's string where this fragment is inserted + charpos + string + attribute) + (defstruct logical-line string attributes + virtual-items left-content end-of-line-cursor-attribute extend-to-end @@ -77,7 +85,34 @@ over-attribute)))) (lem/buffer/line:normalization-elements merged-attributes))) +(defun splice-string (string attributes ov-start ov-end replacement replacement-attr) + "replace [OV-START, OV-END) in STRING with REPLACEMENT, adjusting ATTRIBUTES." + (let* ((rep-len (length replacement)) + (delta (- rep-len (- ov-end ov-start))) + (new-string (str:concat (subseq string 0 ov-start) + replacement + (subseq string ov-end))) + (pruned (lem/buffer/line:remove-elements attributes ov-start ov-end)) + (shifted (if (zerop delta) + pruned + (loop :for (start end attr) :in pruned + :collect (if (>= start ov-end) + (list (+ start delta) (+ end delta) attr) + (list start end attr))))) + (final (if (and replacement-attr (plusp rep-len)) + (lem/buffer/line:put-elements shifted ov-start (+ ov-start rep-len) replacement-attr) + shifted))) + (values new-string final))) + +(defun line-fully-invisible-p (point overlays) + "T if an :invisible overlay spans POINT's line without either endpoint on it." + (loop :for overlay :in overlays + :thereis (and (overlay-get overlay :invisible) + (not (same-line-p (overlay-start overlay) point)) + (not (same-line-p (overlay-end overlay) point))))) + (defun create-logical-line (point overlays active-modes) + "build a logical-line for POINT's line, or NIL if the line is entirely invisible." (flet ((overlay-start-charpos (overlay point) (if (same-line-p point (overlay-start overlay)) (point-charpos (overlay-start overlay)) @@ -85,67 +120,148 @@ (overlay-end-charpos (overlay point) (when (same-line-p point (overlay-end overlay)) (point-charpos (overlay-end overlay))))) - (let* ((end-of-line-cursor-attribute nil) - (extend-to-end-attribute nil) - (line-end-overlay nil) - (left-content - (compute-left-display-area-content active-modes - (point-buffer point) - point)) - (tab-width (variable-value 'tab-width :default point))) - (destructuring-bind (string . attributes) - (get-string-and-attributes-at-point point) - (loop :for overlay :in overlays - :when (overlay-within-point-p overlay point) - :do (cond ((typep overlay 'line-endings-overlay) - (when (same-line-p (overlay-end overlay) point) - (setf line-end-overlay overlay))) - ((typep overlay 'line-overlay) - (let ((attribute (overlay-attribute overlay))) + (let ((overlays (remove-if-not (lambda (ov) (overlay-within-point-p ov point)) + overlays))) + (when (line-fully-invisible-p point overlays) + (return-from create-logical-line nil)) + (let* ((end-of-line-cursor-attribute nil) + (extend-to-end-attribute nil) + (line-end-overlay nil) + (virtual-items) + (left-content + (compute-left-display-area-content active-modes + (point-buffer point) + point)) + (tab-width (variable-value 'tab-width :default point))) + (destructuring-bind (string . attributes) + (get-string-and-attributes-at-point point) + ;; collect string-splice operations from :invisible/:display overlays. + (let ((splice-ops)) + (loop :for overlay :in overlays + :for invisible := (overlay-get overlay :invisible) + :for display := (overlay-get overlay :display) + :do (when (or invisible display) + (let* ((ov-start (overlay-start-charpos overlay point)) + (ov-end (or (overlay-end-charpos overlay point) + (length string))) + (replacement + (cond + (display + (let ((d (alexandria:ensure-list display))) + (if (stringp (first d)) (first d) ""))) + ((eq invisible :ellipsis) "...") + (t ""))) + (repl-attr + (when (listp display) (second display)))) + (when (< ov-start ov-end) + (push (list ov-start ov-end replacement repl-attr) splice-ops))))) + ;; apply splices right-to-left for position stability + (when splice-ops + (dolist (op (sort splice-ops #'> :key #'first)) + (destructuring-bind (ov-start ov-end replacement repl-attr) op + (setf (values string attributes) + (splice-string string + attributes + ov-start + ov-end + replacement + repl-attr)))))) + ;; process all overlays for attributes (virtual text handled separately below). + (loop :for overlay :in overlays + :do (cond + ((typep overlay 'line-endings-overlay) + (when (same-line-p (overlay-end overlay) point) + (setf line-end-overlay overlay))) + ((typep overlay 'line-overlay) + (let ((attribute (overlay-attribute overlay))) + (setf attributes + (overlay-attributes attributes + 0 + (length string) + attribute)) + (setf extend-to-end-attribute attribute))) + ((typep overlay 'cursor-overlay) + (let* ((ov-start (overlay-start-charpos overlay point)) + (ov-end (1+ ov-start)) + (ov-attr (overlay-attribute overlay))) + (unless (cursor-overlay-fake-p overlay) + (set-cursor-attribute ov-attr)) + (if (<= (length string) ov-start) + (setf end-of-line-cursor-attribute ov-attr) + (setf attributes + (overlay-attributes attributes ov-start ov-end ov-attr))))) + (t + (let ((ov-start (overlay-start-charpos overlay point)) + (ov-end (overlay-end-charpos overlay point)) + (ov-attr (overlay-attribute overlay)) + (invisible (overlay-get overlay :invisible)) + (display (overlay-get overlay :display))) + ;; plain attribute (only when not replaced by invisible/display) + (when (and ov-attr (not invisible) (not display)) + (unless ov-end + (setf extend-to-end-attribute ov-attr)) (setf attributes (overlay-attributes attributes - 0 - (length string) - attribute)) - (setf extend-to-end-attribute attribute))) - ((typep overlay 'cursor-overlay) - (let* ((overlay-start-charpos (overlay-start-charpos overlay point)) - (overlay-end-charpos (1+ overlay-start-charpos)) - (overlay-attribute (overlay-attribute overlay))) - (unless (cursor-overlay-fake-p overlay) - (set-cursor-attribute overlay-attribute)) - (if (<= (length string) overlay-start-charpos) - (setf end-of-line-cursor-attribute overlay-attribute) - (setf attributes - (overlay-attributes - attributes - overlay-start-charpos - overlay-end-charpos - overlay-attribute))))) - (t - (let ((overlay-start-charpos (overlay-start-charpos overlay point)) - (overlay-end-charpos (overlay-end-charpos overlay point)) - (overlay-attribute (overlay-attribute overlay))) - (unless overlay-end-charpos - (setf extend-to-end-attribute - (overlay-attribute overlay))) - (setf attributes - (overlay-attributes - attributes - overlay-start-charpos - (or overlay-end-charpos (length string)) - overlay-attribute)))))) - (setf (values string attributes) (expand-tab string attributes tab-width)) - (let ((charpos (point-charpos point))) - (when (< 0 charpos) - (psetf string (subseq string charpos) - attributes (lem/buffer/line:subseq-elements attributes charpos (length string))))) - (make-logical-line :string string - :attributes attributes - :left-content left-content - :extend-to-end extend-to-end-attribute - :end-of-line-cursor-attribute end-of-line-cursor-attribute - :line-end-overlay line-end-overlay))))) + ov-start + (or ov-end (length string)) + ov-attr))))))) + ;; virtual text from :before-string/:after-string overlays. emit each overlay's + ;; :before then :after, visiting overlays in (end, start) order: at any shared + ;; charpos an overlay closing there (smaller end) is emitted before one opening + ;; there, so trailing :after-strings precede leading :before-strings, but a + ;; zero-length overlay's own pair stays adjacent. a final stable sort by charpos + ;; groups them without affecting this order. + (loop :for overlay :in (stable-sort + (loop :for overlay :in overlays + :when (or (overlay-get overlay :before-string) + (overlay-get overlay :after-string)) + :collect overlay) + (lambda (a b) + (let ((a-end (or (overlay-end-charpos a point) (length string))) + (b-end (or (overlay-end-charpos b point) (length string)))) + (if (= a-end b-end) + (< (overlay-start-charpos a point) + (overlay-start-charpos b point)) + (< a-end b-end))))) + :for before-str := (overlay-get overlay :before-string) + :for after-str := (overlay-get overlay :after-string) + :do (when (and before-str (same-line-p (overlay-start overlay) point)) + (let ((bs (alexandria:ensure-list before-str))) + (push (make-virtual-item :charpos (overlay-start-charpos overlay point) + :string (first bs) + :attribute (second bs)) + virtual-items))) + (when (and after-str (same-line-p (overlay-end overlay) point)) + (let ((as (alexandria:ensure-list after-str))) + (push (make-virtual-item :charpos (or (overlay-end-charpos overlay point) + (length string)) + :string (first as) + :attribute (second as)) + virtual-items)))) + (setf virtual-items + (stable-sort (nreverse virtual-items) #'< :key #'virtual-item-charpos)) + (setf (values string attributes) (expand-tab string attributes tab-width)) + (let ((charpos (point-charpos point))) + (when (< 0 charpos) + (psetf string (subseq string charpos) + attributes (lem/buffer/line:subseq-elements + attributes charpos (length string))) + ;; adjust virtual-item positions for the charpos clip (order preserved) + (setf virtual-items + (loop :for vi :in virtual-items + :when (>= (virtual-item-charpos vi) charpos) + :collect (make-virtual-item + :charpos (- (virtual-item-charpos vi) charpos) + :string (virtual-item-string vi) + :attribute (virtual-item-attribute vi)))))) + (make-logical-line + :string string + :attributes attributes + :virtual-items virtual-items + :left-content left-content + :extend-to-end extend-to-end-attribute + :end-of-line-cursor-attribute end-of-line-cursor-attribute + :line-end-overlay line-end-overlay)))))) (defstruct string-with-attribute-item string @@ -191,41 +307,95 @@ (defmethod item-attribute ((item extend-to-eol-item)) nil) +(defun add-or-merge-item (item items) + "add ITEM to the front of ITEMS, or merge it into the previous +string-with-attribute-item when both carry the same attribute. returns the updated list." + (let ((last-item (first items))) + (if (and (string-with-attribute-item-p last-item) + (string-with-attribute-item-p item) + (equal (string-with-attribute-item-attribute last-item) + (string-with-attribute-item-attribute item))) + (progn + (setf (string-with-attribute-item-string last-item) + (str:concat (string-with-attribute-item-string last-item) + (string-with-attribute-item-string item))) + items) + (cons item items)))) + (defun compute-items-from-string-and-attributes (string attributes) (handler-case (let ((items '())) - (flet ((add (item) - (if (null items) - (push item items) - (let ((last-item (first items))) - (if (and (string-with-attribute-item-p last-item) - (string-with-attribute-item-p item) - (equal (string-with-attribute-item-attribute last-item) - (string-with-attribute-item-attribute item))) - (setf (string-with-attribute-item-string (first items)) - (str:concat (string-with-attribute-item-string last-item) - (string-with-attribute-item-string item))) - (push item items)))))) - (loop :for last-pos := 0 :then end - :for (start end attribute) :in attributes - :do (unless (= last-pos start) - (add (make-string-with-attribute-item :string (subseq string last-pos start)))) - (add (if (cursor-attribute-p attribute) - (make-cursor-item :string (subseq string start end) :attribute attribute) - (make-string-with-attribute-item - :string (subseq string start end) - :attribute attribute))) - :finally (push (make-string-with-attribute-item :string (subseq string last-pos)) - items))) + (loop :for last-pos := 0 :then end + :for (start end attribute) :in attributes + :do (unless (= last-pos start) + (setf items (add-or-merge-item + (make-string-with-attribute-item :string (subseq string last-pos start)) + items))) + (setf items (add-or-merge-item + (if (cursor-attribute-p attribute) + (make-cursor-item :string (subseq string start end) :attribute attribute) + (make-string-with-attribute-item + :string (subseq string start end) + :attribute attribute)) + items)) + :finally (push (make-string-with-attribute-item :string (subseq string last-pos)) + items)) items) (error (e) (log:error e string attributes) nil))) +(defun inject-virtual-items (string attributes virtual-items) + "produce items from STRING and ATTRIBUTES, injecting VIRTUAL-ITEMS at their charposes. +VIRTUAL-ITEMS arrive in draw order (from `create-logical-line')." + (let* (;; all positions where we may need to split: attribute boundaries, virtual charposes. + (positions + (sort (remove-duplicates + (nconc (list 0 (length string)) + (mapcar #'virtual-item-charpos virtual-items) + (mapcan (lambda (span) (list (first span) (second span))) + attributes))) + #'<)) + ;; VIRTUAL-ITEMS are already sorted by charpos in draw order + (pending virtual-items) + (items)) + (flet ((add-virtuals-at (pos) + (loop :while (and pending (= (virtual-item-charpos (first pending)) pos)) + :do (let ((vi (pop pending))) + (setf items (add-or-merge-item + (make-string-with-attribute-item + :string (virtual-item-string vi) + :attribute (virtual-item-attribute vi)) + items)))))) + ;; walk segments between break positions, injecting virtual items at each boundary + (loop :for (pos . rest) :on positions + :while rest + :for next-pos := (first rest) + :do (add-virtuals-at pos) + (unless (= pos next-pos) + (let* ((seg (subseq string pos next-pos)) + (attr (loop :for (start end attribute) :in attributes + :when (and (<= start pos) (>= end next-pos)) + :return attribute))) + (unless (string= seg "") + (setf items (add-or-merge-item + (if (cursor-attribute-p attr) + (make-cursor-item :string seg :attribute attr) + (make-string-with-attribute-item :string seg + :attribute attr)) + items)))))) + ;; virtual items at the very end of the string + (add-virtuals-at (length string))) + items)) + (defun compute-items-from-logical-line (logical-line) (let ((items - (compute-items-from-string-and-attributes (logical-line-string logical-line) - (logical-line-attributes logical-line)))) + (if (logical-line-virtual-items logical-line) + (inject-virtual-items (logical-line-string logical-line) + (logical-line-attributes logical-line) + (logical-line-virtual-items logical-line)) + (compute-items-from-string-and-attributes (logical-line-string logical-line) + (logical-line-attributes logical-line))))) (alexandria:when-let (attribute (logical-line-extend-to-end logical-line)) (push (make-extend-to-eol-item :color (attribute-background-color attribute)) @@ -290,7 +460,8 @@ (active-modes (get-active-modes-class-instance (window-buffer window))) (*active-modes* active-modes)) (loop :for logical-line := (create-logical-line point overlays active-modes) - :do (funcall function logical-line) + :do (when logical-line + (funcall function logical-line)) (unless (line-offset point 1) (return)))))) diff --git a/src/display/physical-line.lisp b/src/display/physical-line.lisp index add39e7a5..9fb31f600 100644 --- a/src/display/physical-line.lisp +++ b/src/display/physical-line.lisp @@ -536,6 +536,7 @@ over the top-level spine and tolerant of improper (dotted) lists." (logical-line-end-of-line-cursor-attribute logical-line) (logical-line-extend-to-end logical-line) (logical-line-line-end-overlay logical-line) + (logical-line-virtual-items logical-line) scroll-start left-side-width)) From 535ef07dfc2557cf2890e6c164a4726bbad21a77 Mon Sep 17 00:00:00 2001 From: mahmoodsheikh36 Date: Fri, 19 Jun 2026 01:29:38 +0300 Subject: [PATCH 2/9] make previous-line/next-line skip hidden lines --- lem.asd | 14 ++++++------ src/commands/move.lisp | 44 ++++++++++++++++++++++++++++++++++++-- src/internal-packages.lisp | 7 ++++-- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/lem.asd b/lem.asd index 1ddff52c0..3bcd3daf2 100644 --- a/lem.asd +++ b/lem.asd @@ -149,6 +149,13 @@ (:file "color-theme") + (:module "display" + :serial t + :components ((:file "base") + (:file "char-type") + (:file "logical-line") + (:file "physical-line"))) + (:module "commands" :serial t :components ((:file "move") @@ -168,13 +175,6 @@ (:file "frame") #+sbcl (:file "sprof"))) - (:module "display" - :serial t - :components ((:file "base") - (:file "char-type") - (:file "logical-line") - (:file "physical-line"))) - (:file "external-packages") (:module "ext" diff --git a/src/commands/move.lisp b/src/commands/move.lisp index 35011dadd..c7db8c360 100644 --- a/src/commands/move.lisp +++ b/src/commands/move.lisp @@ -45,6 +45,46 @@ (define-key *global-keymap* "C-x [" 'previous-page-char) (define-key *global-keymap* "M-g" 'goto-line) +(defun line-invisible-p (point) + "T if POINT's line is fully hidden by an :invisible overlay." + (let ((overlays (remove-if-not + (lambda (ov) + (overlay-within-point-p ov point)) + (buffer-overlays (point-buffer point))))) + (line-fully-invisible-p point overlays))) + +(defun skip-invisible-lines (point direction) + "move POINT past any fully invisible lines in DIRECTION (1 or -1). +returns POINT on success, or NIL if a buffer boundary is reached." + (loop :while (line-invisible-p point) + :do (unless (line-offset point direction) + (return-from skip-invisible-lines nil))) + point) + +(defun move-to-next-visible-virtual-line (point n) + "like `move-to-next-virtual-line' but skips fully invisible lines." + (let ((dir (if (plusp n) 1 -1)) + (steps (abs n))) + (loop :repeat steps + :do (unless (move-to-next-virtual-line point dir) + (return-from move-to-next-visible-virtual-line nil)) + (when (line-invisible-p point) + (unless (skip-invisible-lines point dir) + (return-from move-to-next-visible-virtual-line nil)))) + point)) + +(defun visible-line-offset (point n) + "like `line-offset' but skips fully invisible lines." + (let ((dir (if (plusp n) 1 -1)) + (steps (abs n))) + (loop :repeat steps + :do (unless (line-offset point dir) + (return-from visible-line-offset nil)) + (when (line-invisible-p point) + (unless (skip-invisible-lines point dir) + (return-from visible-line-offset nil)))) + point)) + (defun next-line-aux (n point-column-fn forward-line-fn @@ -67,14 +107,14 @@ "Move the cursor to next line." (next-line-aux n #'point-virtual-line-column - #'move-to-next-virtual-line + #'move-to-next-visible-virtual-line #'move-to-virtual-line-column)) (define-command (next-logical-line (:advice-classes movable-advice)) (&optional n) (:universal) "Move the cursor to the next logical line." (next-line-aux n #'point-column - #'line-offset + #'visible-line-offset #'move-to-column)) (define-command (previous-line (:advice-classes movable-advice)) (&optional (n 1)) (:universal) diff --git a/src/internal-packages.lisp b/src/internal-packages.lisp index 6dfa60cc2..92f4329b6 100644 --- a/src/internal-packages.lisp +++ b/src/internal-packages.lisp @@ -580,7 +580,9 @@ :overlay-put :overlay-get :clear-overlays - :point-overlays) + :point-overlays + :buffer-overlays + :overlay-within-point-p) ;; streams.lisp (:export :buffer-input-stream @@ -659,7 +661,8 @@ :compute-wrap-left-area-content) ;; display/logical-line.lisp (:export - :make-region-overlays-using-global-mode) + :make-region-overlays-using-global-mode + :line-fully-invisible-p) ;; interface.lisp (:export :with-implementation From ec4179741674c9a01ddba1913337f89008b091f0 Mon Sep 17 00:00:00 2001 From: mahmoodsheikh36 Date: Fri, 19 Jun 2026 23:33:40 +0300 Subject: [PATCH 3/9] add generic defun-folding and bind it to tab --- src/ext/language-mode.lisp | 70 +++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/src/ext/language-mode.lisp b/src/ext/language-mode.lisp index 573a6bd11..f04652f3a 100644 --- a/src/ext/language-mode.lisp +++ b/src/ext/language-mode.lisp @@ -6,6 +6,9 @@ :idle-function :beginning-of-defun-function :end-of-defun-function + :fold-region-function + :fold-toggle-at-point + :unfold-all :comment-region :uncomment-region :comment-or-uncomment-region @@ -51,6 +54,8 @@ (define-editor-variable idle-function nil) (define-editor-variable beginning-of-defun-function nil) (define-editor-variable end-of-defun-function nil) +(define-editor-variable fold-region-function 'fold-region-default + "function of one point returning (values start end) for the foldable region at point, or NIL.") (define-editor-variable line-comment nil) (define-editor-variable insertion-line-comment nil) (define-editor-variable find-definitions-function nil) @@ -61,6 +66,9 @@ (define-editor-variable root-uri-patterns '()) (define-editor-variable detective-search nil) +(define-attribute fold-attribute + (t :foreground :base04)) + (defun prompt-for-symbol (prompt history-name) (prompt-for-string prompt :history-symbol history-name)) @@ -86,7 +94,7 @@ (define-key *language-mode-keymap* "C-M-a" 'beginning-of-defun) (define-key *language-mode-keymap* "C-M-e" 'end-of-defun) -(define-key *language-mode-keymap* "Tab" 'indent-line-and-complete-symbol) +(define-key *language-mode-keymap* "Tab" 'fold-or-indent-or-complete) (define-key *global-keymap* "C-j" 'newline-and-indent) (define-key *global-keymap* "M-j" 'newline-and-indent) (define-key *language-mode-keymap* "C-M-\\" 'indent-region) @@ -115,6 +123,66 @@ (funcall fn (current-point) n) (beginning-of-defun-1 (- n))))) +(defun fold-region-default (point) + "return (values start end) spanning the defun at POINT, or NIL." + (let ((defun-begin (variable-value 'beginning-of-defun-function :buffer point)) + (defun-end (variable-value 'end-of-defun-function :buffer point))) + (when (and defun-begin defun-end) + (let ((start (copy-point point :temporary)) + (end (copy-point point :temporary))) + (funcall defun-end end 1) + (move-point start end) + (funcall defun-begin start 1) + (when (point< start end) + (values start end)))))) + +(defun fold-region (start end &optional (fold-marker "...")) + "hide the lines of the region [START, END), leaving START's line visible with a fold marker. +returns the fold overlay." + (with-point ((s start)) + (line-end s) + (let ((overlay (make-overlay s end 'fold-attribute))) + (overlay-put overlay :invisible t) + (overlay-put overlay :fold t) + (overlay-put overlay :before-string (list fold-marker 'fold-attribute)) + overlay))) + +(defun fold-overlay-at (point) + "the fold overlay whose header line is POINT's line, or NIL." + (find-if + (lambda (overlay) + (and (overlay-get overlay :fold) + (same-line-p (overlay-start overlay) point))) + (buffer-overlays (point-buffer point)))) + +(defun fold-defun-at (point) + "fold the defun at POINT. Returns T when something was folded." + (let ((fn (variable-value 'fold-region-function :default point))) + (multiple-value-bind (start end) (funcall fn point) + (when (and start end (not (same-line-p start end))) + (fold-region start end) + (move-point point start) + t)))) + +(defun fold-toggle-at-point (&optional (point (current-point))) + "toggle the fold at POINT. returns T when a fold was added or removed, and NIL when there was +nothing to fold." + (let ((fold (fold-overlay-at point))) + (cond (fold (delete-overlay fold) t) + ((fold-defun-at point) t) + (t nil)))) + +(define-command fold-or-indent-or-complete () () + "fold or unfold the defun at point. otherwise indent and complete the symbol." + (unless (fold-toggle-at-point) + (indent-line-and-complete-symbol))) + +(define-command unfold-all () () + "remove every fold in the current buffer." + (dolist (overlay (copy-list (buffer-overlays))) + (when (overlay-get overlay :fold) + (delete-overlay overlay)))) + (define-command (indent (:advice-classes editable-advice)) (&optional (n 1)) (:universal) (if (variable-value 'calc-indent-function) (indent-line (current-point)) From 8570dbb8365ed6f800385a75caef17249ca4aacd Mon Sep 17 00:00:00 2001 From: mahmoodsheikh36 Date: Sat, 27 Jun 2026 12:16:51 +0300 Subject: [PATCH 4/9] improve invisiblity behavior --- extensions/vi-mode/commands.lisp | 19 +- extensions/vi-mode/commands/utils.lisp | 5 +- lem-tests.asd | 3 +- src/commands/move.lisp | 68 ++++- src/display/logical-line.lisp | 392 +++++++++++++++---------- src/ext/language-mode.lisp | 9 +- src/internal-packages.lisp | 3 +- 7 files changed, 326 insertions(+), 173 deletions(-) diff --git a/extensions/vi-mode/commands.lisp b/extensions/vi-mode/commands.lisp index f8d8e2005..dcf4dafd8 100644 --- a/extensions/vi-mode/commands.lisp +++ b/extensions/vi-mode/commands.lisp @@ -198,16 +198,19 @@ (define-command vi-forward-char (&optional (n 1)) (:universal) (let* ((p (current-point)) - (max-offset (- (length (line-string p)) - (point-charpos p)))) + (max-offset (with-point ((e p)) + (visual-line-end e) + (count-characters p e)))) (character-offset p (min n max-offset)))) (define-command vi-backward-char (&optional (n 1)) (:universal) (let ((p (current-point))) - (dotimes (_ n) - (if (bolp p) - (return) - (character-offset p -1))))) + (with-point ((bol p)) + (visual-line-beginning bol) + (dotimes (_ n) + (if (point<= p bol) + (return) + (character-offset p -1)))))) (define-motion vi-next-line (&optional (n 1)) (:universal) (:type :line) @@ -309,7 +312,7 @@ (define-command vi-move-to-beginning-of-line () () (with-point ((start (current-point))) - (line-start start) + (visual-line-beginning start) (or (text-property-at (current-point) :field -1) (previous-single-property-change (current-point) :field @@ -318,7 +321,7 @@ (define-command vi-move-to-end-of-line (&optional (n 1)) (:universal) (vi-line n) - (line-end (current-point))) + (visual-line-end (current-point))) (define-command vi-move-to-last-nonblank () () (vi-move-to-end-of-line) diff --git a/extensions/vi-mode/commands/utils.lisp b/extensions/vi-mode/commands/utils.lisp index fa883655e..4018243c1 100644 --- a/extensions/vi-mode/commands/utils.lisp +++ b/extensions/vi-mode/commands/utils.lisp @@ -61,7 +61,10 @@ (1- len))))) (defun fall-within-line (point) - (when (eolp point) + "clamps the given point to the character before the newline at the end of the line its in." + ;; a buffer-line end whose newline is hidden by a fold is mid-visual-line, so we dont clamp. + (when (and (eolp point) + (not (invisible-overlay-covering point))) (line-end point) (unless (bolp point) (character-offset point *cursor-offset*)))) diff --git a/lem-tests.asd b/lem-tests.asd index c249c2bea..1c4857789 100644 --- a/lem-tests.asd +++ b/lem-tests.asd @@ -79,6 +79,7 @@ (:file "filer") (:file "listener-mode") (:file "interface") - (:file "display-cache")) + (:file "display-cache") + (:file "visual-line")) :perform (test-op (o c) (symbol-call :rove :run c))) diff --git a/src/commands/move.lisp b/src/commands/move.lisp index c7db8c360..385d99079 100644 --- a/src/commands/move.lisp +++ b/src/commands/move.lisp @@ -12,6 +12,8 @@ :move-to-beginning-of-logical-line :move-to-end-of-line :move-to-end-of-logical-line + :visual-line-beginning + :visual-line-end :next-page :previous-page :next-page-char @@ -45,18 +47,64 @@ (define-key *global-keymap* "C-x [" 'previous-page-char) (define-key *global-keymap* "M-g" 'goto-line) -(defun line-invisible-p (point) - "T if POINT's line is fully hidden by an :invisible overlay." - (let ((overlays (remove-if-not - (lambda (ov) - (overlay-within-point-p ov point)) - (buffer-overlays (point-buffer point))))) - (line-fully-invisible-p point overlays))) +(defvar *cursor-positions-before-command* + (make-hash-table :test 'eq) + "maps each cursor point to its absolute buffer position before the running +command, so `snap-cursors-out-of-invisible' can tell which direction it moved.") + +(add-hook *pre-command-hook* 'record-cursor-positions) +(add-hook *post-command-hook* 'snap-cursors-out-of-invisible) + +(defun record-cursor-positions () + "snapshot every cursor's position so a later snap can pick the visible edge on the far side +of a fold the cursor moves into." + (clrhash *cursor-positions-before-command*) + (dolist (point (buffer-cursors (current-buffer))) + (setf (gethash point *cursor-positions-before-command*) + (position-at-point point)))) + +(defun snap-cursors-out-of-invisible () + "a fold collapses its range, so no cursor may rest within it or at its start. move any such +cursor to the nearest visible position in its direction of travel, so the cursor never appears +stuck on hidden text." + (dolist (point (buffer-cursors (current-buffer))) + (let ((overlay (invisible-overlay-covering point))) + (when overlay + (let ((forwardp (let ((before (gethash point *cursor-positions-before-command*))) + (or (null before) + (<= before (position-at-point (overlay-start overlay))))))) + (loop :for ov := (invisible-overlay-covering point) + :while ov + :do (if forwardp + (move-point point (overlay-end ov)) + (progn + (move-point point (overlay-start ov)) + ;; dont continue looping if we are at the beginning of the buffer. + ;; it could cause an endless loop. + (unless (character-offset point -1) + (return)))))))))) + +(defun visual-line-end (point) + "move POINT to the end of its visual line. returns POINT." + (line-end point) + (loop :until (last-line-p point) + :while (invisible-overlay-covering point) + :do (line-offset point 1) + (line-end point)) + point) + +(defun visual-line-beginning (point) + "move POINT to the start of its visual line. returns POINT." + (line-start point) + (loop :while (line-continuation-p point) + :do (line-offset point -1) + (line-start point)) + point) (defun skip-invisible-lines (point direction) "move POINT past any fully invisible lines in DIRECTION (1 or -1). returns POINT on success, or NIL if a buffer boundary is reached." - (loop :while (line-invisible-p point) + (loop :while (line-continuation-p point) :do (unless (line-offset point direction) (return-from skip-invisible-lines nil))) point) @@ -68,7 +116,7 @@ returns POINT on success, or NIL if a buffer boundary is reached." (loop :repeat steps :do (unless (move-to-next-virtual-line point dir) (return-from move-to-next-visible-virtual-line nil)) - (when (line-invisible-p point) + (when (line-continuation-p point) (unless (skip-invisible-lines point dir) (return-from move-to-next-visible-virtual-line nil)))) point)) @@ -80,7 +128,7 @@ returns POINT on success, or NIL if a buffer boundary is reached." (loop :repeat steps :do (unless (line-offset point dir) (return-from visible-line-offset nil)) - (when (line-invisible-p point) + (when (line-continuation-p point) (unless (skip-invisible-lines point dir) (return-from visible-line-offset nil)))) point)) diff --git a/src/display/logical-line.lisp b/src/display/logical-line.lisp index 7d3bd024a..5b5f94d04 100644 --- a/src/display/logical-line.lisp +++ b/src/display/logical-line.lisp @@ -104,6 +104,20 @@ shifted))) (values new-string final))) +(defun adjust-charpos-for-splices (charpos splice-ops) + "map a raw-string CHARPOS to its position after SPLICE-OPS are applied. +SPLICE-OPS is a list of (START END REPLACEMENT . _) covering disjoint ranges, as collected +in `create-logical-line'. used to keep virtual-item markers anchored when several folds on one +visual line each splice text out." + (+ charpos + (loop :for (start end replacement) :in splice-ops + :sum (cond ((<= charpos start) + 0) + ((>= charpos end) + (- (length replacement) (- end start))) + (t + (- start charpos)))))) + (defun line-fully-invisible-p (point overlays) "T if an :invisible overlay spans POINT's line without either endpoint on it." (loop :for overlay :in overlays @@ -111,157 +125,232 @@ (not (same-line-p (overlay-start overlay) point)) (not (same-line-p (overlay-end overlay) point))))) +(defun invisible-overlay-covering (point &optional (overlays (buffer-overlays (point-buffer point)))) + "return the :invisible overlay covering POINT." + (loop :for overlay :in overlays + :thereis (and (overlay-get overlay :invisible) + (point<= (overlay-start overlay) point) + (point< point (overlay-end overlay)) + overlay))) + +(defun line-continuation-p (point) + "whether POINT's line continues a previous visual line. meaning the newline preceding it is +hidden by an :invisible overlay, so the line is not a visual line of its own. +a folded region may hide arbitrary character ranges, including the newlines that join several +buffer lines into one displayed line." + (and (not (first-line-p point)) + (with-point ((p point)) + (line-start p) + (character-offset p -1) + (invisible-overlay-covering p)))) + +(defun collect-visual-line-string-and-attributes (vstart vend) + (with-point ((p vstart)) + (let ((out (make-string-output-stream)) + (attributes) + (base 0)) + (loop + (destructuring-bind (string . attrs) (get-string-and-attributes-at-point p) + (write-string string out) + (loop :for (s e attr) :in attrs + :do (push (list (+ base s) (+ base e) attr) attributes)) + (incf base (length string)) + (when (same-line-p p vend) + (return)) + (write-char #\newline out) + (incf base) + (line-offset p 1))) + (values (get-output-stream-string out) + (nreverse attributes))))) + (defun create-logical-line (point overlays active-modes) - "build a logical-line for POINT's line, or NIL if the line is entirely invisible." - (flet ((overlay-start-charpos (overlay point) - (if (same-line-p point (overlay-start overlay)) - (point-charpos (overlay-start overlay)) - 0)) - (overlay-end-charpos (overlay point) - (when (same-line-p point (overlay-end overlay)) - (point-charpos (overlay-end overlay))))) - (let ((overlays (remove-if-not (lambda (ov) (overlay-within-point-p ov point)) - overlays))) - (when (line-fully-invisible-p point overlays) - (return-from create-logical-line nil)) - (let* ((end-of-line-cursor-attribute nil) - (extend-to-end-attribute nil) - (line-end-overlay nil) - (virtual-items) - (left-content - (compute-left-display-area-content active-modes - (point-buffer point) - point)) - (tab-width (variable-value 'tab-width :default point))) - (destructuring-bind (string . attributes) - (get-string-and-attributes-at-point point) - ;; collect string-splice operations from :invisible/:display overlays. - (let ((splice-ops)) - (loop :for overlay :in overlays - :for invisible := (overlay-get overlay :invisible) - :for display := (overlay-get overlay :display) - :do (when (or invisible display) - (let* ((ov-start (overlay-start-charpos overlay point)) - (ov-end (or (overlay-end-charpos overlay point) - (length string))) - (replacement - (cond - (display - (let ((d (alexandria:ensure-list display))) - (if (stringp (first d)) (first d) ""))) - ((eq invisible :ellipsis) "...") - (t ""))) - (repl-attr - (when (listp display) (second display)))) - (when (< ov-start ov-end) - (push (list ov-start ov-end replacement repl-attr) splice-ops))))) - ;; apply splices right-to-left for position stability - (when splice-ops - (dolist (op (sort splice-ops #'> :key #'first)) - (destructuring-bind (ov-start ov-end replacement repl-attr) op - (setf (values string attributes) - (splice-string string - attributes - ov-start - ov-end - replacement - repl-attr)))))) - ;; process all overlays for attributes (virtual text handled separately below). - (loop :for overlay :in overlays - :do (cond - ((typep overlay 'line-endings-overlay) - (when (same-line-p (overlay-end overlay) point) - (setf line-end-overlay overlay))) - ((typep overlay 'line-overlay) - (let ((attribute (overlay-attribute overlay))) - (setf attributes - (overlay-attributes attributes - 0 - (length string) - attribute)) - (setf extend-to-end-attribute attribute))) - ((typep overlay 'cursor-overlay) - (let* ((ov-start (overlay-start-charpos overlay point)) - (ov-end (1+ ov-start)) - (ov-attr (overlay-attribute overlay))) - (unless (cursor-overlay-fake-p overlay) - (set-cursor-attribute ov-attr)) - (if (<= (length string) ov-start) - (setf end-of-line-cursor-attribute ov-attr) + "build a logical-line for the visual line starting at POINT, joining any following buffer lines +whose preceding newline is hidden by an :invisible overlay. a single displayed line may contain +several folds that each hide arbitrary character ranges across multiple buffer lines." + (let ((invisible-overlays + (remove-if-not (lambda (ov) (overlay-get ov :invisible)) overlays))) + (with-point ((vstart point) + (vend point)) + (line-start vstart) + (line-end vend) + ;; extend VEND across every newline hidden by an invisible overlay so the + ;; visual line reaches the next *visible* newline (or the buffer end). + (loop :until (last-line-p vend) + :while (invisible-overlay-covering vend invisible-overlays) + :do (line-offset vend 1) + (line-end vend)) + (let ((overlays (remove-if-not + (lambda (ov) + (and (point<= (overlay-start ov) vend) + (point<= vstart (overlay-end ov)))) + overlays))) + (flet ((overlay-start-charpos (overlay) + ;; column where the overlay starts on this visual line, clamped to + ;; 0 when it begins before VSTART. + (let ((s (overlay-start overlay))) + (if (point<= vstart s) + (count-characters vstart s) + 0))) + (overlay-end-charpos (overlay) + ;; column where the overlay ends, or NIL when it extends past VEND. + (let ((e (overlay-end overlay))) + (when (point<= e vend) + (count-characters vstart e)))) + (start-in-line-p (overlay) + ;; true when the overlay's start falls within this visual line. + (point<= vstart (overlay-start overlay))) + (end-in-line-p (overlay) + ;; true when the overlay's end falls within this visual line. + (point<= (overlay-end overlay) vend))) + (let* ((end-of-line-cursor-attribute nil) + (extend-to-end-attribute nil) + (line-end-overlay nil) + (virtual-items) + (splice-ops) + (left-content + (compute-left-display-area-content active-modes + (point-buffer point) + point)) + (tab-width (variable-value 'tab-width :default point))) + (multiple-value-bind (string attributes) + (collect-visual-line-string-and-attributes vstart vend) + ;; collect string-splice operations from :invisible/:display overlays. + (loop :for overlay :in overlays + :for invisible := (overlay-get overlay :invisible) + :for display := (overlay-get overlay :display) + :do (when (or invisible display) + (let* ((ov-start (overlay-start-charpos overlay)) + (ov-end (or (overlay-end-charpos overlay) + (length string))) + (replacement + (cond + (display + (let ((d (alexandria:ensure-list display))) + (if (stringp (first d)) (first d) ""))) + ((eq invisible :ellipsis) "...") + (t ""))) + (repl-attr + (when (listp display) (second display)))) + (when (< ov-start ov-end) + (push (list ov-start ov-end replacement repl-attr) splice-ops))))) + ;; apply splices right-to-left for position stability + (when splice-ops + (dolist (op (sort (copy-list splice-ops) #'> :key #'first)) + (destructuring-bind (ov-start ov-end replacement repl-attr) op + (setf (values string attributes) + (splice-string string + attributes + ov-start + ov-end + replacement + repl-attr))))) + ;; process all overlays for attributes (virtual text handled separately below). + (loop :for overlay :in overlays + :do (cond + ((typep overlay 'line-endings-overlay) + (when (end-in-line-p overlay) + (setf line-end-overlay overlay))) + ((typep overlay 'line-overlay) + (let ((attribute (overlay-attribute overlay))) (setf attributes - (overlay-attributes attributes ov-start ov-end ov-attr))))) - (t - (let ((ov-start (overlay-start-charpos overlay point)) - (ov-end (overlay-end-charpos overlay point)) - (ov-attr (overlay-attribute overlay)) - (invisible (overlay-get overlay :invisible)) - (display (overlay-get overlay :display))) - ;; plain attribute (only when not replaced by invisible/display) - (when (and ov-attr (not invisible) (not display)) - (unless ov-end - (setf extend-to-end-attribute ov-attr)) - (setf attributes - (overlay-attributes attributes - ov-start - (or ov-end (length string)) - ov-attr))))))) - ;; virtual text from :before-string/:after-string overlays. emit each overlay's - ;; :before then :after, visiting overlays in (end, start) order: at any shared - ;; charpos an overlay closing there (smaller end) is emitted before one opening - ;; there, so trailing :after-strings precede leading :before-strings, but a - ;; zero-length overlay's own pair stays adjacent. a final stable sort by charpos - ;; groups them without affecting this order. - (loop :for overlay :in (stable-sort - (loop :for overlay :in overlays - :when (or (overlay-get overlay :before-string) - (overlay-get overlay :after-string)) - :collect overlay) - (lambda (a b) - (let ((a-end (or (overlay-end-charpos a point) (length string))) - (b-end (or (overlay-end-charpos b point) (length string)))) - (if (= a-end b-end) - (< (overlay-start-charpos a point) - (overlay-start-charpos b point)) - (< a-end b-end))))) - :for before-str := (overlay-get overlay :before-string) - :for after-str := (overlay-get overlay :after-string) - :do (when (and before-str (same-line-p (overlay-start overlay) point)) - (let ((bs (alexandria:ensure-list before-str))) - (push (make-virtual-item :charpos (overlay-start-charpos overlay point) - :string (first bs) - :attribute (second bs)) - virtual-items))) - (when (and after-str (same-line-p (overlay-end overlay) point)) - (let ((as (alexandria:ensure-list after-str))) - (push (make-virtual-item :charpos (or (overlay-end-charpos overlay point) - (length string)) - :string (first as) - :attribute (second as)) - virtual-items)))) - (setf virtual-items - (stable-sort (nreverse virtual-items) #'< :key #'virtual-item-charpos)) - (setf (values string attributes) (expand-tab string attributes tab-width)) - (let ((charpos (point-charpos point))) - (when (< 0 charpos) - (psetf string (subseq string charpos) - attributes (lem/buffer/line:subseq-elements - attributes charpos (length string))) - ;; adjust virtual-item positions for the charpos clip (order preserved) + (overlay-attributes attributes + 0 + (length string) + attribute)) + (setf extend-to-end-attribute attribute))) + ((typep overlay 'cursor-overlay) + ;; remap the cursor into the spliced string so it lands on the right + ;; column past any folds on this visual line. + (let* ((ov-start (adjust-charpos-for-splices + (overlay-start-charpos overlay) splice-ops)) + (ov-end (1+ ov-start)) + (ov-attr (overlay-attribute overlay))) + (unless (cursor-overlay-fake-p overlay) + (set-cursor-attribute ov-attr)) + (if (<= (length string) ov-start) + (setf end-of-line-cursor-attribute ov-attr) + (setf attributes + (overlay-attributes attributes ov-start ov-end ov-attr))))) + (t + (let ((ov-start (adjust-charpos-for-splices + (overlay-start-charpos overlay) splice-ops)) + (ov-end (let ((e (overlay-end-charpos overlay))) + (when e + (adjust-charpos-for-splices e splice-ops)))) + (ov-attr (overlay-attribute overlay)) + (invisible (overlay-get overlay :invisible)) + (display (overlay-get overlay :display))) + ;; plain attribute (only when not replaced by invisible/display) + (when (and ov-attr (not invisible) (not display)) + (unless ov-end + (setf extend-to-end-attribute ov-attr)) + (setf attributes + (overlay-attributes attributes + ov-start + (or ov-end (length string)) + ov-attr))))))) + ;; virtual text from :before-string/:after-string overlays. emit each overlay's + ;; :before then :after, visiting overlays in (end, start) order: at any shared + ;; charpos an overlay closing there (smaller end) is emitted before one opening + ;; there, so trailing :after-strings precede leading :before-strings, but a + ;; zero-length overlay's own pair stays adjacent. a final stable sort by charpos + ;; groups them without affecting this order. + (loop :for overlay :in (stable-sort + (loop :for overlay :in overlays + :when (or (overlay-get overlay :before-string) + (overlay-get overlay :after-string)) + :collect overlay) + (lambda (a b) + (let ((a-end (or (overlay-end-charpos a) (length string))) + (b-end (or (overlay-end-charpos b) (length string)))) + (if (= a-end b-end) + (< (overlay-start-charpos a) + (overlay-start-charpos b)) + (< a-end b-end))))) + :for before-str := (overlay-get overlay :before-string) + :for after-str := (overlay-get overlay :after-string) + :do (when (and before-str (start-in-line-p overlay)) + (let ((bs (alexandria:ensure-list before-str))) + (push (make-virtual-item :charpos (overlay-start-charpos overlay) + :string (first bs) + :attribute (second bs)) + virtual-items))) + (when (and after-str (end-in-line-p overlay)) + (let ((as (alexandria:ensure-list after-str))) + (push (make-virtual-item :charpos (or (overlay-end-charpos overlay) + (length string)) + :string (first as) + :attribute (second as)) + virtual-items)))) + ;; markers were positioned in raw coordinates; remap them into the + ;; spliced string so several folds on one visual line stay anchored. + (dolist (vi virtual-items) + (setf (virtual-item-charpos vi) + (adjust-charpos-for-splices (virtual-item-charpos vi) splice-ops))) (setf virtual-items - (loop :for vi :in virtual-items - :when (>= (virtual-item-charpos vi) charpos) - :collect (make-virtual-item - :charpos (- (virtual-item-charpos vi) charpos) - :string (virtual-item-string vi) - :attribute (virtual-item-attribute vi)))))) - (make-logical-line - :string string - :attributes attributes - :virtual-items virtual-items - :left-content left-content - :extend-to-end extend-to-end-attribute - :end-of-line-cursor-attribute end-of-line-cursor-attribute - :line-end-overlay line-end-overlay)))))) + (stable-sort (nreverse virtual-items) #'< :key #'virtual-item-charpos)) + (setf (values string attributes) (expand-tab string attributes tab-width)) + (let ((charpos (point-charpos point))) + (when (< 0 charpos) + (psetf string (subseq string charpos) + attributes (lem/buffer/line:subseq-elements + attributes charpos (length string))) + ;; adjust virtual-item positions for the charpos clip (order preserved) + (setf virtual-items + (loop :for vi :in virtual-items + :when (>= (virtual-item-charpos vi) charpos) + :collect (make-virtual-item + :charpos (- (virtual-item-charpos vi) charpos) + :string (virtual-item-string vi) + :attribute (virtual-item-attribute vi)))))) + (make-logical-line + :string string + :attributes attributes + :virtual-items virtual-items + :left-content left-content + :extend-to-end extend-to-end-attribute + :end-of-line-cursor-attribute end-of-line-cursor-attribute + :line-end-overlay line-end-overlay)))))))) (defstruct string-with-attribute-item string @@ -462,8 +551,11 @@ VIRTUAL-ITEMS arrive in draw order (from `create-logical-line')." (loop :for logical-line := (create-logical-line point overlays active-modes) :do (when logical-line (funcall function logical-line)) - (unless (line-offset point 1) - (return)))))) + (loop + (unless (line-offset point 1) + (return-from call-do-logical-line)) + (unless (line-continuation-p point) + (return))))))) (defmacro do-logical-line ((logical-line window) &body body) `(call-do-logical-line ,window (lambda (,logical-line) ,@body))) diff --git a/src/ext/language-mode.lisp b/src/ext/language-mode.lisp index f04652f3a..0cca24512 100644 --- a/src/ext/language-mode.lisp +++ b/src/ext/language-mode.lisp @@ -139,9 +139,14 @@ (defun fold-region (start end &optional (fold-marker "...")) "hide the lines of the region [START, END), leaving START's line visible with a fold marker. returns the fold overlay." - (with-point ((s start)) + (with-point ((s start) + (e end)) (line-end s) - (let ((overlay (make-overlay s end 'fold-attribute))) + ;; dont hide the newline that terminates the folded region's last line, or the line after + ;; the fold gets merged onto the header's visual line. + (when (start-line-p e) + (character-offset e -1)) + (let ((overlay (make-overlay s e 'fold-attribute))) (overlay-put overlay :invisible t) (overlay-put overlay :fold t) (overlay-put overlay :before-string (list fold-marker 'fold-attribute)) diff --git a/src/internal-packages.lisp b/src/internal-packages.lisp index 92f4329b6..e28fc61ad 100644 --- a/src/internal-packages.lisp +++ b/src/internal-packages.lisp @@ -662,7 +662,8 @@ ;; display/logical-line.lisp (:export :make-region-overlays-using-global-mode - :line-fully-invisible-p) + :line-continuation-p + :invisible-overlay-covering) ;; interface.lisp (:export :with-implementation From 548b77e09a71e9485ce1df0c2b448ac7f466bbea Mon Sep 17 00:00:00 2001 From: mahmoodsheikh36 Date: Sat, 27 Jun 2026 12:40:53 +0300 Subject: [PATCH 5/9] add test --- src/attribute.lisp | 3 +++ src/display/logical-line.lisp | 16 ++++++++++++ src/ext/language-mode.lisp | 19 -------------- src/internal-packages.lisp | 4 ++- tests/visual-line.lisp | 49 +++++++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 20 deletions(-) create mode 100644 tests/visual-line.lisp diff --git a/src/attribute.lisp b/src/attribute.lisp index fd2197bf0..66241f38c 100644 --- a/src/attribute.lisp +++ b/src/attribute.lisp @@ -192,6 +192,9 @@ (:light :foreground nil :background "#eedc82") (:dark :foreground nil :background "blue")) +(define-attribute fold-attribute + (t :foreground :base04)) + (define-attribute modeline (t :bold t :background "#404040" :foreground "white")) diff --git a/src/display/logical-line.lisp b/src/display/logical-line.lisp index 5b5f94d04..e51777f65 100644 --- a/src/display/logical-line.lisp +++ b/src/display/logical-line.lisp @@ -133,6 +133,22 @@ visual line each splice text out." (point< point (overlay-end overlay)) overlay))) +(defun fold-region (start end &optional (fold-marker "...")) + "hide the lines of the region [START, END), leaving START's line visible with a fold marker. +returns the fold overlay." + (with-point ((s start) + (e end)) + (line-end s) + ;; dont hide the newline that terminates the folded region's last line, or the line after + ;; the fold gets merged onto the header's visual line. + (when (start-line-p e) + (character-offset e -1)) + (let ((overlay (make-overlay s e 'fold-attribute))) + (overlay-put overlay :invisible t) + (overlay-put overlay :fold t) + (overlay-put overlay :before-string (list fold-marker 'fold-attribute)) + overlay))) + (defun line-continuation-p (point) "whether POINT's line continues a previous visual line. meaning the newline preceding it is hidden by an :invisible overlay, so the line is not a visual line of its own. diff --git a/src/ext/language-mode.lisp b/src/ext/language-mode.lisp index 0cca24512..d417adda9 100644 --- a/src/ext/language-mode.lisp +++ b/src/ext/language-mode.lisp @@ -66,9 +66,6 @@ (define-editor-variable root-uri-patterns '()) (define-editor-variable detective-search nil) -(define-attribute fold-attribute - (t :foreground :base04)) - (defun prompt-for-symbol (prompt history-name) (prompt-for-string prompt :history-symbol history-name)) @@ -136,22 +133,6 @@ (when (point< start end) (values start end)))))) -(defun fold-region (start end &optional (fold-marker "...")) - "hide the lines of the region [START, END), leaving START's line visible with a fold marker. -returns the fold overlay." - (with-point ((s start) - (e end)) - (line-end s) - ;; dont hide the newline that terminates the folded region's last line, or the line after - ;; the fold gets merged onto the header's visual line. - (when (start-line-p e) - (character-offset e -1)) - (let ((overlay (make-overlay s e 'fold-attribute))) - (overlay-put overlay :invisible t) - (overlay-put overlay :fold t) - (overlay-put overlay :before-string (list fold-marker 'fold-attribute)) - overlay))) - (defun fold-overlay-at (point) "the fold overlay whose header line is POINT's line, or NIL." (find-if diff --git a/src/internal-packages.lisp b/src/internal-packages.lisp index e28fc61ad..29e80c378 100644 --- a/src/internal-packages.lisp +++ b/src/internal-packages.lisp @@ -134,6 +134,7 @@ :define-attribute :cursor :region + :fold-attribute :modeline :modeline-inactive :truncate-attribute @@ -663,7 +664,8 @@ (:export :make-region-overlays-using-global-mode :line-continuation-p - :invisible-overlay-covering) + :invisible-overlay-covering + :fold-region) ;; interface.lisp (:export :with-implementation diff --git a/tests/visual-line.lisp b/tests/visual-line.lisp new file mode 100644 index 000000000..88a6a210a --- /dev/null +++ b/tests/visual-line.lisp @@ -0,0 +1,49 @@ +(defpackage :lem-tests/visual-line + (:use :cl :rove :lem) + (:import-from + :lem-tests/utilities + :with-testing-buffer + :make-text-buffer + :lines)) + +(in-package :lem-tests/visual-line) + +(defun point-at-line (buffer line-number) + "temporary point at the start of LINE-NUMBER (0-based) of BUFFER." + (let ((point (copy-point (buffer-start-point buffer) :temporary))) + (line-offset point line-number) + (line-start point) + point)) + +(deftest visual-line-navigation-across-folds + (lem-fake-interface:with-fake-interface () + (with-testing-buffer (buffer (make-text-buffer + (lines "AAAA" "BBBB" "CCCC" "DDDD" "EEEE" "FFFF"))) + (labels ((fold-lines (first last) + "fold buffer lines FIRST..LAST (inclusive) into a single visual line." + (fold-region (point-at-line buffer first) + (point-at-line buffer (1+ last)))) + (expect-visual-line (containing-line first-line last-line) + (testing (format + nil + "buffer line ~D belongs to the visual line spanning lines ~D..~D" + containing-line + first-line + last-line) + (with-point ((p1 (point-at-line buffer containing-line)) + (p2 (point-at-line buffer containing-line))) + (line-offset p1 0 2) + (line-offset p2 0 2) + (ok (= (position-at-point (visual-line-beginning p1)) + (position-at-point (point-at-line buffer first-line)))) + (ok (= (position-at-point (visual-line-end p2)) + (position-at-point (line-end (point-at-line buffer last-line))))))))) + (fold-lines 0 1) + (fold-lines 3 4) + ;; folded lines collapse with others, unfolded lines stand alone. + (expect-visual-line 0 0 1) + (expect-visual-line 1 0 1) + (expect-visual-line 2 2 2) + (expect-visual-line 3 3 4) + (expect-visual-line 4 3 4) + (expect-visual-line 5 5 5))))) \ No newline at end of file From 3b67950942c57c4269928b1413f131a8f05a06c7 Mon Sep 17 00:00:00 2001 From: mahmoodsheikh36 Date: Tue, 30 Jun 2026 22:48:21 +0300 Subject: [PATCH 6/9] make overlay enter/exit behavior customizable --- src/commands/move.lisp | 102 +++++++++++++++++-------- src/display/logical-line.lisp | 72 +++++++++++++++--- src/ext/language-mode.lisp | 7 +- src/internal-packages.lisp | 6 +- tests/visual-line.lisp | 138 +++++++++++++++++++++++++++++++++- 5 files changed, 278 insertions(+), 47 deletions(-) diff --git a/src/commands/move.lisp b/src/commands/move.lisp index 385d99079..da7d448a4 100644 --- a/src/commands/move.lisp +++ b/src/commands/move.lisp @@ -49,40 +49,76 @@ (defvar *cursor-positions-before-command* (make-hash-table :test 'eq) - "maps each cursor point to its absolute buffer position before the running -command, so `snap-cursors-out-of-invisible' can tell which direction it moved.") + "maps each cursor point to its absolute buffer position before the running command, so +`run-overlay-cursor-motion-hooks' can tell which direction it moved.") -(add-hook *pre-command-hook* 'record-cursor-positions) -(add-hook *post-command-hook* 'snap-cursors-out-of-invisible) +(defvar *cursor-overlays-before-command* + (make-hash-table :test 'eq) + "maps each cursor point to the cursor-hook overlays it occupied before the running command, +so `run-overlay-cursor-motion-hooks' can tell which overlays it entered and which it left.") + +(add-hook *pre-command-hook* 'snapshot-cursor-state) +(add-hook *post-command-hook* 'run-overlay-cursor-motion-hooks) -(defun record-cursor-positions () - "snapshot every cursor's position so a later snap can pick the visible edge on the far side -of a fold the cursor moves into." +(defun snapshot-cursor-state () + "snapshot every cursor's position and the cursor-hook overlays it occupies, so that after the +command `run-overlay-cursor-motion-hooks' can tell which overlays each cursor entered and left, +and in which direction." (clrhash *cursor-positions-before-command*) + (clrhash *cursor-overlays-before-command*) (dolist (point (buffer-cursors (current-buffer))) (setf (gethash point *cursor-positions-before-command*) - (position-at-point point)))) + (position-at-point point)) + (setf (gethash point *cursor-overlays-before-command*) + (overlays-with-cursor-hooks-covering point)))) + +(defun run-overlay-cursor-enter-functions (point direction) + "run the :cursor-enter-functions of every cursor-hook overlay POINT has entered since the +command, each called as (FUNCTION point overlay direction). repeats while a handler +repositions POINT so that overlays it is then pushed into also fire." + (let ((seen (copy-list (gethash point *cursor-overlays-before-command*))) + (visited (list (position-at-point point)))) + (loop + (let ((entered (set-difference (overlays-with-cursor-hooks-covering point) seen))) + (when (null entered) + (return)) + (let ((before-pass (position-at-point point))) + (dolist (overlay entered) + (push overlay seen) + (dolist (function (overlay-get overlay :cursor-enter-functions)) + (funcall function point overlay direction))) + (let ((after-pass (position-at-point point))) + ;; stop once a pass leaves POINT put, or revisits a position. the latter guards + ;; against two overlays snapping a cursor back and forth forever. + ;; this is tricky. this is definitely not the best way to do things but it works for now. + ;; the nature of overlay hooks itself is tricky anyway. + (when (or (= before-pass after-pass) + (member after-pass visited)) + (return)) + (push after-pass visited))))))) + +(defun run-overlay-cursor-leave-functions (point direction) + "run the :cursor-leave-functions of every cursor-hook overlay POINT was in before the +command, each called as (FUNCTION point overlay direction)." + (let ((left (set-difference (gethash point *cursor-overlays-before-command*) + (overlays-with-cursor-hooks-covering point)))) + (dolist (overlay left) + (dolist (function (overlay-get overlay :cursor-leave-functions)) + (funcall function point overlay direction))))) -(defun snap-cursors-out-of-invisible () - "a fold collapses its range, so no cursor may rest within it or at its start. move any such -cursor to the nearest visible position in its direction of travel, so the cursor never appears -stuck on hidden text." +(defun cursor-move-direction-overlay (point) + "the direction POINT moved during the command, :forward or :backward (:forward when its prior +position is unknown or unchanged)." + (let ((before (gethash point *cursor-positions-before-command*))) + (if (and before (< (position-at-point point) before)) + :backward + :forward))) + +(defun run-overlay-cursor-motion-hooks () (dolist (point (buffer-cursors (current-buffer))) - (let ((overlay (invisible-overlay-covering point))) - (when overlay - (let ((forwardp (let ((before (gethash point *cursor-positions-before-command*))) - (or (null before) - (<= before (position-at-point (overlay-start overlay))))))) - (loop :for ov := (invisible-overlay-covering point) - :while ov - :do (if forwardp - (move-point point (overlay-end ov)) - (progn - (move-point point (overlay-start ov)) - ;; dont continue looping if we are at the beginning of the buffer. - ;; it could cause an endless loop. - (unless (character-offset point -1) - (return)))))))))) + (let ((direction (cursor-move-direction-overlay point))) + (run-overlay-cursor-enter-functions point direction) + (run-overlay-cursor-leave-functions point direction)))) (defun visual-line-end (point) "move POINT to the end of its visual line. returns POINT." @@ -101,10 +137,16 @@ stuck on hidden text." (line-start point)) point) +(defun line-continuation-to-skip (point) + (let ((overlay (line-continuation-p point))) + (and overlay + (not (overlay-get overlay :cursor-leave-functions)) + overlay))) + (defun skip-invisible-lines (point direction) "move POINT past any fully invisible lines in DIRECTION (1 or -1). returns POINT on success, or NIL if a buffer boundary is reached." - (loop :while (line-continuation-p point) + (loop :while (line-continuation-to-skip point) :do (unless (line-offset point direction) (return-from skip-invisible-lines nil))) point) @@ -116,7 +158,7 @@ returns POINT on success, or NIL if a buffer boundary is reached." (loop :repeat steps :do (unless (move-to-next-virtual-line point dir) (return-from move-to-next-visible-virtual-line nil)) - (when (line-continuation-p point) + (when (line-continuation-to-skip point) (unless (skip-invisible-lines point dir) (return-from move-to-next-visible-virtual-line nil)))) point)) @@ -128,7 +170,7 @@ returns POINT on success, or NIL if a buffer boundary is reached." (loop :repeat steps :do (unless (line-offset point dir) (return-from visible-line-offset nil)) - (when (line-continuation-p point) + (when (line-continuation-to-skip point) (unless (skip-invisible-lines point dir) (return-from visible-line-offset nil)))) point)) diff --git a/src/display/logical-line.lisp b/src/display/logical-line.lisp index e51777f65..b6f4fbffc 100644 --- a/src/display/logical-line.lisp +++ b/src/display/logical-line.lisp @@ -133,20 +133,71 @@ visual line each splice text out." (point< point (overlay-end overlay)) overlay))) -(defun fold-region (start end &optional (fold-marker "...")) +(defun move-point-out-of-overlay (point overlay direction) + "move POINT to the nearest edge of OVERLAY in DIRECTION (:forward or :backward), so it does not +rest inside. usable directly as a :cursor-enter-functions handler; `fold-region' installs it by +default so a cursor never appears stuck on the hidden text of a fold." + (if (eq direction :backward) + (progn + (move-point point (overlay-start overlay)) + ;; step onto the last visible position before the overlay, unless that is the buffer + ;; start, where there is nowhere further to go. + (character-offset point -1)) + (move-point point (overlay-end overlay)))) + +(defun reveal-overlay-on-cursor-enter (point overlay direction) + (overlay-put overlay :invisible nil) + (overlay-put overlay :show-virtual-text nil)) + +(defun hide-overlay-on-cursor-leave (point overlay direction) + (overlay-put overlay :invisible t) + (overlay-put overlay :show-virtual-text t)) + +(defun overlay-show-virtual-text-p (overlay) + "whether OVERLAY's :before-string/:after-string should render. defaults to T." + (let ((value (getf (overlay-plist overlay) :show-virtual-text :unset))) + (if (eq value :unset) t value))) + +(defun overlay-has-cursor-hooks-p (overlay) + (or (overlay-get overlay :cursor-enter-functions) + (overlay-get overlay :cursor-leave-functions))) + +(defun overlays-with-cursor-hooks-covering (point) + "the overlays covering POINT that take part in cursor enter/leave tracking." + (loop :for overlay :in (buffer-overlays (point-buffer point)) + :when (and (overlay-has-cursor-hooks-p overlay) + (point<= (overlay-start overlay) point) + (point< point (overlay-end overlay))) + :collect overlay)) + +;; but reveal behavior isnt relevant for line folding +(defun place-region-placeholder-overlay (start end &key (placeholder "...") (cursor-behavior :move-out) (is-line-fold t)) "hide the lines of the region [START, END), leaving START's line visible with a fold marker. -returns the fold overlay." +returns the fold overlay. CURSOR-BEHAVIOR decides how the cursor is kept off the hidden text: +- :move-out :: move the cursor to the nearest visible edge (see `move-point-out-of-overlay'). +- :reveal :: open the fold while the cursor is inside it and close it again on leave (see + `reveal-overlay-on-cursor-enter' / `hide-overlay-on-cursor-leave'). +- nil :: install nothing; the cursor may rest on the hidden text. +callers can also set the overlay's :cursor-enter-functions / :cursor-leave-functions directly." (with-point ((s start) (e end)) - (line-end s) - ;; dont hide the newline that terminates the folded region's last line, or the line after - ;; the fold gets merged onto the header's visual line. - (when (start-line-p e) - (character-offset e -1)) + (when is-line-fold + (line-end s) + ;; dont hide the newline that terminates the folded region's last line, or the line after + ;; the fold gets merged onto the header's visual line. + (when (start-line-p e) + (character-offset e -1))) (let ((overlay (make-overlay s e 'fold-attribute))) (overlay-put overlay :invisible t) (overlay-put overlay :fold t) - (overlay-put overlay :before-string (list fold-marker 'fold-attribute)) + (overlay-put overlay :before-string (list placeholder 'fold-attribute)) + (ecase cursor-behavior + (:move-out + (overlay-put overlay :cursor-enter-functions (list 'move-point-out-of-overlay))) + (:reveal + (overlay-put overlay :cursor-enter-functions (list 'reveal-overlay-on-cursor-enter)) + (overlay-put overlay :cursor-leave-functions (list 'hide-overlay-on-cursor-leave))) + ((nil))) overlay))) (defun line-continuation-p (point) @@ -313,8 +364,9 @@ several folds that each hide arbitrary character ranges across multiple buffer l ;; groups them without affecting this order. (loop :for overlay :in (stable-sort (loop :for overlay :in overlays - :when (or (overlay-get overlay :before-string) - (overlay-get overlay :after-string)) + :when (and (overlay-show-virtual-text-p overlay) + (or (overlay-get overlay :before-string) + (overlay-get overlay :after-string))) :collect overlay) (lambda (a b) (let ((a-end (or (overlay-end-charpos a) (length string))) diff --git a/src/ext/language-mode.lisp b/src/ext/language-mode.lisp index d417adda9..070628741 100644 --- a/src/ext/language-mode.lisp +++ b/src/ext/language-mode.lisp @@ -146,9 +146,10 @@ (let ((fn (variable-value 'fold-region-function :default point))) (multiple-value-bind (start end) (funcall fn point) (when (and start end (not (same-line-p start end))) - (fold-region start end) - (move-point point start) - t)))) + (let ((overlay (place-region-placeholder-overlay start end :is-line-fold t))) + (move-point point start) + (overlay-put overlay :fold t) + overlay))))) (defun fold-toggle-at-point (&optional (point (current-point))) "toggle the fold at POINT. returns T when a fold was added or removed, and NIL when there was diff --git a/src/internal-packages.lisp b/src/internal-packages.lisp index 29e80c378..8a6e8b04e 100644 --- a/src/internal-packages.lisp +++ b/src/internal-packages.lisp @@ -665,7 +665,11 @@ :make-region-overlays-using-global-mode :line-continuation-p :invisible-overlay-covering - :fold-region) + :move-point-out-of-overlay + :reveal-overlay-on-cursor-enter + :hide-overlay-on-cursor-leave + :overlays-with-cursor-hooks-covering + :place-region-placeholder-overlay) ;; interface.lisp (:export :with-implementation diff --git a/tests/visual-line.lisp b/tests/visual-line.lisp index 88a6a210a..45b576e87 100644 --- a/tests/visual-line.lisp +++ b/tests/visual-line.lisp @@ -15,14 +15,27 @@ (line-start point) point)) +(defun forward-char-command () + (lem-core::save-continue-flags + (lem-core:call-command (make-instance 'lem:forward-char) nil))) + +(defun backward-char-command () + (lem-core::save-continue-flags + (lem-core:call-command (make-instance 'lem:backward-char) nil))) + +(defun next-line-command () + (lem-core::save-continue-flags + (lem-core:call-command (make-instance 'lem:next-line) nil))) + (deftest visual-line-navigation-across-folds (lem-fake-interface:with-fake-interface () (with-testing-buffer (buffer (make-text-buffer (lines "AAAA" "BBBB" "CCCC" "DDDD" "EEEE" "FFFF"))) (labels ((fold-lines (first last) "fold buffer lines FIRST..LAST (inclusive) into a single visual line." - (fold-region (point-at-line buffer first) - (point-at-line buffer (1+ last)))) + (place-region-placeholder-overlay + (point-at-line buffer first) + (point-at-line buffer (1+ last)))) (expect-visual-line (containing-line first-line last-line) (testing (format nil @@ -46,4 +59,123 @@ (expect-visual-line 2 2 2) (expect-visual-line 3 3 4) (expect-visual-line 4 3 4) - (expect-visual-line 5 5 5))))) \ No newline at end of file + (expect-visual-line 5 5 5))))) + +(defun hide-range (buffer start-charpos end-charpos &key (cursor-behavior nil)) + (with-point ((start (buffer-start-point buffer)) + (end (buffer-start-point buffer))) + (character-offset start start-charpos) + (character-offset end end-charpos) + (place-region-placeholder-overlay + start + end + :is-line-fold nil + :cursor-behavior cursor-behavior))) + +;; move-point-out-of-overlay (the :move-out handler) moves the cursor out of an invisible +;; overlay's span +(deftest cursor-moves-out-of-invisible-overlay + (lem-fake-interface:with-fake-interface () + (with-testing-buffer (buffer (make-text-buffer (lines "ABCDEFGH"))) + (lem-core::set-window-buffer buffer (current-window)) + ;; hides "EFG" + (let ((overlay (hide-range buffer 4 7 :cursor-behavior :move-out)) + (point (buffer-point buffer))) + (move-point point (buffer-start-point buffer)) + ;; ABC|DEFGH, just before the range + (character-offset point 3) + ;; ABCD|EFGH, enters the range, gets moved out + (forward-char-command) + (ok (null (invisible-overlay-covering point)) "not left on hidden text") + (ok (point= point (overlay-end overlay)) "pushed forward out of the span") + ;; entering from the far side throws the cursor out the other way + (backward-char-command) + (ok (null (invisible-overlay-covering point)) "not left on hidden text") + (ok (point< point (overlay-start overlay)) "pushed backward out of the span"))))) + +;; reveal-overlay-on-cursor-enter / hide-overlay-on-cursor-leave toggle :invisible as the cursor +;; enters and leaves. +(deftest cursor-reveals-and-hides-invisible-overlay + (lem-fake-interface:with-fake-interface () + (with-testing-buffer (buffer (make-text-buffer (lines "ABCDEFGH"))) + (lem-core::set-window-buffer buffer (current-window)) + (let ((overlay (hide-range buffer 4 7 :cursor-behavior :reveal)) + (point (buffer-point buffer))) + (ok (eq t (overlay-get overlay :invisible)) "starts hidden") + ;; step from just before the hidden range into it; entering must reveal it. + (move-point point (buffer-start-point buffer)) + ;; ABC|DEFGH, just before the range + (character-offset point 3) + ;; ABCD|EFGH, cursor enters the range, text gets revealed + (forward-char-command) + (ok (null (overlay-get overlay :invisible)) "revealed on enter") + ;; keep walking until the cursor passes the far edge. leaving must hide it again. + (loop :until (point<= (overlay-end overlay) point) + :do (forward-char-command)) + (ok (eq t (overlay-get overlay :invisible)) "hidden again on leave"))))) + +;; move-point-out-of-overlay also moves the cursor out of a :move-out fold whose hidden span +;; crosses a newline (an arbitrary line-collapsing fold), so forward-char/backward-char never get +;; stuck on the hidden text when stepping in from either side. +(deftest cursor-moves-out-of-newline-crossing-fold + (lem-fake-interface:with-fake-interface () + (with-testing-buffer (buffer (make-text-buffer + (lines "abc DEF ghi" "JKL mno PQR" "stu VWX yz"))) + (lem-core::set-window-buffer buffer (current-window)) + ;; hide "DEF ghi" + newline + "JKL " + (with-point ((start (point-at-line buffer 0)) + (end (point-at-line buffer 1))) + (character-offset start 4) + (character-offset end 4) + (let ((overlay (place-region-placeholder-overlay + start + end + :is-line-fold nil + :cursor-behavior :move-out)) + (point (buffer-point buffer))) + ;; "abc |DEF..." just before the span. entering it pushes out the forward edge. + (move-point point (point-at-line buffer 0)) + (character-offset point 3) + (forward-char-command) + (ok (null (invisible-overlay-covering point)) "not left on hidden text") + (ok (point= point (overlay-end overlay)) "pushed forward out of the span") + ;; entering from the far side throws the cursor out the other way + (backward-char-command) + (ok (null (invisible-overlay-covering point)) "not left on hidden text") + (ok (point< point (overlay-start overlay)) "pushed backward out of the span")))))) + +;; vertical motion steps clear of a :move-out fold (the default, e.g. a folded defun): next-line +;; from the fold header lands on the first visible line past the hidden lines, not inside them. +(deftest next-line-skips-move-out-fold + (lem-fake-interface:with-fake-interface () + (with-testing-buffer (buffer (make-text-buffer + (lines "AAAA" "BBBB" "CCCC" "DDDD" "EEEE" "FFFF"))) + (lem-core::set-window-buffer buffer (current-window)) + (place-region-placeholder-overlay (point-at-line buffer 1) + (point-at-line buffer 4)) + (let ((point (buffer-point buffer))) + (move-point point (point-at-line buffer 1)) + (next-line-command) + (ok (null (invisible-overlay-covering point)) "not left on a hidden line") + (ok (= (position-at-point point) + (position-at-point (point-at-line buffer 4))) + "next-line landed on the first visible line past the fold"))))) + +;; vertical motion enters a :reveal fold: next-line from the header lands on the first hidden line +;; and the post-command cursor-enter hook reveals it. +(deftest next-line-enters-and-reveals-reveal-fold + (lem-fake-interface:with-fake-interface () + (with-testing-buffer (buffer (make-text-buffer + (lines "AAAA" "BBBB" "CCCC" "DDDD" "EEEE" "FFFF"))) + (lem-core::set-window-buffer buffer (current-window)) + (let ((overlay (place-region-placeholder-overlay (point-at-line buffer 1) + (point-at-line buffer 4) + :cursor-behavior :reveal)) + (point (buffer-point buffer))) + (ok (eq t (overlay-get overlay :invisible)) "starts hidden") + (move-point point (point-at-line buffer 1)) + (next-line-command) + (ok (null (overlay-get overlay :invisible)) "revealed after entering on next-line") + (ok (= (position-at-point point) + (position-at-point (point-at-line buffer 2))) + "cursor landed inside the fold on the first hidden line"))))) \ No newline at end of file From 46c45736a453aa26f1e2b5fe67dd8ba5e854fd2a Mon Sep 17 00:00:00 2001 From: mahmoodsheikh36 Date: Wed, 1 Jul 2026 23:21:57 +0300 Subject: [PATCH 7/9] fold only when on first line of defun --- src/ext/language-mode.lisp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ext/language-mode.lisp b/src/ext/language-mode.lisp index 070628741..af6045534 100644 --- a/src/ext/language-mode.lisp +++ b/src/ext/language-mode.lisp @@ -145,7 +145,9 @@ "fold the defun at POINT. Returns T when something was folded." (let ((fn (variable-value 'fold-region-function :default point))) (multiple-value-bind (start end) (funcall fn point) - (when (and start end (not (same-line-p start end))) + (when (and start end + (not (same-line-p start end)) + (same-line-p start point)) (let ((overlay (place-region-placeholder-overlay start end :is-line-fold t))) (move-point point start) (overlay-put overlay :fold t) From 0a7868495da9e0216399f741093861d4da81585a Mon Sep 17 00:00:00 2001 From: mahmoodsheikh36 Date: Fri, 3 Jul 2026 17:06:55 +0300 Subject: [PATCH 8/9] fix scrolling issue when moving the cursor down there was an issue where the screen randomly jumps a few lines down --- src/window/virtual-line.lisp | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/window/virtual-line.lisp b/src/window/virtual-line.lisp index 3d314759b..d71d36bf5 100644 --- a/src/window/virtual-line.lisp +++ b/src/window/virtual-line.lisp @@ -95,9 +95,30 @@ next line because it is at the end of width." #'inc))) offset))) +(defun count-hidden-lines (start-point end-point) + "number of hidden buffer lines between START-POINT and END-POINT." + (when (point< end-point start-point) + (rotatef start-point end-point)) + (with-point ((p start-point) + (goal end-point)) + (line-start p) + (line-start goal) + (loop :with count := 0 + :until (same-line-p p goal) + :do (unless (line-offset p 1) + (return count)) + (when (line-continuation-p p) + (incf count)) + :finally (return count)))) + (defun window-cursor-y-not-wrapping (window) - (count-lines (window-buffer-point window) - (window-view-point window))) + "number of screen rows between the view point and the cursor. +excludes lines hidden by overlays with :invisible property because those lines dont occupy any +vertical space." + (let ((view-point (window-view-point window)) + (buffer-point (window-buffer-point window))) + (- (count-lines buffer-point view-point) + (count-hidden-lines view-point buffer-point)))) (defun window-cursor-y (window) (if (point< (window-buffer-point window) From 039b7ddee685e7a0f59db8c0e9309054182657d1 Mon Sep 17 00:00:00 2001 From: mahmoodsheikh36 Date: Sat, 4 Jul 2026 22:38:19 +0300 Subject: [PATCH 9/9] support for displaying images in webview --- .../server/frontend/dist/assets/index.css | 2 +- .../server/frontend/dist/assets/index.js | 2 +- frontends/server/frontend/editor.js | 101 +++++++++++++++++- frontends/server/main.lisp | 83 +++++++++++++- src/display/physical-line.lisp | 12 +-- src/internal-packages.lisp | 1 + 6 files changed, 181 insertions(+), 20 deletions(-) diff --git a/frontends/server/frontend/dist/assets/index.css b/frontends/server/frontend/dist/assets/index.css index 3dfcc503b..d88bed8c2 100644 --- a/frontends/server/frontend/dist/assets/index.css +++ b/frontends/server/frontend/dist/assets/index.css @@ -1 +1 @@ -@keyframes lem-cursor-blink{0%,to{opacity:1}50%{opacity:0}}.lem-cursor{position:absolute;pointer-events:none;z-index:300;animation:lem-cursor-blink 1s step-end infinite;overflow:hidden;white-space:pre;line-height:1;box-sizing:border-box}.lem-editor__floating-window--bordered{padding:10px;border:none;border-radius:8px;box-shadow:4px 4px 16px #000,0 0 0 1px #80808033;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.lem-editor__mode-line{border:none;border-radius:4px;box-shadow:0 0 0 1px #80808080}.lem-editor__vertical-border{width:10px;background:linear-gradient(to right,transparent 4px,rgba(128,128,128,.5) 4px 6px,transparent 5px)}.lem-editor__horizontal-border{height:10px;background:linear-gradient(to bottom,transparent 0px,rgba(128,128,128,.5) 3px 5px,transparent 2px)} +@keyframes lem-cursor-blink{0%,to{opacity:1}50%{opacity:0}}.lem-cursor{pointer-events:none;z-index:300;white-space:pre;box-sizing:border-box;line-height:1;animation:1s step-end infinite lem-cursor-blink;position:absolute;overflow:hidden}.lem-editor__floating-window--bordered{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:none;border-radius:8px;padding:10px;box-shadow:4px 4px 16px #000,0 0 0 1px #80808033}.lem-editor__mode-line{border:none;border-radius:4px;box-shadow:0 0 0 1px #80808080}.lem-editor__vertical-border{background:linear-gradient(90deg,#0000 4px,#80808080 4px 6px,#0000 5px);width:10px}.lem-editor__horizontal-border{background:linear-gradient(#0000 0,#80808080 3px 5px,#0000 2px);height:10px} diff --git a/frontends/server/frontend/dist/assets/index.js b/frontends/server/frontend/dist/assets/index.js index a036fd0e9..1cc125100 100644 --- a/frontends/server/frontend/dist/assets/index.js +++ b/frontends/server/frontend/dist/assets/index.js @@ -1 +1 @@ -(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const n of document.querySelectorAll('link[rel="modulepreload"]'))i(n);new MutationObserver(n=>{for(const s of n)if(s.type==="childList")for(const r of s.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&i(r)}).observe(document,{childList:!0,subtree:!0});function t(n){const s={};return n.integrity&&(s.integrity=n.integrity),n.referrerPolicy&&(s.referrerPolicy=n.referrerPolicy),n.crossOrigin==="use-credentials"?s.credentials="include":n.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function i(n){if(n.ep)return;n.ep=!0;const s=t(n);fetch(n.href,s)}})();var dist={},client={},models={},hasRequiredModels;function requireModels(){return hasRequiredModels||(hasRequiredModels=1,function(o){var e=models&&models.__extends||function(){var u=function(h,a){return u=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(c,p){c.__proto__=p}||function(c,p){for(var w in p)Object.prototype.hasOwnProperty.call(p,w)&&(c[w]=p[w])},u(h,a)};return function(h,a){if(typeof a!="function"&&a!==null)throw new TypeError("Class extends value "+String(a)+" is not a constructor or null");u(h,a);function c(){this.constructor=h}h.prototype=a===null?Object.create(a):(c.prototype=a.prototype,new c)}}();Object.defineProperty(o,"__esModule",{value:!0}),o.createJSONRPCNotification=o.createJSONRPCRequest=o.createJSONRPCSuccessResponse=o.createJSONRPCErrorResponse=o.JSONRPCErrorCode=o.JSONRPCErrorException=o.isJSONRPCResponses=o.isJSONRPCResponse=o.isJSONRPCRequests=o.isJSONRPCRequest=o.isJSONRPCID=o.JSONRPC=void 0,o.JSONRPC="2.0";var t=function(u){return typeof u=="string"||typeof u=="number"||u===null};o.isJSONRPCID=t;var i=function(u){return u.jsonrpc===o.JSONRPC&&u.method!==void 0&&u.result===void 0&&u.error===void 0};o.isJSONRPCRequest=i;var n=function(u){return Array.isArray(u)&&u.every(o.isJSONRPCRequest)};o.isJSONRPCRequests=n;var s=function(u){return u.jsonrpc===o.JSONRPC&&u.id!==void 0&&(u.result!==void 0||u.error!==void 0)};o.isJSONRPCResponse=s;var r=function(u){return Array.isArray(u)&&u.every(o.isJSONRPCResponse)};o.isJSONRPCResponses=r;var l=function(u,h,a){var c={code:u,message:h};return a!=null&&(c.data=a),c},f=function(u){e(h,u);function h(a,c,p){var w=u.call(this,a)||this;return Object.setPrototypeOf(w,h.prototype),w.code=c,w.data=p,w}return h.prototype.toObject=function(){return l(this.code,this.message,this.data)},h}(Error);o.JSONRPCErrorException=f,function(u){u[u.ParseError=-32700]="ParseError",u[u.InvalidRequest=-32600]="InvalidRequest",u[u.MethodNotFound=-32601]="MethodNotFound",u[u.InvalidParams=-32602]="InvalidParams",u[u.InternalError=-32603]="InternalError"}(o.JSONRPCErrorCode||(o.JSONRPCErrorCode={}));var d=function(u,h,a,c){return{jsonrpc:o.JSONRPC,id:u,error:l(h,a,c)}};o.createJSONRPCErrorResponse=d;var y=function(u,h){return{jsonrpc:o.JSONRPC,id:u,result:h??null}};o.createJSONRPCSuccessResponse=y;var v=function(u,h,a){return{jsonrpc:o.JSONRPC,id:u,method:h,params:a}};o.createJSONRPCRequest=v;var N=function(u,h){return{jsonrpc:o.JSONRPC,method:u,params:h}};o.createJSONRPCNotification=N}(models)),models}var internal={},hasRequiredInternal;function requireInternal(){return hasRequiredInternal||(hasRequiredInternal=1,Object.defineProperty(internal,"__esModule",{value:!0}),internal.DefaultErrorCode=void 0,internal.DefaultErrorCode=0),internal}var hasRequiredClient;function requireClient(){if(hasRequiredClient)return client;hasRequiredClient=1;var o=client&&client.__awaiter||function(r,l,f,d){function y(v){return v instanceof f?v:new f(function(N){N(v)})}return new(f||(f=Promise))(function(v,N){function u(c){try{a(d.next(c))}catch(p){N(p)}}function h(c){try{a(d.throw(c))}catch(p){N(p)}}function a(c){c.done?v(c.value):y(c.value).then(u,h)}a((d=d.apply(r,l||[])).next())})},e=client&&client.__generator||function(r,l){var f={label:0,sent:function(){if(v[0]&1)throw v[1];return v[1]},trys:[],ops:[]},d,y,v,N;return N={next:u(0),throw:u(1),return:u(2)},typeof Symbol=="function"&&(N[Symbol.iterator]=function(){return this}),N;function u(a){return function(c){return h([a,c])}}function h(a){if(d)throw new TypeError("Generator is already executing.");for(;N&&(N=0,a[0]&&(f=0)),f;)try{if(d=1,y&&(v=a[0]&2?y.return:a[0]?y.throw||((v=y.return)&&v.call(y),0):y.next)&&!(v=v.call(y,a[1])).done)return v;switch(y=0,v&&(a=[a[0]&2,v.value]),a[0]){case 0:case 1:v=a;break;case 4:return f.label++,{value:a[1],done:!1};case 5:f.label++,y=a[1],a=[0];continue;case 7:a=f.ops.pop(),f.trys.pop();continue;default:if(v=f.trys,!(v=v.length>0&&v[v.length-1])&&(a[0]===6||a[0]===2)){f=0;continue}if(a[0]===3&&(!v||a[1]>v[0]&&a[1]0&&m[m.length-1])&&(A[0]===6||A[0]===2)){p=0;continue}if(A[0]===3&&(!m||A[1]>m[0]&&A[1]0&&d[d.length-1])&&(u[0]===6||u[0]===2)){r=0;continue}if(u[0]===3&&(!d||u[1]>d[0]&&u[1]{const[t,i,n]=e;this.requestInternal(t,i,n)}),this.messageQueue=[]}request(e,t,i){this.webSocket.readyState===WebSocket.OPEN?this.requestInternal(e,t,i):this.messageQueue.push([e,t,i])}notify(e,t){switch(this.webSocket.readyState){case WebSocket.OPEN:this.serverAndClient.notify(e,t);break}}connect(e){this.closed||(console.log("connect",this.url),this.webSocket=new WebSocket(this.url),this.serverAndClient||(this.serverAndClient=new distExports.JSONRPCServerAndClient(new distExports.JSONRPCServer,new distExports.JSONRPCClient(t=>{try{return this.webSocket.send(JSON.stringify(t)),Promise.resolve()}catch(i){return Promise.reject(i)}}))),this.webSocket.onmessage=t=>{this.serverAndClient.receiveAndSend(JSON.parse(t.data.toString()))},this.webSocket.onopen=()=>{console.log("WebSocket connection established"),this.connectionEstablished=!0,this.onConnected&&this.onConnected(),this.requestMessageQueue()},this.webSocket.onclose=t=>{console.error("WebScoket closed",t),this.serverAndClient.rejectAllPendingRequests(`Connection is closed (${t.reason}).`),this.connectionEstablished&&this.onClosed(),this.timerId=setTimeout(()=>{this.connect()},3e3)},this.webSocket.onerror=t=>{console.error("WebSocket error:",t),this.webSocket.close()})}}const modifierKeys=["Shift","Control","Alt","Meta","CapsLock"],convertKeyTable={Enter:"Return",ArrowRight:"Right",ArrowLeft:"Left",ArrowUp:"Up",ArrowDown:"Down","¡":"1","™":"2","£":"3","¢":"4","∞":"5","§":"6","¶":"7","•":"8",ª:"9",º:"0","–":"-","≠":"=","“":"[","‘":"]","«":"\\","…":";",æ:"'","≤":",","≥":".","÷":"/","⁄":"!","€":"@","‹":"#","›":"$",fi:"%",fl:"^","‡":"&","°":"*","·":"(","‚":")","—":"_","±":"+","”":"{","’":"}","»":"|",Ú:":",Æ:'"',"¯":"<","˘":">","¿":"?",œ:"q","∑":"w","´":"e","®":"r","†":"t","¥":"y","¨":"u","ˆ":"i",ø:"o",π:"p",å:"a",ß:"s","∂":"d",ƒ:"f","©":"g","˙":"h","∆":"j","˚":"k","¬":"l",Ω:"z","≈":"x",ç:"c","√":"v","∫":"b","˜":"n",µ:"m",Œ:"Q","„":"W","´":"E","‰":"R","ˇ":"T",Á:"Y","¨":"U","ˆ":"I",Ø:"O","∏":"P",Å:"A",Í:"S",Î:"D",Ï:"F","˝":"G",Ó:"H",Ô:"J","":"K",Ò:"L","¸":"Z","˛":"X",Ç:"C","◊":"V",ı:"B","˜":"N",Â:"M"};function getKey(o){return o.altKey?convertKeyTable[o.key]||(o.code.startsWith("Key")?o.code[3].toLowerCase():null)||o.key:convertKeyTable[o.key]||o.key}function convertKeyEvent(o){return modifierKeys.indexOf(o.key)!==-1?null:{key:getKey(o),ctrl:o.ctrlKey,meta:o.altKey,super:o.metaKey,shift:o.shiftKey}}var defs=[[0,31,"N"],[32,126,"Na"],[127,160,"N"],[161,161,"A"],[162,163,"Na"],[164,164,"A"],[165,166,"Na"],[167,168,"A"],[169,169,"N"],[170,170,"A"],[171,171,"N"],[172,172,"Na"],[173,174,"A"],[175,175,"Na"],[176,180,"A"],[181,181,"N"],[182,186,"A"],[187,187,"N"],[188,191,"A"],[192,197,"N"],[198,198,"A"],[199,207,"N"],[208,208,"A"],[209,214,"N"],[215,216,"A"],[217,221,"N"],[222,225,"A"],[226,229,"N"],[230,230,"A"],[231,231,"N"],[232,234,"A"],[235,235,"N"],[236,237,"A"],[238,239,"N"],[240,240,"A"],[241,241,"N"],[242,243,"A"],[244,246,"N"],[247,250,"A"],[251,251,"N"],[252,252,"A"],[253,253,"N"],[254,254,"A"],[255,256,"N"],[257,257,"A"],[258,272,"N"],[273,273,"A"],[274,274,"N"],[275,275,"A"],[276,282,"N"],[283,283,"A"],[284,293,"N"],[294,295,"A"],[296,298,"N"],[299,299,"A"],[300,304,"N"],[305,307,"A"],[308,311,"N"],[312,312,"A"],[313,318,"N"],[319,322,"A"],[323,323,"N"],[324,324,"A"],[325,327,"N"],[328,331,"A"],[332,332,"N"],[333,333,"A"],[334,337,"N"],[338,339,"A"],[340,357,"N"],[358,359,"A"],[360,362,"N"],[363,363,"A"],[364,461,"N"],[462,462,"A"],[463,463,"N"],[464,464,"A"],[465,465,"N"],[466,466,"A"],[467,467,"N"],[468,468,"A"],[469,469,"N"],[470,470,"A"],[471,471,"N"],[472,472,"A"],[473,473,"N"],[474,474,"A"],[475,475,"N"],[476,476,"A"],[477,592,"N"],[593,593,"A"],[594,608,"N"],[609,609,"A"],[610,707,"N"],[708,708,"A"],[709,710,"N"],[711,711,"A"],[712,712,"N"],[713,715,"A"],[716,716,"N"],[717,717,"A"],[718,719,"N"],[720,720,"A"],[721,727,"N"],[728,731,"A"],[732,732,"N"],[733,733,"A"],[734,734,"N"],[735,735,"A"],[736,767,"N"],[768,879,"A"],[880,912,"N"],[913,929,"A"],[930,930,"N"],[931,937,"A"],[938,944,"N"],[945,961,"A"],[962,962,"N"],[963,969,"A"],[970,1024,"N"],[1025,1025,"A"],[1026,1039,"N"],[1040,1103,"A"],[1104,1104,"N"],[1105,1105,"A"],[1106,4351,"N"],[4352,4447,"W"],[4448,8207,"N"],[8208,8208,"A"],[8209,8210,"N"],[8211,8214,"A"],[8215,8215,"N"],[8216,8217,"A"],[8218,8219,"N"],[8220,8221,"A"],[8222,8223,"N"],[8224,8226,"A"],[8227,8227,"N"],[8228,8231,"A"],[8232,8239,"N"],[8240,8240,"A"],[8241,8241,"N"],[8242,8243,"A"],[8244,8244,"N"],[8245,8245,"A"],[8246,8250,"N"],[8251,8251,"A"],[8252,8253,"N"],[8254,8254,"A"],[8255,8307,"N"],[8308,8308,"A"],[8309,8318,"N"],[8319,8319,"A"],[8320,8320,"N"],[8321,8324,"A"],[8325,8360,"N"],[8361,8361,"H"],[8362,8363,"N"],[8364,8364,"A"],[8365,8450,"N"],[8451,8451,"A"],[8452,8452,"N"],[8453,8453,"A"],[8454,8456,"N"],[8457,8457,"A"],[8458,8466,"N"],[8467,8467,"A"],[8468,8469,"N"],[8470,8470,"A"],[8471,8480,"N"],[8481,8482,"A"],[8483,8485,"N"],[8486,8486,"A"],[8487,8490,"N"],[8491,8491,"A"],[8492,8530,"N"],[8531,8532,"A"],[8533,8538,"N"],[8539,8542,"A"],[8543,8543,"N"],[8544,8555,"A"],[8556,8559,"N"],[8560,8569,"A"],[8570,8584,"N"],[8585,8585,"A"],[8586,8591,"N"],[8592,8601,"A"],[8602,8631,"N"],[8632,8633,"A"],[8634,8657,"N"],[8658,8658,"A"],[8659,8659,"N"],[8660,8660,"A"],[8661,8678,"N"],[8679,8679,"A"],[8680,8703,"N"],[8704,8704,"A"],[8705,8705,"N"],[8706,8707,"A"],[8708,8710,"N"],[8711,8712,"A"],[8713,8714,"N"],[8715,8715,"A"],[8716,8718,"N"],[8719,8719,"A"],[8720,8720,"N"],[8721,8721,"A"],[8722,8724,"N"],[8725,8725,"A"],[8726,8729,"N"],[8730,8730,"A"],[8731,8732,"N"],[8733,8736,"A"],[8737,8738,"N"],[8739,8739,"A"],[8740,8740,"N"],[8741,8741,"A"],[8742,8742,"N"],[8743,8748,"A"],[8749,8749,"N"],[8750,8750,"A"],[8751,8755,"N"],[8756,8759,"A"],[8760,8763,"N"],[8764,8765,"A"],[8766,8775,"N"],[8776,8776,"A"],[8777,8779,"N"],[8780,8780,"A"],[8781,8785,"N"],[8786,8786,"A"],[8787,8799,"N"],[8800,8801,"A"],[8802,8803,"N"],[8804,8807,"A"],[8808,8809,"N"],[8810,8811,"A"],[8812,8813,"N"],[8814,8815,"A"],[8816,8833,"N"],[8834,8835,"A"],[8836,8837,"N"],[8838,8839,"A"],[8840,8852,"N"],[8853,8853,"A"],[8854,8856,"N"],[8857,8857,"A"],[8858,8868,"N"],[8869,8869,"A"],[8870,8894,"N"],[8895,8895,"A"],[8896,8977,"N"],[8978,8978,"A"],[8979,8985,"N"],[8986,8987,"W"],[8988,9e3,"N"],[9001,9002,"W"],[9003,9192,"N"],[9193,9196,"W"],[9197,9199,"N"],[9200,9200,"W"],[9201,9202,"N"],[9203,9203,"W"],[9204,9311,"N"],[9312,9449,"A"],[9450,9450,"N"],[9451,9547,"A"],[9548,9551,"N"],[9552,9587,"A"],[9588,9599,"N"],[9600,9615,"A"],[9616,9617,"N"],[9618,9621,"A"],[9622,9631,"N"],[9632,9633,"A"],[9634,9634,"N"],[9635,9641,"A"],[9642,9649,"N"],[9650,9651,"A"],[9652,9653,"N"],[9654,9655,"A"],[9656,9659,"N"],[9660,9661,"A"],[9662,9663,"N"],[9664,9665,"A"],[9666,9669,"N"],[9670,9672,"A"],[9673,9674,"N"],[9675,9675,"A"],[9676,9677,"N"],[9678,9681,"A"],[9682,9697,"N"],[9698,9701,"A"],[9702,9710,"N"],[9711,9711,"A"],[9712,9724,"N"],[9725,9726,"W"],[9727,9732,"N"],[9733,9734,"A"],[9735,9736,"N"],[9737,9737,"A"],[9738,9741,"N"],[9742,9743,"A"],[9744,9747,"N"],[9748,9749,"W"],[9750,9755,"N"],[9756,9756,"A"],[9757,9757,"N"],[9758,9758,"A"],[9759,9791,"N"],[9792,9792,"A"],[9793,9793,"N"],[9794,9794,"A"],[9795,9799,"N"],[9800,9811,"W"],[9812,9823,"N"],[9824,9825,"A"],[9826,9826,"N"],[9827,9829,"A"],[9830,9830,"N"],[9831,9834,"A"],[9835,9835,"N"],[9836,9837,"A"],[9838,9838,"N"],[9839,9839,"A"],[9840,9854,"N"],[9855,9855,"W"],[9856,9874,"N"],[9875,9875,"W"],[9876,9885,"N"],[9886,9887,"A"],[9888,9888,"N"],[9889,9889,"W"],[9890,9897,"N"],[9898,9899,"W"],[9900,9916,"N"],[9917,9918,"W"],[9919,9919,"A"],[9920,9923,"N"],[9924,9925,"W"],[9926,9933,"A"],[9934,9934,"W"],[9935,9939,"A"],[9940,9940,"W"],[9941,9953,"A"],[9954,9954,"N"],[9955,9955,"A"],[9956,9959,"N"],[9960,9961,"A"],[9962,9962,"W"],[9963,9969,"A"],[9970,9971,"W"],[9972,9972,"A"],[9973,9973,"W"],[9974,9977,"A"],[9978,9978,"W"],[9979,9980,"A"],[9981,9981,"W"],[9982,9983,"A"],[9984,9988,"N"],[9989,9989,"W"],[9990,9993,"N"],[9994,9995,"W"],[9996,10023,"N"],[10024,10024,"W"],[10025,10044,"N"],[10045,10045,"A"],[10046,10059,"N"],[10060,10060,"W"],[10061,10061,"N"],[10062,10062,"W"],[10063,10066,"N"],[10067,10069,"W"],[10070,10070,"N"],[10071,10071,"W"],[10072,10101,"N"],[10102,10111,"A"],[10112,10132,"N"],[10133,10135,"W"],[10136,10159,"N"],[10160,10160,"W"],[10161,10174,"N"],[10175,10175,"W"],[10176,10213,"N"],[10214,10221,"Na"],[10222,10628,"N"],[10629,10630,"Na"],[10631,11034,"N"],[11035,11036,"W"],[11037,11087,"N"],[11088,11088,"W"],[11089,11092,"N"],[11093,11093,"W"],[11094,11097,"A"],[11098,11903,"N"],[11904,11929,"W"],[11930,11930,"N"],[11931,12019,"W"],[12020,12031,"N"],[12032,12245,"W"],[12246,12271,"N"],[12272,12287,"W"],[12288,12288,"F"],[12289,12350,"W"],[12351,12352,"N"],[12353,12438,"W"],[12439,12440,"N"],[12441,12543,"W"],[12544,12548,"N"],[12549,12591,"W"],[12592,12592,"N"],[12593,12686,"W"],[12687,12687,"N"],[12688,12771,"W"],[12772,12782,"N"],[12783,12830,"W"],[12831,12831,"N"],[12832,12871,"W"],[12872,12879,"A"],[12880,19903,"W"],[19904,19967,"N"],[19968,42124,"W"],[42125,42127,"N"],[42128,42182,"W"],[42183,43359,"N"],[43360,43388,"W"],[43389,44031,"N"],[44032,55203,"W"],[55204,57343,"N"],[57344,63743,"A"],[63744,64255,"W"],[64256,65023,"N"],[65024,65039,"A"],[65040,65049,"W"],[65050,65071,"N"],[65072,65106,"W"],[65107,65107,"N"],[65108,65126,"W"],[65127,65127,"N"],[65128,65131,"W"],[65132,65280,"N"],[65281,65376,"F"],[65377,65470,"H"],[65471,65473,"N"],[65474,65479,"H"],[65480,65481,"N"],[65482,65487,"H"],[65488,65489,"N"],[65490,65495,"H"],[65496,65497,"N"],[65498,65500,"H"],[65501,65503,"N"],[65504,65510,"F"],[65511,65511,"N"],[65512,65518,"H"],[65519,65532,"N"],[65533,65533,"A"],[65534,94175,"N"],[94176,94180,"W"],[94181,94191,"N"],[94192,94193,"W"],[94194,94207,"N"],[94208,100343,"W"],[100344,100351,"N"],[100352,101589,"W"],[101590,101631,"N"],[101632,101640,"W"],[101641,110575,"N"],[110576,110579,"W"],[110580,110580,"N"],[110581,110587,"W"],[110588,110588,"N"],[110589,110590,"W"],[110591,110591,"N"],[110592,110882,"W"],[110883,110897,"N"],[110898,110898,"W"],[110899,110927,"N"],[110928,110930,"W"],[110931,110932,"N"],[110933,110933,"W"],[110934,110947,"N"],[110948,110951,"W"],[110952,110959,"N"],[110960,111355,"W"],[111356,126979,"N"],[126980,126980,"W"],[126981,127182,"N"],[127183,127183,"W"],[127184,127231,"N"],[127232,127242,"A"],[127243,127247,"N"],[127248,127277,"A"],[127278,127279,"N"],[127280,127337,"A"],[127338,127343,"N"],[127344,127373,"A"],[127374,127374,"W"],[127375,127376,"A"],[127377,127386,"W"],[127387,127404,"A"],[127405,127487,"N"],[127488,127490,"W"],[127491,127503,"N"],[127504,127547,"W"],[127548,127551,"N"],[127552,127560,"W"],[127561,127567,"N"],[127568,127569,"W"],[127570,127583,"N"],[127584,127589,"W"],[127590,127743,"N"],[127744,127776,"W"],[127777,127788,"N"],[127789,127797,"W"],[127798,127798,"N"],[127799,127868,"W"],[127869,127869,"N"],[127870,127891,"W"],[127892,127903,"N"],[127904,127946,"W"],[127947,127950,"N"],[127951,127955,"W"],[127956,127967,"N"],[127968,127984,"W"],[127985,127987,"N"],[127988,127988,"W"],[127989,127991,"N"],[127992,128062,"W"],[128063,128063,"N"],[128064,128064,"W"],[128065,128065,"N"],[128066,128252,"W"],[128253,128254,"N"],[128255,128317,"W"],[128318,128330,"N"],[128331,128334,"W"],[128335,128335,"N"],[128336,128359,"W"],[128360,128377,"N"],[128378,128378,"W"],[128379,128404,"N"],[128405,128406,"W"],[128407,128419,"N"],[128420,128420,"W"],[128421,128506,"N"],[128507,128591,"W"],[128592,128639,"N"],[128640,128709,"W"],[128710,128715,"N"],[128716,128716,"W"],[128717,128719,"N"],[128720,128722,"W"],[128723,128724,"N"],[128725,128727,"W"],[128728,128731,"N"],[128732,128735,"W"],[128736,128746,"N"],[128747,128748,"W"],[128749,128755,"N"],[128756,128764,"W"],[128765,128991,"N"],[128992,129003,"W"],[129004,129007,"N"],[129008,129008,"W"],[129009,129291,"N"],[129292,129338,"W"],[129339,129339,"N"],[129340,129349,"W"],[129350,129350,"N"],[129351,129535,"W"],[129536,129647,"N"],[129648,129660,"W"],[129661,129663,"N"],[129664,129672,"W"],[129673,129679,"N"],[129680,129725,"W"],[129726,129726,"N"],[129727,129733,"W"],[129734,129741,"N"],[129742,129755,"W"],[129756,129759,"N"],[129760,129768,"W"],[129769,129775,"N"],[129776,129784,"W"],[129785,131071,"N"],[131072,196605,"W"],[196606,196607,"N"],[196608,262141,"W"],[262142,917759,"N"],[917760,917999,"A"],[918e3,983039,"N"],[983040,1048573,"A"],[1048574,1048575,"N"],[1048576,1114109,"A"],[1114110,1114111,"N"]];function getEAWOfCodePoint(o){let e=0,t=defs.length-1;for(;e!==t;){const i=e+(t-e>>1),[n,s,r]=defs[i];if(os)e=i+1;else return r}return defs[e][2]}function getEAW(o,e=0){const t=o.codePointAt(e);if(t!==void 0)return getEAWOfCodePoint(t)}const textOffsetY=5,isSafari=/^((?!chrome|android).)*safari/i.test(navigator.userAgent);function isWideChar(o){switch(getEAW(o)){case"F":case"W":return!0;case"A":default:return!1}}function isMacOS(){return window.navigator.userAgent.indexOf("Mac OS X")!==-1}function computeFontSize(o){const t=document.createElement("canvas").getContext("2d");t.font=o;const i=t.measureText("W");return[Math.floor(i.width),Math.round(i.fontBoundingBoxAscent+textOffsetY+(i.emHeightDescent||0))]}function drawBlock({ctx:o,x:e,y:t,width:i,height:n,style:s}){o.fillStyle=s,o.fillRect(e,t,i,n)}function drawText({ctx:o,x:e,y:t,text:i,font:n,style:s,option:r}){t+=Math.round(textOffsetY),o.fillStyle=s,o.font=n,o.textBaseline="top";for(const l of i)isWideChar(l)?(o.fillText(l,e,t,r.fontWidth*2),e+=r.fontWidth*2):(o.fillText(l,e,t,r.fontWidth),e+=r.fontWidth)}function drawHorizontalLine({ctx:o,x:e,y:t,width:i,style:n,lineWidth:s=1}){o.strokeStyle=n,o.lineWidth=s,o.setLineDash=[],o.beginPath(),o.moveTo(e,t),o.lineTo(e+i,t),o.stroke()}class Option{constructor({fontName:e,fontSize:t}){this.setFont(e,t),this.foreground="#cccccc",this.background="#2d2d2d"}setFont(e,t){const i=t+"px "+e,[n,s]=computeFontSize(i);this.fontName=e,this.fontSize=t,this.fontWidth=n,this.fontHeight=s,this.font=i}}function getLemEditorElement(){return document.getElementById("lem-editor")}function normalizeWheelDelta(o,e,t,i){switch(t){case 0:return{dx:o/i,dy:e/i};case 2:return{dx:o*20,dy:e*20};default:return{dx:o,dy:e}}}function extractWholeLines(o,e){const t=Math.trunc(o),i=Math.trunc(e);return{scrollX:t,scrollY:i,remainderX:o-t,remainderY:e-i}}function cursorPosition(o,e){const[t,i]=e.getDisplayRectangle(),n=o.clientX-t,s=o.clientY-i;return{pixelX:n,pixelY:s,x:Math.floor(n/e.option.fontWidth),y:Math.floor(s/e.option.fontHeight)}}function makeWheelHandler(o){let e={x:0,y:0},t=!1,i={pixelX:0,pixelY:0,x:0,y:0};return n=>{n.preventDefault(),i=cursorPosition(n,o);const{dx:s,dy:r}=normalizeWheelDelta(n.deltaX,n.deltaY,n.deltaMode,o.option.fontHeight);e={x:e.x+s,y:e.y+r},t||(t=!0,requestAnimationFrame(()=>{t=!1;const{scrollX:l,scrollY:f,remainderX:d,remainderY:y}=extractWholeLines(e.x,e.y);e={x:d,y},(l!==0||f!==0)&&o.jsonrpc.notify("input",{kind:"wheel",value:{...i,wheelX:-l,wheelY:-f}})}))}}function addMouseEventListeners({dom:o,editor:e,isDraggable:t,draggableStyle:i}){o.addEventListener("contextmenu",r=>{r.preventDefault()});const n=(r,l)=>{r.preventDefault();const[f,d]=e.getDisplayRectangle(),y=r.clientX-f,v=r.clientY-d,N=Math.floor(y/e.option.fontWidth),u=Math.floor(v/e.option.fontHeight);e.jsonrpc.notify("input",{kind:l,value:{x:N,y:u,pixelX:y,pixelY:v,button:r.button,clicks:r.detail}})};o.addEventListener("mousedown",r=>{t&&(document.body.style.cursor=i),e.focusHiddenInput(),n(r,"mousedown")}),o.addEventListener("mouseup",r=>{t&&(document.body.style.cursor="default"),n(r,"mouseup")});let s=0;o.addEventListener("mousemove",r=>{r.preventDefault();const l=Date.now();if(l-s>50){s=l;const[f,d]=e.getDisplayRectangle(),y=r.clientX-f,v=r.clientY-d,N=Math.floor(y/e.option.fontWidth),u=Math.floor(v/e.option.fontHeight);e.jsonrpc.notify("input",{kind:"mousemove",value:{x:N,y:u,pixelX:y,pixelY:v,button:r.buttons===0?null:r.buttons-1}})}}),t&&(o.addEventListener("mouseover",()=>{document.body.style.cursor=i}),o.addEventListener("mouseout",r=>{r.buttons!==1&&(document.body.style.cursor="default")})),o.addEventListener("wheel",makeWheelHandler(e))}const zIndexTable={"floating-window":200,modeline:100,"vertical-border":100,"horizontal-border":101};function zindex(o){return zIndexTable[o]||0}const borderOffsetX=5,borderOffsetY=10;class BaseSurface{constructor({editor:o}){this.editor=o,this.mainDOM=null,this.wrapper=null}delete(){this.wrapper?getLemEditorElement().removeChild(this.wrapper):getLemEditorElement().removeChild(this.mainDOM)}setupDOM({dom:o,isFloating:e,border:t,cssClassName:i}){this.mainDOM=o,e&&t?(this.wrapper=document.createElement("div"),i&&(this.wrapper.className=i),this.wrapper.style.position="absolute",this.wrapper.style.backgroundColor=this.editor.option.background,this.wrapper.style.zIndex=zindex("floating-window"),this.wrapper.appendChild(o),getLemEditorElement().appendChild(this.wrapper)):(i&&(o.className=i),getLemEditorElement().appendChild(o))}move(o,e,t,i){const[n,s]=this.editor.getDisplayRectangle(),r=t!=null?Math.floor(n+t):Math.floor(n+o*this.editor.option.fontWidth),l=i!=null?Math.floor(s+i):Math.floor(s+e*this.editor.option.fontHeight);this.wrapper?(this.wrapper.style.left=r-borderOffsetX+"px",this.wrapper.style.top=l-borderOffsetY+"px",this.mainDOM.style.left=borderOffsetX+"px",this.mainDOM.style.top=borderOffsetY+"px"):(this.mainDOM.style.left=r+"px",this.mainDOM.style.top=l+"px")}_resize(o,e,t,i){const n=window.devicePixelRatio||1,s=t??o*this.editor.option.fontWidth,r=i??e*this.editor.option.fontHeight;this.mainDOM.width=s*n,this.mainDOM.height=r*n,this.mainDOM.style.width=s+"px",this.mainDOM.style.height=r+"px",this.wrapper&&(this.wrapper.style.width=s+borderOffsetX*2+"px",this.wrapper.style.height=r+borderOffsetY*2+"px")}drawBlock(o,e,t,i,n){}drawText(o,e,t,i,n){}touch(){}evalIn(code){return eval(code)}}class CanvasSurface extends BaseSurface{constructor({editor:e,view:t,x:i,y:n,width:s,height:r,styles:l,isFloating:f,border:d,cssClassName:y}){super({editor:e});const v=this.setupCanvas(l);this.setupDOM({dom:v,isFloating:f,border:d,cssClassName:y}),this.move(i,n),this.resize(s,r),this.drawingQueue=[],addMouseEventListeners({dom:v,editor:e})}setupCanvas(e){const t=document.createElement("canvas");if(t.style.position="absolute",e)for(let i in e)t.style[i]=e[i];return t}resize(e,t,i,n){this._resize(e,t,i,n);const s=window.devicePixelRatio||1;this.mainDOM.getContext("2d").scale(s,s)}drawBlock(e,t,i,n,s){const r=this.editor.option;this.drawingQueue.push(function(l){drawBlock({ctx:l,x:e*r.fontWidth,y:t*r.fontHeight,width:i*r.fontWidth,height:n*r.fontHeight,style:s})})}drawText(e,t,i,n,s,r){const l=this.editor.option;this.drawingQueue.push(function(f){if(r=r?`${l.fontSize}px ${r}`:l.font,!s)drawBlock({ctx:f,x:e*l.fontWidth,y:t*l.fontHeight,width:n*l.fontWidth,height:l.fontHeight,style:l.background}),drawText({ctx:f,x:e*l.fontWidth,y:t*l.fontHeight,text:i,style:l.foreground,font:r,option:l});else{let{foreground:d,background:y,bold:v,reverse:N,underline:u,cursor:h}=s;if(d||(d=l.foreground),y||(y=l.background),N){const p=y;y=d,d=p}h&&(y=l.background);const a=e*l.fontWidth,c=t*l.fontHeight;drawBlock({ctx:f,x:a,y:c,width:n*l.fontWidth,height:l.fontHeight,style:y}),drawText({ctx:f,x:a,y:c,text:i,style:d,font:v?"bold "+r:r,option:l}),u&&drawHorizontalLine({ctx:f,x:a,y:c+l.fontHeight-2,width:n*l.fontWidth,style:typeof u=="string"?u:d,lineWidth:2})}})}touch(){const e=this.mainDOM.getContext("2d");for(let t of this.drawingQueue)t(e);this.drawingQueue=[]}activate(){this.mainDOM.dataset.store="active"}deactivate(){this.mainDOM.dataset.store="inactive"}}class HTMLSurface extends BaseSurface{constructor({editor:e,x:t,y:i,width:n,height:s,styles:r,option:l,isFloating:f,border:d,html:y}){super({editor:e});const v=document.createElement("iframe");this.setupDOM({dom:v,isFloating:f,border:d}),v.style.position="absolute",v.style.backgroundColor=l.background,v.setAttribute("sandbox","allow-scripts allow-same-origin"),v.srcdoc=y,v.addEventListener("load",()=>{const N=v.contentWindow;N.invokeLem=(u,h)=>parent.postMessage({type:"invoke-lem",method:u,args:h})}),this.iframe=v,this.move(t,i),this.resize(n,s)}resize(e,t,i,n){this._resize(e,t,i,n)}update(e){const t=this.iframe.contentWindow.scrollY;this.iframe.srcdoc=e,this.iframe.onload=()=>{this.iframe.onload=null,this.iframe.contentWindow.scrollTo(0,t)}}evalIn(e){return this.iframe.contentWindow.eval(e)}}class VerticalBorder{constructor({x:e,y:t,height:i,option:n,editor:s}){this.option=n,this.editor=s,this.line=document.createElement("div"),this.line.className="lem-editor__vertical-border",this.line.style.height=i*n.fontHeight+"px",this.line.style.position="absolute",this.line.style.zIndex=zindex("vertical-border"),getLemEditorElement().appendChild(this.line),this.move(e,t),addMouseEventListeners({dom:this.line,editor:s,isDraggable:!0,draggableStyle:"col-resize"})}delete(){this.line.parentNode.removeChild(this.line)}move(e,t){const[i,n]=this.editor.getDisplayRectangle();this.line.style.left=Math.floor(i+e*this.option.fontWidth-this.option.fontWidth/2)+"px",this.line.style.top=n+t*this.option.fontHeight+"px"}resize(e){this.line.style.height=e*this.option.fontHeight+"px"}}class HorizontalBorder{constructor({x:e,y:t,width:i,option:n,editor:s}){this.option=n,this.editor=s,this.line=document.createElement("div"),this.line.className="lem-editor__horizontal-border",this.line.style.width=i*n.fontWidth+"px",this.line.style.position="absolute",this.line.style.zIndex=zindex("horizontal-border"),getLemEditorElement().appendChild(this.line),this.move(e,t),addMouseEventListeners({dom:this.line,editor:s,isDraggable:!0,draggableStyle:"row-resize"})}delete(){this.line.parentNode.removeChild(this.line)}move(e,t){const[i,n]=this.editor.getDisplayRectangle();this.line.style.left=i+e*this.option.fontWidth+"px",this.line.style.top=Math.floor(n+t*this.option.fontHeight-4)+"px"}resize(e){this.line.style.width=e*this.option.fontWidth+"px"}}const viewStyles={header:()=>{},tile:()=>{},floating:o=>({boxSizing:"border-box",borderColor:o.foreground,backgroundColor:o.background})};function getViewStyle(o,e){return viewStyles[o](e)||{}}class View{constructor({id:e,x:t,y:i,width:n,height:s,pixelX:r,pixelY:l,pixelWidth:f,pixelHeight:d,useModeline:y,kind:v,type:N,content:u,border:h,borderShape:a,option:c,editor:p}){switch(this.option=c,this.id=e,this.x=t,this.y=i,this.width=n,this.height=s,this.pixelX=r,this.pixelY=l,this.pixelWidth=f,this.pixelHeight=d,this.useModeline=y,this.kind=v,this.type=N,this.border=h,this.borderShape=a,this.editor=p,this.bottomBar=null,this.leftsideBar=null,v){case"tile":this.mainSurface=this.makeSurface(N,u),this.leftSideBar=new VerticalBorder({x:t,y:i,height:s+(y?1:0),option:c,editor:p}),y||(this.bottomBar=new HorizontalBorder({x:t,y:i+s-1,width:n,option:c,editor:p}));break;case"header":this.mainSurface=this.makeSurface(N,u);break;case"floating":this.mainSurface=this.makeSurface(N,u),a==="left-border"&&(this.leftSideBar=new VerticalBorder({x:t,y:i,height:s,option:c,editor:p}));break}this.modelineSurface=y?this.makeModelineSurface():null,v==="floating"&&(r!=null||l!=null)&&this.move(t,i,r,l)}delete(){this.mainSurface.delete(),this.modelineSurface&&this.modelineSurface.delete(),this.leftSideBar&&this.leftSideBar.delete(),this.bottomBar&&this.bottomBar.delete()}move(e,t,i,n){if(this.x=e,this.y=t,this.pixelX=i,this.pixelY=n,this.mainSurface.move(e,t,i,n),this.modelineSurface){const s=n!=null&&this.pixelHeight!=null?n+this.pixelHeight:null;this.modelineSurface.move(e,t+this.height,i,s)}this.leftSideBar&&this.leftSideBar.move(e,t),this.bottomBar&&this.bottomBar.move(e,t+this.height)}resize(e,t,i,n){if(this.width=e,this.height=t,this.pixelWidth=i,this.pixelHeight=n,this.mainSurface.resize(e,t,i,n),this.modelineSurface){const s=this.pixelY!=null&&n!=null?this.pixelY+n:null;this.modelineSurface.move(this.x,this.y+this.height,this.pixelX,s),this.modelineSurface.resize(e,1)}this.leftSideBar&&this.leftSideBar.resize(t+(this.modelineSurface?1:0)),this.bottomBar&&this.bottomBar.resize(e)}clear(){this.mainSurface.drawBlock(0,0,this.width,this.height,this.option.background)}clearEol(e,t){this.mainSurface.drawBlock(e,t,this.width-e,1,this.option.background)}clearEob(e,t){this.mainSurface.drawBlock(e,t,this.width,this.height-t,this.option.background)}print(e,t,i,n,s,r){this.mainSurface.drawText(e,t,i,n,s,r)}printToModeline(e,t,i,n,s){this.modelineSurface&&this.modelineSurface.drawText(e,t,i,n,s)}touch(e){this.mainSurface.touch(),this.modelineSurface&&(this.modelineSurface.touch(),e?this.modelineSurface.activate():this.modelineSurface.deactivate())}makeSurface(e,t){switch(e){case"html":return this.makeHTMLSurface(t);case"editor":return this.makeEditorSurface();default:console.error(`unknown type: ${e}`)}}makeHTMLSurface(e){return new HTMLSurface({editor:this.editor,x:this.x,y:this.y,width:this.width,height:this.height,styles:getViewStyle(this.kind,this.option),option:this.option,isFloating:this.kind==="floating",border:this.border,html:e})}makeEditorSurface(){const e=this.borderShape==="left-border"?0:this.border,t=this.kind==="floating";return new CanvasSurface({option:this.editor.option,x:this.x,y:this.y,width:this.width,height:this.height,styles:getViewStyle(this.kind,this.option),editor:this.editor,border:e,isFloating:t,view:this,cssClassName:t&&e?"lem-editor__floating-window--bordered":null})}makeModelineSurface(){const e=new CanvasSurface({option:this.editor.option,x:this.x,y:this.y+this.height,width:this.width,height:1,editor:this.editor,view:this,styles:{zIndex:zindex("modeline")},cssClassName:"lem-editor__mode-line"});return addMouseEventListeners({dom:e.mainDOM,editor:this.editor,isDraggable:!0,draggableStyle:"row-resize"}),e}changeToHTMLContent(e){this.mainSurface.constructor.name==="HTMLSurface"?this.mainSurface.update(e):(this.mainSurface.delete(),this.mainSurface=this.makeHTMLSurface(e))}changeToEditorContent(){this.mainSurface.delete(),this.mainSurface=this.makeEditorSurface()}evalIn(e){return this.mainSurface.evalIn(e)}}function isPasteKeyEvent(o){return isMacOS()?o.metaKey&&o.key==="v":o.ctrlKey&&o.shiftKey&&o.key==="V"}class Input{constructor(e){const t=e.option;this.editor=e,this.composition=!1,this.ignoreKeydownAfterCompositionend=!1,this.span=document.createElement("span"),this.span.style.color=t.foreground,this.span.style.backgroundColor=t.background,this.span.style.position="absolute",this.span.style.zIndex=1e6,this.span.style.top="0",this.span.style.left="0",this.span.style.font=t.font,this.input=document.createElement("input"),this.input.style.backgroundColor="transparent",this.input.style.color="transparent",this.input.style.width="0",this.input.style.padding="0",this.input.style.margin="0",this.input.style.border="none",this.input.style.position="absolute",this.input.style.zIndex="-10",this.input.style.top="0",this.input.style.left="0",this.input.style.font=t.font,this.input.addEventListener("blur",i=>{this.input.focus()}),this.input.addEventListener("input",i=>{this.composition===!1&&(this.input.value="",this.span.innerHTML="",this.input.style.width="0",isMacOS()||this.editor.emitInputString(i.data))}),this.input.addEventListener("paste",async i=>{i.preventDefault();const n=i.clipboardData||window.Clipboard.data,s=n?.getData("text")??n?.getData("text/plain");if(s&&s.length>0){this.editor.emitInputString(s);return}try{if(navigator.clipboard?.readText){const r=await navigator.clipboard.readText();if(r&&r.length>0){this.editor.emitInputString(r);return}}}catch(r){console.warn("clipboard.readText() failed:",r)}alert("Paste failed (permission/environment restriction")}),this.input.addEventListener("keydown",i=>{if(!isPasteKeyEvent(i)&&!(i.isComposing||this.composition)&&i.key!=="Process"){if(this.ignoreKeydownAfterCompositionend&&(isSafari||isMacOS())){i.preventDefault(),this.ignoreKeydownAfterCompositionend=!1;return}if(!(!isMacOS()&&!i.ctrlKey&&!i.altKey&&i.key.length===1)&&(i.preventDefault(),i.isComposing!==!0&&i.code!==""))return setTimeout(()=>{this.composition||(this.editor.emitInput(i),this.input.value="")},0),!1}}),this.input.addEventListener("compositionstart",i=>{this.composition=!0,this.span.innerHTML=this.input.value,this.input.style.width=this.span.offsetWidth+"px"}),this.input.addEventListener("compositionupdate",i=>{this.span.innerHTML=i.data,this.input.style.width=this.span.offsetWidth+"px"}),this.input.addEventListener("compositionend",i=>{this.composition=!1,this.editor.emitInputString(this.input.value),this.input.value="",this.span.innerHTML=this.input.value,this.input.style.width="0",this.ignoreKeydownAfterCompositionend=!0}),document.body.appendChild(this.input),document.body.appendChild(this.span),this.input.focus()}finalize(){document.body.removeChild(this.input),document.body.removeChild(this.span)}move(e,t){const[i,n]=this.editor.getDisplayRectangle();this.span.style.top=n+t+"px",this.span.style.left=i+e+"px",this.input.style.top=this.span.offsetTop+"px",this.input.style.left=this.span.offsetLeft+"px"}updateForeground(e){this.span.style.color=e}updateBackground(e){this.span.style.backgroundColor=e}}class MessageTable{constructor(){this.map=new Map}register(e,t){for(const i in t){const n=t[i];this.map.set(i,n),e.on(i,n)}}get(e){return this.map.get(e)}}function getDisplayRectangleDefault(){return[0,0,window.innerWidth,window.innerHeight]}class Editor{constructor({getDisplayRectangle:e=getDisplayRectangleDefault,fontName:t,fontSize:i,url:n,onExit:s,onClosed:r}){this.getDisplayRectangle=e,this.option=new Option({fontName:t,fontSize:i}),this.onExit=s,this.input=new Input(this),this.cursors=new Map,this.cursorOverlay=document.createElement("div"),this.cursorOverlay.className="lem-cursor",this.cursorOverlay.style.width=this.option.fontWidth+"px",this.cursorOverlay.style.height=this.option.fontHeight+"px",this.cursorOverlay.style.backgroundColor="#ffffff",this.cursorType="box",this.viewMap=new Map,this.jsonrpc=new JSONRPC(n,{onClosed:()=>{r()}}),this.messageTable=new MessageTable,this.messageTable.register(this.jsonrpc,{"update-foreground":this.updateForeground.bind(this),"update-background":this.updateBackground.bind(this),"make-view":this.makeView.bind(this),"delete-view":this.deleteView.bind(this),"resize-view":this.resize.bind(this),"move-view":this.move.bind(this),"redraw-view-after":this.redrawViewAfter.bind(this),clear:this.clear.bind(this),"clear-eol":this.clearEol.bind(this),"clear-eob":this.clearEob.bind(this),put:this.put.bind(this),"modeline-put":this.modelinePut.bind(this),"update-display":this.updateDisplay.bind(this),"move-cursor":this.moveCursor.bind(this),"change-view":this.changeView.bind(this),"resize-display":this.resizeDisplay.bind(this),bulk:this.bulk.bind(this),exit:this.exitEditor.bind(this),"get-clipboard-text":this.getClipboardText.bind(this),"set-clipboard-text":this.setClipboardText.bind(this),"js-eval":this.jsEval.bind(this),"set-font":this.setFont.bind(this),"get-font":this.getFont.bind(this),"get-display-size":this.getDisplaySize.bind(this),"load-css":this.loadCSS.bind(this),"update-cursor-shape":this.updateCursorShape.bind(this)}),this.login(),this.boundedHandleResize=this.handleResize.bind(this),this.focusHiddenInput=this.focusHiddenInput.bind(this)}init(){window.addEventListener("resize",this.boundedHandleResize),document.getElementsByTagName("html")[0].style["background-color"]="#333",getLemEditorElement().appendChild(this.cursorOverlay)}finalize(){window.removeEventListener("resize",this.boundedHandleResize),this.input.finalize(),this.cursorOverlay.parentNode&&this.cursorOverlay.parentNode.removeChild(this.cursorOverlay)}closeConnection(){this.jsonrpc.close()}emitInput(e){const t=convertKeyEvent(e);if(t){if(t.key==="]"&&t.ctrl&&!t.meta&&!t.super&&!t.shift){this.jsonrpc.notify("input",{kind:"abort"});return}t.key!=="Unidentified"&&this.jsonrpc.notify("input",{kind:"key",value:t})}}emitInputString(e){e?this.jsonrpc.notify("input",{kind:"input-string",value:e}):console.error("unexpected argument",e)}handleResize(e){this.jsonrpc.notify("redraw",{size:this.getDisplaySize()})}focusHiddenInput(){const e=this.input?.input;if(e){try{window.focus()}catch{}requestAnimationFrame(()=>{setTimeout(()=>{e.focus({preventScroll:!0})},0)})}}sendNotification(e,t){this.jsonrpc.notify(e,t)}request(e,t,i){this.jsonrpc.request(e,t,i)}getDisplaySize(){const[e,t,i,n]=this.getDisplayRectangle(),s=Math.floor(i/this.option.fontWidth),r=Math.floor(n/this.option.fontHeight);return{width:s,height:r}}callMessage(e,t){this.messageTable.get(e)(t)}findViewById(e){return this.viewMap.get(e)}login(){this.jsonrpc.request("login",{size:this.getDisplaySize(),foreground:this.option.foreground,background:this.option.background},e=>{if(this.updateForeground(e.foreground),this.updateBackground(e.background),e.views)for(const t of e.views)this.makeView(t);this.jsonrpc.notify("redraw",{size:this.getDisplaySize()})})}updateForeground(e){e!=null&&(this.option.foreground=e,this.input.updateForeground(e))}updateBackground(e){if(e==null)return;this.option.background=e,this.input.updateBackground(e);const t=getLemEditorElement();t.style.backgroundColor=e}makeView({id:e,x:t,y:i,width:n,height:s,pixelX:r,pixelY:l,pixelWidth:f,pixelHeight:d,use_modeline:y,kind:v,type:N,content:u,border:h,border_shape:a}){const c=new View({option:this.option,id:e,x:t,y:i,width:n,height:s,pixelX:r,pixelY:l,pixelWidth:f,pixelHeight:d,useModeline:y,kind:v,type:N,content:u,border:h,borderShape:a,editor:this});this.viewMap.set(e,c)}deleteView({viewInfo:{id:e}}){this.findViewById(e).delete(),this.viewMap.delete(e)}resize({viewInfo:{id:e},width:t,height:i,pixelWidth:n,pixelHeight:s}){const r=this.findViewById(e);r?r.resize(t,i,n,s):console.warn(`resize: view not found for id ${e}`)}move({viewInfo:{id:e},x:t,y:i,pixelX:n,pixelY:s}){const r=this.findViewById(e);r?r.move(t,i,n,s):console.warn(`move: view not found for id ${e}`)}redrawViewAfter({viewInfo:{id:e},isActive:t}){this.findViewById(e).touch(t)}clear({viewInfo:{id:e}}){this.findViewById(e).clear()}clearEol({viewInfo:{id:e},x:t,y:i}){this.findViewById(e).clearEol(t,i)}clearEob({viewInfo:{id:e},x:t,y:i}){this.findViewById(e).clearEob(t,i)}put({viewInfo:{id:e},x:t,y:i,text:n,textWidth:s,attribute:r,font:l}){this.findViewById(e).print(t,i,n,s,r,l)}modelinePut({viewInfo:{id:e},x:t,y:i,text:n,textWidth:s,attribute:r}){this.findViewById(e).printToModeline(t,i,n,s,r)}updateDisplay(){}moveCursor({viewInfo:{id:e},x:t,y:i,color:n,cursorText:s,cursorForeground:r}){const l=this.findViewById(e),[f,d]=this.getDisplayRectangle(),y=l.x*this.option.fontWidth+t*this.option.fontWidth,v=l.y*this.option.fontHeight+i*this.option.fontHeight;this.input.move(y,v);const N=n||this.option.foreground,u=r||this.option.background,h=this.cursorOverlay;switch(this.cursorType){case"bar":h.style.left=f+y+"px",h.style.top=d+v+"px",h.style.width="2px",h.style.height=this.option.fontHeight+"px",h.style.backgroundColor=N,h.textContent="",h.style.color="",h.style.font="",h.style.paddingTop="";break;case"underline":h.style.left=f+y+"px",h.style.top=d+v+this.option.fontHeight-2+"px",h.style.width=this.option.fontWidth+"px",h.style.height="2px",h.style.backgroundColor=N,h.textContent="",h.style.color="",h.style.font="",h.style.paddingTop="";break;case"box":default:h.style.left=f+y+"px",h.style.top=d+v+"px",h.style.width=this.option.fontWidth+"px",h.style.height=this.option.fontHeight+"px",h.style.backgroundColor=N,h.style.font=this.option.font,h.style.paddingTop=textOffsetY+"px",h.textContent=s||"",h.style.color=u;break}h.style.animation="none",h.offsetHeight,h.style.animation=""}updateCursorShape({cursorType:e}){this.cursorType=e||"box"}changeView({viewInfo:{id:e},type:t,content:i}){const n=this.findViewById(e);switch(t){case"html":n.changeToHTMLContent(i);break;case"editor":n.changeToEditorContent();break}}resizeDisplay({width:e,height:t}){const i=getLemEditorElement();i.style.width=Math.floor(e*this.option.fontWidth)+"px",i.style.height=Math.floor(t*this.option.fontHeight)+"px"}bulk(e){for(const{method:t,argument:i}of e)this.callMessage(t,i)}exitEditor(){this.onExit&&this.onExit()}getClipboardText(){navigator.clipboard?.readText().then(e=>{this.jsonrpc.notify("got-clipboard-text",{text:e})})}setClipboardText({text:e}){navigator.clipboard&&navigator.clipboard.writeText(e)}jsEval({viewInfo:{id:e},code:t}){const n=this.findViewById(e).evalIn(t);return n&&n.toString()}setFont({fontName:e,fontSize:t}){this.option.setFont(e||this.option.fontName,t||this.option.fontSize)}getFont(){return{name:this.option.fontName,size:this.option.fontSize}}loadCSS({content:e}){const t=document.createElement("style");t.textContent=e,document.head.appendChild(t)}notifyToServer(e,t){this.jsonrpc.notify("invoke",{method:e,args:t})}}const canvas=document.querySelector("#editor");async function main(){await Promise.all([document.fonts.load("19px file-icons"),document.fonts.load("19px AllTheIcons"),document.fonts.load("19px fontawesome"),document.fonts.load("19px material-design-icons"),document.fonts.load("19px octicons")]),await document.fonts.ready;const o=window.location.protocol==="https:"?"wss":"ws",e=new Editor({canvas,fontName:"Monospace",fontSize:18,url:`${o}://${window.location.hostname}:${window.location.port}`,onExit:null,onClosed:null});window.addEventListener("message",t=>{t.data.type==="invoke-lem"&&e.notifyToServer(t.data.method,t.data.args)}),e.init()}main(); +var __defProp=Object.defineProperty,__commonJSMin=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),__exportAll=(e,t)=>{let n={};for(var r in e)__defProp(n,r,{get:e[r],enumerable:!0});return t||__defProp(n,Symbol.toStringTag,{value:`Module`}),n};(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var require_models=__commonJSMin((e=>{var t=e&&e.__extends||(function(){var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)};return function(t,n){if(typeof n!=`function`&&n!==null)throw TypeError(`Class extends value `+String(n)+` is not a constructor or null`);e(t,n);function r(){this.constructor=t}t.prototype=n===null?Object.create(n):(r.prototype=n.prototype,new r)}})();Object.defineProperty(e,"__esModule",{value:!0}),e.createJSONRPCNotification=e.createJSONRPCRequest=e.createJSONRPCSuccessResponse=e.createJSONRPCErrorResponse=e.JSONRPCErrorCode=e.JSONRPCErrorException=e.isJSONRPCResponses=e.isJSONRPCResponse=e.isJSONRPCRequests=e.isJSONRPCRequest=e.isJSONRPCID=e.JSONRPC=void 0,e.JSONRPC=`2.0`,e.isJSONRPCID=function(e){return typeof e==`string`||typeof e==`number`||e===null},e.isJSONRPCRequest=function(t){return t.jsonrpc===e.JSONRPC&&t.method!==void 0&&t.result===void 0&&t.error===void 0},e.isJSONRPCRequests=function(t){return Array.isArray(t)&&t.every(e.isJSONRPCRequest)},e.isJSONRPCResponse=function(t){return t.jsonrpc===e.JSONRPC&&t.id!==void 0&&(t.result!==void 0||t.error!==void 0)},e.isJSONRPCResponses=function(t){return Array.isArray(t)&&t.every(e.isJSONRPCResponse)};var n=function(e,t,n){var r={code:e,message:t};return n!=null&&(r.data=n),r};e.JSONRPCErrorException=function(e){t(r,e);function r(t,n,i){var a=e.call(this,t)||this;return Object.setPrototypeOf(a,r.prototype),a.code=n,a.data=i,a}return r.prototype.toObject=function(){return n(this.code,this.message,this.data)},r}(Error),(function(e){e[e.ParseError=-32700]=`ParseError`,e[e.InvalidRequest=-32600]=`InvalidRequest`,e[e.MethodNotFound=-32601]=`MethodNotFound`,e[e.InvalidParams=-32602]=`InvalidParams`,e[e.InternalError=-32603]=`InternalError`})(e.JSONRPCErrorCode||={}),e.createJSONRPCErrorResponse=function(t,r,i,a){return{jsonrpc:e.JSONRPC,id:t,error:n(r,i,a)}},e.createJSONRPCSuccessResponse=function(t,n){return{jsonrpc:e.JSONRPC,id:t,result:n??null}},e.createJSONRPCRequest=function(t,n,r){return{jsonrpc:e.JSONRPC,id:t,method:n,params:r}},e.createJSONRPCNotification=function(t,n){return{jsonrpc:e.JSONRPC,method:t,params:n}}})),require_internal=__commonJSMin((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.DefaultErrorCode=void 0,e.DefaultErrorCode=0})),require_client=__commonJSMin((e=>{var t=e&&e.__awaiter||function(e,t,n,r){function i(e){return e instanceof n?e:new n(function(t){t(e)})}return new(n||=Promise)(function(n,a){function o(e){try{c(r.next(e))}catch(e){a(e)}}function s(e){try{c(r.throw(e))}catch(e){a(e)}}function c(e){e.done?n(e.value):i(e.value).then(o,s)}c((r=r.apply(e,t||[])).next())})},n=e&&e.__generator||function(e,t){var n={label:0,sent:function(){if(a[0]&1)throw a[1];return a[1]},trys:[],ops:[]},r,i,a,o;return o={next:s(0),throw:s(1),return:s(2)},typeof Symbol==`function`&&(o[Symbol.iterator]=function(){return this}),o;function s(e){return function(t){return c([e,t])}}function c(s){if(r)throw TypeError(`Generator is already executing.`);for(;o&&(o=0,s[0]&&(n=0)),n;)try{if(r=1,i&&(a=s[0]&2?i.return:s[0]?i.throw||((a=i.return)&&a.call(i),0):i.next)&&!(a=a.call(i,s[1])).done)return a;switch(i=0,a&&(s=[s[0]&2,a.value]),s[0]){case 0:case 1:a=s;break;case 4:return n.label++,{value:s[1],done:!1};case 5:n.label++,i=s[1],s=[0];continue;case 7:s=n.ops.pop(),n.trys.pop();continue;default:if((a=n.trys,!(a=a.length>0&&a[a.length-1]))&&(s[0]===6||s[0]===2)){n=0;continue}if(s[0]===3&&(!a||s[1]>a[0]&&s[1]{Object.defineProperty(e,"__esModule",{value:!0})})),require_server=__commonJSMin((e=>{var t=e&&e.__assign||function(){return t=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&a[a.length-1]))&&(s[0]===6||s[0]===2)){n=0;continue}if(s[0]===3&&(!a||s[1]>a[0]&&s[1]{var t=e&&e.__awaiter||function(e,t,n,r){function i(e){return e instanceof n?e:new n(function(t){t(e)})}return new(n||=Promise)(function(n,a){function o(e){try{c(r.next(e))}catch(e){a(e)}}function s(e){try{c(r.throw(e))}catch(e){a(e)}}function c(e){e.done?n(e.value):i(e.value).then(o,s)}c((r=r.apply(e,t||[])).next())})},n=e&&e.__generator||function(e,t){var n={label:0,sent:function(){if(a[0]&1)throw a[1];return a[1]},trys:[],ops:[]},r,i,a,o;return o={next:s(0),throw:s(1),return:s(2)},typeof Symbol==`function`&&(o[Symbol.iterator]=function(){return this}),o;function s(e){return function(t){return c([e,t])}}function c(s){if(r)throw TypeError(`Generator is already executing.`);for(;o&&(o=0,s[0]&&(n=0)),n;)try{if(r=1,i&&(a=s[0]&2?i.return:s[0]?i.throw||((a=i.return)&&a.call(i),0):i.next)&&!(a=a.call(i,s[1])).done)return a;switch(i=0,a&&(s=[s[0]&2,a.value]),s[0]){case 0:case 1:a=s;break;case 4:return n.label++,{value:s[1],done:!1};case 5:n.label++,i=s[1],s=[0];continue;case 7:s=n.ops.pop(),n.trys.pop();continue;default:if((a=n.trys,!(a=a.length>0&&a[a.length-1]))&&(s[0]===6||s[0]===2)){n=0;continue}if(s[0]===3&&(!a||s[1]>a[0]&&s[1]{var t=e&&e.__createBinding||(Object.create?(function(e,t,n,r){r===void 0&&(r=n);var i=Object.getOwnPropertyDescriptor(t,n);(!i||(`get`in i?!t.__esModule:i.writable||i.configurable))&&(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,r,i)}):(function(e,t,n,r){r===void 0&&(r=n),e[r]=t[n]})),n=e&&e.__exportStar||function(e,n){for(var r in e)r!=="default"&&!Object.prototype.hasOwnProperty.call(n,r)&&t(n,e,r)};Object.defineProperty(e,"__esModule",{value:!0}),n(require_client(),e),n(require_interfaces(),e),n(require_models(),e),n(require_server(),e),n(require_server_and_client(),e)})),import_dist=require_dist(),JSONRPC=class{constructor(e,{onConnected:t,onClosed:n}){this.url=e,this.onConnected=t,this.onClosed=n,this.messageQueue=[],this.serverAndClient=null,this.connect(),this.connectionEstablished=!1,this.timerId=null,this.closed=!1}close(){this.timerId&&clearTimeout(this.timerId),this.webSocket.close(),this.closed=!0}on(e,t){this.serverAndClient.addMethod(e,t)}async requestInternal(e,t,n){let r=await this.serverAndClient.request(e,t);n&&n(r)}requestMessageQueue(){this.messageQueue.forEach(e=>{let[t,n,r]=e;this.requestInternal(t,n,r)}),this.messageQueue=[]}request(e,t,n){this.webSocket.readyState===WebSocket.OPEN?this.requestInternal(e,t,n):this.messageQueue.push([e,t,n])}notify(e,t){switch(this.webSocket.readyState){case WebSocket.OPEN:this.serverAndClient.notify(e,t);break;case WebSocket.CLOSED:break}}connect(e){this.closed||(console.log(`connect`,this.url),this.webSocket=new WebSocket(this.url),this.serverAndClient||=new import_dist.JSONRPCServerAndClient(new import_dist.JSONRPCServer,new import_dist.JSONRPCClient(e=>{try{return this.webSocket.send(JSON.stringify(e)),Promise.resolve()}catch(e){return Promise.reject(e)}})),this.webSocket.onmessage=e=>{this.serverAndClient.receiveAndSend(JSON.parse(e.data.toString()))},this.webSocket.onopen=()=>{console.log(`WebSocket connection established`),this.connectionEstablished=!0,this.onConnected&&this.onConnected(),this.requestMessageQueue()},this.webSocket.onclose=e=>{console.error(`WebScoket closed`,e),this.serverAndClient.rejectAllPendingRequests(`Connection is closed (${e.reason}).`),this.connectionEstablished&&this.onClosed(),this.timerId=setTimeout(()=>{this.connect()},3e3)},this.webSocket.onerror=e=>{console.error(`WebSocket error:`,e),this.webSocket.close()})}},keyevent_exports=__exportAll({convertKeyEvent:()=>convertKeyEvent}),modifierKeys=[`Shift`,`Control`,`Alt`,`Meta`,`CapsLock`],convertKeyTable={Enter:`Return`,ArrowRight:`Right`,ArrowLeft:`Left`,ArrowUp:`Up`,ArrowDown:`Down`,"¡":`1`,"™":`2`,"£":`3`,"¢":`4`,"∞":`5`,"§":`6`,"¶":`7`,"•":`8`,ª:`9`,º:`0`,"–":`-`,"≠":`=`,"“":`[`,"‘":`]`,"«":`\\`,"…":`;`,æ:`'`,"≤":`,`,"≥":`.`,"÷":`/`,"⁄":`!`,"€":`@`,"‹":`#`,"›":`$`,fi:`%`,fl:`^`,"‡":`&`,"°":`*`,"·":`(`,"‚":`)`,"—":`_`,"±":`+`,"”":`{`,"’":`}`,"»":`|`,Ú:`:`,Æ:`"`,"¯":`<`,"˘":`>`,"¿":`?`,œ:`q`,"∑":`w`,"´":`e`,"®":`r`,"†":`t`,"¥":`y`,"¨":`u`,ˆ:`i`,ø:`o`,π:`p`,å:`a`,ß:`s`,"∂":`d`,ƒ:`f`,"©":`g`,"˙":`h`,"∆":`j`,"˚":`k`,"¬":`l`,Ω:`z`,"≈":`x`,ç:`c`,"√":`v`,"∫":`b`,"˜":`n`,µ:`m`,Œ:`Q`,"„":`W`,"´":`E`,"‰":`R`,ˇ:`T`,Á:`Y`,"¨":`U`,ˆ:`I`,Ø:`O`,"∏":`P`,Å:`A`,Í:`S`,Î:`D`,Ï:`F`,"˝":`G`,Ó:`H`,Ô:`J`,"":`K`,Ò:`L`,"¸":`Z`,"˛":`X`,Ç:`C`,"◊":`V`,ı:`B`,"˜":`N`,Â:`M`};function getKey(e){return e.altKey?convertKeyTable[e.key]||(e.code.startsWith(`Key`)?e.code[3].toLowerCase():null)||e.key:convertKeyTable[e.key]||e.key}function convertKeyEvent(e){return modifierKeys.indexOf(e.key)===-1?{key:getKey(e),ctrl:e.ctrlKey,meta:e.altKey,super:e.metaKey,shift:e.shiftKey}:null}var lib_exports=__exportAll({computeWidth:()=>computeWidth,eawVersion:()=>version,getEAW:()=>getEAW}),defs=[[0,31,`N`],[32,126,`Na`],[127,160,`N`],[161,161,`A`],[162,163,`Na`],[164,164,`A`],[165,166,`Na`],[167,168,`A`],[169,169,`N`],[170,170,`A`],[171,171,`N`],[172,172,`Na`],[173,174,`A`],[175,175,`Na`],[176,180,`A`],[181,181,`N`],[182,186,`A`],[187,187,`N`],[188,191,`A`],[192,197,`N`],[198,198,`A`],[199,207,`N`],[208,208,`A`],[209,214,`N`],[215,216,`A`],[217,221,`N`],[222,225,`A`],[226,229,`N`],[230,230,`A`],[231,231,`N`],[232,234,`A`],[235,235,`N`],[236,237,`A`],[238,239,`N`],[240,240,`A`],[241,241,`N`],[242,243,`A`],[244,246,`N`],[247,250,`A`],[251,251,`N`],[252,252,`A`],[253,253,`N`],[254,254,`A`],[255,256,`N`],[257,257,`A`],[258,272,`N`],[273,273,`A`],[274,274,`N`],[275,275,`A`],[276,282,`N`],[283,283,`A`],[284,293,`N`],[294,295,`A`],[296,298,`N`],[299,299,`A`],[300,304,`N`],[305,307,`A`],[308,311,`N`],[312,312,`A`],[313,318,`N`],[319,322,`A`],[323,323,`N`],[324,324,`A`],[325,327,`N`],[328,331,`A`],[332,332,`N`],[333,333,`A`],[334,337,`N`],[338,339,`A`],[340,357,`N`],[358,359,`A`],[360,362,`N`],[363,363,`A`],[364,461,`N`],[462,462,`A`],[463,463,`N`],[464,464,`A`],[465,465,`N`],[466,466,`A`],[467,467,`N`],[468,468,`A`],[469,469,`N`],[470,470,`A`],[471,471,`N`],[472,472,`A`],[473,473,`N`],[474,474,`A`],[475,475,`N`],[476,476,`A`],[477,592,`N`],[593,593,`A`],[594,608,`N`],[609,609,`A`],[610,707,`N`],[708,708,`A`],[709,710,`N`],[711,711,`A`],[712,712,`N`],[713,715,`A`],[716,716,`N`],[717,717,`A`],[718,719,`N`],[720,720,`A`],[721,727,`N`],[728,731,`A`],[732,732,`N`],[733,733,`A`],[734,734,`N`],[735,735,`A`],[736,767,`N`],[768,879,`A`],[880,912,`N`],[913,929,`A`],[930,930,`N`],[931,937,`A`],[938,944,`N`],[945,961,`A`],[962,962,`N`],[963,969,`A`],[970,1024,`N`],[1025,1025,`A`],[1026,1039,`N`],[1040,1103,`A`],[1104,1104,`N`],[1105,1105,`A`],[1106,4351,`N`],[4352,4447,`W`],[4448,8207,`N`],[8208,8208,`A`],[8209,8210,`N`],[8211,8214,`A`],[8215,8215,`N`],[8216,8217,`A`],[8218,8219,`N`],[8220,8221,`A`],[8222,8223,`N`],[8224,8226,`A`],[8227,8227,`N`],[8228,8231,`A`],[8232,8239,`N`],[8240,8240,`A`],[8241,8241,`N`],[8242,8243,`A`],[8244,8244,`N`],[8245,8245,`A`],[8246,8250,`N`],[8251,8251,`A`],[8252,8253,`N`],[8254,8254,`A`],[8255,8307,`N`],[8308,8308,`A`],[8309,8318,`N`],[8319,8319,`A`],[8320,8320,`N`],[8321,8324,`A`],[8325,8360,`N`],[8361,8361,`H`],[8362,8363,`N`],[8364,8364,`A`],[8365,8450,`N`],[8451,8451,`A`],[8452,8452,`N`],[8453,8453,`A`],[8454,8456,`N`],[8457,8457,`A`],[8458,8466,`N`],[8467,8467,`A`],[8468,8469,`N`],[8470,8470,`A`],[8471,8480,`N`],[8481,8482,`A`],[8483,8485,`N`],[8486,8486,`A`],[8487,8490,`N`],[8491,8491,`A`],[8492,8530,`N`],[8531,8532,`A`],[8533,8538,`N`],[8539,8542,`A`],[8543,8543,`N`],[8544,8555,`A`],[8556,8559,`N`],[8560,8569,`A`],[8570,8584,`N`],[8585,8585,`A`],[8586,8591,`N`],[8592,8601,`A`],[8602,8631,`N`],[8632,8633,`A`],[8634,8657,`N`],[8658,8658,`A`],[8659,8659,`N`],[8660,8660,`A`],[8661,8678,`N`],[8679,8679,`A`],[8680,8703,`N`],[8704,8704,`A`],[8705,8705,`N`],[8706,8707,`A`],[8708,8710,`N`],[8711,8712,`A`],[8713,8714,`N`],[8715,8715,`A`],[8716,8718,`N`],[8719,8719,`A`],[8720,8720,`N`],[8721,8721,`A`],[8722,8724,`N`],[8725,8725,`A`],[8726,8729,`N`],[8730,8730,`A`],[8731,8732,`N`],[8733,8736,`A`],[8737,8738,`N`],[8739,8739,`A`],[8740,8740,`N`],[8741,8741,`A`],[8742,8742,`N`],[8743,8748,`A`],[8749,8749,`N`],[8750,8750,`A`],[8751,8755,`N`],[8756,8759,`A`],[8760,8763,`N`],[8764,8765,`A`],[8766,8775,`N`],[8776,8776,`A`],[8777,8779,`N`],[8780,8780,`A`],[8781,8785,`N`],[8786,8786,`A`],[8787,8799,`N`],[8800,8801,`A`],[8802,8803,`N`],[8804,8807,`A`],[8808,8809,`N`],[8810,8811,`A`],[8812,8813,`N`],[8814,8815,`A`],[8816,8833,`N`],[8834,8835,`A`],[8836,8837,`N`],[8838,8839,`A`],[8840,8852,`N`],[8853,8853,`A`],[8854,8856,`N`],[8857,8857,`A`],[8858,8868,`N`],[8869,8869,`A`],[8870,8894,`N`],[8895,8895,`A`],[8896,8977,`N`],[8978,8978,`A`],[8979,8985,`N`],[8986,8987,`W`],[8988,9e3,`N`],[9001,9002,`W`],[9003,9192,`N`],[9193,9196,`W`],[9197,9199,`N`],[9200,9200,`W`],[9201,9202,`N`],[9203,9203,`W`],[9204,9311,`N`],[9312,9449,`A`],[9450,9450,`N`],[9451,9547,`A`],[9548,9551,`N`],[9552,9587,`A`],[9588,9599,`N`],[9600,9615,`A`],[9616,9617,`N`],[9618,9621,`A`],[9622,9631,`N`],[9632,9633,`A`],[9634,9634,`N`],[9635,9641,`A`],[9642,9649,`N`],[9650,9651,`A`],[9652,9653,`N`],[9654,9655,`A`],[9656,9659,`N`],[9660,9661,`A`],[9662,9663,`N`],[9664,9665,`A`],[9666,9669,`N`],[9670,9672,`A`],[9673,9674,`N`],[9675,9675,`A`],[9676,9677,`N`],[9678,9681,`A`],[9682,9697,`N`],[9698,9701,`A`],[9702,9710,`N`],[9711,9711,`A`],[9712,9724,`N`],[9725,9726,`W`],[9727,9732,`N`],[9733,9734,`A`],[9735,9736,`N`],[9737,9737,`A`],[9738,9741,`N`],[9742,9743,`A`],[9744,9747,`N`],[9748,9749,`W`],[9750,9755,`N`],[9756,9756,`A`],[9757,9757,`N`],[9758,9758,`A`],[9759,9791,`N`],[9792,9792,`A`],[9793,9793,`N`],[9794,9794,`A`],[9795,9799,`N`],[9800,9811,`W`],[9812,9823,`N`],[9824,9825,`A`],[9826,9826,`N`],[9827,9829,`A`],[9830,9830,`N`],[9831,9834,`A`],[9835,9835,`N`],[9836,9837,`A`],[9838,9838,`N`],[9839,9839,`A`],[9840,9854,`N`],[9855,9855,`W`],[9856,9874,`N`],[9875,9875,`W`],[9876,9885,`N`],[9886,9887,`A`],[9888,9888,`N`],[9889,9889,`W`],[9890,9897,`N`],[9898,9899,`W`],[9900,9916,`N`],[9917,9918,`W`],[9919,9919,`A`],[9920,9923,`N`],[9924,9925,`W`],[9926,9933,`A`],[9934,9934,`W`],[9935,9939,`A`],[9940,9940,`W`],[9941,9953,`A`],[9954,9954,`N`],[9955,9955,`A`],[9956,9959,`N`],[9960,9961,`A`],[9962,9962,`W`],[9963,9969,`A`],[9970,9971,`W`],[9972,9972,`A`],[9973,9973,`W`],[9974,9977,`A`],[9978,9978,`W`],[9979,9980,`A`],[9981,9981,`W`],[9982,9983,`A`],[9984,9988,`N`],[9989,9989,`W`],[9990,9993,`N`],[9994,9995,`W`],[9996,10023,`N`],[10024,10024,`W`],[10025,10044,`N`],[10045,10045,`A`],[10046,10059,`N`],[10060,10060,`W`],[10061,10061,`N`],[10062,10062,`W`],[10063,10066,`N`],[10067,10069,`W`],[10070,10070,`N`],[10071,10071,`W`],[10072,10101,`N`],[10102,10111,`A`],[10112,10132,`N`],[10133,10135,`W`],[10136,10159,`N`],[10160,10160,`W`],[10161,10174,`N`],[10175,10175,`W`],[10176,10213,`N`],[10214,10221,`Na`],[10222,10628,`N`],[10629,10630,`Na`],[10631,11034,`N`],[11035,11036,`W`],[11037,11087,`N`],[11088,11088,`W`],[11089,11092,`N`],[11093,11093,`W`],[11094,11097,`A`],[11098,11903,`N`],[11904,11929,`W`],[11930,11930,`N`],[11931,12019,`W`],[12020,12031,`N`],[12032,12245,`W`],[12246,12271,`N`],[12272,12287,`W`],[12288,12288,`F`],[12289,12350,`W`],[12351,12352,`N`],[12353,12438,`W`],[12439,12440,`N`],[12441,12543,`W`],[12544,12548,`N`],[12549,12591,`W`],[12592,12592,`N`],[12593,12686,`W`],[12687,12687,`N`],[12688,12771,`W`],[12772,12782,`N`],[12783,12830,`W`],[12831,12831,`N`],[12832,12871,`W`],[12872,12879,`A`],[12880,19903,`W`],[19904,19967,`N`],[19968,42124,`W`],[42125,42127,`N`],[42128,42182,`W`],[42183,43359,`N`],[43360,43388,`W`],[43389,44031,`N`],[44032,55203,`W`],[55204,57343,`N`],[57344,63743,`A`],[63744,64255,`W`],[64256,65023,`N`],[65024,65039,`A`],[65040,65049,`W`],[65050,65071,`N`],[65072,65106,`W`],[65107,65107,`N`],[65108,65126,`W`],[65127,65127,`N`],[65128,65131,`W`],[65132,65280,`N`],[65281,65376,`F`],[65377,65470,`H`],[65471,65473,`N`],[65474,65479,`H`],[65480,65481,`N`],[65482,65487,`H`],[65488,65489,`N`],[65490,65495,`H`],[65496,65497,`N`],[65498,65500,`H`],[65501,65503,`N`],[65504,65510,`F`],[65511,65511,`N`],[65512,65518,`H`],[65519,65532,`N`],[65533,65533,`A`],[65534,94175,`N`],[94176,94180,`W`],[94181,94191,`N`],[94192,94193,`W`],[94194,94207,`N`],[94208,100343,`W`],[100344,100351,`N`],[100352,101589,`W`],[101590,101631,`N`],[101632,101640,`W`],[101641,110575,`N`],[110576,110579,`W`],[110580,110580,`N`],[110581,110587,`W`],[110588,110588,`N`],[110589,110590,`W`],[110591,110591,`N`],[110592,110882,`W`],[110883,110897,`N`],[110898,110898,`W`],[110899,110927,`N`],[110928,110930,`W`],[110931,110932,`N`],[110933,110933,`W`],[110934,110947,`N`],[110948,110951,`W`],[110952,110959,`N`],[110960,111355,`W`],[111356,126979,`N`],[126980,126980,`W`],[126981,127182,`N`],[127183,127183,`W`],[127184,127231,`N`],[127232,127242,`A`],[127243,127247,`N`],[127248,127277,`A`],[127278,127279,`N`],[127280,127337,`A`],[127338,127343,`N`],[127344,127373,`A`],[127374,127374,`W`],[127375,127376,`A`],[127377,127386,`W`],[127387,127404,`A`],[127405,127487,`N`],[127488,127490,`W`],[127491,127503,`N`],[127504,127547,`W`],[127548,127551,`N`],[127552,127560,`W`],[127561,127567,`N`],[127568,127569,`W`],[127570,127583,`N`],[127584,127589,`W`],[127590,127743,`N`],[127744,127776,`W`],[127777,127788,`N`],[127789,127797,`W`],[127798,127798,`N`],[127799,127868,`W`],[127869,127869,`N`],[127870,127891,`W`],[127892,127903,`N`],[127904,127946,`W`],[127947,127950,`N`],[127951,127955,`W`],[127956,127967,`N`],[127968,127984,`W`],[127985,127987,`N`],[127988,127988,`W`],[127989,127991,`N`],[127992,128062,`W`],[128063,128063,`N`],[128064,128064,`W`],[128065,128065,`N`],[128066,128252,`W`],[128253,128254,`N`],[128255,128317,`W`],[128318,128330,`N`],[128331,128334,`W`],[128335,128335,`N`],[128336,128359,`W`],[128360,128377,`N`],[128378,128378,`W`],[128379,128404,`N`],[128405,128406,`W`],[128407,128419,`N`],[128420,128420,`W`],[128421,128506,`N`],[128507,128591,`W`],[128592,128639,`N`],[128640,128709,`W`],[128710,128715,`N`],[128716,128716,`W`],[128717,128719,`N`],[128720,128722,`W`],[128723,128724,`N`],[128725,128727,`W`],[128728,128731,`N`],[128732,128735,`W`],[128736,128746,`N`],[128747,128748,`W`],[128749,128755,`N`],[128756,128764,`W`],[128765,128991,`N`],[128992,129003,`W`],[129004,129007,`N`],[129008,129008,`W`],[129009,129291,`N`],[129292,129338,`W`],[129339,129339,`N`],[129340,129349,`W`],[129350,129350,`N`],[129351,129535,`W`],[129536,129647,`N`],[129648,129660,`W`],[129661,129663,`N`],[129664,129672,`W`],[129673,129679,`N`],[129680,129725,`W`],[129726,129726,`N`],[129727,129733,`W`],[129734,129741,`N`],[129742,129755,`W`],[129756,129759,`N`],[129760,129768,`W`],[129769,129775,`N`],[129776,129784,`W`],[129785,131071,`N`],[131072,196605,`W`],[196606,196607,`N`],[196608,262141,`W`],[262142,917759,`N`],[917760,917999,`A`],[918e3,983039,`N`],[983040,1048573,`A`],[1048574,1048575,`N`],[1048576,1114109,`A`],[1114110,1114111,`N`]],version=`15.1.0`;function getEAWOfCodePoint(e){let t=0,n=defs.length-1;for(;t!==n;){let r=t+(n-t>>1),[i,a,o]=defs[r];if(ea)t=r+1;else return o}return defs[t][2]}function getEAW(e,t=0){let n=e.codePointAt(t);if(n!==void 0)return getEAWOfCodePoint(n)}var defaultWidths={N:1,Na:1,W:2,F:2,H:1,A:1};function computeWidth(e,t){let n=0;for(let r of e){let e=getEAW(r);n+=t&&t[e]||defaultWidths[e]}return n}var textOffsetY=5,isSafari=/^((?!chrome|android).)*safari/i.test(navigator.userAgent);function isWideChar(e){switch(getEAW(e)){case`F`:case`W`:return!0;default:return!1}}function isMacOS(){return window.navigator.userAgent.indexOf(`Mac OS X`)!==-1}function computeFontSize(e){let t=document.createElement(`canvas`).getContext(`2d`);t.font=e;let n=t.measureText(`W`);return[Math.floor(n.width),Math.round(n.fontBoundingBoxAscent+textOffsetY+(n.emHeightDescent||0))]}function drawBlock({ctx:e,x:t,y:n,width:r,height:i,style:a}){e.fillStyle=a,e.fillRect(t,n,r,i)}function drawText({ctx:e,x:t,y:n,text:r,font:i,style:a,option:o}){n+=Math.round(textOffsetY),e.fillStyle=a,e.font=i,e.textBaseline=`top`;for(let i of r)isWideChar(i)?(e.fillText(i,t,n,o.fontWidth*2),t+=o.fontWidth*2):(e.fillText(i,t,n,o.fontWidth),t+=o.fontWidth)}function drawHorizontalLine({ctx:e,x:t,y:n,width:r,style:i,lineWidth:a=1}){e.strokeStyle=i,e.lineWidth=a,e.setLineDash=[],e.beginPath(),e.moveTo(t,n),e.lineTo(t+r,n),e.stroke()}var Option=class{constructor({fontName:e,fontSize:t}){this.setFont(e,t),this.foreground=`#cccccc`,this.background=`#2d2d2d`}setFont(e,t){let n=t+`px `+e,[r,i]=computeFontSize(n);this.fontName=e,this.fontSize=t,this.fontWidth=r,this.fontHeight=i,this.font=n}};function getLemEditorElement(){return document.getElementById(`lem-editor`)}function normalizeWheelDelta(e,t,n,r){switch(n){case 0:return{dx:e/r,dy:t/r};case 2:return{dx:e*20,dy:t*20};default:return{dx:e,dy:t}}}function extractWholeLines(e,t){let n=Math.trunc(e),r=Math.trunc(t);return{scrollX:n,scrollY:r,remainderX:e-n,remainderY:t-r}}function cursorPosition(e,t){let[n,r]=t.getDisplayRectangle(),i=e.clientX-n,a=e.clientY-r;return{pixelX:i,pixelY:a,x:Math.floor(i/t.option.fontWidth),y:Math.floor(a/t.option.fontHeight)}}function makeWheelHandler(e){let t={x:0,y:0},n=!1,r={pixelX:0,pixelY:0,x:0,y:0};return i=>{i.preventDefault(),r=cursorPosition(i,e);let{dx:a,dy:o}=normalizeWheelDelta(i.deltaX,i.deltaY,i.deltaMode,e.option.fontHeight);t={x:t.x+a,y:t.y+o},n||(n=!0,requestAnimationFrame(()=>{n=!1;let{scrollX:i,scrollY:a,remainderX:o,remainderY:s}=extractWholeLines(t.x,t.y);t={x:o,y:s},(i!==0||a!==0)&&e.jsonrpc.notify(`input`,{kind:`wheel`,value:{...r,wheelX:-i,wheelY:-a}})}))}}function addMouseEventListeners({dom:e,editor:t,isDraggable:n,draggableStyle:r}){e.addEventListener(`contextmenu`,e=>{e.preventDefault()});let i=(e,n)=>{e.preventDefault();let[r,i]=t.getDisplayRectangle(),a=e.clientX-r,o=e.clientY-i,s=Math.floor(a/t.option.fontWidth),c=Math.floor(o/t.option.fontHeight);t.jsonrpc.notify(`input`,{kind:n,value:{x:s,y:c,pixelX:a,pixelY:o,button:e.button,clicks:e.detail}})};e.addEventListener(`mousedown`,e=>{n&&(document.body.style.cursor=r),t.focusHiddenInput(),i(e,`mousedown`)}),e.addEventListener(`mouseup`,e=>{n&&(document.body.style.cursor=`default`),i(e,`mouseup`)});let a=0;e.addEventListener(`mousemove`,e=>{e.preventDefault();let n=Date.now();if(n-a>50){a=n;let[r,i]=t.getDisplayRectangle(),o=e.clientX-r,s=e.clientY-i,c=Math.floor(o/t.option.fontWidth),l=Math.floor(s/t.option.fontHeight);t.jsonrpc.notify(`input`,{kind:`mousemove`,value:{x:c,y:l,pixelX:o,pixelY:s,button:e.buttons===0?null:e.buttons-1}})}}),n&&(e.addEventListener(`mouseover`,()=>{document.body.style.cursor=r}),e.addEventListener(`mouseout`,e=>{e.buttons!==1&&(document.body.style.cursor=`default`)})),e.addEventListener(`wheel`,makeWheelHandler(t))}var zIndexTable={"floating-window":200,modeline:100,"vertical-border":100,"horizontal-border":101};function zindex(e){return zIndexTable[e]||0}var borderOffsetX=5,borderOffsetY=10,BaseSurface=class{constructor({editor:e}){this.editor=e,this.mainDOM=null,this.wrapper=null}delete(){this.wrapper?getLemEditorElement().removeChild(this.wrapper):getLemEditorElement().removeChild(this.mainDOM)}setupDOM({dom:e,isFloating:t,border:n,cssClassName:r}){this.mainDOM=e,t&&n?(this.wrapper=document.createElement(`div`),r&&(this.wrapper.className=r),this.wrapper.style.position=`absolute`,this.wrapper.style.backgroundColor=this.editor.option.background,this.wrapper.style.zIndex=zindex(`floating-window`),this.wrapper.appendChild(e),getLemEditorElement().appendChild(this.wrapper)):(r&&(e.className=r),getLemEditorElement().appendChild(e))}move(e,t,n,r){let[i,a]=this.editor.getDisplayRectangle(),o=Math.floor(n==null?i+e*this.editor.option.fontWidth:i+n),s=Math.floor(r==null?a+t*this.editor.option.fontHeight:a+r);this.wrapper?(this.wrapper.style.left=o-borderOffsetX+`px`,this.wrapper.style.top=s-borderOffsetY+`px`,this.mainDOM.style.left=borderOffsetX+`px`,this.mainDOM.style.top=borderOffsetY+`px`):(this.mainDOM.style.left=o+`px`,this.mainDOM.style.top=s+`px`)}_resize(e,t,n,r){let i=window.devicePixelRatio||1,a=n??e*this.editor.option.fontWidth,o=r??t*this.editor.option.fontHeight;this.mainDOM.width=a*i,this.mainDOM.height=o*i,this.mainDOM.style.width=a+`px`,this.mainDOM.style.height=o+`px`,this.wrapper&&(this.wrapper.style.width=a+borderOffsetX*2+`px`,this.wrapper.style.height=o+borderOffsetY*2+`px`)}drawBlock(e,t,n,r,i){}drawText(e,t,n,r,i){}drawImage(e,t,n,r,i,a,o){}clearImages(e,t){}clearAllImages(){}touch(){}evalIn(code){return eval(code)}},CanvasSurface=class extends BaseSurface{constructor({editor:e,view:t,x:n,y:r,width:i,height:a,styles:o,isFloating:s,border:c,cssClassName:l}){super({editor:e});let u=this.setupCanvas(o);this.setupDOM({dom:u,isFloating:s,border:c,cssClassName:l}),this.move(n,r),this.resize(i,a),this.drawingQueue=[],addMouseEventListeners({dom:u,editor:e})}setupCanvas(e){let t=document.createElement(`canvas`);if(t.style.position=`absolute`,e)for(let n in e)t.style[n]=e[n];return t}resize(e,t,n,r){this._resize(e,t,n,r);let i=window.devicePixelRatio||1;this.mainDOM.getContext(`2d`).scale(i,i)}move(e,t,n,r){if(super.move(e,t,n,r),this.imageEls)for(let[,e]of this.imageEls)this.positionImage(e)}delete(){this.clearAllImages(),super.delete()}drawBlock(e,t,n,r,i){let a=this.editor.option;this.drawingQueue.push(function(o){drawBlock({ctx:o,x:e*a.fontWidth,y:t*a.fontHeight,width:n*a.fontWidth,height:r*a.fontHeight,style:i})})}drawText(e,t,n,r,i,a){let o=this.editor.option;this.drawingQueue.push(function(s){if(a=a?`${o.fontSize}px ${a}`:o.font,!i)drawBlock({ctx:s,x:e*o.fontWidth,y:t*o.fontHeight,width:r*o.fontWidth,height:o.fontHeight,style:o.background}),drawText({ctx:s,x:e*o.fontWidth,y:t*o.fontHeight,text:n,style:o.foreground,font:a,option:o});else{let{foreground:c,background:l,bold:u,reverse:d,underline:f,cursor:p}=i;if(c||=o.foreground,l||=o.background,d){let e=l;l=c,c=e}p&&(l=o.background);let m=e*o.fontWidth,h=t*o.fontHeight;drawBlock({ctx:s,x:m,y:h,width:r*o.fontWidth,height:o.fontHeight,style:l}),drawText({ctx:s,x:m,y:h,text:n,style:c,font:u?`bold `+a:a,option:o}),f&&drawHorizontalLine({ctx:s,x:m,y:h+o.fontHeight-2,width:r*o.fontWidth,style:typeof f==`string`?f:c,lineWidth:2})}})}imageBaseLeft(){return parseFloat(this.mainDOM.style.left)||0}imageBaseTop(){return parseFloat(this.mainDOM.style.top)||0}drawImage(e,t,n,r,i,a,o){this.imageEls||=new Map;let s=e+`,`+t,c=this.imageEls.get(s);if(c&&c.url!==o&&(c.el.remove(),this.imageEls.delete(s),c=null),!c){let e=document.createElement(`img`);e.style.position=`absolute`,e.style.pointerEvents=`none`,e.style.zIndex=`1`,e.src=o,this.mainDOM.parentNode.appendChild(e),c={el:e,url:o},this.imageEls.set(s,c)}c.x=e,c.y=t,c.widthCells=n,c.heightCells=r,c.pixelWidth=i,c.pixelHeight=a,this.positionImage(c)}positionImage(e){let t=this.editor.option,n=e.widthCells*t.fontWidth,r=e.heightCells*t.fontHeight;e.el.style.left=this.imageBaseLeft()+e.x*t.fontWidth+`px`,e.el.style.top=this.imageBaseTop()+e.y*t.fontHeight+`px`,e.el.style.width=(e.pixelWidth==null?n:Math.min(e.pixelWidth,n))+`px`,e.el.style.height=(e.pixelHeight==null?r:Math.min(e.pixelHeight,r))+`px`}clearImages(e,t){if(this.imageEls)for(let[n,r]of this.imageEls)r.ye&&(r.el.remove(),this.imageEls.delete(n))}clearAllImages(){if(this.imageEls){for(let[,e]of this.imageEls)e.el.remove();this.imageEls.clear()}}touch(){let e=this.mainDOM.getContext(`2d`);for(let t of this.drawingQueue)t(e);this.drawingQueue=[]}activate(){this.mainDOM.dataset.store=`active`}deactivate(){this.mainDOM.dataset.store=`inactive`}},HTMLSurface=class extends BaseSurface{constructor({editor:e,x:t,y:n,width:r,height:i,styles:a,option:o,isFloating:s,border:c,html:l}){super({editor:e});let u=document.createElement(`iframe`);this.setupDOM({dom:u,isFloating:s,border:c}),u.style.position=`absolute`,u.style.backgroundColor=o.background,u.setAttribute(`sandbox`,`allow-scripts allow-same-origin`),u.srcdoc=l,u.addEventListener(`load`,()=>{let e=u.contentWindow;e.invokeLem=(e,t)=>parent.postMessage({type:`invoke-lem`,method:e,args:t})}),this.iframe=u,this.move(t,n),this.resize(r,i)}resize(e,t,n,r){this._resize(e,t,n,r)}update(e){let t=this.iframe.contentWindow.scrollY;this.iframe.srcdoc=e,this.iframe.onload=()=>{this.iframe.onload=null,this.iframe.contentWindow.scrollTo(0,t)}}evalIn(e){return this.iframe.contentWindow.eval(e)}},VerticalBorder=class{constructor({x:e,y:t,height:n,option:r,editor:i}){this.option=r,this.editor=i,this.line=document.createElement(`div`),this.line.className=`lem-editor__vertical-border`,this.line.style.height=n*r.fontHeight+`px`,this.line.style.position=`absolute`,this.line.style.zIndex=zindex(`vertical-border`),getLemEditorElement().appendChild(this.line),this.move(e,t),addMouseEventListeners({dom:this.line,editor:i,isDraggable:!0,draggableStyle:`col-resize`})}delete(){this.line.parentNode.removeChild(this.line)}move(e,t){let[n,r]=this.editor.getDisplayRectangle();this.line.style.left=Math.floor(n+e*this.option.fontWidth-this.option.fontWidth/2)+`px`,this.line.style.top=r+t*this.option.fontHeight+`px`}resize(e){this.line.style.height=e*this.option.fontHeight+`px`}},HorizontalBorder=class{constructor({x:e,y:t,width:n,option:r,editor:i}){this.option=r,this.editor=i,this.line=document.createElement(`div`),this.line.className=`lem-editor__horizontal-border`,this.line.style.width=n*r.fontWidth+`px`,this.line.style.position=`absolute`,this.line.style.zIndex=zindex(`horizontal-border`),getLemEditorElement().appendChild(this.line),this.move(e,t),addMouseEventListeners({dom:this.line,editor:i,isDraggable:!0,draggableStyle:`row-resize`})}delete(){this.line.parentNode.removeChild(this.line)}move(e,t){let[n,r]=this.editor.getDisplayRectangle();this.line.style.left=n+e*this.option.fontWidth+`px`,this.line.style.top=Math.floor(r+t*this.option.fontHeight-4)+`px`}resize(e){this.line.style.width=e*this.option.fontWidth+`px`}},viewStyles={header:()=>{},tile:()=>{},floating:e=>({boxSizing:`border-box`,borderColor:e.foreground,backgroundColor:e.background})};function getViewStyle(e,t){return viewStyles[e](t)||{}}var View=class{constructor({id:e,x:t,y:n,width:r,height:i,pixelX:a,pixelY:o,pixelWidth:s,pixelHeight:c,useModeline:l,kind:u,type:d,content:f,border:p,borderShape:m,option:h,editor:g}){switch(this.option=h,this.id=e,this.x=t,this.y=n,this.width=r,this.height=i,this.pixelX=a,this.pixelY=o,this.pixelWidth=s,this.pixelHeight=c,this.useModeline=l,this.kind=u,this.type=d,this.border=p,this.borderShape=m,this.editor=g,this.bottomBar=null,this.leftsideBar=null,u){case`tile`:this.mainSurface=this.makeSurface(d,f),this.leftSideBar=new VerticalBorder({x:t,y:n,height:i+ +!!l,option:h,editor:g}),l||(this.bottomBar=new HorizontalBorder({x:t,y:n+i-1,width:r,option:h,editor:g}));break;case`header`:this.mainSurface=this.makeSurface(d,f);break;case`floating`:this.mainSurface=this.makeSurface(d,f),m===`left-border`&&(this.leftSideBar=new VerticalBorder({x:t,y:n,height:i,option:h,editor:g}));break}this.modelineSurface=l?this.makeModelineSurface():null,u===`floating`&&(a!=null||o!=null)&&this.move(t,n,a,o)}delete(){this.mainSurface.delete(),this.modelineSurface&&this.modelineSurface.delete(),this.leftSideBar&&this.leftSideBar.delete(),this.bottomBar&&this.bottomBar.delete()}move(e,t,n,r){if(this.x=e,this.y=t,this.pixelX=n,this.pixelY=r,this.mainSurface.move(e,t,n,r),this.modelineSurface){let i=r!=null&&this.pixelHeight!=null?r+this.pixelHeight:null;this.modelineSurface.move(e,t+this.height,n,i)}this.leftSideBar&&this.leftSideBar.move(e,t),this.bottomBar&&this.bottomBar.move(e,t+this.height)}resize(e,t,n,r){if(this.width=e,this.height=t,this.pixelWidth=n,this.pixelHeight=r,this.mainSurface.resize(e,t,n,r),this.modelineSurface){let t=this.pixelY!=null&&r!=null?this.pixelY+r:null;this.modelineSurface.move(this.x,this.y+this.height,this.pixelX,t),this.modelineSurface.resize(e,1)}this.leftSideBar&&this.leftSideBar.resize(t+ +!!this.modelineSurface),this.bottomBar&&this.bottomBar.resize(e)}clear(){this.mainSurface.drawBlock(0,0,this.width,this.height,this.option.background)}clearEol(e,t,n=1){this.mainSurface.drawBlock(e,t,this.width-e,n,this.option.background),this.mainSurface.clearImages(t,t+n)}clearEob(e,t){this.mainSurface.drawBlock(e,t,this.width,this.height-t,this.option.background),this.mainSurface.clearImages(t,this.height)}print(e,t,n,r,i,a){this.mainSurface.drawText(e,t,n,r,i,a)}printImage(e,t,n,r,i,a,o){this.mainSurface.drawImage(e,t,n,r,i,a,o)}printToModeline(e,t,n,r,i){this.modelineSurface&&this.modelineSurface.drawText(e,t,n,r,i)}touch(e){this.mainSurface.touch(),this.modelineSurface&&(this.modelineSurface.touch(),e?this.modelineSurface.activate():this.modelineSurface.deactivate())}makeSurface(e,t){switch(e){case`html`:return this.makeHTMLSurface(t);case`editor`:return this.makeEditorSurface();default:console.error(`unknown type: ${e}`)}}makeHTMLSurface(e){return new HTMLSurface({editor:this.editor,x:this.x,y:this.y,width:this.width,height:this.height,styles:getViewStyle(this.kind,this.option),option:this.option,isFloating:this.kind===`floating`,border:this.border,html:e})}makeEditorSurface(){let e=this.borderShape===`left-border`?0:this.border,t=this.kind===`floating`;return new CanvasSurface({option:this.editor.option,x:this.x,y:this.y,width:this.width,height:this.height,styles:getViewStyle(this.kind,this.option),editor:this.editor,border:e,isFloating:t,view:this,cssClassName:t&&e?`lem-editor__floating-window--bordered`:null})}makeModelineSurface(){let e=new CanvasSurface({option:this.editor.option,x:this.x,y:this.y+this.height,width:this.width,height:1,editor:this.editor,view:this,styles:{zIndex:zindex(`modeline`)},cssClassName:`lem-editor__mode-line`});return addMouseEventListeners({dom:e.mainDOM,editor:this.editor,isDraggable:!0,draggableStyle:`row-resize`}),e}changeToHTMLContent(e){this.mainSurface.constructor.name===`HTMLSurface`?this.mainSurface.update(e):(this.mainSurface.delete(),this.mainSurface=this.makeHTMLSurface(e))}changeToEditorContent(){this.mainSurface.delete(),this.mainSurface=this.makeEditorSurface()}evalIn(e){return this.mainSurface.evalIn(e)}};function isPasteKeyEvent(e){return isMacOS()?e.metaKey&&e.key===`v`:e.ctrlKey&&e.shiftKey&&e.key===`V`}var Input=class{constructor(e){let t=e.option;this.editor=e,this.composition=!1,this.ignoreKeydownAfterCompositionend=!1,this.span=document.createElement(`span`),this.span.style.color=t.foreground,this.span.style.backgroundColor=t.background,this.span.style.position=`absolute`,this.span.style.zIndex=1e6,this.span.style.top=`0`,this.span.style.left=`0`,this.span.style.font=t.font,this.input=document.createElement(`input`),this.input.style.backgroundColor=`transparent`,this.input.style.color=`transparent`,this.input.style.width=`0`,this.input.style.padding=`0`,this.input.style.margin=`0`,this.input.style.border=`none`,this.input.style.position=`absolute`,this.input.style.zIndex=`-10`,this.input.style.top=`0`,this.input.style.left=`0`,this.input.style.font=t.font,this.input.addEventListener(`blur`,e=>{this.input.focus()}),this.input.addEventListener(`input`,e=>{this.composition===!1&&(this.input.value=``,this.span.innerHTML=``,this.input.style.width=`0`,isMacOS()||this.editor.emitInputString(e.data))}),this.input.addEventListener(`paste`,async e=>{e.preventDefault();let t=e.clipboardData||window.Clipboard.data,n=t?.getData(`text`)??t?.getData(`text/plain`);if(n&&n.length>0){this.editor.emitInputString(n);return}try{if(navigator.clipboard?.readText){let e=await navigator.clipboard.readText();if(e&&e.length>0){this.editor.emitInputString(e);return}}}catch(e){console.warn(`clipboard.readText() failed:`,e)}alert(`Paste failed (permission/environment restriction`)}),this.input.addEventListener(`keydown`,e=>{if(!isPasteKeyEvent(e)&&!(e.isComposing||this.composition)&&e.key!==`Process`){if(this.ignoreKeydownAfterCompositionend&&(isSafari||isMacOS())){e.preventDefault(),this.ignoreKeydownAfterCompositionend=!1;return}if(!(!isMacOS()&&!e.ctrlKey&&!e.altKey&&e.key.length===1)&&(e.preventDefault(),e.isComposing!==!0&&e.code!==``))return setTimeout(()=>{this.composition||(this.editor.emitInput(e),this.input.value=``)},0),!1}}),this.input.addEventListener(`compositionstart`,e=>{this.composition=!0,this.span.innerHTML=this.input.value,this.input.style.width=this.span.offsetWidth+`px`}),this.input.addEventListener(`compositionupdate`,e=>{this.span.innerHTML=e.data,this.input.style.width=this.span.offsetWidth+`px`}),this.input.addEventListener(`compositionend`,e=>{this.composition=!1,this.editor.emitInputString(this.input.value),this.input.value=``,this.span.innerHTML=this.input.value,this.input.style.width=`0`,this.ignoreKeydownAfterCompositionend=!0}),document.body.appendChild(this.input),document.body.appendChild(this.span),this.input.focus()}finalize(){document.body.removeChild(this.input),document.body.removeChild(this.span)}move(e,t){let[n,r]=this.editor.getDisplayRectangle();this.span.style.top=r+t+`px`,this.span.style.left=n+e+`px`,this.input.style.top=this.span.offsetTop+`px`,this.input.style.left=this.span.offsetLeft+`px`}updateForeground(e){this.span.style.color=e}updateBackground(e){this.span.style.backgroundColor=e}},MessageTable=class{constructor(){this.map=new Map}register(e,t){for(let n in t){let r=t[n];this.map.set(n,r),e.on(n,r)}}get(e){return this.map.get(e)}};function getDisplayRectangleDefault(){return[0,0,window.innerWidth,window.innerHeight]}var Editor=class{constructor({getDisplayRectangle:e=getDisplayRectangleDefault,fontName:t,fontSize:n,url:r,onExit:i,onClosed:a}){this.getDisplayRectangle=e,this.option=new Option({fontName:t,fontSize:n}),this.onExit=i,this.input=new Input(this),this.cursors=new Map,this.cursorOverlay=document.createElement(`div`),this.cursorOverlay.className=`lem-cursor`,this.cursorOverlay.style.width=this.option.fontWidth+`px`,this.cursorOverlay.style.height=this.option.fontHeight+`px`,this.cursorOverlay.style.backgroundColor=`#ffffff`,this.cursorType=`box`,this.viewMap=new Map,this.jsonrpc=new JSONRPC(r,{onClosed:()=>{a()}}),this.messageTable=new MessageTable,this.messageTable.register(this.jsonrpc,{"update-foreground":this.updateForeground.bind(this),"update-background":this.updateBackground.bind(this),"make-view":this.makeView.bind(this),"delete-view":this.deleteView.bind(this),"resize-view":this.resize.bind(this),"move-view":this.move.bind(this),"redraw-view-after":this.redrawViewAfter.bind(this),clear:this.clear.bind(this),"clear-eol":this.clearEol.bind(this),"clear-eob":this.clearEob.bind(this),put:this.put.bind(this),"put-image":this.putImage.bind(this),"modeline-put":this.modelinePut.bind(this),"update-display":this.updateDisplay.bind(this),"move-cursor":this.moveCursor.bind(this),"change-view":this.changeView.bind(this),"resize-display":this.resizeDisplay.bind(this),bulk:this.bulk.bind(this),exit:this.exitEditor.bind(this),"get-clipboard-text":this.getClipboardText.bind(this),"set-clipboard-text":this.setClipboardText.bind(this),"js-eval":this.jsEval.bind(this),"set-font":this.setFont.bind(this),"get-font":this.getFont.bind(this),"get-display-size":this.getDisplaySize.bind(this),"load-css":this.loadCSS.bind(this),"update-cursor-shape":this.updateCursorShape.bind(this)}),this.login(),this.boundedHandleResize=this.handleResize.bind(this),this.focusHiddenInput=this.focusHiddenInput.bind(this)}init(){window.addEventListener(`resize`,this.boundedHandleResize),document.getElementsByTagName(`html`)[0].style[`background-color`]=`#333`,getLemEditorElement().appendChild(this.cursorOverlay)}finalize(){window.removeEventListener(`resize`,this.boundedHandleResize),this.input.finalize(),this.cursorOverlay.parentNode&&this.cursorOverlay.parentNode.removeChild(this.cursorOverlay)}closeConnection(){this.jsonrpc.close()}emitInput(e){let t=convertKeyEvent(e);if(t){if(t.key===`]`&&t.ctrl&&!t.meta&&!t.super&&!t.shift){this.jsonrpc.notify(`input`,{kind:`abort`});return}t.key!==`Unidentified`&&this.jsonrpc.notify(`input`,{kind:`key`,value:t})}}emitInputString(e){e?this.jsonrpc.notify(`input`,{kind:`input-string`,value:e}):console.error(`unexpected argument`,e)}handleResize(e){let t=!0;this.jsonrpc.notify(`redraw`,{size:this.getDisplaySize()})}focusHiddenInput(){let e=this.input?.input;if(e){try{window.focus()}catch{}requestAnimationFrame(()=>{setTimeout(()=>{e.focus({preventScroll:!0})},0)})}}sendNotification(e,t){this.jsonrpc.notify(e,t)}request(e,t,n){this.jsonrpc.request(e,t,n)}getDisplaySize(){let[e,t,n,r]=this.getDisplayRectangle();return{width:Math.floor(n/this.option.fontWidth),height:Math.floor(r/this.option.fontHeight)}}callMessage(e,t){this.messageTable.get(e)(t)}findViewById(e){return this.viewMap.get(e)}login(){this.jsonrpc.request(`login`,{size:this.getDisplaySize(),foreground:this.option.foreground,background:this.option.background,fontWidth:this.option.fontWidth,fontHeight:this.option.fontHeight},e=>{if(this.updateForeground(e.foreground),this.updateBackground(e.background),e.views)for(let t of e.views)this.makeView(t);this.jsonrpc.notify(`redraw`,{size:this.getDisplaySize()})})}updateForeground(e){e!=null&&(this.option.foreground=e,this.input.updateForeground(e))}updateBackground(e){if(e==null)return;this.option.background=e,this.input.updateBackground(e);let t=getLemEditorElement();t.style.backgroundColor=e}makeView({id:e,x:t,y:n,width:r,height:i,pixelX:a,pixelY:o,pixelWidth:s,pixelHeight:c,use_modeline:l,kind:u,type:d,content:f,border:p,border_shape:m}){let h=new View({option:this.option,id:e,x:t,y:n,width:r,height:i,pixelX:a,pixelY:o,pixelWidth:s,pixelHeight:c,useModeline:l,kind:u,type:d,content:f,border:p,borderShape:m,editor:this});this.viewMap.set(e,h)}deleteView({viewInfo:{id:e}}){this.findViewById(e).delete(),this.viewMap.delete(e)}resize({viewInfo:{id:e},width:t,height:n,pixelWidth:r,pixelHeight:i}){let a=this.findViewById(e);a?a.resize(t,n,r,i):console.warn(`resize: view not found for id ${e}`)}move({viewInfo:{id:e},x:t,y:n,pixelX:r,pixelY:i}){let a=this.findViewById(e);a?a.move(t,n,r,i):console.warn(`move: view not found for id ${e}`)}redrawViewAfter({viewInfo:{id:e},isActive:t}){this.findViewById(e).touch(t)}clear({viewInfo:{id:e}}){this.findViewById(e).clear()}clearEol({viewInfo:{id:e},x:t,y:n,height:r}){this.findViewById(e).clearEol(t,n,r)}clearEob({viewInfo:{id:e},x:t,y:n}){this.findViewById(e).clearEob(t,n)}put({viewInfo:{id:e},x:t,y:n,text:r,textWidth:i,attribute:a,font:o}){this.findViewById(e).print(t,n,r,i,a,o)}putImage({viewInfo:{id:e},x:t,y:n,width:r,height:i,pixelWidth:a,pixelHeight:o,url:s}){this.findViewById(e).printImage(t,n,r,i,a,o,s)}modelinePut({viewInfo:{id:e},x:t,y:n,text:r,textWidth:i,attribute:a}){this.findViewById(e).printToModeline(t,n,r,i,a)}updateDisplay(){}moveCursor({viewInfo:{id:e},x:t,y:n,color:r,cursorText:i,cursorForeground:a}){let o=this.findViewById(e),[s,c]=this.getDisplayRectangle(),l=o.x*this.option.fontWidth+t*this.option.fontWidth,u=o.y*this.option.fontHeight+n*this.option.fontHeight;this.input.move(l,u);let d=r||this.option.foreground,f=a||this.option.background,p=this.cursorOverlay;switch(this.cursorType){case`bar`:p.style.left=s+l+`px`,p.style.top=c+u+`px`,p.style.width=`2px`,p.style.height=this.option.fontHeight+`px`,p.style.backgroundColor=d,p.textContent=``,p.style.color=``,p.style.font=``,p.style.paddingTop=``;break;case`underline`:p.style.left=s+l+`px`,p.style.top=c+u+this.option.fontHeight-2+`px`,p.style.width=this.option.fontWidth+`px`,p.style.height=`2px`,p.style.backgroundColor=d,p.textContent=``,p.style.color=``,p.style.font=``,p.style.paddingTop=``;break;default:p.style.left=s+l+`px`,p.style.top=c+u+`px`,p.style.width=this.option.fontWidth+`px`,p.style.height=this.option.fontHeight+`px`,p.style.backgroundColor=d,p.style.font=this.option.font,p.style.paddingTop=textOffsetY+`px`,p.textContent=i||``,p.style.color=f;break}p.style.animation=`none`,p.offsetHeight,p.style.animation=``}updateCursorShape({cursorType:e}){this.cursorType=e||`box`}changeView({viewInfo:{id:e},type:t,content:n}){let r=this.findViewById(e);switch(t){case`html`:r.changeToHTMLContent(n);break;case`editor`:r.changeToEditorContent();break}}resizeDisplay({width:e,height:t}){let n=getLemEditorElement();n.style.width=Math.floor(e*this.option.fontWidth)+`px`,n.style.height=Math.floor(t*this.option.fontHeight)+`px`}bulk(e){for(let{method:t,argument:n}of e)this.callMessage(t,n)}exitEditor(){this.onExit&&this.onExit()}getClipboardText(){navigator.clipboard?.readText().then(e=>{this.jsonrpc.notify(`got-clipboard-text`,{text:e})})}setClipboardText({text:e}){navigator.clipboard&&navigator.clipboard.writeText(e)}jsEval({viewInfo:{id:e},code:t}){let n=this.findViewById(e).evalIn(t);return n&&n.toString()}setFont({fontName:e,fontSize:t}){this.option.setFont(e||this.option.fontName,t||this.option.fontSize)}getFont(){return{name:this.option.fontName,size:this.option.fontSize}}loadCSS({content:e}){let t=document.createElement(`style`);t.textContent=e,document.head.appendChild(t)}notifyToServer(e,t){this.jsonrpc.notify(`invoke`,{method:e,args:t})}},canvas=document.querySelector(`#editor`);async function main(){await Promise.all([document.fonts.load(`19px file-icons`),document.fonts.load(`19px AllTheIcons`),document.fonts.load(`19px fontawesome`),document.fonts.load(`19px material-design-icons`),document.fonts.load(`19px octicons`)]),await document.fonts.ready;let e=new Editor({canvas,fontName:`Monospace`,fontSize:18,url:`${window.location.protocol===`https:`?`wss`:`ws`}://${window.location.hostname}:${window.location.port}`,onExit:null,onClosed:null});window.addEventListener(`message`,t=>{t.data.type===`invoke-lem`&&e.notifyToServer(t.data.method,t.data.args)}),e.init()}main(); \ No newline at end of file diff --git a/frontends/server/frontend/editor.js b/frontends/server/frontend/editor.js index 55027113a..c52afb54b 100644 --- a/frontends/server/frontend/editor.js +++ b/frontends/server/frontend/editor.js @@ -346,6 +346,10 @@ class BaseSurface { drawBlock(x, y, width, height, color) { } drawText(x, y, text, textWidth, attribute) { } + drawImage(x, y, widthCells, heightCells, pixelWidth, pixelHeight, url) { } + + clearImages(yStart, yEnd) { } + clearAllImages() { } touch() { return; @@ -388,6 +392,18 @@ class CanvasSurface extends BaseSurface { ctx.scale(ratio, ratio); } + move(x, y, pixelX, pixelY) { + super.move(x, y, pixelX, pixelY); + if (this.imageEls) { + for (const [, entry] of this.imageEls) this.positionImage(entry); + } + } + + delete() { + this.clearAllImages(); + super.delete(); + } + drawBlock(x, y, width, height, color) { const option = this.editor.option; this.drawingQueue.push(function(ctx) { @@ -475,6 +491,69 @@ class CanvasSurface extends BaseSurface { }); } + // images are rendered as DOM elements on a layer above the canvas rather than into it. + imageBaseLeft() { return parseFloat(this.mainDOM.style.left) || 0; } + imageBaseTop() { return parseFloat(this.mainDOM.style.top) || 0; } + + drawImage(x, y, widthCells, heightCells, pixelWidth, pixelHeight, url) { + if (!this.imageEls) + // mapping "x,y" to { el, url, x, y, widthCells, heightCells, pixelWidth, pixelHeight } + this.imageEls = new Map(); + const key = x + ',' + y; + let entry = this.imageEls.get(key); + if (entry && entry.url !== url) { + entry.el.remove(); + this.imageEls.delete(key); + entry = null; + } + if (!entry) { + const el = document.createElement('img'); + el.style.position = 'absolute'; + el.style.pointerEvents = 'none'; + // above the surface's own canvas but below the modeline/floating windows. + el.style.zIndex = '1'; + el.src = url; + this.mainDOM.parentNode.appendChild(el); + entry = { el, url }; + this.imageEls.set(key, entry); + } + entry.x = x; + entry.y = y; + entry.widthCells = widthCells; + entry.heightCells = heightCells; + entry.pixelWidth = pixelWidth; + entry.pixelHeight = pixelHeight; + this.positionImage(entry); + } + + positionImage(entry) { + const option = this.editor.option; + // we reserved widthCells x heightCells cells for this image. + const boxWidth = entry.widthCells * option.fontWidth; + const boxHeight = entry.heightCells * option.fontHeight; + entry.el.style.left = (this.imageBaseLeft() + entry.x * option.fontWidth) + 'px'; + entry.el.style.top = (this.imageBaseTop() + entry.y * option.fontHeight) + 'px'; + entry.el.style.width = (entry.pixelWidth != null ? Math.min(entry.pixelWidth, boxWidth) : boxWidth) + 'px'; + entry.el.style.height = (entry.pixelHeight != null ? Math.min(entry.pixelHeight, boxHeight) : boxHeight) + 'px'; + } + + // remove image elements whose row span intersects [yStart, yEnd). + clearImages(yStart, yEnd) { + if (!this.imageEls) return; + for (const [key, entry] of this.imageEls) { + if (entry.y < yEnd && (entry.y + entry.heightCells) > yStart) { + entry.el.remove(); + this.imageEls.delete(key); + } + } + } + + clearAllImages() { + if (!this.imageEls) return; + for (const [, entry] of this.imageEls) entry.el.remove(); + this.imageEls.clear(); + } + touch() { const ctx = this.mainDOM.getContext('2d'); for (let fn of this.drawingQueue) { @@ -778,14 +857,15 @@ class View { ); } - clearEol(x, y) { + clearEol(x, y, height=1) { this.mainSurface.drawBlock( x, y, this.width - x, - 1, + height, this.option.background, ); + this.mainSurface.clearImages(y, y + height); } clearEob(x, y) { @@ -796,6 +876,7 @@ class View { this.height - y, this.option.background, ); + this.mainSurface.clearImages(y, this.height); } print(x, y, text, textWidth, attribute, font) { @@ -809,6 +890,10 @@ class View { ); } + printImage(x, y, width, height, pixelWidth, pixelHeight, url) { + this.mainSurface.drawImage(x, y, width, height, pixelWidth, pixelHeight, url); + } + printToModeline(x, y, text, textWidth, attribute) { if (this.modelineSurface) { this.modelineSurface.drawText( @@ -1166,6 +1251,7 @@ export class Editor { 'clear-eol': this.clearEol.bind(this), 'clear-eob': this.clearEob.bind(this), 'put': this.put.bind(this), + 'put-image': this.putImage.bind(this), 'modeline-put': this.modelinePut.bind(this), 'update-display': this.updateDisplay.bind(this), 'move-cursor': this.moveCursor.bind(this), @@ -1280,6 +1366,8 @@ export class Editor { size: this.getDisplaySize(), foreground: this.option.foreground, background: this.option.background, + fontWidth: this.option.fontWidth, + fontHeight: this.option.fontHeight, }, (response) => { this.updateForeground(response.foreground); this.updateBackground(response.background); @@ -1364,9 +1452,9 @@ export class Editor { view.clear(); } - clearEol({ viewInfo: { id }, x, y }) { + clearEol({ viewInfo: { id }, x, y, height }) { const view = this.findViewById(id); - view.clearEol(x, y); + view.clearEol(x, y, height); } clearEob({ viewInfo: { id }, x, y }) { @@ -1379,6 +1467,11 @@ export class Editor { view.print(x, y, text, textWidth, attribute, font); } + putImage({ viewInfo: { id }, x, y, width, height, pixelWidth, pixelHeight, url }) { + const view = this.findViewById(id); + view.printImage(x, y, width, height, pixelWidth, pixelHeight, url); + } + modelinePut({ viewInfo: { id }, x, y, text, textWidth, attribute }) { const view = this.findViewById(id); view.printToModeline(x, y, text, textWidth, attribute); diff --git a/frontends/server/main.lisp b/frontends/server/main.lisp index aa6b8570b..4a30ed42e 100644 --- a/frontends/server/main.lisp +++ b/frontends/server/main.lisp @@ -101,7 +101,12 @@ (message-queue :initform (queue:make-queue) :reader jsonrpc-message-queue) (editor-thread :initform nil - :accessor jsonrpc-editor-thread)) + :accessor jsonrpc-editor-thread) + ;; pixel size of one character cell, reported by the client. + (cell-width :initform nil + :accessor jsonrpc-cell-width) + (cell-height :initform nil + :accessor jsonrpc-cell-height)) (:default-initargs :name :jsonrpc :redraw-after-modifying-floating-window t @@ -162,6 +167,10 @@ the same immutable instance for every subsequent message." (let ((width (gethash "width" size)) (height (gethash "height" size))) (resize-display jsonrpc width height))) + (alexandria:when-let ((fw (gethash "fontWidth" params))) + (when (plusp fw) (setf (jsonrpc-cell-width jsonrpc) fw))) + (alexandria:when-let ((fh (gethash "fontHeight" params))) + (when (plusp fh) (setf (jsonrpc-cell-height jsonrpc) fh))) (when background (alexandria:when-let (color (lem:parse-color background)) (setf (jsonrpc-background-color jsonrpc) color))) @@ -584,7 +593,34 @@ the same immutable instance for every subsequent message." (lem-core:string-width (lem-core/display:text-object-string drawing-object))) (defmethod object-width ((drawing-object display:image-object)) - 0) + ;; width in character cells. when :pixel-width is given (and the client's cell size is known), + ;; round it up to whole cells so the column reserves enough grid space. otherwise use :width + ;; (a cell count) from the attribute. + (let ((pw (image-pixel-dimension drawing-object :pixel-width)) + (cw (jsonrpc-cell-width (lem-core:implementation)))) + (if (and pw cw) + (ceiling pw cw) + (or (display:image-object-width drawing-object) 1)))) + +(defgeneric object-height (drawing-object) + (:documentation "height of DRAWING-OBJECT in character cells. +we advance the vertical position of the next line by the tallest object's height (see +`max-height-of-objects'), so returning more than 1 for an image makes its line grow to fit.")) + +(defmethod object-height (drawing-object) + 1) + +(defmethod object-height ((drawing-object display:image-object)) + (let ((ph (image-pixel-dimension drawing-object :pixel-height)) + (ch (jsonrpc-cell-height (lem-core:implementation)))) + (if (and ph ch) + (ceiling ph ch) + (or (display:image-object-height drawing-object) 1)))) + +(defun image-pixel-dimension (object key) + "pixel value of KEY (:pixel-width / :pixel-height) on OBJECT's attribute, or NIL." + (let ((attribute (display:image-object-attribute object))) + (and attribute (lem:attribute-value attribute key)))) (defgeneric draw-object (jsonrpc object x y view)) @@ -703,8 +739,42 @@ same hash." attribute :text-width width))) +(defun image-object-url (object) + "return a URL the JS client can load for OBJECT's image, or NIL. +a pathname or plain-string path is served through the existing /local static route. +a string already carrying a data:/https: URL is passed through unchanged." + (let ((image (display:image-object-image object))) + (typecase image + (pathname (format nil "/local~A" (namestring image))) + (string (if (or (alexandria:starts-with-subseq "data:" image) + (alexandria:starts-with-subseq "http:" image) + (alexandria:starts-with-subseq "https:" image)) + image + (format nil "/local~A" image))) + (t nil)))) + (defmethod draw-object (jsonrpc (object display:image-object) x y view) - (values)) + (let ((url (image-object-url object))) + (when url + (with-error-handler () + ;; use the attribute's :pixel-width/:pixel-height if given, else the reserved cell box + ;; (cells * cell pixel size) when the cell size is known. + (let ((pw (or (image-pixel-dimension object :pixel-width) + (alexandria:when-let ((cw (jsonrpc-cell-width jsonrpc))) + (* (object-width object) cw)))) + (ph (or (image-pixel-dimension object :pixel-height) + (alexandria:when-let ((ch (jsonrpc-cell-height jsonrpc))) + (* (object-height object) ch))))) + (notify* jsonrpc + "put-image" + (hash "viewInfo" (view-id-hash view) + "x" x + "y" y + "width" (object-width object) + "height" (object-height object) + "pixelWidth" pw + "pixelHeight" ph + "url" url))))))) (defun render-line (jsonrpc view x y objects) (loop :for object :in objects @@ -719,11 +789,14 @@ same hash." (defmethod lem-if:render-line ((jsonrpc jsonrpc) view x y objects height) (with-error-handler () + ;; clear the line's full height (not just one row) since a tall object such as an image may + ;; occupy several rows. (notify* jsonrpc "clear-eol" (hash "viewInfo" (view-id-hash view) "x" x - "y" y)) + "y" y + "height" height)) (render-line jsonrpc view x y objects))) (defmethod lem-if:render-line-on-modeline ((jsonrpc jsonrpc) view left-objects right-objects @@ -745,7 +818,7 @@ same hash." (object-width drawing-object)) (defmethod lem-if:object-height ((jsonrpc jsonrpc) drawing-object) - 1) + (object-height drawing-object)) (defmethod lem-if:clear-to-end-of-window ((jsonrpc jsonrpc) view y) (notify* jsonrpc diff --git a/src/display/physical-line.lisp b/src/display/physical-line.lisp index 9fb31f600..fa905ea48 100644 --- a/src/display/physical-line.lisp +++ b/src/display/physical-line.lisp @@ -103,9 +103,9 @@ (line-end-object-offset drawing-object-2)))) (defmethod drawing-object-equal ((drawing-object-1 image-object) (drawing-object-2 image-object)) - (and (eq (image-object-image drawing-object-1) (image-object-image drawing-object-1)) - (equal (image-object-width drawing-object-1) (image-object-width drawing-object-1)) - (equal (image-object-height drawing-object-1) (image-object-height drawing-object-1)))) + (and (eq (image-object-image drawing-object-1) (image-object-image drawing-object-2)) + (equal (image-object-width drawing-object-1) (image-object-width drawing-object-2)) + (equal (image-object-height drawing-object-1) (image-object-height drawing-object-2)))) (defgeneric drawing-object-mergable-p (drawing-object-1 drawing-object-2)) @@ -137,12 +137,6 @@ (equal (line-end-object-offset drawing-object-1) (line-end-object-offset drawing-object-2)))) -(defmethod drawing-object-mergable-p ((drawing-object-1 image-object) (drawing-object-2 image-object)) - (and (eq (image-object-image drawing-object-1) (image-object-image drawing-object-1)) - (equal (image-object-width drawing-object-1) (image-object-width drawing-object-1)) - (equal (image-object-height drawing-object-1) (image-object-height drawing-object-1)))) - - (defgeneric drawing-object-merge (drawing-object-1 drawing-object-2)) (defmethod drawing-object-merge ((drawing-object-1 void-object) (drawing-object-2 void-object)) diff --git a/src/internal-packages.lisp b/src/internal-packages.lisp index 8a6e8b04e..22b461bb7 100644 --- a/src/internal-packages.lisp +++ b/src/internal-packages.lisp @@ -14,6 +14,7 @@ :folder-object :icon-object :image-object + :image-object-attribute :image-object-height :image-object-image :image-object-width