* bug#28907: 26.0.90; [eww] some problems in input/textarea
2017-11-02 7:39 ` Katsumi Yamaoka
@ 2017-11-10 7:57 ` Katsumi Yamaoka
2017-11-10 8:28 ` Eli Zaretskii
2017-11-14 8:26 ` Katsumi Yamaoka
0 siblings, 2 replies; 14+ messages in thread
From: Katsumi Yamaoka @ 2017-11-10 7:57 UTC (permalink / raw)
To: Eli Zaretskii; +Cc: Lars Ingebrigtsen, 28907
[-- Attachment #1: Type: text/plain, Size: 2373 bytes --]
On Thu, 02 Nov 2017 16:39:53 +0900, Katsumi Yamaoka wrote:
> On Mon, 30 Oct 2017 08:30:39 +0900, Katsumi Yamaoka wrote:
>> On Fri, 27 Oct 2017 16:31:49 +0300, Eli Zaretskii wrote:
>>>> Date: Fri, 20 Oct 2017 17:39:29 +0900
>>>> From: Katsumi Yamaoka <yamaoka@jpl.org>
>>>> There are some problems with an input form and a textarea as
>>>> follows:
>>>> ・Can't enter text at the beginnig of input/textarea if there is
>>>> a link just above the form.
>>>> ・Can't enter space at (1- eol).
>>>> ・Can't kill a line.
>>>> ・Can't undo.
>>>> ・Can't retrieve the response for the submit in a certain site.
>>>> ・Padding width gets incorrect if there is a wide character.
>>>> ・A major mode command like `g' doesn't work at the right outside
>>>> of textarea.
[...]
Some generic editing commands, except for `undo', now almost work
in input/textarea. To try writing something in those fields, you
can use the following two pages:
<https://html.com/tags/input/#Code_Example>
<https://html.com/tags/textarea/#Code_Example>
A patch is below.
* lisp/net/eww.el (eww-preprocess-text-input): New function that
calculates changed length based on string width, not characters.
(eww-render): Add it to before-change-functions.
(eww-tag-a): Make keymap text property non-sticky.
(eww-text-map, eww-textarea-map): Remap some generic editing keys.
(eww-beginning-of-text, eww-end-of-text): Work on textarea as well.
(eww-field-extract-text, eww-field-uniline-text, eww-field-insert-text)
(eww-field-funcall): New functions.
(eww-field-backward-delete-char-untabify)
(eww-field-delete-backward-char, eww-field-delete-char)
(eww-field-kill-line, eww-field-kill-region, eww-field-newline)
(eww-field-open-line, eww-field-yank, eww-field-yank-pop): New commands.
(eww-form-text): Pass field width to eww-form property.
(eww-field-replace-length): New internal variable.
(eww-process-text-input): Use string width based changed length;
enable a user to enter space at (1- eol);
don't break the form when killing a line at the beginning of the form.
(eww-tag-textarea): Treat textarea's text as its value if it is null;
work the padding correctly when there is a wide character;
make keymap prop non-sticky;
pass field width and height to eww-form property.
(eww-size-text-inputs): Make the field end position a marker.
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: Type: text/x-patch, Size: 19527 bytes --]
--- eww.el~ 2017-09-14 22:06:50.000000000 +0000
+++ eww.el 2017-11-10 07:54:06.465612900 +0000
@@ -397,7 +397,9 @@
(setq eww-history-position 0)
(and last-coding-system-used
(set-buffer-file-coding-system last-coding-system-used))
- (run-hooks 'eww-after-render-hook)))
+ (run-hooks 'eww-after-render-hook)
+ (add-hook 'before-change-functions
+ 'eww-preprocess-text-input nil t)))
(kill-buffer data-buffer))))
(defun eww-parse-headers ()
@@ -546,7 +548,8 @@
(eww-handle-link dom)
(let ((start (point)))
(shr-tag-a dom)
- (put-text-property start (point) 'keymap eww-link-keymap)))
+ (put-text-property start (point) 'keymap eww-link-keymap)
+ (put-text-property start (point) 'rear-nonsticky '(keymap))))
(defun eww-update-header-line-format ()
(setq header-line-format
@@ -955,17 +958,29 @@
(define-key map [(control a)] 'eww-beginning-of-text)
(define-key map [(control c) (control c)] 'eww-submit)
(define-key map [(control e)] 'eww-end-of-text)
+ (define-key map [(control j)] 'eww-submit)
(define-key map [?\t] 'shr-next-link)
(define-key map [?\M-\t] 'shr-previous-link)
+ (dolist (cmd '(backward-delete-char-untabify
+ delete-backward-char delete-char
+ kill-line kill-region yank yank-pop))
+ (define-key map `[remap ,cmd] (intern (format "eww-field-%s" cmd))))
+ (define-key map [remap open-line] 'ignore)
map))
(defvar eww-textarea-map
(let ((map (make-keymap)))
(set-keymap-parent map text-mode-map)
- (define-key map "\r" 'forward-line)
+ (define-key map "\r" 'eww-field-newline)
+ (define-key map [(control a)] 'eww-beginning-of-text)
(define-key map [(control c) (control c)] 'eww-submit)
+ (define-key map [(control e)] 'eww-end-of-text)
(define-key map [?\t] 'shr-next-link)
(define-key map [?\M-\t] 'shr-previous-link)
+ (dolist (cmd '(backward-delete-char-untabify
+ delete-backward-char delete-char
+ kill-line kill-region open-line yank yank-pop))
+ (define-key map `[remap ,cmd] (intern (format "eww-field-%s" cmd))))
map))
(defvar eww-select-map
@@ -975,20 +990,343 @@
map))
(defun eww-beginning-of-text ()
- "Move to the start of the input field."
+ "Move to the beginning of the current line in the input field."
(interactive)
- (goto-char (eww-beginning-of-field)))
+ (beginning-of-line)
+ (goto-char (text-property-not-all (point) (line-end-position)
+ 'inhibit-read-only nil)))
(defun eww-end-of-text ()
- "Move to the end of the text in the input field."
+ "Move to the end of the text of the current line in the input field.
+When the point is already in the end of the text, move to the end of
+the current line."
(interactive)
- (goto-char (eww-end-of-field))
- (let ((start (eww-beginning-of-field)))
- (while (and (equal (following-char) ? )
- (> (point) start))
- (forward-char -1))
- (when (> (point) start)
- (forward-char 1))))
+ (let ((end (or (text-property-any (point) (line-end-position)
+ 'inhibit-read-only nil)
+ (line-end-position))))
+ (unless (prog1
+ (and (looking-at " *") (>= (match-end 0) end))
+ (goto-char (if (get-text-property end 'inhibit-read-only)
+ end (1- end))))
+ (skip-chars-backward " "))))
+
+(defun eww-field-extract-text ()
+ "Return the contents of the input/textarea field where the point is.
+The contents will be divided into a list of two (or three) parts by
+the mark (if exists as either active or inactive in the field) and
+the current position, and returned as the form:
+
+(FLAG (\"TEXT\" ...) (\"TEXT\" ...) ...)
+
+Where FLAG will be non-nil if the mark is behind the current position.
+Each part is a list of text lines; text lines may be nil that means
+a null text; a text line does not end with a newline; assume that
+there is a newline between adjacent text lines."
+ (let* ((form (get-text-property (point) 'eww-form))
+ (start (or (cdr (assq :start form)) (point-min)))
+ (end (or (cdr (assq :end form)) (point-max)))
+ (mk (mark t))
+ (pos (point))
+ (from start)
+ swap opos boundary bound to lines parts)
+ (if (and mk (>= mk start) (<= mk end))
+ (when (setq swap (> mk pos))
+ (setq mk (prog1 (setq opos pos) (setq pos mk))))
+ (setq mk nil))
+ (setq boundary (or mk pos))
+ (while (< from end)
+ (if (= from boundary)
+ (progn
+ (push '(nil) parts)
+ (if mk
+ (setq mk nil
+ boundary pos)
+ (setq boundary end)))
+ (goto-char from)
+ (setq bound (min boundary (line-end-position))
+ to (or (and form
+ (text-property-any from bound 'inhibit-read-only nil))
+ bound))
+ (push (buffer-substring-no-properties from to) lines)
+ (when (or (= to boundary)
+ (not (setq from (if form
+ (text-property-not-all
+ (1+ to) boundary 'inhibit-read-only nil)
+ (forward-line 1)
+ (and (bolp) (< (point) boundary)
+ (point))))))
+ (goto-char boundary)
+ (when (or (bolp)
+ (and form
+ (not (get-text-property (1- boundary)
+ 'inhibit-read-only))))
+ (push "" lines))
+ (push (nreverse lines) parts)
+ (setq lines nil
+ from boundary)
+ (if mk
+ (setq mk nil
+ boundary pos)
+ (setq boundary end)))))
+ (goto-char (or opos pos))
+ (if parts
+ (cons swap (if (= pos end)
+ (nconc (nreverse parts) '((nil)))
+ (nreverse parts)))
+ `(,swap (nil) (nil) (nil)))))
+
+(defun eww-field-uniline-text ()
+ "Extract text from the current temporary buffer excluding newlines."
+ (let ((pos (point))
+ parts)
+ (while (re-search-forward "\\([\t ]+\\)\n+\\|\n+\\([\t ]+\\)" nil t)
+ (replace-match "\\1\\2"))
+ (goto-char pos)
+ (while (re-search-forward "\n+" nil 'move)
+ (replace-match " "))
+ (skip-chars-backward "\t ")
+ (setq parts (list (list (buffer-substring pos (point)))))
+ (delete-region pos (point-max))
+ (when (setq pos (mark t))
+ (goto-char pos)
+ (while (re-search-forward "\\([\t ]+\\)\n+\\|\n+\\([\t ]+\\)" nil t)
+ (replace-match "\\1\\2"))
+ (goto-char pos)
+ (while (re-search-forward "\n+" nil t)
+ (replace-match " "))
+ (push (list (buffer-substring pos (point-max))) parts)
+ (delete-region pos (point-max)))
+ (goto-char (point-min))
+ (while (re-search-forward "\\([\t ]+\\)\n+\\|\n+\\([\t ]+\\)" nil t)
+ (replace-match "\\1\\2"))
+ (goto-char (point-min))
+ (while (re-search-forward "\n+" nil t)
+ (replace-match " "))
+ (cons (list (buffer-string)) parts)))
+
+(defun eww-field-insert-text (parts &optional swap-point-and-mark)
+ "Insert PARTS, a list of lists of text lines, in the current buffer.
+Put an inactive mark at the beginning of the second part if there are
+tree parts. Position point at the beginning of the last part. If
+SWAP-POINT-AND-MARK is non-nil, the point and the mark will be swapped."
+ (let* ((form (get-text-property (point) 'eww-form))
+ (textareap (equal (plist-get form :type) "textarea"))
+ (start (cdr (assq :start form)))
+ (end (cdr (assq :end form)))
+ mk pos)
+ (if form ;; We are in an eww buffer.
+ (let ((properties (text-properties-at start))
+ (width (plist-get form :width))
+ (inhibit-read-only t)
+ (inhibit-modification-hooks t)
+ indent lines line from to len)
+ (unless textareap
+ (with-temp-buffer
+ (eww-field-insert-text parts)
+ (setq parts (eww-field-uniline-text))))
+ (goto-char start)
+ (setq indent (current-column))
+ (while (< (point) end)
+ (if parts
+ (while (setq lines (pop parts))
+ (while lines
+ ;; We are in the beginning of the text line.
+ (when (setq line (pop lines))
+ (setq from (point))
+ (goto-char (or (text-property-any from (line-end-position)
+ 'inhibit-read-only nil)
+ (line-end-position)))
+ (setq to (cons (current-column) (point-marker)))
+ (goto-char from)
+ (unless (equal line "")
+ (insert-before-markers line)
+ (set-text-properties from (point) properties)
+ (setq from (point)))
+ (unless (or textareap (eq (char-before) ? ))
+ (insert-before-markers " ")
+ (set-text-properties from (point) properties))
+ (when (> (setq len (- width (current-column) indent)) 0)
+ (set-text-properties
+ (point) (progn
+ (insert-before-markers (make-string len ? ))
+ (point))
+ properties))
+ (delete-region (point) (cdr to))
+ (when (and textareap (eolp))
+ (set-text-properties (point) (1+ (point)) properties))
+ (if lines
+ (if (or (= (point) end)
+ (progn
+ (forward-line 1)
+ (>= (point) end)))
+ (progn
+ (setq from (point))
+ (insert-before-markers
+ (make-string (car to) ? )
+ (if (memq (char-after (cdr to)) '(nil ?\n))
+ "\n" ""))
+ (set-text-properties from (point) properties)
+ (forward-char -1)
+ (move-to-column indent))
+ (move-to-column indent))
+ (goto-char from)
+ (unless (or (not textareap)
+ (bolp)
+ (not (get-text-property
+ (1- from) 'inhibit-read-only)))
+ (forward-line 1)
+ (when (>= (point) end)
+ (goto-char end)
+ (insert (make-string (car to) ? ) "\n")
+ (set-text-properties end (point) properties)
+ (set-marker end (point)))
+ (goto-char from)))
+ (set-marker (cdr to) nil)))
+ (cond ((cdr parts) (setq mk (point)))
+ (parts (setq pos (point)))))
+ (setq from (point)) ;; The end of the last text line.
+ (goto-char (or (text-property-any from (line-end-position)
+ 'inhibit-read-only nil)
+ (line-end-position)))
+ (setq to (cons (current-column) (point-marker)))
+ (if textareap
+ (progn
+ (goto-char from)
+ (insert-before-markers
+ (make-string (max 0 (- (car to) (current-column))) ? ))
+ (set-text-properties from (point) properties)
+ (delete-region (point) (cdr to))
+ (when (eolp)
+ (set-text-properties (point) (1+ (point)) properties))
+ (forward-line 1)
+ (when (< (point) end)
+ (move-to-column indent)))
+ (skip-chars-backward " " start)
+ (when (>= (setq len (- width
+ (string-width
+ (buffer-substring start (point)))))
+ 0)
+ (forward-char len)
+ (delete-region (point) (cdr to)))
+ (goto-char (cdr to))
+ (unless (eq (char-before) ? )
+ (insert-before-markers " ")
+ (set-text-properties (1- (point)) (point) properties)))
+ (set-marker (cdr to) nil)))
+ ;; We are in `end' that is the end of the textarea.
+ (when (and textareap
+ (setq lines (plist-get form :lines))
+ (progn
+ (goto-char pos)
+ (forward-line 1)
+ (prog1
+ (< (point) end)
+ (goto-char end)))
+ (< (setq lines (- lines (count-lines start end))) 0))
+ (forward-line lines)
+ (when (re-search-forward "\\(?:[^\n ]+[\n ]+\\)*[^\n ]+" end t)
+ (forward-line 1))
+ (delete-region (point) end)))
+ ;; We are in a temporary buffer.
+ (while parts
+ (insert (mapconcat #'identity (pop parts) "\n"))
+ (cond ((cdr parts) (setq mk (point)))
+ (parts (setq pos (point))))))
+ (when mk
+ (when swap-point-and-mark
+ (setq mk (prog1 pos (setq pos mk))))
+ (goto-char mk)
+ (push-mark))
+ (goto-char (if end (min pos (1- end)) pos))))
+
+(defun eww-field-funcall (function &rest args)
+ "Run FUNCTION on the copied field and replace the field with the result.
+ARGS is a list of arguments that will be passed to FUNCTION."
+ (let* ((parts (eww-field-extract-text))
+ (swap (pop parts))
+ pos)
+ (unwind-protect
+ (with-temp-buffer
+ (eww-field-insert-text parts swap)
+ (setq pos (point-marker))
+ (goto-char (point-min))
+ (while (re-search-forward " +$" pos t)
+ (replace-match ""))
+ (goto-char pos)
+ (while (re-search-forward " +$" nil t)
+ (replace-match ""))
+ (goto-char pos)
+ (set-marker pos nil)
+ (unwind-protect
+ (apply function args)
+ (setq pos (point))
+ (goto-char (point-max))
+ (skip-chars-backward "\n " pos)
+ (delete-region (point) (point-max))
+ (goto-char pos)
+ (setq parts (cdr (eww-field-extract-text)))))
+ (eww-field-insert-text parts swap)
+ (deactivate-mark))))
+
+(defun eww-field-backward-delete-char-untabify (arg &optional killp)
+ "Delete characters backward, changing tabs into spaces.
+See `backward-delete-char-untabify' for more information."
+ (interactive "*p\nP")
+ (eww-field-funcall #'backward-delete-char-untabify arg killp))
+
+(defun eww-field-delete-backward-char (n &optional killflag)
+ "Delete the previous N characters (following if N is negative).
+See `delete-backward-char' for more information."
+ (interactive "p\nP")
+ (eww-field-funcall #'delete-backward-char n killflag))
+
+(defun eww-field-delete-char (n &optional killflag)
+ "Delete the following N characters (previous if N is negative).
+See `delete-char' for more information."
+ (interactive "p\nP")
+ (eww-field-funcall #'delete-char n killflag))
+
+(defun eww-field-kill-line (&optional arg)
+ "Kill the rest of the current line. See `kill-line' for more info."
+ (interactive "*P")
+ (eww-field-funcall #'kill-line arg))
+
+(defun eww-field-kill-region (beg end &optional region)
+ "Kill text between point and mark. See `kill-region' for more info."
+ (interactive (list (mark) (point) 'region))
+ (unless (and beg end)
+ (user-error "The mark is not set now, so there is no region"))
+ (eww-field-funcall #'kill-region beg end region))
+
+(defun eww-field-newline (&optional arg interactive)
+ "Insert a newline, and move to left margin of the new line if it's blank.
+See `newline' for more information."
+ (interactive "*P\np")
+ (eww-field-funcall #'newline arg interactive))
+
+(defun eww-field-open-line (n)
+ "Insert a newline and leave point before it.
+See `open-line' for more information."
+ (interactive "*p")
+ (eww-field-funcall #'open-line n))
+
+(defun eww-field-yank (&optional arg)
+ "Reinsert the last stretch of killed text. See `yank' for more info."
+ (interactive "*P")
+ (push-mark)
+ (eww-field-funcall #'yank arg))
+
+(defun eww-field-yank-pop (&optional arg)
+ "Replace just-yanked stretch of killed text with a different stretch.
+See `yank-pop' for more infomation."
+ (interactive "*p")
+ (let ((start (eww-beginning-of-field))
+ (mk (mark t)))
+ (unless (and mk (>= mk start) (<= mk (eww-end-of-field)))
+ (goto-char (prog1 (point)
+ (goto-char start)
+ (push-mark))))
+ (eww-field-funcall #'yank-pop arg)))
(defun eww-beginning-of-field ()
(cond
@@ -1099,7 +1437,10 @@
(list :eww-form eww-form
:value value
:type type
- :name (dom-attr dom 'name)))
+ :name (dom-attr dom 'name)
+ :width width))
+ ;; Enable the major mode keymap on the subsequent gap.
+ (put-text-property start (point) 'rear-nonsticky '(local-map))
(insert " ")))
(defconst eww-text-input-types '("text" "password" "textarea"
@@ -1109,19 +1450,26 @@
"List of input types which represent a text input.
See URL `https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input'.")
-(defun eww-process-text-input (beg end replace-length)
+(defvar eww-field-replace-length nil)
+
+(defun eww-preprocess-text-input (beg end)
+ (setq eww-field-replace-length (string-width (buffer-substring beg end))))
+
+(defun eww-process-text-input (beg end _replace-length)
(when-let* ((pos (and (< (1+ end) (point-max))
(> (1- end) (point-min))
(cond
+ ((get-text-property end 'eww-form)
+ end)
((get-text-property (1+ end) 'eww-form)
(1+ end))
((get-text-property (1- end) 'eww-form)
(1- end))))))
(let* ((form (get-text-property pos 'eww-form))
(properties (text-properties-at pos))
- (buffer-undo-list t)
(inhibit-read-only t)
- (length (- end beg replace-length))
+ (length (- (string-width (buffer-substring beg end))
+ eww-field-replace-length))
(type (plist-get form :type)))
(when (and form
(member type eww-text-input-types))
@@ -1134,6 +1482,8 @@
(1- (line-end-position))
(eww-end-of-field)))
(while (and (> length 0)
+ ;; Don't delete space a user enters at (1- eol).
+ (< end (point))
(eql (char-after (1- (point))) ? ))
(delete-region (1- (point)) (point))
(cl-decf length))))
@@ -1141,13 +1491,10 @@
;; Add padding.
(save-excursion
(goto-char end)
- (goto-char
- (if (equal type "textarea")
- (1- (line-end-position))
- (1+ (eww-end-of-field))))
- (let ((start (point)))
- (insert (make-string (abs length) ? ))
- (set-text-properties start (point) properties))
+ (if (equal type "textarea")
+ (end-of-line)
+ (goto-char (1+ (eww-end-of-field))))
+ (insert-before-markers (make-string (abs length) ? ))
(goto-char (1- end)))))
(set-text-properties (cdr (assq :start form))
(cdr (assq :end form))
@@ -1166,13 +1513,14 @@
'display (make-string (length value) ?*)))))))))
(defun eww-tag-textarea (dom)
- (let ((start (point))
- (value (or (dom-attr dom 'value) ""))
+ (let ((value (or (dom-attr dom 'value) (dom-text dom)))
(lines (string-to-number (or (dom-attr dom 'rows) "10")))
(width (string-to-number (or (dom-attr dom 'cols) "10")))
- end)
+ start end)
(shr-ensure-newline)
- (insert value)
+ (setq start (point))
+ (let ((fill-column width))
+ (fill-region start (progn (insert value) (point))))
(shr-ensure-newline)
(when (< (count-lines start (point)) lines)
(dotimes (_ (- lines (count-lines start (point))))
@@ -1181,20 +1529,24 @@
(goto-char start)
(while (< (point) end)
(end-of-line)
- (let ((pad (- width (- (point) (line-beginning-position)))))
+ (let ((pad (- width (current-column)))) ;; There may be a wide character.
(when (> pad 0)
(insert (make-string pad ? ))))
(add-face-text-property (line-beginning-position)
(point) 'eww-form-textarea)
- (put-text-property (line-beginning-position) (point) 'inhibit-read-only t)
- (put-text-property (line-beginning-position) (point)
- 'local-map eww-textarea-map)
+ (add-text-properties (line-beginning-position) (point)
+ `(inhibit-read-only t
+ local-map ,eww-textarea-map
+ ;; Enable the major mode keymap on newlines.
+ rear-nonsticky (local-map)))
(forward-line 1))
(put-text-property start (point) 'eww-form
(list :eww-form eww-form
:value value
:type "textarea"
- :name (dom-attr dom 'name)))))
+ :name (dom-attr dom 'name)
+ :lines lines
+ :width width))))
(defun eww-tag-input (dom)
(let ((type (downcase (or (dom-attr dom 'type) "text")))
@@ -1366,7 +1718,7 @@
(nconc props (list (cons :start start)))
(setq start (next-single-property-change
start 'eww-form nil (point-max)))
- (nconc props (list (cons :end start))))))))
+ (nconc props (list (cons :end (set-marker (make-marker) start)))))))))
(defun eww-input-value (input)
(let ((type (plist-get input :type))
^ permalink raw reply [flat|nested] 14+ messages in thread