[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] externals/crdt 973e761 21/80: Work on Jean's todo list
From: |
ELPA Syncer |
Subject: |
[elpa] externals/crdt 973e761 21/80: Work on Jean's todo list |
Date: |
Sat, 28 Aug 2021 10:57:33 -0400 (EDT) |
branch: externals/crdt
commit 973e761f125b27f1c2c19d4939444dca6a1fdd0c
Author: Qiantan Hong <qhong@mit.edu>
Commit: Qiantan Hong <qhong@mit.edu>
Work on Jean's todo list
- better formatting for docs
- a few more clarification in README
- separate crdt-stop-session and crdt-disconnect
- ask for confirmation when stopping a session
- default display to user full name
- default crdt-connect port to 6530
---
HACKING.org | 27 +++++++++--
README.org | 55 ++++++++++++++++++-----
crdt.el | 146 +++++++++++++++++++++++++++++++++++++++---------------------
3 files changed, 164 insertions(+), 64 deletions(-)
diff --git a/HACKING.org b/HACKING.org
index e20327e..d6bc99b 100644
--- a/HACKING.org
+++ b/HACKING.org
@@ -1,5 +1,7 @@
* Algorithm
+Background reading:
[[https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type][CRDT]]
+
This packages implements the Logoot split algorithm
~André, Luc, et al. "Supporting adaptable granularity of changes for
massive-scale collaborative editing." 9th IEEE International Conference on
Collaborative Computing: Networking, Applications and Worksharing. IEEE, 2013.~
@@ -17,60 +19,76 @@ and second last two bytes represent site ID.
=CRDT--SESSION-LIST= is a list of "CRDT status buffer"s.
Currently those buffers are always empty, but they have some buffer local
variables,
which are used as "session variables" that can be accessed from any buffer
shared in the same session.
+
For a buffer shared in some session, this buffer always has its buffer local
variable
=CRDT--STATUS-BUFFER= set to a CRDT status buffer. It can then access any
session variables through it.
+
For a network process dedicated to a session, its ='status-buffer= process
property is always set to the status buffer for that session.
+
The macro =CRDT--DEFVAR-SESSION= do the chores of defining a buffer local
variable for status buffer,
and creating a function (together with =SETF= setter) with the same name as
the variables, and can
be invoked with no argument in any CRDT shared buffer to access or modify that
session variable.
-
* Protocol
+
Text-based version
(it should be easy to migrate to a binary version. Using text for better
debugging for now)
- Every message takes the form (type . body)
+
+ Every message takes the form =(type . body)=
+
type can be: insert delete cursor hello challenge sync desync
overlay-(add,move,put,remove)
+
- insert ::
body takes the form =(buffer-name crdt-id position-hint content)=
- =position-hint= is the buffer position where the operation happens at
the site
which generates the operation. Then we can play the trick that start
search
near this position at other sites to speedup CRDT ID search
- =content= is the string to be inserted
+
- delete ::
body takes the form =(buffer-name position-hint (crdt-id . length)*)=
+
- cursor ::
body takes the form
=(site-id point-position-hint point-crdt-id mark-position-hint
mark-crdt-id)=
=*-crdt-id= can be either a CRDT ID, or
- =nil=, which means clear the point/mark
- =""=, which means =(point-max)=
+
- contact ::
body takes the form
=(site-id name address port)=
when name is =nil=, clear the contact for this =site-id=
+
- focus ::
body takes the form =(site-id buffer-name)=
+
- hello ::
This message is sent from client to server, when a client connect to the
server.
body takes the form =(client-name &optional response)=
+
- challenge ::
body takes the form =(salt)=
+
- login ::
It's always sent after server receives a hello message.
Assigns an ID to the client
body takes the form =(site-id session-name)=.
+
- sync ::
This message is sent from server to client to get it sync to the state on
the server.
Might be used for error recovery or other optimization in the future.
One optimization I have in mind is let server try to merge all CRDT item
into a single
one and try to synchronize this state to clients at best effort.
- body takes the form =(buffer-name major-mode content . crdt-id-list)=
+ body takes the form =(buffer-name major-mode . crdt-id-list)=
- =major-mode= is the major mode used at the server site
- =content= is the string in the buffer
- =crdt-id-list= is generated from =CRDT--DUMP-IDS=
+
- desync ::
Indicates that the server has stopped sharing a buffer.
body takes the form =(buffer-name)=
+
- overlay-add ::
body takes the form
#+BEGIN_SRC
@@ -79,6 +97,7 @@ be invoked with no argument in any CRDT shared buffer to
access or modify that s
start-position-hint start-crdt-id
end-position-hint end-crdt-id)
#+END_SRC
+
- overlay-move ::
body takes the form
#+BEGIN_SRC
@@ -86,8 +105,10 @@ be invoked with no argument in any CRDT shared buffer to
access or modify that s
start-position-hint start-crdt-id
end-position-hint end-crdt-id)
#+END_SRC
+
- overlay-put ::
body takes the form =(buffer-name site-id logical-clock prop value)=
+
- overlay-remove ::
body takes the form =(buffer-name site-id logical-clock)=
diff --git a/README.org b/README.org
index 2e0ad26..682d279 100644
--- a/README.org
+++ b/README.org
@@ -1,4 +1,5 @@
* Introduction
+
~crdt.el~ is a real-time collaborative editing environment for Emacs using
Conflict-free Replicated Data Types.
Highlights:
@@ -6,30 +7,62 @@ Highlights:
- Share multiple buffer in one session
- See other users' cursor and region
- (experimental) synchronize Org mode folding status
+
* Usage
+
** Installation
+
Just =M-x load-file= =crdt.el=, or =M-x eval-buffer= in =crdt.el=,
or =(require 'crdt)=. Or whatever package management tool you use.
-** Share a buffer
-In that buffer, =M-x crdt-share-buffer=. Then enter session name.
-
-If a new session is to be created, enter port, optional password and your
display name.
-If there's a existing session with the name, current buffer is added to that
session.
-** Connect to a shared buffer
-=M-x crdt-connect=
-** List active users.
+
+** Start a shared session
+
+A shared session is a place that can contains multiple buffers (or files),
+and multiple users can join to collaboratively edit those buffers (or files).
+Think about a meeting room with some people working together on some papers.
+
+In some buffer, =M-x crdt-share-buffer=. Then enter session name.
+This add the current buffer to the existing session with that name.
+If no such exists, it creates a new session with the provided session name,
+and initially contains the current buffer as a shared buffer.
+
+If a new session is to be created, you need to enter port (default to 6530),
+optional password and your display name (default to your current
=(USER-FULL-NAME)=).
+
+** Join a session
+
+=M-x crdt-connect=, then enter address, port, and your display name.
+
+** List active users
+
In a CRDT shared buffer (either server or client), =M-x crdt-list-users=.
In the displayed user list, press ~RET~ on an entry to goto that user's cursor
position.
-** List all sessions, and buffer in current session.
+
+** List all sessions, and buffer in current session
+
=M-x crdt-list-sessions= lists all sessions.
+
=M-x crdt-list-buffers= lists all buffers in current session. Or you can also
press ~RET~ in the session list to see buffers in the selected session.
-** Stop sharing.
-=M-x crdt-stop-session= stops the current session. You can also press ~k~ in
the session list.
+
+** Stop sharing
+
+=M-x crdt-stop-session= stops a session you've started and disconnect all
other users from it.
+This will ask for your confirmation, customize =crdt-confirm-stop-session= if
you want to disable it.
+
+You can also press ~k~ in the session list (show it by =M-x
crdt-list-sessions=).
=M-x crdt-stop-share-buffer= removes current buffer from its CRDT session
(this operation is only allowed at server side). Or press ~k~ in the buffer
list.
+
+** Disconnect from a session
+
+=M-x crdt-disconnect=, then choose a session to disconnect from.
+
+You can also press ~k~ in the session list (show it by =M-x
crdt-list-sessions=).
+
** Synchronizing Org folding status
+
Turn on =crdt-org-sync-overlay-mode=. All peers that have this enabled have
their
folding status synchronized. Peers without enabling this minor mode are
unaffected.
diff --git a/crdt.el b/crdt.el
index 4ea6043..9b33c83 100644
--- a/crdt.el
+++ b/crdt.el
@@ -32,10 +32,10 @@
:group 'applications)
(defcustom crdt-ask-for-name t
- "Ask for display name everytime a CRDT session is to be started."
+ "Ask for display name everytime a CRDT session is to be started or
connected."
:type 'boolean)
-(defcustom crdt-default-name "anonymous"
+(defcustom crdt-default-name (user-full-name)
"Default display name."
:type 'string)
@@ -43,6 +43,11 @@
"Ask for server password everytime a CRDT server is to be started."
:type 'boolean)
+(defcustom crdt-confirm-stop-session t
+ "Ask for confirmation when a CRDT server is to be stopped,
+and there are some client connected to it currently."
+ :type 'boolean)
+
(require 'cl-lib)
(require 'subr-x)
@@ -376,7 +381,7 @@ to avoid recusive calling of CRDT synchronization
functions.")
(interactive)
(with-current-buffer
(tabulated-list-get-id)
- (crdt-stop-session)))
+ (crdt--stop-session (current-buffer))))
(defvar crdt-session-menu-mode-map
(let ((map (make-sparse-keymap)))
@@ -726,9 +731,9 @@ Start the search from POS."
`(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
+(defun crdt--remote-delete (position-hint id-items)
+ (dolist (id-item id-items)
+ (cl-destructuring-bind (length id) id-item
(while (> length 0)
(goto-char (crdt--find-id id position-hint t))
(let* ((end-of-block (next-single-property-change (point) 'crdt-id nil
(point-max)))
@@ -836,31 +841,39 @@ Start the search from POS."
;;; 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.
-The list contains CONSes of the form (LENGTH CRDT-ID-BASE64 . END-OF-BLOCK-P),
-or (LENGTH . CRDT-ID-BASE64) if OMIT-END-OF-BLOCK-P is non-NIL.
-in the order that they appears in the document"
+(defun crdt--dump-ids (beg end object &optional omit-end-of-block-p
include-content)
+ "Serialize all CRDT IDs in OBJECT from BEG to END into a list.
+The list contains CONSes of the form (LENGTH CRDT-ID-BASE64 END-OF-BLOCK-P),
+or (LENGTH CRDT-ID-BASE64) if OMIT-END-OF-BLOCK-P is non-NIL,
+in the order that they appears in the document.
+If INCLUDE-CONTENT is non-NIL, the list contains STRING instead of LENGTH."
(let (ids (pos end))
(while (> pos beg)
(let ((prev-pos (previous-single-property-change pos 'crdt-id object
beg)))
- (push (cons (- pos prev-pos)
- (cl-destructuring-bind (id . eob) (crdt--get-crdt-id-pair
prev-pos object)
- (let ((id-base64 (base64-encode-string id)))
- (if omit-end-of-block-p id-base64 (cons id-base64
eob)))))
- ids)
+ (when (crdt--get-crdt-id-pair prev-pos object)
+ (push (cons (if include-content
+ (cond ((not object) (buffer-substring-no-properties
prev-pos pos))
+ ((bufferp object)
+ (with-current-buffer object
+ (buffer-substring-no-properties prev-pos
pos)))
+ (t (substring object prev-pos pos)))
+ (- pos prev-pos))
+ (cl-destructuring-bind (id . eob)
(crdt--get-crdt-id-pair prev-pos object)
+ (print omit-end-of-block-p)
+ (let ((id-base64 (base64-encode-string id)))
+ (if omit-end-of-block-p (list id-base64) (list
id-base64 eob)))))
+ 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."
- (let ((pos (point-min)))
- (dolist (id-pair ids)
- (let ((next-pos (+ pos (car id-pair))))
- (put-text-property pos next-pos 'crdt-id
- (cons (base64-decode-string (cadr id-pair)) (cddr
id-pair)))
- (setq pos next-pos)))))
+ (goto-char (point-min))
+ (dolist (id-item ids)
+ (cl-destructuring-bind (content id-base64 eob) id-item
+ (insert (propertize content 'crdt-id
+ (cons (base64-decode-string id-base64) eob))))))
(defun crdt--verify-buffer ()
"Debug helper function.
@@ -883,7 +896,9 @@ Verify that CRDT IDs in a document follows ascending order."
;;; Network protocol
(defun crdt--format-message (args)
- (format "%S" args))
+ (let ((print-level nil)
+ (print-length nil))
+ (prin1-to-string args)))
(cl-defun crdt--broadcast-maybe (message-string &optional (without t))
"Broadcast or send MESSAGE-STRING.
@@ -936,8 +951,7 @@ to server when WITHOUT is T."
(process-send-string process (crdt--format-message `(sync
,crdt--buffer-network-name
,major-mode
-
,(buffer-substring-no-properties (point-min) (point-max))
- ,@
(crdt--dump-ids (point-min) (point-max) nil))))
+ ,@
(crdt--dump-ids (point-min) (point-max) nil nil t))))
;; synchronize cursor
(maphash (lambda (site-id ov-pair)
(cl-destructuring-bind (cursor-ov . region-ov)
ov-pair
@@ -1033,7 +1047,7 @@ Must be called when CURRENT-BUFFER is a CRDT status
buffer."
(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)
- (mapc (lambda (p) (rplacd p (base64-decode-string (cdr p))))
id-base64-pairs)
+ (mapc (lambda (p) (rplaca (cdr p) (base64-decode-string (cadr p))))
id-base64-pairs)
(crdt--with-buffer-name
buffer-name
(crdt--remote-delete position-hint id-base64-pairs))))
@@ -1055,7 +1069,7 @@ Must be called when CURRENT-BUFFER is a CRDT status
buffer."
(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)
+ (cl-destructuring-bind (buffer-name mode . ids) (cdr message)
(crdt--with-buffer-name
buffer-name
(erase-buffer)
@@ -1064,7 +1078,6 @@ Must be called when CURRENT-BUFFER is a CRDT status
buffer."
(funcall mode) ; trust your server...
(crdt-mode))
(message "Server uses %s, but not available locally." mode))
- (insert content)
(crdt--load-ids ids)))
(crdt--refresh-buffers-maybe)))
@@ -1166,7 +1179,7 @@ Must be called when CURRENT-BUFFER is a CRDT status
buffer."
(process-contact process :host)
(process-contact process :service))
(if (crdt--server-p)
(delete-process process)
- (crdt-stop-session))))))
+ (crdt--stop-session crdt--status-buffer))))))
(delete-region (point-min) (point))
(goto-char (point-min)))))))
@@ -1192,7 +1205,7 @@ Must be called when CURRENT-BUFFER is a CRDT status
buffer."
(defun crdt--client-process-sentinel (process message)
(with-current-buffer (process-get process 'status-buffer)
(unless (eq (process-status process) 'open)
- (crdt-stop-session))))
+ (crdt--stop-session (current-buffer)))))
;;; UI commands
@@ -1223,12 +1236,15 @@ Must be called when CURRENT-BUFFER is a CRDT status
buffer."
,@ (crdt--dump-ids (point-min) (point-max)
nil)))))
(crdt--refresh-buffers-maybe)
(crdt--refresh-sessions-maybe))
- (message "Only server can add new buffer.")))
+ (error "Only server can add new buffer")))
-(defsubst crdt--get-session-names ()
- (mapcar (lambda (s)
- (with-current-buffer s crdt--session-name))
- crdt--session-list))
+(defsubst crdt--get-session-names (server)
+ (let (session-names)
+ (dolist (status-buffer crdt--session-list)
+ (with-current-buffer status-buffer
+ (when (eq (crdt--server-p) server)
+ (push crdt--session-name session-names))))
+ (nreverse session-names)))
(defsubst crdt--get-session (name)
(cl-find name crdt--session-list
@@ -1242,8 +1258,8 @@ If SESSION-NAME is empty, use the buffer name of the
current buffer."
(progn
(when (and crdt-mode crdt--status-buffer)
(error "Current buffer is already shared in a CRDT session"))
- (list (let ((session-name (completing-read "Enter a session name (create
if not exist): "
- (crdt--get-session-names))))
+ (list (let ((session-name (completing-read "Choose a server session
(create if not exist): "
+ (crdt--get-session-names t))))
(unless (and session-name (> (length session-name) 0))
(setq session-name (buffer-name (current-buffer))))
session-name))))
@@ -1251,6 +1267,8 @@ If SESSION-NAME is empty, use the buffer name of the
current buffer."
(if session
(crdt--share-buffer (current-buffer) session)
(let ((port (read-from-minibuffer "Create new session on port (default
6530): " nil nil t nil "6530")))
+ (when (not (numberp port))
+ (error "Port must be a number"))
(crdt--share-buffer (current-buffer) (crdt-new-session port
session-name))))))
(defun crdt-stop-share-buffer ()
@@ -1304,16 +1322,15 @@ If SESSION-NAME is empty, use the buffer name of the
current buffer."
(push new-session crdt--session-list)
new-session))
-(defun crdt-stop-session (&optional session-name)
- "Stop sharing the current session."
- (interactive
- (list (completing-read "Choose a session (create if not exist): "
- (crdt--get-session-names) nil t
- (when crdt--status-buffer (crdt--session-name)))))
- (let ((status-buffer (if session-name
- (crdt--get-session session-name)
- crdt--status-buffer)))
- (with-current-buffer status-buffer
+(defun crdt--stop-session (status-buffer)
+ "Kill the session associated with STATUS-BUFFER.
+Disconnect if it's a client session, or stop serving if it's a server session."
+ (with-current-buffer status-buffer
+ (when (if (and crdt-confirm-stop-session
+ (crdt--server-p)
+ crdt--network-clients)
+ (yes-or-no-p "Stopping the session will disconnect every client,
proceed? ")
+ t)
(dolist (client crdt--network-clients)
(when (process-live-p client)
(delete-process client))
@@ -1331,13 +1348,42 @@ If SESSION-NAME is empty, use the buffer name of the
current buffer."
(delq status-buffer crdt--session-list))
(crdt--refresh-sessions-maybe)
(delete-process crdt--network-process)
- (message "Disconnected."))
- (kill-buffer status-buffer)))
+ (message "Disconnected.")
+ (kill-buffer status-buffer))))
+
+(defun crdt-stop-session (&optional session-name)
+ "Stop sharing the session with SESSION-NAME.
+If SESSION-NAME is nil, stop sharing the current session."
+ (interactive
+ (list (completing-read "Choose a server session: "
+ (crdt--get-session-names t) nil t
+ (when (and crdt--status-buffer (crdt--server-p))
+ (crdt--session-name)))))
+ (let ((status-buffer (if session-name
+ (crdt--get-session session-name)
+ crdt--status-buffer)))
+ (crdt--stop-session status-buffer)))
+
+(defun crdt-disconnect (&optional session-name)
+ (interactive
+ (list (completing-read "Choose a client session: "
+ (crdt--get-session-names nil) nil t
+ (when (and crdt--status-buffer (not
(crdt--server-p)))
+ (crdt--session-name)))))
+ (let ((status-buffer (if session-name
+ (crdt--get-session session-name)
+ crdt--status-buffer)))
+ (crdt--stop-session status-buffer)))
(defun crdt-connect (address port &optional name)
"Connect to a CRDT server running at ADDRESS:PORT.
Open a new buffer to display the shared content."
- (interactive "MAddress: \nnPort: ")
+ (interactive
+ (list (read-from-minibuffer "Address: ")
+ (let ((port (read-from-minibuffer "Port (default 6530): " nil nil t
nil "6530")))
+ (when (not (numberp port))
+ (error "Port must be a number"))
+ port)))
(unless name
(setq name (crdt--read-name)))
(setq crdt--status-buffer
- [elpa] externals/crdt 4f069d5 77/80: Do not use executable-find to find tuntox, (continued)
- [elpa] externals/crdt 4f069d5 77/80: Do not use executable-find to find tuntox, ELPA Syncer, 2021/08/28
- [elpa] externals/crdt 8be5ff7 78/80: Replace define-minor-mode positional arguments with keywords, ELPA Syncer, 2021/08/28
- [elpa] externals/crdt 8cbd0fd 80/80: bump version number, ELPA Syncer, 2021/08/28
- [elpa] externals/crdt eee7611 27/80: imaginary bug fix, ELPA Syncer, 2021/08/28
- [elpa] externals/crdt 47ca3a7 25/80: fix makefile, ELPA Syncer, 2021/08/28
- [elpa] externals/crdt b31d05a 42/80: remove status buffer hack section in HACKING.org, ELPA Syncer, 2021/08/28
- [elpa] externals/crdt 0608e11 48/80: add tuntox support, fix yank not clearing pseudo-region, ELPA Syncer, 2021/08/28
- [elpa] externals/crdt 5a9ab2d 54/80: support for comint (tested scheme-mode), ELPA Syncer, 2021/08/28
- [elpa] externals/crdt 9d39b42 55/80: quick hack for xscheme.el, ELPA Syncer, 2021/08/28
- [elpa] externals/crdt cb7b701 58/80: add client side recovery, better error message, ELPA Syncer, 2021/08/28
- [elpa] externals/crdt 973e761 21/80: Work on Jean's todo list,
ELPA Syncer <=
- [elpa] externals/crdt 810af7e 32/80: fix bug when beg/end are markers in crdt--*-change, ELPA Syncer, 2021/08/28
- [elpa] externals/crdt fbcb870 31/80: more consistent name, ELPA Syncer, 2021/08/28
- [elpa] externals/crdt 58ca0a6 34/80: documents, ELPA Syncer, 2021/08/28
- [elpa] externals/crdt 06a2f1a 46/80: added some docstrings, ELPA Syncer, 2021/08/28
- [elpa] externals/crdt 2dfff42 50/80: add license, ELPA Syncer, 2021/08/28
- [elpa] externals/crdt 1a08765 51/80: slightly cleanup protocol and doc, ELPA Syncer, 2021/08/28
- [elpa] externals/crdt efdafb9 52/80: fix bug on emacs 25, ELPA Syncer, 2021/08/28
- [elpa] externals/crdt 8457254 47/80: fix cursor movement when remote insert/delete, add URL parsing, ELPA Syncer, 2021/08/28
- [elpa] externals/crdt 817c265 49/80: fix document, ELPA Syncer, 2021/08/28
- [elpa] externals/crdt bdada96 57/80: Disconnect user command for the server, ELPA Syncer, 2021/08/28