FG42/core/fg42/cube.el

234 lines
9.1 KiB
EmacsLisp
Raw Normal View History

;;; Cube --- Cube library of FG42 -*- lexical-binding: t; -*-
;;
;; Copyright (c) 2010-2022 Sameer Rahmani & Contributors
;;
;; Author: Sameer Rahmani <lxsameer@gnu.org>
;; URL: https://devheroes.codes/FG42/FG42
;; Version: 3.0.0
;;
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;
;;; Commentary:
;; Cubes are the building blocks of any `FG42' editor. Each `cube' is a
;; unit which defines different abilities in a deterministic and idempotent
;; way. Cubes are composable and a composition of cubes creates an editor.
;;
;;; Code:
2020-10-24 21:44:12 +01:00
(require 'seq)
(require 'fg42/utils)
(require 'fg42/themes)
2021-12-19 15:18:21 +00:00
(defvar fg42/after-cubes-setup-hook nil
2022-09-12 15:24:31 +01:00
"A hook that will be run after all the active cubes got setup.
This hook is dedicated for the codes that need to do stuff based on other cubes
2022-09-12 15:24:31 +01:00
presence. With this hook we eliminate the need for cube ordering.
2021-12-19 15:18:21 +00:00
It will be called in the `fg42-config' and the proper way to use
2022-09-12 15:24:31 +01:00
it is to use `fg42/after-cubes' macro.")
2021-12-19 15:18:21 +00:00
(defvar fg42/available-cubes '()
"A list of all the registered cubes.")
(defvar fg42/-cubes-body-hook nil
"A Hook that is internal to FG42 and will be used to run the cub bodies.
The execution happens after Emacs is initialized.")
(defun fg42/extract-props (body-list &optional acc)
"Extract the props pairs from BODY-LIST with an optional accumulator ACC.
It will returen a pair in form of (body . props)."
(let ((k (car body-list))
(rest (cdr body-list)))
(if (and k (keywordp k))
(fg42/extract-props
(cdr rest)
(cons (cdr rest) (plist-put (cdr acc) k (car rest))))
(cons body-list (cdr acc)))))
(defun fg42/run-cube-after-initialization (f)
"Run the given Cube body function F after FG42's initialization.
If FG42 is already initialized, just run F."
(if (null fg42/initialized)
(add-hook 'fg42/-cubes-body-hook f)
(funcall f)))
(defmacro defcube (cube-name docs &rest props-n-body)
"Define a cube with the given CUBE-NAME, DOCS and a PROPS-N-BODY.
TODO: Docs"
(declare (indent defun) (doc-string 2))
;; Make sure that props is a plist and contains the `:docs' key
;; TODO: Maybe use `cl-check-type' here
(when (not (stringp docs))
(error "Missing docstring for '%s' cube" cube-name))
(let* ((parsed-body (fg42/extract-props props-n-body))
(body (car parsed-body))
(props (cdr parsed-body)))
(when (not (plist-get props :title))
(error "Missing :titel key for '%s' cube" cube-name))
(let ((complete-props (plist-put props :docs docs))
(cube-name-internal (intern (format "%s-internal" cube-name)))
;; prop hooks
(init-hook (plist-get props :init-hook))
(ui-hook (plist-get props :ui-hook))
(params-var (intern (format "%s-params" cube-name)))
(active-var (intern (format "%s-active-p" cube-name)))
(pre-lang-server-up-hook (intern (format "%s-pre-lang-server-up-hook" cube-name)))
(post-lang-server-up-hook (intern (format "%s-post-lang-server-up-hook" cube-name)))
(pre-lang-server-down-hook (intern (format "%s-pre-lang-server-down-hook" cube-name)))
(post-lang-server-down-hook (intern (format "%s-post-lang-server-down-hook" cube-name)))
(pre-init-hook (intern (format "%s-pre-init-hook" cube-name)))
(post-init-hook (intern (format "%s-post-init-hook" cube-name)))
(post-init-hook (intern (format "%s-post-init-hook" cube-name)))
(flag-var (or (plist-get props :flag) cube-name))
(flag-docstring-var (or (plist-get props :flag-doc)
(format "The flag to enable/disable the '%s' cube." cube-name)))
(flag-default (plist-get props :flag-default))
(flag-conflict (plist-get props :conflicts-with))
(no-flag? (or (plist-get props :no-flag) ())))
(add-to-list 'fg42/available-cubes cube-name)
`(progn
;; Create a new flag for each cube to control the cubes systemwide.
(when (not ,no-flag?)
(defflag ,flag-var ,flag-docstring-var ,flag-default))
;; Params variable contains the list of params the is passed to
;; the current cube call
(defvar ,params-var nil
,(format "Parameters for the '%s' cube." cube-name))
;; * Hooks
;; This hook can be used by others to run code just before running that
;; cube's body
(defvar ,pre-init-hook nil
,(format "The hook that runs befor the '%s' cube initialization." cube-name))
;; This hook can be used by others to run code just after the body of
;; the cube
(defvar ,post-init-hook nil
,(format "The hook that runs after the '%s' cube initialization." cube-name))
;; TODO: Move language server related hooks to lang-server
;; TODO: Provide a way to let different parts of the
;; codebase to create cube hooks
;; ** Language Server
;;; The hook that enables users to change the language server configuration
;;; of the current cube before activating the server
(defvar ,pre-lang-server-up-hook nil
,(format "The hook that runs befor the '%s' cube's language server activates ." cube-name))
;;; The hook to do any post configuration for the lang server of the cube
(defvar ,post-lang-server-up-hook nil
,(format "The hook that runs after the '%s' cube's language server activates." cube-name))
;;; The hook to run code just before the language server is about to shutdown
(defvar ,pre-lang-server-down-hook nil
,(format "The hook that runs befor the '%s' cube's language server shuts down." cube-name))
;;; The hook to run code after the language server successfully shuts down
(defvar ,post-lang-server-down-hook nil
,(format "The hook that runs after the '%s' cube's language server shuts down." cube-name))
;; This way we can bypass the flag system if we really really want to.
(defun ,cube-name-internal (params)
(if (or (not (boundp (quote ,active-var)))
(not ,active-var))
(progn
;; Mark this cube as active
(setq ,active-var t)
;; Set the parameters in CUBE-NAME-params to be accessable
;; in the body
(setq ,params-var params)
;; Run the pre init hook
(run-hooks (quote ,pre-init-hook))
(fg42/info "Initializing '%s' cube." (quote ,cube-name))
;; Run the body
(let ((result (progn ,@body)))
;; Run the post init hook
(run-hooks (quote ,post-init-hook))
result))
(fg42/info "The '%s' cube is already active." ',cube-name)))
(defun ,cube-name (&rest params)
(interactive)
(let ((fg42/---f-sym
(lambda ()
(when (not (null ,ui-hook))
(add-hook 'fg42/ui-hook #',ui-hook))
(when (not (null ,init-hook))
(funcall #',init-hook params))
;; Run the cube internal after initialization or
;; if Emacs is already up, just run it.
(fg42/run-cube-after-initialization
(lambda ()
(,cube-name-internal params))))))
(if ,no-flag?
;; If no flag is need to control this cube
(funcall fg42/---f-sym)
;; Otherwise check for the flag to be active
(if-flag ,flag-var
(funcall fg42/---f-sym)
(fg42/info "The flag for '%s' cube is disabled. Skiping." ,(symbol-name cube-name))))))
;; Set the symbol-plist of the cube-name to its props
(setplist ',cube-name ',complete-props)))))
2021-12-19 15:18:21 +00:00
(defmacro fg42/after-cubes (&rest body)
"Add the BODY to `fg42/after-cubes-setup-hook' hook."
(declare (indent defun))
`(add-hook 'fg42/after-cubes-setup-hook
(lambda ()
,@body)))
(defmacro ->cube (pkg docs &rest body)
"A wrapper to create a cube that use only a PKG.
It passes the BODY to `fpkg/use'.
And passes DOCS to `defcube' as the cube documentation."
(declare (indent defun) (doc-string 2))
`(defcube ,(intern (format "fg42/%s-cube" pkg))
,docs
:title ,(format "%s cube" pkg)
:flag ,pkg
:flag-default t
(fpkg/use ,pkg ,@body)))
(provide 'fg42/cube)
;;; cube.el ends here