(require 'eieio)
(require 'm-buffer)
(require 'm-buffer-at)
(require 'dash)
(defvar lentic-doc "lenticular.org")
(defvar lentic-doc-html-files '("lenticular.css"))
(defvar lentic-init nil
"Function that initializes lentics for this buffer.
This should be one or a list of functions that each return a
`lentic-configuration' object.")
(make-variable-buffer-local 'lentic-init)
(defvar lentic-config nil
"Configuration for lentic.
This is a list of objects of the class `lentic-configuration'
lentic-configuration', which defines the way in which the text in
the different buffers is kept synchronized. This configuration is
resilient to changes of mode in the current buffer.")
(make-variable-buffer-local 'lentic-config)
(put 'lentic-config 'permanent-local t)
(defvar lentic-counter 0)
(defun lentic-config-name (buffer)
"Given BUFFER, return a name for the configuration object."
(format "lentic \"%s:%s\"" buffer (setq lentic-counter (+ 1 lentic-counter))))
(defvar lentic-init-functions nil
"All functions that can be used as `lentic-init' function.")
(defclass lentic-configuration ()
((this-buffer
:initarg :this-buffer
:documentation
"The this-buffer for this configuration. This should be the
current-buffer when this configuration is present in `lentic-config'." )
(that-buffer
:initarg :that-buffer
:documentation
"The that-buffer for this configuration. The that-buffer (if
live) should a lentic-configuration object for this-buffer in
its `lentic-config'." )
(creator
:initarg :creator :initform nil
:documentation
"Non-nil if this lentic-configuration was used to create a
lentic view. This is used to determine the behaviour when the
buffer is killed: killing the creator kills all views, but killing
a view does not kill the creator.")
(delete-on-exit
:initarg :delete-on-exit
:initform nil
:documentation
"Non-nil if the file associated with this should be deleted on exit.")
(singleton :initarg :singleton
:initform nil
:documentation
"Non-nil if only one lentic (and therefore object) of this type
can exist for a given buffer.")
(sync-point
:initarg :sync-point
:initform t
:documentation
"Non-nil if changes to the location of point in this-buffer
should be percolated into that-buffer.")
(last-change-start
:initarg :last-change-start :initform nil
:documentation
"The location of the start of the last before-change event.
This should only be set by lentic.")
(last-change-start-converted
:initarg :last-change-start-converted :initform nil
:documentation
"The location of the start of the last before-change event,
converted into the equivalent location in that-buffer. This
should only be set by lentic.")
(last-change-stop
:initarg :last-change-stop :initform nil
:documentation
"The location of the stop of the last before-change event.
This should only be set by lentic." )
(last-change-stop-converted
:initarg :last-change-stop-converted :initform nil
"The location of the stop of the last before-change event,
converted into the equivalent location in that-buffer. This
should only be set by lentic."))
"Configuration object for lentic which defines the behavior of
the lentic buffer.")
(cl-defgeneric lentic-create (conf)
"Create the lentic for this configuration.
Given a `lentic-configuration' object, create the lentic
appropriate for that configurationuration. It is the callers
responsibility to check that buffer has not already been
created.")
(cl-defgeneric lentic-convert (conf location)
"Convert LOCATION in this-buffer to an equivalent location in
that-buffer. LOCATION is a numeric location, rather than a
marker. By equivalent, we mean the same semantic location as
determined by the transformation between the buffers. It is
possible that a given LOCATION could map to more than one
location in the lentic buffer.")
(cl-defgeneric lentic-clone (conf)
"Updates that-buffer to reflect the contents in this-buffer.
Updates at least the region that has been given between start and
stop in the this-buffer, into the region start-converted and
stop-converted in that-buffer.
Returns a list of the start location in that-buffer of the
change, the stop location in that-buffer of the change and the
length-before in that buffer of the region changed before the
change, if and only if the changes are exactly that suggested by
the START, STOP, _LENGTH-BEFORE, START-CONVERTED and
STOP-CONVERTED. Otherwise, this should return nil.")
(cl-defgeneric lentic-invert (conf)
"Return a new configuration object for the lentic buffer.
This method is called at the time that the lentic is created. It
is the callers responsibility to ensure that this is only called
at creation time and not subsequently. The invert function should
only return the configuration object and NOT create the lentic
buffer.")
(cl-defgeneric lentic-coexist? (this-conf that-conf)
"Return non-nil if THIS-CONF and co-exist with THAT-CONF.
By co-exist this means that both configurations are valid for a
given buffer at the same time. A nil return indicates that there
should only be one of these two for a given buffer.")
(cl-defmethod lentic-this ((conf lentic-configuration))
"Returns this-buffer for this configuration object.
In most cases, this is likely to be the `current-buffer' but
this should not be relied on."
(oref conf this-buffer))
(cl-defmethod lentic-that ((conf lentic-configuration))
"Returns the that-buffer for this configuration object.
This may return nil if there is not that-buffer, probably because
it has not been created."
(and (slot-boundp conf 'that-buffer)
(oref conf that-buffer)))
(cl-defmethod lentic-ensure-that ((conf lentic-configuration))
"Get the lentic for this configuration
or create it if it does not exist."
(or (lentic-that conf)
(lentic-create conf)))
(cl-defmethod lentic-mode-line-string ((conf lentic-configuration))
"Returns a mode-line string for this configuration object."
(when (slot-boundp conf 'that-buffer)
(let ((that (oref conf that-buffer)))
(if
(and that
(buffer-live-p that))
"on"
""))))
(defclass lentic-default-configuration (lentic-configuration)
((lentic-file
:initform nil
:initarg :lentic-file
:documentation
"The name of the file that will be associated with that lentic buffer.")
(lentic-mode
:initform nil
:initarg :lentic-mode :documentation
"The mode for that lentic buffer."))
"Configuration which maintains two lentics with the same contents.")
(defun lentic-insertion-string-transform (string)
"Transform the STRING that is about to be inserted.
This function is not meant to do anything. It's useful to
advice."
string)
(cl-defmethod lentic-create ((conf lentic-default-configuration))
"Create an new lentic buffer. This creates the new buffer sets
the mode to the same as the main buffer or which ever is
specified in the configuration. The current contents of the main
buffer are copied."
(lentic-ensure-hooks)
(let* ((this-buffer
(lentic-this conf))
(that-buffer
(generate-new-buffer
(format "*lentic: %s*"
(buffer-name
this-buffer))))
(sec-file (oref conf lentic-file))
(sec-mode
(or
(oref conf lentic-mode)
(if sec-file
'normal-mode
major-mode))))
(oset conf creator t)
(oset conf that-buffer that-buffer)
(lentic-update-contents conf)
(with-current-buffer that-buffer
(when sec-mode
(funcall sec-mode))
(when sec-file
(set-visited-file-name sec-file))
(setq lentic-config
(list (lentic-invert conf))))
that-buffer))
(defun lentic--file-equal-p (f1 f2)
(let ((a1 (file-attributes f1))
(a2 (file-attributes f2)))
(and a1 (equal a1 a2))))
(cl-defmethod lentic-coexist? ((this-conf lentic-default-configuration)
that-conf)
"By default, we can have multiple lentic buffers with the same
configuration, unless specifically disallowed, or unless it has
the same associated file as pre-existing buffer (which is going
to break!)."
(and
(not (oref this-conf singleton))
(not
(and (oref this-conf lentic-file)
(oref that-conf lentic-file)
(lentic--file-equal-p
(oref this-conf lentic-file)
(oref that-conf lentic-file))))))
(cl-defmethod lentic-invert ((conf lentic-default-configuration))
"By default, return a clone of the existing object, but switch
the this and that buffers around. "
(clone
conf
:this-buffer (lentic-that conf)
:that-buffer (lentic-this conf)
:sync-point (oref conf sync-point)))
(cl-defmethod lentic-convert ((_conf lentic-default-configuration)
location)
"The two buffers should be identical, so we just return the
same location."
location)
(cl-defmethod lentic-clone ((conf lentic-configuration)
&optional start stop _length-before
start-converted stop-converted)
"The default clone method cuts out the before region and pastes
in the new."
(let ((this-b (lentic-this conf))
(that-b (lentic-that conf)))
(with-current-buffer this-b
(save-window-excursion
(save-restriction
(widen)
(let* ((start (or start (point-min)))
(stop (or stop (point-max))))
(with-current-buffer that-b
(save-restriction
(widen)
(let ((converted-start
(max (point-min)
(or start-converted
(point-min))))
(converted-stop
(min (point-max)
(or stop-converted
(point-max)))))
(delete-region converted-start
converted-stop)
(save-excursion
(goto-char converted-start)
(insert
(with-current-buffer this-b
(lentic-insertion-string-transform
(buffer-substring-no-properties
start stop))))
(list converted-start
(+ converted-start (- stop start))
(- converted-stop converted-start))))))))))))
(defun lentic-default-init ()
"Default init function.
see `lentic-init' for details."
(lentic-default-configuration
:this-buffer (current-buffer)))
(add-to-list 'lentic-init-functions #'lentic-default-init)
(defmacro lentic-when-lentic (&rest body)
"Evaluate BODY when the `current-buffer' has a lentic buffer."
(declare (debug t))
`(when (and
lentic-config
(-any?
(lambda (conf)
(-when-let
(buf (lentic-that conf))
(buffer-live-p buf)))
lentic-config))
,@body))
(defmacro lentic-when-buffer (buffer &rest body)
"When BUFFER is a live buffer eval BODY."
(declare (debug t)
(indent 1))
`(when (buffer-live-p ,buffer)
,@body))
(defmacro lentic-when-with-current-buffer (buffer &rest body)
"When BUFFER is a live buffer eval BODY with BUFFER current."
(declare (debug t)
(indent 1))
`(lentic-when-buffer ,buffer
(with-current-buffer ,buffer
,@body)))
(defmacro lentic-with-lentic-buffer (buffer &rest body)
"With BUFFER as current, eval BODY when BUFFER has a lentic."
(declare (debug t)
(indent 1))
`(lentic-when-with-current-buffer ,buffer
(when lentic-config
,@body)))
(defvar lentic-condition-case-disabled
noninteractive
"If non-nil throw exceptions from errors.
By default this is set to the value of noninteractive, so that
Emacs crashes with backtraces in batch." )
(defmacro lentic-condition-case-unless-disabled (var bodyform &rest handlers)
"Like `condition-case' but can be disabled like `condition-case-unless-debug'."
(declare (debug condition-case) (indent 2))
`(if lentic-condition-case-disabled
,bodyform
(condition-case-unless-debug ,var
,bodyform
,@handlers)))
(defmacro lentic-widen (conf &rest body)
"Widen both buffers in CONF, then evaluate BODY."
(declare (debug t)
(indent 1))
`(with-current-buffer
(lentic-that ,conf)
(save-restriction
(widen)
(with-current-buffer
(lentic-this ,conf)
(save-restriction
(widen)
,@body)))))
(defun lentic-each (buffer fn &optional seen-buffer)
"Starting at BUFFER, call FN on every lentic-buffer.
FN should take a single argument which is the buffer.
SEEN-BUFFER is a list of buffers to ignore."
(lentic-with-lentic-buffer buffer
(setq seen-buffer (cons buffer seen-buffer))
(-map
(lambda (conf)
(let ((that
(lentic-that conf)))
(when (and (not (-contains? seen-buffer that))
(buffer-live-p that))
(funcall fn that)
(lentic-each that fn seen-buffer))))
lentic-config)))
(defun lentic-garbage-collect-config ()
"Remove non-live configs in current-buffer."
(setq lentic-config
(--filter
(buffer-live-p
(lentic-that it))
lentic-config)))
(defun lentic-ensure-init ()
"Ensure that the `lentic-init' has been run."
(lentic-garbage-collect-config)
(setq lentic-config
(-concat
lentic-config
(-filter
(lambda (this-conf)
(-all?
(lambda (that-conf)
(lentic-coexist? this-conf that-conf))
lentic-config))
(-map
(lambda (init)
(funcall init))
(if (not lentic-init)
'(lentic-default-init)
(-list lentic-init)))))))
(defun lentic-init-all-create ()
"Create all lentics fo the current buffer."
(lentic-ensure-init)
(-map
(lambda (conf)
(if (and
(slot-boundp conf 'that-buffer)
(buffer-live-p
(lentic-that conf)))
(lentic-that conf)
(lentic-create conf)))
(-list lentic-config)))
(defun lentic-ensure-hooks ()
"Ensures that the hooks that this mode requires are in place."
(add-hook 'post-command-hook
#'lentic-post-command-hook)
(add-hook 'after-change-functions
#'lentic-after-change-function)
(add-hook 'before-change-functions
#'lentic-before-change-function)
(add-hook 'after-save-hook
#'lentic-after-save-hook)
(add-hook 'kill-buffer-hook
#'lentic-kill-buffer-hook)
(add-hook 'kill-emacs-hook
#'lentic-kill-emacs-hook))
(defvar lentic-log nil)
(defmacro lentic-log (&rest rest)
"Log REST."
`(when lentic-log
(lentic-when-lentic
(let ((msg
(concat
(format ,@rest)
"\n")))
(princ msg #'external-debugging-output)))))
(defvar lentic-emergency nil
"Iff non-nil halt all lentic activity.
This is not the same as disabling lentic mode. It stops all
lentic related activity in all buffers; this happens as a result
of an error condition. If lentic was to carry on in these
circumstances, serious data loss could occur. In normal use, this
variable will only be set as a result of a problem with the code;
it is not recoverable from a user perspective.
It is useful to toggle this state on during development. Once
enabled, buffers will not update automaticaly but only when
explicitly told to. This is much easier than try to debug errors
happening on the after-change-hooks. The
function `lentic-emergency' and `lentic-unemergency' functions
enable this.")
(defvar lentic-emergency-debug nil
"Iff non-nil, lentic will store change data, even
during a `lentic-emergency'.
Normally, `lentic-emergency' disables all activity, but this makes
testing incremental changes charge. With this variable set, lentic will
attempt to store enough change data to operate manually. This does require
running some lentic code (notably `lentic-convert'). This is low
risk code, but may still be buggy, and so setting this variable can cause
repeated errors.")
(defun lentic-emergency ()
"Stop lentic from working due to code problem."
(interactive)
(setq lentic-emergency t)
(lentic-update-all-display))
(defun lentic-unemergency ()
"Start lentic working after stop due to code problem."
(interactive)
(setq lentic-emergency nil)
(lentic-update-all-display))
(defun lentic-hook-fail (err hook)
"Give an informative message when we have to fail.
ERR is the error. HOOK is the hook type."
(message "lentic mode has failed on \"%s\" hook: %s "
hook (error-message-string err))
(lentic-emergency)
(with-output-to-temp-buffer "*lentic-fail*"
(princ "There has been an error in lentic-mode.\n")
(princ "The following is debugging information\n\n")
(princ (format "Hook: %s\n" hook))
(princ (error-message-string err)))
(select-window (get-buffer-window "*lentic-fail*")))
(defun lentic-after-save-hook ()
"Error protected call to real after save hook."
(unless lentic-emergency
(lentic-condition-case-unless-disabled err
(lentic-after-save-hook-1)
(error
(lentic-hook-fail err "after-save-hook")))))
(defun lentic-after-save-hook-1 ()
"Respond to a save in the `current-buffer'.
This also saves every lentic which is file-associated."
(lentic-each
(current-buffer)
(lambda (buffer)
(with-current-buffer
buffer
(when (buffer-file-name)
(save-buffer))))))
(defvar lentic-kill-retain nil
"If non-nil retain files even if requested to delete on exit.")
(defun lentic-kill-buffer-hook ()
"Error protected call to real `kill-buffer-hook'."
(unless lentic-emergency
(lentic-condition-case-unless-disabled err
(lentic-kill-buffer-hook-1)
(error
(lentic-hook-fail err "kill-buffer-hook")))))
(defvar lentic--killing-p nil)
(defun lentic-kill-buffer-hook-1 ()
"Respond to any buffer being killed.
If this killed buffer is lentic and is `creator', then kill all
lentic-buffers recursively. If the buffer is `delete-on-exit',
then remove any associated file."
(lentic-when-lentic
(when
(and
(--any?
(oref it delete-on-exit)
lentic-config)
(file-exists-p buffer-file-name)
(not noninteractive)
(not lentic-kill-retain))
(delete-file buffer-file-name))
(defvar lentic-killing-p)
(let ((lentic-killing-p t))
(when
(and
(not lentic-killing-p)
(--any?
(oref it creator)
lentic-config))
(lentic-each
(current-buffer)
#'kill-buffer)))))
(defun lentic-kill-emacs-hook ()
"Error protected call to real `kill-emacs-hook'."
(unless lentic-emergency
(lentic-condition-case-unless-disabled err
(lentic-kill-emacs-hook-1)
(error
(lentic-hook-fail err "kill-emacs-hook")))))
(defun lentic-kill-emacs-hook-1 ()
"Respond to `kill-emacs-hook.
This removes any files associated with lentics which are
marked as :delete-on-exit."
(-map
(lambda (buffer)
(lentic-with-lentic-buffer
buffer
(-map
(lambda (conf)
(and
(oref conf delete-on-exit)
(file-exists-p buffer-file-name)
(not noninteractive)
(delete-file (buffer-file-name))))
lentic-config)))
(buffer-list)))
(defun lentic-post-command-hook ()
"Update point according to config, with error handling."
(unless lentic-emergency
(lentic-condition-case-unless-disabled err
(progn
(when lentic-config
(lentic-post-command-hook-1 (current-buffer))))
(error
(lentic-hook-fail err "post-command-hook")))))
(defun lentic-post-command-hook-1 (buffer &optional seen-buffer)
"Update point in BUFFER according to config.
SEEN-BUFFER is a list of lentics that have already been updated."
(lentic-with-lentic-buffer
buffer
(setq seen-buffer (cons buffer seen-buffer))
(-map
(lambda (config)
(let ((that
(lentic-that config)))
(unless (-contains? seen-buffer that)
(lentic-when-buffer
that
(lentic-update-point config))
(lentic-post-command-hook-1 (lentic-that config) seen-buffer))))
lentic-config)))
(defvar lentic-emergency-last-change nil)
(make-variable-buffer-local 'lentic-emergency-last-change)
(defun lentic-after-change-transform (_buffer _start _stop _length-before)
"Function called after every change percolated by lentic.
This function does nothing and is meant for advising. See
lentic-dev."
)
(defun lentic-after-change-function (start stop length-before)
"Run change update according to `lentic-config'.
Errors are handled.
START is at most the start of the change.
STOP is at least the end of the change.
LENGTH-BEFORE is the length of the area before the change."
(when lentic-emergency-debug
(setq lentic-emergency-last-change (list start stop length-before)))
(unless lentic-emergency
(lentic-condition-case-unless-disabled err
(lentic-after-change-function-1
(current-buffer) start stop length-before)
(error
(lentic-hook-fail err "after change")))))
(defun lentic-after-change-function-1
(buffer start stop
length-before &optional seen-buffer)
"Run change update according to `lentic-config'.
BUFFER is the changed buffer.
START is at most the start of the change.
STOP is at least the end of the change.
LENGTH-BEFORE is the length of the area before the change.
SEEN-BUFFER is a list of buffers to which we have already percolated
the change."
(lentic-with-lentic-buffer buffer
(setq seen-buffer (cons buffer seen-buffer))
(-map
(lambda (config)
(unless
(or (-contains? seen-buffer (lentic-that config))
(not (buffer-live-p (lentic-that config))))
(let ((updates
(or
(lentic-update-contents config
start stop length-before)
'(nil nil nil))))
(apply #'lentic-after-change-transform
(lentic-that config)
updates)
(lentic-after-change-function-1
(lentic-that config)
(nth 0 updates)
(nth 1 updates)
(nth 2 updates)
seen-buffer))))
lentic-config)))
(defun lentic-before-change-function (start stop)
"Error protected call to real `before-change-function'.
START is at most the start of the change.
STOP is at least the end of the change."
(unless (and
lentic-emergency
(not lentic-emergency-debug))
(lentic-condition-case-unless-disabled err
(lentic-before-change-function-1 (current-buffer) start stop)
(error
(lentic-hook-fail err "before change")))))
(defun lentic-before-change-function-1 (buffer start stop &optional seen-buffer)
"Calculate change position in all lentic buffers.
BUFFER is the buffer being changed.
START is at most the start of the change.
STOP is at least the end of the change.
SEEN-BUFFER is a list of buffers to which the change has been percolated."
(lentic-with-lentic-buffer buffer
(setq seen-buffer (cons buffer seen-buffer))
(-map
(lambda (config)
(unless
(or (-contains? seen-buffer (lentic-that config))
(not (buffer-live-p (lentic-that config))))
(lentic-widen
config
(oset config last-change-start start)
(oset config
last-change-start-converted
(lentic-convert
config
start))
(oset config last-change-stop stop)
(oset config
last-change-stop-converted
(lentic-convert
config
stop))
(lentic-before-change-function-1
(lentic-that config)
(oref config last-change-start-converted)
(oref config last-change-stop-converted)
seen-buffer))))
lentic-config)))
(defun lentic-update-contents (conf &optional start stop length-before)
"Update the contents of that-buffer with the contents of this-buffer.
update mechanism depends on CONF.
START is at most the start of the change.
STOP is at least the end of the change.
LENGTH-BEFORE is the length of area before the change."
(let ((inhibit-read-only t)
(no-fall-back
(and start stop length-before)))
(when
(and no-fall-back
(< (+ start length-before) (oref conf last-change-stop)))
(let ((diff
(- (oref conf last-change-stop)
(+ start length-before))))
(lentic-log "Skew detected %s" this-command)
(cl-incf length-before diff)
(cl-incf stop diff)))
(m-buffer-with-markers
((start-converted
(when
(and no-fall-back
(oref conf last-change-start-converted))
(set-marker (make-marker)
(oref conf last-change-start-converted)
(lentic-that conf))))
(stop-converted
(when
(and no-fall-back
(oref conf last-change-stop-converted))
(set-marker (make-marker)
(oref conf last-change-stop-converted)
(lentic-that conf)))))
(oset conf last-change-start nil)
(oset conf last-change-start-converted nil)
(oset conf last-change-stop nil)
(oset conf last-change-stop-converted nil)
(lentic-widen
conf
(if (not no-fall-back)
(lentic-clone conf)
(lentic-clone conf start stop length-before
start-converted stop-converted))))))
(defun lentic-update-point (conf)
"Update the location of point in that-buffer to reflect this-buffer.
This also attempts to update any windows so that they show the
same top-left location. Update details depend on CONF."
(when (oref conf sync-point)
(let* ((from-point
(lentic-convert
conf
(m-buffer-at-point
(lentic-this conf))))
(from-window-start
(lentic-convert
conf
(window-start
(get-buffer-window
(lentic-this conf))))))
(with-current-buffer
(lentic-that conf)
(goto-char from-point))
(mapc
(lambda (window)
(with-selected-window window
(progn
(goto-char from-point)
(set-window-start window from-window-start))))
(get-buffer-window-list (lentic-that conf))))))
(defun lentic-update-display ()
"Update the display with information about lentic's state."
(when (fboundp 'lentic-mode-update-mode-line)
(lentic-mode-update-mode-line)))
(defun lentic-update-all-display ()
(when (fboundp 'lentic-mode-update-all-display)
(lentic-mode-update-all-display)))
(defun lentic-m-oset (obj &rest plist)
"On OBJ set all properties in PLIST.
Returns OBJ. See also `lentic-a-oset'"
(lentic-a-oset obj plist))
(defun lentic-a-oset (obj plist)
"On OBJ, set all properties in PLIST.
This is a utility function which just does the same as oset, but
for lots of things at once. Returns OBJ."
(dolist (n (-partition 2 plist))
(eieio-oset obj (car n) (cadr n)))
obj)
(defun lentic-batch-clone-and-save-with-config (filename init)
"Open FILENAME, set INIT function, then clone and save.
This function does potentially evil things if the file or the
lentic is open already."
(let ((retn))
(with-current-buffer
(find-file-noselect filename)
(setq lentic-init init)
(with-current-buffer
(car
(lentic-init-all-create))
(setq retn lentic-config)
(save-buffer)
(kill-buffer))
(kill-buffer))
retn))
(defun lentic-batch-clone-with-config
(filename init)
"Open FILENAME, set INIT function, then clone.
Return the lentic contents without properties."
(let ((retn nil))
(with-current-buffer
(find-file-noselect filename)
(setq lentic-init init)
(with-current-buffer
(car
(lentic-init-all-create))
(setq retn
(buffer-substring-no-properties
(point-min)
(point-max)))
(set-buffer-modified-p nil)
(oset (car lentic-config) delete-on-exit nil)
(kill-buffer))
(set-buffer-modified-p nil)
(kill-buffer))
retn))
(provide 'lentic)