branch: externals/crdt commit a157310f585ed39823c7df47975ecc89146e2d43 Author: Qiantan Hong <qh...@mit.edu> Commit: Qiantan Hong <qh...@mit.edu>
better formatting --- crdt.el | 251 +++++++++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 193 insertions(+), 58 deletions(-) diff --git a/crdt.el b/crdt.el index ba37a4b..0aceb5e 100644 --- a/crdt.el +++ b/crdt.el @@ -20,6 +20,10 @@ ;; along with crdt.el. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: +;; This package provides a collaborative editing environment for Emacs. + +;;; Code: + ;; * Algorithm ;; This packages implements the Logoot split algorithm ;; André, Luc, et al. @@ -86,29 +90,33 @@ ;; - overlay-remove ;; body takes the form (buffer-name site-id logical-clock) - -;;; Code: - - ;;; Customs + (defgroup crdt nil "Collaborative editing using Conflict-free Replicated Data Types." :prefix "crdt-" :group 'applications) + (defcustom crdt-ask-for-name t "Ask for display name everytime a CRDT session is to be started." :type 'boolean) + (defcustom crdt-default-name "anonymous" "Default display name." :type 'string) + (defcustom crdt-ask-for-password t "Ask for server password everytime a CRDT server is to be started." :type 'boolean) (require 'cl-lib) +(require 'subr-x) + + ;;; Pseudo cursor/region utils (require 'color) + (defvar crdt-cursor-region-colors (let ((n 10)) (cl-loop for i below n @@ -122,9 +130,11 @@ (defun crdt--get-cursor-color (site-id) "Get cursor color for SITE-ID." (car (nth (mod site-id (length crdt-cursor-region-colors)) crdt-cursor-region-colors))) + (defun crdt--get-region-color (site-id) "Get region color for SITE-ID." (cdr (nth (mod site-id (length crdt-cursor-region-colors)) crdt-cursor-region-colors))) + (defun crdt--move-cursor (ov pos) "Move pseudo cursor overlay OV to POS." ;; Hax! @@ -141,10 +151,12 @@ (propertize " " 'face (overlay-get ov 'face)))))) (move-overlay ov pos end) (overlay-put ov 'before-string display-string))) + (defun crdt--move-region (ov pos mark) "Move pseudo marked region overlay OV to mark between POS and MARK." (move-overlay ov (min pos mark) (max pos mark))) + ;;; CRDT ID utils ;; CRDT IDs are represented by unibyte strings (for efficient comparison) ;; Every two bytes represent a big endian encoded integer @@ -155,11 +167,13 @@ ;; (defconst crdt--max-value 16) ;; for debug (defconst crdt--low-byte-mask 255) + (defsubst crdt--get-two-bytes (string index) "Get the big-endian encoded integer from STRING starting from INDEX. INDEX is counted by bytes." (logior (lsh (elt string index) 8) (elt string (1+ index)))) + (defsubst crdt--get-two-bytes-with-offset (string offset index default) "Helper function for CRDT--GENERATE-ID. Get the big-endian encoded integer from STRING starting from INDEX, @@ -177,19 +191,23 @@ and padded infintely by DEFAULT to the right." Note that it might deviate from real offset for a character in the middle of a block." (crdt--get-two-bytes id (- (string-bytes id) 2))) + (defsubst crdt--set-id-offset (id offset) "Set the OFFSET portion of ID destructively." (let ((length (string-bytes id))) (aset id (- length 2) (lsh offset -8)) (aset id (- length 1) (logand offset crdt--low-byte-mask)))) + (defsubst crdt--id-replace-offset (id offset) "Create and return a new id string by replacing the OFFSET portion from ID." (let ((new-id (substring id))) (crdt--set-id-offset new-id offset) new-id)) + (defsubst crdt--id-site (id) "Get the site id from ID." (crdt--get-two-bytes id (- (string-bytes id) 4))) + (defsubst crdt--generate-id (low-id low-offset high-id high-offset site-id) "Generate a new ID between LOW-ID and HIGH-ID. The generating site is marked as SITE-ID. @@ -216,23 +234,28 @@ and HIGH-OFFSET. (to save two copying from using CRDT--ID-REPLACE-OFFSET)" (defsubst crdt--get-crdt-id-pair (pos &optional obj) "Get the (CRDT-ID . END-OF-BLOCK-P) pair at POS in OBJ." (get-text-property pos 'crdt-id obj)) + (defsubst crdt--get-starting-id (pos &optional obj) "Get the CRDT-ID at POS in OBJ." (car (crdt--get-crdt-id-pair pos obj))) + (defsubst crdt--end-of-block-p (pos &optional obj) "Get the END-OF-BLOCK-P at POS in OBJ." (cdr (crdt--get-crdt-id-pair pos obj))) + (defsubst crdt--get-starting-id-maybe (pos &optional obj limit) "Get the CRDT-ID at POS in OBJ if POS is no smaller than LIMIT. Return NIL otherwise." (unless (< pos (or limit (point-min))) (car (get-text-property pos 'crdt-id obj)))) + (defsubst crdt--get-id-offset (starting-id pos &optional obj limit) "Get the real offset integer for a character at POS. Assume the stored literal ID is STARTING-ID." (let* ((start-pos (previous-single-property-change (1+ pos) 'crdt-id obj (or limit (point-min))))) (+ (- pos start-pos) (crdt--id-offset starting-id)))) + ;;; CRDT ID and text property utils (defsubst crdt--get-id (pos &optional obj left-limit right-limit) "Get the real CRDT ID at POS." @@ -270,6 +293,7 @@ with ID and END-OF-BLOCK-P." (beg-limit ,beg-limit) (end-limit ,end-limit)) ,@body)) + (defmacro crdt--split-maybe () '(when (and not-end (eq starting-id (crdt--get-starting-id end end-obj))) ;; need to split id block @@ -285,16 +309,20 @@ with ID and END-OF-BLOCK-P." (let ((base-length (- a-length 2))) (eq t (compare-strings a 0 base-length b 0 base-length)))))) + ;;; Buffer local variables (defmacro crdt--defvar-permanent-local (name &optional val docstring) `(progn (defvar-local ,name ,val ,docstring) (put ',name 'permanent-local t))) + (crdt--defvar-permanent-local crdt--status-buffer) + (defsubst crdt--assimilate-status-buffer (buffer) (let ((status-buffer crdt--status-buffer)) (with-current-buffer buffer (setq crdt--status-buffer status-buffer)))) + (defmacro crdt--defvar-session (name &optional val docstring) (let ((setter-name (intern (format "%s-setter" name)))) `(progn @@ -308,55 +336,116 @@ with ID and END-OF-BLOCK-P." (gv-define-simple-setter ,name ,setter-name)))) (crdt--defvar-session crdt--local-id nil "Local site-id.") + (crdt--defvar-session crdt--local-clock 0 "Local logical clock.") + (defvar crdt--inhibit-update nil "When set, don't call CRDT--LOCAL-* on change. This is useful for functions that apply remote change to local buffer, to avoid recusive calling of CRDT synchronization functions.") + (crdt--defvar-permanent-local crdt--changed-string nil) + (crdt--defvar-permanent-local crdt--last-point nil) + (crdt--defvar-permanent-local crdt--last-mark nil) + (crdt--defvar-permanent-local crdt--pseudo-cursor-table nil "A hash table that maps SITE-ID to CONSes of the form (CURSOR-OVERLAY . REGION-OVERLAY).") + (cl-defstruct (crdt--contact-metadata (:constructor crdt--make-contact-metadata (display-name focused-buffer-name host service))) display-name host service focused-buffer-name) + (crdt--defvar-session crdt--contact-table nil "A hash table that maps SITE-ID to CRDT--CONTACT-METADATAs.") + (cl-defstruct (crdt--overlay-metadata (:constructor crdt--make-overlay-metadata (lamport-timestamp species front-advance rear-advance plist)) (:copier crdt--copy-overlay-metadata)) "" lamport-timestamp species front-advance rear-advance plist) + (crdt--defvar-permanent-local crdt--overlay-table nil "A hash table that maps CONSes of the form (SITE-ID . LOGICAL-CLOCK) to overlays.") + (defvar crdt--track-overlay-species nil) + (crdt--defvar-permanent-local crdt--enabled-overlay-species nil) + (crdt--defvar-permanent-local crdt--buffer-network-name) (crdt--defvar-session crdt--local-name nil) + (crdt--defvar-session crdt--focused-buffer-name nil) + (crdt--defvar-session crdt--user-menu-buffer nil) + (crdt--defvar-session crdt--buffer-menu-buffer nil) + (defvar crdt--session-alist nil) + (defvar crdt--session-menu-buffer nil) +(crdt--defvar-session crdt--network-process nil) + +(crdt--defvar-session crdt--network-clients nil) + +(crdt--defvar-session crdt--next-client-id) + +(crdt--defvar-session crdt--buffer-table) + + +;;; crdt-mode +(defun crdt--install-hooks () + (add-hook 'after-change-functions #'crdt--after-change nil t) + (add-hook 'before-change-functions #'crdt--before-change nil t) + (add-hook 'post-command-hook #'crdt--post-command nil t)) + +(defun crdt--uninstall-hooks () + (remove-hook 'after-change-functions #'crdt--after-change t) + (remove-hook 'before-change-functions #'crdt--before-change t) + (remove-hook 'post-command-hook #'crdt--post-command t)) + +(defsubst crdt--clear-pseudo-cursor-table () + (when crdt--pseudo-cursor-table + (maphash (lambda (key pair) + (delete-overlay (car pair)) + (delete-overlay (cdr pair))) + crdt--pseudo-cursor-table) + (setq crdt--pseudo-cursor-table nil))) + +(define-minor-mode crdt-mode + "CRDT mode" nil " CRDT" nil + (if crdt-mode + (progn + (setq crdt--pseudo-cursor-table (make-hash-table)) + (setq crdt--overlay-table (make-hash-table :test 'equal)) + (crdt--install-hooks)) + (crdt--uninstall-hooks) + (crdt--clear-pseudo-cursor-table) + (setq crdt--overlay-table nil))) + + ;;; Session menu (defun crdt--session-menu-goto () (interactive) (with-current-buffer (process-get (tabulated-list-get-id) 'status-buffer) (crdt-list-buffer))) + (defun crdt--session-menu-kill () (interactive) (with-current-buffer (process-get (tabulated-list-get-id) 'status-buffer) (crdt-stop-session))) + (defvar crdt-session-menu-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "RET") #'crdt--session-menu-goto) (define-key map (kbd "k") #'crdt--session-menu-kill) map)) + (define-derived-mode crdt-session-menu-mode tabulated-list-mode "CRDT User List" (setq tabulated-list-format [("Session Name" 15 t) @@ -364,10 +453,11 @@ to avoid recusive calling of CRDT synchronization functions.") ("My Display Name" 15 t) ("Buffers" 15 t) ("Users" 15 t)])) + (defun crdt-list-sessions (&optional crdt-buffer display-buffer) "Display a list of active CRDT sessions. -If DISPLAY-BUFFER is provided, display the output there. Otherwise use a dedicated -buffer for displaying active users on CRDT-BUFFER." +If DISPLAY-BUFFER is provided, display the output there. +Otherwise use a dedicated buffer for displaying active users on CRDT-BUFFER." (interactive) (unless display-buffer (unless (and crdt--session-menu-buffer (buffer-live-p crdt--session-menu-buffer)) @@ -376,6 +466,7 @@ buffer for displaying active users on CRDT-BUFFER." (setq display-buffer crdt--session-menu-buffer)) (crdt-refresh-sessions display-buffer) (switch-to-buffer-other-window display-buffer)) + (defun crdt-refresh-sessions (display-buffer) (with-current-buffer display-buffer (crdt-session-menu-mode) @@ -400,35 +491,41 @@ buffer for displaying active users on CRDT-BUFFER." crdt--session-alist) (tabulated-list-init-header) (tabulated-list-print))) + (defsubst crdt--refresh-sessions-maybe () (when (and crdt--session-menu-buffer (buffer-live-p crdt--session-menu-buffer)) (crdt-refresh-sessions crdt--session-menu-buffer))) + ;;; Buffer menu (defun crdt--buffer-menu-goto () (interactive) (switch-to-buffer-other-window (tabulated-list-get-id))) + (defun crdt--buffer-menu-kill () (interactive) (with-current-buffer (tabulated-list-get-id) (crdt-stop-share-buffer))) + (defvar crdt-buffer-menu-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "RET") #'crdt--buffer-menu-goto) (define-key map (kbd "k") #'crdt--buffer-menu-kill) map)) + (define-derived-mode crdt-buffer-menu-mode tabulated-list-mode "CRDT User List" (setq tabulated-list-format [("Buffer" 15 t) ("Network Name" 15 t)])) + (defun crdt-list-buffer (&optional crdt-buffer display-buffer) "Display a list of buffers shared in the current CRDT session. -If DISPLAY-BUFFER is provided, display the output there. Otherwise use a dedicated -buffer for displaying active users on CRDT-BUFFER." +If DISPLAY-BUFFER is provided, display the output there. +Otherwise use a dedicated buffer for displaying active users on CRDT-BUFFER." (interactive) (with-current-buffer (or crdt-buffer (current-buffer)) (unless (or crdt-mode crdt--network-process) - (error "Not a CRDT shared buffer.")) + (error "Not a CRDT shared buffer")) (unless display-buffer (unless (and (crdt--buffer-menu-buffer) (buffer-live-p (crdt--buffer-menu-buffer))) (setf (crdt--buffer-menu-buffer) @@ -439,6 +536,7 @@ buffer for displaying active users on CRDT-BUFFER." (with-current-buffer crdt--status-buffer (crdt-refresh-buffers display-buffer)) (switch-to-buffer-other-window display-buffer))) + (defun crdt-refresh-buffers (display-buffer) (with-current-buffer display-buffer (crdt-buffer-menu-mode) @@ -449,11 +547,13 @@ buffer for displaying active users on CRDT-BUFFER." (crdt--buffer-table)) (tabulated-list-init-header) (tabulated-list-print))) + (defsubst crdt--refresh-buffers-maybe () (when (and (crdt--buffer-menu-buffer) (buffer-live-p (crdt--buffer-menu-buffer))) (crdt-refresh-buffers (crdt--buffer-menu-buffer))) (crdt--refresh-sessions-maybe)) + ;;; User menu (defun crdt--user-menu-goto () (interactive) @@ -467,24 +567,27 @@ buffer for displaying active users on CRDT-BUFFER." (switch-to-buffer-other-window focused-buffer) (when site-id (goto-char (overlay-start (car (gethash site-id crdt--pseudo-cursor-table))))))) + (defvar crdt-user-menu-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "RET") #'crdt--user-menu-goto) map)) + (define-derived-mode crdt-user-menu-mode tabulated-list-mode "CRDT User List" (setq tabulated-list-format [("Display Name" 15 t) ("Focused Buffer" 15 t) ("Address" 15 t) ("Port" 7 t)])) + (defun crdt-list-users (&optional crdt-buffer display-buffer) "Display a list of active users working on a CRDT-shared buffer CRDT-BUFFER. -If DISPLAY-BUFFER is provided, display the output there. Otherwise use a dedicated -buffer for displaying active users on CRDT-BUFFER." +If DISPLAY-BUFFER is provided, display the output there. +Otherwise use a dedicated buffer for displaying active users on CRDT-BUFFER." (interactive) (with-current-buffer (or crdt-buffer (current-buffer)) (unless crdt-mode - (error "Not a CRDT shared buffer.")) + (error "Not a CRDT shared buffer")) (unless display-buffer (unless (and (crdt--user-menu-buffer) (buffer-live-p (crdt--user-menu-buffer))) (setf (crdt--user-menu-buffer) @@ -495,6 +598,7 @@ buffer for displaying active users on CRDT-BUFFER." (with-current-buffer crdt--status-buffer (crdt-refresh-users display-buffer)) (switch-to-buffer-other-window display-buffer))) + (defun crdt-refresh-users (display-buffer) "Must be called with CURRENT-BUFFER set to a CRDT status buffer." (let (table local-name local-id) @@ -522,14 +626,17 @@ buffer for displaying active users on CRDT-BUFFER." table) (tabulated-list-init-header) (tabulated-list-print)))) + (defsubst crdt--refresh-users-maybe () (when (and (crdt--user-menu-buffer) (buffer-live-p (crdt--user-menu-buffer))) (crdt-refresh-users (crdt--user-menu-buffer))) (crdt--refresh-sessions-maybe)) + ;;; CRDT insert/delete (defsubst crdt--base64-encode-maybe (str) (when str (base64-encode-string str))) + (defun crdt--local-insert (beg end) "To be called after a local insert happened in current buffer from BEG to END. Returns a list of (insert type) messages to be sent." @@ -568,7 +675,7 @@ Returns a list of (insert type) messages to be sent." (nreverse resulting-commands))) (defun crdt--find-id (id pos &optional before) - "Find the first position *after* ID if BEFORE is NIL, or *before* ID otherwise. + "Find the first position *after* ID if BEFORE is NIL or *before* ID otherwise. Start the search from POS." (let* ((left-pos (previous-single-property-change (if (< pos (point-max)) (1+ pos) pos) 'crdt-id nil (point-min))) @@ -600,6 +707,7 @@ Start the search from POS." (- (crdt--get-two-bytes id (- (string-bytes left-id) 2)) (crdt--id-offset left-id)))) right-pos)))))))) + (defun crdt--remote-insert (id position-hint content) (let ((crdt--inhibit-update t)) (let* ((beg (crdt--find-id id position-hint)) end) @@ -636,6 +744,7 @@ Start the search from POS." ;; (crdt--verify-buffer) `(delete ,crdt--buffer-network-name ,beg ,@ (crdt--dump-ids 0 (length crdt--changed-string) crdt--changed-string t))) + (defun crdt--remote-delete (position-hint id-pairs) (dolist (id-pair id-pairs) (cl-destructuring-bind (length . id) id-pair @@ -660,14 +769,17 @@ Start the search from POS." ))) (defun crdt--before-change (beg end) + (print (list beg end crdt--inhibit-update)) (unless crdt--inhibit-update - (setq crdt--changed-string (buffer-substring beg end)))) + (setq crdt--changed-string (buffer-substring beg end))) + (print crdt--changed-string)) (defun crdt--after-change (beg end length) (mapc (lambda (ov) (when (eq (overlay-get ov 'category) 'crdt-pseudo-cursor) (crdt--move-cursor ov beg))) (overlays-in beg (min (point-max) (1+ beg)))) + (print (list 1 crdt--changed-string (buffer-substring-no-properties beg end))) (when (crdt--local-id) ; CRDT--LOCAL-ID is NIL when a client haven't received the first sync message (unless crdt--inhibit-update (let ((crdt--inhibit-update t)) @@ -675,6 +787,7 @@ Start the search from POS." ;; ignore property only changes (save-excursion (goto-char beg) + (print (list crdt--changed-string (buffer-substring-no-properties beg end))) (unless (and (= length (- end beg)) (string-equal crdt--changed-string (buffer-substring-no-properties beg end))) @@ -693,6 +806,7 @@ Start the search from POS." (if (> (string-bytes id) 0) (crdt--find-id id hint t) (point-max))) + (defun crdt--remote-cursor (site-id point-position-hint point-crdt-id mark-position-hint mark-crdt-id) (when site-id (let ((ov-pair (gethash site-id crdt--pseudo-cursor-table))) @@ -733,6 +847,7 @@ Start the search from POS." (mark-id-base64 (when mark (base64-encode-string (crdt--get-id mark))))) `(cursor ,crdt--buffer-network-name ,(crdt--local-id) ,point ,point-id-base64 ,mark ,mark-id-base64))))) + (defun crdt--post-command () (unless (eq crdt--buffer-network-name (crdt--focused-buffer-name)) (crdt--broadcast-maybe @@ -742,6 +857,7 @@ Start the search from POS." (when cursor-message (crdt--broadcast-maybe (crdt--format-message cursor-message))))) + ;;; CRDT ID (de)serialization (defun crdt--dump-ids (beg end object &optional omit-end-of-block-p) "Serialize all CRDT ids in OBJECT from BEG to END into a list. @@ -758,6 +874,7 @@ in the order that they appears in the document" ids) (setq pos prev-pos))) ids)) + (defun crdt--load-ids (ids) "Load the CRDT ids in IDS (generated by CRDT--DUMP-IDS) into current buffer." @@ -767,6 +884,7 @@ into current buffer." (put-text-property pos next-pos 'crdt-id (cons (base64-decode-string (cadr id-pair)) (cddr id-pair))) (setq pos next-pos))))) + (defun crdt--verify-buffer () "Debug helper function. Verify that CRDT IDs in a document follows ascending order." @@ -785,13 +903,11 @@ Verify that CRDT IDs in a document follows ascending order." (setq pos next-pos) (setq id next-id)))))) + ;;; Network protocol -(crdt--defvar-session crdt--network-process nil) -(crdt--defvar-session crdt--network-clients nil) -(crdt--defvar-session crdt--next-client-id) -(crdt--defvar-session crdt--buffer-table) (defun crdt--format-message (args) (format "%S" args)) + (cl-defun crdt--broadcast-maybe (message-string &optional (without t)) "Broadcast or send MESSAGE-STRING. If CRDT--NETWORK-PROCESS is a server process, broadcast MESSAGE-STRING @@ -811,6 +927,7 @@ to server when WITHOUT is T." (process-send-string (crdt--network-process) message-string) ;; (run-at-time 1 nil #'process-send-string crdt--network-process message-string) ))) + (defsubst crdt--overlay-add-message (id clock species front-advance rear-advance beg end) `(overlay-add ,crdt--buffer-network-name ,id ,clock ,species ,front-advance ,rear-advance @@ -820,15 +937,17 @@ to server when WITHOUT is T." ,end ,(if rear-advance (base64-encode-string (crdt--get-id end)) (crdt--base64-encode-maybe (crdt--get-id (1- end)))))) + (defun crdt--generate-challenge () (apply #'unibyte-string (cl-loop for i below 32 collect (random 256)))) + (defun crdt--greet-client (process) (with-current-buffer (process-get process 'status-buffer) (cl-pushnew process crdt--network-clients) (let ((client-id (process-get process 'client-id))) (unless client-id (unless (< crdt--next-client-id crdt--max-value) - (error "Used up client IDs. Need to implement allocation algorithm.")) + (error "Used up client IDs. Need to implement allocation algorithm")) (process-put process 'client-id crdt--next-client-id) (setq client-id crdt--next-client-id) (process-send-string process (crdt--format-message `(login ,client-id))) @@ -920,15 +1039,18 @@ Must be called when CURRENT-BUFFER is a CRDT status buffer." ,@body)))))) (cl-defgeneric crdt-process-message (message process)) + (cl-defmethod crdt-process-message (message process) (message "Unrecognized message %S from %s:%s." message (process-contact process :host) (process-contact process :service))) + (cl-defmethod crdt-process-message ((message (head insert)) process) (cl-destructuring-bind (buffer-name crdt-id position-hint content) (cdr message) (crdt--with-buffer-name buffer-name (crdt--remote-insert (base64-decode-string crdt-id) position-hint content))) (crdt--broadcast-maybe (crdt--format-message message) (process-get process 'client-id))) + (cl-defmethod crdt-process-message ((message (head delete)) process) (crdt--broadcast-maybe (crdt--format-message message) (process-get process 'client-id)) (cl-destructuring-bind (buffer-name position-hint . id-base64-pairs) (cdr message) @@ -936,6 +1058,7 @@ Must be called when CURRENT-BUFFER is a CRDT status buffer." (crdt--with-buffer-name buffer-name (crdt--remote-delete position-hint id-base64-pairs)))) + (cl-defmethod crdt-process-message ((message (head cursor)) process) (cl-destructuring-bind (buffer-name site-id point-position-hint point-crdt-id mark-position-hint mark-crdt-id) @@ -947,6 +1070,10 @@ Must be called when CURRENT-BUFFER is a CRDT status buffer." mark-position-hint (and mark-crdt-id (base64-decode-string mark-crdt-id))))) (crdt--broadcast-maybe (crdt--format-message message) (process-get process 'client-id))) + +(defsubst crdt--server-p () + (process-contact (crdt--network-process) :server)) + (cl-defmethod crdt-process-message ((message (head sync)) process) (unless (crdt--server-p) ; server shouldn't receive this (cl-destructuring-bind (buffer-name mode content . ids) (cdr message) @@ -961,6 +1088,7 @@ Must be called when CURRENT-BUFFER is a CRDT status buffer." (insert content) (crdt--load-ids ids))) (crdt--refresh-buffers-maybe))) + (cl-defmethod crdt-process-message ((message (head desync)) process) (cl-destructuring-bind (buffer-name) (cdr message) (let ((buffer (gethash buffer-name crdt--buffer-table))) @@ -973,6 +1101,7 @@ Must be called when CURRENT-BUFFER is a CRDT status buffer." (crdt--broadcast-maybe (crdt--format-message message) (when process (process-get process 'client-id))) (crdt--refresh-buffers-maybe)) + (cl-defmethod crdt-process-message ((message (head login)) process) (cl-destructuring-bind (id) (cdr message) (puthash 0 (crdt--make-contact-metadata nil nil @@ -981,6 +1110,7 @@ Must be called when CURRENT-BUFFER is a CRDT status buffer." crdt--contact-table) (setq crdt--local-id id) (crdt--refresh-sessions-maybe))) + (cl-defmethod crdt-process-message ((message (head challenge)) process) (unless (crdt--server-p) ; server shouldn't receive this (message nil) @@ -990,6 +1120,7 @@ Must be called when CURRENT-BUFFER is a CRDT status buffer." (process-contact (crdt--network-process) :service))))) (crdt--broadcast-maybe (crdt--format-message `(hello ,(crdt--local-name) ,(gnutls-hash-mac 'SHA1 password (cadr message)))))))) + (cl-defmethod crdt-process-message ((message (head contact)) process) (cl-destructuring-bind (site-id display-name &optional host service) (cdr message) @@ -1003,6 +1134,7 @@ Must be called when CURRENT-BUFFER is a CRDT status buffer." (remhash site-id crdt--contact-table)) (crdt--refresh-users-maybe)) (crdt--broadcast-maybe (crdt--format-message message) (process-get process 'client-id))) + (cl-defmethod crdt-process-message ((message (head focus)) process) (cl-destructuring-bind (site-id buffer-name) (cdr message) @@ -1014,9 +1146,6 @@ Must be called when CURRENT-BUFFER is a CRDT status buffer." (crdt--refresh-users-maybe)) (crdt--broadcast-maybe (crdt--format-message message) (process-get process 'client-id))) -(defsubst crdt--server-p () - (process-contact (crdt--network-process) :server)) - (defun crdt--network-filter (process string) (unless (and (process-buffer process) (buffer-live-p (process-buffer process))) @@ -1060,6 +1189,7 @@ Must be called when CURRENT-BUFFER is a CRDT status buffer." (crdt-stop-session)))))) (delete-region (point-min) (point)) (goto-char (point-min))))))) + (defun crdt--server-process-sentinel (client message) (with-current-buffer (process-get client 'status-buffer) (unless (or (process-contact client :server) ; it's actually server itself @@ -1078,17 +1208,20 @@ Must be called when CURRENT-BUFFER is a CRDT status buffer." client)) crdt--buffer-table) (crdt--refresh-users-maybe))))) + (defun crdt--client-process-sentinel (process message) (with-current-buffer (process-get process 'status-buffer) (unless (eq (process-status process) 'open) (crdt-stop-session)))) + ;;; UI commands (defun crdt--read-name () (if crdt-ask-for-name (let ((input (read-from-minibuffer (format "Display name (default %S): " crdt-default-name)))) (if (> (length input) 0) input crdt-default-name)) crdt-default-name)) + (defun crdt--share-buffer (buffer session) (if (process-contact session :server) (with-current-buffer buffer @@ -1110,6 +1243,7 @@ Must be called when CURRENT-BUFFER is a CRDT status buffer." (crdt--refresh-buffers-maybe) (crdt--refresh-sessions-maybe)) (message "Only server can add new buffer."))) + (defun crdt-share-buffer (session-name) "Share the current buffer in the CRDT session with name SESSION-NAME. Create a new one if such a CRDT session doesn't exist. @@ -1127,6 +1261,7 @@ If SESSION-NAME is empty, use the buffer name of the current buffer." (crdt--share-buffer (current-buffer) (cdr session)) (let ((port (read-from-minibuffer "Create new session on Port (default 1333): " nil nil t nil "1333"))) (crdt--share-buffer (current-buffer) (crdt-new-session port session-name))))))) + (defun crdt-stop-share-buffer () "Stop sharing the current buffer." (interactive) @@ -1138,6 +1273,7 @@ If SESSION-NAME is empty, use the buffer name of the current buffer." (crdt-process-message desync-message nil)))) (message "Only server can stop sharing a buffer.")) (message "Not a CRDT shared buffer."))) + (defun crdt-new-session (port session-name &optional password display-name) "Start a new CRDT session on PORT." (let ((new-session @@ -1175,13 +1311,7 @@ If SESSION-NAME is empty, use the buffer name of the current buffer." crdt--network-process))) (push (cons session-name new-session) crdt--session-alist) new-session)) -(defsubst crdt--clear-pseudo-cursor-table () - (when crdt--pseudo-cursor-table - (maphash (lambda (key pair) - (delete-overlay (car pair)) - (delete-overlay (cdr pair))) - crdt--pseudo-cursor-table) - (setq crdt--pseudo-cursor-table nil))) + (defun crdt-stop-session () "Stop sharing the current session." (interactive) @@ -1240,14 +1370,21 @@ Open a new buffer to display the shared content." (setq crdt--contact-table (make-hash-table :test 'equal)) (setq crdt--buffer-table (make-hash-table :test 'equal)) (setq crdt--status-buffer (current-buffer))))) + (defun crdt-test-client () (interactive) (crdt-connect "127.0.0.1" 1333)) + (defun crdt-test-server () (interactive) (crdt--share-buffer (current-buffer) (crdt-new-session 1333 "test"))) + ;;; overlay tracking +(defvar crdt--inhibit-overlay-advices nil) + +(defvar crdt--modifying-overlay-metadata nil) + (defun crdt--enable-overlay-species (species) (push species crdt--enabled-overlay-species) (when crdt-mode @@ -1258,6 +1395,7 @@ Open a new buffer to display the shared content." (cl-loop for (prop value) on (crdt--overlay-metadata-plist meta) by #'cddr do (overlay-put ov prop value))))) crdt--overlay-table)))) + (defun crdt--disable-overlay-species (species) (setq crdt--enabled-overlay-species (delq species crdt--enabled-overlay-species)) (when crdt-mode @@ -1268,9 +1406,10 @@ Open a new buffer to display the shared content." (cl-loop for (prop value) on (crdt--overlay-metadata-plist meta) by #'cddr do (overlay-put ov prop nil))))) crdt--overlay-table)))) + (defun crdt--make-overlay-advice (orig-fun beg end &optional buffer front-advance rear-advance) - ; should we check if we are in the current buffer? (let ((new-overlay (funcall orig-fun beg end buffer front-advance rear-advance))) + ;; should we check if we are in the current buffer? (when crdt-mode (when crdt--track-overlay-species (crdt--broadcast-maybe @@ -1287,6 +1426,7 @@ Open a new buffer to display the shared content." (overlay-put new-overlay 'crdt-meta meta))) (cl-incf (crdt--local-clock)))) new-overlay)) + (cl-defmethod crdt-process-message ((message (head overlay-add)) process) (cl-destructuring-bind (buffer-name site-id logical-clock species @@ -1307,6 +1447,7 @@ Open a new buffer to display the shared content." (crdt--modifying-overlay-metadata t)) (overlay-put new-overlay 'crdt-meta meta))))) (crdt--broadcast-maybe (crdt--format-message message) (process-get process 'client-id))) + (defun crdt--move-overlay-advice (orig-fun ov beg end &rest args) (when crdt-mode (unless crdt--inhibit-overlay-advices @@ -1325,6 +1466,7 @@ Open a new buffer to display the shared content." (base64-encode-string (crdt--get-id end)) (crdt--base64-encode-maybe (crdt--get-id (1- end)))))))))))) (apply orig-fun ov beg end args)) + (cl-defmethod crdt-process-message ((message (head overlay-move)) process) (cl-destructuring-bind (buffer-name site-id logical-clock start-hint start-id-base64 end-hint end-id-base64) @@ -1342,8 +1484,7 @@ Open a new buffer to display the shared content." (let ((crdt--inhibit-overlay-advices t)) (move-overlay ov start end))))))) (crdt--broadcast-maybe (crdt--format-message message) (process-get process 'client-id))) -(defvar crdt--inhibit-overlay-advices nil) -(defvar crdt--modifying-overlay-metadata nil) + (defun crdt--delete-overlay-advice (orig-fun ov) (unless crdt--inhibit-overlay-advices (when crdt-mode @@ -1354,6 +1495,7 @@ Open a new buffer to display the shared content." (crdt--broadcast-maybe (crdt--format-message `(overlay-remove ,crdt--buffer-network-name ,(car key) ,(cdr key))))))))) (funcall orig-fun ov)) + (cl-defmethod crdt-process-message ((message (head overlay-remove)) process) (cl-destructuring-bind (buffer-name site-id logical-clock) (cdr message) (crdt--with-buffer-name @@ -1365,6 +1507,7 @@ Open a new buffer to display the shared content." (let ((crdt--inhibit-overlay-advices t)) (delete-overlay ov)))))) (crdt--broadcast-maybe (crdt--format-message message) (process-get process 'client-id))) + (defun crdt--overlay-put-advice (orig-fun ov prop value) (unless (and (eq prop 'crdt-meta) (not crdt--modifying-overlay-metadata)) @@ -1377,11 +1520,12 @@ Open a new buffer to display the shared content." (message (crdt--format-message `(overlay-put ,crdt--buffer-network-name ,(car key) ,(cdr key) ,prop ,value)))) (condition-case nil - (progn ; filter non-readable object + (progn ; filter non-readable object (read-from-string message) (crdt--broadcast-maybe message)) (invalid-read-syntax))))))) (funcall orig-fun ov prop value))) + (cl-defmethod crdt-process-message ((message (head overlay-put)) process) (cl-destructuring-bind (buffer-name site-id logical-clock prop value) (cdr message) (crdt--with-buffer-name @@ -1395,37 +1539,17 @@ Open a new buffer to display the shared content." (let ((crdt--inhibit-overlay-advices t)) (overlay-put ov prop value)))))))) (crdt--broadcast-maybe (crdt--format-message message) (process-get process 'client-id))) + (advice-add 'make-overlay :around #'crdt--make-overlay-advice) + (advice-add 'move-overlay :around #'crdt--move-overlay-advice) + (advice-add 'delete-overlay :around #'crdt--delete-overlay-advice) + (advice-add 'overlay-put :around #'crdt--overlay-put-advice) -(defun crdt--install-hooks () - (add-hook 'after-change-functions #'crdt--after-change nil t) - (add-hook 'before-change-functions #'crdt--before-change nil t) - (add-hook 'post-command-hook #'crdt--post-command nil t)) -(defun crdt--uninstall-hooks () - (remove-hook 'after-change-functions #'crdt--after-change t) - (remove-hook 'before-change-functions #'crdt--before-change t) - (remove-hook 'post-command-hook #'crdt--post-command t)) -(define-minor-mode crdt-mode - "CRDT mode" nil " CRDT" nil - (if crdt-mode - (progn - (setq crdt--pseudo-cursor-table (make-hash-table)) - (setq crdt--overlay-table (make-hash-table :test 'equal)) - (crdt--install-hooks)) - (crdt--uninstall-hooks) - (crdt--clear-pseudo-cursor-table) - (setq crdt--overlay-table nil))) + ;;; Org integration -(defun crdt--org-overlay-advice (orig-fun &rest args) - (if crdt-org-sync-overlay-mode - (let ((crdt--track-overlay-species 'org)) - (apply orig-fun args)) - (apply orig-fun args))) -(cl-loop for command in '(org-cycle org-shifttab) - do (advice-add command :around #'crdt--org-overlay-advice)) (define-minor-mode crdt-org-sync-overlay-mode "" nil " Sync Org Overlay" nil (if crdt-org-sync-overlay-mode @@ -1440,4 +1564,15 @@ Open a new buffer to display the shared content." (crdt--enable-overlay-species 'org)) (crdt--disable-overlay-species 'org))) +(defun crdt--org-overlay-advice (orig-fun &rest args) + (if crdt-org-sync-overlay-mode + (let ((crdt--track-overlay-species 'org)) + (apply orig-fun args)) + (apply orig-fun args))) + +(cl-loop for command in '(org-cycle org-shifttab) + do (advice-add command :around #'crdt--org-overlay-advice)) + + (provide 'crdt) +;;; crdt.el ends here