#+PROPERTY: header-args:emacs-lisp :lexical t * Coorgi Client - A collaborative org document sync client. Coorgi stands for "cooperative org interchange", and intends to enable multiple users to sync org documents between one another. Possible uses include issue trackers, shared agendas, and near real-time collaboration. ** Start hacking on this literate program Assuming you're using Emacs, this project is written as literate code. This document /is/ the program, written for humans first. Some things to ensure your environment is ready to go: - enable emacs lisp and org as org-babel languages #+begin_src emacs-lisp (org-babel-do-load-languages 'org-babel-load-languages '((emacs-lisp . t) (org . t))) #+end_src - install the ~annotate~ package (optional) #+begin_src emacs-lisp (package-install 'annotate) #+end_src We're experimenting with using ~annotate-mode~ to comment on the document. The ~.dir-locals~ file here will set the annotate database file to the ~annotations~ file in this repository. ** Overview The following block is the source of [[coorgi-client.el]]. It wraps all the relevant code blocks into sections and is tangled into the resulting source file. If you evaluate the block, it will effectively "eval-buffer" the whole coorgi-client package, as it currently exists in this file. #+begin_src emacs-lisp :tangle yes :noweb no-export :results silent ;;; coorgi-client.el --- A collaborative org document sync client. -*- lexical-binding: t; -*- <> <> <> <> <> <> <> ;;; coorgi-client.el ends here #+end_src * Coorgifying an Org-mode document ** Parsing, modifying, and re-inserting org data ~org-element-parse-buffer~ will turn any org document into a parsed data tree. ~org-element-interpret-data~ will turn any (valid) parsed org-data structure back into a string. therefore, we /should/ be able to act upon the parsed data with Coorgi functions to update the document, rather than mucking with live buffer edits and updates. *** TODO example proof of concept For example: #+begin_src emacs-lisp :var org-doc=trivial-example() :results verbatim (let ((subject (with-temp-buffer (insert org-doc) (org-element-parse-buffer)))) (org-element-interpret-data subject)) #+end_src ** defvar coorgi-properties Coorgi utilizes org-mode's headline properties in order to track and sync changes between collaborators. They also make it easier to find and update the various elements within an org document. ~coorgi-properties~ holds a list of keyword symbols which correspond to properties defined in a headline's property drawer. They are upcased to match the way ~org-element-parse-buffer~ returns them in a headline's data structure. #+begin_src emacs-lisp :noweb-ref defvars :results silent (defvar coorgi-properties '(:COORGI-KEY :COORGI-LAST-MODIFIED :COORGI-LAST-MODIFIED-BY :COORGI-VERSION) "List of properties the Coorgi server cares about.") #+end_src ** defun coorgify-at-point Converts the current org outline section into a coorgi-node data structure. *** Implementation #+name: coorgify-at-point #+begin_src emacs-lisp :results none (defun coorgify-at-point () (interactive) (save-excursion (let* ((current-element (org-element-at-point)) (entry (if (not (equal (car current-element) 'heading)) (progn (org-up-heading-safe) (org-element-at-point)) current-element)) (parsed (print (org-element-parse-buffer))) (props (org-element--get-node-properties)) (non-coorgi-props (cl-loop for (key value) on props by #'cddr unless (member key coorgi-properties) append (list key value))) (content-bounds (cons (progn (org-down-element) (org-forward-element) (point)) (plist-get (cadr entry) :contents-end)))) `( type entry headline ,(plist-get (cadr entry) :title) version ,(string-to-number (plist-get props :COORGI-VERSION)) last-modified-by ,(plist-get props :COORGI-LAST-MODIFIED-BY) last-modified ,(plist-get props :COORGI-LAST-MODIFIED) key ,(plist-get props :COORGI-KEY) properties ,non-coorgi-props contents ,(buffer-substring-no-properties (car content-bounds) (cdr content-bounds)))))) #+end_src *** Testing **** Trivial Org Example Given this org document: #+name: trivial-example #+begin_src org ,* MOO :PROPERTIES: :coorgi-version: 1 :coorgi-last-modified-by: colin/123 :coorgi-last-modified: 123 :coorgi-key: 101 :foo: bar :END: hello #+end_src ~coorgify-at-point~ should return something like this: #+name: trivial-expected-value #+begin_src emacs-lisp '(type entry headline "MOO" version 1 last-modified-by "colin/123" last-modified "123" key "101" properties (:FOO "bar") content " hello " children ()) #+end_src No matter where the cursor is in the buffer (there's only one coorgi node) ***** TODO When the cursor is in the body of the heading #+name: test-trivial-example-cursor-in-body #+begin_src emacs-lisp :var org-doc=trivial-example() expected=trivial-expected-value :results code (let ((subject (with-temp-buffer (insert org-doc) (coorgify-at-point)))) (if (not (equal subject expected)) `((actual ,subject) (expected ,expected)) "PASS")) #+end_src #+RESULTS: test-trivial-example-cursor-in-body #+begin_example ,* MOO :PROPERTIES: :coorgi-version: 1 :coorgi-last-modified-by: colin/123 :coorgi-last-modified: 123 :coorgi-key: 101 :foo: bar :END: hello #+end_example ***** TODO When the cursor at the top of the document #+name: test-trivial-example-cursor-at-top #+begin_src emacs-lisp :var org-doc=trivial-example() expected=trivial-expected-value :results code (let ((subject (with-temp-buffer (insert org-doc) (beginning-of-buffer) (coorgify-at-point)))) (if (not (equal subject expected)) `((actual ,subject) (expected ,expected)) "PASS")) #+end_src #+RESULTS: test-trivial-example-cursor-at-top #+begin_src emacs-lisp ((actual (type entry headline "MOO" version 1 last-modified-by "colin/123" last-modified "123" key "101" properties (:FOO "bar"))) (expected (type entry headline "MOO" version 1 last-modified-by "colin/123" last-modified "123" key "101" properties (:FOO "bar") content "\n\nhello\n\n" children nil))) #+end_src **** Basic Org Example #+name: basic-example #+begin_src org ,* MOO :PROPERTIES: :coorgi-version: 211 :coorgi-last-modified-by: “colin/1231234123” :coorgi-last-modified: 123124331 :coorgi-key: “1010101010101” :foo: “bar” :END: hello ,** TODO [#B] Cow :foobar: :PROPERTIES: :coorgi-version: 32 :coorgi-last-modified-by: “colin/1231234123” :coorgi-last-modified: 123124331 :coorgi-key: “123sdfas3wasd” :foo: “bar” :END: Cows say moo. CURSORPOSITION ,*** Hunky a cow ,*** Dory another cow #+end_src ** defun coorgi--find-headline We need to used the parsed version of the org buffer to look up the elements we're turning into coorgi nodes and get the relevant data to pack it up recursively. #+name: coorgi--find-headline #+begin_src emacs-lisp :results none (defun coorgi--find-headline (headline org-data) (car (org-element-map org-data 'headline (lambda (hl) (when (string-equal (org-element-property :raw-value hl) (org-element-property :raw-value headline)) hl))))) #+end_src #+begin_src emacs-lisp :var org-doc=trivial-example() :results code (let ((org-data (with-temp-buffer (insert org-doc) (org-element-parse-buffer)))) (coorgi--find-headline '(headline (:raw-value "MOO")) org-data)) #+end_src #+RESULTS: #+begin_src emacs-lisp (headline (:raw-value "MOO" :begin 1 :end 142 :pre-blank 0 :contents-begin 7 :contents-end 142 :level 1 :priority nil :tags nil :todo-keyword nil :todo-type nil :post-blank 0 :footnote-section-p nil :archivedp nil :commentedp nil :post-affiliated 1 :FOO "bar" :COORGI-KEY "101" :COORGI-LAST-MODIFIED "123" :COORGI-LAST-MODIFIED-BY "colin/123" :COORGI-VERSION "1" :title (#("MOO" 0 3 (:parent #0))) :parent (org-data nil #0)) (section (:begin 7 :end 142 :contents-begin 7 :contents-end 142 :post-blank 0 :post-affiliated 7 :parent #0) (property-drawer (:begin 7 :end 136 :contents-begin 20 :contents-end 129 :post-blank 1 :post-affiliated 7 :parent #1) (node-property (:key "coorgi-version" :value "1" :begin 20 :end 39 :post-blank 0 :post-affiliated 20 :parent #2)) (node-property (:key "coorgi-last-modified-by" :value "colin/123" :begin 39 :end 75 :post-blank 0 :post-affiliated 39 :parent #2)) (node-property (:key "coorgi-last-modified" :value "123" :begin 75 :end 102 :post-blank 0 :post-affiliated 75 :parent #2)) (node-property (:key "coorgi-key" :value "101" :begin 102 :end 119 :post-blank 0 :post-affiliated 102 :parent #2)) (node-property (:key "foo" :value "bar" :begin 119 :end 129 :post-blank 0 :post-affiliated 119 :parent #2))) (paragraph (:begin 136 :end 142 :contents-begin 136 :contents-end 142 :post-blank 0 :post-affiliated 136 :parent #1) #("hello\n" 0 6 (:parent #2))))) #+end_src ** defun coorgi--find-property-drawer It is useful to know whether or not a headline has a property drawer. If not, we know that Coorgi doesn't know about it yet. If so, we need to parse the existing properties into the coorgified entry data structure. In addition, we get more information about the org document's structure as it gets parsed into its various elements. #+name: coorgi--find-property-drawer #+begin_src emacs-lisp :results none (defun coorgi--find-property-drawer (headline) (caddr (caddr headline))) #+end_src *** tests #+begin_src emacs-lisp :var org-doc=trivial-example() (let* ((org-data (with-temp-buffer (insert org-doc) (org-element-parse-buffer))) (headline (coorgi--find-headline '(headline (:raw-value "MOO")) org-data))) (when (equal (car (coorgi--find-property-drawer headline)) 'property-drawer) "PASS")) #+end_src #+RESULTS: : PASS * Syncing Coorgi documents with collaborators ** TODO ~coorgi-initialize~ creates the document on the server ** TODO Coorgi syncs on the ~after-save-hook~ ** TODO Coorgi syncs on buffer change if its been idle X time * Copyright #+begin_src emacs-lisp :noweb-ref copyright ;; Copyright (C) 2022 Grant Shangreaux ;; Author: Grant Shangreaux ;; Maintainer: Grant Shangreaux ;; Created: 11 Nov 2022 ;; Keywords: org sync collaborative ;; URL: https://cicadas.surf/cgit/coorgi-client.git #+end_src * License ** GNU General Public License version 3 #+begin_src emacs-lisp :noweb-ref license ;; This file is not part of GNU Emacs. ;; This file 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. ;; ;; GNU Emacs 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 file. If not, see . #+end_src * Dependencies #+begin_src emacs-lisp :results silent :noweb-ref dependencies (require 'cl-lib) #+end_src