From bbc8eabd48e197e6ca3af13e205fb9161050fdbe Mon Sep 17 00:00:00 2001 From: "Andre A. Gomes" Date: Mon, 23 Oct 2023 10:15:03 +0300 Subject: [PATCH 1/6] gitmodules: Bump trivial-clipboard. --- _build/trivial-clipboard | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_build/trivial-clipboard b/_build/trivial-clipboard index 6ddf8d5dff8..aee67d6132a 160000 --- a/_build/trivial-clipboard +++ b/_build/trivial-clipboard @@ -1 +1 @@ -Subproject commit 6ddf8d5dff8f5c2102af7cd1a1751cbe6408377b +Subproject commit aee67d6132a46237f61d508ae4bd9ff44032566d From 4b51f03b57a9f463ca3e03c189e312fc254930e0 Mon Sep 17 00:00:00 2001 From: "Andre A. Gomes" Date: Fri, 20 Oct 2023 12:06:46 +0300 Subject: [PATCH 2/6] Handle types of slot external-editor-program. 9868be8c2a37b8f948a13a37a920b9d146601205 introduced the idea, but the implementation was flawed. --- source/browser.lisp | 14 +++++++-- source/external-editor.lisp | 54 +++++++++-------------------------- source/mode/file-manager.lisp | 7 +++-- source/spinneret-tags.lisp | 4 +-- 4 files changed, 32 insertions(+), 47 deletions(-) diff --git a/source/browser.lisp b/source/browser.lisp index d1bad78ee3f..5199d0876af 100644 --- a/source/browser.lisp +++ b/source/browser.lisp @@ -273,10 +273,12 @@ The handlers take the `prompt-buffer' as argument.") (or (uiop:getenv "VISUAL") (uiop:getenv "EDITOR")) :type (or (cons string *) string null) + :reader nil + :writer t :export t :documentation "The external editor to use for editing files. -The full command line arguments may specified as a list of strings, or a single -string with spaces between the arguments.")) +The full command, including its arguments, may be specified as list of strings +or as a single string.")) (:export-class-name-p t) (:export-accessor-names-p t) (:documentation "The browser class defines the overall behavior of Nyxt, in @@ -299,6 +301,14 @@ prevents otherwise.") (declare (ignore ignored)) (make-instance 'theme:theme)) +(defmethod external-editor-program ((browser browser)) + "Specialized reader for `external-editor-program' slot." + (with-slots ((cmd external-editor-program)) browser + (typecase cmd + (list (unless (sera:blankp (first cmd)) cmd)) + (string (unless (sera:blankp cmd) (str:split " " cmd))) + (t (echo-warning "Invalid value of `external-editor-program' browser slot.") nil)))) + (defmethod get-containing-window-for-buffer ((buffer buffer) (browser browser)) "Get the window containing a buffer." (find buffer (alex:hash-table-values (windows browser)) :key #'active-buffer)) diff --git a/source/external-editor.lisp b/source/external-editor.lisp index c8bafdfb8d0..d17201fd3a1 100644 --- a/source/external-editor.lisp +++ b/source/external-editor.lisp @@ -3,43 +3,6 @@ (in-package :nyxt) -(-> %append-uiop-command ((or string (list-of string)) &rest string) (values (or string (list-of string)) &optional)) -(defun %append-uiop-command (command &rest args) - "Appends ARGS to an existing COMMAND (for `uiop:run-program' or `uiop:launch-program'). - -If COMMAND is a string, ARGS is concatenated to it with spaces between the -arguments. - -If COMMAND is a list, ARGS is appended to it. - -Signals an error if COMMAND is nil or an empty string." - ;; The uiop functions expect either the entire command as a string, or a list - ;; of strings with the command as the first element, and each parameter as - ;; subsequent elements. Mixing them signals an error. This is the reason for - ;; this custom append function. - (cond - ((null command) (error "Unable to append arguments to a null command.")) - ((consp command) (append command args)) - ((str:emptyp command) (error "Unable to append arguments to an empty command.")) - ((null args) command) - ((stringp command) (uiop:reduce/strcat (list command " " (str:unwords args)))))) - -(export-always 'run-external-editor) -(defun run-external-editor (path &optional (program (external-editor-program *browser*))) - "Calls `uiop:run-program' with PATH as an extra parameter to PROGRAM. -PROGRAM defaults to `external-editor-program'" - (let ((command (%append-uiop-command program (uiop:native-namestring path)))) - (log:debug "External editor opens ~s" command) - (uiop:run-program command))) - -(export-always 'launch-external-editor) -(defun launch-external-editor (path &optional (program (external-editor-program *browser*))) - "Calls `uiop:launch-program' with PATH as an extra parameter to PROGRAM. -PROGRAM defaults to `external-editor-program'" - (let ((command (%append-uiop-command program (uiop:native-namestring path)))) - (log:debug "Launch external editor ~s" command) - (uiop:launch-program command))) - (defun %edit-with-external-editor (&optional input-text) "Edit `input-text' using `external-editor-program'. Create a temporary file and return its content. The editor runs synchronously @@ -50,8 +13,12 @@ so invoke on a separate thread when possible." (with-open-file (f p :direction :io :if-exists :append) (write-sequence input-text f))) + (log:debug "External editor ~s opens ~s" + (external-editor-program *browser*) p) (with-protect ("Failed editing: ~a" :condition) - (run-external-editor p)) + (uiop:run-program (append (uiop:ensure-list (external-editor-program *browser*)) + (list (uiop:native-namestring p))) + :ignore-error-status t)) (uiop:read-file-string p))) (define-parenscript select-input-field () @@ -108,7 +75,10 @@ If the user file is GPG-encrypted, the editor must be capable of decrypting it." (let* ((file (prompt1 :prompt "Edit user file in external editor" :sources 'user-file-source)) (path (files:expand file))) - (launch-external-editor (uiop:native-namestring path))) + + (echo "Using \"~{~a~^ ~}\" to edit ~s." (external-editor-program *browser*) path) + (uiop:launch-program `(,@(uiop:ensure-list (external-editor-program *browser*)) + ,(uiop:native-namestring path)))) (echo-warning "Please set `external-editor-program' browser slot."))) (defun %view-source-with-external-editor () @@ -123,8 +93,12 @@ separate thread when possible." (if (> (length page-source) 0) (progn (alexandria:write-string-into-file page-source p :if-exists :supersede) + (log:debug "External editor ~s opens ~s" + (external-editor-program *browser*) p) (with-protect ("Failed editing: ~a" :condition) - (run-external-editor p))) + (uiop:run-program (append (uiop:ensure-list (external-editor-program *browser*)) + (list (uiop:native-namestring p))) + :ignore-error-status t))) (echo-warning "Nothing to edit."))))) (define-command-global view-source-with-external-editor () diff --git a/source/mode/file-manager.lisp b/source/mode/file-manager.lisp index e3d5e8f3f3a..7f91dd116a6 100644 --- a/source/mode/file-manager.lisp +++ b/source/mode/file-manager.lisp @@ -235,9 +235,10 @@ See `supported-media-types' of `file-mode'." :sources 'file-source))) "Edit the FILES using `external-editor-program'. If FILES are not provided, prompt for them." - (if (external-editor-program *browser*) - (apply #'launch-external-editor (mapcar #'uiop:native-namestring files)) - (echo-warning "Please set `external-editor-program' browser slot."))) + (echo "Issued \"~{~a~^ ~}\" to edit ~s." (external-editor-program *browser*) files) + (with-protect ("Failed editing: ~a. See `external-editor-program' slot." :condition) + (uiop:launch-program `(,@(external-editor-program *browser*) + ,@(mapcar #'uiop:native-namestring files))))) (defmethod initialize-instance :after ((source open-file-source) &key) (setf (slot-value source 'prompter:actions-on-return) diff --git a/source/spinneret-tags.lisp b/source/spinneret-tags.lisp index f09148b4a76..cf091982683 100644 --- a/source/spinneret-tags.lisp +++ b/source/spinneret-tags.lisp @@ -535,8 +535,8 @@ Most *-P arguments mandate whether to add the buttons for: `(`((external-editor "Open in external editor" "Open the file this code comes from in external editor.") - (funcall (read-from-string "nyxt:launch-external-editor") - (uiop:native-namestring ,,file-var)))))))) + (funcall (read-from-string "nyxt/mode/file-manager:edit-file-with-external-editor") + (uiop:ensure-list ,,file-var)))))))) (declare (ignorable keys)) `(let* ((,body-var (list ,@body)) (,first (first ,body-var)) From 06887949d96a74bd01c888ae4056e0ca9edd2836 Mon Sep 17 00:00:00 2001 From: "Andre A. Gomes" Date: Fri, 20 Oct 2023 15:55:45 +0300 Subject: [PATCH 3/6] external-editor: Major refactor. Delete move-caret-to-end. No longer needed since its logic is covered by ffi-buffer-paste. Simplify view-source-with-external-editor. Update comments. --- source/external-editor.lisp | 124 ++++++++++-------------------------- 1 file changed, 33 insertions(+), 91 deletions(-) diff --git a/source/external-editor.lisp b/source/external-editor.lisp index d17201fd3a1..5de22a07bc0 100644 --- a/source/external-editor.lisp +++ b/source/external-editor.lisp @@ -3,108 +3,50 @@ (in-package :nyxt) -(defun %edit-with-external-editor (&optional input-text) - "Edit `input-text' using `external-editor-program'. +(defun %edit-with-external-editor (content &key (read-only nil)) + "Edit CONTENT using `external-editor-program'. Create a temporary file and return its content. The editor runs synchronously so invoke on a separate thread when possible." - (uiop:with-temporary-file (:directory (files:expand (make-instance 'nyxt-data-directory)) - :pathname p) - (when (> (length input-text) 0) - (with-open-file (f p :direction :io - :if-exists :append) - (write-sequence input-text f))) - (log:debug "External editor ~s opens ~s" - (external-editor-program *browser*) p) - (with-protect ("Failed editing: ~a" :condition) - (uiop:run-program (append (uiop:ensure-list (external-editor-program *browser*)) - (list (uiop:native-namestring p))) - :ignore-error-status t)) - (uiop:read-file-string p))) + (with-accessors ((cmd external-editor-program)) *browser* + (uiop:with-temporary-file (:directory (files:expand (make-instance 'nyxt-data-directory)) + :pathname p) + (with-open-file (f p :direction :io :if-exists :supersede) (write-sequence content f)) + (log:debug "External editor ~s opens ~s" cmd p) + (with-protect ("Failed editing: ~a. See `external-editor-program' slot." :condition) + (uiop:run-program `(,@cmd ,(uiop:native-namestring p)))) + (unless read-only (uiop:read-file-string p))))) +;; BUG: Fails when the input field loses its focus, e.g the DuckDuckGo search +;; bar. A possible solution is to keep track of the last focused element for +;; each buffer. (define-parenscript select-input-field () (let ((active-element (nyxt/ps:active-element document))) (when (nyxt/ps:element-editable-p active-element) (ps:chain active-element (select))))) -(define-parenscript move-caret-to-end () - ;; Inspired by https://stackoverflow.com/questions/4715762/javascript-move-caret-to-last-character. - (let ((el (nyxt/ps:active-element document))) - (if (string= (ps:chain (typeof (ps:@ el selection-start))) - "number") - (progn - (setf (ps:chain el selection-end) - (ps:chain el value length)) - (setf (ps:chain el selection-start) - (ps:chain el selection-end))) - (when (not (string= (ps:chain (typeof (ps:@ el create-text-range))) - "undefined")) - (ps:chain el (focus)) - (let ((range (ps:chain el (create-text-range)))) - (ps:chain range (collapse false)) - (ps:chain range (select))))))) - -;; TODO: - -;; BUG: Fails when the input field loses its focus, e.g the DuckDuckGo search -;; bar. Can probably be solved with JS. - -;; There could be an optional exiting behavior -- set-caret-on-end or -;; undo-selection. - -;; (define-parenscript undo-selection () -;; (ps:chain window (get-selection) (remove-all-ranges))) - -;; It could be extended so that the coordinates of the cursor (line,column) -;; could be shared between Nyxt and the external editor. A general solution -;; can't be achieved since not all editors, e.g. vi, accept the syntax -;; `+line:column' as an option to start the editor. - (define-command-global edit-with-external-editor () "Edit the current input field using `external-editor-program'." - (if (external-editor-program *browser*) - (run-thread "external editor" - (select-input-field) - (ffi-buffer-paste (current-buffer) (%edit-with-external-editor (ffi-buffer-copy (current-buffer)))) - (move-caret-to-end)) - (echo-warning "Please set `external-editor-program' browser slot."))) + (run-thread "external editor" + (select-input-field) + (ffi-buffer-paste (current-buffer) + (%edit-with-external-editor (ffi-buffer-copy (current-buffer)))))) +;; Should belong to user-files.lisp but the define-command-global macro is +;; defined later. (define-command-global edit-user-file-with-external-editor () "Edit the queried user file using `external-editor-program'. If the user file is GPG-encrypted, the editor must be capable of decrypting it." - (if (external-editor-program *browser*) - (let* ((file (prompt1 :prompt "Edit user file in external editor" - :sources 'user-file-source)) - (path (files:expand file))) - - (echo "Using \"~{~a~^ ~}\" to edit ~s." (external-editor-program *browser*) path) - (uiop:launch-program `(,@(uiop:ensure-list (external-editor-program *browser*)) - ,(uiop:native-namestring path)))) - (echo-warning "Please set `external-editor-program' browser slot."))) - -(defun %view-source-with-external-editor () - "View page source using `external-editor-program'. -Create a temporary file. The editor runs synchronously so invoke on a -separate thread when possible." - (let ((page-source (if (web-buffer-p (current-buffer)) - (plump:serialize (document-model (current-buffer)) nil) - (ffi-buffer-get-document (current-buffer))))) - (uiop:with-temporary-file (:directory (files:expand (make-instance 'nyxt-data-directory)) - :pathname p) - (if (> (length page-source) 0) - (progn - (alexandria:write-string-into-file page-source p :if-exists :supersede) - (log:debug "External editor ~s opens ~s" - (external-editor-program *browser*) p) - (with-protect ("Failed editing: ~a" :condition) - (uiop:run-program (append (uiop:ensure-list (external-editor-program *browser*)) - (list (uiop:native-namestring p))) - :ignore-error-status t))) - (echo-warning "Nothing to edit."))))) - -(define-command-global view-source-with-external-editor () - "Edit the current page source using `external-editor-program'. -Has no effect on the page, use only to look at sources!" - (if (external-editor-program *browser*) - (run-thread "source viewer" - (%view-source-with-external-editor)) - (echo-warning "Please set `external-editor-program' browser slot."))) + (let ((cmd (external-editor-program *browser*)) + (path (files:expand (prompt1 :prompt "Edit user file in external editor" + :sources 'user-file-source)))) + (echo "Issued \"~{~a~^ ~}\" to edit ~s." cmd path) + (with-protect ("Failed editing: ~a. See `external-editor-program' slot." :condition) + (uiop:run-program `(,@cmd ,(uiop:native-namestring path)))))) + +(define-command-global view-source-with-external-editor (&optional (buffer (current-buffer))) + "View the current page source using `external-editor-program'." + (run-thread "source viewer" + (%edit-with-external-editor (if (web-buffer-p buffer) + (plump:serialize (document-model buffer) nil) + (ffi-buffer-get-document buffer)) + :read-only t))) From 23a8a6255c52bd3de74ab897bb7dd329b19acea2 Mon Sep 17 00:00:00 2001 From: "Andre A. Gomes" Date: Fri, 20 Oct 2023 22:14:28 +0300 Subject: [PATCH 4/6] nyxt.asd: Silence CCL failing test. --- nyxt.asd | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nyxt.asd b/nyxt.asd index 3f4d3f27440..378e7f8407b 100644 --- a/nyxt.asd +++ b/nyxt.asd @@ -281,7 +281,8 @@ The renderer is configured from NYXT_RENDERER or `*nyxt-renderer*'.")) :defsystem-depends-on ("nasdf") :class :nasdf-compilation-test-system :depends-on (nyxt) - :packages (:nyxt)) + :packages (:nyxt) + :undocumented-symbols-to-ignore (:external-editor-program)) ;; TODO: Test that Nyxt starts and that --help, --version work. (defsystem "nyxt/tests" From d24f4521f34c99dd09bc6828494c884f09949cfd Mon Sep 17 00:00:00 2001 From: "Andre A. Gomes" Date: Mon, 23 Oct 2023 13:07:17 +0300 Subject: [PATCH 5/6] browser(external-editor-program): Extend default value. Add "gio open" when available and prefer uiop:getenvp over uiop:getenv. --- source/browser.lisp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/browser.lisp b/source/browser.lisp index 5199d0876af..e70f5bd5691 100644 --- a/source/browser.lisp +++ b/source/browser.lisp @@ -270,8 +270,9 @@ The handlers take the `prompt-buffer' as argument.") :documentation "Hook run while waiting for the prompt buffer to be available. The handlers take the `prompt-buffer' as argument.") (external-editor-program - (or (uiop:getenv "VISUAL") - (uiop:getenv "EDITOR")) + (or (uiop:getenvp "VISUAL") + (uiop:getenvp "EDITOR") + (when (sera:resolve-executable "gio") "gio open")) :type (or (cons string *) string null) :reader nil :writer t From 0beba436908023025c9f19bb08680b0b5421394b Mon Sep 17 00:00:00 2001 From: "Andre A. Gomes" Date: Mon, 23 Oct 2023 16:10:16 +0300 Subject: [PATCH 6/6] changelog: Mention fixes relative to external-editor-program. --- source/changelog.lisp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/source/changelog.lisp b/source/changelog.lisp index 69e0c434dd7..23edf5a2c6a 100644 --- a/source/changelog.lisp +++ b/source/changelog.lisp @@ -901,7 +901,10 @@ Nyxt version exists. It is only raised when the major version differs.") (:nxref :command 'nyxt/mode/buffer-listing:buffers-panel) "."))) (:nsection :title "Bug fixes" (:ul - (:li "Fix command " (:nxref :command 'nyxt/mode/bookmark:bookmark-url) ".")))) + (:li "Fix command " (:nxref :command 'nyxt/mode/bookmark:bookmark-url) ".") + (:li "Fix commands that rely on " + (:nxref :class-name 'browser :slot 'external-editor-program) + ".")))) (define-version "4-pre-release-1" (:li "When on pre-release, push " (:code "X-pre-release")