diff --git a/.dir-locals.el b/.dir-locals.el index dcf681b..e3a7aed 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -1,6 +1,13 @@ -;;; Directory Local Variables -;;; For more information see (info "(emacs) Directory Variables") - ((emacs-lisp-mode - (eval . (require 'org-roam-dev)) - (eval . (org-roam-dev-mode)))) + (fill-column . 110) + (indent-tabs-mode . nil) + (elisp-lint-ignored-validators . ("byte-compile" "package-lint")) + (elisp-lint-indent-specs . ((describe . 1) + (it . 1) + (org-element-map . defun) + (org-roam-with-temp-buffer . 1) + (org-with-point-at . 1) + (magit-insert-section . defun) + (magit-section-case . 0) + (->> . 1) + (org-roam-with-file . 2))))) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9e71d88..1418daa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,39 +34,28 @@ jobs: build: runs-on: ubuntu-latest strategy: - fail-fast: false matrix: emacs_version: + - 27.1 - snapshot steps: - - uses: purcell/setup-emacs@master - with: - version: ${{ matrix.emacs_version }} + - uses: purcell/setup-emacs@master + with: + version: ${{ matrix.emacs_version }} - - uses: actions/checkout@v2 + - uses: actions/checkout@v2 - - name: Create Sandbox Directory - run: | - SANDBOX_DIR=$(mktemp -d) || exit 1 - echo "SANDBOX_DIR=$SANDBOX_DIR" >> $GITHUB_ENV + - name: Install Eldev + run: curl -fsSL https://raw.github.com/doublep/eldev/master/webinstall/github-eldev | sh - - name: Initialize Sandbox - run: | - ./makem.sh -vv --sandbox $SANDBOX_DIR --install-deps --install-linters + - name: Install dependencies + run: make prepare - # The "all" rule is not used, because it treats compilation warnings - # as failures, so linting and testing are run as separate steps. - - # org-roam-compat is excluded from linting because it contains - # symbols/aliases from other packages - - name: Lint - continue-on-error: false - run: ./makem.sh -vv --sandbox $SANDBOX_DIR --exclude org-roam-compat.el lint - - - name: Test - if: always() # Run test even if linting fails. - run: ./makem.sh -vv --sandbox $SANDBOX_DIR test + - name: Lint + run: make lint + - name: Test + run: make test # Local Variables: # eval: (outline-minor-mode) # End: diff --git a/.gitignore b/.gitignore index dc330ab..c2df335 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ /doc/stats/ /config.mk /doc/manual.html +/.eldev/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 36e9a72..998140b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,21 @@ # Changelog + ## 1.2.4 (TBD) ### Added + - [#1396](https://github.com/org-roam/org-roam/pull/1396) add option to choose between prepending, appending, and omitting `roam_tags` in file completion - [#1270](https://github.com/org-roam/org-roam/pull/1270) capture: create OLP if it does not exist. Removes need for OLP setup in `:head`. - [#1353](https://github.com/org-roam/org-roam/pull/1353) support file-level property drawers ### Changed + - [#1352](https://github.com/org-roam/org-roam/pull/1352) prefer lower-case for roam_tag and roam_alias in interactive commands - [#1513](https://github.com/org-roam/org-roam/pull/1513) replaced hardcoded "svg" with defcustom org-roam-graph-filetype - [#1540](https://github.com/org-roam/org-roam/pull/1540) allow `roam_tag` and `roam_alias` to be specified on multiple lines ### Fixed + - [#1281](https://github.com/org-roam/org-roam/pull/1281) fixed idle-timer not instantiated on `org-roam-mode` - [#1308](https://github.com/org-roam/org-roam/pull/1308) fixed file renames corrupting database - [#1325](https://github.com/org-roam/org-roam/pull/1325) make titles and tags extracted unique per note @@ -20,7 +24,7 @@ - [#1347](https://github.com/org-roam/org-roam/pull/1347) allow use of `%a` element in regular Org-roam captures - [#1352](https://github.com/org-roam/org-roam/pull/1352) fixed org-roam-{tag/alias}-{add/delete} altering the original case of the Org property - [#1374](https://github.com/org-roam/org-roam/pull/1374) fix headline completions erroring out -- [#1375](https://github.com/org-roam/org-roam/pull/1375) fix org-roam-protocol to use existing ref file +- [#1375](https://github.com/org-roam/org-roam/pull/1375) fix org-roam-protocol to use existing ref file - [#1403](https://github.com/org-roam/org-roam/issues/1403) fixed inconsistency between how we write and read props like alias and tags - [#1409](https://github.com/org-roam/org-roam/issues/1398) prevent inclusion of non-org-roam files in `org-roam-dailies--list-files` - [#1542](https://github.com/org-roam/org-roam/issues/1542) fix files not excluded when `org-roam-list-files-commands` is nil @@ -32,6 +36,7 @@ Primarily a stabilization and bug-fix release. Org-roam-dailies has also been revamped to include new features, see [this video](https://www.youtube.com/watch?v=1q9x2aZCJJ4) for a quick overview. ### Added + - [#978](https://github.com/org-roam/org-roam/pull/978) Revamp org-roam-dailies - [#1183](https://github.com/org-roam/org-roam/pull/1183) Interactive functions for managing aliases and tags in Org-roam file, namely `org-roam-alias-add`, `org-roam-alias-delete`, `org-roam-tag-add`, and `org-roam-tag-delete`. - [#1215](https://github.com/org-roam/org-roam/pull/1215) Multiple `ROAM_KEY` keywords can now be specified in one file. This allows bibliographical entries to share the same note file. @@ -40,9 +45,11 @@ Org-roam-dailies has also been revamped to include new features, see [this video - [#1264](https://github.com/org-roam/org-roam/pull/1264) add `org-roam-db-update-method` to control when the cache is rebuilt. ### Changed + - [#1264](https://github.com/org-roam/org-roam/pull/1264) renamed `org-roam-update-db-idle-seconds` to `org-roam-db-idle-idle-seconds` ### Fixed + - [#1074](https://github.com/org-roam/org-roam/issues/1074) fix `org-roam--extract-links` to handle content boundaries. - [#1193](https://github.com/org-roam/org-roam/issues/1193) fix `org-roam-db-build-cache` by not killing temporary buffer in `org-roam--extract-links`. - [#1195](https://github.com/org-roam/org-roam/issues/1195) fix ID face showing as invalid if within Org ID files, but not Org-roam's. diff --git a/Eldev b/Eldev new file mode 100644 index 0000000..338ef96 --- /dev/null +++ b/Eldev @@ -0,0 +1,29 @@ +; -*- mode: emacs-lisp; lexical-binding: t; no-byte-compile: t -*- + +;; explicitly set main file +(setf eldev-project-main-file "org-roam.el") + +(eldev-use-package-archive 'gnu) +(eldev-use-package-archive 'melpa-unstable) + +;; allow to load test helpers +(eldev-add-loading-roots 'test "test/utils") + +;; Tell checkdoc not to demand two spaces after a period. +(setq sentence-end-double-space nil) + +(setf eldev-lint-default '(elisp)) +(setf eldev-standard-excludes `(:or ,eldev-standard-excludes "org-roam-macs.el")) + +(with-eval-after-load 'elisp-lint + ;; We will byte-compile with Eldev. + (setf elisp-lint-ignored-validators '("package-lint" "fill-column") + enable-local-variables :all)) + +;; Teach linter how to properly indent emacsql vectors +(eldev-add-extra-dependencies 'lint 'emacsql) +(add-hook 'eldev-lint-hook + (lambda () + (eldev-load-project-dependencies 'lint nil t) + (require 'emacsql) + (call-interactively #'emacsql-fix-vector-indentation))) diff --git a/Makefile b/Makefile index cc0e573..9ebb858 100644 --- a/Makefile +++ b/Makefile @@ -1,70 +1,29 @@ -# * makem.sh/Makefile --- Script to aid building and testing Emacs Lisp packages +.PHONY: clean +clean: + eldev clean all -# This Makefile is from the makem.sh repo: . +.PHONY: prepare +prepare: + eldev -C --unstable -p -dtT prepare -# * Arguments +.PHONY: lint +lint: + eldev -C --unstable -T lint -# For consistency, we use only var=val options, not hyphen-prefixed options. - -# NOTE: I don't like duplicating the arguments here and in makem.sh, -# but I haven't been able to find a way to pass arguments which -# conflict with Make's own arguments through Make to the script. -# Using -- doesn't seem to do it. - -ifdef install-deps - INSTALL_DEPS = "--install-deps" -endif -ifdef install-linters - INSTALL_LINTERS = "--install-linters" -endif - -ifdef sandbox - ifeq ($(sandbox), t) - SANDBOX = --sandbox - else - SANDBOX = --sandbox $(sandbox) - endif -endif - -ifdef debug - DEBUG = "--debug" -endif - -# ** Verbosity - -# Since the "-v" in "make -v" gets intercepted by Make itself, we have -# to use a variable. - -verbose = $(v) - -ifneq (,$(findstring vv,$(verbose))) - VERBOSE = "-vv" -else ifneq (,$(findstring v,$(verbose))) - VERBOSE = "-v" -endif - -# * Rules - -# TODO: Handle cases in which "test" or "tests" are called and a -# directory by that name exists, which can confuse Make. - -%: - @./makem.sh $(DEBUG) $(VERBOSE) $(SANDBOX) $(INSTALL_DEPS) $(INSTALL_LINTERS) $(@) - -.DEFAULT: init -init: - @./makem.sh $(DEBUG) $(VERBOSE) $(SANDBOX) $(INSTALL_DEPS) $(INSTALL_LINTERS) +.PHONY: test +test: + eldev -C --unstable -T test docs: - @$(MAKE) -C doc all + make -C doc all html: - @$(MAKE) -C doc html-dir + make -C doc html-dir install: install-docs install-docs: docs - @$(MAKE) -C doc install-docs + make -C doc install-docs install-info: info - @$(MAKE) -C doc install-info + make -C doc install-info diff --git a/README.md b/README.md index 4c5422c..6ca5a14 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Org-roam [![GitHub Release][release-badge]][release] [![MELPA][melpa-badge]][melpa] [![License GPL 3][gpl3-badge]][gpl3] - + Org-roam Logo Org-roam is a plain-text knowledge management system. It brings some of @@ -44,17 +44,19 @@ Here's a sample configuration with `use-package`: ```emacs-lisp (use-package org-roam :ensure t - :hook - (after-init . org-roam-mode) :custom (org-roam-directory (file-truename "/path/to/org-files/")) - :bind (:map org-roam-mode-map - (("C-c n l" . org-roam) - ("C-c n f" . org-roam-find-file) - ("C-c n g" . org-roam-graph)) - :map org-mode-map - (("C-c n i" . org-roam-insert)) - (("C-c n I" . org-roam-insert-immediate)))) + :bind (("C-c n l" . org-roam-buffer-toggle) + ("C-c n f" . org-roam-node-find) + ("C-c n g" . org-roam-graph) + ("C-c n i" . org-roam-node-insert) + ("C-c n c" . org-roam-capture) + ;; Dailies + ("C-c n j" . org-roam-dailies-capture-today)) + :config + (org-roam-setup) + ;; If using org-roam-protocol + (require 'org-roam-protocol)) ``` The `file-truename` function is only necessary when you use symbolic links diff --git a/doc/org-roam.org b/doc/org-roam.org index 109abfa..eaf6589 100644 --- a/doc/org-roam.org +++ b/doc/org-roam.org @@ -1,23 +1,23 @@ #+title: Org-roam User Manual #+author: Jethro Kuan #+email: jethrokuan95@gmail.com -#+date: 2020-2020 +#+date: 2020-2021 #+language: en #+texinfo_deffn: t #+texinfo_dir_category: Emacs #+texinfo_dir_title: Org-roam: (org-roam). -#+texinfo_dir_desc: Rudimentary Roam Replica for Emacs. -#+subtitle: for version 1.2.3 +#+texinfo_dir_desc: Roam Research for Emacs. +#+subtitle: for version 2.0.0 #+options: H:4 num:3 toc:nil creator:t ':t #+property: header-args :eval never #+texinfo: @noindent -This manual is for Org-roam version 1.2.3. +This manual is for Org-roam version 2.0.0. #+BEGIN_QUOTE -Copyright (C) 2020-2020 Jethro Kuan +Copyright (C) 2020-2021 Jethro Kuan You can redistribute this document and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either @@ -31,26 +31,22 @@ General Public License for more details. * Introduction -Org-roam is a tool for network thought. It reproduces some of [[https://roamresearch.com/][Roam -Research's]] [fn:roam] features within the all-powerful [[https://orgmode.org/][Org-mode]]. +Org-roam is a tool for networked thought. It reproduces some of [[https://roamresearch.com/][Roam +Research's]] [fn:roam] key features within [[https://orgmode.org/][Org-mode]]. -Org-roam is a solution for effortless non-hierarchical note-taking with -Org-mode. With Org-roam, notes flow naturally, making note-taking fun and easy. -Org-roam keeps closely to Org syntax, and will work for anyone already using -Org-mode for their personal wiki. +Org-roam allows for effortless non-hierarchical note-taking: with Org-roam, +notes flow naturally, making note-taking fun and easy. Org-roam augments the +Org-mode syntax, and will work for anyone already using Org-mode for their +personal wiki. -Org-roam gains its superpowers by leveraging the mature ecosystem around -Org-mode. For example, it has first-class support for [[https://github.com/jkitchin/org-ref][org-ref]] for citation -management. +Org-roam leverages the mature ecosystem around Org-mode. For example, it has +first-class support for [[https://github.com/jkitchin/org-ref][org-ref]] for citation management, and is able to +piggyback off Org's excellent LaTeX and source-block evaluation capabilities. -Org-roam aims to implement the core features of Roam, leveraging the -mature ecosystem around Org-mode where possible. Eventually, we hope -to further introduce features enabled by the Emacs ecosystem. +Org-roam provides these benefits over other tooling: -Org-roam provides several benefits over other tooling: - -- *Privacy and Security:* Keep your personal wiki entirely offline and in your - control. Encrypt your notes with GPG. +- *Privacy and Security:* Your personal wiki belongs only to you, entirely + offline and in your control. Encrypt your notes with GPG. - *Longevity of Plain Text:* Unlike web solutions like Roam Research, the notes are first and foremost plain Org-mode files -- Org-roam simply builds an auxiliary database to give the personal wiki superpowers. Having your notes @@ -60,28 +56,28 @@ Org-roam provides several benefits over other tooling: - *Free and Open Source:* Org-roam is free and open-source, which means that if you feel unhappy with any part of Org-roam, you may choose to extend Org-roam, or open a pull request. -- *Leverage the Org-mode ecosystem:* Over the years, Emacs and Org-mode has +- *Leverage the Org-mode ecosystem:* Over the decades, Emacs and Org-mode has developed into a mature system for plain-text organization. Building upon Org-mode already puts Org-roam light-years ahead of many other solutions. -- *Built on Emacs:* Emacs is also a fantastic interface for editing text, and we - can inherit many of the powerful text-navigation and editing packages +- *Built on Emacs:* Emacs is also a fantastic interface for editing text, and + Org-roam inherits many of the powerful text-navigation and editing packages available to Emacs. * Target Audience Org-roam is a tool that will appear unfriendly to anyone unfamiliar with Emacs -and Org-mode, but is also extremely powerful to those willing to put effort in -mastering the intricacies of the tools. Org-roam stands on the shoulders on -giants. Emacs was first created in 1976, and remains a top tier tool for editing +and Org-mode, but it is also extremely powerful to those willing to put effort +inn mastering the intricacies. Org-roam stands on the shoulders of giants. Emacs +was first created in 1976, and remains the tool of choice for many for editing text and designing textual interfaces. The malleability of Emacs allowed the creation of Org-mode, an all-purpose plain-text system for maintaining TODO lists, planning projects, and authoring documents. Both of these tools are incredibly vast and require significant time investment to master. -Org-roam assumes basic familiarity with these tools. It is not difficult to get -up and running with basic text-editing functionality, but one will only fully -appreciate the power of building Roam functionality into Emacs and Org-mode when -the usage of these tools become more advanced. +Org-roam assumes only basic familiarity with these tools. It is not difficult to +get up and running with basic text-editing functionality, but one will only +fully appreciate the power of building Roam functionality into Emacs and +Org-mode when the usage of these tools become more advanced. One key advantage to Org-roam is that building on top of Emacs gives it malleability. This is especially important for note-taking workflows. It is our @@ -132,7 +128,7 @@ plain-text, Org-mode file. In the same way one would maintain a paper slip-box, Org-roam makes it easy to create new zettels, pre-filling boilerplate content using a powerful templating system. -** Fleeting notes +*Fleeting notes* A slip-box requires a method for quickly capturing ideas. These are called *fleeting notes*: they are simple reminders of information or ideas that will @@ -141,7 +137,7 @@ need to be processed later on, or trashed. This is typically accomplished using functionality (see [[*Daily-notes][Daily-notes]]). This provides a central inbox for collecting thoughts, to be processed later into permanent notes. -** Permanent notes +*Permanent notes* Permanent notes are further split into two categories: *literature notes* and *concept notes*. Literature notes can be brief annotations on a particular @@ -150,6 +146,9 @@ Concept notes require much more care in authoring: they need to be self-explanatory and detailed. Org-roam's templating system supports the addition of different templates to facilitate the creation of these notes. +For further reading on the Zettelkasten method, "How to Take Smart Notes" by +Sonke Ahrens is a decent guide. + * Installation Org-roam can be installed using Emacs' package manager or manually from its @@ -212,7 +211,7 @@ using Apt: Org-roam will then be autoloaded into Emacs. -** Installing from the Git Repository +** Installing from Source You may install Org-roam directly from the repository on [[https://github.com/org-roam/org-roam][GitHub]] if you like. This will give you access to the latest version hours or days before it appears @@ -294,40 +293,71 @@ install-info /path/to/my/info/files/org-roam.info /path/to/my/info/files/dir ** Post-Installation Tasks -Org-roam uses ~emacsql-sqlite3~, which requires ~sqlite3~ to be located on -~exec-path~. Please ensure that ~sqlite3~ is installed appropriately on your -operating system. You can verify that this is the case by executing: +Org-roam requires ~sqlite3~ to be locatable by Emacs (i.e. on ~exec-path~). +Please ensure that ~sqlite3~ is installed appropriately on your operating +system. You can verify that this is the case by executing [fn:2]: #+BEGIN_SRC emacs-lisp (executable-find "sqlite3") #+END_SRC If you have ~sqlite3~ installed, and ~executable-find~ still reports ~nil~, then -it is likely that the path to the executable is not a member of the Emacs -variable ~exec-path~. You may rectify this by manually adding the path within -your Emacs configuration: +the path to the executable is not a member of the Emacs variable ~exec-path~. +Rectify this by manually adding the path within your Emacs configuration: #+BEGIN_SRC emacs-lisp (add-to-list 'exec-path "path/to/sqlite3") #+END_SRC * Getting Started +** The Org-roam Node -This short tutorial describes the essential commands used in Org-roam, to help -you get started. +We first begin with some terminology we'll use throughout the manual. We term +the basic denomination in Org-roam a node. We define a node as follows: -First, it is important to understand how Org-roam was designed. Org-roam was -built to support a workflow that was not possible with vanilla Org-mode. This -flow is modelled after the [[https://zettelkasten.de/][Zettelkasten Method]], and many of [[https://roamresearch.com][Roam Research's]] -workflows. Org-roam does not magically make note-taking better -- this often -requires a radical change in your current note-taking workflow. To understand -more about the methods and madness, see [[*Note-taking Workflows][Note-taking Workflows]]. +#+begin_quote +A node is any headline or top level file with an ID. +#+end_quote -To first start using Org-roam, one needs to pick a location to store the -Org-roam files. The directory that will contain your notes is specified by the -variable ~org-roam-directory~. This variable needs to be set before any calls to -Org-roam functions, including enabling ~org-roam-mode~. For this tutorial, -create an empty directory, and set ~org-roam-directory~: +For example, with this example file content: + +#+begin_src org + :PROPERTIES: + :ID: foo + :END: + ,#+title: Foo + + ,* Bar + :PROPERTIES: + :ID: bar + :END: +#+end_src + +We create two nodes: + +1. A file node "Foo" with id ~foo~. +2. A headline node "Bar" with id ~bar~. + +Headlines without IDs will not be considered Org-roam nodes. Org IDs can be +added to files or headlines via the interactive command ~M-x org-id-get-create~. + +** Links between Nodes + +We link between nodes using Org's standard ID link (e.g. ~id:foo~). While only +ID links will be considered during the computation of links between nodes, +Org-roam caches all other links in the documents for external use. + +** Setting up Org-roam + +Org-roam's capabilities stem from its aggressive caching: it crawls all files +within ~org-roam-directory~, and maintains a cache of all links and nodes. + +To start using Org-roam, pick a location to store the Org-roam files. The +directory that will contain your notes is specified by the variable +~org-roam-directory~. Org-roam searches recursively within ~org-roam-directory~ +for notes. This variable needs to be set before any calls to Org-roam functions. + +For this tutorial, create an empty directory, and set ~org-roam-directory~: #+BEGIN_SRC emacs-lisp (make-directory "~/org-roam") @@ -337,725 +367,322 @@ create an empty directory, and set ~org-roam-directory~: The ~file-truename~ function is only necessary when you use symbolic links inside ~org-roam-directory~: Org-roam does not resolve symbolic links. -We encourage using a flat hierarchy for storing notes, but some prefer using -folders for storing specific kinds of notes (e.g. websites, papers). This is -fine; Org-roam searches recursively within ~org-roam-directory~ for notes. -Instead of relying on the file hierarchy for any form of categorization, one -should use links between files to establish connections between notes. +Next, we setup Org-roam to run functions on file changes to maintain cache +consistency. This is achieved by running ~M-x org-roam-setup~. To ensure that +Org-roam is available on startup, place this in your Emacs configuration: -Next, we need to enable the global minor mode ~org-roam-mode~. This sets up -Emacs with several hooks, building a cache that is kept consistent as your -slip-box grows. We recommend starting ~org-roam-mode~ on startup: +#+begin_src emacs-lisp + (require 'org-roam) + (org-roam-setup) +#+end_src -#+BEGIN_SRC emacs-lisp -(add-hook 'after-init-hook 'org-roam-mode) -#+END_SRC +To build the cache manually, run ~M-x org-roam-db-build-cache~. Cache builds may +take a while the first time, but subsequent builds are often instantaneous +because they only reprocess modified files. -To build the cache manually, one can run ~M-x org-roam-db-build-cache~. Cache -builds may take a while the first time, but is often instantaneous in subsequent -runs because it only reprocesses modified files. +** Customizing Node Caching -Let us now create our first note. Call ~M-x org-roam-find-file~. This shows a -list of titles for notes that reside in ~org-roam-directory~. It should show -nothing right now, since there are no notes in the directory. Entering the title -of the note you wish to create, and pressing ~RET~ should begin the note +By default, all nodes (any headline or file with an ID) are cached by Org-roam. +There are instances where you may want to have headlines with ID, but not have +them cached by Org-roam. + +To exclude a headline from the Org-roam database, set the ~ROAM_EXCLUDE~ +property to a non-nil value. For example: + +#+begin_src org +,* Foo + :PROPERTIES: + :ID: foo + :ROAM_EXCLUDE: t + :END: +#+end_src + +One can also set ~org-roam-db-node-include-function~. For example, to exclude +all headlines with the ~ATTACH~ tag from the Org-roam database, one can set: + +#+begin_src org +(setq org-roam-db-node-include-function + (lambda () + (not (member "ATTACH" (org-get-tags))))) +#+end_src + +** TODO Creating and Linking Nodes + +Org-roam makes it easy to create notes and link them together. There are 2 main +functions for creating nodes: + +- ~org-roam-node-insert~: creates a node if it does not exist, and inserts a + link to the node at point. +- ~org-roam-node-find~: creates a node if it does not exist, and visits the + node. +- ~org-roam-capture~: creates a node if it does not exist, and restores the + current window configuration upon completion. + +Let's first try ~org-roam-node-find~. Calling ~M-x org-roam-node-find~ will +show a list of titles for nodes that reside in ~org-roam-directory~. It should +show nothing right now, since there are no notes in the directory. Enter the +title of the note you wish to create, and press ~RET~. This begins the note creation process. This process uses ~org-capture~'s templating system, and can be customized (see [[*The Templating System][The Templating System]]). Using the default template, pressing ~C-c C-c~ finishes the note capture. -By default, Org-roam updates the cache asynchronously in the background to -avoid getting in the way of writing. Org-roam queues updates to the files, -waits for you to be idle for 2 seconds, and then automatically triggers -updating the cache. After the cache has been updated, running ~M-x -org-roam-find-file~ again should show the note you have created, and selecting -that entry will bring you to that note [fn:1]. One can customize the waiting -time by setting ~org-roam-db-update-idle-seconds~; or change the cache update -to be triggered immediately after buffer save by setting -~org-roam-db-update-method~ to ~'immediate~. +Now that we have a node, we can try inserting a link to the node using ~M-x +org-roam-node-insert~. This brings up the list of nodes, which should contain +the node you just created. Selecting the node will insert an ~id:~ link to the +node. If you instead entered a title that does not exist, you will once again be +brought through the node creation process. -For experienced ~org-capture~ users, the behavior of ~M-x org-roam-find-file~ -may seem unfamiliar: after finishing a capture with ~C-c C-c~, you are returned -not to the original buffer from which you called ~M-x org-roam-find-file~, but -to a buffer pointing to the note you just created. For the usual ~org-capture~ -behavior you can call ~M-x org-roam-capture~ instead of ~M-x org-roam-find-file~. +One can also conveniently insert links via the completion-at-point functions +Org-roam provides (see [[id:70083bfd-d1e3-42b9-bf83-5b05708791c0][Completion]]). -Org-roam makes it easy to create notes, and link them together. To link notes -together, we call ~M-x org-roam-insert~. This brings up a prompt with a list of -title for existing notes. Selecting an existing entry will create and insert a -link to the current file. Entering a non-existent title will create a new note -with that title. Good usage of Org-roam requires liberally linking files: this -facilitates building up a dense graph of inter-connected notes. +* The Org-roam Buffer -Org-roam provides an interface to view backlinks. It shows backlinks for the -currently active Org-roam note, along with some surrounding context. To toggle -the visibility of this buffer, call ~M-x org-roam~. +Org-roam provides the Org-roam buffer: an interface to view relationships with +other notes (backlinks, reference links, unlinked references etc.). There are +two main functions to use here: -For a visual representation of the notes and their connections, Org-roam also -provides graphing capabilities, using Graphviz. It generates graphs with notes -as nodes, and links between them as edges. The generated graph can be used to -navigate to the files, but this requires some additional setup (see [[*Roam -Protocol][Roam Protocol]]). +- ~org-roam-buffer~: Launch an Org-roam buffer for the current node at point. +- ~org-roam-buffer-toggle~: Launch an Org-roam buffer that tracks the node + currently at point. This means that the content of the buffer changes as the + point is moved, if necessary. -* Files -:PROPERTIES: -:ID: 3edec3e6-8e26-4a43-8a0a-bf204268bbb3 -:END: +Use ~org-roam-buffer-toggle~ when you want wish for the Org-roam buffer to +buffer, call ~M-x org-roam-buffer~. -In Org-roam, notes typically consist of multiple files, where each file is a -zettel. +- Function: org-roam-buffer -While the bulk of Org-roam's functionality is built on top of vanilla Org-mode, -Org-roam adds several Org-roam-specific keywords to support additional -functionality. + Launch an Org-roam buffer for the current node at point. -This section explains the important components of a file, and the extensions to -Org-mode. +To bring up a buffer that tracks the current node at point, call ~M-x +org-roam-buffer-toggle~. -** File Titles +- Function: org-roam-buffer-toggle -To easily find a note, a title needs to be prescribed to a note. + Toggle display of the ~org-roam-buffer~. -A note can have many titles: this allows a note to be referred to by different -names, which is especially useful for topics or concepts with acronyms. For -example, for a note like "World War 2", it may be desirable to also refer to it -using the acronym "WWII". +** Navigating the Org-roam Buffer -Org-roam calls ~org-roam--extract-titles~ to extract titles. It uses the -variable ~org-roam-title-sources~, to control how the titles are extracted. The -title extraction methods supported are: +The Org-roam buffer uses ~magit-section~, making the typical ~magit-section~ +keybindings available. Here are several of the more useful ones: -1. ~'title~: This extracts the title using the file ~#+title~ property -2. ~'headline~: This extracts the title from the first headline in the Org file -3. ~'alias~: This extracts a list of titles using the ~#+roam_alias~ property. - The aliases are space-delimited, and can be multi-worded using quotes. +- ~M-{N}~: ~magit-section-show-level-{N}-all~ +- ~n~: ~magit-section-forward~ +-~~: ~magit-section-toggle~ +- ~~: ~org-roam-visit-thing~ -Take for example the following org file: +~org-roam-visit-thing~ is a placeholder command, that is replaced by +section-specific commands such as ~org-roam-node-visit~. -#+BEGIN_SRC org - #+title: World War 2 - #+roam_alias: "WWII" "World War II" +** Configuring what is displayed in the buffer - * Headline -#+END_SRC +There are currently 3 provided widget types: -| Method | Titles | -|-------------+--------------------------| -| ~'title~ | '("World War 2") | -| ~'headline~ | '("Headline") | -| ~'alias~ | '("WWII" "World War II") | +- Backlinks :: View (preview of) nodes that link to this node +- Reference Links :: Nodes that reference this node (see [[id:57c1f991-be38-4fab-b27d-60227047f3b7][Refs]]) +- Unlinked references :: View nodes that contain text that match the nodes + title/alias but are not linked -If no title is provided, Org-roam defaults to using the file-path. +To configure what sections are displayed in the buffer, set ~org-roam-mode-sections~. -*** Customizing Title Extraction +#+begin_src emacs-lisp + (setq org-roam-mode-sections + (list #'org-roam-backlinks-section + #'org-roam-reflinks-section + ;; #'org-roam-unlinked-references-section + )) +#+end_src -To control how Org-roam extracts titles, customize ~org-roam-title-sources~. If -all methods of title extraction return no results, the file-name is used as the -note's title. +Note that computing unlinked references may be slow, and has not been added in by default. -- User Option: org-roam-title-sources +** Configuring the Org-roam buffer display - The list of sources from which to retrieve a note title. - Each element in the list is either: +Org-roam does not control how the pop-up buffer is displayed: this is left to +the user. The author's recommended configuration is as follows: -1. a symbol -- this symbol corresponds to a title retrieval function, which - returns the list of titles for the current buffer - 2. a list of symbols -- symbols in the list are treated as with (1). The - return value of this list is the first symbol in the list returning a - non-nil value. +#+begin_src emacs-lisp + (add-to-list 'display-buffer-alist + '(("\\*org-roam\\*" + (display-buffer-in-direction) + (direction . right) + (window-width . 0.33) + (window-height . fit-window-to-buffer)))) +#+end_src - The return results of the root list are concatenated. +Crucially, the window is a regular window (not a side-window), and this allows +for predictable navigation: - For example the setting: '((title headline) alias) means the following: +- ~RET~ navigates to thing-at-point in the current window, replacing the + Org-roam buffer. +- ~C-u RET~ navigates to thing-at-point in the other window. - 1. Return the 'title + 'alias, if the title of current buffer is non-empty; - 2. Or return 'headline + 'alias otherwise. +** TODO Styling the Org-roam buffer +* Node Properties +** Standard Org properties - The currently supported symbols are: +Org-roam caches most of the standard Org properties. The full list now includes: - ~'title~ - The ~#+title~ property of org file. +- outline level +- todo state +- priority +- scheduled +- deadline +- tags - ~'alias~ - The ~#+roam_alias~ property of the org file, using - space-delimited strings. +** Titles and Aliases - ~'headline~ - The first headline in the org file. +Each node has a single title. For file nodes, this is specified with the +`#+title` property for the file. For headline nodes, this is the main text. -Adding your own title extraction method requires two steps. First, define a -method ~(defun org-roam--extract-titles-foo () ...)~, where ~foo~ a -self-prescribed name for the title extraction method. This method takes no -arguments, and returns a list of strings (titles). Finally, push the symbol -~foo~ into ~org-roam-title-sources~. You may need to rebuild the cache from -scratch to re-process all files to pick up the new titles. +Nodes can also have multiple aliases. Aliases allow searching for nodes via an +alternative name. For example, one may want to assign a well-known acronym (AI) +to a node titled "Artificial Intelligence". -** File Tags +To assign an alias to a node, add the "ROAM_ALIASES" property to the node: -Tags are used as meta-data for files: they facilitate interactions with notes -where titles are insufficient. For example, tags allow for categorization of -notes: differentiating between bibliographical and structure notes during -interactive commands. +#+begin_src org + ,* Artificial Intelligence + :PROPERTIES: + :ROAM_ALIASES: AI + :END: +#+end_src -By default, tags are extracted from the ~#+roam_tags~ property. To add -additional extraction methods, see [[id:c986edba-9498-4af1-b033-c94b733d42c8][Customizing Tag Extraction]]. +Alternatively, Org-roam provides some functions to add or remove aliases. -*** Customizing Tag Extraction -:PROPERTIES: -:ID: c986edba-9498-4af1-b033-c94b733d42c8 -:END: +- Function: org-roam-alias-add alias -Org-roam calls ~org-roam--extract-tags~ to extract tags from files. The variable -~org-roam-tag-sources~, to control how tags are extracted. + Add ALIAS to the node at point. When called interactively, prompt for the + alias to add. -- User Option: org-roam-tag-sources +- Function: org-roam-alias-remove -Sources to obtain tags from. + Remove an alias from the node at point. -It should be a list of symbols representing any of the following extraction -methods: +** Tags - ~'prop~ - Extract tags from the ~#+roam_tags~ property. - Tags are space delimited. - Tags may contain spaces if they are double-quoted. - e.g. ~#+roam_tags: TAG "tag with spaces"~ +Tags for top-level (file) nodes are pulled from the variable ~org-file-tags~, +which is set by the ~#+filetags~ keyword, as well as other tags the file may +have inherited. Tags for headline level nodes are regular Org tags. - ~'vanilla~ - Extract vanilla org-mode tags, including ~#+FILETAGS~ and - inherited tags. +Note that the ~#+filetags~ keyword results in tags being inherited by headers +within the file. This makes it impossible for selective tag inheritance: i.e. +either tag inheritance is turned off, or all headline nodes will inherit the +tags from the file node. This is a design compromise of Org-roam. - ~'all-directories~ - Extract sub-directories relative to ~org-roam-directory~. - That is, if a file is located at relative path foo/bar/file.org, - the file will have tags "foo" and "bar". +** Refs - ~'last-directory~ - Extract the last directory relative to `org-roam-directory'. - That is, if a file is located at relative path foo/bar/file.org, - the file will have tag \"bar\". +Refs are unique identifiers for nodes. These keys allow references to the key to +show up in the Org-roam buffer. For example, a node for a website may use the URL +as the ref, and a node for a paper may use an Org-ref citation key. - ~'first-directory~ - Extract the first directory relative to ~org-roam-directory~. - That is, if a file is located at relative path foo/bar/file.org, - the file will have tag "foo" - -By default, only the ~'prop~ extraction method is enabled. To enable the other -extraction methods, you may modify ~org-roam-tag-sources~, for example: +To add a ref, add to the "ROAM_REFS" property as follows: -#+BEGIN_SRC emacs-lisp -(setq org-roam-tag-sources '(prop last-directory)) -#+END_SRC +#+begin_src org + ,* Google + :PROPERTIES: + :ROAM_REFS: https://www.google.com/ + :END: +#+end_src -Adding your own tag extraction method requires two steps. First, define a method -~(defun org-roam--extract-tags-foo (file) ...)~, where ~foo~ a self-prescribed -name for the tag extraction method. This method takes the file path as an -argument, and returns a list of strings (titles). Finally, push the symbol ~foo~ -into ~org-roam-tag-sources~. You may need to rebuild the cache from scratch to -re-process all files to pick up the new tags. - -** File Refs - -Refs are unique identifiers for files. For example, a note for a website may -contain a ref: - -#+BEGIN_SRC org - #+title: Google - #+roam_key: https://www.google.com/ -#+END_SRC - -These keys allow references to the key to show up in the backlinks buffer. For -instance, with the example above, if another file then links to -https://www.google.com, that will show up as a “Ref Backlink”. +With the above example, if another node links to https://www.google.com/, it +will show up as a “reference backlink”. These keys also come in useful for when taking website notes, using the ~roam-ref~ protocol (see [[*Roam Protocol][Roam Protocol]]). -[[https://github.com/jkitchin/org-ref][org-ref]] citation keys can also be used as refs: - -#+BEGIN_SRC org - #+title: Neural Ordinary Differential Equations - #+roam_key: cite:chen18_neural_ordin_differ_equat -#+END_SRC - -#+CAPTION: org-ref-citelink -[[file:images/org-ref-citelink.png]] - -You may assign multiple refs to a single file, for example when you want +You may assign multiple refs to a single node, for example when you want multiple papers in a series to share the same note, or an article has a citation key and a URL at the same time. -* The Templating System +Org-roam also provides some functions to add or remove refs. -Rather than creating blank files on ~org-roam-insert~ and ~org-roam-find-file~, -it may be desirable to prefill the file with templated content. This may -include: +- Function: org-roam-ref-add ref -- Time of creation -- File it was created from -- Clipboard content -- Any other data you may want to input manually + Add REF to the node at point. When called interactively, prompt for the + ref to add. -This requires a complex template insertion system. Fortunately, Org ships with a -powerful one: ~org-capture~ (see info:org#capture). However, org-capture was not -designed for such use. Org-roam abuses ~org-capture~, extending its syntax and -capabilities. To first understand how org-roam's templating system works, it may -be useful to look into basic usage of ~org-capture~. +- Function: org-roam-ref-remove -For these reasons, Org-roam capture templates are not compatible with regular -~org-capture~. Hence, Org-roam's templates can be customized by instead -modifying the variable ~org-roam-capture-templates~. Just like -~org-capture-templates~, ~org-roam-capture-templates~ can contain multiple -templates. If ~org-roam-capture-templates~ only contains one template, there -will be no prompt for template selection. + Remove a ref from the node at point. -** Template Walkthrough +* Completion +:PROPERTIES: +:ID: 70083bfd-d1e3-42b9-bf83-5b05708791c0 +:END: -To demonstrate the additions made to org-capture templates. Here, we walkthrough -the default template, reproduced below. +Completions for Org-roam are provided via ~completion-at-point~. Org-roam +currently provides completions in two scenarios: -#+BEGIN_SRC emacs-lisp - ("d" "default" plain (function org-roam--capture-get-point) - "%?" - :file-name "%<%Y%m%d%H%M%S>-${slug}" - :head "#+title: ${title}\n" - :unnarrowed t) -#+END_SRC +- When within an Org bracket link +- Anywhere -1. The template has short key ~"d"~. If you have only one template, org-roam - automatically chooses this template for you. -2. The template is given a description of ~"default"~. -3. ~plain~ text is inserted. Other options include Org headings via - ~entry~. -4. ~(function org-roam--capture-get-point)~ should not be changed. -5. ~"%?"~ is the template inserted on each call to ~org-roam-capture--capture~. - This template means don't insert any content, but place the cursor here. -6. ~:file-name~ is the file-name template for a new note, if it doesn't yet - exist. This creates a file at path that looks like - ~/path/to/org-roam-directory/20200213032037-foo.org~. This template also - allows you to specify if you want the note to go into a subdirectory. For - example, the template ~private/${slug}~ will create notes in - ~/path/to/org-roam-directory/private~. -7. ~:head~ contains the initial template to be inserted (once only), at - the beginning of the file. Here, the title global attribute is - inserted. -8. ~:unnarrowed t~ tells org-capture to show the contents for the whole - file, rather than narrowing to just the entry. +Completions are installed locally in all Org-roam files. To trigger completions, +call ~M-x completion-at-point~. If using ~company-mode~, add ~company-capf~ to +~company-backends~. -Other options you may want to learn about include ~:immediate-finish~. +Completions respect ~completion-styles~: the user is free to choose how +candidates are matched. An example of a completion style that has grown in +popularity is [[https://github.com/oantolin/orderless][orderless]]. -** Org-roam Template Expansion +** Completing within Link Brackets -Org-roam's template definitions also extend org-capture's template syntax, to -allow prefilling of strings. We have seen a glimpse of this in [[*Template Walkthrough][Template -Walkthrough]]. +Completions within link brackets are provided by +~org-roam-complete-link-at-point~. -In org-roam templates, the ~${var}~ syntax allows for the expansion of -variables, stored in ~org-roam-capture--info~. For example, during -~org-roam-insert~, the user is prompted for a title. Upon entering a -non-existent title, the ~title~ key in ~org-roam-capture--info~ is set to the -provided title. ~${title}~ is then expanded into the provided title during the -org-capture process. Any variables that do not contain strings, are prompted for -values using ~completing-read~. +The completion candidates are the titles and aliases for all Org-roam nodes. +Upon choosing a candidate, a ~roam:Title~ link will be inserted, linking to node +of choice. -After doing this expansion, the org-capture's template expansion system is used -to fill up the rest of the template. You may read up more on this on -[[https://orgmode.org/manual/Template-expansion.html#Template-expansion][org-capture's documentation page]]. +** Completing anywhere -To illustrate this dual expansion process, take for example the template string: -~"%<%Y%m%d%H%M%S>-${title}"~, with the title ~"Foo"~. The template is first -expanded into ~%<%Y%m%d%H%M%S>-Foo~. Then org-capture expands ~%<%Y%m%d%H%M%S>~ -with timestamp: e.g. ~20200213032037-Foo~. +The same completions can be triggered anywhere for the symbol at point if not +within a bracketed link. This is provided by ~org-roam-complete-everywhere~. +Similarly, the completion candidates are the titles and aliases for all Org-roam +nodes, and upon choosing a candidate a ~roam:Title~ link will be inserted +linking to the node of choice. -All of the flexibility afforded by Emacs and Org-mode are available. For -example, if you want to encode a UTC timestamp in the filename, you can take -advantage of org-mode's ~%(EXP)~ template expansion to call ~format-time-string~ -directly to provide its third argument to specify UTC. +This is disable by default. To enable it, set ~org-roam-completion-everywhere~ +to ~t~: -#+BEGIN_SRC emacs-lisp - ("d" "default" plain (function org-roam--capture-get-point) - "%?" - :file-name "%(format-time-string \"%Y-%m-%d--%H-%M-%SZ--${slug}\" (current-time) t)" - :head "#+title: ${title}\n" - :unnarrowed t) -#+END_SRC +#+begin_src emacs-lisp +(setq org-roam-completion-everywhere t) +#+end_src -* Concepts and Configuration -The number of configuration options is deliberately kept small, to keep the -Org-roam codebase manageable. However, we attempt to accommodate as many usage -styles as possible. +- Variable: org-roam-completion-everywhere -All of Org-roam's customization options can be viewed via ~M-x customize-group -org-roam~. - -** Directories and Files - -This section concerns the placement and creation of files. - -- Variable: org-roam-directory - - This is the default path to Org-roam files. All Org files, at any level of - nesting, are considered part of the Org-roam. - -- Variable: org-roam-db-location - - Location of the Org-roam database. If this is non-nil, the Org-roam sqlite - database is saved here. - - It is the user’s responsibility to set this correctly, especially when used - with multiple Org-roam instances. - -- Variable: org-roam-file-exclude-regexp - - Files matching this regular expression are excluded from the Org-roam. - -** The Org-roam Buffer - -The Org-roam buffer displays backlinks for the currently active Org-roam note. - -- User Option: org-roam-buffer - - The name of the org-roam buffer. Defaults to ~*org-roam*~. - -- User Option: org-roam-buffer-position - - The position of the Org-roam buffer side window. Valid values are ~'left~, - ~'right~, ~'top~, ~'bottom~. - -- User Option: org-roam-buffer-width - - Width of ~org-roam-buffer~. Has an effect only if ~org-roam-buffer-position~ is - ~'left~ or ~'right~. - -- User Option: org-roam-buffer-height - - Height of ~org-roam-buffer~. Has an effect only if ~org-roam-buffer-position~ is - ~'top~ or ~'bottom~. - -- User Option: org-roam-buffer-window-parameters - - Additional window parameters for the org-roam-buffer side window. - - For example one can prevent the window from being deleted when calling - ~delete-other-windows~, by setting it with the following: - - ~(setq org-roam-buffer-window-parameters '((no-delete-other-windows . t)))~ - -** Org-roam Files - -Org-roam files are created and prefilled using Org-roam's templating -system. The templating system is customizable (see [[*The Templating System][The Templating System]]). - -** Org-roam Faces - -Org-roam introduces several faces to distinguish links within the same buffer. -These faces are enabled by default in Org-roam notes. - -- User Option: org-roam-link-use-custom-faces - - When ~t~, use custom faces only inside Org-roam notes. - When ~everywhere~, the custom face is applied additionally to non Org-roam notes. - When ~nil~, do not use Org-roam's custom faces. - -The ~org-roam-link~ face is the face applied to links to other Org-roam files. -This distinguishes internal links from external links (e.g. external web links). - -The ~org-roam-link-current~ face corresponds to links to the same file it is in. - -The ~org-roam-link-invalid~ face is applied to links that are broken. These are -links to files or IDs that cannot be found. -** TODO The Database - -Org-roam is backed by a Sqlite database. - -- User Option: org-roam-db-update-method - - Method to update the Org-roam database. - - ~'immediate~: Update the database immediately upon file changes. - - ~'idle-timer~: Updates the database if dirty, if Emacs idles for - ~org-roam-db-update-idle-seconds~. - -- User Option: org-roam-db-update-idle-seconds - - Number of idle seconds before triggering an Org-roam database update. This is - only valid if ~org-roam-db-update-method~ is ~'idle-timer~. - -* Inserting Links - -The preferred mode of linking is via ~file~ links to files, and ~id~ links for -headlines. This maintains the strongest compatibility with Org-mode, ensuring -that the links still function without Org-roam, and work well exporting to other -backends. - -~file~ links can be inserted via ~org-roam-insert~. Links to headlines can be -inserted by navigating to the desired headline and calling ~org-store-link~. -This will create an ID for the headline if it does not already exist, and -populate the Org-roam database. The link can then be inserted via -~org-insert-link~. - -An alternative mode of insertion is using Org-roam's ~roam~ links. Org-roam -registers this link type, and interprets the path as follows: - -- ~[[roam:title]]~ :: links to an Org-roam file with title or alias "title" -- ~[[roam:*headline]]~ :: links to the headline "headline" in the current - Org-roam file -- ~[[roam:title*headline]]~ :: links to the headline "headline" in the Org-roam - file with title or alias "title" - -- User Option: org-roam-link-title-format - - To distinguish between org-roam links and regular links, one may choose to use - special indicators for Org-roam links. Defaults to ~"%s"~. - - If your version of Org is at least ~9.2~, consider styling the link differently, - by customizing the ~org-roam-link~, and ~org-roam-link-current~ faces. - -- User Option: org-roam-link-auto-replace - - When non-nil, ~roam~ links will be replaced with ~file~ or ~id~ links when - they are navigated to, and on file save, when a match is found. This is - desirable to maintain compatibility with vanilla Org, but resolved links are - harder to edit. Defaults to ~t~. - -* Completions - -Completions for Org-roam are provided via ~completion-at-point~. Completion -suggestions are implemented as separate functions. Org-roam installs all -functions in ~org-roam-completion-functions~ to ~completion-at-point-functions~. - -- Variable: org-roam-completion-functions - - The list of functions to be used with ~completion-at-point~. - -- User Option: org-roam-completion-ignore-case - - When non-nil, the ~roam~ link completions are ignore case. For example, - calling ~completion-at-point~ within ~[[roam:fo]]~ will present a completion - for a file with title "Foo". Defaults to ~t~. - -To use the completions from Org-roam with ~company-mode~, prepend ~company-capf~ -to variable ~company-backends~. - -** Link Completion - -~roam~ links support auto-completion via ~completion-at-point~: simply call -~M-x completion-at-point~ within a roam link. That is, where the ~|~ character -represents the cursor: - -- ~[[|]]~: completes for a file title -- ~[[roam:]]~: completes for a file title -- ~[[*|]]~: completes for a headline within this file -- ~[[foo*|]]~: completes a headline within the file with title "foo" -- ~[[roam:foo*|]]~ completes a headline within the file with title "foo" - -Completions account for the current input. For example, for ~[[f|]]~, the -completions (by default) only show for files with titles that start with "f". - -- Function: org-roam-link-complete-at-point - - Do appropriate completion for the link at point. - -*** Link Completions Everywhere - -Org-roam is able to provide completions from the current word at point, enabling -as-you-type link completions. However, this is disabled by default: the author -believes that linking should be a deliberate action and linking should be -performed with great care. - -Setting ~org-roam-completion-everywhere~ to ~t~ will enable word-at-point -completions. - -- User Option: org-roam-completion-everywhere - - If non-nil, provide completions from the current word at point. That is, in - the scenario ~this is a sent|~, calling ~completion-at-point~ will show - completions for titles that begin with "sent". - -** Tag Completion - -Org-roam facilitates the insertion of existing tags via ~completion-at-point~. - -That is, suppose you have notes with tags "foo", and "bar". Now, in a note, if -you're on a line beginning with ~#+roam_tags:~, completions for these will -appear as-you-type if they match. - -This functionality is implemented in ~org-roam-complete-tags-at-point~. - -* Navigating Around -** Index File - -As your collection grows, you might want to create an index where you keep links -to your main files. - -In Org-roam, you can define the path to your index file by setting -~org-roam-index-file~. - -- Variable: org-roam-index-file - - Path to the Org-roam index file. - - The path can be a string or a function. If it is a string, it should be the - path (absolute or relative to ~org-roam-directory~) to the index file. If it - is is a function, the function should return the path to the index file. - Otherwise, the index is assumed to be a note in ~org-roam-index~ whose - title is ~"Index"~. - -- Function: org-roam-find-index - - Opens the Index file in the current ~org-roam-directory~. +When non-nil, provide link completion matching outside of Org links. * Encryption -One may wish to keep private, encrypted files. Org-roam supports encryption (via -GPG), which can be enabled for all new files by setting ~org-roam-encrypt-files~ -to ~t~. When enabled, new files are created with the ~.org.gpg~ extension and -decryption are handled automatically by EasyPG. +Emacs has support for creating and editing encrypted gpg files, and Org-roam need +not provide additional tooling. To create encrypted files, simply add the ~.gpg~ +extension in your Org-roam capture templates. For example: -Note that Emacs will prompt for a password for encrypted files during cache -updates if it requires reading the encrypted file. To reduce the number of -password prompts, you may wish to cache the password. +#+begin_src emacs-lisp +(setq org-roam-capture-templates '(("d" "default" plain "%?" + :if-new (file+head "${slug}.org.gpg" + "#+title: ${title}\n") + :unnarrowed t))) +#+end_src -- User Option: org-roam-encrypt-files +* Org-roam Protocol - Whether to encrypt new files. If true, create files with .org.gpg extension. - -* Graphing - -Org-roam provides graphing capabilities to explore interconnections between -notes. This is done by performing SQL queries and generating images using -[[https://graphviz.org/][Graphviz]]. The graph can also be navigated: see [[*Roam Protocol][Roam Protocol]]. - -The entry point to graph creation is ~org-roam-graph~. - -- Function: org-roam-graph & optional arg file node-query - - Build and possibly display a graph for FILE from NODE-QUERY. - If FILE is nil, default to current buffer’s file name. - ARG may be any of the following values: - - - ~nil~ show the graph. - - ~C-u~ show the graph for FILE. - - ~C-u N~ show the graph for FILE limiting nodes to N steps. - - ~C-u C-u~ build the graph. - - ~C-u -~ build the graph for FILE. - - ~C-u -N~ build the graph for FILE limiting nodes to N steps. - -- User Option: org-roam-graph-executable - - Path to the graphing executable (in this case, Graphviz). Set this if Org-roam - is unable to find the Graphviz executable on your system. - - You may also choose to use ~neato~ in place of ~dot~, which generates a more - compact graph layout. - -- User Option: org-roam-graph-viewer - - Org-roam defaults to using Firefox (located on PATH) to view the SVG, but you - may choose to set it to: - - 1. A string, which is a path to the program used - 2. a function accepting a single argument: the graph file path. - - ~nil~ uses ~view-file~ to view the graph. - - If you are using WSL2 and would like to open the graph in Windows, you can use - the second option to set the browser and network file path: - - #+BEGIN_SRC emacs-lisp - (setq org-roam-graph-viewer - (lambda (file) - (let ((org-roam-graph-viewer "/mnt/c/Program Files/Mozilla Firefox/firefox.exe")) - (org-roam-graph--open (concat "file://///wsl$/Ubuntu" file))))) - #+END_SRC - -** Graph Options - -Graphviz provides many options for customizing the graph output, and Org-roam -supports some of them. See https://graphviz.gitlab.io/_pages/doc/info/attrs.html -for customizable options. - -- User Option: org-roam-graph-extra-config - - Extra options passed to graphviz for the digraph (The "G" attributes). - Example: ~'~(("rankdir" . "LR"))~ - -- User Option: org-roam-graph-node-extra-config - - Extra options for nodes in the graphviz output (The "N" attributes). - Example: ~'(("color" . "skyblue"))~ - -- User Option: org-roam-graph-edge-extra-config - - Extra options for edges in the graphviz output (The "E" attributes). - Example: ~'(("dir" . "back"))~ - -- User Option: org-roam-graph-edge-cites-extra-config - - Extra options for citation edges in the graphviz output. - Example: ~'(("color" . "red"))~ - -** Excluding Nodes and Edges - -One may want to exclude certain files to declutter the graph. - -- User Option: org-roam-graph-exclude-matcher - - Matcher for excluding nodes from the generated graph. Any nodes and links for - file paths matching this string is excluded from the graph. - - If value is a string, the string is the only matcher. - - If value is a list, all file paths matching any of the strings - are excluded. - -#+BEGIN_EXAMPLE - (setq org-roam-graph-exclude-matcher '("private" "dailies")) -#+END_EXAMPLE - -This setting excludes all files whose path contain "private" or "dailies". - -* Minibuffer Completion - -Org-roam allows customization of which minibuffer completion system to use for -its interactive commands. The default setting uses Emacs' standard -~completing-read~ mechanism. - -#+BEGIN_SRC emacs-lisp - (setq org-roam-completion-system 'default) -#+END_SRC - -If you have installed Helm or Ivy, and have their modes enabled, under the -~'default~ setting they will be used. - -In the rare scenario where you use Ivy globally, but prefer [[https://emacs-helm.github.io/helm/][Helm]] for org-roam -commands, set: - -#+BEGIN_SRC emacs-lisp - (setq org-roam-completion-system 'helm) -#+END_SRC - -Other options include ~'ido~, and ~'ivy~. - -* Roam Protocol - -Org-roam extends ~org-protocol~ with 2 protocols: the ~roam-file~ and ~roam-ref~ -protocols. +Org-roam provides extensions for capturing content from external applications +such as the browser, via ~org-protocol~. Org-roam extends ~org-protocol~ with 2 +protocols: the ~roam-node~ and ~roam-ref~ protocols. ** Installation -To enable Org-roam's protocol extensions, you have to add the following to your -init file: +To enable Org-roam's protocol extensions, simply add the following to your init +file: #+BEGIN_SRC emacs-lisp (require 'org-roam-protocol) #+END_SRC -The instructions for setting up ~org-protocol~ are reproduced below. - -We will also need to create a desktop application for ~emacsclient~. The -instructions for various platforms are shown below. +We also need to set up ~org-protocol~: the instructions for setting up +~org-protocol~ are reproduced below. +*** Linux For Linux users, create a desktop application in ~~/.local/share/applications/org-protocol.desktop~: @@ -1096,7 +723,8 @@ make the new policy take effect. See [[https://www.chromium.org/administrators/linux-quick-start][here]] for more info on the ~/etc/opt/chrome/policies/managed~ directory and [[https://cloud.google.com/docs/chrome-enterprise/policies/?policy=ExternalProtocolDialogShowAlwaysOpenCheckbox][here]] for information on the ~ExternalProtocolDialogShowAlwaysOpenCheckbox~ policy. -For MacOS, we need to create our own application. +*** Mac OS +For Mac OS, we need to create our own application. 1. Launch Script Editor 2. Use the following script, paying attention to the path to ~emacsclient~: @@ -1139,7 +767,6 @@ without confirmation. To do this, run in a shell: defaults write com.google.Chrome ExternalProtocolDialogShowAlwaysOpenCheckbox -bool true #+END_SRC - If you're using [[https://github.com/railwaycat/homebrew-emacsmacport][Emacs Mac Port]], it registered its `Emacs.app` as the default handler for the URL scheme `org-protocol`. To make ~OrgProtocol.app~ the default handler instead, run: @@ -1151,6 +778,7 @@ defaults write com.apple.LaunchServices/com.apple.launchservices.secure LSHandle Then restart your computer. +*** Windows For Windows, create a temporary ~org-protocol.reg~ file: #+BEGIN_SRC text @@ -1175,15 +803,15 @@ Windows, replace the last line with: After executing the .reg file, the protocol is registered and you can delete the file. -** The roam-file protocol +** The roam-node protocol -This is a simple protocol that opens the path specified by the ~file~ -key (e.g. ~org-protocol://roam-file?file=/tmp/file.org~). This is used -in the generated graph. +The roam-node protocol opens the node with ID specified by the ~node~ key (e.g. +~org-protocol://roam-node?node=node-id~). ~org-roam-graph~ uses this to make the +graph navigable. ** The roam-ref protocol -This protocol finds or creates a new note with a given ~roam_key~ (see [[id:3edec3e6-8e26-4a43-8a0a-bf204268bbb3][Files]]): +This protocol finds or creates a new note with a given ~roam_key~: [[file:images/roam-ref.gif]] @@ -1210,7 +838,139 @@ where ~template~ is the template key for a template in ~org-roam-capture-ref-templates~ (see [[*The Templating System][The Templating System]]). These templates should contain a ~#+roam_key: ${ref}~ in it. -* Daily-notes +* The Org-roam Templating System + +Org-roam extends the ~org-capture~ system, providing a smoother note-taking +experience. However, these extensions mean Org-roam capture templates are +incompatible with ~org-capture~ templates. + +Org-roam's templates are specified by ~org-roam-capture-templates~. Just like +~org-capture-templates~, ~org-roam-capture-templates~ can contain multiple +templates. If ~org-roam-capture-templates~ only contains one template, there +will be no prompt for template selection. + +** Template Walkthrough + +To demonstrate the additions made to org-capture templates. Here, we explain +the default template, reproduced below. You will find some most of the elements +of the template are similar to ~org-capture~ templates. + +#+BEGIN_SRC emacs-lisp +(("d" "default" plain "%?" + :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" + "#+title: ${title}\n") + :unnarrowed t)) +#+END_SRC + +1. The template has short key ~"d"~. If you have only one template, org-roam + automatically chooses this template for you. +2. The template is given a description of ~"default"~. +3. ~plain~ text is inserted. Other options include Org headings via + ~entry~. +4. Notice that the ~target~ that's usually in Org-capture templates is missing + here. +5. ~"%?"~ is the template inserted on each call to ~org-roam-capture-~. + This template means don't insert any content, but place the cursor here. +6. ~:if-new~ is a compulsory specification in the Org-roam capture template. + This indicates the location for the new node. +7. ~:unnarrowed t~ tells org-capture to show the contents for the whole file, + rather than narrowing to just the entry. This is part of the Org-capture + templates. + +See the ~org-roam-capture-templates~ documentation for more details and +customization options. + +** Org-roam Template Expansion + +Org-roam's template definitions also extend org-capture's template syntax, to +allow prefilling of strings. We have seen a glimpse of this in [[*Template Walkthrough][Template +Walkthrough]]. + +Org-roam provides the ~${foo}~ syntax for substituting variables with known +strings. ~${foo}~'s substitution is performed as follows: + +1. If ~foo~ is a function, ~foo~ is called with the current node as its + argument. +2. Else if ~org-roam-node-foo~ is a function, ~foo~ is called with the current node + as its argument. The ~org-roam-node-~ prefix defines many of Org-roam's node + accessors such as ~org-roam-node-title~ and ~org-roam-node-level~. +3. Else look up ~org-roam-capture--info~ for ~foo~. This is an internal variable + that is set before the capture process begins. +4. If none of the above applies, read a string using ~completing-read~. + +* Graphing + +Org-roam provides basic graphing capabilities to explore interconnections +between notes, in ~org-roam-graph~. This is done by performing SQL queries and +generating images using [[https://graphviz.org/][Graphviz]]. The graph can also be navigated: see [[*Roam Protocol][Roam +Protocol]]. + +The entry point to graph creation is ~org-roam-graph~. + +- Function: org-roam-graph & optional arg node + +Build and display a graph for NODE. +ARG may be any of the following values: + + - ~nil~ show the full graph. + - ~integer~ an integer argument ~N~ will show the graph for the connected + components to node up to ~N~ steps away. + +- User Option: org-roam-graph-executable + + Path to the graphing executable (in this case, Graphviz). Set this if Org-roam + is unable to find the Graphviz executable on your system. + + You may also choose to use ~neato~ in place of ~dot~, which generates a more + compact graph layout. + +- User Option: org-roam-graph-viewer + + Org-roam defaults to using Firefox (located on PATH) to view the SVG, but you + may choose to set it to: + + 1. A string, which is a path to the program used + 2. a function accepting a single argument: the graph file path. + + ~nil~ uses ~view-file~ to view the graph. + + If you are using WSL2 and would like to open the graph in Windows, you can use + the second option to set the browser and network file path: + + #+BEGIN_SRC emacs-lisp + (setq org-roam-graph-viewer + (lambda (file) + (let ((org-roam-graph-viewer "/mnt/c/Program Files/Mozilla Firefox/firefox.exe")) + (org-roam-graph--open (concat "file://///wsl$/Ubuntu" file))))) + #+END_SRC + +** Graph Options + +Graphviz provides many options for customizing the graph output, and Org-roam +supports some of them. See https://graphviz.gitlab.io/_pages/doc/info/attrs.html +for customizable options. + +- User Option: org-roam-graph-filetype + + The file type to generate for graphs. This defaults to ~"svg"~. + +- User Option: org-roam-graph-extra-config + + Extra options passed to graphviz for the digraph (The "G" attributes). + Example: ~'~(("rankdir" . "LR"))~ + +- User Option: org-roam-graph-node-extra-config + + An alist of options to style the nodes. + The car of the alist node type such as ~"id"~, or ~"http"~. The cdr of the + list is another alist of Graphviz node options (the "N" attributes). + +- User Option: org-roam-graph-edge-extra-config + + Extra options for edges in the graphviz output (The "E" attributes). + Example: ~'(("dir" . "back"))~ + +* Org-roam Dailies Org-roam provides journaling capabilities akin to [[#org-journal][Org-journal]] with ~org-roam-dailies~. @@ -1221,7 +981,7 @@ For ~org-roam-dailies~ to work, you need to define two variables: - Variable: ~org-roam-dailies-directory~ - Path to daily-notes. + Path to daily-notes. This path is relative to ~org-roam-directory~. - Variable: ~org-roam-dailies-capture-templates~ @@ -1230,52 +990,26 @@ For ~org-roam-dailies~ to work, you need to define two variables: Here is a sane default configuration: #+begin_src emacs-lisp - (setq org-roam-dailies-directory "daily/") +(setq org-roam-dailies-directory "daily/") - (setq org-roam-dailies-capture-templates - '(("d" "default" entry - #'org-roam-capture--get-point - "* %?" - :file-name "daily/%<%Y-%m-%d>" - :head "#+title: %<%Y-%m-%d>\n\n"))) +(setq org-roam-dailies-capture-templates + '(("d" "default" entry + "* %?" + :if-new (file+head "%<%Y-%m-%d>.org" + "#+title: %<%Y-%m-%d>\n")))) #+end_src -Make sure that ~org-roam-dailies-directory~ appears in ~:file-name~ for your -notes to be recognized as daily-notes. You can have different templates placing -their notes in different directories, but the one in -~org-roam-dailies-directory~ will be considered as the main one in commands. +See [[*The Templating System][The Templating System]] for creating new templates. -See [[*The Templating System][The Templating System]] for creating new -templates. ~org-roam-dailies~ provides an extra ~:olp~ option which allows -specifying the outline-path to a heading: +** Usage -#+begin_src emacs-lisp - (setq org-roam-dailies-capture-templates - '(("l" "lab" entry - #'org-roam-capture--get-point - "* %?" - :file-name "daily/%<%Y-%m-%d>" - :head "#+title: %<%Y-%m-%d>\n" - :olp ("Lab notes")) - - ("j" "journal" entry - #'org-roam-capture--get-point - "* %?" - :file-name "daily/%<%Y-%m-%d>" - :head "#+title: %<%Y-%m-%d>\n" - :olp ("Journal")))) -#+end_src - -The template ~l~ will put its notes under the heading ‘Lab notes’, and the -template ~j~ will put its notes under the heading ‘Journal’. - -** Capturing and finding daily-notes +~org-roam-dailies~ provides these interactive functions: - Function: ~org-roam-dailies-capture-today~ &optional goto Create an entry in the daily note for today. - When ~goto~ is non-nil, go the note without creating an entry. + When ~goto~ is non-nil, go to the note without creating an entry. - Function: ~org-roam-dailies-find-today~ @@ -1310,10 +1044,6 @@ There are also commands which allow you to use Emacs’s ~calendar~ to find the Prefer past dates, unless ~prefer-future~ is non-nil. -** Navigation - -You can navigate between daily-notes: - - Function: ~org-roam-dailies-find-directory~ Find and open ~org-roam-dailies-directory~. @@ -1325,63 +1055,7 @@ You can navigate between daily-notes: - Function: ~org-roam-dailies-find-next-note~ When in an daily-note, find the next one. - -* Diagnosing and Repairing Files - -Org-roam provides a utility for diagnosing and repairing problematic files via -~org-roam-doctor~. By default, ~org-roam-doctor~ runs the check on the current -Org-roam file. To run the check only for all Org-roam files, run ~C-u M-x -org-roam-doctor~, but note that this may take some time. - -- Function: org-roam-doctor &optional this-buffer - - Perform a check on Org-roam files to ensure cleanliness. If THIS-BUFFER, run - the check only for the current buffer. - -The checks run are defined in ~org-roam-doctor--checkers~. By default, there are -checkers for broken links and invalid =#+roam_*= properties. - -Each checker is an instance of ~org-roam-doctor-checker~. To define a checker, -use ~make-org-roam-doctor-checker~. Here is a sample definition: - -#+BEGIN_SRC emacs-lisp -(make-org-roam-doctor-checker - :name 'org-roam-doctor-broken-links - :description "Fix broken links." - :actions '(("d" . ("Unlink" . org-roam-doctor--remove-link)) - ("r" . ("Replace link" . org-roam-doctor--replace-link)) - ("R" . ("Replace link (keep label)" . org-roam-doctor--replace-link-keep-label)))) -#+END_SRC - -The ~:name~ property is the name of the function run. The function takes in the -Org parse tree, and returns a list of ~(point error-message)~. ~:description~ is -a short description of what the checker does. ~:actions~ is an alist containing -elements of the form ~(char . (prompt . function))~. These actions are defined -per checker, to perform autofixes for the errors. For each error detected, -~org-roam-doctor~ will move the point to the current error, and pop-up a help -window displaying the error message, as well as the list of actions that can be -taken provided in ~:actions~. - -* Finding Unlinked References - -Unlinked references are occurrences of strings of text that exactly match the -title or alias of an existing note in the Org-roam database. Org-roam provides -facilities for discovering these unlinked references, so one may decide whether -to convert them into links. - -To use this feature, simply call ~M-x org-roam-unlinked-references~ from within -an Org-roam note. Internally, Org-roam uses [[https://github.com/BurntSushi/ripgrep][ripgrep]] and a clever PCRE regex to -find occurrences of the title or aliases of the currently open note in all -Org-roam files. Hence, this requires a version of ripgrep that is compiled with -PCRE support. - -#+begin_quote -NOTE: Since ripgrep cannot read encrypted files, this function cannot find -unlinked references within encrypted files. -#+end_quote - * Performance Optimization -** TODO Profiling Key Operations ** Garbage Collection During the cache-build process, Org-roam generates a lot of in-memory @@ -1398,13 +1072,337 @@ operations. To reduce the number of garbage collection processes, one may set (setq org-roam-db-gc-threshold most-positive-fixnum) #+END_SRC +* The Org-mode Ecosystem + +Because Org-roam is built on top of Org-mode, it benefits from the vast number +of packages already available. + +** Browsing History with winner-mode + +~winner-mode~ is a global minor mode that allows one to undo and redo changes in +the window configuration. It is included with GNU Emacs since version 20. + +~winner-mode~ can be used as a simple version of browser history for Org-roam. +Each click through org-roam links (from both Org files and the backlinks buffer) +causes changes in window configuration, which can be undone and redone using +~winner-mode~. To use ~winner-mode~, simply enable it, and bind the appropriate +interactive functions: + +#+BEGIN_SRC emacs-lisp + (winner-mode +1) + (define-key winner-mode-map (kbd "") #'winner-undo) + (define-key winner-mode-map (kbd "") #'winner-redo) + +#+END_SRC +** Versioning Notes + +Since Org-roam notes are just plain text, it is trivial to track changes in your +notes database using version control systems such as [[https://git-scm.com/][Git]]. Simply initialize +~org-roam-directory~ as a Git repository, and commit your files at regular or +appropriate intervals. [[https://magit.vc/][Magit]] is a great interface to Git within Emacs. + +In addition, it may be useful to observe how a particular note has evolved, by +looking at the file history. [[https://gitlab.com/pidu/git-timemachine][Git-timemachine]] allows you to visit historic +versions of a tracked Org-roam note. + +** Full-text search with Deft + +[[https://jblevins.org/projects/deft/][Deft]] provides a nice interface for browsing and filtering org-roam notes. + +#+BEGIN_SRC emacs-lisp + (use-package deft + :after org + :bind + ("C-c n d" . deft) + :custom + (deft-recursive t) + (deft-use-filter-string-for-filename t) + (deft-default-extension "org") + (deft-directory org-roam-directory)) +#+END_SRC + +The Deft interface can slow down quickly when the number of files get huge. +[[https://github.com/hasu/notdeft][Notdeft]] is a fork of Deft that uses an external search engine and indexer. + +** Org-journal + +[[https://github.com/bastibe/org-journal][Org-journal]] provides journaling capabilities to Org-mode. A lot of its +functionalities have been incorporated into Org-roam under the name +[[*Daily-notes][~org-roam-dailies~]]. It remains a good tool if you want to isolate your verbose +journal entries from the ideas you would write on a scratchpad. + +#+BEGIN_SRC emacs-lisp + (use-package org-journal + :bind + ("C-c n j" . org-journal-new-entry) + :custom + (org-journal-date-prefix "#+title: ") + (org-journal-file-format "%Y-%m-%d.org") + (org-journal-dir "/path/to/journal/files/") + (org-journal-date-format "%A, %d %B %Y")) +#+END_SRC + +** Org-download + +[[https://github.com/abo-abo/org-download][Org-download]] lets you screenshot and yank images from the web into your notes: + +#+CAPTION: org-download +[[file:images/org-download.gif]] + +#+BEGIN_SRC emacs-lisp + (use-package org-download + :after org + :bind + (:map org-mode-map + (("s-Y" . org-download-screenshot) + ("s-y" . org-download-yank)))) +#+END_SRC + +** mathpix.el + +[[https://github.com/jethrokuan/mathpix.el][mathpix.el]] uses [[https://mathpix.com/][Mathpix's]] API to convert clips into latex equations: + +#+CAPTION: mathpix +[[file:images/mathpix.gif]] + +#+BEGIN_SRC emacs-lisp + (use-package mathpix.el + :straight (:host github :repo "jethrokuan/mathpix.el") + :custom ((mathpix-app-id "app-id") + (mathpix-app-key "app-key")) + :bind + ("C-x m" . mathpix-screenshot)) +#+END_SRC + +** Org-noter / Interleave + +[[https://github.com/weirdNox/org-noter][Org-noter]] and [[https://github.com/rudolfochrist/interleave][Interleave]] are both projects that allow synchronised annotation of +documents (PDF, EPUB etc.) within Org-mode. + +** Bibliography + +[[https://github.com/org-roam/org-roam-bibtex][org-roam-bibtex]] offers tight integration between [[https://github.com/jkitchin/org-ref][org-ref]], [[https://github.com/tmalsburg/helm-bibtex][helm-bibtex]] and +~org-roam~. This helps you manage your bibliographic notes under ~org-roam~. + +For example, though helm-bibtex provides the ability to visit notes for +bibliographic entries, org-roam-bibtex extends it with the ability to visit the +file with the right =#+ROAM_KEYS=. + +** Spaced Repetition + +[[https://www.leonrische.me/fc/index.html][Org-fc]] is a spaced repetition system that scales well with a large number of +files. Other alternatives include [[https://orgmode.org/worg/org-contrib/org-drill.html][org-drill]], and [[https://github.com/abo-abo/pamparam][pamparam]]. + +To use Anki for spaced repetition, [[https://github.com/louietan/anki-editor][anki-editor]] allows you to write your cards in +Org-mode, and sync your cards to Anki via [[https://github.com/FooSoft/anki-connect#installation][anki-connect]]. + +* FAQ +** How do I have more than one Org-roam directory? + +Emacs supports directory-local variables, allowing the value of +~org-roam-directory~ to be different in different directories. It does this by +checking for a file named ~.dir-locals.el~. + +To add support for multiple directories, override the ~org-roam-directory~ +variable using directory-local variables. This is what ~.dir-locals.el~ may +contain: + +#+BEGIN_SRC emacs-lisp + ((nil . ((org-roam-directory . (expand-file-name ".")) + (org-roam-db-location . (expand-file-name "./org-roam.db"))))) +#+END_SRC + +All files within that directory will be treated as their own separate set of +Org-roam files. Remember to run ~org-roam-db-sync~ from a file within +that directory, at least once. + +** How do I migrate from Roam Research? + +Fabio has produced a command-line tool that converts markdown files exported +from Roam Research into Org-roam compatible markdown. More instructions are +provided [[https://github.com/fabioberger/roam-migration][in the repository]]. + +** How do I create a note whose title already matches one of the candidates? + +This situation arises when, for example, one would like to create a note titled +"bar" when "barricade" already exists. + +The solution is dependent on the mini-buffer completion framework in use. Here +are the solutions: + +- Ivy :: call ~ivy-immediate-done~, typically bound to ~C-M-j~. Alternatively, + set ~ivy-use-selectable-prompt~ to ~t~, so that "bar" is now selectable. +- Helm :: Org-roam should provide a selectable "[?] bar" candidate at the top of + the candidate list. + +* Migrating from Org-roam v1 + +Those coming from Org-roam v1 will do well treating v2 as entirely new software. +V2 has a smaller core and fewer moving parts, while retaining the bulk of its +functionality. It is recommended to read the documentation above about nodes. + +It is still desirable to migrate notes collected in v1 to v2. To migrate your v1 +notes to v2, you may use the migration script provided in [[https://gist.github.com/jethrokuan/02f41028fb4a6f81787dc420fb99b6e4][this gist]], or [[https://gist.github.com/jethrokuan/02f41028fb4a6f81787dc420fb99b6e4#gistcomment-3737019][this +gist]], the latter being better tested. [[https://d12frosted.io/posts/2021-06-11-path-to-org-roam-v2.html][This blog post]] provides a good overview of +what's new in v2 and how to migrate. + +Simply put, to migrate notes from v1 to v2, one must: + +1. Add IDs to all existing notes. These are located in top-level property + drawers (Although note that in v2, not all files need to have IDs) +2. Update the Org-roam database to conform to the new schema. +3. Replace ~#+ROAM_KEY~ into the ~ROAM_REFS~ property +4. Replace ~#+ROAM_ALIAS~ into the ~ROAM_ALIASES~ property +5. Move ~#+ROAM_TAGS~ into the ~#+FILETAGS~ property for file-level nodes, and + the ~ROAM_TAGS~ property for headline nodes +6. Replace existing file links with ID links. + +* Developer's Guide to Org-roam +** Org-roam's Design Principle + +Org-roam is primarily motivated by the need for a dual representation. We +(humans) love operating in a plain-text environment. The syntax rules of +Org-mode are simple and fit snugly within our brain. This also allows us to use +the tools and packages we love to explore and edit our notes. Org-mode is simply +the most powerful plain-text format available, with support for images, LaTeX, +TODO planning and much more. + +But this plain-text format is simply ill-suited for exploration of these notes: +plain-text is simply not amenable for answering large-scale, complex queries +(e.g. how many tasks do I have that are due by next week?). Interfaces such as +Org-agenda slow to a crawl when the number of files becomes unwieldy, which can +quickly become the case. + +At its core, Org-roam provides a database abstraction layer, providing a dual +representation of what's already available in plain-text. This allows us +(humans) to continue working with plain-text, while programs can utilize the +database layer to perform complex queries. These capabilities include, but are +not limited to: + +- link graph traversal and visualization +- Instantaneous SQL-like queries on headlines + - What are my TODOs, scheduled for X, or due by Y? +- Accessing the properties of a node, such as its tags, refs, TODO state or + priority + +All of these functionality is powered by this database abstraction layer. Hence, +at its core Org-roam's primary goal is to provide a resilient dual +representation that is cheap to maintain, easy to understand, and is as +up-to-date as it possibly can. Org-roam also then exposes an API to this +database abstraction layer for users who would like to perform programmatic +queries on their Org files. + +** Building Extensions and Advanced Customization of Org-roam + +Because Org-roam's core functionality is small, it is possible and sometimes +desirable to build extensions on top of it. These extensions may one or more of +the following functionalities: + +- Access to Org-roam's database +- Usage/modification of Org-roam's interactive commands + +Org-roam provides no guarantees that extensions will continue to function as +Org-roam evolves, but by following these simple rules, extensions can be made +robust to local changes in Org-roam. + +1. Extensions should not modify the database schema. Any extension that requires + the caching of additional data should make a request upstream to Org-roam. +2. Extensions requiring access to the database should explicitly state support + for the database version (~org-roam-db-version~), and only conditionally + load when support is available. + +*** Accessing the Database + +Access to the database is provided singularly by ~org-roam-db-query~, for +example: + +#+begin_src emacs-lisp +(org-roam-db-query [:select * :from nodes]) +#+end_src + +One can refer to the database schema by looking up +~org-roam-db--table-schemata~. There are multiple helper functions within +Org-roam that call ~org-roam-db-query~, these are subject to change. To ensure +that extensions/customizations are robust to change, extensions should only use +~org-roam-db-query~, and perhaps replicate the SQL query if necessary. + +*** Accessing and Modifying Nodes + +The node interface is cleanly defined using ~cl-defstruct~. The primary +method to access nodes is ~org-roam-node-at-point~ and ~org-roam-node-read~: + +- Function: org-roam-node-at-point &optional assert + + Return the node at point. If ASSERT, throw an error if there is no node at + point. + +- Function: org-roam-node-read &optional initial-input filter-fn sort-fn + require-match + + Read and return an `org-roam-node'. + INITIAL-INPUT is the initial minibuffer prompt value. FILTER-FN + is a function to filter out nodes: it takes a single argument (an + ~org-roam-node~), and when nil is returned the node will be + filtered out. + SORT-FN is a function to sort nodes. See ~org-roam-node-sort-by-file-mtime~ + for an example sort function. + If REQUIRE-MATCH, the minibuffer prompt will require a match. + +Once you obtain the node, you can use the accessors for the node, e.g. +~org-roam-node-id~ or ~org-roam-node-todo~. + +It is possible to define (or override existing) properties on nodes. This is +simply done using a ~cl-defmethod~ on the ~org-roam-node~ struct: + +#+begin_src emacs-lisp +(cl-defmethod org-roam-node-namespace ((node org-roam-node)) + "Return the namespace for NODE. +The namespace is the final directory of the file for the node." + (file-name-nondirectory + (directory-file-name + (file-name-directory (org-roam-node-file node))))) +#+end_src + +The snippet above defines a new property ~namespace~ on ~org-roam-node~, which +making it available for use in capture templates. + +*** Extending the Capture System + +Org-roam applies some patching over Org's capture system to smooth out the user +experience, and sometimes it is desirable to use Org-roam's capturing system +instead. The exposed function to be used in extensions is ~org-roam-capture-~: + +- Function: org-roam-capture- &key goto keys node info props templates + + Main entry point. + GOTO and KEYS correspond to `org-capture' arguments. + INFO is an alist for filling up Org-roam's capture templates. + NODE is an `org-roam-node' construct containing information about the node. + PROPS is a plist containing additional Org-roam properties for each template. + TEMPLATES is a list of org-roam templates. + +An example of an extension using ~org-roam-capture-~ is ~org-roam-dailies~ +itself: + +#+begin_src emacs-lisp +(defun org-roam-dailies--capture (time &optional goto) + "Capture an entry in a daily-note for TIME, creating it if necessary. + +When GOTO is non-nil, go the note without creating an entry." + (org-roam-capture- :goto (when goto '(4)) + :node (org-roam-node-create) + :templates org-roam-dailies-capture-templates + :props (list :override-default-time time)) + (when goto (run-hooks 'org-roam-dailies-find-file-hook))) +#+end_src + * _ Copying :PROPERTIES: :COPYING: t :END: #+BEGIN_QUOTE -Copyright (C) 2020-2020 Jethro Kuan +Copyright (C) 2020-2021 Jethro Kuan You can redistribute this document and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -1433,225 +1431,6 @@ General Public License for more details. - [[https://www.youtube.com/watch?v=RvWic15iXjk][How to Use Roam to Outline a New Article in Under 20 Minutes]] ** Ecosystem -*** Browsing History with winner-mode - -~winner-mode~ is a global minor mode that allows one to undo and redo changes in -the window configuration. It is included with GNU Emacs since version 20. - -~winner-mode~ can be used as a simple version of browser history for Org-roam. -Each click through org-roam links (from both Org files and the backlinks buffer) -causes changes in window configuration, which can be undone and redone using -~winner-mode~. To use ~winner-mode~, simply enable it, and bind the appropriate -interactive functions: - -#+BEGIN_SRC emacs-lisp - (winner-mode +1) - (define-key winner-mode-map (kbd "") #'winner-undo) - (define-key winner-mode-map (kbd "") #'winner-redo) - -#+END_SRC -*** Versioning Notes - -Since Org-roam notes are just plain text, it is trivial to track changes in your -notes database using version control systems such as [[https://git-scm.com/][Git]]. Simply initialize -~org-roam-directory~ as a Git repository, and commit your files at regular or -appropriate intervals. [[https://magit.vc/][Magit]] is a great interface to Git within Emacs. - -In addition, it may be useful to observe how a particular note has evolved, by -looking at the file history. [[https://gitlab.com/pidu/git-timemachine][Git-timemachine]] allows you to visit historic -versions of a tracked Org-roam note. - -*** Full-text search interface with Deft - :PROPERTIES: - :CUSTOM_ID: deft - :END: - -[[https://jblevins.org/projects/deft/][Deft]] provides a nice interface for browsing and filtering org-roam notes. - -#+BEGIN_SRC emacs-lisp - (use-package deft - :after org - :bind - ("C-c n d" . deft) - :custom - (deft-recursive t) - (deft-use-filter-string-for-filename t) - (deft-default-extension "org") - (deft-directory "/path/to/org-roam-files/")) -#+END_SRC - -If the title of the Org file is not the first line, you might not get nice -titles. You may choose to patch this to use ~org-roam~'s functionality. Here I'm -using [[https://github.com/raxod502/el-patch][el-patch]]: - -#+BEGIN_SRC emacs-lisp - (use-package el-patch - :straight (:host github - :repo "raxod502/el-patch" - :branch "develop")) - - (eval-when-compile - (require 'el-patch)) - - (use-package deft - ;; same as above... - :config/el-patch - (defun deft-parse-title (file contents) - "Parse the given FILE and CONTENTS and determine the title. - If `deft-use-filename-as-title' is nil, the title is taken to - be the first non-empty line of the FILE. Else the base name of the FILE is - used as title." - (el-patch-swap (if deft-use-filename-as-title - (deft-base-filename file) - (let ((begin (string-match "^.+$" contents))) - (if begin - (funcall deft-parse-title-function - (substring contents begin (match-end 0)))))) - (org-roam-db--get-title file)))) -#+END_SRC - -The Deft interface can slow down quickly when the number of files get huge. -[[https://github.com/hasu/notdeft][Notdeft]] is a fork of Deft that uses an external search engine and indexer. - -*** Org-journal - :PROPERTIES: - :CUSTOM_ID: org-journal - :END: - -[[https://github.com/bastibe/org-journal][Org-journal]] provides journaling capabilities to Org-mode. A lot of its -functionalities have been incorporated into Org-roam under the name -[[*Daily-notes][~org-roam-dailies~]]. It remains a good tool if you want to isolate your verbose -journal entries from the ideas you would write on a scratchpad. - -#+BEGIN_SRC emacs-lisp - (use-package org-journal - :bind - ("C-c n j" . org-journal-new-entry) - :custom - (org-journal-date-prefix "#+title: ") - (org-journal-file-format "%Y-%m-%d.org") - (org-journal-dir "/path/to/journal/files/") - (org-journal-date-format "%A, %d %B %Y")) -#+END_SRC - -*** Note-taking Add-ons - :PROPERTIES: - :CUSTOM_ID: note-taking-add-ons - :END: - -These are some plugins that make note-taking in Org-mode more enjoyable. - -**** Org-download - :PROPERTIES: - :CUSTOM_ID: org-download - :END: - -[[https://github.com/abo-abo/org-download][Org-download]] lets you screenshot and yank images from the web into your notes: - -#+CAPTION: org-download -[[file:images/org-download.gif]] - -#+BEGIN_SRC emacs-lisp - (use-package org-download - :after org - :bind - (:map org-mode-map - (("s-Y" . org-download-screenshot) - ("s-y" . org-download-yank)))) -#+END_SRC - -**** mathpix.el - :PROPERTIES: - :CUSTOM_ID: mathpix.el - :END: - -[[https://github.com/jethrokuan/mathpix.el][mathpix.el]] uses [[https://mathpix.com/][Mathpix's]] API to convert clips into latex equations: - -#+CAPTION: mathpix -[[file:images/mathpix.gif]] - -#+BEGIN_SRC emacs-lisp - (use-package mathpix.el - :straight (:host github :repo "jethrokuan/mathpix.el") - :custom ((mathpix-app-id "app-id") - (mathpix-app-key "app-key")) - :bind - ("C-x m" . mathpix-screenshot)) -#+END_SRC - -**** Org-noter / Interleave - :PROPERTIES: - :CUSTOM_ID: org-noter-interleave - :END: - -[[https://github.com/weirdNox/org-noter][Org-noter]] and -[[https://github.com/rudolfochrist/interleave][Interleave]] are both -projects that allow synchronised annotation of documents (PDF, EPUB -etc.) within Org-mode. - -**** Bibliography - :PROPERTIES: - :CUSTOM_ID: bibliography - :END: - -[[https://github.com/org-roam/org-roam-bibtex][org-roam-bibtex]] offers -tight integration between -[[https://github.com/jkitchin/org-ref][org-ref]], -[[https://github.com/tmalsburg/helm-bibtex][helm-bibtex]] and -~org-roam~. This helps you manage your bibliographic notes under -~org-roam~. - -For example, though helm-bibtex provides the ability to visit notes for -bibliographic entries, org-roam-bibtex extends it with the ability to visit the -file with the right =#+roam_key=. - -**** Spaced Repetition - :PROPERTIES: - :CUSTOM_ID: spaced-repetition - :END: - -[[https://www.leonrische.me/fc/index.html][Org-fc]] is a spaced repetition system that scales well with a large number of -files. Other alternatives include [[https://orgmode.org/worg/org-contrib/org-drill.html][org-drill]], and [[https://github.com/abo-abo/pamparam][pamparam]]. - -* FAQ -** How do I have more than one Org-roam directory? - -Emacs supports directory-local variables, allowing the value of -~org-roam-directory~ to be different in different directories. It does this by -checking for a file named ~.dir-locals.el~. - -To add support for multiple directories, override the ~org-roam-directory~ -variable using directory-local variables. This is what ~.dir-locals.el~ may -contain: - -#+BEGIN_SRC emacs-lisp - ((nil . ((org-roam-directory . (expand-file-name ".")) - (org-roam-db-location . (expand-file-name "./org-roam.db"))))) -#+END_SRC - -All files within that directory will be treated as their own separate set of -Org-roam files. Remember to run ~org-roam-db-build-cache~ from a file within -that directory, at least once. - -** How do I migrate from Roam Research? - -Fabio has produced a command-line tool that converts markdown files exported -from Roam Research into Org-roam compatible markdown. More instructions are -provided [[https://github.com/fabioberger/roam-migration][in the repository]]. - -** How do I create a note whose title already matches one of the candidates? - -This situation arises when, for example, one would like to create a note titled -"bar" when "barricade" already exists. - -The solution is dependent on the mini-buffer completion framework in use. Here -are the solutions: - -- Ivy :: call ~ivy-immediate-done~, typically bound to ~C-M-j~. Alternatively, - set ~ivy-use-selectable-prompt~ to ~t~, so that "bar" is now selectable. -- Helm :: Org-roam should provide a selectable "[?] bar" candidate at the top of - the candidate list. - * Keystroke Index :PROPERTIES: :APPENDIX: t @@ -1675,16 +1454,17 @@ are the solutions: :END: * Footnotes - [fn:1] Depending on your completion framework, you may need to press TAB to see the list. +[fn:2] Two easy ways to evaluate elisp: 1) Place the cursor after the closing +paren and run =M-x eval-last-sexp RET= or 2) Press =C-c C-c= with your cursor in +an Org file code block (like =#+BEGIN_SRC emacs-lisp=). [fn:roam] To understand more about Roam, a collection of links are available in [[*Note-taking Workflows][Note-taking Workflows]]. # Local Variables: # eval: (require 'ol-info) # eval: (require 'ox-texinfo+ nil t) # eval: (auto-fill-mode +1) -# before-save-hook: org-make-toc # after-save-hook: (lambda nil (progn (require 'ox-texinfo nil t) (org-texinfo-export-to-info))) # indent-tabs-mode: nil # org-src-preserve-indentation: nil diff --git a/doc/org-roam.texi b/doc/org-roam.texi index 3dbfaee..6a6e2af 100644 --- a/doc/org-roam.texi +++ b/doc/org-roam.texi @@ -25,13 +25,13 @@ General Public License for more details. @dircategory Emacs @direntry -* Org-roam: (org-roam). Rudimentary Roam Replica for Emacs. +* Org-roam: (org-roam). Roam Research for Emacs. @end direntry @finalout @titlepage @title Org-roam User Manual -@subtitle for version 1.2.3 +@subtitle for version 2.0.0 @author Jethro Kuan @page @vskip 0pt plus 1filll @@ -44,10 +44,10 @@ General Public License for more details. @noindent -This manual is for Org-roam version 1.2.3. +This manual is for Org-roam version 2.0.0. @quotation -Copyright (C) 2020-2020 Jethro Kuan +Copyright (C) 2020-2021 Jethro Kuan You can redistribute this document and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either @@ -67,13 +67,24 @@ General Public License for more details. * A Brief Introduction to the Zettelkasten Method:: * Installation:: * Getting Started:: -* Files:: +* Viewing the links:: +* Node Properties:: +* The Org-roam Buffer:: +* Styling Org-roam:: +* Completion:: +* Encryption:: +* Org-roam protocol:: +* Diagnosing and Repair:: +* Building Extensions:: +* The Org-mode Ecosystem:: +* Frequently Asked Questions:: +* Developer's Guide to Org-roam:: * The Templating System:: * Concepts and Configuration:: * Inserting Links:: * Completions:: * Navigating Around:: -* Encryption:: +* Encryption: Encryption (1). * Graphing:: * Minibuffer Completion:: * Roam Protocol:: @@ -91,31 +102,33 @@ General Public License for more details. @detailmenu --- The Detailed Node Listing --- -A Brief Introduction to the Zettelkasten Method - -* Fleeting notes:: -* Permanent notes:: - Installation * Installing from MELPA:: * Installing from Apt:: -* Installing from the Git Repository:: +* Installing from Source:: * Post-Installation Tasks:: -Files +Getting Started -* File Titles:: -* File Tags:: -* File Refs:: +* The Org-roam Node:: +* Links between Nodes:: +* Setting up Org-roam:: +* Creating and Linking Nodes:: -File Titles +Node Properties -* Customizing Title Extraction:: +* Standard Org properties:: +* Aliases:: +* Refs:: -File Tags +Building Extensions -* Customizing Tag Extraction:: +* Public Interface:: + +Developer's Guide to Org-roam + +* Org-roam's Design Principles:: The Templating System @@ -125,10 +138,9 @@ The Templating System Concepts and Configuration * Directories and Files:: -* The Org-roam Buffer:: +* The Org-roam Buffer: The Org-roam Buffer (1). * Org-roam Files:: * Org-roam Faces:: -* The Database:: Completions @@ -198,28 +210,24 @@ FAQ @node Introduction @chapter Introduction -Org-roam is a tool for network thought. It reproduces some of @uref{https://roamresearch.com/, Roam -Research's} @footnote{To understand more about Roam, a collection of links are available in @ref{Note-taking Workflows}.} features within the all-powerful @uref{https://orgmode.org/, Org-mode}. +Org-roam is a tool for networked thought. It reproduces some of @uref{https://roamresearch.com/, Roam +Research's} @footnote{To understand more about Roam, a collection of links are available in @ref{Note-taking Workflows}.} key features within @uref{https://orgmode.org/, Org-mode}. -Org-roam is a solution for effortless non-hierarchical note-taking with -Org-mode. With Org-roam, notes flow naturally, making note-taking fun and easy. -Org-roam keeps closely to Org syntax, and will work for anyone already using -Org-mode for their personal wiki. +Org-roam allows for effortless non-hierarchical note-taking: with Org-roam, +notes flow naturally, making note-taking fun and easy. Org-roam augments the +Org-mode syntax, and will work for anyone already using Org-mode for their +personal wiki. -Org-roam gains its superpowers by leveraging the mature ecosystem around -Org-mode. For example, it has first-class support for @uref{https://github.com/jkitchin/org-ref, org-ref} for citation -management. +Org-roam leverages the mature ecosystem around Org-mode. For example, it has +first-class support for @uref{https://github.com/jkitchin/org-ref, org-ref} for citation management, and is able to +piggyback off Org's excellent @LaTeX{} and source-block evaluation capabilities. -Org-roam aims to implement the core features of Roam, leveraging the -mature ecosystem around Org-mode where possible. Eventually, we hope -to further introduce features enabled by the Emacs ecosystem. - -Org-roam provides several benefits over other tooling: +Org-roam provides these benefits over other tooling: @itemize @item -@strong{Privacy and Security:} Keep your personal wiki entirely offline and in your -control. Encrypt your notes with GPG@. +@strong{Privacy and Security:} Your personal wiki belongs only to you, entirely +offline and in your control. Encrypt your notes with GPG@. @item @strong{Longevity of Plain Text:} Unlike web solutions like Roam Research, the notes @@ -235,13 +243,13 @@ you feel unhappy with any part of Org-roam, you may choose to extend Org-roam, or open a pull request. @item -@strong{Leverage the Org-mode ecosystem:} Over the years, Emacs and Org-mode has +@strong{Leverage the Org-mode ecosystem:} Over the decades, Emacs and Org-mode has developed into a mature system for plain-text organization. Building upon Org-mode already puts Org-roam light-years ahead of many other solutions. @item -@strong{Built on Emacs:} Emacs is also a fantastic interface for editing text, and we -can inherit many of the powerful text-navigation and editing packages +@strong{Built on Emacs:} Emacs is also a fantastic interface for editing text, and +Org-roam inherits many of the powerful text-navigation and editing packages available to Emacs. @end itemize @@ -249,18 +257,18 @@ available to Emacs. @chapter Target Audience Org-roam is a tool that will appear unfriendly to anyone unfamiliar with Emacs -and Org-mode, but is also extremely powerful to those willing to put effort in -mastering the intricacies of the tools. Org-roam stands on the shoulders on -giants. Emacs was first created in 1976, and remains a top tier tool for editing +and Org-mode, but it is also extremely powerful to those willing to put effort +inn mastering the intricacies. Org-roam stands on the shoulders of giants. Emacs +was first created in 1976, and remains the tool of choice for many for editing text and designing textual interfaces. The malleability of Emacs allowed the creation of Org-mode, an all-purpose plain-text system for maintaining TODO lists, planning projects, and authoring documents. Both of these tools are incredibly vast and require significant time investment to master. -Org-roam assumes basic familiarity with these tools. It is not difficult to get -up and running with basic text-editing functionality, but one will only fully -appreciate the power of building Roam functionality into Emacs and Org-mode when -the usage of these tools become more advanced. +Org-roam assumes only basic familiarity with these tools. It is not difficult to +get up and running with basic text-editing functionality, but one will only +fully appreciate the power of building Roam functionality into Emacs and +Org-mode when the usage of these tools become more advanced. One key advantage to Org-roam is that building on top of Emacs gives it malleability. This is especially important for note-taking workflows. It is our @@ -313,13 +321,7 @@ plain-text, Org-mode file. In the same way one would maintain a paper slip-box, Org-roam makes it easy to create new zettels, pre-filling boilerplate content using a powerful templating system. -@menu -* Fleeting notes:: -* Permanent notes:: -@end menu - -@node Fleeting notes -@section Fleeting notes +@strong{Fleeting notes} A slip-box requires a method for quickly capturing ideas. These are called @strong{fleeting notes}: they are simple reminders of information or ideas that will @@ -328,8 +330,7 @@ need to be processed later on, or trashed. This is typically accomplished using functionality (see @ref{Daily-notes}). This provides a central inbox for collecting thoughts, to be processed later into permanent notes. -@node Permanent notes -@section Permanent notes +@strong{Permanent notes} Permanent notes are further split into two categories: @strong{literature notes} and @strong{concept notes}. Literature notes can be brief annotations on a particular @@ -338,6 +339,9 @@ Concept notes require much more care in authoring: they need to be self-explanatory and detailed. Org-roam's templating system supports the addition of different templates to facilitate the creation of these notes. +For further reading on the Zettelkasten method, ``How to Take Smart Notes'' by +Sonke Ahrens is a decent guide. + @node Installation @chapter Installation @@ -347,7 +351,7 @@ development repository. @menu * Installing from MELPA:: * Installing from Apt:: -* Installing from the Git Repository:: +* Installing from Source:: * Post-Installation Tasks:: @end menu @@ -416,8 +420,8 @@ apt-get install elpa-org-roam Org-roam will then be autoloaded into Emacs. -@node Installing from the Git Repository -@section Installing from the Git Repository +@node Installing from Source +@section Installing from Source You may install Org-roam directly from the repository on @uref{https://github.com/org-roam/org-roam, GitHub} if you like. This will give you access to the latest version hours or days before it appears @@ -520,18 +524,19 @@ install-info /path/to/my/info/files/org-roam.info /path/to/my/info/files/dir @node Post-Installation Tasks @section Post-Installation Tasks -Org-roam uses @code{emacsql-sqlite3}, which requires @code{sqlite3} to be located on -@code{exec-path}. Please ensure that @code{sqlite3} is installed appropriately on your -operating system. You can verify that this is the case by executing: +Org-roam requires @code{sqlite3} to be locatable by Emacs (i.e. on @code{exec-path}). +Please ensure that @code{sqlite3} is installed appropriately on your operating +system. You can verify that this is the case by executing @footnote{Two easy ways to evaluate elisp: 1) Place the cursor after the closing +paren and run @samp{M-x eval-last-sexp RET} or 2) Press @samp{C-c C-c} with your cursor in +an Org file code block (like @samp{#+BEGIN_SRC emacs-lisp}).}: @lisp (executable-find "sqlite3") @end lisp If you have @code{sqlite3} installed, and @code{executable-find} still reports @code{nil}, then -it is likely that the path to the executable is not a member of the Emacs -variable @code{exec-path}. You may rectify this by manually adding the path within -your Emacs configuration: +the path to the executable is not a member of the Emacs variable @code{exec-path}. +Rectify this by manually adding the path within your Emacs configuration: @lisp (add-to-list 'exec-path "path/to/sqlite3") @@ -540,283 +545,145 @@ your Emacs configuration: @node Getting Started @chapter Getting Started -This short tutorial describes the essential commands used in Org-roam, to help -you get started. +@menu +* The Org-roam Node:: +* Links between Nodes:: +* Setting up Org-roam:: +* Creating and Linking Nodes:: +@end menu -First, it is important to understand how Org-roam was designed. Org-roam was -built to support a workflow that was not possible with vanilla Org-mode. This -flow is modelled after the @uref{https://zettelkasten.de/, Zettelkasten Method}, and many of @uref{https://roamresearch.com, Roam Research's} -workflows. Org-roam does not magically make note-taking better -- this often -requires a radical change in your current note-taking workflow. To understand -more about the methods and madness, see @ref{Note-taking Workflows}. +@node The Org-roam Node +@section The Org-roam Node + +We first begin with some terminology we'll use throughout the manual. We term +the basic denomination in Org-roam a node. We define a node as follows: + +@quotation +A node is any headline or top level file with an ID@. + +@end quotation + +For example, with this example file content: + +@example +:PROPERTIES: +:ID: foo +:END: +#+title: Foo + +* Bar +:PROPERTIES: +:ID: bar +:END: +@end example + +We create two nodes: + +@itemize +@item +A file node ``Foo'' with id @code{foo}. + +@item +A headline node ``Bar'' with id @code{bar}. +@end itemize + +Headlines without IDs will not be considered Org-roam nodes. Org IDs can be +added to files or headlines via the interactive command @code{M-x org-id-get-create}. + +@node Links between Nodes +@section Links between Nodes + +We link between nodes using Org's standard ID link (e.g. @code{id:foo}). While only +ID links will be considered during the computation of links between nodes, +Org-roam caches all other links in the documents for external use. + +@node Setting up Org-roam +@section Setting up Org-roam + +Org-roam's capabilities stem from its aggressive caching: it crawls all files +within @code{org-roam-directory}, keeping a cache of all its links and nodes, while +making sure that the cache is consistent. To first start using Org-roam, one needs to pick a location to store the Org-roam files. The directory that will contain your notes is specified by the -variable @code{org-roam-directory}. This variable needs to be set before any calls to -Org-roam functions, including enabling @code{org-roam-mode}. For this tutorial, -create an empty directory, and set @code{org-roam-directory}: +variable @code{org-roam-directory}. Org-roam searches recursively within +@code{org-roam-directory} for notes. This variable needs to be set before any calls +to Org-roam functions. For this tutorial, create an empty directory, and set +@code{org-roam-directory}: @lisp (make-directory "~/org-roam") (setq org-roam-directory "~/org-roam") @end lisp -We encourage using a flat hierarchy for storing notes, but some prefer using -folders for storing specific kinds of notes (e.g. websites, papers). This is -fine; Org-roam searches recursively within @code{org-roam-directory} for notes. -Instead of relying on the file hierarchy for any form of categorization, one -should use links between files to establish connections between notes. - -Next, we need to enable the global minor mode @code{org-roam-mode}. This sets up -Emacs with several hooks, building a cache that is kept consistent as your -slip-box grows. We recommend starting @code{org-roam-mode} on startup: +Next, we need to setup Org-roam to maintain cache consistency. This is achieved +by running @code{M-x org-roam-setup}. To ensure that Org-roam is available on +startup, one can place this in their Emacs configuration: @lisp -(add-hook 'after-init-hook 'org-roam-mode) +(require 'org-roam) +(org-roam-setup) @end lisp To build the cache manually, one can run @code{M-x org-roam-db-build-cache}. Cache builds may take a while the first time, but is often instantaneous in subsequent runs because it only reprocesses modified files. -Let us now create our first note. Call @code{M-x org-roam-find-file}. This shows a -list of titles for notes that reside in @code{org-roam-directory}. It should show -nothing right now, since there are no notes in the directory. Entering the title -of the note you wish to create, and pressing @code{RET} should begin the note +@node Creating and Linking Nodes +@section @strong{TODO} Creating and Linking Nodes + +Org-roam makes it easy to create notes and link them together. There are 2 main +functions for creating nodes: + +@itemize +@item +@code{org-roam-node-insert}: creates a node if it does not exist, and inserts a +link to the node at point. + +@item +@code{org-roam-node-find}: creates a node if it does not exist, and visits the +node. +@end itemize + +Let's first try @code{org-roam-node-find}. Calling @code{M-x org-roam-node-find} will +show a list of titles for nodes that reside in @code{org-roam-directory}. It should +show nothing right now, since there are no notes in the directory. Enter the +title of the note you wish to create, and press @code{RET}. This begins the note creation process. This process uses @code{org-capture}'s templating system, and can be customized (see @ref{The Templating System}). Using the default template, pressing @code{C-c C-c} finishes the note capture. -By default, Org-roam updates the cache asynchronously in the background to -avoid getting in the way of writing. Org-roam queues updates to the files, -waits for you to be idle for 2 seconds, and then automatically triggers -updating the cache. After the cache has been updated, running @code{M-x -org-roam-find-file} again should show the note you have created, and selecting -that entry will bring you to that note @footnote{Depending on your completion framework, you may need to press TAB to -see the list.}. One can customize the waiting -time by setting @code{org-roam-db-update-idle-seconds}; or change the cache update -to be triggered immediately after buffer save by setting -@code{org-roam-db-update-method} to @code{'immediate}. +Now that we have a node, we can try inserting a link to the node using @code{M-x +org-roam-node-insert}. This brings up the list of nodes, which should contain +the node you just created. Selecting the node will insert an @code{id:} link to the +node. If you instead entered a title that does not exist, you will once again be +brought through the node creation process. To enable link auto-completion, +see @ref{Completion}. -For experienced @code{org-capture} users, the behavior of @code{M-x org-roam-find-file} -may seem unfamiliar: after finishing a capture with @code{C-c C-c}, you are returned -not to the original buffer from which you called @code{M-x org-roam-find-file}, but -to a buffer pointing to the note you just created. For the usual @code{org-capture} -behavior you can call @code{M-x org-roam-capture} instead of @code{M-x org-roam-find-file}. +@node Viewing the links +@chapter @strong{TODO} Viewing the links -Org-roam makes it easy to create notes, and link them together. To link notes -together, we call @code{M-x org-roam-insert}. This brings up a prompt with a list of -title for existing notes. Selecting an existing entry will create and insert a -link to the current file. Entering a non-existent title will create a new note -with that title. Good usage of Org-roam requires liberally linking files: this -facilitates building up a dense graph of inter-connected notes. +Org-roam provides an interface to view relationships with other notes +(backlinks, reference links, unlinked references etc.). To pop up this info +buffer, call @code{M-x org-roam-buffer}. -Org-roam provides an interface to view backlinks. It shows backlinks for the -currently active Org-roam note, along with some surrounding context. To toggle -the visibility of this buffer, call @code{M-x org-roam}. - -For a visual representation of the notes and their connections, Org-roam also -provides graphing capabilities, using Graphviz. It generates graphs with notes -as nodes, and links between them as edges. The generated graph can be used to -navigate to the files, but this requires some additional setup (see @ref{Roam Protocol}). - -@node Files -@chapter Files - -In Org-roam, notes typically consist of multiple files, where each file is a -zettel. - -While the bulk of Org-roam's functionality is built on top of vanilla Org-mode, -Org-roam adds several Org-roam-specific keywords to support additional -functionality. - -This section explains the important components of a file, and the extensions to -Org-mode. +@node Node Properties +@chapter @strong{TODO} Node Properties @menu -* File Titles:: -* File Tags:: -* File Refs:: +* Standard Org properties:: +* Aliases:: +* Refs:: @end menu -@node File Titles -@section File Titles +@node Standard Org properties +@section @strong{TODO} Standard Org properties -To easily find a note, a title needs to be prescribed to a note. +@node Aliases +@section @strong{TODO} Aliases -A note can have many titles: this allows a note to be referred to by different -names, which is especially useful for topics or concepts with acronyms. For -example, for a note like ``World War 2'', it may be desirable to also refer to it -using the acronym ``WWII''. - -Org-roam calls @code{org-roam--extract-titles} to extract titles. It uses the -variable @code{org-roam-title-sources}, to control how the titles are extracted. The -title extraction methods supported are: - -@itemize -@item -@code{'title}: This extracts the title using the file @code{#+title} property - -@item -@code{'headline}: This extracts the title from the first headline in the Org file - -@item -@code{'alias}: This extracts a list of titles using the @code{#+roam_alias} property. -The aliases are space-delimited, and can be multi-worded using quotes. -@end itemize - -Take for example the following org file: - -@example -#+title: World War 2 -#+roam_alias: "WWII" "World War II" - -* Headline -@end example - -@multitable {aaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaa} -@headitem Method -@tab Titles -@item @code{'title} -@tab '(``World War 2'') -@item @code{'headline} -@tab '(``Headline'') -@item @code{'alias} -@tab '(``WWII'' ``World War II'') -@end multitable - -If no title is provided, Org-roam defaults to using the file-path. - -@menu -* Customizing Title Extraction:: -@end menu - -@node Customizing Title Extraction -@subsection Customizing Title Extraction - -To control how Org-roam extracts titles, customize @code{org-roam-title-sources}. If -all methods of title extraction return no results, the file-name is used as the -note's title. - -@defopt org-roam-title-sources - -The list of sources from which to retrieve a note title. -Each element in the list is either: -@end defopt - -@itemize -@item -a symbol -- this symbol corresponds to a title retrieval function, which -returns the list of titles for the current buffer -@itemize -@item -a list of symbols -- symbols in the list are treated as with (1). The -return value of this list is the first symbol in the list returning a -non-nil value. -@end itemize - -The return results of the root list are concatenated. - -For example the setting: '((title headline) alias) means the following: - -@itemize -@item -Return the 'title + 'alias, if the title of current buffer is non-empty; - -@item -Or return 'headline + 'alias otherwise. -@end itemize - -The currently supported symbols are: - -@code{'title} -The @code{#+title} property of org file. - -@code{'alias} -The @code{#+roam_alias} property of the org file, using -space-delimited strings. - -@code{'headline} -The first headline in the org file. -@end itemize - -Adding your own title extraction method requires two steps. First, define a -method @code{(defun org-roam--extract-titles-foo () ...)}, where @code{foo} a -self-prescribed name for the title extraction method. This method takes no -arguments, and returns a list of strings (titles). Finally, push the symbol -@code{foo} into @code{org-roam-title-sources}. You may need to rebuild the cache from -scratch to re-process all files to pick up the new titles. - -@node File Tags -@section File Tags - -Tags are used as meta-data for files: they facilitate interactions with notes -where titles are insufficient. For example, tags allow for categorization of -notes: differentiating between bibliographical and structure notes during -interactive commands. - -By default, tags are extracted from the @code{#+roam_tags} property. To add -additional extraction methods, see @ref{Customizing Tag Extraction}. - -@menu -* Customizing Tag Extraction:: -@end menu - -@node Customizing Tag Extraction -@subsection Customizing Tag Extraction - -Org-roam calls @code{org-roam--extract-tags} to extract tags from files. The variable -@code{org-roam-tag-sources}, to control how tags are extracted. - -@defopt org-roam-tag-sources -@end defopt - -Sources to obtain tags from. - -It should be a list of symbols representing any of the following extraction -methods: - -@code{'prop} - Extract tags from the @code{#+roam_tags} property. - Tags are space delimited. - Tags may contain spaces if they are double-quoted. - e.g. @code{#+roam_tags: TAG "tag with spaces"} - -@code{'vanilla} - Extract vanilla org-mode tags, including @code{#+FILETAGS} and - inherited tags. - -@code{'all-directories} - Extract sub-directories relative to @code{org-roam-directory}. - That is, if a file is located at relative path foo/bar/file.org, - the file will have tags ``foo'' and ``bar''. - -@code{'last-directory} - Extract the last directory relative to `org-roam-directory'. - That is, if a file is located at relative path foo/bar/file.org, - the file will have tag \``bar\''. - -@code{'first-directory} - Extract the first directory relative to @code{org-roam-directory}. - That is, if a file is located at relative path foo/bar/file.org, - the file will have tag ``foo'' - -By default, only the @code{'prop} extraction method is enabled. To enable the other -extraction methods, you may modify @code{org-roam-tag-sources}, for example: - -@lisp -(setq org-roam-tag-sources '(prop last-directory)) -@end lisp - -Adding your own tag extraction method requires two steps. First, define a method -@code{(defun org-roam--extract-tags-foo (file) ...)}, where @code{foo} a self-prescribed -name for the tag extraction method. This method takes the file path as an -argument, and returns a list of strings (titles). Finally, push the symbol @code{foo} -into @code{org-roam-tag-sources}. You may need to rebuild the cache from scratch to -re-process all files to pick up the new tags. - -@node File Refs -@section File Refs +@node Refs +@section @strong{TODO} Refs Refs are unique identifiers for files. For example, a note for a website may contain a ref: @@ -849,6 +716,159 @@ You may assign multiple refs to a single file, for example when you want multiple papers in a series to share the same note, or an article has a citation key and a URL at the same time. +@node The Org-roam Buffer +@chapter @strong{TODO} The Org-roam Buffer + +@node Styling Org-roam +@chapter @strong{TODO} Styling Org-roam + +@node Completion +@chapter @strong{TODO} Completion + +@node Encryption +@chapter @strong{TODO} Encryption + +@node Org-roam protocol +@chapter @strong{TODO} Org-roam protocol + +@node Diagnosing and Repair +@chapter @strong{TODO} Diagnosing and Repair + +@node Building Extensions +@chapter @strong{TODO} Building Extensions + +@menu +* Public Interface:: +@end menu + +@node Public Interface +@section @strong{TODO} Public Interface + +Database (for developers) +@itemize +@item +querying: (org-roam-db-query) + +@item +updating full database: (org-roam-db-sync) + +@item +update current file: (org-roam-db-update-file) + +@item +remove file from database: (org-roam-db-clear-file) +@end itemize + +Nodes: +@itemize +@item +List all nodes + +@item +Read a node in from completion + +@item +Get node at point +@end itemize + +Links: +@itemize +@item +Get backlinks for node +@end itemize + +Tags: +@itemize +@item +Add tag to node + +@item +delete tag for node + +@item +Get tags for node +@end itemize + +Aliases: +@itemize +@item +Add alias to node + +@item +Delete alias for node + +@item +Get aliases for node +@end itemize + +Ref: +@itemize +@item +Add ref to node +@end itemize + +Capture: + +Navigation: +@itemize +@item +Find or create a node +@end itemize + +@node The Org-mode Ecosystem +@chapter @strong{TODO} The Org-mode Ecosystem + +@node Frequently Asked Questions +@chapter @strong{TODO} Frequently Asked Questions + +@node Developer's Guide to Org-roam +@chapter @strong{TODO} Developer's Guide to Org-roam + +@menu +* Org-roam's Design Principles:: +@end menu + +@node Org-roam's Design Principles +@section Org-roam's Design Principles + +Org-roam is primarily motivated by the need for a dual representation. We +(humans) love operating in a plain-text environment. The syntax rules of +Org-mode are simple and fit snugly within our brain. This also allows us to use +the tools and packages we love to explore and edit our notes. Org-mode is simply +the most powerful plain-text format available, with support for images, @LaTeX{}, +TODO planning and much more. + +But this plain-text format is simply ill-suited for exploration of these notes: +plain-text is simply not amenable for answering large-scale, complex queries +(e.g. how many tasks do I have that are due by next week?). Interfaces such as +Org-agenda slow to a crawl when the number of files becomes unwieldy, which +can quickly become the case. + +At its core, Org-roam provides a database abstraction layer, providing a dual +representation of what's already available in plain-text. This allows us +(humans) to continue working with plain-text, while programs can utilize the +database layer to perform complex queries. These capabilities include, but are +not limited to: + +@itemize +@item +link graph traversal and visualization + +@item +Instantaneous SQL-like queries on headlines +@itemize +@item +What are my TODOs, scheduled for X, or due by Y@? +@end itemize +@end itemize + +All of these functionality is powered by this database abstraction layer. Hence, +at its core Org-roam's primary goal is to provide a resilient dual +representation that is cheap to maintain, easy to understand, and is as +up-to-date as it possibly can. Org-roam also then exposes an API to this +database abstraction layer for users who would like to perform programmatic +queries on their Org files. + @node The Templating System @chapter The Templating System @@ -918,7 +938,7 @@ The template is given a description of @code{"default"}. @code{(function org-roam--capture-get-point)} should not be changed. @item -@code{"%?"} is the template inserted on each call to @code{org-roam-capture--capture}. +@code{"%?"} is the template inserted on each call to @code{org-roam-capture-}. This template means don't insert any content, but place the cursor here. @item @@ -990,10 +1010,9 @@ org-roam}. @menu * Directories and Files:: -* The Org-roam Buffer:: +* The Org-roam Buffer: The Org-roam Buffer (1). * Org-roam Files:: * Org-roam Faces:: -* The Database:: @end menu @node Directories and Files @@ -1021,7 +1040,7 @@ with multiple Org-roam instances. Files matching this regular expression are excluded from the Org-roam. @end defvar -@node The Org-roam Buffer +@node The Org-roam Buffer (1) @section The Org-roam Buffer The Org-roam buffer displays backlinks for the currently active Org-roam note. @@ -1086,27 +1105,6 @@ The @code{org-roam-link-current} face corresponds to links to the same file it i The @code{org-roam-link-invalid} face is applied to links that are broken. These are links to files or IDs that cannot be found. -@node The Database -@section @strong{TODO} The Database - -Org-roam is backed by a Sqlite database. - -@defopt org-roam-db-update-method - -Method to update the Org-roam database. - -@code{'immediate}: Update the database immediately upon file changes. - -@code{'idle-timer}: Updates the database if dirty, if Emacs idles for -@code{org-roam-db-update-idle-seconds}. -@end defopt - -@defopt org-roam-db-update-idle-seconds - -Number of idle seconds before triggering an Org-roam database update. This is -only valid if @code{org-roam-db-update-method} is @code{'idle-timer}. -@end defopt - @node Inserting Links @chapter Inserting Links @@ -1194,7 +1192,7 @@ represents the cursor: @code{[[|]]}: completes for a file title @item -@code{[[roam:]]}: completes for a file title +@code{[[roam:|]]}: completes for a file title @item @code{[[*|]]}: completes for a headline within this file @@ -1206,6 +1204,9 @@ represents the cursor: @code{[[roam:foo*|]]} completes a headline within the file with title ``foo'' @end itemize +If you don't see the literal display of your links like the above examples, +call @code{M-x org-toggle-link-display} + Completions account for the current input. For example, for @code{[[f|]]}, the completions (by default) only show for files with titles that start with ``f''. @@ -1279,7 +1280,7 @@ title is @code{"Index"}. Opens the Index file in the current @code{org-roam-directory}. @end defun -@node Encryption +@node Encryption (1) @chapter Encryption One may wish to keep private, encrypted files. Org-roam supports encryption (via @@ -1617,7 +1618,7 @@ in the generated graph. @node The roam-ref protocol @section The roam-ref protocol -This protocol finds or creates a new note with a given @code{roam_key} (see @ref{Files}): +This protocol finds or creates a new note with a given @code{roam_key}: @image{images/roam-ref,,,,gif} @@ -1720,7 +1721,7 @@ template @code{j} will put its notes under the heading ‘Journal’. Create an entry in the daily note for today. -When @code{goto} is non-nil, go the note without creating an entry. +When @code{goto} is non-nil, go to the note without creating an entry. @end defun @defun @code{org-roam-dailies-find-today} @@ -1986,22 +1987,6 @@ using @uref{https://github.com/raxod502/el-patch, el-patch}: (eval-when-compile (require 'el-patch)) - -(use-package deft - ;; same as above... - :config/el-patch - (defun deft-parse-title (file contents) - "Parse the given FILE and CONTENTS and determine the title. -If `deft-use-filename-as-title' is nil, the title is taken to -be the first non-empty line of the FILE. Else the base name of the FILE is -used as title." - (el-patch-swap (if deft-use-filename-as-title - (deft-base-filename file) - (let ((begin (string-match "^.+$" contents))) - (if begin - (funcall deft-parse-title-function - (substring contents begin (match-end 0)))))) - (org-roam-db--get-title file)))) @end lisp The Deft interface can slow down quickly when the number of files get huge. @@ -2105,6 +2090,9 @@ file with the right @samp{#+roam_key}. @uref{https://www.leonrische.me/fc/index.html, Org-fc} is a spaced repetition system that scales well with a large number of files. Other alternatives include @uref{https://orgmode.org/worg/org-contrib/org-drill.html, org-drill}, and @uref{https://github.com/abo-abo/pamparam, pamparam}. +To use Anki for spaced repetition, @uref{https://github.com/louietan/anki-editor, anki-editor} allows you to write your cards in +Org-mode, and sync your cards to Anki via @uref{https://github.com/FooSoft/anki-connect#installation, anki-connect}. + @node FAQ @chapter FAQ diff --git a/makem.sh b/makem.sh deleted file mode 100755 index 89204b6..0000000 --- a/makem.sh +++ /dev/null @@ -1,1087 +0,0 @@ -#!/usr/bin/env bash - -# * makem.sh --- Script to aid building and testing Emacs Lisp packages - -# https://github.com/alphapapa/makem.sh - -# * Commentary: - -# makem.sh is a script helps to build, lint, and test Emacs Lisp -# packages. It aims to make linting and testing as simple as possible -# without requiring per-package configuration. - -# It works similarly to a Makefile in that "rules" are called to -# perform actions such as byte-compiling, linting, testing, etc. - -# Source and test files are discovered automatically from the -# project's Git repo, and package dependencies within them are parsed -# automatically. - -# Output is simple: by default, there is no output unless errors -# occur. With increasing verbosity levels, more detail gives positive -# feedback. Output is colored by default to make reading easy. - -# The script can run Emacs with the developer's local Emacs -# configuration, or with a clean, "sandbox" configuration that can be -# optionally removed afterward. This is especially helpful when -# upstream dependencies may have released new versions that differ -# from those installed in the developer's personal configuration. - -# * License: - -# 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 . - -# * Functions - -function usage { - cat <$file <$file <$file <"$file" <$file <&1) - - # Set output file. - output_file=$(mktemp) || die "Unable to make output file." - paths_temp+=("$output_file") - - # Run Emacs. - debug "run_emacs: ${emacs_command[@]} $@ &>\"$output_file\"" - "${emacs_command[@]}" "$@" &>"$output_file" - - # Check exit code and output. - exit=$? - [[ $exit != 0 ]] \ - && debug "Emacs exited non-zero: $exit" - - [[ $verbose -gt 1 || $exit != 0 ]] \ - && cat $output_file - - return $exit -} - -# ** Compilation - -function batch-byte-compile { - debug "batch-byte-compile: ERROR-ON-WARN:$compile_error_on_warn" - - [[ $compile_error_on_warn ]] && local error_on_warn=(--eval "(setq byte-compile-error-on-warn t)") - - run_emacs \ - "${error_on_warn[@]}" \ - --funcall batch-byte-compile \ - "$@" -} - -# ** Files - -function dirs-project { - # Echo list of directories to be used in load path. - files-project-feature | dirnames - files-project-test | dirnames -} - -function files-project-elisp { - # Echo list of Elisp files in project. - git ls-files 2>/dev/null \ - | egrep "\.el$" \ - | filter-files-exclude-default \ - | filter-files-exclude-args -} - -function files-project-feature { - # Echo list of Elisp files that are not tests and provide a feature. - files-project-elisp \ - | egrep -v "$test_files_regexp" \ - | filter-files-feature -} - -function files-project-test { - # Echo list of Elisp test files. - files-project-elisp | egrep "$test_files_regexp" -} - -function dirnames { - # Echo directory names for files on STDIN. - while read file - do - dirname "$file" - done -} - -function filter-files-exclude-default { - # Filter out paths (STDIN) which should be excluded by default. - egrep -v "(/\.cask/|-autoloads.el|-macs.el|.dir-locals)" -} - -function filter-files-exclude-args { - # Filter out paths (STDIN) which are excluded with --exclude. - if [[ ${files_exclude[@]} ]] - then - ( - # We use a subshell to set IFS temporarily so we can send - # the list of files to grep -F. This is ugly but more - # correct than replacing spaces with line breaks. Note - # that, for some reason, using IFS="\n" or IFS='\n' doesn't - # work, and a literal line break seems to be required. - IFS=" -" - grep -Fv "${files_exclude[*]}" - ) - else - cat - fi -} - -function filter-files-feature { - # Read paths on STDIN and echo ones that (provide 'a-feature). - while read path - do - egrep "^\\(provide '" "$path" &>/dev/null \ - && echo "$path" - done -} - -function args-load-files { - # For file in $@, echo "--load $file". - for file in "$@" - do - printf -- '--load %q ' "$file" - done -} - -function args-load-path { - # Echo load-path arguments. - for path in $(dirs-project | sort -u) - do - printf -- '-L %q ' "$path" - done -} - -function test-files-p { - # Return 0 if $files_project_test is non-empty. - [[ "${files_project_test[@]}" ]] -} - -function buttercup-tests-p { - # Return 0 if Buttercup tests are found. - test-files-p || die "No tests found." - debug "Checking for Buttercup tests..." - - grep "(require 'buttercup)" "${files_project_test[@]}" &>/dev/null -} - -function ert-tests-p { - # Return 0 if ERT tests are found. - test-files-p || die "No tests found." - debug "Checking for ERT tests..." - - # We check for this rather than "(require 'ert)", because ERT may - # already be loaded in Emacs and might not be loaded with - # "require" in a test file. - grep "(ert-deftest" "${files_project_test[@]}" &>/dev/null -} - -function dependencies { - # Echo list of package dependencies. - - # Search package headers. - egrep '^;; Package-Requires: ' $(files-project-feature) $(files-project-test) \ - | egrep -o '\([^([:space:]][^)]*\)' \ - | egrep -o '^[^[:space:])]+' \ - | sed -r 's/\(//g' \ - | egrep -v '^emacs$' # Ignore Emacs version requirement. - - # Search Cask file. - if [[ -r Cask ]] - then - egrep '\(depends-on "[^"]+"' Cask \ - | sed -r -e 's/\(depends-on "([^"]+)".*/\1/g' - fi - - # Search -pkg.el file. - if [[ $(git ls-files ./*-pkg.el 2>/dev/null) ]] - then - sed -nr 's/.*\(([-[:alnum:]]+)[[:blank:]]+"[.[:digit:]]+"\).*/\1/p' $(git ls-files ./*-pkg.el 2>/dev/null) - fi -} - -# ** Sandbox - -function sandbox { - # Initialize sandbox. - - # *** Sandbox arguments - - # Check or make user-emacs-directory. - if [[ $sandbox_dir ]] - then - # Directory given as argument: ensure it exists. - if ! [[ -d $sandbox_dir ]] - then - debug "Making sandbox directory: $sandbox_dir" - mkdir -p "$sandbox_dir" || die "Unable to make sandbox dir." - fi - - # Add Emacs version-specific subdirectory, creating if necessary. - sandbox_dir="$sandbox_dir/$(emacs-version)" - if ! [[ -d $sandbox_dir ]] - then - mkdir "$sandbox_dir" || die "Unable to make sandbox subdir: $sandbox_dir" - fi - else - # Not given: make temp directory, and delete it on exit. - local sandbox_dir=$(mktemp -d) || die "Unable to make sandbox dir." - paths_temp+=("$sandbox_dir") - fi - - # Make argument to load init file if it exists. - init_file="$sandbox_dir/init.el" - [[ -r $init_file ]] \ - && local args_load_init_file=(--load "$init_file") - - # Set sandbox args. This is a global variable used by the run_emacs function. - args_sandbox=( - --title "makem.sh: $(basename $(pwd)) (sandbox: $sandbox_dir)" - --eval "(setq user-emacs-directory (file-truename \"$sandbox_dir\"))" - --eval "(setq user-init-file (file-truename \"$init_file\"))" - "${args_load_init_file[@]}" - ) - - # Add package-install arguments for dependencies. - if [[ $install_deps ]] - then - local deps=($(dependencies)) - debug "Installing dependencies: ${deps[@]}" - - for package in "${deps[@]}" - do - args_sandbox_package_install+=(--eval "(package-install '$package)") - done - fi - - # Add package-install arguments for linters. - if [[ $install_linters ]] - then - debug "Installing linters: package-lint relint" - - args_sandbox_package_install+=(--eval "(package-install 'package-lint)" - --eval "(package-install 'relint)") - fi - - # *** Install packages into sandbox - - if [[ ${args_sandbox_package_install[@]} ]] - then - # Initialize the sandbox (installs packages once rather than for every rule). - debug "Initializing sandbox..." - - run_emacs \ - --eval "(package-refresh-contents)" \ - "${args_sandbox_package_install[@]}" \ - || die "Unable to initialize sandbox." - fi - - debug "Sandbox initialized." -} - -# ** Utility - -function cleanup { - # Remove temporary paths (${paths_temp[@]}). - - for path in "${paths_temp[@]}" - do - if [[ $debug ]] - then - debug "Debugging enabled: not deleting temporary path: $path" - elif [[ -r $path ]] - then - rm -rf "$path" - else - debug "Temporary path doesn't exist, not deleting: $path" - fi - done -} - -function echo-unset-p { - # Echo 0 if $1 is set, otherwise 1. IOW, this returns the exit - # code of [[ $1 ]] as STDOUT. - [[ $1 ]] - echo $? -} - -function ensure-package-available { - # If package $1 is available, return 0. Otherwise, return 1, and - # if $2 is set, give error otherwise verbose. Outputting messages - # here avoids repetition in callers. - local package=$1 - local direct_p=$2 - - if ! run_emacs --load $package &>/dev/null - then - if [[ $direct_p ]] - then - error "$package not available." - else - verbose 2 "$package not available." - fi - return 1 - fi -} - -function ensure-tests-available { - # If tests of type $1 (like "ERT") are available, return 0. Otherwise, if - # $2 is set, give an error and return 1; otherwise give verbose message. $1 - # should have a corresponding predicate command, like ert-tests-p for ERT. - local test_name=$1 - local test_command="${test_name,,}-tests-p" # Converts name to lowercase. - local direct_p=$2 - - if ! $test_command - then - if [[ $direct_p ]] - then - error "$test_name tests not found." - else - verbose 2 "$test_name tests not found." - fi - return 1 - fi -} - -function echo_color { - # This allows bold, italic, etc. without needing a function for - # each variation. - local color_code="COLOR_$1" - shift - - if [[ $color ]] - then - echo -e "${!color_code}${@}${COLOR_off}" - else - echo "$@" - fi -} -function debug { - if [[ $debug ]] - then - function debug { - echo_color yellow "DEBUG ($(ts)): $@" >&2 - } - debug "$@" - else - function debug { - true - } - fi -} - -function error_nonblocking { - echo_color red "ERROR ($(ts)): $@" >&2 - ((errors_nonblocking++)) -} - -function error { - echo_color red "ERROR ($(ts)): $@" >&2 - ((errors++)) - return 1 -} -function die { - [[ $@ ]] && error "$@" - exit $errors -} -function log { - echo "LOG ($(ts)): $@" >&2 -} -function log_color { - local color_name=$1 - shift - echo_color $color_name "LOG ($(ts)): $@" >&2 -} -function success { - if [[ $verbose -ge 2 ]] - then - log_color green "$@" >&2 - fi -} -function verbose { - # $1 is the verbosity level, rest are echoed when appropriate. - if [[ $verbose -ge $1 ]] - then - [[ $1 -eq 1 ]] && local color_name=blue - [[ $1 -ge 2 ]] && local color_name=cyan - - shift - log_color $color_name "$@" >&2 - fi -} - -function ts { - date "+%Y-%m-%d %H:%M:%S" -} - -function emacs-version { - # Echo Emacs version number. - - # Don't use run_emacs function, which does more than we need. - "${emacs_command[@]}" -Q --batch --eval "(princ emacs-version)" \ - || die "Unable to get Emacs version." -} - -function rule-p { - # Return 0 if $1 is a rule. - [[ $1 =~ ^(lint-?|tests?)$ ]] \ - || [[ $1 =~ ^(batch|interactive)$ ]] \ - || [[ $(type -t "$2" 2>/dev/null) =~ function ]] -} - -# * Rules - -# These functions are intended to be called as rules, like a Makefile. -# Some rules test $1 to determine whether the rule is being called -# directly or from a meta-rule; if directly, an error is given if the -# rule can't be run, otherwise it's skipped. - -function all { - verbose 1 "Running all rules..." - - lint - tests -} - -function compile { - [[ $compile ]] || return 0 - unset compile # Only compile once. - - verbose 1 "Compiling..." - debug "Byte-compile files: ${files_project_byte_compile[@]}" - - batch-byte-compile "${files_project_byte_compile[@]}" \ - && success "Compiling finished without errors." \ - || error "Compilation failed." -} - -function batch { - # Run Emacs with $args_batch and with project source and test files loaded. - verbose 1 "Executing Emacs with arguments: ${args_batch[@]}" - - run_emacs \ - $(args-load-files "${files_project_feature[@]}" "${files_project_test[@]}") \ - "${args_batch_interactive[@]}" -} - -function interactive { - # Run Emacs interactively. Most useful with --sandbox and --install-deps. - unset arg_batch - run_emacs \ - $(args-load-files "${files_project_feature[@]}" "${files_project_test[@]}") \ - "${args_batch_interactive[@]}" - arg_batch="--batch" -} - -function lint { - verbose 1 "Linting..." - - lint-checkdoc - lint-compile - lint-declare - # lint-indent - lint-package - lint-regexps -} - -function lint-checkdoc { - verbose 1 "Linting checkdoc..." - - local checkdoc_file="$(elisp-checkdoc-file)" - paths_temp+=("$checkdoc_file") - - run_emacs \ - --load="$checkdoc_file" \ - "${files_project_feature[@]}" \ - && success "Linting checkdoc finished without errors." \ - || error "Linting checkdoc failed." -} - -function lint-compile { - verbose 1 "Linting compilation..." - - compile_error_on_warn=true - batch-byte-compile "${files_project_byte_compile[@]}" \ - && success "Linting compilation finished without errors." \ - || error "Linting compilation failed." - unset compile_error_on_warn -} - -function lint-declare { - verbose 1 "Linting declarations..." - - local check_declare_file="$(elisp-check-declare-file)" - paths_temp+=("$check_declare_file") - - run_emacs \ - --load "$check_declare_file" \ - -f makem-check-declare-files-and-exit \ - "${files_project_feature[@]}" \ - && success "Linting declarations finished without errors." \ - || error "Linting declarations failed." -} - -function lint-indent { - verbose 1 "Linting indentation..." - - # We load project source files as well, because they may contain - # macros with (declare (indent)) rules which must be loaded to set - # indentation. - - run_emacs \ - --load "$(elisp-lint-indent-file)" \ - $(args-load-files "${files_project_feature[@]}" "${files_project_test[@]}") \ - --funcall makem-lint-indent-batch-and-exit \ - "${files_project_feature[@]}" "${files_project_test[@]}" \ - && success "Linting indentation finished without errors." \ - || error "Linting indentation failed." -} - -function lint-package { - ensure-package-available package-lint $1 || return $(echo-unset-p $1) - - verbose 1 "Linting package..." - - run_emacs \ - --load package-lint \ - --funcall package-lint-batch-and-exit \ - "${files_project_feature[@]}" \ - && success "Linting package finished without errors." \ - || error_nonblocking "Linting package failed." -} - -function lint-regexps { - ensure-package-available relint $1 || return $(echo-unset-p $1) - - verbose 1 "Linting regexps..." - - run_emacs \ - --load relint \ - --funcall relint-batch \ - "${files_project_source[@]}" \ - && success "Linting regexps finished without errors." \ - || error "Linting regexps failed." -} - -function tests { - verbose 1 "Running all tests..." - - test-ert - test-buttercup -} - -function test-ert-interactive { - verbose 1 "Running ERT tests interactively..." - - unset arg_batch - run_emacs \ - $(args-load-files "${files_project_test[@]}") \ - --eval "(ert-run-tests-interactively t)" - arg_batch="--batch" -} - -function test-buttercup { - ensure-tests-available Buttercup $1 || return $(echo-unset-p $1) - compile || die - - verbose 1 "Running Buttercup tests..." - - local buttercup_file="$(elisp-buttercup-file)" - paths_temp+=("$buttercup_file") - - run_emacs \ - $(args-load-files "${files_project_test[@]}") \ - -f buttercup-run \ - && success "Buttercup tests finished without errors." \ - || error "Buttercup tests failed." -} - -function test-ert { - ensure-tests-available ERT $1 || return $(echo-unset-p $1) - compile || die - - verbose 1 "Running ERT tests..." - debug "Test files: ${files_project_test[@]}" - - run_emacs \ - $(args-load-files "${files_project_test[@]}") \ - -f ert-run-tests-batch-and-exit \ - && success "ERT tests finished without errors." \ - || error "ERT tests failed." -} - -# * Defaults - -test_files_regexp='^((tests?|t)/)|-test.el$|^test-' - -emacs_command=("emacs") -errors=0 -errors_nonblocking=0 -verbose=0 -compile=true -arg_batch="--batch" - -# MAYBE: Disable color if not outputting to a terminal. (OTOH, the -# colorized output is helpful in CI logs, and I don't know if, -# e.g. GitHub Actions logging pretends to be a terminal.) -color=true - -# TODO: Using the current directory (i.e. a package's repo root directory) in -# load-path can cause weird errors in case of--you guessed it--stale .ELC files, -# the zombie problem that just won't die. It's incredible how many different ways -# this problem presents itself. In this latest example, an old .ELC file, for a -# .EL file that had since been renamed, was present on my local system, which meant -# that an example .EL file that hadn't been updated was able to "require" that .ELC -# file's feature without error. But on another system (in this case, trying to -# setup CI using GitHub Actions), the old .ELC was not present, so the example .EL -# file was not able to load the feature, which caused a byte-compilation error. - -# In this case, I will prevent such example files from being compiled. But in -# general, this can cause weird problems that are tedious to debug. I guess -# the best way to fix it would be to actually install the repo's code as a -# package into the sandbox, but doing that would require additional tooling, -# pulling in something like Quelpa or package-build--and if the default recipe -# weren't being used, the actual recipe would have to be fetched off MELPA or -# something, which seems like getting too smart for our own good. - -# TODO: Emit a warning if .ELC files that don't match any .EL files are detected. - -# ** Colors - -COLOR_off='\e[0m' -COLOR_black='\e[0;30m' -COLOR_red='\e[0;31m' -COLOR_green='\e[0;32m' -COLOR_yellow='\e[0;33m' -COLOR_blue='\e[0;34m' -COLOR_purple='\e[0;35m' -COLOR_cyan='\e[0;36m' -COLOR_white='\e[0;37m' - -# ** Package system args - -args_package_archives=( - --eval "(add-to-list 'package-archives '(\"gnu\" . \"https://elpa.gnu.org/packages/\") t)" - --eval "(add-to-list 'package-archives '(\"melpa\" . \"https://melpa.org/packages/\") t)" -) - -args_org_package_archives=( - --eval "(add-to-list 'package-archives '(\"org\" . \"https://orgmode.org/elpa/\") t)" -) - -args_package_init=( - --eval "(package-initialize)" -) - -elisp_org_package_archive="(add-to-list 'package-archives '(\"org\" . \"https://orgmode.org/elpa/\") t)" - -# * Args - -args=$(getopt -n "$0" \ - -o dhe:E:i:s:vf:CO \ - -l exclude:,emacs:,install-deps,install-linters,debug,debug-load-path,help,install:,verbose,file:,no-color,no-compile,no-org-repo,sandbox: \ - -- "$@") \ - || { usage; exit 1; } -eval set -- "$args" - -while true -do - case "$1" in - --install-deps) - install_deps=true - ;; - --install-linters) - install_linters=true - ;; - -d|--debug) - debug=true - verbose=2 - args_debug=(--eval "(setq init-file-debug t)" - --eval "(setq debug-on-error t)") - ;; - --debug-load-path) - debug_load_path=true - ;; - -h|--help) - usage - exit - ;; - -E|--emacs) - shift - emacs_command=($1) - ;; - -i|--install) - shift - args_sandbox_package_install+=(--eval "(package-install '$1)") - ;; - -s|--sandbox) - sandbox=true - # Check whether next argument is an option, rule, or a sandbox directory. - if [[ $2 ]] && ! [[ $2 =~ ^-- ]] \ - && ! rule-p "$2" - then - debug "Sandbox dir: $1" - shift - sandbox_dir="$1" - else - debug "No sandbox dir: installing dependencies." - install_deps=true - # HACK: Next argument is another option, so prepend blank arg to the - # argument list so it will be processed by next loop iteration. getopts - # doesn't allow options to have optional arguments, so we do this manually. - if [[ $2 =~ ^- ]] - then - # Next argument is an option: process it next. - new_args=("" "$@") - else - # Next argument is not an option: put it on the end. - new_arg="$2" - shift - shift - new_args=("" "$@" "$new_arg") - fi - debug "Setting new args: ${new_args[@]}" - set -- "${new_args[@]}" - fi - ;; - -v|--verbose) - ((verbose++)) - ;; - -e|--exclude) - shift - debug "Excluding file: $1" - files_exclude+=("$1") - ;; - -f|--file) - shift - project_source_files+=("$1") - project_byte_compile_files+=("$1") - ;; - -O|--no-org-repo) - unset elisp_org_package_archive - ;; - --no-color) - unset color - ;; - -C|--no-compile) - unset compile - ;; - --) - # Remaining args (required; do not remove) - shift - rest=("$@") - break - ;; - esac - - shift -done - -debug "ARGS: $args" -debug "Remaining args: ${rest[@]}" - -# Set package elisp (which depends on --no-org-repo arg). -package_initialize_file="$(elisp-package-initialize-file)" -paths_temp+=("$package_initialize_file") - -# * Main - -trap cleanup EXIT INT TERM - -# Discover project files. -files_project_feature=($(files-project-feature)) -files_project_test=($(files-project-test)) -files_project_byte_compile=("${files_project_feature[@]}" "${files_project_test[@]}") - -debug "EXCLUDING FILES: ${files_exclude[@]}" -debug "FEATURE FILES: ${files_project_feature[@]}" -debug "TEST FILES: ${files_project_test[@]}" -debug "BYTE-COMPILE FILES: ${files_project_byte_compile[@]}" - -if ! [[ ${files_project_feature[@]} ]] -then - error "No files specified and not in a git repo." - exit 1 -fi - -# Set load path. -args_load_paths=($(args-load-path)) -debug "LOAD PATH ARGS: ${args_load_paths[@]}" - -# If rules include linters and sandbox-dir is unspecified, install -# linters automatically. -if [[ $sandbox && ! $sandbox_dir ]] && [[ "${rest[@]}" =~ lint ]] -then - debug "Installing linters automatically." - install_linters=true -fi - -# Initialize sandbox. -[[ $sandbox ]] && sandbox - -# Run rules. -for rule in "${rest[@]}" -do - if [[ $batch || $interactive ]] - then - debug "Adding batch/interactive argument: $rule" - args_batch_interactive+=("$rule") - - elif [[ $rule = batch ]] - then - # Remaining arguments are passed to Emacs. - batch=true - elif [[ $rule = interactive ]] - then - # Remaining arguments are passed to Emacs. - interactive=true - - elif type -t "$rule" 2>/dev/null | grep function &>/dev/null - then - # Pass called-directly as $1 to indicate that the rule is - # being called directly rather than from a meta-rule. - $rule called-directly - elif [[ $rule = test ]] - then - # Allow the "tests" rule to be called as "test". Since "test" - # is a shell builtin, this workaround is required. - tests - else - error "Invalid rule: $rule" - fi -done - -# Batch/interactive rules. -[[ $batch ]] && batch -[[ $interactive ]] && interactive - -if [[ $errors -gt 0 ]] -then - log_color red "Finished with $errors errors and $errors_nonblocking non-blocking errors." -else - success "Finished with $errors errors and $errors_nonblocking non-blocking errors." -fi - -exit $errors diff --git a/org-roam-buffer.el b/org-roam-buffer.el deleted file mode 100644 index f4df75e..0000000 --- a/org-roam-buffer.el +++ /dev/null @@ -1,357 +0,0 @@ -;;; org-roam-buffer.el --- Metadata buffer -*- coding: utf-8; lexical-binding: t; -*- - -;; Copyright © 2020 Jethro Kuan - -;; Author: Jethro Kuan -;; URL: https://github.com/org-roam/org-roam -;; Keywords: org-mode, roam, convenience -;; Version: 1.2.3 -;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (s "1.12.0") (org "9.3") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2")) - -;; This file is NOT part of GNU Emacs. - -;; 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, 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 GNU Emacs; see the file COPYING. If not, write to the -;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, -;; Boston, MA 02110-1301, USA. - -;;; Commentary: -;; -;; This library provides the org-roam-buffer functionality for org-roam -;;; Code: -;;;; Library Requires -(eval-when-compile (require 'subr-x)) -(require 'cl-lib) -(require 'dash) -(require 's) -(require 'f) -(require 'ol) -(require 'org-element) -(require 'org-roam-macs) - -(defvar org-roam-directory) -(defvar org-link-frame-setup) -(defvar org-return-follows-link) -(defvar org-roam-backlinks-mode) -(defvar org-roam-last-window) -(defvar org-ref-cite-types) ;; in org-ref-core.el -(defvar org-roam-mode) -(defvar org-roam--org-link-bracket-typed-re) - -(declare-function org-roam-db--ensure-built "org-roam-db") -(declare-function org-roam-db--get-title "org-roam-db") -(declare-function org-roam-db-has-file-p "org-roam-db") -(declare-function org-roam--extract-refs "org-roam") -(declare-function org-roam--extract-titles "org-roam") -(declare-function org-roam--get-backlinks "org-roam") -(declare-function org-roam-backlinks-mode "org-roam") -(declare-function org-roam-mode "org-roam") -(declare-function org-roam--find-file "org-roam") -(declare-function org-roam-format-link "org-roam") -(declare-function org-roam-link-get-path "org-roam-link") - -(defcustom org-roam-buffer-position 'right - "Position of `org-roam' buffer. -Valid values are - * left, - * right, - * top, - * bottom, - * a function returning one of the above." - :type '(choice (const left) - (const right) - (const top) - (const bottom) - function) - :group 'org-roam) - -(defcustom org-roam-buffer-width 0.33 - "Width of `org-roam' buffer. -Has an effect if and only if `org-roam-buffer-position' is `left' or `right'." - :type 'number - :group 'org-roam) - -(defcustom org-roam-buffer-height 0.27 - "Height of `org-roam' buffer. -Has an effect if and only if `org-roam-buffer-position' is `top' or `bottom'." - :type 'number - :group 'org-roam) - - -(defcustom org-roam-buffer "*org-roam*" - "Org-roam buffer name." - :type 'string - :group 'org-roam) - -(defcustom org-roam-buffer-prepare-hook '(org-roam-buffer--insert-title - org-roam-buffer--insert-backlinks - org-roam-buffer--insert-ref-links) - "Hook run in the `org-roam-buffer' before it is displayed." - :type 'hook - :group 'org-roam) - -(defcustom org-roam-buffer-preview-function #'org-roam-buffer--preview - "Function to obtain preview contents for a given link. -The function takes in two arguments, the FILE containing the -link, and the POINT of the link." - :type 'function - :group 'org-roam) - -(defcustom org-roam-buffer-window-parameters nil - "Additional window parameters for the `org-roam-buffer' side window. -For example: (setq org-roam-buffer-window-parameters '((no-other-window . t)))" - :type '(alist) - :group 'org-roam) - -(defvar org-roam-buffer--current nil - "Currently displayed file in `org-roam' buffer.") - -(defun org-roam-buffer--find-file (file) - "Open FILE in the window `org-roam' was called from." - (setq file (expand-file-name file)) - (let ((last-window org-roam-last-window)) - (if (window-valid-p last-window) - (progn (with-selected-window last-window - (org-roam--find-file file)) - (select-window last-window)) - (org-roam--find-file file)))) - -(defun org-roam-buffer--insert-title () - "Insert the org-roam-buffer title." - (insert (propertize (org-roam-db--get-title - (buffer-file-name org-roam-buffer--current)) - 'font-lock-face - 'org-document-title))) - -(defun org-roam-buffer--preview (file point) - "Get preview content for FILE at POINT." - (save-excursion - (org-roam--with-temp-buffer file - (goto-char point) - (let ((elem (org-element-at-point))) - (or (org-element-property :raw-value elem) - (when-let ((begin (org-element-property :begin elem)) - (end (org-element-property :end elem))) - (string-trim (buffer-substring-no-properties begin end)))))))) - -(defun org-roam-buffer--pluralize (string number) - "Conditionally pluralize STRING if NUMBER is above 1." - (let ((l (pcase number - ((pred (listp)) (length number)) - ((pred (integerp)) number) - (wrong-type (signal 'wrong-type-argument - `((listp integerp) - ,wrong-type)))))) - (concat string (when (> l 1) "s")))) - -(defun org-roam-buffer-expand-links (content orig-path) - "Crawl CONTENT for relative links and corrects them to be correctly displayed. -ORIG-PATH is the path where the CONTENT originated." - (with-temp-buffer - (insert content) - (goto-char (point-min)) - (let (link link-type) - (while (re-search-forward org-roam--org-link-bracket-typed-re (point-max) t) - (setq link-type (match-string 1) - link (match-string 2)) - (when (and (string-equal link-type "file") - (f-relative-p link)) - (replace-match (org-roam-link-get-path (expand-file-name link (file-name-directory orig-path))) - nil t nil 2)))) - (buffer-string))) - -(defun org-roam-buffer--insert-ref-links () - "Insert ref backlinks for the current buffer." - (when-let* ((refs (with-temp-buffer - (insert-buffer-substring org-roam-buffer--current) - (org-roam--extract-refs))) - (paths (mapcar #'cdr refs))) - (if-let* ((key-backlinks (mapcan #'org-roam--get-backlinks paths)) - (grouped-backlinks (--group-by (nth 0 it) key-backlinks))) - (progn - (insert (let ((l (length key-backlinks))) - (format "\n\n* %d %s\n" - l (org-roam-buffer--pluralize "Ref Backlink" l)))) - (dolist (group grouped-backlinks) - (let ((file-from (car group)) - (bls (cdr group))) - (insert (format "** %s\n" - (org-roam-format-link file-from - (org-roam-db--get-title file-from) - "file"))) - (dolist (backlink bls) - (pcase-let ((`(,file-from _ ,props) backlink)) - (insert (if-let ((content (funcall org-roam-buffer-preview-function file-from (plist-get props :point)))) - (propertize (org-roam-buffer-expand-links content file-from) - 'help-echo "mouse-1: visit backlinked note" - 'file-from file-from - 'file-from-point (plist-get props :point)) - "") - "\n\n")))))) - (insert "\n\n* No ref backlinks!")))) - -(defun org-roam-buffer--insert-backlinks () - "Insert the org-roam-buffer backlinks string for the current buffer." - (let (props file-from) - (if-let* ((file-path (buffer-file-name org-roam-buffer--current)) - (titles (with-current-buffer org-roam-buffer--current - (org-roam--extract-titles))) - (backlinks (org-roam--get-backlinks (push file-path titles))) - (grouped-backlinks (--group-by (nth 0 it) backlinks))) - (progn - (insert (let ((l (length backlinks))) - (format "\n\n* %d %s\n" - l (org-roam-buffer--pluralize "Backlink" l)))) - (dolist (group grouped-backlinks) - (setq file-from (car group)) - (setq props (mapcar (lambda (row) (nth 2 row)) (cdr group))) - (setq props (seq-sort-by (lambda (p) (plist-get p :point)) #'< props)) - (insert (format "** %s\n" - (org-roam-format-link file-from - (org-roam-db--get-title file-from) - "file"))) - (dolist (prop props) - (insert "*** " - (if-let ((outline (plist-get prop :outline))) - (-> outline - (string-join " > ") - (org-roam-buffer-expand-links file-from)) - "Top") - "\n" - (if-let ((content (funcall org-roam-buffer-preview-function file-from (plist-get prop :point)))) - (propertize - (s-trim (s-replace "\n" " " (org-roam-buffer-expand-links content file-from))) - 'help-echo "mouse-1: visit backlinked note" - 'file-from file-from - 'file-from-point (plist-get prop :point)) - "") - "\n\n")))) - (insert "\n\n* No backlinks!")))) - -(defun org-roam-buffer-update () - "Update the `org-roam-buffer'." - (interactive) - (org-roam-db--ensure-built) - (let* ((source-org-roam-directory org-roam-directory)) - (with-current-buffer org-roam-buffer - ;; When dir-locals.el is used to override org-roam-directory, - ;; org-roam-buffer should have a different local org-roam-directory and - ;; default-directory, as relative links are relative from the overridden - ;; org-roam-directory. - (setq-local org-roam-directory source-org-roam-directory) - (setq-local default-directory source-org-roam-directory) - ;; Locally overwrite the file opening function to re-use the - ;; last window org-roam was called from - (setq-local org-link-frame-setup - (cons '(file . org-roam--find-file) org-link-frame-setup)) - (let ((inhibit-read-only t)) - (erase-buffer) - (unless (eq major-mode 'org-mode) - (org-mode)) - (unless org-roam-backlinks-mode - (org-roam-backlinks-mode)) - (make-local-variable 'org-return-follows-link) - (setq org-return-follows-link t) - (run-hooks 'org-roam-buffer-prepare-hook) - (read-only-mode 1))))) - - -(cl-defun org-roam-buffer--update-maybe (&key redisplay) - "Reconstructs `org-roam-buffer'. -This needs to be quick or infrequent, because this is run at -`post-command-hook'. If REDISPLAY, force an update of -`org-roam-buffer'." - (let ((buffer (window-buffer))) - (when (and (or redisplay - (not (eq org-roam-buffer--current buffer))) - (eq 'visible (org-roam-buffer--visibility)) - (buffer-file-name buffer) - (org-roam-db-has-file-p (buffer-file-name buffer))) - (setq org-roam-buffer--current buffer) - (org-roam-buffer-update)))) - -;;;; Toggling the org-roam buffer -(define-inline org-roam-buffer--visibility () - "Return whether the current visibility state of the org-roam buffer. -Valid states are 'visible, 'exists and 'none." - (declare (side-effect-free t)) - (inline-quote - (cond - ((get-buffer-window org-roam-buffer) 'visible) - ((get-buffer org-roam-buffer) 'exists) - (t 'none)))) - -(defun org-roam-buffer--set-width (width) - "Set the width of `org-roam-buffer' to `WIDTH'." - (unless (one-window-p) - (let ((window-size-fixed) - (w (max width window-min-width))) - (cond - ((> (window-width) w) - (shrink-window-horizontally (- (window-width) w))) - ((< (window-width) w) - (enlarge-window-horizontally (- w (window-width)))))))) - -(defun org-roam-buffer--set-height (height) - "Set the height of `org-roam-buffer' to `HEIGHT'." - (unless (one-window-p) - (let ((window-size-fixed) - (h (max height window-min-height))) - (cond - ((> (window-height) h) - (shrink-window (- (window-height) h))) - ((< (window-height) h) - (enlarge-window (- h (window-height)))))))) - -(defun org-roam-buffer--get-create () - "Set up the `org-roam' buffer at `org-roam-buffer-position'." - (let ((position (if (functionp org-roam-buffer-position) - (funcall org-roam-buffer-position) - org-roam-buffer-position))) - (save-selected-window - (-> (get-buffer-create org-roam-buffer) - (display-buffer-in-side-window - `((side . ,position) - (window-parameters . ,org-roam-buffer-window-parameters))) - (select-window)) - (pcase position - ((or 'right 'left) - (org-roam-buffer--set-width - (round (* (frame-width) org-roam-buffer-width)))) - ((or 'top 'bottom) - (org-roam-buffer--set-height - (round (* (frame-height) org-roam-buffer-height)))))))) - -(defun org-roam-buffer-activate () - "Activate display of the `org-roam-buffer'." - (interactive) - (unless org-roam-mode (org-roam-mode)) - (setq org-roam-last-window (get-buffer-window)) - (org-roam-buffer--get-create)) - -(defun org-roam-buffer-deactivate () - "Deactivate display of the `org-roam-buffer'." - (interactive) - (setq org-roam-last-window (get-buffer-window)) - (delete-window (get-buffer-window org-roam-buffer))) - -(defun org-roam-buffer-toggle-display () - "Toggle display of the `org-roam-buffer'." - (interactive) - (pcase (org-roam-buffer--visibility) - ('visible (org-roam-buffer-deactivate)) - ((or 'exists 'none) (org-roam-buffer-activate)))) - -(provide 'org-roam-buffer) - -;;; org-roam-buffer.el ends here diff --git a/org-roam-capture.el b/org-roam-capture.el index 529d578..75f3e90 100644 --- a/org-roam-capture.el +++ b/org-roam-capture.el @@ -5,8 +5,8 @@ ;; Author: Jethro Kuan ;; URL: https://github.com/org-roam/org-roam ;; Keywords: org-mode, roam, convenience -;; Version: 1.2.3 -;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (s "1.12.0") (org "9.3") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2")) +;; Version: 2.0.0 +;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite "1.0.0") (magit-section "2.90.1")) ;; This file is NOT part of GNU Emacs. @@ -29,259 +29,393 @@ ;; ;; This library provides capture functionality for org-roam ;;; Code: +;;; ;;;; Library Requires (require 'org-capture) -(require 'org-roam-macs) +(eval-when-compile + (require 'org-roam-macs) + (require 'org-macs)) (require 'org-roam-db) (require 'dash) -(require 's) (require 'cl-lib) ;; Declarations -(defvar org-roam-encrypt-files) +(declare-function org-roam-ref-add "org-roam" (ref)) +(declare-function org-datetree-find-date-create "org-datetree" (date &optional keep-restriction)) +(declare-function org-datetree-find-month-create "org-datetree" (d &optional keep-restriction)) + (defvar org-roam-directory) -(defvar org-roam-mode) -(defvar org-roam-title-to-slug-function) -(defvar org-roam-file-extensions) -(declare-function org-roam--get-title-path-completions "org-roam") -(declare-function org-roam--get-ref-path-completions "org-roam") -(declare-function org-roam--find-file "org-roam") -(declare-function org-roam-format-link "org-roam") -(declare-function org-roam--split-ref "org-roam") -(declare-function org-roam-mode "org-roam") -(declare-function org-roam-completion--completing-read "org-roam-completion") - -(defvar org-roam-capture--file-path nil - "The file path for the Org-roam capture. -This variable is set during the Org-roam capture process.") - -(defvar org-roam-capture--info nil - "An alist of additional information passed to the Org-roam template. +(defvar org-roam-capture--node nil + "The node passed during an Org-roam capture. This variable is populated dynamically, and is only non-nil during the Org-roam capture process.") -(defvar org-roam-capture--context nil - "A symbol, that reflects the context for obtaining the exact point in a file. -This variable is populated dynamically, and is only active during -an Org-roam capture process. +(defvar org-roam-capture--info nil + "A property-list of additional information passed to the Org-roam template. +This variable is populated dynamically, and is only non-nil +during the Org-roam capture process.") -The `title' context is used in `org-roam-insert' and -`org-roam-find-file', where the capture process is triggered upon -trying to create a new file without that `title'. - -The `ref' context is used by `org-roam-protocol', where the -capture process is triggered upon trying to find or create a new -note with the given `ref'.") - -(defvar org-roam-capture-additional-template-props nil - "Additional props to be added to the Org-roam template.") - -(defconst org-roam-capture--template-keywords '(:file-name :head :olp) +(defconst org-roam-capture--template-keywords (list :if-new :id :link-description :call-location + :region :override-default-time) "Keywords used in `org-roam-capture-templates' specific to Org-roam.") (defcustom org-roam-capture-templates - `(("d" "default" plain (function org-roam-capture--get-point) - "%?" - :file-name "%<%Y%m%d%H%M%S>-${slug}" - :head "#+title: ${title}\n" + '(("d" "default" plain "%?" + :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" + "#+title: ${title}\n") :unnarrowed t)) - "Capture templates for Org-roam. -The Org-roam capture-templates builds on the default behaviours of -`org-capture-templates' by expanding them in 3 areas: + "Templates for the creation of new entries within Org-roam. -1. Template-expansion capabilities are extended with additional - custom syntax. See `org-roam-capture--fill-template' for more - details. +Each entry is a list with the following items: -2. The `:file-name' key is added, which defines the naming format - to use when creating new notes. This file-name is relative to - `org-roam-directory', and is without the file-extension. +keys The keys that will select the template, as a string, characters only, for + example \"a\" for a template to be selected with a single key, or + \"bt\" for selection with two keys. When using several keys, keys + using the same prefix must be together in the list and preceded by a + 2-element entry explaining the prefix key, for example: -3. The `:head' key is added, which contains the template that is - inserted upon the creation of a new file. This is where you - your note metadata should go. + (\"b\" \"Templates for marking stuff to buy\") -Each template should have the following structure: + The \"C\" key is used by default for quick access to the customization of + the template variable. But if you want to use that key for a template, + you can. -\(KEY DESCRIPTION `plain' `(function org-roam-capture--get-point)' - TEMPLATE - `:file-name' FILENAME-FORMAT - `:head' HEADER-FORMAT - `:unnarrowed t' - OPTIONS-PLIST) +description A short string describing the template, which will be shown + during selection. -The elements of a template-entry and their placement are the same -as in `org-capture-templates', except that the entry type must -always be the symbol `plain', and that the target must always be -the list `(function org-roam-capture--get-point)'. +type The type of entry. Valid types are: + entry an Org node, with a headline. Will be filed + as the child of the target entry or as a + top level entry. Its default template is: + \"* %?\n %a\" + item a plain list item, will be placed in the + first plain list at the target location. + Its default template is: + \"- %?\" + checkitem a checkbox item. This differs from the + plain list item only in so far as it uses a + different default template. Its default + template is: + \"- [ ] %?\" + table-line a new line in the first table at target location. + Its default template is: + \"| %? |\" + plain text to be inserted as it is. -Org-roam requires the plist elements `:file-name' and `:head' to -be present, and it’s recommended that `:unnarrowed' be set to t." +template The template for creating the capture item. + If it is an empty string or nil, a default template based on + the entry type will be used (see the \"type\" section above). + Instead of a string, this may also be one of: + + (file \"/path/to/template-file\") + (function function-returning-the-template) + + in order to get a template from a file, or dynamically + from a function. + +The template contains a compulsory :if-new property. This determines the +location of the new node. The :if-new property contains a list, supporting +the following options: + + (file \"path/to/file\") + The file will be created, and prescribed an ID. + + (file+head \"path/to/file\" \"head content\") + The file will be created, prescribed an ID, and head content will be + inserted into the file. + + (file+olp \"path/to/file\" (\"h1\" \"h2\")) + The file will be created, prescribed an ID. The OLP (h1, h2) will be + created, and the point placed after. + + (file+head+olp \"path/to/file\" \"head content\" (\"h1\" \"h2\")) + The file will be created, prescribed an ID. Head content will be + inserted at the start of the file. The OLP (h1, h2) will be created, + and the point placed after. + + (file+datetree \"path/to/file\" day) + The file will be created, prescribed an ID. Head content will be + inserted at the start of the file. The datetree will be created, + available options are day, week, month. + +The rest of the entry is a property list of additional options. Recognized +properties are: + + :prepend Normally newly captured information will be appended at + the target location (last child, last table line, + last list item...). Setting this property will + change that. + + :immediate-finish When set, do not offer to edit the information, just + file it away immediately. This makes sense if the + template only needs information that can be added + automatically. + + :jump-to-captured When set, jump to the captured entry when finished. + + :empty-lines Set this to the number of lines that should be inserted + before and after the new item. Default 0, only common + other value is 1. + + :empty-lines-before Set this to the number of lines that should be inserted + before the new item. Overrides :empty-lines for the + number lines inserted before. + + :empty-lines-after Set this to the number of lines that should be inserted + after the new item. Overrides :empty-lines for the + number of lines inserted after. + + :clock-in Start the clock in this item. + + :clock-keep Keep the clock running when filing the captured entry. + + :clock-resume Start the interrupted clock when finishing the capture. + Note that :clock-keep has precedence over :clock-resume. + When setting both to t, the current clock will run and + the previous one will not be resumed. + + :time-prompt Prompt for a date/time to be used for date/week trees + and when filling the template. + + :tree-type When `week', make a week tree instead of the month-day + tree. When `month', make a month tree instead of the + month-day tree. + + :unnarrowed Do not narrow the target buffer, simply show the + full buffer. Default is to narrow it so that you + only see the new stuff. + + :table-line-pos Specification of the location in the table where the + new line should be inserted. It should be a string like + \"II-3\", meaning that the new line should become the + third line before the second horizontal separator line. + + :kill-buffer If the target file was not yet visited by a buffer when + capture was invoked, kill the buffer again after capture + is finalized. + + :no-save Do not save the target file after finishing the capture. + +The template defines the text to be inserted. Often this is an +Org mode entry (so the first line should start with a star) that +will be filed as a child of the target headline. It can also be +freely formatted text. Furthermore, the following %-escapes will +be replaced with content and expanded: + + %[pathname] Insert the contents of the file given by + `pathname'. These placeholders are expanded at the very + beginning of the process so they can be used to extend the + current template. + %(sexp) Evaluate elisp `(sexp)' and replace it with the results. + Only placeholders pre-existing within the template, or + introduced with %[pathname] are expanded this way. Since this + happens after expanding non-interactive %-escapes, those can + be used to fill the expression. + %<...> The result of `format-time-string' on the ... format specification. + %t Time stamp, date only. The time stamp is the current time, + except when called from agendas with `\\[org-agenda-capture]' or + with `org-capture-use-agenda-date' set. + %T Time stamp as above, with date and time. + %u, %U Like the above, but inactive time stamps. + %i Initial content, copied from the active region. If + there is text before %i on the same line, such as + indentation, and %i is not inside a %(sexp), that prefix + will be added before every line in the inserted text. + %a Annotation, normally the link created with `org-store-link'. + %A Like %a, but prompt for the description part. + %l Like %a, but only insert the literal link. + %L Like %l, but without brackets (the link content itself). + %c Current kill ring head. + %x Content of the X clipboard. + %k Title of currently clocked task. + %K Link to currently clocked task. + %n User name (taken from the variable `user-full-name'). + %f File visited by current buffer when `org-capture' was called. + %F Full path of the file or directory visited by current buffer. + %:keyword Specific information for certain link types, see below. + %^g Prompt for tags, with completion on tags in target file. + %^G Prompt for tags, with completion on all tags in all agenda files. + %^t Like %t, but prompt for date. Similarly %^T, %^u, %^U. + You may define a prompt like: %^{Please specify birthday}t. + The default date is that of %t, see above. + %^C Interactive selection of which kill or clip to use. + %^L Like %^C, but insert as link. + %^{prop}p Prompt the user for a value for property `prop'. + A default value can be specified like this: + %^{prop|default}p. + %^{prompt} Prompt the user for a string and replace this sequence with it. + A default value and a completion table can be specified like this: + %^{prompt|default|completion2|completion3|...}. + %? After completing the template, position cursor here. + %\\1 ... %\\N Insert the text entered at the nth %^{prompt}, where N + is a number, starting from 1. + +Apart from these general escapes, you can access information specific to +the link type that is created. For example, calling `org-capture' in emails +or in Gnus will record the author and the subject of the message, which you +can access with \"%:from\" and \"%:subject\", respectively. Here is a +complete list of what is recorded for each link type. + +Link type | Available information +------------------------+------------------------------------------------------ +bbdb | %:type %:name %:company +vm, wl, mh, mew, rmail, | %:type %:subject %:message-id +gnus | %:from %:fromname %:fromaddress + | %:to %:toname %:toaddress + | %:fromto (either \"to NAME\" or \"from NAME\") + | %:date %:date-timestamp (as active timestamp) + | %:date-timestamp-inactive (as inactive timestamp) +gnus | %:group, for messages also all email fields +eww, w3, w3m | %:type %:url +info | %:type %:file %:node +calendar | %:type %:date + +When you need to insert a literal percent sign in the template, +you can escape ambiguous cases with a backward slash, e.g., \\%i. + +In addition to all of the above, Org-roam supports additional +substitutions within its templates. \"${foo}\" will look for the +foo property in the Org-roam node (see the `org-roam-node'). If +the property does not exist, the user will be prompted to fill in +the string value. + +Org-roam templates are NOT compatible with regular Org capture: +they rely on additional hacks and hooks to achieve the +streamlined user experience in Org-roam." :group 'org-roam - ;; Adapted from `org-capture-templates' - :type - '(repeat - (choice :value ("d" "default" plain (function org-roam-capture--get-point) - "%?" - :file-name "%<%Y%m%d%H%M%S>-${slug}" - :head "#+title: ${title}\n" - :unnarrowed t) - (list :tag "Multikey description" - (string :tag "Keys ") - (string :tag "Description")) - (list :tag "Template entry" - (string :tag "Keys ") - (string :tag "Description ") - (const :format "" plain) - (const :format "" (function org-roam-capture--get-point)) - (choice :tag "Template " - (string :tag "String" - :format "String:\n \ -Template string :\n%v") - (list :tag "File" - (const :format "" file) - (file :tag "Template file ")) - (list :tag "Function" - (const :format "" function) - (function :tag "Template function "))) - (const :format "File name format :" :file-name) - (string :format " %v" :value "#+title: ${title}\n") - (const :format "Header format :" :head) - (string :format "\n%v" :value "%<%Y%m%d%H%M%S>-${slug}") - (const :format "" :unnarrowed) (const :format "" t) - (plist :inline t - :tag "Options" - ;; Give the most common options as checkboxes - :options - (((const :format "%v " :prepend) (const t)) - ((const :format "%v " :immediate-finish) (const t)) - ((const :format "%v " :jump-to-captured) (const t)) - ((const :format "%v " :empty-lines) (const 1)) - ((const :format "%v " :empty-lines-before) (const 1)) - ((const :format "%v " :empty-lines-after) (const 1)) - ((const :format "%v " :clock-in) (const t)) - ((const :format "%v " :clock-keep) (const t)) - ((const :format "%v " :clock-resume) (const t)) - ((const :format "%v " :time-prompt) (const t)) - ((const :format "%v " :tree-type) (const week)) - ((const :format "%v " :table-line-pos) (string)) - ((const :format "%v " :kill-buffer) (const t)))))))) + :type '(repeat + (choice (list :tag "Multikey description" + (string :tag "Keys ") + (string :tag "Description")) + (list :tag "Template entry" + (string :tag "Keys ") + (string :tag "Description ") + (choice :tag "Capture Type " :value entry + (const :tag "Org entry" entry) + (const :tag "Plain list item" item) + (const :tag "Checkbox item" checkitem) + (const :tag "Plain text" plain) + (const :tag "Table line" table-line)) + (choice :tag "Template " + (string) + (list :tag "File" + (const :format "" file) + (file :tag "Template file")) + (list :tag "Function" + (const :format "" function) + (function :tag "Template function"))) + (plist :inline t + ;; Give the most common options as checkboxes + :options (((const :format "%v " :if-new) + (choice :tag "Node location" + (list :tag "File" + (const :format "" file) + (string :tag " File")) + (list :tag "File & Head Content" + (const :format "" file+head) + (string :tag " File") + (string :tag " Head Content")) + (list :tag "File & Outline path" + (const :format "" file+olp) + (string :tag " File") + (list :tag "Outline path" + (repeat (string :tag "Headline")))) + (list :tag "File & Head Content & Outline path" + (const :format "" file+head+olp) + (string :tag " File") + (string :tag " Head Content") + (list :tag "Outline path" + (repeat (string :tag "Headline")))))) + ((const :format "%v " :prepend) (const t)) + ((const :format "%v " :immediate-finish) (const t)) + ((const :format "%v " :jump-to-captured) (const t)) + ((const :format "%v " :empty-lines) (const 1)) + ((const :format "%v " :empty-lines-before) (const 1)) + ((const :format "%v " :empty-lines-after) (const 1)) + ((const :format "%v " :clock-in) (const t)) + ((const :format "%v " :clock-keep) (const t)) + ((const :format "%v " :clock-resume) (const t)) + ((const :format "%v " :time-prompt) (const t)) + ((const :format "%v " :tree-type) (const week)) + ((const :format "%v " :unnarrowed) (const t)) + ((const :format "%v " :table-line-pos) (string)) + ((const :format "%v " :kill-buffer) (const t)))))))) -(defcustom org-roam-capture-immediate-template - (append (car org-roam-capture-templates) '(:immediate-finish t)) - "Capture template to use for immediate captures in Org-roam. -This is a single template, so do not enclose it into a list. -See `org-roam-capture-templates' for details on templates." - :group 'org-roam - ;; Adapted from `org-capture-templates' - :type - '(list :tag "Template entry" - :value ("d" "default" plain (function org-roam-capture--get-point) - "%?" - :file-name "%<%Y%m%d%H%M%S>-${slug}" - :head "#+title: ${title}\n" - :unnarrowed t - :immediate-finish t) - (string :tag "Keys ") - (string :tag "Description ") - (const :format "" plain) - (const :format "" (function org-roam-capture--get-point)) - (choice :tag "Template " - (string :tag "String" - :format "String:\n \ -Template string :\n%v") - (list :tag "File" - (const :format "" file) - (file :tag "Template file ")) - (list :tag "Function" - (const :format "" function) - (function :tag "Template function "))) - (const :format "File name format :" :file-name) - (string :format " %v" :value "#+title: ${title}\n") - (const :format "Header format :" :head) - (string :format "\n%v" :value "%<%Y%m%d%H%M%S>-${slug}") - (const :format "" :unnarrowed) (const :format "" t) - (const :format "" :immediate-finish) (const :format "" t) - (plist :inline t - :tag "Options" - ;; Give the most common options as checkboxes - :options - (((const :format "%v " :prepend) (const t)) - ((const :format "%v " :jump-to-captured) (const t)) - ((const :format "%v " :empty-lines) (const 1)) - ((const :format "%v " :empty-lines-before) (const 1)) - ((const :format "%v " :empty-lines-after) (const 1)) - ((const :format "%v " :clock-in) (const t)) - ((const :format "%v " :clock-keep) (const t)) - ((const :format "%v " :clock-resume) (const t)) - ((const :format "%v " :time-prompt) (const t)) - ((const :format "%v " :tree-type) (const week)) - ((const :format "%v " :table-line-pos) (string)) - ((const :format "%v " :kill-buffer) (const t)))))) +(defvar org-roam-capture-new-node-hook (list #'org-roam-capture--insert-ref) + "Normal-mode hooks run when a new Org-roam node is created. +The current point is the point of the new node. +The hooks must not move the point.") (defcustom org-roam-capture-ref-templates - '(("r" "ref" plain #'org-roam-capture--get-point - "%?" - :file-name "${slug}" - :head "#+title: ${title}\n#+roam_key: ${ref}" + '(("r" "ref" plain "%?" + :if-new (file+head "${slug}.org" + "#+title: ${title}") :unnarrowed t)) "The Org-roam templates used during a capture from the roam-ref protocol. -Details on how to specify for the template is given in -`org-roam-capture-templates'." +See `org-roam-capture-templates' for the template documentation." :group 'org-roam - ;; Adapted from `org-capture-templates' - :type - '(repeat - (choice :value ("d" "default" plain (function org-roam-capture--get-point) - "%?" - :file-name "${slug}" - :head "#+title: ${title}\n#+roam_key: ${ref}\n" - :unnarrowed t) - (list :tag "Multikey description" - (string :tag "Keys ") - (string :tag "Description")) - (list :tag "Template entry" - (string :tag "Keys ") - (string :tag "Description ") - (const :format "" plain) - (const :format "" (function org-roam-capture--get-point)) - (choice :tag "Template " - (string :tag "String" - :format "String:\n \ -Template string :\n%v") - (list :tag "File" - (const :format "" file) - (file :tag "Template file ")) - (list :tag "Function" - (const :format "" function) - (function :tag "Template function "))) - (const :format "File name format :" :file-name) - (string :format " %v" :value "#+title: ${title}\n") - (const :format "Header format :" :head) - (string :format "\n%v" :value "%<%Y%m%d%H%M%S>-${slug}") - (const :format "" :unnarrowed) (const :format "" t) - (plist :inline t - :tag "Options" - ;; Give the most common options as checkboxes - :options - (((const :format "%v " :prepend) (const t)) - ((const :format "%v " :immediate-finish) (const t)) - ((const :format "%v " :jump-to-captured) (const t)) - ((const :format "%v " :empty-lines) (const 1)) - ((const :format "%v " :empty-lines-before) (const 1)) - ((const :format "%v " :empty-lines-after) (const 1)) - ((const :format "%v " :clock-in) (const t)) - ((const :format "%v " :clock-keep) (const t)) - ((const :format "%v " :clock-resume) (const t)) - ((const :format "%v " :time-prompt) (const t)) - ((const :format "%v " :tree-type) (const week)) - ((const :format "%v " :table-line-pos) (string)) - ((const :format "%v " :kill-buffer) (const t)))))))) + :type '(repeat + (choice (list :tag "Multikey description" + (string :tag "Keys ") + (string :tag "Description")) + (list :tag "Template entry" + (string :tag "Keys ") + (string :tag "Description ") + (choice :tag "Capture Type " :value entry + (const :tag "Org entry" entry) + (const :tag "Plain list item" item) + (const :tag "Checkbox item" checkitem) + (const :tag "Plain text" plain) + (const :tag "Table line" table-line)) + (choice :tag "Template " + (string) + (list :tag "File" + (const :format "" file) + (file :tag "Template file")) + (list :tag "Function" + (const :format "" function) + (function :tag "Template function"))) + (plist :inline t + ;; Give the most common options as checkboxes + :options (((const :format "%v " :if-new) + (choice :tag "Node location" + (list :tag "File" + (const :format "" file) + (string :tag " File")) + (list :tag "File & Head Content" + (const :format "" file+head) + (string :tag " File") + (string :tag " Head Content")) + (list :tag "File & Outline path" + (const :format "" file+olp) + (string :tag " File") + (list :tag "Outline path" + (repeat (string :tag "Headline")))) + (list :tag "File & Head Content & Outline path" + (const :format "" file+head+olp) + (string :tag " File") + (string :tag " Head Content") + (list :tag "Outline path" + (repeat (string :tag "Headline")))))) + ((const :format "%v " :prepend) (const t)) + ((const :format "%v " :immediate-finish) (const t)) + ((const :format "%v " :jump-to-captured) (const t)) + ((const :format "%v " :empty-lines) (const 1)) + ((const :format "%v " :empty-lines-before) (const 1)) + ((const :format "%v " :empty-lines-after) (const 1)) + ((const :format "%v " :clock-in) (const t)) + ((const :format "%v " :clock-keep) (const t)) + ((const :format "%v " :clock-resume) (const t)) + ((const :format "%v " :time-prompt) (const t)) + ((const :format "%v " :tree-type) (const week)) + ((const :format "%v " :unnarrowed) (const t)) + ((const :format "%v " :table-line-pos) (string)) + ((const :format "%v " :kill-buffer) (const t)))))))) (defun org-roam-capture-p () "Return t if the current capture process is an Org-roam capture. -This function is to only be called when org-capture-plist is +This function is to only be called when `org-capture-plist' is valid for the capture (i.e. initialization, and finalization of the capture)." (plist-get org-capture-plist :org-roam)) @@ -317,38 +451,45 @@ the capture)." (advice-add 'org-capture-finalize :before #'org-roam-capture--update-plist) +(defun org-roam-capture--finalize-find-file () + "Visit the buffer after Org-capture is done. +This function is to be called in the Org-capture finalization process. +ID is unused." + (switch-to-buffer (org-capture-get :buffer))) + +(defun org-roam-capture--finalize-insert-link () + "Insert a link to ID into the buffer where Org-capture was called. +ID is the Org id of the newly captured content. +This function is to be called in the Org-capture finalization process." + (when-let* ((mkr (org-roam-capture--get :call-location)) + (buf (marker-buffer mkr))) + (with-current-buffer buf + (when-let ((region (org-roam-capture--get :region))) + (org-roam-unshield-region (car region) (cdr region)) + (delete-region (car region) (cdr region)) + (set-marker (car region) nil) + (set-marker (cdr region) nil)) + (org-with-point-at mkr + (insert (org-link-make-string (concat "id:" (org-roam-capture--get :id)) + (org-roam-capture--get :link-description))))))) + (defun org-roam-capture--finalize () "Finalize the `org-roam-capture' process." - (let* ((finalize (org-roam-capture--get :finalize)) - ;; In case any regions were shielded before, unshield them - (region (when-let ((region (org-roam-capture--get :region))) - (org-roam-unshield-region (car region) (cdr region)))) - (beg (car region)) - (end (cdr region))) - (unless org-note-abort - (pcase finalize - ('find-file - (when-let ((file-path (org-roam-capture--get :file-path))) - (org-roam--find-file file-path) - (run-hooks 'org-roam-capture-after-find-file-hook))) - ('insert-link - (when-let* ((mkr (org-roam-capture--get :insert-at)) - (buf (marker-buffer mkr))) - (with-current-buffer buf - (when region - (delete-region (car region) (cdr region))) - (let ((path (org-roam-capture--get :file-path)) - (type (org-roam-capture--get :link-type)) - (desc (org-roam-capture--get :link-description))) - (if (eq (point) (marker-position mkr)) - (insert (org-roam-format-link path desc type)) - (org-with-point-at mkr - (insert (org-roam-format-link path desc type)))))))))) - (when region - (set-marker beg nil) - (set-marker end nil)) - (org-roam-capture--save-file-maybe) - (remove-hook 'org-capture-after-finalize-hook #'org-roam-capture--finalize))) + (when-let ((region (org-roam-capture--get :region))) + (org-roam-unshield-region (car region) (cdr region))) + (if org-note-abort + (when-let ((new-file (org-roam-capture--get :new-file))) + (org-roam-message "Deleting file for aborted capture %s" new-file) + (when (find-buffer-visiting new-file) + (kill-buffer (find-buffer-visiting new-file))) + (delete-file new-file)) + (when-let* ((finalize (org-roam-capture--get :finalize)) + (org-roam-finalize-fn (intern (concat "org-roam-capture--finalize-" + (symbol-name finalize))))) + (if (functionp org-roam-finalize-fn) + (funcall org-roam-finalize-fn) + (funcall finalize)))) + (remove-hook 'org-capture-after-finalize-hook #'org-roam-capture--finalize)) (defun org-roam-capture--install-finalize () "Install `org-roam-capture--finalize' if the capture is an Org-roam capture." @@ -357,105 +498,179 @@ the capture)." (add-hook 'org-capture-prepare-finalize-hook #'org-roam-capture--install-finalize) -(defun org-roam-capture--fill-template (str) - "Expand the template STR, returning the string. -This is an extension of org-capture's template expansion. +(defun org-roam-capture--fill-template (template &optional org-capture-p) + "Expand TEMPLATE and return it. +It expands ${var} occurrences in TEMPLATE. When ORG-CAPTURE-P, +also run Org-capture's template expansion." + (funcall (if org-capture-p #'org-capture-fill-template #'identity) + (org-roam-format + template + (lambda (key) + (let ((fn (intern key)) + (node-fn (intern (concat "org-roam-node-" key))) + (ksym (intern (concat ":" key)))) + (cond + ((fboundp fn) + (funcall fn org-roam-capture--node)) + ((fboundp node-fn) + (funcall node-fn org-roam-capture--node)) + ((plist-get org-roam-capture--info ksym) + (plist-get org-roam-capture--info ksym)) + (t (let ((r (completing-read (format "%s: " key) nil))) + (plist-put org-roam-capture--info ksym r) + r)))))))) -First, it expands ${var} occurrences in STR, using `org-roam-capture--info'. -If there is a ${var} with no matching var in the alist, the value -of var is prompted for via `completing-read'. +(defun org-roam-capture--insert-ref () + "Insert the ref if any." + (when-let ((ref (plist-get org-roam-capture--info :ref))) + (org-roam-ref-add ref))) -Next, it expands the remaining template string using -`org-capture-fill-template'." - (-> str - (s-format (lambda (key) - (or (s--aget org-roam-capture--info key) - (when-let ((val (completing-read (format "%s: " key) nil))) - (push (cons key val) org-roam-capture--info) - val))) nil) - (org-capture-fill-template))) +(defun org-roam-capture--goto-location () + "Initialize the buffer, and goto the location of the new capture. +Return the ID of the location." + (let (p) + (pcase (or (org-roam-capture--get :if-new) + (user-error "Template needs to specify `:if-new'")) + (`(file ,path) + (setq path (expand-file-name + (string-trim (org-roam-capture--fill-template path t)) + org-roam-directory)) + (unless (file-exists-p path) + (org-roam-capture--put :new-file path)) + (set-buffer (org-capture-target-buffer path)) + (widen) + (setq p (goto-char (point-min)))) + (`(file+olp ,path ,olp) + (setq path (expand-file-name + (string-trim (org-roam-capture--fill-template path t)) + org-roam-directory)) + (set-buffer (org-capture-target-buffer path)) + (unless (file-exists-p path) + (org-roam-capture--put :new-file path)) + (setq p (point-min)) + (let ((m (org-roam-capture-find-or-create-olp olp))) + (goto-char m)) + (widen)) + (`(file+head ,path ,head) + (setq path (expand-file-name + (string-trim (org-roam-capture--fill-template path t)) + org-roam-directory)) + (set-buffer (org-capture-target-buffer path)) + (unless (file-exists-p path) + (org-roam-capture--put :new-file path) + (insert (org-roam-capture--fill-template head t))) + (widen) + (setq p (goto-char (point-min)))) + (`(file+head+olp ,path ,head ,olp) + (setq path (expand-file-name + (string-trim (org-roam-capture--fill-template path t)) + org-roam-directory)) + (widen) + (set-buffer (org-capture-target-buffer path)) + (unless (file-exists-p path) + (org-roam-capture--put :new-file path) + (insert (org-roam-capture--fill-template head t))) + (setq p (point-min)) + (let ((m (org-roam-capture-find-or-create-olp olp))) + (goto-char m))) + (`(file+datetree ,path ,tree-type) + (setq path (expand-file-name + (string-trim (org-roam-capture--fill-template path t)) + org-roam-directory)) + (require 'org-datetree) + (widen) + (set-buffer (org-capture-target-buffer path)) + (unless (file-exists-p path) + (org-roam-capture--put :new-file path)) + (funcall + (pcase tree-type + (`week #'org-datetree-find-iso-week-create) + (`month #'org-datetree-find-month-create) + (_ #'org-datetree-find-date-create)) + (calendar-gregorian-from-absolute + (cond + (org-overriding-default-time + ;; Use the overriding default time. + (time-to-days org-overriding-default-time)) + ((org-capture-get :default-time) + (time-to-days (org-capture-get :default-time))) + ((org-capture-get :time-prompt) + ;; Prompt for date. Bind `org-end-time-was-given' so + ;; that `org-read-date-analyze' handles the time range + ;; case and returns `prompt-time' with the start value. + (let* ((org-time-was-given nil) + (org-end-time-was-given nil) + (prompt-time (org-read-date + nil t nil "Date for tree entry:"))) + (org-capture-put + :default-time + (if (or org-time-was-given + (= (time-to-days prompt-time) (org-today))) + prompt-time + ;; Use 00:00 when no time is given for another + ;; date than today? + (apply #'encode-time 0 0 + org-extend-today-until + (cl-cdddr (decode-time prompt-time))))) + (time-to-days prompt-time))) + (t + ;; Current date, possibly corrected for late night + ;; workers. + (org-today))))) + (setq p (point))) + (`(node ,title-or-id) + ;; first try to get ID, then try to get title/alias + (let ((node (or (org-roam-node-from-id title-or-id) + (org-roam-node-from-title-or-alias title-or-id) + (user-error "No node with title or id \"%s\" title-or-id")))) + (set-buffer (org-capture-target-buffer (org-roam-node-file node))) + (goto-char (org-roam-node-point node)) + (setq p (org-roam-node-point node))))) + (prog1 + ;; Setup `org-id' for the current capture target and return it back to + ;; the caller. + (save-excursion + (goto-char p) + (when-let* ((node org-roam-capture--node) + (id (org-roam-node-id node))) + (org-entry-put p "ID" id)) + (prog1 + (org-id-get-create) + (run-hooks 'org-roam-capture-new-node-hook))) + ;; Adjust the point only after ID was generated and polluted to the + ;; current target in the capture buffer. + (org-roam-capture--adjust-point-for-capture-type)))) -(defun org-roam-capture--save-file-maybe () - "Save the file conditionally. -The file is saved if the original value of :no-save is not t and -`org-note-abort' is not t. It is added to -`org-capture-after-finalize-hook'." - (cond - ((and (org-roam-capture--get :new-file) - org-note-abort) - (with-current-buffer (org-capture-get :buffer) - (set-buffer-modified-p nil) - (kill-buffer))) - ((and (not (org-roam-capture--get :orig-no-save)) - (not org-note-abort)) - (with-current-buffer (org-capture-get :buffer) - (save-buffer))))) +(defun org-roam-capture--adjust-point-for-capture-type (&optional pos) + "Reposition the point for template insertion dependently on the capture type. +Return the newly adjusted position of `point'. -(defun org-roam-capture--get-file-path (basename) - "Return path for Org-roam file with BASENAME." - (let* ((ext (or (car org-roam-file-extensions) - "org")) - (file (concat basename "." ext))) - (expand-file-name - (if org-roam-encrypt-files - (concat file ".gpg") - file) - org-roam-directory))) - -(defun org-roam-capture--new-file (&optional allow-existing-file-p) - "Return the path to file during an Org-roam capture. - -This function reads the file-name attribute of the currently -active Org-roam template. - -If the file path already exists, and not ALLOW-EXISTING-FILE-P, -raise a warning. - -Else, to insert the header content in the file, `org-capture' -prepends the `:head' property of the Org-roam capture template. - -To prevent the creation of a new file if the capture process is -aborted, we do the following: - -1. Save the original value of the capture template's :no-save. -2. Set the capture template's :no-save to t. -3. Add a function on `org-capture-before-finalize-hook' that saves -the file if the original value of :no-save is not t and -`org-note-abort' is not t." - (let* ((name-templ (or (org-roam-capture--get :file-name) - (user-error "Template needs to specify `:file-name'"))) - (new-id (s-trim (org-roam-capture--fill-template - name-templ))) - (file-path (org-roam-capture--get-file-path new-id)) - (roam-head (or (org-roam-capture--get :head) - "")) - (org-template (org-capture-get :template)) - (roam-template (concat roam-head org-template))) - (if (or (file-exists-p file-path) - (find-buffer-visiting file-path)) - (unless allow-existing-file-p - (lwarn '(org-roam) :warning - "Attempted to recreate existing file: %s. -This can happen when your org-roam db is not in sync with your notes. -Using existing file..." file-path)) - (make-directory (file-name-directory file-path) t) - (org-roam-capture--put :orig-no-save (org-capture-get :no-save) - :new-file t) - (pcase org-roam-capture--context - ('dailies - ;; Populate the header of the daily file before capture to prevent it - ;; from appearing in the buffer-restriction - (save-window-excursion - (find-file file-path) - (insert (substring (org-capture-fill-template (concat roam-head "*")) - 0 -2)) - (set-buffer-modified-p nil)) - (org-capture-put :template org-template)) - (_ - (org-capture-put :template roam-template - :type 'plain))) - (org-capture-put :no-save t)) - file-path)) +POS is the current position of point (an integer) inside the +currently active capture buffer, where the adjustment should +start to begin from. If it's nil, then it will default to +the current value of `point'." + (or pos (setq pos (point))) + (goto-char pos) + (let ((location-type (if (= pos 1) 'beginning-of-file 'heading-at-point))) + (and (eq location-type 'heading-at-point) + (cl-assert (org-at-heading-p))) + (pcase (org-capture-get :type) + (`plain + (cl-case location-type + (beginning-of-file + (if (org-capture-get :prepend) + (let ((el (org-element-at-point))) + (while (and (not (eobp)) + (memq (org-element-type el) + '(drawer property-drawer keyword comment comment-block horizontal-rule))) + (goto-char (org-element-property :end el)) + (setq el (org-element-at-point)))) + (goto-char (org-entry-end-position)))) + (heading-at-point + (if (org-capture-get :prepend) + (org-end-of-meta-data t) + (goto-char (org-entry-end-position)))))))) + (point)) (defun org-roam-capture-find-or-create-olp (olp) "Return a marker pointing to the entry at OLP in the current buffer. @@ -486,8 +701,10 @@ you can catch it with `condition-case'." ;; Create heading if it doesn't exist (goto-char end) (unless (bolp) (newline)) - (org-insert-heading nil nil t) - (unless (= lmax 1) (org-do-demote)) + (let (org-insert-heading-respect-content) + (org-insert-heading nil nil t)) + (unless (= lmax 1) + (dotimes (_ level) (org-do-demote))) (insert heading) (setq end (point)) (goto-char start) @@ -501,81 +718,63 @@ you can catch it with `condition-case'." end (save-excursion (org-end-of-subtree t t)))) (point-marker)))) -(defun org-roam-capture--get-ref-path (type path) - "Get the file path to the ref with TYPE and PATH." - (caar (org-roam-db-query - [:select [file] - :from refs - :where (= type $s1) - :and (= ref $s2) - :limit 1] - type path))) +(defun org-roam-capture--get-node-from-ref (ref) + "Return the node from reference REF." + (save-match-data + (when (string-match org-link-plain-re ref) + (let ((type (match-string 1 ref)) + (path (match-string 2 ref))) + (when-let ((id (caar (org-roam-db-query + [:select [nodes:id] + :from refs + :left-join nodes + :on (= refs:node-id nodes:id) + :where (= refs:type $s1) + :and (= refs:ref $s2) + :limit 1] + type path)))) + (org-roam-populate (org-roam-node-create :id id))))))) (defun org-roam-capture--get-point () "Return exact point to file for org-capture-template. -The file to use is dependent on the context: - -If the search is via title, it is assumed that the file does not -yet exist, and Org-roam will attempt to create new file. - -If the search is via daily notes, 'time will be passed via -`org-roam-capture--info'. This is used to alter the default time -in `org-capture-templates'. - -If the search is via ref, it is matched against the Org-roam database. -If there is no file with that ref, a file with that ref is created. - This function is used solely in Org-roam's capture templates: see `org-roam-capture-templates'." - (let* ((file-path (pcase org-roam-capture--context - ('capture - (or (cdr (assoc 'file org-roam-capture--info)) - (org-roam-capture--new-file))) - ('title - (org-roam-capture--new-file)) - ('dailies - (org-capture-put :default-time (cdr (assoc 'time org-roam-capture--info))) - (org-roam-capture--new-file 'allow-existing)) - ('ref - (if-let ((ref (cdr (assoc 'ref org-roam-capture--info)))) - (pcase (org-roam--split-ref ref) - (`(,type . ,path) - (or (org-roam-capture--get-ref-path type path) - (org-roam-capture--new-file))) - (_ (user-error "%s is not a valid ref" ref))) - (error "Ref not found in `org-roam-capture--info'"))) - (_ (error "Invalid org-roam-capture-context"))))) + (when (org-roam-capture--get :override-default-time) + (org-capture-put :default-time (org-roam-capture--get :override-default-time))) + (let ((id (cond ((plist-get org-roam-capture--info :ref) + (if-let ((node (org-roam-capture--get-node-from-ref + (plist-get org-roam-capture--info :ref)))) + (progn + (set-buffer (org-capture-target-buffer (org-roam-node-file node))) + (goto-char (org-roam-node-point node)) + (widen) + (org-end-of-subtree t t)) + (org-roam-capture--goto-location))) + ((and (org-roam-node-file org-roam-capture--node) + (org-roam-node-point org-roam-capture--node)) + (set-buffer (org-capture-target-buffer (org-roam-node-file org-roam-capture--node))) + (goto-char (org-roam-node-point org-roam-capture--node)) + (widen) + (org-end-of-subtree t t) + (org-roam-node-id org-roam-capture--node)) + (t + (org-roam-capture--goto-location))))) (org-capture-put :template (org-roam-capture--fill-template (org-capture-get :template))) - (org-roam-capture--put :file-path file-path - :finalize (or (org-capture-get :finalize) - (org-roam-capture--get :finalize))) - (while org-roam-capture-additional-template-props - (let ((prop (pop org-roam-capture-additional-template-props)) - (val (pop org-roam-capture-additional-template-props))) - (org-roam-capture--put prop val))) - (set-buffer (org-capture-target-buffer file-path)) - (widen) - (if-let* ((olp (org-roam-capture--get :olp))) - (condition-case err - (when-let ((marker (org-roam-capture-find-or-create-olp olp))) - (goto-char marker) - (set-marker marker nil)) - (error - (when (org-roam-capture--get :new-file) - (kill-buffer)) - (signal (car err) (cdr err)))) - (goto-char (point-max))))) + (org-roam-capture--put :id id) + (org-roam-capture--put :finalize (or (org-capture-get :finalize) + (org-roam-capture--get :finalize))))) -(defun org-roam-capture--convert-template (template) - "Convert TEMPLATE from Org-roam syntax to `org-capture-templates' syntax." +(defun org-roam-capture--convert-template (template &optional props) + "Convert TEMPLATE from Org-roam syntax to `org-capture-templates' syntax. +PROPS is a plist containing additional Org-roam specific +properties to be added to the template." (pcase template - (`(,_key ,_description) template) - (`(,key ,description ,type ,target . ,rest) - (let ((converted `(,key ,description ,type ,target - ,(unless (keywordp (car rest)) (pop rest)))) - org-roam-plist - options) + (`(,_key ,_desc) + template) + (`(,key ,desc ,type ,body . ,rest) + (setq rest (append rest props)) + (let (org-roam-plist options) (while rest (let* ((key (pop rest)) (val (pop rest)) @@ -583,36 +782,34 @@ This function is used solely in Org-roam's capture templates: see (when (and custom (not val)) (user-error "Invalid capture template format: %s\nkey %s cannot be nil" template key)) - (push val (if custom org-roam-plist options)) - (push key (if custom org-roam-plist options)))) - (append converted options `(:org-roam ,org-roam-plist)))) - (_ (user-error "Invalid capture template format: %s" template)))) + (if custom + (setq org-roam-plist (plist-put org-roam-plist key val)) + (setq options (plist-put options key val))))) + (append `(,key ,desc ,type #'org-roam-capture--get-point ,body) + options + (list :org-roam org-roam-plist)))) + (_ + (signal 'invalid-template template)))) -(defcustom org-roam-capture-after-find-file-hook nil - "Hook that is run right after an Org-roam capture process is finalized. -Suitable for moving point." - :group 'org-roam - :type 'hook) - -(defcustom org-roam-capture-function #'org-capture - "Function that is invoked to start the `org-capture' process." - :group 'org-roam - :type 'function) - -(defun org-roam-capture--capture (&optional goto keys) - "Create a new file, and return the path to the edited file. -The templates are defined at `org-roam-capture-templates'. The -GOTO and KEYS argument have the same functionality as -`org-capture'." - (let* ((org-capture-templates (mapcar #'org-roam-capture--convert-template org-roam-capture-templates)) - (one-template-p (= (length org-capture-templates) 1)) - org-capture-templates-contexts) - (when one-template-p +;;;###autoload +(cl-defun org-roam-capture- (&key goto keys node info props templates) + "Main entry point. +GOTO and KEYS correspond to `org-capture' arguments. +INFO is an alist for filling up Org-roam's capture templates. +NODE is an `org-roam-node' construct containing information about the node. +PROPS is a plist containing additional Org-roam properties for each template. +TEMPLATES is a list of org-roam templates." + (let* ((props (plist-put props :call-location (point-marker))) + (org-capture-templates + (mapcar (lambda (template) + (org-roam-capture--convert-template template props)) + (or templates org-roam-capture-templates))) + (org-roam-capture--node node) + (org-roam-capture--info info)) + (when (and (not keys) + (= (length org-capture-templates) 1)) (setq keys (caar org-capture-templates))) - (if (or one-template-p - (eq org-roam-capture-function 'org-capture)) - (org-capture goto keys) - (funcall-interactively org-roam-capture-function)))) + (org-capture goto keys))) ;;;###autoload (defun org-roam-capture (&optional goto keys) @@ -620,21 +817,11 @@ GOTO and KEYS argument have the same functionality as This uses the templates defined at `org-roam-capture-templates'. Arguments GOTO and KEYS see `org-capture'." (interactive "P") - (unless org-roam-mode (org-roam-mode)) - (let* ((completions (org-roam--get-title-path-completions)) - (title-with-keys (org-roam-completion--completing-read "File: " - completions)) - (res (cdr (assoc title-with-keys completions))) - (title (or (plist-get res :title) title-with-keys)) - (file-path (plist-get res :path))) - (let ((org-roam-capture--info (list (cons 'title title) - (cons 'slug (funcall org-roam-title-to-slug-function title)) - (cons 'file file-path))) - (org-roam-capture--context 'capture)) - (condition-case err - (org-roam-capture--capture goto keys) - (error (user-error "%s. Please adjust `org-roam-capture-templates'" - (error-message-string err))))))) + (let ((node (org-roam-node-read))) + (org-roam-capture- :goto goto + :keys keys + :node node + :props '(:immediate-finish nil)))) (provide 'org-roam-capture) diff --git a/org-roam-compat.el b/org-roam-compat.el index c61fedd..cfa9692 100644 --- a/org-roam-compat.el +++ b/org-roam-compat.el @@ -5,8 +5,8 @@ ;; Author: Jethro Kuan ;; URL: https://github.com/org-roam/org-roam ;; Keywords: org-mode, roam, convenience -;; Version: 1.2.3 -;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (s "1.12.0") (org "9.3") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2")) +;; Version: 2.0.0 +;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite "1.0.0") (magit-section "2.90.1")) ;; This file is NOT part of GNU Emacs. @@ -34,78 +34,7 @@ ;;;; Library Requires ;;; Obsolete aliases (remove after next major release) -;;;; Functions -(define-obsolete-function-alias 'org-roam--capture-get-point 'org-roam-capture--get-point - "org-roam 1.0.0") -(define-obsolete-function-alias 'org-roam-build-cache 'org-roam-db-build-cache - "org-roam 1.0.0") -(define-obsolete-function-alias 'org-roam-sql 'org-roam-db-query - "org-roam 1.0.0") -(define-obsolete-function-alias 'org-roam--db-clear 'org-roam-db--clear - "org-roam 1.0.0") -(define-obsolete-function-alias 'org-roam-show-graph 'org-roam-graph-show - "org-roam 1.0.0") -(define-obsolete-function-alias 'org-roam--maybe-update-buffer - 'org-roam-buffer--update-maybe "org-roam 1.0.0") -(define-obsolete-function-alias 'org-roam--current-visibility - 'org-roam-buffer--visibility "org-roam 1.0.0") -(define-obsolete-function-alias 'org-roam-update 'org-roam-buffer-update - "org-roam 1.0.0") -(define-obsolete-function-alias 'org-roam--set-width 'org-roam-buffer--set-width - "org-roam 1.0.0") -(define-obsolete-function-alias 'org-roam--set-height 'org-roam-buffer--set-height - "org-roam 1.0.0") -(define-obsolete-function-alias 'org-roam--set-up-buffer 'org-roam-buffer--get-create - "org-roam 1.0.0") -(define-obsolete-function-alias 'org-roam-today 'org-roam-dailies-today - "org-roam 1.0.0") -(define-obsolete-function-alias 'org-roam-tomorrow 'org-roam-dailies-tomorrow - "org-roam 1.0.0") -(define-obsolete-function-alias 'org-roam-yesterday 'org-roam-dailies-yesterday - "org-roam 1.0.0") -(define-obsolete-function-alias 'org-roam-date 'org-roam-dailies-date - "org-roam 1.0.0") -(define-obsolete-function-alias 'org-roam-graph-show 'org-roam-graph - "org-roam 1.0.0") -(define-obsolete-function-alias 'org-roam-graph-build 'org-roam-graph - "org-roam 1.0.0") -(define-obsolete-function-alias 'org-roam-find-index 'org-roam-jump-to-index - "org-roam 1.1.0") -(define-obsolete-function-alias 'org-roam--pluralize 'org-roam-buffer--pluralize - "org-roam 1.1.0") -(define-obsolete-function-alias 'org-roam--capture 'org-roam-capture--capture - "org-roam 1.1.0") -(define-obsolete-function-alias 'org-roam-db--clear 'org-roam-db-clear - "org-roam 1.2.0") -(define-obsolete-function-alias 'org-roam-dailies-today 'org-roam-dailies-find-today - "org-roam 1.2.2") -(define-obsolete-function-alias 'org-roam-dailies-yesterday 'org-roam-dailies-find-yesterday - "org-roam 1.2.2") -(define-obsolete-function-alias 'org-roam-dailies-tomorrow 'org-roam-dailies-find-tomorrow - "org-roam 1.2.2") -(define-obsolete-function-alias 'org-roam-dailies-date 'org-roam-dailies-find-date - "org-roam 1.2.2") - -;;;; Variables -(define-obsolete-variable-alias 'org-roam-graphviz-extra-options - 'org-roam-graph-extra-config "org-roam 1.0.0") -(define-obsolete-variable-alias 'org-roam-grapher-extra-options - 'org-roam-graph-extra-config "org-roam 1.0.0") -(define-obsolete-variable-alias 'org-roam-graph-node-shape - 'org-roam-graph-node-extra-config "org-roam 1.0.0") -(define-obsolete-variable-alias 'org-roam--db-connection - 'org-roam-db--connection "org-roam 1.0.0") -(define-obsolete-variable-alias 'org-roam--current-buffer - 'org-roam-buffer--current "org-roam 1.0.0") -(define-obsolete-variable-alias 'org-roam-date-title-format - 'org-roam-dailies-capture-templates "org-roam 1.0.0") -(define-obsolete-variable-alias 'org-roam-date-filename-format - 'org-roam-dailies-capture-templates "org-roam 1.0.0") -(define-obsolete-variable-alias 'org-roam-update-db-idle-seconds - 'org-roam-db-update-idle-seconds "org-roam 1.2.2") - -(make-obsolete-variable 'org-roam-buffer-no-delete-other-windows - 'org-roam-buffer-window-parameters "org-roam 1.1.1") +;;; Obsolete functions (provide 'org-roam-compat) diff --git a/org-roam-completion.el b/org-roam-completion.el index 7be1573..0201152 100644 --- a/org-roam-completion.el +++ b/org-roam-completion.el @@ -5,9 +5,8 @@ ;; Author: Jethro Kuan ;; URL: https://github.com/org-roam/org-roam ;; Keywords: org-mode, roam, convenience -;; Version: 1.2.3 -;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (s "1.12.0") (org "9.3") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2")) - +;; Version: 2.0.0 +;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite "1.0.0") (magit-section "2.90.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify @@ -27,90 +26,81 @@ ;;; Commentary: ;; -;; This library provides completion for org-roam. +;; This library provides completion-at-point functions for Org-roam. +;; +;; The two main functions provided to capf are: +;; +;; `org-roam-complete-link-at-point' provides completions to nodes +;; within link brackets +;; +;; `org-roam-complete-everywhere' provides completions for nodes everywhere, +;; matching the symbol at point +;; ;;; Code: -;;;; Library Requires (require 'cl-lib) -(require 's) +(require 'org-element) -(defvar helm-pattern) -(declare-function helm "ext:helm") -(declare-function helm-make-source "ext:helm-source" (name class &rest args) t) +(declare-function org-roam--get-titles "org-roam") -(defcustom org-roam-completion-system 'default - "The completion system to be used by `org-roam'." - :type '(radio - (const :tag "Default" default) - (const :tag "Ido" ido) - (const :tag "Ivy" ivy) - (const :tag "Helm" helm) - (function :tag "Custom function")) - :group 'org-roam) - -(defcustom org-roam-completion-ignore-case t - "Whether to ignore case in Org-roam `completion-at-point' completions." +(defcustom org-roam-completion-everywhere nil + "When non-nil, provide link completion matching outside of Org links." :group 'org-roam :type 'boolean) -(defun org-roam-completion--helm-candidate-transformer (candidates _source) - "Transforms CANDIDATES for Helm-based completing read. -SOURCE is not used." - (let ((prefix (propertize "[?] " - 'face 'helm-ff-prefix))) - (cons (propertize helm-pattern - 'display (concat prefix helm-pattern)) - candidates))) +(defvar org-roam-completion-functions (list #'org-roam-complete-link-at-point + #'org-roam-complete-everywhere) + "List of functions to be used with `completion-at-point' for Org-roam.") -(cl-defun org-roam-completion--completing-read (prompt choices &key - require-match initial-input - action) - "Present a PROMPT with CHOICES and optional INITIAL-INPUT. -If REQUIRE-MATCH is t, the user must select one of the CHOICES. -Return user choice." - (let (res) - (setq res - (cond - ((eq org-roam-completion-system 'ido) - (let ((candidates (mapcar #'car choices))) - (ido-completing-read prompt candidates nil require-match initial-input))) - ((eq org-roam-completion-system 'default) - (completing-read prompt choices nil require-match initial-input)) - ((eq org-roam-completion-system 'ivy) - (if (fboundp 'ivy-read) - (ivy-read prompt choices - :initial-input initial-input - :preselect initial-input - :require-match require-match - :action (prog1 action - (setq action nil)) - :caller 'org-roam--completing-read) - (user-error "Please install ivy from \ -https://github.com/abo-abo/swiper"))) - ((eq org-roam-completion-system 'helm) - (unless (and (fboundp 'helm) - (fboundp 'helm-make-source)) - (user-error "Please install helm from \ -https://github.com/emacs-helm/helm")) - (let ((source (helm-make-source prompt 'helm-source-sync - :candidates (mapcar #'car choices) - :filtered-candidate-transformer - (and (not require-match) - #'org-roam-completion--helm-candidate-transformer))) - (buf (concat "*org-roam " - (s-downcase (s-chop-suffix ":" (s-trim prompt))) - "*"))) - (or (helm :sources source - :action (if action - (prog1 action - (setq action nil)) - #'identity) - :prompt prompt - :input initial-input - :buffer buf) - (keyboard-quit)))))) - (if action - (funcall action res) - res))) +(defconst org-roam-bracket-completion-re + "\\[\\[\\(\\(?:roam:\\)?\\)\\([^z-a]*\\)]]" + "Regex for completion within link brackets. +We use this as a substitute for `org-link-bracket-re', because +`org-link-bracket-re' requires content within the brackets for a match.") + +(defun org-roam-complete-everywhere () + "Provides completions for links for any word at point. +This is a `completion-at-point' function, and is active when +`org-roam-completion-everywhere' is non-nil." + (when (and org-roam-completion-everywhere + (thing-at-point 'word) + (not (save-match-data (org-in-regexp org-link-any-re)))) + (let ((bounds (bounds-of-thing-at-point 'word))) + (list (car bounds) (cdr bounds) + (completion-table-dynamic + (lambda (_) + (funcall #'org-roam--get-titles))) + :exit-function + (lambda (str _status) + (delete-char (- (length str))) + (insert "[[roam:" str "]]")))))) + +(defun org-roam-complete-link-at-point () + "Do appropriate completion for the link at point." + (let (roam-p start end) + (when (org-in-regexp org-roam-bracket-completion-re 1) + (setq roam-p (not (string-blank-p (match-string 1))) + start (match-beginning 2) + end (match-end 2)) + (list start end + (completion-table-dynamic + (lambda (_) + (funcall #'org-roam--get-titles))) + :exit-function + (lambda (str &rest _) + (delete-char (- 0 (length str))) + (insert (concat (unless roam-p "roam:") + str)) + (forward-char 2)))))) + +(defun org-roam-complete-at-point () + "." + (run-hook-with-args-until-success 'org-roam-completion-functions)) + +(defun org-roam--register-completion-functions () + "." + (add-hook 'completion-at-point-functions #'org-roam-complete-at-point nil t)) + +(add-hook 'org-roam-find-file-hook #'org-roam--register-completion-functions) (provide 'org-roam-completion) diff --git a/org-roam-dailies.el b/org-roam-dailies.el index 9110e7f..e5a88df 100644 --- a/org-roam-dailies.el +++ b/org-roam-dailies.el @@ -4,11 +4,11 @@ ;; Copyright © 2020 Leo Vivier ;; Author: Jethro Kuan -;; Leo Vivier +;; Leo Vivier ;; URL: https://github.com/org-roam/org-roam ;; Keywords: org-mode, roam, convenience -;; Version: 1.2.3 -;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (s "1.12.0") (org "9.3") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2")) +;; Version: 2.0.0 +;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite "1.0.0") (magit-section "2.90.1")) ;; This file is NOT part of GNU Emacs. @@ -36,20 +36,23 @@ ;;; Library Requires (require 'org-capture) (require 'org-roam-capture) -(require 'org-roam-macs) (require 'f) ;;;; Declarations -(defvar org-roam-mode) (defvar org-roam-directory) (defvar org-roam-file-extensions) -(declare-function org-roam--org-file-p "org-roam") -(declare-function org-roam--find-file "org-roam") -(declare-function org-roam-mode "org-roam") +(declare-function org-roam-file-p "org-roam") + +;;;; Faces +(defface org-roam-dailies-calendar-note + '((t :inherit (org-link) :underline nil)) + "Face for dates with a daily-note in the calendar." + :group 'org-roam-faces) ;;;; Customizable variables (defcustom org-roam-dailies-directory "daily/" - "Path to daily-notes." + "Path to daily-notes. +This path is relative to `org-roam-directory'." :group 'org-roam :type 'string) @@ -59,120 +62,117 @@ :type 'hook) (defcustom org-roam-dailies-capture-templates - `(("d" "default" entry (function org-roam-capture--get-point) + `(("d" "default" entry "* %?" - :file-name ,(concat org-roam-dailies-directory "%<%Y-%m-%d>") - :head "#+title: %<%Y-%m-%d>\n")) - "Capture templates for daily-notes in Org-roam." + :if-new (file+head "%<%Y-%m-%d>.org" + "#+title: %<%Y-%m-%d>\n"))) + "Capture templates for daily-notes in Org-roam. +See `org-roam-capture-templates' for the template documentation." :group 'org-roam - ;; Adapted from `org-capture-templates' - :type - `(repeat - (choice :value ("d" "default" plain (function org-roam-capture--get-point) - "%?" - :file-name ,(concat org-roam-dailies-directory - "%<%Y-%m-%d>") - :head "#+title: %<%Y-%m-%d>\n" - :unnarrowed t) - (list :tag "Multikey description" - (string :tag "Keys ") - (string :tag "Description")) - (list :tag "Template entry" - (string :tag "Keys ") - (string :tag "Description ") - (choice :tag "Type " - (const :tag "Plain" plain) - (const :tag "Entry (for creating headlines)" entry)) - (const :format "" #'org-roam-capture--get-point) - (choice :tag "Template " - (string :tag "String" - :format "String:\n \ -Template string :\n%v") - (list :tag "File" - (const :format "" file) - (file :tag "Template file ")) - (list :tag "Function" - (const :format "" function) - (function :tag "Template function "))) - (const :format "File name format :" :file-name) - (string :format " %v" :value ,(concat org-roam-dailies-directory - "%<%Y-%m-%d>")) - (const :format "Header format :" :head) - (string :format " %v" :value "#+title: ${title}\n") - (plist :inline t - :tag "Options" - ;; Give the most common options as checkboxes - :options - (((const :tag "Outline path" :olp) - (repeat :tag "Headings" - (string :tag "Heading"))) - ((const :format "%v " :unnarrowed) (const t)) - ((const :format "%v " :prepend) (const t)) - ((const :format "%v " :immediate-finish) (const t)) - ((const :format "%v " :jump-to-captured) (const t)) - ((const :format "%v " :empty-lines) (const 1)) - ((const :format "%v " :empty-lines-before) (const 1)) - ((const :format "%v " :empty-lines-after) (const 1)) - ((const :format "%v " :clock-in) (const t)) - ((const :format "%v " :clock-keep) (const t)) - ((const :format "%v " :clock-resume) (const t)) - ((const :format "%v " :time-prompt) (const t)) - ((const :format "%v " :tree-type) (const week)) - ((const :format "%v " :table-line-pos) (string)) - ((const :format "%v " :kill-buffer) (const t)))))))) - -;;;; Utilities -(defun org-roam-dailies-directory--get-absolute-path () - "Get absolute path to `org-roam-dailies-directory'." - (expand-file-name org-roam-dailies-directory org-roam-directory)) + :type '(repeat + (choice (list :tag "Multikey description" + (string :tag "Keys ") + (string :tag "Description")) + (list :tag "Template entry" + (string :tag "Keys ") + (string :tag "Description ") + (choice :tag "Capture Type " :value entry + (const :tag "Org entry" entry) + (const :tag "Plain list item" item) + (const :tag "Checkbox item" checkitem) + (const :tag "Plain text" plain) + (const :tag "Table line" table-line)) + (choice :tag "Template " + (string) + (list :tag "File" + (const :format "" file) + (file :tag "Template file")) + (list :tag "Function" + (const :format "" function) + (function :tag "Template function"))) + (plist :inline t + ;; Give the most common options as checkboxes + :options (((const :format "%v " :if-new) + (choice :tag "Node location" + (list :tag "File" + (const :format "" file) + (string :tag " File")) + (list :tag "File & Head Content" + (const :format "" file+head) + (string :tag " File") + (string :tag " Head Content")) + (list :tag "File & Outline path" + (const :format "" file+olp) + (string :tag " File") + (list :tag "Outline path" + (repeat (string :tag "Headline")))) + (list :tag "File & Head Content & Outline path" + (const :format "" file+head+olp) + (string :tag " File") + (string :tag " Head Content") + (list :tag "Outline path" + (repeat (string :tag "Headline")))))) + ((const :format "%v " :prepend) (const t)) + ((const :format "%v " :immediate-finish) (const t)) + ((const :format "%v " :jump-to-captured) (const t)) + ((const :format "%v " :empty-lines) (const 1)) + ((const :format "%v " :empty-lines-before) (const 1)) + ((const :format "%v " :empty-lines-after) (const 1)) + ((const :format "%v " :clock-in) (const t)) + ((const :format "%v " :clock-keep) (const t)) + ((const :format "%v " :clock-resume) (const t)) + ((const :format "%v " :time-prompt) (const t)) + ((const :format "%v " :tree-type) (const week)) + ((const :format "%v " :unnarrowed) (const t)) + ((const :format "%v " :table-line-pos) (string)) + ((const :format "%v " :kill-buffer) (const t)))))))) +;;;###autoload (defun org-roam-dailies-find-directory () "Find and open `org-roam-dailies-directory'." (interactive) - (org-roam--find-file (org-roam-dailies-directory--get-absolute-path))) + (find-file (expand-file-name org-roam-dailies-directory org-roam-directory))) (defun org-roam-dailies--daily-note-p (&optional file) "Return t if FILE is an Org-roam daily-note, nil otherwise. - If FILE is not specified, use the current buffer's file-path." - (when-let ((path (or file - (-> (buffer-base-buffer) - (buffer-file-name)))) - (directory (org-roam-dailies-directory--get-absolute-path))) + (when-let ((path (expand-file-name + (or file + (buffer-file-name (buffer-base-buffer))))) + (directory (expand-file-name org-roam-dailies-directory org-roam-directory))) (setq path (expand-file-name path)) (save-match-data (and - (org-roam--org-file-p path) + (org-roam-file-p path) (f-descendant-of-p path directory))))) (defun org-roam-dailies--capture (time &optional goto) "Capture an entry in a daily-note for TIME, creating it if necessary. - When GOTO is non-nil, go the note without creating an entry." - (unless org-roam-mode (org-roam-mode)) - (let ((org-roam-capture-templates (--> org-roam-dailies-capture-templates - (if goto (list (car it)) it))) - (org-roam-capture--info (list (cons 'time time))) - (org-roam-capture--context 'dailies)) - (org-roam-capture--capture (when goto '(4))))) + (let ((org-roam-directory (expand-file-name org-roam-dailies-directory org-roam-directory))) + (org-roam-capture- :goto (when goto '(4)) + :node (org-roam-node-create) + :templates org-roam-dailies-capture-templates + :props (list :override-default-time time))) + (when goto (run-hooks 'org-roam-dailies-find-file-hook))) ;;;; Commands ;;; Today +;;;###autoload (defun org-roam-dailies-capture-today (&optional goto) "Create an entry in the daily-note for today. - When GOTO is non-nil, go the note without creating an entry." (interactive "P") - (org-roam-dailies--capture (current-time) goto) - (when goto - (run-hooks 'org-roam-dailies-find-file-hook))) + (org-roam-dailies--capture (current-time) goto)) -(defun org-roam-dailies-find-today () +;;;###autoload +(defun org-roam-dailies-goto-today () "Find the daily-note for today, creating it if necessary." (interactive) (org-roam-dailies-capture-today t)) ;;; Tomorrow +;;;###autoload (defun org-roam-dailies-capture-tomorrow (n &optional goto) "Create an entry in the daily-note for tomorrow. @@ -183,7 +183,8 @@ creating an entry." (interactive "p") (org-roam-dailies--capture (time-add (* n 86400) (current-time)) goto)) -(defun org-roam-dailies-find-tomorrow (n) +;;;###autoload +(defun org-roam-dailies-goto-tomorrow (n) "Find the daily-note for tomorrow, creating it if necessary. With numeric argument N, use the daily-note N days in the @@ -192,6 +193,7 @@ future." (org-roam-dailies-capture-tomorrow n t)) ;;; Yesterday +;;;###autoload (defun org-roam-dailies-capture-yesterday (n &optional goto) "Create an entry in the daily-note for yesteday. @@ -201,7 +203,8 @@ When GOTO is non-nil, go the note without creating an entry." (interactive "p") (org-roam-dailies-capture-tomorrow (- n) goto)) -(defun org-roam-dailies-find-yesterday (n) +;;;###autoload +(defun org-roam-dailies-goto-yesterday (n) "Find the daily-note for yesterday, creating it if necessary. With numeric argument N, use the daily-note N days in the @@ -210,68 +213,46 @@ future." (org-roam-dailies-capture-tomorrow (- n) t)) ;;; Calendar -(defvar org-roam-dailies-calendar-hook (list 'org-roam-dailies-calendar-mark-entries) - "Hooks to run when showing the `org-roam-dailies-calendar'.") - -(defun org-roam-dailies-calendar--install-hook () - "Install Org-roam-dailies hooks to calendar." - (add-hook 'calendar-today-visible-hook #'org-roam-dailies-calendar--run-hook) - (add-hook 'calendar-today-invisible-hook #'org-roam-dailies-calendar--run-hook)) - -(defun org-roam-dailies-calendar--run-hook () - "Run Org-roam-dailies hooks to calendar." - (run-hooks 'org-roam-dailies-calendar-hook) - (remove-hook 'calendar-today-visible-hook #'org-roam-dailies-calendar--run-hook) - (remove-hook 'calendar-today-invisible-hook #'org-roam-dailies-calendar--run-hook)) - (defun org-roam-dailies-calendar--file-to-date (&optional file) "Convert FILE to date. - Return (MONTH DAY YEAR)." (let ((file (or file - (-> (buffer-base-buffer) - (buffer-file-name))))) + (buffer-base-buffer (buffer-file-name))))) (cl-destructuring-bind (_ _ _ d m y _ _ _) - (-> file - (file-name-nondirectory) - (file-name-sans-extension) - (org-parse-time-string)) + (org-parse-time-string + (file-name-sans-extension + (file-name-nondirectory file))) (list m d y)))) (defun org-roam-dailies-calendar--date-to-time (date) - "Convert DATE as returned from the calendar (MONTH DAY YEAR) to a time." + "Convert DATE as returned from then calendar (MONTH DAY YEAR) to a time." (encode-time 0 0 0 (nth 1 date) (nth 0 date) (nth 2 date))) (defun org-roam-dailies-calendar-mark-entries () "Mark days in the calendar for which a daily-note is present." - (when (file-exists-p (org-roam-dailies-directory--get-absolute-path)) + (when (file-exists-p (expand-file-name org-roam-dailies-directory org-roam-directory)) (dolist (date (mapcar #'org-roam-dailies-calendar--file-to-date (org-roam-dailies--list-files))) (when (calendar-date-is-visible-p date) (calendar-mark-visible-date date 'org-roam-dailies-calendar-note))))) ;;; Date +;;;###autoload (defun org-roam-dailies-capture-date (&optional goto prefer-future) "Create an entry in the daily-note for a date using the calendar. - Prefer past dates, unless PREFER-FUTURE is non-nil. - With a `C-u' prefix or when GOTO is non-nil, go the note without creating an entry." (interactive "P") - (org-roam-dailies-calendar--install-hook) - (let* ((time-str (let ((org-read-date-prefer-future prefer-future)) - (org-read-date nil nil nil (if goto - "Find daily-note: " - "Capture to daily-note: ")))) - (time (org-read-date nil t time-str))) - (org-roam-dailies--capture time goto) - (when goto - (run-hooks 'org-roam-dailies-find-file-hook)))) + (let ((time (let ((org-read-date-prefer-future prefer-future)) + (org-read-date t t nil (if goto + "Find daily-note: " + "Capture to daily-note: "))))) + (org-roam-dailies--capture time goto))) -(defun org-roam-dailies-find-date (&optional prefer-future) +;;;###autoload +(defun org-roam-dailies-goto-date (&optional prefer-future) "Find the daily-note for a date using the calendar, creating it if necessary. - Prefer past dates, unless PREFER-FUTURE is non-nil." (interactive) (org-roam-dailies-capture-date t prefer-future)) @@ -280,8 +261,8 @@ Prefer past dates, unless PREFER-FUTURE is non-nil." (defun org-roam-dailies--list-files (&rest extra-files) "List all files in `org-roam-dailies-directory'. EXTRA-FILES can be used to append extra files to the list." - (let ((dir (org-roam-dailies-directory--get-absolute-path)) - (regexp (rx-to-string `(and "." (or ,@org-roam-file-extensions))))) + (let ((dir (expand-file-name org-roam-dailies-directory org-roam-directory)) + (regexp (rx-to-string `(and "." (or ,@org-roam-file-extensions))))) (append (--remove (let ((file (file-name-nondirectory it))) (when (or (auto-save-file-name-p file) (backup-file-name-p file) @@ -290,7 +271,7 @@ EXTRA-FILES can be used to append extra files to the list." (directory-files-recursively dir regexp)) extra-files))) -(defun org-roam-dailies-find-next-note (&optional n) +(defun org-roam-dailies-goto-next-note (&optional n) "Find next daily-note. With numeric argument N, find note N days in the future. If N is @@ -305,27 +286,30 @@ negative, find note N days in the past." (string= (buffer-file-name (buffer-base-buffer)) candidate)) dailies)) note) - (unless position - (user-error "Can't find current note file - have you saved it yet?")) - (pcase n - ((pred (natnump)) - (when (eq position (- (length dailies) 1)) - (user-error "Already at newest note"))) - ((pred (integerp)) - (when (eq position 0) - (user-error "Already at oldest note")))) - (setq note (nth (+ position n) dailies)) - (find-file note) - (run-hooks 'org-roam-dailies-find-file-hook))) + (unless position + (user-error "Can't find current note file - have you saved it yet?")) + (pcase n + ((pred (natnump)) + (when (eq position (- (length dailies) 1)) + (user-error "Already at newest note"))) + ((pred (integerp)) + (when (eq position 0) + (user-error "Already at oldest note")))) + (setq note (nth (+ position n) dailies)) + (find-file note) + (run-hooks 'org-roam-dailies-find-file-hook))) -(defun org-roam-dailies-find-previous-note (&optional n) +(defun org-roam-dailies-goto-previous-note (&optional n) "Find previous daily-note. With numeric argument N, find note N days in the past. If N is negative, find note N days in the future." (interactive "p") (let ((n (if n (- n) -1))) - (org-roam-dailies-find-next-note n))) + (org-roam-dailies-goto-next-note n))) + +(add-hook 'calendar-today-visible-hook #'org-roam-dailies-calendar-mark-entries) +(add-hook 'calendar-today-invisible-hook #'org-roam-dailies-calendar-mark-entries) ;;;; Bindings (defvar org-roam-dailies-map (make-sparse-keymap) @@ -333,16 +317,35 @@ negative, find note N days in the future." (define-prefix-command 'org-roam-dailies-map) -(define-key org-roam-dailies-map (kbd "d") #'org-roam-dailies-find-today) -(define-key org-roam-dailies-map (kbd "y") #'org-roam-dailies-find-yesterday) -(define-key org-roam-dailies-map (kbd "t") #'org-roam-dailies-find-tomorrow) +(define-key org-roam-dailies-map (kbd "d") #'org-roam-dailies-goto-today) +(define-key org-roam-dailies-map (kbd "y") #'org-roam-dailies-goto-yesterday) +(define-key org-roam-dailies-map (kbd "t") #'org-roam-dailies-goto-tomorrow) (define-key org-roam-dailies-map (kbd "n") #'org-roam-dailies-capture-today) -(define-key org-roam-dailies-map (kbd "f") #'org-roam-dailies-find-next-note) -(define-key org-roam-dailies-map (kbd "b") #'org-roam-dailies-find-previous-note) -(define-key org-roam-dailies-map (kbd "c") #'org-roam-dailies-find-date) +(define-key org-roam-dailies-map (kbd "f") #'org-roam-dailies-goto-next-note) +(define-key org-roam-dailies-map (kbd "b") #'org-roam-dailies-goto-previous-note) +(define-key org-roam-dailies-map (kbd "c") #'org-roam-dailies-goto-date) (define-key org-roam-dailies-map (kbd "v") #'org-roam-dailies-capture-date) (define-key org-roam-dailies-map (kbd ".") #'org-roam-dailies-find-directory) +(define-obsolete-function-alias + 'org-roam-dailies-find-today + 'org-roam-dailies-goto-today "org-roam 2.0") +(define-obsolete-function-alias + 'org-roam-dailies-find-yesterday + 'org-roam-dailies-goto-yesterday "org-roam 2.0") +(define-obsolete-function-alias + 'org-roam-dailies-find-tomorrow + 'org-roam-dailies-goto-tomorrow "org-roam 2.0") +(define-obsolete-function-alias + 'org-roam-dailies-find-next-note + 'org-roam-dailies-goto-next-note "org-roam 2.0") +(define-obsolete-function-alias + 'org-roam-dailies-find-previous-note + 'org-roam-dailies-goto-previous-note "org-roam 2.0") +(define-obsolete-function-alias + 'org-roam-dailies-find-date + 'org-roam-dailies-goto-date "org-roam 2.0") + (provide 'org-roam-dailies) ;;; org-roam-dailies.el ends here diff --git a/org-roam-db.el b/org-roam-db.el index 48d2427..363f578 100644 --- a/org-roam-db.el +++ b/org-roam-db.el @@ -5,8 +5,8 @@ ;; Author: Jethro Kuan ;; URL: https://github.com/org-roam/org-roam ;; Keywords: org-mode, roam, convenience -;; Version: 1.2.3 -;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (s "1.12.0") (org "9.3") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2")) +;; Version: 2.0.0 +;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite "1.0.0") (magit-section "2.90.1")) ;; This file is NOT part of GNU Emacs. @@ -27,37 +27,31 @@ ;;; Commentary: ;; -;; This library is provides the underlying database api to org-roam +;; This library provides the underlying database api to org-roam. ;; ;;; Code: ;;;; Library Requires (eval-when-compile (require 'subr-x)) (require 'emacsql) -(require 'emacsql-sqlite3) +(require 'emacsql-sqlite) (require 'seq) (eval-and-compile (require 'org-roam-macs) ;; For `org-with-wide-buffer' (require 'org-macs)) +(require 'org) +(require 'ol) +(require 'org-roam-utils) +(defvar org-roam-find-file-hook) (defvar org-roam-directory) -(defvar org-roam-enable-headline-linking) (defvar org-roam-verbose) -(defvar org-roam-file-name) - (defvar org-agenda-files) -(declare-function org-roam--org-roam-file-p "org-roam") -(declare-function org-roam--extract-titles "org-roam") -(declare-function org-roam--extract-refs "org-roam") -(declare-function org-roam--extract-tags "org-roam") -(declare-function org-roam--extract-ids "org-roam") -(declare-function org-roam--extract-links "org-roam") -(declare-function org-roam--list-all-files "org-roam") -(declare-function org-roam--path-to-slug "org-roam") -(declare-function org-roam--file-name-extension "org-roam") -(declare-function org-roam-buffer--update-maybe "org-roam-buffer") +(declare-function org-roam-id-at-point "org-roam") +(declare-function org-roam--list-all-files "org-roam") +(declare-function org-roam-node-at-point "org-roam") ;;;; Options (defcustom org-roam-db-location (expand-file-name "org-roam.db" user-emacs-directory) @@ -71,7 +65,7 @@ when used with multiple Org-roam instances." (defcustom org-roam-db-gc-threshold gc-cons-threshold "The value to temporarily set the `gc-cons-threshold' threshold to. -During large, heavy operations like `org-roam-db-build-cache', +During large, heavy operations like `org-roam-db-sync', many GC operations happen because of the large number of temporary structures generated (e.g. parsed ASTs). Temporarily increasing `gc-cons-threshold' will help reduce the number of GC @@ -84,34 +78,20 @@ value like `most-positive-fixnum'." :type 'int :group 'org-roam) -(defconst org-roam-db--version 10) +(defcustom org-roam-db-node-include-function (lambda () t) + "A custom function to check if the headline at point is a node." + :type 'function + :group 'org-roam) + +(defconst org-roam-db-version 16) +(defconst org-roam--sqlite-available-p + (with-demoted-errors "Org-roam initialization: %S" + (emacsql-sqlite-ensure-binary) + t)) (defvar org-roam-db--connection (make-hash-table :test #'equal) "Database connection to Org-roam database.") -(defvar org-roam-db-dirty nil - "Whether the org-roam database is dirty and requires an update. -Contains pairs of `org-roam-directory' and `org-roam-db-location' -so that multi-directories are updated.") - -(defcustom org-roam-db-update-method 'idle-timer - "Method to update the Org-roam database. - -`immediate' - Update the database immediately upon file changes. - -`idle-timer' - Updates the database if dirty, if Emacs idles for - `org-roam-db-update-idle-seconds'." - :type '(choice (const :tag "idle-timer" idle-timer) - (const :tag "immediate" immediate)) - :group 'org-roam) - -(defcustom org-roam-db-update-idle-seconds 2 - "Number of idle seconds before triggering an Org-roam database update." - :type 'integer - :group 'org-roam) - ;;;; Core Functions (defun org-roam-db--get-connection () @@ -127,7 +107,7 @@ Performs a database upgrade when required." (emacsql-live-p (org-roam-db--get-connection))) (let ((init-db (not (file-exists-p org-roam-db-location)))) (make-directory (file-name-directory org-roam-db-location) t) - (let ((conn (emacsql-sqlite3 org-roam-db-location))) + (let ((conn (emacsql-sqlite org-roam-db-location))) (set-process-query-on-exit-flag (emacsql-process conn) nil) (puthash (expand-file-name org-roam-directory) conn @@ -137,72 +117,103 @@ Performs a database upgrade when required." (let* ((version (caar (emacsql conn "PRAGMA user_version"))) (version (org-roam-db--upgrade-maybe conn version))) (cond - ((> version org-roam-db--version) + ((> version org-roam-db-version) (emacsql-close conn) (user-error "The Org-roam database was created with a newer Org-roam version. " "You need to update the Org-roam package")) - ((< version org-roam-db--version) + ((< version org-roam-db-version) (emacsql-close conn) (error "BUG: The Org-roam database scheme changed %s" "and there is no upgrade path"))))))) (org-roam-db--get-connection)) ;;;; Entrypoint: (org-roam-db-query) +(define-error 'emacsql-constraint "SQL constraint violation") (defun org-roam-db-query (sql &rest args) "Run SQL query on Org-roam database with ARGS. SQL can be either the emacsql vector representation, or a string." - (if (stringp sql) - (emacsql (org-roam-db) (apply #'format sql args)) - (apply #'emacsql (org-roam-db) sql args))) + (apply #'emacsql (org-roam-db) sql args)) + +(defun org-roam-db-query! (handler sql &rest args) + "Run SQL query on Org-roam database with ARGS. +SQL can be either the emacsql vector representation, or a string. +The query is expected to be able to fail, in this situation, run HANDLER." + (condition-case err + (org-roam-db-query sql args) + (emacsql-constraint + (funcall handler err)))) ;;;; Schemata (defconst org-roam-db--table-schemata '((files [(file :unique :primary-key) (hash :not-null) - (meta :not-null)]) + (atime :not-null) + (mtime :not-null)]) - (ids - [(id :unique :primary-key) - (file :not-null) - (level :not-null)]) + (nodes + ([(id :not-null :primary-key) + (file :not-null) + (level :not-null) + (pos :not-null) + todo + priority + (scheduled text) + (deadline text) + title + properties + olp] + (:foreign-key [file] :references files [file] :on-delete :cascade))) - (links - [(source :not-null) - (dest :not-null) - (type :not-null) - (properties :not-null)]) - - (tags - [(file :unique :primary-key) - (tags)]) - - (titles - [(file :not-null) - title]) + (aliases + ([(node-id :not-null) + alias] + (:foreign-key [node-id] :references nodes [id] :on-delete :cascade))) (refs - [(ref :unique :not-null) - (file :not-null) - (type :not-null)]))) + ([(node-id :not-null) + (ref :not-null) + (type :not-null)] + (:foreign-key [node-id] :references nodes [id] :on-delete :cascade))) + + (tags + ([(node-id :not-null) + tag] + (:foreign-key [node-id] :references nodes [id] :on-delete :cascade))) + + (links + ([(pos :not-null) + (source :not-null) + (dest :not-null) + (type :not-null) + (properties :not-null)] + (:foreign-key [source] :references nodes [id] :on-delete :cascade))))) + +(defconst org-roam-db--table-indices + '((alias-node-id aliases [node-id]) + (refs-node-id refs [node-id]) + (tags-node-id tags [node-id]))) (defun org-roam-db--init (db) "Initialize database DB with the correct schema and user version." (emacsql-with-transaction db - (pcase-dolist (`(,table . ,schema) org-roam-db--table-schemata) + (emacsql db "PRAGMA foreign_keys = ON") + (pcase-dolist (`(,table ,schema) org-roam-db--table-schemata) (emacsql db [:create-table $i1 $S2] table schema)) - (emacsql db (format "PRAGMA user_version = %s" org-roam-db--version)))) + (pcase-dolist (`(,index-name ,table ,columns) org-roam-db--table-indices) + (emacsql db [:create-index $i1 :on $i2 $S3] index-name table columns)) + (emacsql db (format "PRAGMA user_version = %s" org-roam-db-version)))) (defun org-roam-db--upgrade-maybe (db version) "Upgrades the database schema for DB, if VERSION is old." (emacsql-with-transaction db 'ignore - (if (< version org-roam-db--version) + (if (< version org-roam-db-version) (progn (org-roam-message (format "Upgrading the Org-roam database from version %d to version %d" - version org-roam-db--version)) - (org-roam-db-build-cache t)))) + version org-roam-db-version)) + (org-roam-db-sync t)))) version) (defun org-roam-db--close (&optional db) @@ -219,268 +230,216 @@ the current `org-roam-directory'." (dolist (conn (hash-table-values org-roam-db--connection)) (org-roam-db--close conn))) -;;;; Timer-based updating -(defvar org-roam-db-file-update-timer nil - "Timer for updating the database when dirty.") - -(defun org-roam-db-mark-dirty () - "Mark the Org-roam database as dirty." - (add-to-list 'org-roam-db-dirty (list org-roam-directory org-roam-db-location) - nil #'equal)) - -(defun org-roam-db-update-cache-on-timer () - "Update the cache if the database is dirty. -This function is called on `org-roam-db-file-update-timer'." - (pcase-dolist (`(,org-roam-directory ,org-roam-db-location) org-roam-db-dirty) - (org-roam-db-build-cache)) - (setq org-roam-db-dirty nil)) - ;;;; Database API -;;;;; Initialization -(defun org-roam-db--initialized-p () - "Whether the Org-roam cache has been initialized." - (and (file-exists-p org-roam-db-location) - (> (caar (org-roam-db-query [:select (funcall count) :from titles])) - 0))) - -(defun org-roam-db--ensure-built () - "Ensures that Org-roam cache is built." - (unless (org-roam-db--initialized-p) - (error "[Org-roam] your cache isn't built yet! Please run org-roam-db-build-cache"))) - ;;;;; Clearing -(defun org-roam-db-clear () +(defun org-roam-db-clear-all () "Clears all entries in the Org-roam cache." (interactive) (when (file-exists-p org-roam-db-location) (dolist (table (mapcar #'car org-roam-db--table-schemata)) (org-roam-db-query `[:delete :from ,table])))) -(defun org-roam-db--clear-file (&optional file) +(defun org-roam-db-clear-file (&optional file) "Remove any related links to the FILE. -This is equivalent to removing the node from the graph." +This is equivalent to removing the node from the graph. +If FILE is nil, clear the current buffer." (setq file (or file (buffer-file-name (buffer-base-buffer)))) - (dolist (table (mapcar #'car org-roam-db--table-schemata)) - (org-roam-db-query `[:delete :from ,table - :where (= ,(if (eq table 'links) 'source 'file) $s1)] - file))) + (org-roam-db-query [:delete :from files + :where (= file $s1)] + file)) -;;;;; Inserting -(defun org-roam-db--insert-meta (&optional update-p) - "Update the metadata of the current buffer into the cache. -If UPDATE-P is non-nil, first remove the meta for the file in the database." - (let* ((file (or org-roam-file-name (buffer-file-name))) +;;;;; Updating tables +(defun org-roam-db-insert-file () + "Update the files table for the current buffer. +If UPDATE-P is non-nil, first remove the file in the database." + (let* ((file (buffer-file-name)) (attr (file-attributes file)) (atime (file-attribute-access-time attr)) (mtime (file-attribute-modification-time attr)) (hash (org-roam-db--file-hash))) - (when update-p - (org-roam-db-query [:delete :from files - :where (= file $s1)] - file)) (org-roam-db-query [:insert :into files :values $v1] - (list (vector file hash (list :atime atime :mtime mtime)))))) + (list (vector file hash atime mtime))))) -(defun org-roam-db--insert-titles (&optional update-p) - "Update the titles of the current buffer into the cache. -If UPDATE-P is non-nil, first remove titles for the file in the database. -Returns the number of rows inserted." - (let* ((file (or org-roam-file-name (buffer-file-name))) - (titles (or (org-roam--extract-titles) - (list (org-roam--path-to-slug file)))) - (rows (mapcar (lambda (title) - (vector file title)) titles))) - (when update-p - (org-roam-db-query [:delete :from titles - :where (= file $s1)] - file)) - (org-roam-db-query - [:insert :into titles - :values $v1] - rows) - (length rows))) +(defun org-roam-db-get-scheduled-time () + "Return the scheduled time at point in ISO8601 format." + (when-let ((time (org-get-scheduled-time (point)))) + (org-format-time-string "%FT%T%z" time))) -(defun org-roam-db--insert-refs (&optional update-p) - "Update the refs of the current buffer into the cache. -If UPDATE-P is non-nil, first remove the ref for the file in the database." - (let ((file (or org-roam-file-name (buffer-file-name))) - (count 0)) - (when update-p - (org-roam-db-query [:delete :from refs - :where (= file $s1)] - file)) - (when-let ((refs (org-roam--extract-refs))) - (dolist (ref refs) - (let ((key (cdr ref)) - (type (car ref))) - (condition-case nil - (progn - (org-roam-db-query - [:insert :into refs :values $v1] - (list (vector key file type))) - (cl-incf count)) - (error - (lwarn '(org-roam) :error - (format "Duplicate ref %s in:\n\nA: %s\nB: %s\n\nskipping..." - key - file - (caar (org-roam-db-query - [:select file :from refs - :where (= ref $v1)] - (vector key)))))))))) - count)) +(defun org-roam-db-get-deadline-time () + "Return the deadline time at point in ISO8601 format." + (when-let ((time (org-get-deadline-time (point)))) + (org-format-time-string "%FT%T%z" time))) -(defun org-roam-db--insert-links (&optional update-p) - "Update the file links of the current buffer in the cache. -If UPDATE-P is non-nil, first remove the links for the file in the database. -Return the number of rows inserted." - (let ((file (or org-roam-file-name (buffer-file-name)))) - (when update-p - (org-roam-db-query [:delete :from links - :where (= source $s1)] - file)) - (if-let ((links (org-roam--extract-links))) - (progn - (org-roam-db-query - [:insert :into links +(defun org-roam-db-node-p () + "Return t if headline at point is a node, else return nil." + (and (org-id-get) + (not (cdr (assoc "ROAM_EXCLUDE" (org-entry-properties)))) + (funcall org-roam-db-node-include-function))) + +(defun org-roam-db-map-nodes (fns) + "Run FNS over all nodes in the current buffer." + (org-with-point-at 1 + (org-map-entries + (lambda () + (when (org-roam-db-node-p) + (dolist (fn fns) + (funcall fn))))))) + +(defun org-roam-db-map-links (fns) + "Run FNS over all links in the current buffer." + (org-with-point-at 1 + (org-element-map (org-element-parse-buffer) 'link + (lambda (link) + (dolist (fn fns) + (funcall fn link)))))) + +(defun org-roam-db-insert-file-node () + "Insert the file-level node into the Org-roam cache." + (org-with-point-at 1 + (when (and (= (org-outline-level) 0) + (org-roam-db-node-p)) + (when-let ((id (org-id-get))) + (let* ((file (buffer-file-name (buffer-base-buffer))) + (title (org-link-display-format + (or (cadr (assoc "TITLE" (org-collect-keywords '("title")) + #'string-equal)) + (file-relative-name file org-roam-directory)))) + (pos (point)) + (todo nil) + (priority nil) + (scheduled nil) + (deadline nil) + (level 0) + (aliases (org-entry-get (point) "ROAM_ALIASES")) + (tags org-file-tags) + (refs (org-entry-get (point) "ROAM_REFS")) + (properties (org-entry-properties)) + (olp (org-get-outline-path))) + (org-roam-db-query! + (lambda (err) + (lwarn 'org-roam :warning "%s for %s (%s) in %s" + (error-message-string err) + title id file)) + [:insert :into nodes :values $v1] - links) - (length links)) - 0))) - -(defun org-roam-db--insert-ids (&optional update-p) - "Update the ids of the current buffer into the cache. -If UPDATE-P is non-nil, first remove ids for the file in the database. -Returns the number of rows inserted." - (let ((file (or org-roam-file-name (buffer-file-name)))) - (when update-p - (org-roam-db-query [:delete :from ids - :where (= file $s1)] - file)) - (if-let ((ids (org-roam--extract-ids file))) - (condition-case nil - (progn + (vector id file level pos todo priority + scheduled deadline title properties olp)) + (when tags (org-roam-db-query - [:insert :into ids + [:insert :into tags :values $v1] - ids) - (length ids)) - (error - (lwarn '(org-roam) :error - (format "Duplicate IDs in %s, one of:\n\n%s\n\nskipping..." - (aref (car ids) 1) - (string-join (mapcar (lambda (hl) - (aref hl 0)) ids) "\n"))) - 0)) - 0))) + (mapcar (lambda (tag) + (vector id (substring-no-properties tag))) + tags))) + (when aliases + (org-roam-db-query + [:insert :into aliases + :values $v1] + (mapcar (lambda (alias) + (vector id alias)) + (split-string-and-unquote aliases)))) + (when refs + (setq refs (split-string-and-unquote refs)) + (let (rows) + (dolist (ref refs) + (if (string-match org-link-plain-re ref) + (progn + (push (vector id (match-string 2 ref) + (match-string 1 ref)) rows)) + (lwarn '(org-roam) :warning + "%s:%s\tInvalid ref %s, skipping..." + (buffer-file-name) (point) ref))) + (when rows + (org-roam-db-query + [:insert :into refs + :values $v1] + rows))))))))) -(defun org-roam-db--insert-tags (&optional update-p) - "Insert tags for the current buffer into the Org-roam cache. -If UPDATE-P is non-nil, first remove tags for the file in the database. -Return the number of rows inserted." - (let* ((file (or org-roam-file-name (buffer-file-name))) - (tags (org-roam--extract-tags file))) - (when update-p - (org-roam-db-query [:delete :from tags - :where (= file $s1)] - file)) - (if tags - (progn (org-roam-db-query - [:insert :into tags - :values $v1] - (list (vector file tags))) - 1) - 0))) +(defun org-roam-db-insert-node-data () + "Insert node data for headline at point into the Org-roam cache." + (when-let ((id (org-id-get))) + (let* ((file (buffer-file-name (buffer-base-buffer))) + (heading-components (org-heading-components)) + (pos (point)) + (todo (nth 2 heading-components)) + (priority (nth 3 heading-components)) + (level (nth 1 heading-components)) + (scheduled (org-roam-db-get-scheduled-time)) + (deadline (org-roam-db-get-deadline-time)) + (title (org-link-display-format (nth 4 heading-components))) + (properties (org-entry-properties)) + (olp (org-get-outline-path))) + (org-roam-db-query! + (lambda (err) + (lwarn 'org-roam :warning "%s for %s (%s) in %s" + (error-message-string err) + title id file)) + [:insert :into nodes + :values $v1] + (vector id file level pos todo priority + scheduled deadline title properties olp))))) + +(defun org-roam-db-insert-aliases () + "Insert aliases for node at point into Org-roam cache." + (when-let ((node-id (org-id-get)) + (aliases (org-entry-get (point) "ROAM_ALIASES"))) + (org-roam-db-query [:insert :into aliases + :values $v1] + (mapcar (lambda (alias) + (vector node-id alias)) + (split-string-and-unquote aliases))))) + +(defun org-roam-db-insert-tags () + "Insert tags for node at point into Org-roam cache." + (when-let ((node-id (org-id-get)) + (tags (org-get-tags))) + (org-roam-db-query [:insert :into tags + :values $v1] + (mapcar (lambda (tag) + (vector node-id (substring-no-properties tag))) tags)))) + +(defun org-roam-db-insert-refs () + "Insert refs for node at point into Org-roam cache." + (when-let* ((node-id (org-id-get)) + (refs (org-entry-get (point) "ROAM_REFS")) + (refs (split-string-and-unquote refs))) + (let (rows) + (dolist (ref refs) + (save-match-data + (if (string-match org-link-plain-re ref) + (progn + (push (vector node-id (match-string 2 ref) (match-string 1 ref)) rows)) + (lwarn '(org-roam) :warning + "%s:%s\tInvalid ref %s, skipping..." (buffer-file-name) (point) ref)))) + (org-roam-db-query [:insert :into refs + :values $v1] + rows)))) + +(defun org-roam-db-insert-link (link) + "Insert link data for LINK at current point into the Org-roam cache." + (save-excursion + (goto-char (org-element-property :begin link)) + (let ((type (org-element-property :type link)) + (dest (org-element-property :path link)) + (properties (list :outline (org-get-outline-path))) + (source (org-roam-id-at-point))) + (when source + (org-roam-db-query + [:insert :into links + :values $v1] + (vector (point) source dest type properties)))))) ;;;;; Fetching -(defun org-roam-db-has-file-p (file) - "Return t if FILE is in the database, nil otherwise." - (> (caar (org-roam-db-query [:select (funcall count) :from files - :where (= file $s1)] - file)) - 0)) - (defun org-roam-db--get-current-files () "Return a hash-table of file to the hash of its file contents." - (let* ((current-files (org-roam-db-query [:select * :from files])) - (ht (make-hash-table :test #'equal))) + (let ((current-files (org-roam-db-query [:select [file hash] :from files])) + (ht (make-hash-table :test #'equal))) (dolist (row current-files) (puthash (car row) (cadr row) ht)) ht)) -(defun org-roam-db--get-title (file) - "Return the main title of FILE from the cache." - (caar (org-roam-db-query [:select [title] :from titles - :where (= file $s1) - :limit 1] - file))) - -(defun org-roam-db--get-tags () - "Return all distinct tags from the cache." - (let ((rows (org-roam-db-query [:select :distinct [tags] :from tags])) - acc) - (dolist (row rows) - (dolist (tag (car row)) - (unless (member tag acc) - (push tag acc)))) - acc)) - -(defun org-roam-db--connected-component (file) - "Return all files reachable from/connected to FILE, including the file itself. -If the file does not have any connections, nil is returned." - (let* ((query "WITH RECURSIVE - links_of(file, link) AS - (WITH filelinks AS (SELECT * FROM links WHERE NOT \"type\" = '\"cite\"'), - citelinks AS (SELECT * FROM links - JOIN refs ON links.\"dest\" = refs.\"ref\" - AND links.\"type\" = '\"cite\"') - SELECT \"source\", \"dest\" FROM filelinks UNION - SELECT \"dest\", \"source\" FROM filelinks UNION - SELECT \"file\", \"source\" FROM citelinks UNION - SELECT \"dest\", \"file\" FROM citelinks), - connected_component(file) AS - (SELECT link FROM links_of WHERE file = $s1 - UNION - SELECT link FROM links_of JOIN connected_component USING(file)) - SELECT * FROM connected_component;") - (files (mapcar 'car-safe (emacsql (org-roam-db) query file)))) - files)) - -(defun org-roam-db--links-with-max-distance (file max-distance) - "Return all files connected to FILE in at most MAX-DISTANCE steps. -This includes the file itself. If the file does not have any -connections, nil is returned." - (let* ((query "WITH RECURSIVE - links_of(file, link) AS - (WITH filelinks AS (SELECT * FROM links WHERE NOT \"type\" = '\"cite\"'), - citelinks AS (SELECT * FROM links - JOIN refs ON links.\"dest\" = refs.\"ref\" - AND links.\"type\" = '\"cite\"') - SELECT \"source\", \"dest\" FROM filelinks UNION - SELECT \"dest\", \"source\" FROM filelinks UNION - SELECT \"file\", \"source\" FROM citelinks UNION - SELECT \"source\", \"file\" FROM citelinks), - -- Links are traversed in a breadth-first search. In order to calculate the - -- distance of nodes and to avoid following cyclic links, the visited nodes - -- are tracked in 'trace'. - connected_component(file, trace) AS - (VALUES($s1, json_array($s1)) - UNION - SELECT lo.link, json_insert(cc.trace, '$[' || json_array_length(cc.trace) || ']', lo.link) FROM - connected_component AS cc JOIN links_of AS lo USING(file) - WHERE ( - -- Avoid cycles by only visiting each file once. - (SELECT count(*) FROM json_each(cc.trace) WHERE json_each.value == lo.link) == 0 - -- Note: BFS is cut off early here. - AND json_array_length(cc.trace) < ($s2 + 1))) - SELECT DISTINCT file, min(json_array_length(trace)) AS distance - FROM connected_component GROUP BY file ORDER BY distance;") - ;; In principle the distance would be available in the second column. - (files (mapcar 'car-safe (emacsql (org-roam-db) query file max-distance)))) - files)) - (defun org-roam-db--file-hash (&optional file-path) "Compute the hash of FILE-PATH, a file or current buffer." (if file-path @@ -492,30 +451,9 @@ connections, nil is returned." (secure-hash 'sha1 (current-buffer))))) ;;;;; Updating -(defun org-roam-db--update-file (&optional file-path) - "Update Org-roam cache for FILE-PATH. -If the file does not exist anymore, remove it from the cache. -If the file exists, update the cache with information." - (setq file-path (or file-path - (buffer-file-name (buffer-base-buffer)))) - (if (not (file-exists-p file-path)) - (org-roam-db--clear-file file-path) - ;; save the file before performing a database update - (when-let ((buf (find-buffer-visiting file-path))) - (with-current-buffer buf - (save-buffer))) - (org-roam--with-temp-buffer file-path - (emacsql-with-transaction (org-roam-db) - (org-roam-db--insert-meta 'update) - (org-roam-db--insert-tags 'update) - (org-roam-db--insert-titles 'update) - (org-roam-db--insert-refs 'update) - (when org-roam-enable-headline-linking - (org-roam-db--insert-ids 'update)) - (org-roam-db--insert-links 'update))))) - -(defun org-roam-db-build-cache (&optional force) - "Build the cache for `org-roam-directory'. +;;;###autoload +(defun org-roam-db-sync (&optional force) + "Synchronize the cache state with the current Org files on-disk. If FORCE, force a rebuild of the cache from scratch." (interactive "P") (when force (delete-file org-roam-db-location)) @@ -525,109 +463,61 @@ If FORCE, force a rebuild of the cache from scratch." (org-agenda-files nil) (org-roam-files (org-roam--list-all-files)) (current-files (org-roam-db--get-current-files)) - (count-plist nil) - (deleted-count 0) (modified-files nil)) (dolist (file org-roam-files) (let ((contents-hash (org-roam-db--file-hash file))) (unless (string= (gethash file current-files) - contents-hash) - (push (cons file contents-hash) modified-files))) + contents-hash) + (push file modified-files))) (remhash file current-files)) - (dolist (file (hash-table-keys current-files)) - ;; These files are no longer around, remove from cache... - (org-roam-db--clear-file file) - (setq deleted-count (1+ deleted-count))) - (setq count-plist (org-roam-db--update-files modified-files)) - (org-roam-message "total: Δ%s, files-modified: Δ%s, ids: Δ%s, links: Δ%s, tags: Δ%s, titles: Δ%s, refs: Δ%s, deleted: Δ%s" - (- (length org-roam-files) (plist-get count-plist :error-count)) - (plist-get count-plist :modified-count) - (plist-get count-plist :id-count) - (plist-get count-plist :link-count) - (plist-get count-plist :tag-count) - (plist-get count-plist :title-count) - (plist-get count-plist :ref-count) - deleted-count))) + (emacsql-with-transaction (org-roam-db) + (if (fboundp 'dolist-with-progress-reporter) + (dolist-with-progress-reporter (file (hash-table-keys current-files)) + "Clearing removed files..." + (org-roam-db-clear-file file)) + (dolist (file (hash-table-keys current-files)) + (org-roam-db-clear-file file))) + (if (fboundp 'dolist-with-progress-reporter) + (dolist-with-progress-reporter (file modified-files) + "Processing modified files..." + (org-roam-db-update-file file)) + (dolist (file modified-files) + (org-roam-db-update-file file)))))) -(defun org-roam-db--get-file-hash-from-db (&optional file-path) - "Get hash from Org-roam database for FILE-PATH." - (setq file-path (or file-path - (buffer-file-name (buffer-base-buffer)))) - (caar (org-roam-db-query [:select hash :from files - :where (= file $s1)] file-path))) - -(defun org-roam-db-update-file (file-path) +(defun org-roam-db-update-file (&optional file-path) "Update Org-roam cache for FILE-PATH. If the file does not exist anymore, remove it from the cache. If the file exists, update the cache with information." + (setq file-path (or file-path (buffer-file-name (buffer-base-buffer)))) (let ((content-hash (org-roam-db--file-hash file-path)) - (db-hash (org-roam-db--get-file-hash-from-db file-path))) + (db-hash (caar (org-roam-db-query [:select hash :from files + :where (= file $s1)] file-path)))) (unless (string= content-hash db-hash) - (org-roam-db--update-files (list (cons file-path content-hash))) - (org-roam-message "Updated: %s" file-path)))) + (org-roam-with-file file-path nil + (save-excursion + (org-set-regexps-and-options 'tags-only) + (org-roam-db-clear-file) + (org-roam-db-insert-file) + (org-roam-db-insert-file-node) + (org-roam-db-map-nodes + (list #'org-roam-db-insert-node-data + #'org-roam-db-insert-aliases + #'org-roam-db-insert-tags + #'org-roam-db-insert-refs)) + (org-roam-db-map-links + (list #'org-roam-db-insert-link))))))) -(defun org-roam-db--update-files (modified-files) - "Update Org-roam cache for a list of MODIFIED-FILES. -FILES is a list of (file . hash) pairs." - (let* ((gc-cons-threshold org-roam-db-gc-threshold) - (org-agenda-files nil) - (error-count 0) - (id-count 0) - (link-count 0) - (tag-count 0) - (title-count 0) - (ref-count 0) - (modified-count 0)) - (pcase-dolist (`(,file . _) modified-files) - (org-roam-db--clear-file file)) - ;; Process all the files for IDs first - ;; - ;; We do this so that link extraction is cheaper: this eliminates the need - ;; to read the file to check if the ID really exists - (pcase-dolist (`(,file . ,contents-hash) modified-files) - (let* ((attr (file-attributes file)) - (atime (file-attribute-access-time attr)) - (mtime (file-attribute-modification-time attr))) - (condition-case nil - (org-roam--with-temp-buffer file - (org-roam-db-query - [:insert :into files - :values $v1] - (vector file contents-hash (list :atime atime :mtime mtime))) - (when org-roam-enable-headline-linking - (setq id-count (+ id-count (org-roam-db--insert-ids))))) - (file-error - (setq error-count (1+ error-count)) - (org-roam-db--clear-file file) - (lwarn '(org-roam) :warning - "Skipping unreadable file while building cache: %s" file))))) +(defun org-roam-db--update-on-save-h () + "." + (add-hook 'after-save-hook #'org-roam-db-update-file nil t)) - ;; Process titles, tags, links and ref links of file - (pcase-dolist (`(,file . _) modified-files) - (org-roam-message "Processed %s/%s modified files..." modified-count (length modified-files)) - (condition-case nil - (org-roam--with-temp-buffer file - (setq modified-count (1+ modified-count)) - (setq link-count (+ link-count (org-roam-db--insert-links))) - (setq tag-count (+ tag-count (org-roam-db--insert-tags))) - (setq title-count (+ title-count (org-roam-db--insert-titles))) - (setq ref-count (+ ref-count (org-roam-db--insert-refs)))) - (file-error - (setq error-count (1+ error-count)) - (org-roam-db--clear-file file) - (lwarn '(org-roam) :warning - "Skipping unreadable file while building cache: %s" file)))) - (list :error-count error-count :modified-count modified-count :id-count id-count :title-count title-count :tag-count tag-count :link-count link-count :ref-count ref-count))) +(add-hook 'org-roam-find-file-hook #'org-roam-db--update-on-save-h) -(defun org-roam-db-update () - "Update the database." - (pcase org-roam-db-update-method - ('immediate - (org-roam-db-update-file (buffer-file-name (buffer-base-buffer)))) - ('idle-timer - (org-roam-db-mark-dirty)) - (_ - (user-error "Invalid `org-roam-db-update-method'")))) +;; Diagnostic Interactives +(defun org-roam-db-diagnose-node () + "Print information about node at point." + (interactive) + (prin1 (org-roam-node-at-point))) (provide 'org-roam-db) diff --git a/org-roam-dev.el b/org-roam-dev.el deleted file mode 100644 index 47686b0..0000000 --- a/org-roam-dev.el +++ /dev/null @@ -1,46 +0,0 @@ -;;; org-roam-dev.el --- Org-roam development code -mode -*- coding: utf-8; lexical-binding: t; -*- - -;; Copyright © 2020 Jethro Kuan - -;; Author: Jethro Kuan -;; URL: https://github.com/org-roam/org-roam -;; Keywords: org-mode, roam, convenience -;; Version: 1.2.3 -;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (s "1.12.0") (org "9.3") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2")) - -;; This file is NOT part of GNU Emacs. - -;; 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, 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 GNU Emacs; see the file COPYING. If not, write to the -;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, -;; Boston, MA 02110-1301, USA. - -;;; Commentary: -;; -;; This library provides code for org-roam developers. -;; It is intended to be loaded before editing org-roam source files. -;; It ensures consistent application of various developer settings. -;; -;;; Code: -(require 'emacsql) - -;;;###autoload -(define-minor-mode org-roam-dev-mode - "Minor mode for setting the dev environment of Org-roam." - :lighter " ORD" - (when org-roam-dev-mode - (emacsql-fix-vector-indentation) - (setq-local sentence-end-double-space nil))) - -(provide 'org-roam-dev) -;;; org-roam-dev.el ends here diff --git a/org-roam-doctor.el b/org-roam-doctor.el deleted file mode 100644 index 66c334b..0000000 --- a/org-roam-doctor.el +++ /dev/null @@ -1,318 +0,0 @@ -;;; org-roam-doctor.el --- Linter for Org-roam files -*- coding: utf-8; lexical-binding: t; -*- -;; -;; Copyright © 2020 Jethro Kuan - -;; Author: Jethro Kuan -;; URL: https://github.com/jethrokuan/org-roam -;; Keywords: org-mode, roam, convenience -;; Version: 1.2.3 -;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (s "1.12.0") (org "9.3") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2")) - -;; This file is NOT part of GNU Emacs. - -;; 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, 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 GNU Emacs; see the file COPYING. If not, write to the -;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, -;; Boston, MA 02110-1301, USA. - - -;;; Commentary: -;; -;; This library provides `org-roam-doctor', a utility for diagnosing and fixing -;; Org-roam files. Running `org-roam-doctor' launches a list of checks defined -;; by `org-roam-doctor--checkers'. Every checker is an instance of -;; `org-roam-doctor-checker'. -;; -;; Each checker is given the Org parse tree (AST), and is expected to return a -;; list of errors. The checker can also provide "actions" for auto-fixing errors -;; (see `org-roam-doctor--remove-link' for an example). -;; -;; The UX experience is inspired by both org-lint and checkdoc, and their code -;; is heavily referenced. -;; -;;; Code: -;; Library Requires -(require 'cl-lib) -(require 'org) -(require 'org-element) -(require 's) -(require 'dash) -(require 'org-roam-macs) - -(declare-function org-roam-insert "org-roam") -(declare-function org-roam--get-roam-buffers "org-roam") -(declare-function org-roam--list-all-files "org-roam") -(declare-function org-roam--org-roam-file-p "org-roam") -(declare-function org-roam-mode "org-roam") - -(defvar org-roam-verbose) -(defvar org-roam-mode) - -(defcustom org-roam-doctor-inhibit-startup t - "Inhibit `org-mode' startup when processing files with `org-doctor'. -When non-nil, images and LaTeX preview will not be generated, -tables will not be aligned, and headlines will not respect -startup visability. This significantly improves performance when -processing multiple files" - :type 'boolean - :group 'org-roam) - -(cl-defstruct (org-roam-doctor-checker (:copier nil)) - (name 'missing-checker-name) - (description "") - (actions nil)) - -(defconst org-roam-doctor--checkers - (list - (make-org-roam-doctor-checker - :name 'org-roam-doctor-broken-links - :description "Fix broken links." - :actions '(("d" . ("Unlink" . org-roam-doctor--remove-link)) - ("r" . ("Replace link" . org-roam-doctor--replace-link)) - ("R" . ("Replace link (keep label)" . org-roam-doctor--replace-link-keep-label)))) - (make-org-roam-doctor-checker - :name 'org-roam-doctor-check-roam-props - :description "Check #+roam_* properties.") - (make-org-roam-doctor-checker - :name 'org-roam-doctor-check-tags - :description "Check #+roam_tags.") - (make-org-roam-doctor-checker - :name 'org-roam-doctor-check-alias - :description "Check #+roam_alias."))) - -(defconst org-roam-doctor--supported-roam-properties - '("roam_tags" "roam_alias" "roam_key") - "List of supported Org-roam properties.") - -(defun org-roam-doctor-check-roam-props (ast) - "Checker for detecting invalid #+roam_* properties. -AST is the org-element parse tree." - (let (reports) - (org-element-map ast 'keyword - (lambda (kw) - (let ((key (org-element-property :key kw))) - (when (and (string-prefix-p "ROAM_" key t) - (not (member (downcase key) org-roam-doctor--supported-roam-properties))) - (push - `(,(org-element-property :begin kw) - ,(concat "Possible mispelled key: " - (prin1-to-string key) - "\nOrg-roam supports the following keys: " - (s-join ", " org-roam-doctor--supported-roam-properties))) - reports))))) - reports)) - -(defun org-roam-doctor-check-tags (ast) - "Checker for detecting invalid #+roam_tags. -AST is the org-element parse tree." - (let (reports) - (org-element-map ast 'keyword - (lambda (kw) - (when (string-collate-equalp (org-element-property :key kw) "roam_tags" nil t) - (let ((tags (org-element-property :value kw))) - (condition-case nil - (split-string-and-unquote tags) - (error - (push - `(,(org-element-property :begin kw) - ,(concat "Unable to parse tags: " - tags - (when (s-contains? "," tags) - "\nCheck that your tags are not comma-separated."))) - reports))))))) - reports)) - -(defun org-roam-doctor-check-alias (ast) - "Checker for detecting invalid #+roam_alias. -AST is the org-element parse tree." - (let (reports) - (org-element-map ast 'keyword - (lambda (kw) - (when (string-collate-equalp (org-element-property :key kw) "roam_alias" nil t) - (let ((aliases (org-element-property :value kw))) - (condition-case nil - (split-string-and-unquote aliases) - (error - (push - `(,(org-element-property :begin kw) - ,(concat "Unable to parse aliases: " - aliases - (when (s-contains? "," aliases) - "\nCheck that your aliases are not comma-separated."))) - reports))))))) - reports)) - -(defun org-roam-doctor-broken-links (ast) - "Checker for detecting broken links. -AST is the org-element parse tree." - (let (reports) - (org-element-map ast 'link - (lambda (l) - (when (equal "file" (org-element-property :type l)) - (let ((file (org-element-property :path l))) - (or (file-exists-p file) - (file-remote-p file) - (push - `(,(org-element-property :begin l) - ,(format (if (org-element-lineage l '(link)) - "Link to non-existent image file \"%s\"\ - in link description" - "Link to non-existent local file \"%s\"") - file)) - reports)))))) - reports)) - -(defun org-roam-doctor--check (buffer checkers) - "Check BUFFER for errors. -CHECKERS is the list of checkers used." - (with-current-buffer buffer - (save-excursion - (goto-char (point-min)) - (let* ((ast (org-element-parse-buffer)) - (errors (sort (cl-mapcan - (lambda (c) - (mapcar - (lambda (report) - (list (set-marker (make-marker) (car report)) - (nth 1 report) c)) - (save-excursion - (funcall - (org-roam-doctor-checker-name c) - ast)))) - checkers) - #'car-less-than-car))) - (dolist (e errors) - (pcase-let ((`(,m ,msg ,checker) e)) - (switch-to-buffer buffer) - (goto-char m) - (org-reveal) - (undo-boundary) - (org-roam-doctor--resolve msg checker) - (set-marker m nil))) - errors)))) - -;;; Actions -(defun org-roam-doctor--recursive-edit () - "Launch into a recursive edit." - (message "When you're done editing press C-M-c to continue.") - (recursive-edit)) - -(defun org-roam-doctor--skip () - "Skip the current error." - (org-roam-message "Skipping...")) - -(defun org-roam-doctor--replace-link () - "Replace the current link with a new link." - (save-match-data - (unless (org-in-regexp org-link-bracket-re 1) - (user-error "No link at point")) - (let ((orig (buffer-string)) - (p (point))) - (condition-case nil - (save-excursion - (replace-match "") - (org-roam-insert)) - (quit (progn - (replace-buffer-contents orig) - (goto-char p))))))) - -(defun org-roam-doctor--replace-link-keep-label () - "Replace the current link with a new link, keeping the current link's label." - (save-match-data - (unless (org-in-regexp org-link-bracket-re 1) - (user-error "No link at point")) - (let ((orig (buffer-string)) - (p (point))) - (condition-case nil - (save-excursion - (let ((label (if (match-end 2) - (match-string-no-properties 2) - (org-link-unescape (match-string-no-properties 1))))) - (replace-match "") - (org-roam-insert nil nil label))) - (quit (progn - (replace-buffer-contents orig) - (goto-char p))))))) - -(defun org-roam-doctor--remove-link () - "Unlink the text at point." - (unless (org-in-regexp org-link-bracket-re 1) - (user-error "No link at point")) - (save-excursion - (let ((label (if (match-end 2) - (match-string-no-properties 2) - (org-link-unescape (match-string-no-properties 1))))) - (delete-region (match-beginning 0) (match-end 0)) - (insert label)))) - -(defun org-roam-doctor--resolve (msg checker) - "Resolve an error. -MSG is the error that was found, which is displayed in a help buffer. -CHECKER is a org-roam-doctor checker instance." - (let ((actions (org-roam-doctor-checker-actions checker)) - c) - (push '("e" . ("Edit" . org-roam-doctor--recursive-edit)) actions) - (push '("s" . ("Skip" . org-roam-doctor--skip)) actions) - (with-output-to-temp-buffer "*Org-roam-doctor Help*" - (mapc #'princ - (list "Error message:\n " msg "\n\n")) - (dolist (action actions) - (princ (format "[%s]: %s\n" - (car action) - (cadr action)))) - (princ "\n\n")) - (shrink-window-if-larger-than-buffer - (get-buffer-window "*Org-roam-doctor Help*")) - (message "Press key for command:") - (unwind-protect - (progn - (cl-loop - do (setq c (char-to-string (read-char-exclusive))) - until (assoc c actions) - do (message "Please enter a valid key for command:")) - (funcall (cddr (assoc c actions))) - (redisplay)) - (when (get-buffer-window "*Org-roam-doctor Help*") - (delete-window (get-buffer-window "*Org-roam-doctor Help*")) - (kill-buffer "*Org-roam-doctor Help*"))))) - -;;;###autoload -(defun org-roam-doctor (&optional checkall) - "Perform a check on the current buffer to ensure cleanliness. -If CHECKALL, run the check for all Org-roam files." - (interactive "P") - (unless org-roam-mode (org-roam-mode)) - (let ((files (if checkall - (org-roam--list-all-files) - (unless (org-roam--org-roam-file-p) - (user-error "Not in an org-roam file")) - `(,(buffer-file-name))))) - (org-roam-doctor-start files org-roam-doctor--checkers))) - -(defun org-roam-doctor-start (files checkers) - "Lint FILES using CHECKERS." - (save-window-excursion - (let ((existing-buffers (org-roam--get-roam-buffers)) - (org-inhibit-startup org-roam-doctor-inhibit-startup)) - (dolist (f files) - (let ((buf (find-file-noselect f))) - (org-roam-doctor--check buf checkers) - (unless (memq buf existing-buffers) - (with-current-buffer buf - (save-buffer)) - (kill-buffer buf)))))) - (org-roam-message "Linting completed.")) - -(provide 'org-roam-doctor) - -;;; org-roam-doctor.el ends here diff --git a/org-roam-faces.el b/org-roam-faces.el deleted file mode 100644 index b3014fc..0000000 --- a/org-roam-faces.el +++ /dev/null @@ -1,77 +0,0 @@ -;;; org-roam-faces.el --- Face definitions -*- coding: utf-8; lexical-binding: t; -*- - -;; Copyright © 2020 Jethro Kuan - -;; Author: Jethro Kuan -;; URL: https://github.com/org-roam/org-roam -;; Keywords: org-mode, roam, convenience -;; Version: 1.2.3 -;; Package-Requires: ((emacs "26.1")) - -;; This file is NOT part of GNU Emacs. - -;; 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, 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 GNU Emacs; see the file COPYING. If not, write to the -;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, -;; Boston, MA 02110-1301, USA. - -;;; Commentary: - -;; This file contains the face definitions for Org-roam. - -;;; Code: - -(defgroup org-roam-faces nil - "Faces used by Org-roam." - :group 'org-roam - :group 'faces) - -;;; Definitions -(defface org-roam-link - '((t :inherit org-link)) - "Face for Org-roam links." - :group 'org-roam-faces) - -(defface org-roam-tag - '((t :weight bold)) - "Face for Org-roam tags in minibuffer commands." - :group 'org-roam-faces) - -(defface org-roam-link-current - '((t :inherit org-link)) - "Face for Org-roam links pointing to the current buffer." - :group 'org-roam-faces) - -(defface org-roam-link-invalid - '((t :inherit (error org-link))) - "Face for Org-roam links that are not valid. -This face is used for links without a destination." - :group 'org-roam-faces) - -(defface org-roam-link-shielded - '((t :inherit (warning org-link))) - "Face for Org-roam links that are shielded. -This face is used on the region target by `org-roam-insertion' -during an `org-roam-capture'." - :group 'org-roam-faces) - -(defface org-roam-dailies-calendar-note - '((t :inherit (org-roam-link) :underline nil)) - "Face for dates with a daily-note in the calendar." - :group 'org-roam-faces) - -;;; _ - -(provide 'org-roam-faces) - -;;; org-roam-faces.el ends here diff --git a/org-roam-graph.el b/org-roam-graph.el index d115cf2..1c264dd 100644 --- a/org-roam-graph.el +++ b/org-roam-graph.el @@ -1,12 +1,12 @@ ;;; org-roam-graph.el --- Graphing API -*- coding: utf-8; lexical-binding: t; -*- -;; Copyright © 2020 Jethro Kuan +;; Copyright © 2020-2021 Jethro Kuan ;; Author: Jethro Kuan ;; URL: https://github.com/org-roam/org-roam ;; Keywords: org-mode, roam, convenience -;; Version: 1.2.3 -;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (s "1.12.0") (org "9.3") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2")) +;; Version: 2.0.0 +;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite "1.0.0") (magit-section "2.90.1")) ;; This file is NOT part of GNU Emacs. @@ -31,17 +31,12 @@ ;; ;;; Code: (require 'xml) ;xml-escape-string -(require 's) ;s-truncate, s-replace (eval-and-compile (require 'org-roam-macs)) (require 'org-roam-db) ;;;; Declarations (defvar org-roam-directory) -(defvar org-roam-mode) -(declare-function org-roam--org-roam-file-p "org-roam") -(declare-function org-roam--path-to-slug "org-roam") -(declare-function org-roam-mode "org-roam") ;;;; Options (defcustom org-roam-graph-viewer (executable-find "firefox") @@ -74,31 +69,34 @@ Example: :type '(alist) :group 'org-roam) +(defcustom org-roam-graph-edge-extra-config nil + "Extra edge options passed to graphviz. +Example: + '((\"dir\" . \"back\"))" + :type '(alist) + :group 'org-roam) + (defcustom org-roam-graph-node-extra-config - '(("shape" . "underline") - ("style" . "rounded,filled") - ("fillcolor" . "#EEEEEE") - ("color" . "#C9C9C9") - ("fontcolor" . "#111111")) - "Extra options for graphviz nodes. -Example: - '((\"color\" . \"skyblue\"))" + '(("id" . (("style" . "bold,rounded,filled") + ("fillcolor" . "#EEEEEE") + ("color" . "#C9C9C9") + ("fontcolor" . "#111111"))) + ("http" . (("style" . "rounded,filled") + ("fillcolor" . "#EEEEEE") + ("color" . "#C9C9C9") + ("fontcolor" . "#0A97A6"))) + ("https" . (("shape" . "rounded,filled") + ("fillcolor" . "#EEEEEE") + ("color" . "#C9C9C9") + ("fontcolor" . "#0A97A6")))) + "Extra options for graphviz nodes." :type '(alist) :group 'org-roam) -(defcustom org-roam-graph-edge-extra-config - '(("color" . "#333333")) - "Extra options for graphviz edges. -Example: - '((\"dir\" . \"back\"))" - :type '(alist) - :group 'org-roam) - -(defcustom org-roam-graph-edge-cites-extra-config '(("color" . "red")) - "Extra options for graphviz edges for citation links. -Example: - '((\"dir\" . \"back\"))" - :type '(alist) +(defcustom org-roam-graph-link-hidden-types + '("file") + "What sort of links to hide from the Org-roam graph." + :type '(repeat string) :group 'org-roam) (defcustom org-roam-graph-max-title-length 100 @@ -119,45 +117,6 @@ All other values including nil will have no effect." (const :tag "no" nil)) :group 'org-roam) -(defcustom org-roam-graph-exclude-matcher nil - "Matcher for excluding nodes from the generated graph. -Any nodes and links for file paths matching this string is -excluded from the graph. - -If value is a string, the string is the only matcher. - -If value is a list, all file paths matching any of the strings -are excluded." - :type '(choice - (string :tag "Matcher") - (list :tag "Matchers")) - :group 'org-roam) - -;;;; Functions -(defun org-roam-graph--expand-matcher (col &optional negate where) - "Return the exclusion regexp from `org-roam-graph-exclude-matcher'. -COL is the symbol to be matched against. if NEGATE, add :not to sql query. -set WHERE to true if WHERE query already exists." - (let ((matchers (pcase org-roam-graph-exclude-matcher - ('nil nil) - ((pred stringp) `(,(concat "%" org-roam-graph-exclude-matcher "%"))) - ((pred listp) (mapcar (lambda (m) - (concat "%" m "%")) - org-roam-graph-exclude-matcher)) - (_ (error "Invalid org-roam-graph-exclude-matcher")))) - res) - (dolist (match matchers) - (if where - (push :and res) - (push :where res) - (setq where t)) - (push col res) - (when negate - (push :not res)) - (push :like res) - (push match res)) - (nreverse res))) - (defun org-roam-graph--dot-option (option &optional wrap-key wrap-val) "Return dot string of form KEY=VAL for OPTION cons. If WRAP-KEY is non-nil it wraps the KEY. @@ -166,71 +125,104 @@ If WRAP-VAL is non-nil it wraps the VAL." "=" wrap-val (cdr option) wrap-val)) -(defun org-roam-graph--dot (node-query) - "Build the graphviz dot string for NODE-QUERY. -The Org-roam database titles table is read, to obtain the list of titles. -The links table is then read to obtain all directed links, and formatted -into a digraph." - (org-roam-db--ensure-built) - (org-roam--with-temp-buffer nil - (let* ((nodes (org-roam-db-query node-query)) - (edges-query - `[:with selected :as [:select [file] :from ,node-query] - :select :distinct [dest source] :from links - :where (and (in dest selected) (in source selected))]) - (edges-cites-query - `[:with selected :as [:select [file] :from ,node-query] - :select :distinct [file source] - :from links :inner :join refs :on (and (= links:dest refs:ref) - (= links:type "cite") - (= refs:type "cite")) - :where (and (in file selected) (in source selected))]) - (edges (org-roam-db-query edges-query)) - (edges-cites (org-roam-db-query edges-cites-query))) +(defun org-roam-graph--connected-component (id distance) + "Return the edges for all nodes reachable from/connected to ID. +DISTANCE is the maximum distance away from the root node." + (let* ((query + (if (= distance 0) + " +WITH RECURSIVE + links_of(source, dest) AS + (SELECT source, dest FROM links UNION + SELECT dest, source FROM links), + connected_component(source) AS + (SELECT dest FROM links_of WHERE source = $s1 UNION + SELECT dest FROM links_of JOIN connected_component USING(source)) +SELECT source, dest, type FROM links WHERE source IN connected_component OR dest IN connected_component;" + " +WITH RECURSIVE + links_of(source, dest) AS + (SELECT source, dest FROM links UNION + SELECT dest, source FROM links), + connected_component(source, trace) AS + (VALUES ($s1 , json_array($s1)) UNION + SELECT lo.dest, json_insert(cc.trace, '$[' || json_array_length(cc.trace) || ']', lo.dest) FROM + connected_component AS cc JOIN links_of AS lo USING(source) + WHERE ( + -- Avoid cycles by only visiting each node once. + (SELECT count(*) FROM json_each(cc.trace) WHERE json_each.value == lo.dest) == 0 + -- Note: BFS is cut off early here. + AND json_array_length(cc.trace) < $s2)), + nodes(source) as (SELECT DISTINCT source + FROM connected_component GROUP BY source ORDER BY min(json_array_length(trace))) +SELECT source, dest, type FROM links WHERE source IN nodes OR dest IN nodes;"))) + (org-roam-db-query query id distance))) + +(defun org-roam-graph--dot (&optional edges all-nodes) + "Build the graphviz given the EDGES of the graph. +If ALL-NODES, include also nodes without edges." + (let ((org-roam-directory-temp org-roam-directory) + (nodes-table (org-roam--nodes-table)) + (seen-nodes (list)) + (edges (or edges (org-roam-db-query [:select :distinct [source dest type] :from links])))) + (with-temp-buffer + (setq-local org-roam-directory org-roam-directory-temp) (insert "digraph \"org-roam\" {\n") (dolist (option org-roam-graph-extra-config) (insert (org-roam-graph--dot-option option) ";\n")) - (dolist (attribute '("node" "edge")) - (insert (format " %s [%s];\n" attribute - (mapconcat (lambda (var) - (org-roam-graph--dot-option var nil "\"")) - (symbol-value - (intern (concat "org-roam-graph-" attribute "-extra-config"))) - ",")))) - (dolist (node nodes) - (let* ((file (xml-escape-string (car node))) - (title (or (cadr node) - (org-roam--path-to-slug file))) - (shortened-title (pcase org-roam-graph-shorten-titles - (`truncate (s-truncate org-roam-graph-max-title-length title)) - (`wrap (s-word-wrap org-roam-graph-max-title-length title)) - (_ title))) - (shortened-title (org-roam-string-quote shortened-title)) - (title (org-roam-string-quote title)) - (node-properties - `(("label" . ,shortened-title) - ("URL" . ,(concat "org-protocol://roam-file?file=" (url-hexify-string file))) - ("tooltip" . ,(xml-escape-string title))))) - (insert - (format " \"%s\" [%s];\n" file - (mapconcat (lambda (n) - (org-roam-graph--dot-option n nil "\"")) - node-properties ","))))) - (dolist (edge edges) - (insert (apply #'format `(" \"%s\" -> \"%s\";\n" - ,@(mapcar #'xml-escape-string edge))))) - (insert (format " edge [%s];\n" - (mapconcat #'org-roam-graph--dot-option - org-roam-graph-edge-cites-extra-config ","))) - (dolist (edge edges-cites) - (insert (apply #'format `(" \"%s\" -> \"%s\";\n" - ,@(mapcar #'xml-escape-string edge))))) + (insert (format " edge [%s];\n" + (mapconcat (lambda (var) + (org-roam-graph--dot-option var nil "\"")) + org-roam-graph-edge-extra-config + ","))) + (pcase-dolist (`(,source ,dest ,type) edges) + (unless (member type org-roam-graph-link-hidden-types) + (pcase-dolist (`(,node ,node-type) `((,source "id") + (,dest ,type))) + (unless (member node seen-nodes) + (insert (org-roam-graph--format-node + (or (gethash node nodes-table) node) node-type)) + (push node seen-nodes))) + (insert (format " \"%s\" -> \"%s\";\n" + (xml-escape-string source) + (xml-escape-string dest))))) + (when all-nodes + (maphash (lambda (id node) + (unless (member id seen-nodes) + (insert (org-roam-graph--format-node node "id")))) + nodes-table)) (insert "}") (buffer-string)))) -(defun org-roam-graph--build (&optional node-query callback) - "Generate a graph showing the relations between nodes in NODE-QUERY. -Execute CALLBACK when process exits successfully. +(defun org-roam-graph--format-node (node type) + "Return a graphviz NODE with TYPE. +Handles both Org-roam nodes, and string nodes (e.g. urls)." + (let (node-id node-properties) + (if (org-roam-node-p node) + (let* ((title (org-roam-quote-string (org-roam-node-title node))) + (shortened-title (org-roam-quote-string + (pcase org-roam-graph-shorten-titles + (`truncate (org-roam-truncate org-roam-graph-max-title-length title)) + (`wrap (s-word-wrap org-roam-graph-max-title-length title)) + (_ title))))) + (setq node-id (org-roam-node-id node) + node-properties `(("label" . ,shortened-title) + ("URL" . ,(concat "org-protocol://roam-node?node=" + (url-hexify-string (org-roam-node-id node)))) + ("tooltip" . ,(xml-escape-string title))))) + (setq node-id node + node-properties (append `(("label" . ,(concat type ":" node))) + (when (member type (list "http" "https")) + `(("URL" . ,(xml-escape-string (concat type ":" node)))))))) + (format "\"%s\" [%s];\n" + node-id + (mapconcat (lambda (n) + (org-roam-graph--dot-option n nil "\"")) + (append (cdr (assoc type org-roam-graph-node-extra-config)) + node-properties) ",")))) + +(defun org-roam-graph--build (graph &optional callback) + "Generate the GRAPH, and execute CALLBACK when process exits successfully. CALLBACK is passed the graph file as its sole argument." (unless (stringp org-roam-graph-executable) (user-error "`org-roam-graph-executable' is not a string")) @@ -238,12 +230,7 @@ CALLBACK is passed the graph file as its sole argument." (user-error (concat "Cannot find executable \"%s\" to generate the graph. " "Please adjust `org-roam-graph-executable'") org-roam-graph-executable)) - (let* ((node-query (or node-query - `[:select [file title] :from titles - ,@(org-roam-graph--expand-matcher 'file t) - :group :by file])) - (graph (org-roam-graph--dot node-query)) - (temp-dot (make-temp-file "graph." nil ".dot" graph)) + (let* ((temp-dot (make-temp-file "graph." nil ".dot" graph)) (temp-graph (make-temp-file "graph." nil (concat "." org-roam-graph-filetype)))) (org-roam-message "building graph") (make-process @@ -268,47 +255,26 @@ CALLBACK is passed the graph file as its sole argument." ('nil (view-file file)) (_ (signal 'wrong-type-argument `((functionp stringp null) ,org-roam-graph-viewer))))) -(defun org-roam-graph--build-connected-component (file &optional max-distance callback) - "Build a graph of nodes connected to FILE. -If MAX-DISTANCE is non-nil, limit nodes to MAX-DISTANCE steps. -CALLBACK is passed to `org-roam-graph--build'." - (let* ((file (expand-file-name file)) - (files (or (if (and max-distance (>= max-distance 0)) - (org-roam-db--links-with-max-distance file max-distance) - (org-roam-db--connected-component file)) - (list file))) - (query `[:select [file title] - :from titles - :where (in file [,@files])])) - (org-roam-graph--build query callback))) - ;;;; Commands ;;;###autoload -(defun org-roam-graph (&optional arg file node-query) - "Build and possibly display a graph for FILE from NODE-QUERY. -If FILE is nil, default to current buffer's file name. +(defun org-roam-graph (&optional arg node) + "Build and possibly display a graph for NODE. ARG may be any of the following values: - nil show the graph. - - `\\[universal-argument]' show the graph for FILE. - - `\\[universal-argument]' N show the graph for FILE limiting nodes to N steps. - - `\\[universal-argument] \\[universal-argument]' build the graph. - - `\\[universal-argument]' - build the graph for FILE. - - `\\[universal-argument]' -N build the graph for FILE limiting nodes to N steps." - (interactive "P") - (unless org-roam-mode (org-roam-mode)) - (let ((file (or file (buffer-file-name (buffer-base-buffer))))) - (unless (or (not arg) (equal arg '(16))) - (unless file - (user-error "Cannot build graph for nil file. Is current buffer visiting a file?")) - (unless (org-roam--org-roam-file-p file) - (user-error "\"%s\" is not an org-roam file" file))) - (pcase arg - ('nil (org-roam-graph--build node-query #'org-roam-graph--open)) - ('(4) (org-roam-graph--build-connected-component file nil #'org-roam-graph--open)) - ((pred integerp) (org-roam-graph--build-connected-component file (abs arg) (when (>= arg 0) #'org-roam-graph--open))) - ('(16) (org-roam-graph--build node-query)) - ('- (org-roam-graph--build-connected-component file)) - (_ (user-error "Unrecognized ARG: %s" arg))))) + - `\\[universal-argument]' show the graph for NODE. + - `\\[universal-argument]' N show the graph for NODE limiting nodes to N steps." + (interactive + (list current-prefix-arg + (and current-prefix-arg + (org-roam-node-at-point 'assert)))) + (let ((graph (cl-typecase arg + (null (org-roam-graph--dot nil 'all-nodes)) + (cons (org-roam-graph--dot (org-roam-graph--connected-component + (org-roam-node-id node) 0))) + (integer (org-roam-graph--dot (org-roam-graph--connected-component + (org-roam-node-id node) (abs arg))))))) + (org-roam-graph--build graph #'org-roam-graph--open))) + (provide 'org-roam-graph) diff --git a/org-roam-link.el b/org-roam-link.el deleted file mode 100644 index b954341..0000000 --- a/org-roam-link.el +++ /dev/null @@ -1,324 +0,0 @@ -;;; org-roam-link.el --- Custom links for Org-roam -*- coding: utf-8; lexical-binding: t; -*- - -;; Copyright © 2020 Jethro Kuan -;; Alan Carroll - -;; Author: Jethro Kuan -;; URL: https://github.com/org-roam/org-roam -;; Keywords: org-mode, roam, convenience -;; Version: 1.2.3 -;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (s "1.12.0") (org "9.3") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2")) - -;; This file is NOT part of GNU Emacs. - -;; 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, 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 GNU Emacs; see the file COPYING. If not, write to the -;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, -;; Boston, MA 02110-1301, USA. - -;;; Commentary: -;; -;; This adds the custom `roam:' link to Org-roam. `roam:' links allow linking to -;; Org-roam files via their titles and headlines. -;; -;;; Code: -;;;; Dependencies - -(require 'ol) -(require 'org-roam-compat) -(require 'org-roam-macs) -(require 'org-roam-db) - -(require 'org-element) - -(defvar org-roam-completion-ignore-case) -(defvar org-roam-directory) -(declare-function org-roam--find-file "org-roam") -(declare-function org-roam-find-file "org-roam") -(declare-function org-roam-format-link "org-roam") - -(defcustom org-roam-link-auto-replace t - "When non-nil, replace Org-roam's roam links with file/id equivalents." - :group 'org-roam - :type 'boolean) - -(defcustom org-roam-link-file-path-type 'relative - "How the path name in file links should be stored. -Valid values are: - -relative Relative to the current directory, i.e. the directory of the file - into which the link is being inserted. -absolute Absolute path, if possible with ~ for home directory. -noabbrev Absolute path, no abbreviation of home directory." - :group 'org-roam - :type '(choice - (const relative) - (const absolute) - (const noabbrev)) - :safe #'symbolp) - -;;; the roam: link -(org-link-set-parameters "roam" - :follow #'org-roam-link-follow-link) - -(defun org-roam-link-follow-link (_path) - "Navigates to location in Org-roam link. -This function is called by Org when following links of the type -`roam'. While the path is passed, assume that the cursor is on -the link." - (pcase-let ((`(,link-type ,loc ,desc ,mkr) (org-roam-link--get-location))) - (when (and org-roam-link-auto-replace loc desc) - (org-roam-link--replace-link link-type loc desc)) - (pcase link-type - ("file" - (if loc - (org-roam--find-file loc) - (org-roam-find-file desc nil nil t))) - ("id" - (org-goto-marker-or-bmk mkr))))) - -;;; Retrieval Functions -(defun org-roam-link--get-titles () - "Return all titles within Org-roam." - (mapcar #'car (org-roam-db-query [:select [titles:title] :from titles]))) - -(defun org-roam-link--get-headlines (&optional file with-marker use-stack) - "Return all outline headings for the current buffer. -If FILE, return outline headings for passed FILE instead. -If WITH-MARKER, return a cons cell of (headline . marker). -If USE-STACK, include the parent paths as well." - (org-roam-with-file file (when with-marker 'keep) - (let* ((outline-level-fn outline-level) - (path-separator "/") - (stack-level 0) - stack cands name level marker) - (save-excursion - (goto-char (point-min)) - (while (re-search-forward org-complex-heading-regexp nil t) - (save-excursion - (setq name (substring-no-properties (or (match-string 4) ""))) - (setq marker (point-marker)) - (when use-stack - (goto-char (match-beginning 0)) - (setq level (funcall outline-level-fn)) - ;; Update stack. The empty entry guards against incorrect - ;; headline hierarchies, e.g. a level 3 headline - ;; immediately following a level 1 entry. - (while (<= level stack-level) - (pop stack) - (cl-decf stack-level)) - (while (> level stack-level) - (push name stack) - (cl-incf stack-level)) - (setq name (mapconcat #'identity - (reverse stack) - path-separator))) - (push (if with-marker - (cons name marker) - name) cands)))) - (nreverse cands)))) - - -(defun org-roam-link--get-file-from-title (title &optional no-interactive) - "Return the file path corresponding to TITLE. -When NO-INTERACTIVE, return nil if there are multiple options." - (let ((files (mapcar #'car (org-roam-db-query [:select [titles:file] :from titles - :where (= titles:title $v1)] - (vector title))))) - (pcase files - ('nil nil) - (`(,file) file) - (_ - (unless no-interactive - (completing-read "Select file: " files)))))) - -(defun org-roam-link--get-id-from-headline (headline &optional file) - "Return (marker . id) correspondng to HEADLINE in FILE. -If FILE is nil, get ID from current buffer. -If there is no corresponding headline, return nil." - (save-excursion - (org-roam-with-file file 'keep - (let ((headlines (org-roam-link--get-headlines file 'with-markers))) - (when-let ((marker (cdr (assoc-string headline headlines)))) - (goto-char marker) - (cons marker - (when org-roam-link-auto-replace - (org-id-get-create)))))))) - -;;; Path-related functions -(defun org-roam-link-get-path (path &optional type) - "Return the PATH of the link to use. -If TYPE is non-nil, create a link of TYPE. Otherwise, respect -`org-link-file-path-type'." - (pcase (or type org-roam-link-file-path-type) - ('absolute - (abbreviate-file-name (expand-file-name path))) - ('noabbrev - (expand-file-name path)) - ('relative - (file-relative-name path)))) - -(defun org-roam-link--split-path (path) - "Splits PATH into title and headline. -Return a list of the form (type title has-headline-p headline star-idx). -type is one of `title', `headline', `title+headline'. -title is the title component of the path. -headline is the headline component of the path. -star-idx is the index of the asterisk, if any." - (save-match-data - (let* ((star-index (string-match-p "\\*" path)) - (title (substring-no-properties path 0 star-index)) - (headline (if star-index - (substring-no-properties path (+ 1 star-index)) - "")) - (type (cond ((not star-index) - 'title) - ((= 0 star-index) - 'headline) - (t 'title+headline)))) - (list type title headline star-index)))) - -(defun org-roam-link--get-location () - "Return the location of the Org-roam fuzzy link at point. -The location is returned as a list containing (link-type loc desc marker). -nil is returned if there is no matching location. - -link-type is either \"file\" or \"id\". -loc is the target location: e.g. a file path, or an id. -marker is a marker to the headline, if applicable. - -desc is either the the description of the link under point, or -the target of LINK (title or heading content)." - (let ((context (org-element-context)) - mkr link-type desc loc) - (pcase (org-element-lineage context '(link) t) - (`nil (error "Not at an Org link")) - (link - (if (not (string-equal "roam" (org-element-property :type link))) - (error "Not at Org-roam link") - (setq desc (and (org-element-property :contents-begin link) - (org-element-property :contents-end link) - (buffer-substring-no-properties - (org-element-property :contents-begin link) - (org-element-property :contents-end link)))) - (pcase-let ((`(,type ,title ,headline _) (org-roam-link--split-path - (org-element-property :path link)))) - (pcase type - ('title+headline - (let ((file (org-roam-link--get-file-from-title title))) - (if (not file) - (org-roam-message "Cannot find matching file") - (setq mkr (org-roam-link--get-id-from-headline headline file)) - (pcase mkr - (`(,marker . ,target-id) - (progn - (setq mkr marker - loc target-id - desc (or desc headline) - link-type "id"))) - (_ (org-roam-message "Cannot find matching id")))))) - ('title - (setq loc (org-roam-link--get-file-from-title title) - link-type "file" - desc (or desc title))) - ('headline - (setq mkr (org-roam-link--get-id-from-headline headline)) - (pcase mkr - (`(,marker . ,target-id) - (setq mkr marker - loc target-id - link-type "id" - desc (or desc headline))) - (_ (org-roam-message "Cannot find matching headline"))))))))) - (list link-type loc desc mkr))) - -;;; Conversion Functions -(defun org-roam-link--replace-link (link-type loc &optional desc) - "Replace link at point with a vanilla Org link. -LINK-TYPE is the Org link type, typically \"file\" or \"id\". -LOC is path for the Org link. -DESC is the link description." - (save-excursion - (save-match-data - (unless (org-in-regexp org-link-bracket-re 1) - (user-error "No link at point")) - (replace-match "") - (insert (org-roam-format-link loc desc link-type))))) - -(defun org-roam-link-replace-all () - "Replace all roam links in the current buffer." - (interactive) - (save-excursion - (goto-char (point-min)) - (while (re-search-forward org-link-bracket-re nil t) - (condition-case nil - (pcase-let ((`(,link-type ,loc ,desc _) (org-roam-link--get-location))) - (when (and link-type loc) - (org-roam-link--replace-link link-type loc desc))) - (error nil))))) - -(defun org-roam-link--replace-link-on-save () - "Hook to replace all roam links on save." - (when org-roam-link-auto-replace - (org-roam-link-replace-all))) - -;;; Completion -(defun org-roam-link-complete-at-point () - "Do appropriate completion for the link at point." - (let ((end (point)) - (start (point)) - collection link-type headline-only-p) - (when (org-in-regexp org-link-bracket-re 1) - (setq start (match-beginning 1) - end (match-end 1)) - (let ((context (org-element-context))) - (pcase (org-element-lineage context '(link) t) - (`nil nil) - (link - (setq link-type (org-element-property :type link)) - (when (member link-type '("roam" "fuzzy")) - (when (string= link-type "roam") (setq start (+ start (length "roam:")))) - (pcase-let ((`(,type ,title _ ,star-idx) - (org-roam-link--split-path (org-element-property :path link)))) - (pcase type - ('title+headline - (when-let ((file (org-roam-link--get-file-from-title title t))) - (setq collection (apply-partially #'org-roam-link--get-headlines file)) - (setq start (+ start star-idx 1)))) - ('title - (setq collection #'org-roam-link--get-titles)) - ('headline - (setq collection #'org-roam-link--get-headlines) - (setq start (+ start star-idx 1)) - (setq headline-only-p t))))))))) - (when collection - (let ((prefix (buffer-substring-no-properties start end))) - (list start end - (if (functionp collection) - (completion-table-case-fold - (completion-table-dynamic - (lambda (_) - (cl-remove-if (apply-partially #'string= prefix) - (funcall collection)))) - (not org-roam-completion-ignore-case)) - collection) - :exit-function - (lambda (str &rest _) - (delete-char (- 0 (length str) - (if headline-only-p 1 0))) - (insert (concat (unless (string= link-type "roam") "roam:") - (when headline-only-p "*") - (org-link-escape str))))))))) - -(provide 'org-roam-link) -;;; org-roam-link.el ends here diff --git a/org-roam-macs.el b/org-roam-macs.el index f918f5c..22ffea4 100644 --- a/org-roam-macs.el +++ b/org-roam-macs.el @@ -5,8 +5,8 @@ ;; Author: Jethro Kuan ;; URL: https://github.com/org-roam/org-roam ;; Keywords: org-mode, roam, convenience -;; Version: 1.2.3 -;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (s "1.12.0") (org "9.3") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2")) +;; Version: 2.0.0 +;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite "1.0.0") (magit-section "2.90.1")) ;; This file is NOT part of GNU Emacs. @@ -27,30 +27,20 @@ ;;; Commentary: ;; -;; This library implements macros and utility functions used throughout -;; org-roam. -;; +;; This library implements macros used throughout org-roam. ;; ;;; Code: -;;;; Library Requires -(require 'dash) -(require 's) - -(defvar org-roam-verbose) - -;; This is necessary to ensure all dependents on this module see -;; `org-mode-hook' and `org-inhibit-startup' as dynamic variables, -;; regardless of whether Org is loaded before their compilation. -(require 'org) - -;;;; Utility Functions -(defun org-roam--list-interleave (lst separator) - "Interleaves elements in LST with SEPARATOR." - (when lst - (let ((new-lst (list (pop lst)))) - (dolist (it lst) - (nconc new-lst (list separator it))) - new-lst))) +(defmacro org-roam-plist-map! (fn plist) + "Map FN over PLIST, modifying it in-place." + (declare (indent 1)) + (let ((plist-var (make-symbol "plist")) + (k (make-symbol "k")) + (v (make-symbol "v"))) + `(let ((,plist-var (copy-sequence ,plist))) + (while ,plist-var + (setq ,k (pop ,plist-var)) + (setq ,v (pop ,plist-var)) + (setq ,plist (plist-put ,plist ,k (funcall ,fn ,k ,v))))))) (defmacro org-roam-with-file (file keep-buf-p &rest body) "Execute BODY within FILE. @@ -58,6 +48,7 @@ If FILE is nil, execute BODY in the current buffer. Kills the buffer if KEEP-BUF-P is nil, and FILE is not yet visited." (declare (indent 2) (debug t)) `(let* (new-buf + (auto-mode-alist nil) (buf (or (and (not ,file) (current-buffer)) ;If FILE is nil, use current buffer (find-buffer-visiting ,file) ; If FILE is already visited, find buffer @@ -66,6 +57,11 @@ Kills the buffer if KEEP-BUF-P is nil, and FILE is not yet visited." (find-file-noselect ,file)))) ; Else, visit FILE and return buffer res) (with-current-buffer buf + (unless (equal major-mode 'org-mode) + (delay-mode-hooks + (let ((org-inhibit-startup t) + (org-agenda-files nil)) + (org-mode)))) (setq res (progn ,@body)) (unless (and new-buf (not ,keep-buf-p)) (save-buffer))) @@ -74,57 +70,21 @@ Kills the buffer if KEEP-BUF-P is nil, and FILE is not yet visited." (kill-buffer (find-buffer-visiting ,file)))) res)) -(defmacro org-roam--with-temp-buffer (file &rest body) +(defmacro org-roam-with-temp-buffer (file &rest body) "Execute BODY within a temp buffer. Like `with-temp-buffer', but propagates `org-roam-directory'. -If FILE, set `org-roam-temp-file-name' to file and insert its contents." +If FILE, set `default-directory' to FILE's directory and insert its contents." (declare (indent 1) (debug t)) (let ((current-org-roam-directory (make-symbol "current-org-roam-directory"))) `(let ((,current-org-roam-directory org-roam-directory)) (with-temp-buffer - (let ((org-roam-directory ,current-org-roam-directory) - (org-mode-hook nil) - (org-inhibit-startup t)) - (org-mode) + (let ((org-roam-directory ,current-org-roam-directory)) + (delay-mode-hooks (org-mode)) (when ,file (insert-file-contents ,file) - (setq-local org-roam-file-name ,file) (setq-local default-directory (file-name-directory ,file))) ,@body))))) -(defun org-roam-message (format-string &rest args) - "Pass FORMAT-STRING and ARGS to `message' when `org-roam-verbose' is t." - (when org-roam-verbose - (apply #'message `(,(concat "(org-roam) " format-string) ,@args)))) - -(defun org-roam-string-quote (str) - "Quote STR." - (->> str - (s-replace "\\" "\\\\") - (s-replace "\"" "\\\""))) - -;;; Shielding regions -(defun org-roam-shield-region (beg end) - "Shield REGION against modifications. -REGION must be a cons-cell containing the marker to the region -beginning and maximum values." - (when (and beg end) - (add-text-properties beg end - '(font-lock-face org-roam-link-shielded - read-only t) - (marker-buffer beg)) - (cons beg end))) - -(defun org-roam-unshield-region (beg end) - "Unshield the shielded REGION." - (when (and beg end) - (let ((inhibit-read-only t)) - (remove-text-properties beg end - '(font-lock-face org-roam-link-shielded - read-only t) - (marker-buffer beg))) - (cons beg end))) - (provide 'org-roam-macs) ;;; org-roam-macs.el ends here diff --git a/org-roam-migrate.el b/org-roam-migrate.el new file mode 100644 index 0000000..dbf6565 --- /dev/null +++ b/org-roam-migrate.el @@ -0,0 +1,192 @@ +;;; org-roam-migrate.el --- Migration utilities from v1 to v2 -*- coding: utf-8; lexical-binding: t; -*- + +;; Copyright © 2020-2021 Jethro Kuan + +;; Author: Jethro Kuan +;; URL: https://github.com/org-roam/org-roam +;; Keywords: org-mode, roam, convenience +;; Version: 2.0.0 +;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite "1.0.0") (magit-section "2.90.1")) + +;; This file is NOT part of GNU Emacs. + +;; 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, 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 GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +;; Boston, MA 02110-1301, USA. + +;;; Commentary: +;; +;; To ease transition from v1 to v2, we provide various migration utilities. +;; This library helps convert v1 notes to v2, and informs the user. +;; +;;; Code: +;;;; Dependencies +;;;; +;;; v1 breaking warning +(require 'org-roam-db) + +(defvar org-roam-v2-ack nil) + +(unless org-roam-v2-ack + (lwarn 'org-roam :error " +------------------------------------ +WARNING: You're now on Org-roam v2! +------------------------------------ + +You may have arrived here from a package upgrade. Please read the +wiki entry at +https://github.com/org-roam/org-roam/wiki/Hitchhiker's-Rough-Guide-to-Org-roam-V2 +for an overview of the major changes. + +Notes taken in v1 are incompatible with v1, but you can upgrade +them to the v2 format via a simple command. To migrate your +notes, run: + + M-x org-roam-migrate-wizard + +If you wish to stay on v1, v1 is unfortunately not distributed on +MELPA. See org-roam/org-roam-v1 on GitHub on how to install v1. + +If you've gone through the migration steps (if necessary), and +know what you're doing set `org-roam-v2-ack' to `t' to disable +this warning. You can do so by adding: + +(setq org-roam-v2-ack t) + +To your init file. + +")) + +;;;###autoload +(defun org-roam-migrate-wizard () + "Migrate all notes from to be compatible with Org-roam v2. +1. Convert all notes from v1 format to v2. +2. Rebuild the cache. +3. Replace all file links with ID links." + (interactive) + (when (yes-or-no-p "Org-roam will now convert all your notes from v1 to v2. +This will take a while. Are you sure you want to do this?") + ;; Back up notes + (let ((backup-dir (expand-file-name "org-roam.bak" + (file-name-directory (directory-file-name org-roam-directory))))) + (message "Backing up files to %s" backup-dir) + (copy-directory org-roam-directory backup-dir)) + + ;; Convert v1 to v2 + (dolist (f (org-roam--list-all-files)) + (org-roam-with-file f nil + (org-roam-migrate-v1-to-v2))) + ;; Rebuild cache + (org-roam-db-sync 'force) + + ;;Replace all file links with ID links + (dolist (f (org-roam--list-all-files)) + (org-roam-with-file f nil + (org-roam-migrate-replace-file-links-with-id) + (save-buffer))))) + +(defun org-roam-migrate-v1-to-v2 () + "Convert the current buffer to v2 format." + ;; Create file level ID + (org-with-point-at 1 + (org-id-get-create)) + ;; Replace roam_key into properties drawer roam_ref + (when-let* ((refs (mapcan #'split-string-and-unquote + (cdar (org-collect-keywords '("roam_key")))))) + (let ((case-fold-search t)) + (org-with-point-at 1 + (dolist (ref refs) + (org-roam-ref-add ref)) + (while (re-search-forward "^#\\+roam_key:" (point-max) t) + (beginning-of-line) + (kill-line 1))))) + + ;; Replace roam_alias into properties drawer roam_aliases + (when-let* ((aliases (mapcan #'split-string-and-unquote + (cdar (org-collect-keywords '("roam_alias")))))) + (let ((case-fold-search t)) + (org-with-point-at 1 + (dolist (alias aliases) + (org-roam-alias-add alias)) + (while (re-search-forward "^#\\+roam_alias:" (point-max) t) + (beginning-of-line) + (kill-line 1))))) + + ;; Replace #+roam_tags into #+filetags + (org-with-point-at 1 + (let* ((roam-tags (org-roam-migrate-get-prop-list "ROAM_TAGS")) + (file-tags (org-roam-migrate-get-prop-list "FILETAGS")) + (tags (append roam-tags file-tags)) + (tags (seq-map (lambda (tag) + (replace-regexp-in-string + "[^[:alnum:]_@#%]" + "_" + tag)) tags)) + (tags (seq-uniq tags))) + (when tags + (org-roam-migrate-prop-set "filetags" (string-join tags " ")))) + (let ((case-fold-search t)) + (org-with-point-at 1 + (while (re-search-forward "^#\\+roam_tags:" (point-max) t) + (beginning-of-line) + (kill-line 1))))) + (save-buffer)) + +(defun org-roam-migrate-get-prop-list (keyword) + "Return prop list for KEYWORD." + (let ((re (format "^#\\+%s:[ \t]*\\([^\n]+\\)" (upcase keyword))) + lst) + (goto-char (point-min)) + (while (re-search-forward re 2048 t) + (setq lst (append lst (split-string-and-unquote + (buffer-substring-no-properties + (match-beginning 1) (match-end 1)))))) + lst)) + +(defun org-roam-migrate-prop-set (name value) + "Set a file property called NAME to VALUE in buffer file. +If the property is already set, replace its value." + (setq name (downcase name)) + (org-with-point-at 1 + (let ((case-fold-search t)) + (if (re-search-forward (concat "^#\\+" name ":\\(.*\\)") + (point-max) t) + (replace-match (concat "#+" name ": " value) 'fixedcase) + (while (and (not (eobp)) + (looking-at "^[#:]")) + (if (save-excursion (end-of-line) (eobp)) + (progn + (end-of-line) + (insert "\n")) + (forward-line) + (beginning-of-line))) + (insert "#+" name ": " value "\n"))))) + +(defun org-roam-migrate-replace-file-links-with-id () + "Replace all file: links with ID links in current buffer." + (org-with-point-at 1 + (while (re-search-forward org-link-bracket-re nil t) + (let* ((mdata (match-data)) + (path (match-string 1)) + (desc (match-string 2))) + (when (string-prefix-p "file:" path) + (setq path (expand-file-name (substring path 5))) + (when-let ((node-id (caar (org-roam-db-query [:select [id] :from nodes + :where (= file $s1) + :and (= level 0)] path)))) + (set-match-data mdata) + (replace-match (org-link-make-string (concat "id:" node-id) desc)))))))) + +(provide 'org-roam-migrate) +;;; org-roam-migrate.el ends here diff --git a/org-roam-mode.el b/org-roam-mode.el new file mode 100644 index 0000000..a6abcd7 --- /dev/null +++ b/org-roam-mode.el @@ -0,0 +1,468 @@ +;;; org-roam-mode.el --- create and refresh Org-roam buffers -*- lexical-binding: t -*- +;; Copyright © 2020 Jethro Kuan + +;; Author: Jethro Kuan +;; URL: https://github.com/org-roam/org-roam +;; Keywords: org-mode, roam, convenience +;; Version: 2.0.0 +;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite "1.0.0") (magit-section "2.90.1")) + +;; This file is NOT part of GNU Emacs. + +;; 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, 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 GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +;; Boston, MA 02110-1301, USA. + +;;; Commentary: +;; +;; This library implements the abstract major-mode `org-roam-mode', from which +;; almost all other Org-roam major-modes derive. +;; +;;; Code: +(require 'magit-section) + +(require 'org-roam-utils) + +(defvar org-roam-directory) +(defvar org-roam-find-file-hook) + +(declare-function org-roam-node-at-point "org-roam") + +;;; Faces +(defface org-roam-header-line + `((((class color) (background light)) + ,@(and (>= emacs-major-version 27) '(:extend t)) + :foreground "DarkGoldenrod4" + :weight bold) + (((class color) (background dark)) + ,@(and (>= emacs-major-version 27) '(:extend t)) + :foreground "LightGoldenrod2" + :weight bold)) + "Face for the `header-line' in some Org-roam modes." + :group 'org-roam-faces) + +(defface org-roam-title + '((t :weight bold)) + "Face for Org-roam titles." + :group 'org-roam-faces) + +(defface org-roam-olp + '((((class color) (background light)) :foreground "grey60") + (((class color) (background dark)) :foreground "grey40")) + "Face for the OLP of the node." + :group 'org-roam-faces) + +(defface org-roam-preview-heading + `((((class color) (background light)) + ,@(and (>= emacs-major-version 27) '(:extend t)) + :background "grey80" + :foreground "grey30") + (((class color) (background dark)) + ,@(and (>= emacs-major-version 27) '(:extend t)) + :background "grey25" + :foreground "grey70")) + "Face for preview headings." + :group 'org-roam-faces) + +(defface org-roam-preview-heading-highlight + `((((class color) (background light)) + ,@(and (>= emacs-major-version 27) '(:extend t)) + :background "grey75" + :foreground "grey30") + (((class color) (background dark)) + ,@(and (>= emacs-major-version 27) '(:extend t)) + :background "grey35" + :foreground "grey70")) + "Face for current preview headings." + :group 'org-roam-faces) + +(defface org-roam-preview-heading-selection + `((((class color) (background light)) + ,@(and (>= emacs-major-version 27) '(:extend t)) + :inherit org-roam-preview-heading-highlight + :foreground "salmon4") + (((class color) (background dark)) + ,@(and (>= emacs-major-version 27) '(:extend t)) + :inherit org-roam-preview-heading-highlight + :foreground "LightSalmon3")) + "Face for selected preview headings." + :group 'org-roam-faces) + +(defface org-roam-preview-region + `((t :inherit bold + ,@(and (>= emacs-major-version 27) + (list :extend (ignore-errors (face-attribute 'region :extend)))))) + "Face used by `org-roam-highlight-preview-region-using-face'. + +This face is overlaid over text that uses other hunk faces, +and those normally set the foreground and background colors. +The `:foreground' and especially the `:background' properties +should be avoided here. Setting the latter would cause the +loss of information. Good properties to set here are `:weight' +and `:slant'." + :group 'org-roam-faces) + +(defface org-roam-dim + '((((class color) (background light)) :foreground "grey60") + (((class color) (background dark)) :foreground "grey40")) + "Face for the dimmer part of the widgets." + :group 'org-roam-faces) + +;;; Variables +(defvar org-roam-current-node nil + "The current node at point.") + +(defvar org-roam-current-directory nil + "The `org-roam-directory' value for the current node.") + +(defcustom org-roam-mode-section-functions (list #'org-roam-backlinks-section + #'org-roam-reflinks-section) + "Functions which insert sections of the `org-roam-buffer'. +Each function is called with one argument, which is the current org-roam node at point." + :group 'org-roam + :type 'hook) + +;;; The mode +(defvar org-roam-mode-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map magit-section-mode-map) + (define-key map [C-return] 'org-roam-visit-thing) + (define-key map (kbd "C-m") 'org-roam-visit-thing) + (define-key map [remap revert-buffer] 'org-roam-buffer-render) + map) + "Parent keymap for all keymaps of modes derived from `org-roam-mode'.") + +(define-derived-mode org-roam-mode magit-section-mode "Org-roam" + "Major mode for Org-roam's buffer." + :group 'org-roam + (face-remap-add-relative 'header-line 'org-roam-header-line)) + +;;; Key functions +(defun org-roam-visit-thing () + "This is a placeholder command. +Where applicable, section-specific keymaps bind another command +which visits the thing at point." + (interactive) + (user-error "There is no thing at point that could be visited")) + +(defun org-roam-buffer-render () + "Render the current node at point." + (interactive) + (when (derived-mode-p 'org-roam-mode) + (let ((inhibit-read-only t)) + (erase-buffer) + (setq-local default-directory org-roam-current-directory) + (setq-local org-roam-directory org-roam-current-directory) + (org-roam-set-header-line-format (org-roam-node-title org-roam-current-node)) + (magit-insert-section (org-roam) + (magit-insert-heading) + (run-hook-with-args 'org-roam-mode-section-functions org-roam-current-node))))) + +(defun org-roam-buffer () + "Launch an Org-roam buffer for the current node at point." + (interactive) + (if-let ((node (org-roam-node-at-point)) + (source-org-roam-directory org-roam-directory)) + (progn + (let ((buffer (get-buffer-create + (concat "org-roam: " + (file-relative-name (buffer-file-name) org-roam-directory))))) + (with-current-buffer buffer + (org-roam-mode) + (setq-local org-roam-current-node node) + (setq-local org-roam-current-directory source-org-roam-directory) + (org-roam-buffer-render)) + (switch-to-buffer-other-window buffer))) + (user-error "No node at point"))) + +;;; Persistent buffer +(defvar org-roam-buffer "*org-roam*" + "The persistent Org-roam buffer name.") + +(defun org-roam-buffer--post-command-h () + "Reconstructs the Org-roam buffer. +This needs to be quick or infrequent, because this is run at +`post-command-hook'. If REDISPLAY, force an update of +the Org-roam buffer." + (when (get-buffer-window org-roam-buffer) + (when-let ((node (org-roam-node-at-point))) + (unless (equal node org-roam-current-node) + (setq org-roam-current-node node) + (setq org-roam-current-directory org-roam-directory) + (org-roam-buffer-persistent-redisplay))))) + +(define-inline org-roam-buffer--visibility () + "Return whether the current visibility state of the org-roam buffer. +Valid states are 'visible, 'exists and 'none." + (declare (side-effect-free t)) + (inline-quote + (cond + ((get-buffer-window org-roam-buffer) 'visible) + ((get-buffer org-roam-buffer) 'exists) + (t 'none)))) + +(defun org-roam-buffer-toggle () + "Toggle display of the Org-roam buffer." + (interactive) + (pcase (org-roam-buffer--visibility) + ('visible + (progn + (delete-window (get-buffer-window org-roam-buffer)) + (remove-hook 'post-command-hook #'org-roam-buffer--post-command-h))) + ((or 'exists 'none) + (progn + (setq org-roam-current-node (org-roam-node-at-point) + org-roam-current-directory org-roam-directory) + (display-buffer (get-buffer-create org-roam-buffer)) + (org-roam-buffer-persistent-redisplay))))) + +(defun org-roam-buffer-persistent-redisplay () + "Recompute contents of the persistent Org-roam buffer. +Has no effect when `org-roam-current-node' is nil." + (when org-roam-current-node + (with-current-buffer (get-buffer-create org-roam-buffer) + (let ((inhibit-read-only t)) + (erase-buffer) + (org-roam-mode) + (setq-local default-directory org-roam-current-directory) + (setq-local org-roam-directory org-roam-current-directory) + (org-roam-set-header-line-format (org-roam-node-title org-roam-current-node)) + (magit-insert-section (org-roam) + (magit-insert-heading) + (dolist (fn org-roam-mode-section-functions) + (funcall fn org-roam-current-node))))))) + +(defun org-roam-buffer--redisplay () + "." + (add-hook 'post-command-hook #'org-roam-buffer--post-command-h nil t)) + +(add-hook 'org-roam-find-file-hook #'org-roam-buffer--redisplay) + +;;; Sections +;;;; Backlinks +(cl-defstruct (org-roam-backlink (:constructor org-roam-backlink-create) + (:copier nil)) + source-node target-node + point properties) + +(cl-defmethod org-roam-populate ((backlink org-roam-backlink)) + "Populate BACKLINK from database." + (setf (org-roam-backlink-source-node backlink) + (org-roam-populate (org-roam-backlink-source-node backlink)) + (org-roam-backlink-target-node backlink) + (org-roam-populate (org-roam-backlink-target-node backlink))) + backlink) + +(defun org-roam-backlinks-get (node) + "Return the backlinks for NODE." + (let ((backlinks (org-roam-db-query + [:select [source dest pos properties] + :from links + :where (= dest $s1) + :and (= type "id")] + (org-roam-node-id node)))) + (cl-loop for backlink in backlinks + collect (pcase-let ((`(,source-id ,dest-id ,pos ,properties) backlink)) + (org-roam-populate + (org-roam-backlink-create + :source-node (org-roam-node-create :id source-id) + :target-node (org-roam-node-create :id dest-id) + :point pos + :properties properties)))))) + +(defun org-roam-backlinks-sort (a b) + "Default sorting function for backlinks A and B. +Sorts by title." + (string< (org-roam-node-title (org-roam-backlink-source-node a)) + (org-roam-node-title (org-roam-backlink-source-node b)))) + +(defun org-roam-backlinks-section (node) + "The backlinks section for NODE." + (when-let ((backlinks (seq-sort #'org-roam-backlinks-sort (org-roam-backlinks-get node)))) + (magit-insert-section (org-roam-backlinks) + (magit-insert-heading "Backlinks:") + (dolist (backlink backlinks) + (org-roam-node-insert-section + :source-node (org-roam-backlink-source-node backlink) + :point (org-roam-backlink-point backlink) + :properties (org-roam-backlink-properties backlink))) + (insert ?\n)))) + +;;;; Reflinks +(cl-defstruct (org-roam-reflink (:constructor org-roam-reflink-create) + (:copier nil)) + source-node ref + point properties) + +(cl-defmethod org-roam-populate ((reflink org-roam-reflink)) + "Populate REFLINK from database." + (setf (org-roam-reflink-source-node reflink) + (org-roam-populate (org-roam-reflink-source-node reflink))) + reflink) + +(defun org-roam-reflinks-get (node) + "Return the reflinks for NODE." + (let ((refs (org-roam-db-query [:select [ref] :from refs + :where (= node-id $s1)] + (org-roam-node-id node))) + links) + (pcase-dolist (`(,ref) refs) + (pcase-dolist (`(,source-id ,pos ,properties) (org-roam-db-query + [:select [source pos properties] + :from links + :where (= dest $s1)] + ref)) + (push (org-roam-populate + (org-roam-reflink-create + :source-node (org-roam-node-create :id source-id) + :ref ref + :point pos + :properties properties)) links))) + links)) + +(defun org-roam-reflinks-sort (a b) + "Default sorting function for reflinks A and B. +Sorts by title." + (string< (org-roam-node-title (org-roam-reflink-source-node a)) + (org-roam-node-title (org-roam-reflink-source-node b)))) + +(defun org-roam-reflinks-section (node) + "The reflinks section for NODE." + (when (org-roam-node-refs node) + (let* ((reflinks (seq-sort #'org-roam-reflinks-sort (org-roam-reflinks-get node)))) + (magit-insert-section (org-roam-reflinks) + (magit-insert-heading "Reflinks:") + (dolist (reflink reflinks) + (org-roam-node-insert-section + :source-node (org-roam-reflink-source-node reflink) + :point (org-roam-reflink-point reflink) + :properties (org-roam-reflink-properties reflink))) + (insert ?\n))))) + +;;;; Unlinked references +(defvar org-roam-grep-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map org-roam-mode-map) + (define-key map [remap org-roam-visit-thing] 'org-roam-file-visit) + map) + "Keymap for Org-roam grep result sections.") + +(defclass org-roam-grep-section (magit-section) + ((keymap :initform 'org-roam-grep-map) + (file :initform nil) + (row :initform nil) + (col :initform nil))) + +(defun org-roam-file-at-point (&optional assert) + "Return the file at point. +If ASSERT, throw an error." + (if-let ((file (magit-section-case + (org-roam-node-section (org-roam-node-file (oref it node))) + (org-roam-grep-section (oref it file)) + (org-roam-preview-section (oref it file))))) + file + (when assert + (user-error "No file at point")))) + +(defun org-roam-file-visit (file &optional other-window row col) + "Visits FILE. +With a prefix argument OTHER-WINDOW, display the buffer in +another window instead. +If ROW, move to the row, and if COL move to the COL." + (interactive (list (org-roam-file-at-point t) + current-prefix-arg + (oref (magit-current-section) row) + (oref (magit-current-section) col))) + (let ((buf (find-file-noselect file))) + (with-current-buffer buf + (widen) + (goto-char (point-min)) + (when row + (forward-line (1- row))) + (when col + (forward-char (1- col)))) + (funcall (if other-window + #'switch-to-buffer-other-window + #'pop-to-buffer-same-window) buf))) + +(defvar org-roam-unlinked-references-result-re + (rx (group (one-or-more anything)) + ":" + (group (one-or-more digit)) + ":" + (group (one-or-more digit)) + ":" + (group (zero-or-more anything))) + "Regex for the return result of a ripgrep query.") + +(defun org-roam-unlinked-references-preview-line (file row) + "Return the preview line from FILE. +This is the ROW within FILE." + (with-temp-buffer + (insert-file-contents-literally file) + (forward-line (1- row)) + (buffer-substring-no-properties + (save-excursion + (beginning-of-line) + (point)) + (save-excursion + (end-of-line) + (point))))) + +(defun org-roam-unlinked-references-section (node) + "The unlinked references section for NODE. +References from FILE are excluded." + (when (and (executable-find "rg") + (not (string-match "PCRE2 is not available" + (shell-command-to-string "rg --pcre2-version")))) + (let* ((titles (cons (org-roam-node-title node) + (org-roam-node-aliases node))) + (rg-command (concat "rg -o --vimgrep -P -i " + (mapconcat (lambda (glob) (concat "-g " glob)) + (org-roam--list-files-search-globs org-roam-file-extensions) + " ") + (format " '\\[([^[]]++|(?R))*\\]%s' " + (mapconcat (lambda (title) + (format "|(\\b%s\\b)" (shell-quote-argument title))) + titles "")) + org-roam-directory)) + (results (split-string (shell-command-to-string rg-command) "\n")) + f row col match) + (magit-insert-section (unlinked-references) + (magit-insert-heading "Unlinked References:") + (dolist (line results) + (save-match-data + (when (string-match org-roam-unlinked-references-result-re line) + (setq f (match-string 1 line) + row (string-to-number (match-string 2 line)) + col (string-to-number (match-string 3 line)) + match (match-string 4 line)) + (when (and match + (not (f-equal-p (org-roam-node-file node) f)) + (member (downcase match) (mapcar #'downcase titles))) + (magit-insert-section section (org-roam-grep-section) + (oset section file f) + (oset section row row) + (oset section col col) + (insert (propertize (format "%s:%s:%s" + (truncate-string-to-width (file-name-base f) 15 nil nil "...") + row col) 'font-lock-face 'org-roam-dim) + " " + (org-roam-fontify-like-in-org-mode + (org-roam-unlinked-references-preview-line f row)) + "\n")))))) + (insert ?\n))))) + +(provide 'org-roam-mode) +;;; org-roam-mode.el ends here diff --git a/org-roam-overlay.el b/org-roam-overlay.el new file mode 100644 index 0000000..f7dc9d8 --- /dev/null +++ b/org-roam-overlay.el @@ -0,0 +1,98 @@ +;;; org-roam-overlay.el --- Link overlay for Org-roam nodes -*- coding: utf-8; lexical-binding: t; -*- + +;; Copyright © 2020-2021 Jethro Kuan + +;; Author: Jethro Kuan +;; URL: https://github.com/org-roam/org-roam +;; Keywords: org-mode, roam, convenience +;; Version: 2.0.0 +;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite "1.0.0") (magit-section "2.90.1")) + +;; This file is NOT part of GNU Emacs. + +;; 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, 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 GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +;; Boston, MA 02110-1301, USA. + +;;; Commentary: +;; +;; This library is an attempt at injecting Roam functionality into Org-mode. +;; This is achieved primarily through building caches for forward links, +;; backward links, and file titles. +;; +;; +;;; Code: +;;;; Dependencies + +(defface org-roam-overlay + '((((class color) (background light)) + :background "grey90" :box (:line-width -1 :color "black")) + (((class color) (background dark)) + :background "grey10" :box (:line-width -1 :color "white"))) + "Face for the Org-roam overlay." + :group 'org-roam-faces) + +(defun org-roam-overlay--make (l r &rest props) + "Make an overlay from L to R with PROPS." + (let ((o (make-overlay l (or r l)))) + (overlay-put o 'category 'org-roam) + (while props (overlay-put o (pop props) (pop props))) + o)) + +(defun org-roam-overlay-make-link-overlay (link) + "Create overlay for LINK." + (save-excursion + (save-match-data + (let* ((type (org-element-property :type link)) + (id (org-element-property :path link)) + (pos (org-element-property :end link)) + (desc-p (org-element-property :contents-begin link)) + node) + (when (and (string-equal type "id") + (setq node (org-roam-node-from-id id)) + (not desc-p)) + (org-roam-overlay--make + pos pos + 'after-string (format "%s " + (propertize (org-roam-node-title node) + 'face 'org-roam-overlay)))))))) + +(defun org-roam-overlay-enable () + "Enable Org-roam overlays." + (org-roam-db-map-links + (list #'org-roam-overlay-make-link-overlay))) + +(defun org-roam-overlay-disable () + "Disable Org-roam overlays." + (remove-overlays nil nil 'category 'org-roam)) + +(defun org-roam-overlay-redisplay () + "Redisplay Org-roam overlays." + (org-roam-overlay-disable) + (org-roam-overlay-enable)) + +(define-minor-mode org-roam-overlay-mode + "Overlays for Org-roam ID links. +Org-roam overlay mode is a minor mode. When enabled, +overlay displaying the node's title is displayed." + :lighter " org-roam-overlay" + (if org-roam-overlay-mode + (progn + (org-roam-overlay-enable) + (add-hook 'after-save-hook #'org-roam-overlay-redisplay nil t)) + (org-roam-overlay-disable) + (remove-hook 'after-save-hook #'org-roam-overlay-redisplay t))) + +(provide 'org-roam-overlay) +;;; org-roam-overlay.el ends here diff --git a/org-roam-protocol.el b/org-roam-protocol.el index c058b28..a113e7f 100644 --- a/org-roam-protocol.el +++ b/org-roam-protocol.el @@ -4,8 +4,8 @@ ;; Author: Jethro Kuan ;; URL: https://github.com/org-roam/org-roam ;; Keywords: org-mode, roam, convenience -;; Version: 1.2.3 -;; Package-Requires: ((emacs "26.1") (org "9.3")) +;; Version: 2.0.0 +;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite "1.0.0") (magit-section "2.90.1")) ;; This file is NOT part of GNU Emacs. @@ -31,12 +31,14 @@ ;; ;; We define 2 protocols: ;; -;; 1. "roam-file": This protocol simply opens the file given by the FILE key +;; 1. "roam-node": This protocol simply opens the node given by the node ID ;; 2. "roam-ref": This protocol creates or opens a note with the given REF ;; ;;; Code: (require 'org-protocol) (require 'org-roam) +(eval-when-compile + (require 'org-roam-macs)) (require 'ol) ;; for org-link-decode (defcustom org-roam-protocol-store-links nil @@ -54,40 +56,34 @@ It opens or creates a note with the given ref. encodeURIComponent(location.href) + \\='&title=\\=' + \\ encodeURIComponent(document.title) + \\='&body=\\=' + \\ encodeURIComponent(window.getSelection())" - (when-let* ((alist (org-roam--plist-to-alist info)) - (decoded-alist (mapcar (lambda (k.v) - (let ((key (car k.v)) - (val (cdr k.v))) - (cons key (org-link-decode val)))) alist))) - (unless (assoc 'ref decoded-alist) - (error "No ref key provided")) - (when-let ((title (cdr (assoc 'title decoded-alist)))) - (push (cons 'slug (funcall org-roam-title-to-slug-function title)) decoded-alist)) - (let-alist decoded-alist - (let* ((ref (org-protocol-sanitize-uri .ref)) - (type (and (string-match org-link-plain-re ref) - (match-string 1 ref))) - (title (or .title "")) - (body (or .body "")) - (orglink - (org-link-make-string ref (or (org-string-nw-p title) ref)))) - (when org-roam-protocol-store-links - (push (list ref title) org-stored-links)) - (org-link-store-props :type type - :link ref - :annotation orglink - :initial body))) - (let* ((org-roam-capture-templates org-roam-capture-ref-templates) - (org-roam-capture--context 'ref) - (org-roam-capture--info decoded-alist) - (org-capture-link-is-already-stored t) - (template (cdr (assoc 'template decoded-alist)))) - (raise-frame) - (org-roam-capture--capture nil template) - (org-roam-message "Item captured."))) + (unless (plist-get info :ref) + (user-error "No ref key provided")) + (org-roam-plist-map! (lambda (k v) + (org-link-decode + (if (equal k :ref) + (org-protocol-sanitize-uri v) + v))) info) + (when org-roam-protocol-store-links + (push (list (plist-get info :ref) + (plist-get info :title)) org-stored-links)) + (org-link-store-props :type (and (string-match org-link-plain-re + (plist-get info :ref)) + (match-string 1 (plist-get info :ref))) + :link (plist-get info :ref) + :annotation (org-link-make-string (plist-get info :ref) + (or (plist-get info :title) + (plist-get info :ref))) + :initial (or (plist-get info :body) "")) + (raise-frame) + (org-roam-capture- + :keys (plist-get info :template) + :node (org-roam-node-create :title (plist-get info :title)) + :info (list :ref (plist-get info :ref) + :body (plist-get info :body)) + :templates org-roam-capture-ref-templates) nil) -(defun org-roam-protocol-open-file (info) +(defun org-roam-protocol-open-node (info) "This handler simply opens the file with emacsclient. INFO is an alist containing additional information passed by the protocol URL. @@ -95,15 +91,15 @@ It should contain the FILE key, pointing to the path of the file to open. Example protocol string: -org-protocol://roam-file?file=/path/to/file.org" - (when-let ((file (plist-get info :file))) +org-protocol://roam-node?node=uuid" + (when-let ((node (plist-get info :node))) (raise-frame) - (org-roam--find-file file)) + (org-roam-node-visit (org-roam-populate (org-roam-node-create :id node)))) nil) (push '("org-roam-ref" :protocol "roam-ref" :function org-roam-protocol-open-ref) org-protocol-protocol-alist) -(push '("org-roam-file" :protocol "roam-file" :function org-roam-protocol-open-file) +(push '("org-roam-node" :protocol "roam-node" :function org-roam-protocol-open-node) org-protocol-protocol-alist) (provide 'org-roam-protocol) diff --git a/org-roam-utils.el b/org-roam-utils.el new file mode 100644 index 0000000..837d3d9 --- /dev/null +++ b/org-roam-utils.el @@ -0,0 +1,296 @@ +;;; org-roam-utils.el --- Utilities for Org-roam -*- coding: utf-8; lexical-binding: t; -*- + +;; Copyright © 2020 Jethro Kuan + +;; Author: Jethro Kuan +;; URL: https://github.com/org-roam/org-roam +;; Keywords: org-mode, roam, convenience +;; Version: 2.0.0 +;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite "1.0.0") (magit-section "2.90.1")) + +;; This file is NOT part of GNU Emacs. + +;; 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, 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 GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +;; Boston, MA 02110-1301, USA. + +;;; Commentary: +;; +;; This library implements utility functions used throughout +;; Org-roam. +;; +;; +;;; Code: +;;;; Library Requires +(require 'dash) + +(eval-when-compile + (require 'org-roam-macs) + (require 'org-macs)) + +(defvar org-roam-verbose) + +;; This is necessary to ensure all dependents on this module see +;; `org-mode-hook' and `org-inhibit-startup' as dynamic variables, +;; regardless of whether Org is loaded before their compilation. +(require 'org) + +;;;; String Utilities +(defun org-roam-truncate (len s &optional ellipsis) + "If S is longer than LEN, cut it down and add ELLIPSIS to the end. + +The resulting string, including ellipsis, will be LEN characters +long. + +When not specified, ELLIPSIS defaults to ‘...’." + (declare (pure t) (side-effect-free t)) + (unless ellipsis + (setq ellipsis "...")) + (if (> (length s) len) + (format "%s%s" (substring s 0 (- len (length ellipsis))) ellipsis) + s)) + +(defun org-roam-replace (old new s) + "Replace OLD with NEW in S." + (declare (pure t) (side-effect-free t)) + (replace-regexp-in-string (regexp-quote old) new s t t)) + +(defun org-roam-quote-string (s) + "Quotes string S." + (->> s + (org-roam-replace "\\" "\\\\") + (org-roam-replace "\"" "\\\""))) + +;;;; Utility Functions +(defun org-roam--list-interleave (lst separator) + "Interleaves elements in LST with SEPARATOR." + (when lst + (let ((new-lst (list (pop lst)))) + (dolist (it lst) + (nconc new-lst (list separator it))) + new-lst))) + +(defun org-roam-up-heading-or-point-min () + "Fixed version of Org's `org-up-heading-or-point-min'." + (ignore-errors (org-back-to-heading t)) + (let ((p (point))) + (if (< 1 (funcall outline-level)) + (progn + (org-up-heading-safe) + (when (= (point) p) + (goto-char (point-min)))) + (unless (bobp) (goto-char (point-min)))))) + +(defun org-roam-message (format-string &rest args) + "Pass FORMAT-STRING and ARGS to `message' when `org-roam-verbose' is t." + (when org-roam-verbose + (apply #'message `(,(concat "(org-roam) " format-string) ,@args)))) + +(defvar org-ref-buffer-hacked) + +(defun org-roam-fontify-like-in-org-mode (s) + "Fontify string S like in Org mode. +Like `org-fontify-like-in-org-mode', but supports `org-ref'." + ;; NOTE: pretend that the temporary buffer created by `org-fontify-like-in-org-mode' to + ;; fontify a `cite:' reference has been hacked by org-ref, whatever that means; + ;; + ;; `org-ref-cite-link-face-fn', which is used to supply a face for `cite:' links, calls + ;; `hack-dir-local-variables' rationalizing that `bibtex-completion' would throw some warnings + ;; otherwise. This doesn't seem to be the case and calling this function just before + ;; `org-font-lock-ensure' (alias of `font-lock-ensure') actually instead of fixing the alleged + ;; warnings messes the things so badly that `font-lock-ensure' crashes with error and doesn't let + ;; org-roam to proceed further. I don't know what's happening there exactly but disabling this hackery + ;; fixes the crashing. Fortunately, org-ref provides the `org-ref-buffer-hacked' switch, which we use + ;; here to make it believe that the buffer was hacked. + ;; + ;; This is a workaround for `cite:' links and does not have any effect on other ref types. + ;; + ;; `org-ref-buffer-hacked' is a buffer-local variable, therefore we inline + ;; `org-fontify-like-in-org-mode' here + (with-temp-buffer + (insert s) + (let ((org-ref-buffer-hacked t)) + (org-mode) + (org-font-lock-ensure) + (buffer-string)))) + +(defun org-roam-set-header-line-format (string) + "Set the header-line using STRING. +If the `face' property of any part of STRING is already set, then +that takes precedence. Also pad the left side of STRING so that +it aligns with the text area." + (setq-local header-line-format + (concat (propertize " " 'display '(space :align-to 0)) + string))) + +;;; Keywords +(defun org-roam--get-keyword (name &optional bound) + "Return keyword property NAME in current buffer. +If BOUND, scan up to BOUND bytes of the buffer." + (save-excursion + (let ((re (format "^#\\+%s:[ \t]*\\([^\n]+\\)" (upcase name)))) + (goto-char (point-min)) + (when (re-search-forward re bound t) + (buffer-substring-no-properties (match-beginning 1) (match-end 1)))))) + +(defun org-roam-get-keyword (name &optional file bound) + "Return keyword property NAME from an org FILE. +FILE defaults to current file. +Only scans up to BOUND bytes of the document." + (unless bound + (setq bound 1024)) + (if file + (with-temp-buffer + (insert-file-contents-literally file nil 0 bound) + (org-roam--get-keyword name)) + (org-roam--get-keyword name bound))) + +;;; Shielding regions +(defface org-roam-shielded + '((t :inherit (warning))) + "Face for regions that are shielded (marked as read-only). +This face is used on the region target by org-roam-insertion +during an `org-roam-capture'." + :group 'org-roam-faces) + +(defun org-roam-shield-region (beg end) + "Shield region against modifications. +BEG and END are markers for the beginning and end regions. +REGION must be a cons-cell containing the marker to the region +beginning and maximum values." + (add-text-properties beg end + '(font-lock-face org-roam-shielded + read-only t) + (marker-buffer beg))) + +(defun org-roam-unshield-region (beg end) + "Unshield the shielded REGION. +BEG and END are markers for the beginning and end regions." + (let ((inhibit-read-only t)) + (remove-text-properties beg end + '(font-lock-face org-roam-shielded + read-only t) + (marker-buffer beg)))) + +;;; Formatting +(defun org-roam-format (template replacer) + "Format TEMPLATE with the function REPLACER. +REPLACER takes an argument of the format variable and optionally +an extra argument which is the EXTRA value from the call to +`org-roam-format'. +Adapted from `s-format'." + (let ((saved-match-data (match-data))) + (unwind-protect + (replace-regexp-in-string + "\\${\\([^}]+\\)}" + (lambda (md) + (let ((var (match-string 1 md)) + (replacer-match-data (match-data))) + (unwind-protect + (let ((v (progn + (set-match-data saved-match-data) + (funcall replacer var)))) + (if v (format "%s" v) (signal 'org-roam-format-resolve md))) + (set-match-data replacer-match-data)))) template + ;; Need literal to make sure it works + t t) + (set-match-data saved-match-data)))) + +(defvar org-roam--cached-display-format nil) + +(defun org-roam--process-display-format (format) + "Pre-calculate minimal widths needed by the FORMAT string." + (or org-roam--cached-display-format + (setq org-roam--cached-display-format + (let* ((fields-width 0) + (string-width + (string-width + (org-roam-format + format + (lambda (field) + (setq fields-width + (+ fields-width + (string-to-number + (or (cadr (split-string field ":")) + ""))))))))) + (cons format (+ fields-width string-width)))))) + +;;; for org-roam-demote-entire-buffer in org-roam-refile.el +(defun org-roam--file-keyword-get (keyword) + "Pull a KEYWORD setting from the top of the file. + +Keyword must be specified in ALL CAPS." + (cadr (assoc keyword + (org-collect-keywords (list keyword))))) + +(defun org-roam--file-keyword-kill (keyword) + "Erase KEYWORD setting line from the top of the file." + (let ((case-fold-search t)) + (org-with-point-at 1 + (when (re-search-forward (concat "^#\\+" keyword ":") nil t) + (beginning-of-line) + (delete-region (point) (line-end-position)) + (delete-char 1))))) + +(defun org-roam--kill-empty-buffer () + "If the source buffer has been emptied, kill it. + +If the buffer is associated with a file, delete the file. + +If the buffer is associated with an in-process capture operation, abort the operation." + (when (eq (buffer-size) 0) + (if (buffer-file-name) + (delete-file (buffer-file-name))) + (set-buffer-modified-p nil) + (when (and org-capture-mode + (buffer-base-buffer (current-buffer))) + (org-capture-kill)) + (kill-buffer (current-buffer)))) + +;;; Diagnostics +;;;###autoload +(defun org-roam-version (&optional message) + "Return `org-roam' version. +Interactively, or when MESSAGE is non-nil, show in the echo area." + (interactive) + (let* ((version + (with-temp-buffer + (insert-file-contents-literally (locate-library "org-roam.el")) + (goto-char (point-min)) + (save-match-data + (if (re-search-forward "\\(?:;; Version: \\([^z-a]*?$\\)\\)" nil nil) + (substring-no-properties (match-string 1)) + "N/A"))))) + (if (or message (called-interactively-p 'interactive)) + (message "%s" version) + version))) + +;;;###autoload +(defun org-roam-diagnostics () + "Collect and print info for `org-roam' issues." + (interactive) + (with-current-buffer (switch-to-buffer-other-window (get-buffer-create "*org-roam diagnostics*")) + (erase-buffer) + (insert (propertize "Copy info below this line into issue:\n" 'face '(:weight bold))) + (insert (format "- Emacs: %s\n" (emacs-version))) + (insert (format "- Framework: %s\n" + (condition-case _ + (completing-read "I'm using the following Emacs framework:" + '("Doom" "Spacemacs" "N/A" "I don't know")) + (quit "N/A")))) + (insert (format "- Org: %s\n" (org-version nil 'full))) + (insert (format "- Org-roam: %s" (org-roam-version))))) + +(provide 'org-roam-utils) +;;; org-roam-utils.el ends here diff --git a/org-roam.el b/org-roam.el index a815c7b..c7afc83 100644 --- a/org-roam.el +++ b/org-roam.el @@ -1,12 +1,12 @@ ;;; org-roam.el --- Roam Research replica with Org-mode -*- coding: utf-8; lexical-binding: t; -*- -;; Copyright © 2020 Jethro Kuan +;; Copyright © 2020-2021 Jethro Kuan ;; Author: Jethro Kuan ;; URL: https://github.com/org-roam/org-roam ;; Keywords: org-mode, roam, convenience -;; Version: 1.2.3 -;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (s "1.12.0") (org "9.3") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2")) +;; Version: 2.0.0 +;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite "1.0.0") (magit-section "2.90.1")) ;; This file is NOT part of GNU Emacs. @@ -43,37 +43,30 @@ (require 'dash) (require 'f) (require 'rx) -(require 's) (require 'seq) +(require 'magit-section) (eval-when-compile (require 'subr-x)) ;;;; Features +(require 'org-roam-migrate) (require 'org-roam-compat) -(require 'org-roam-macs) -;; These features should be able to be loaded order independently. -;; @TODO: implement something akin to `org-modules' that allows -;; selectively loading different sets of features. -;; ~NV [2020-05-22 Fri] - -(require 'org-roam-faces) -(require 'org-roam-buffer) +(eval-when-compile + (require 'org-roam-macs) + (require 'org-macs)) +(require 'org-roam-utils) +(require 'org-roam-mode) (require 'org-roam-completion) (require 'org-roam-capture) (require 'org-roam-dailies) (require 'org-roam-db) -(require 'org-roam-doctor) -(require 'org-roam-graph) -(require 'org-roam-link) -;;;; Declarations +;;; Declarations ;; From org-ref-core.el (defvar org-ref-cite-types) (declare-function org-ref-split-and-strip-string "ext:org-ref-utils" (string)) ;; From org-id.el -(defvar org-id-link-to-org-use-id) (declare-function org-id-find-id-in-file "ext:org-id" (id file &optional markerp)) -;;;; Customizable variables (defgroup org-roam nil "Roam Research replica in Org-mode." :group 'org @@ -81,17 +74,23 @@ :link '(url-link :tag "Github" "https://github.com/org-roam/org-roam") :link '(url-link :tag "Online Manual" "https://www.orgroam.com/manual.html")) +(defgroup org-roam-faces nil + "Faces used by Org-roam." + :group 'org-roam + :group 'faces) + +;;;; Variables +(defcustom org-roam-verbose t + "Echo messages that are not errors." + :type 'boolean + :group 'org-roam) + (defcustom org-roam-directory (expand-file-name "~/org-roam/") "Default path to Org-roam files. All Org files, at any level of nesting, are considered part of the Org-roam." :type 'directory :group 'org-roam) -(defcustom org-roam-encrypt-files nil - "Whether to encrypt new files. If true, create files with .gpg extension." - :type 'boolean - :group 'org-roam) - (defcustom org-roam-file-extensions '("org") "Detected file extensions to include in the Org-roam ecosystem. The first item in the list is used as the default file extension. @@ -108,256 +107,51 @@ ensure that." (const :tag "Include everything" nil)) :group 'org-roam) -(defcustom org-roam-find-file-function nil - "Function called when visiting files in Org-roam commands. -If nil, `find-file' is used." - :type 'function - :group 'org-roam) - -(defcustom org-roam-include-type-in-ref-path-completions nil - "When t, include the type in ref-path completions. -Note that this only affects interactive calls. -See `org-roam--get-ref-path-completions' for details." - :type 'boolean - :group 'org-roam) - -(defcustom org-roam-index-file "index.org" - "Path to the Org-roam index file. -The path can be a string or a function. If it is a string, it -should be the path (absolute or relative to `org-roam-directory') -to the index file. If it is is a function, the function should -return the path to the index file. Otherwise, the index is -assumed to be a note in `org-roam-directory' whose title is -'Index'." - :type '(choice - (string :tag "Path to index" "%s") - (function :tag "Function to generate the path")) - :group 'org-roam) - -(defcustom org-roam-link-title-format "%s" - "The formatter used when inserting Org-roam links that use their title. -Formatter may be a function that takes title as its only argument." - :type '(choice - (string :tag "String Format" "%s") - (function :tag "Custom function")) - :group 'org-roam) - -(defcustom org-roam-prefer-id-links t - "If non-nil, use ID for linking instead where available." - :type 'boolean - :group 'org-roam) - (defcustom org-roam-list-files-commands (if (member system-type '(windows-nt ms-dos cygwin)) nil - '(find rg)) + '(find fd fdfind rg)) "Commands that will be used to find Org-roam files. It should be a list of symbols or cons cells representing any of the following - supported file search methods. +supported file search methods. The commands will be tried in order until an executable for a command is found. The Elisp implementation is used if no command in the list is found. - `rg' - Use ripgrep as the file search method. - Example command: rg /path/to/dir/ --files -g \"*.org\" -g \"*.org.gpg\" - `find' Use find as the file search method. Example command: find /path/to/dir -type f \( -name \"*.org\" -o -name \"*.org.gpg\" \) + `fd' + Use fd as the file search method. + Example command: fd /path/to/dir/ --type file -e \".org\" -e \".org.gpg\" + + `fdfind' + Same as `fd'. It's an alias that used in some OSes (e.g. Debian, Ubuntu) + + `rg' + Use ripgrep as the file search method. + Example command: rg /path/to/dir/ --files -g \"*.org\" -g \"*.org.gpg\" + By default, `executable-find' will be used to look up the path to the executable. If a custom path is required, it can be specified together with the method symbol as a cons cell. For example: '(find (rg . \"/path/to/rg\"))." :type '(set (const :tag "find" find) (const :tag "rg" rg))) -(defcustom org-roam-tag-separator "," - "String to use to separate tags when `org-roam-tag-sources' is non-nil." - :type 'string - :group 'org-roam) - -(defcustom org-roam-tag-sort nil - "When non-nil, sort the tags in the completions. -When t, sort the tags alphabetically, regardless of case. -`org-roam-tag-sort' can also be a list of arguments to be applied -to `cl-sort'. For example, these are the arguments used when -`org-roam-tag-sort' is set to t: - \('string-lessp :key 'downcase) -Only relevant when `org-roam-tag-sources' is non-nil." - :type '(choice - (boolean) - (list :tag "Arguments to cl-loop")) - :group 'org-roam) - -(defcustom org-roam-tag-sources '(prop) - "Sources to obtain tags from. - -It should be a list of symbols representing any of the following -extraction methods: - - `prop' - Extract tags from the #+roam_tags property. - Tags are space delimited. - Tags may contain spaces if they are double-quoted. - e.g. #+roam_tags: TAG \"tag with spaces\" - - `vanilla' - Extract vanilla `org-mode' tags, including #+FILETAGS and - inherited tags. - - `all-directories' - Extract sub-directories relative to `org-roam-directory'. - That is, if a file is located at relative path foo/bar/file.org, - the file will have tags \"foo\" and \"bar\". - - `last-directory' - Extract the last directory relative to `org-roam-directory'. - That is, if a file is located at relative path foo/bar/file.org, - the file will have tag \"bar\". - - `first-directory' - Extract the first directory relative to `org-roam-directory'. - That is, if a file is located at relative path foo/bar/file.org, - the file will have tag \"foo\"." - :type '(set (const :tag "#+roam_tags" prop) - (const :tag "buffer org tags" vanilla) - (const :tag "sub-directories" all-directories) - (const :tag "parent directory" last-directory) - (const :tag "first sub-directory" first-directory))) - -(defcustom org-roam-title-to-slug-function #'org-roam--title-to-slug - "Function to be used in converting a title to the filename slug. -Function should return a filename string based on title." - :type 'function - :group 'org-roam) - -(defcustom org-roam-slug-trim-chars - '(;; Combining Diacritical Marks https://www.unicode.org/charts/PDF/U0300.pdf - 768 ; U+0300 COMBINING GRAVE ACCENT - 769 ; U+0301 COMBINING ACUTE ACCENT - 770 ; U+0302 COMBINING CIRCUMFLEX ACCENT - 771 ; U+0303 COMBINING TILDE - 772 ; U+0304 COMBINING MACRON - 774 ; U+0306 COMBINING BREVE - 775 ; U+0307 COMBINING DOT ABOVE - 776 ; U+0308 COMBINING DIAERESIS - 777 ; U+0309 COMBINING HOOK ABOVE - 778 ; U+030A COMBINING RING ABOVE - 780 ; U+030C COMBINING CARON - 795 ; U+031B COMBINING HORN - 803 ; U+0323 COMBINING DOT BELOW - 804 ; U+0324 COMBINING DIAERESIS BELOW - 805 ; U+0325 COMBINING RING BELOW - 807 ; U+0327 COMBINING CEDILLA - 813 ; U+032D COMBINING CIRCUMFLEX ACCENT BELOW - 814 ; U+032E COMBINING BREVE BELOW - 816 ; U+0330 COMBINING TILDE BELOW - 817 ; U+0331 COMBINING MACRON BELOW - ) - "Characters to trim from Unicode normalization for slug. - -By default, the characters are specified to remove Diacritical -Marks from the Latin alphabet." - :type '(repeat character) - :group 'org-roam) - -(defcustom org-roam-title-sources '((title headline) alias) - "The list of sources from which to retrieve a note title. -Each element in the list is either: - -1. a symbol -- this symbol corresponds to a title retrieval -function, which returns the list of titles for the current buffer -2. a list of symbols -- symbols in the list are treated as -with (1). The return value of this list is the first symbol in -the list returning a non-nil value. - -The return results of the root list are concatenated. - -For example the setting: '((title headline) alias) means the following: - -1. Return the 'title + 'alias, if the title of current buffer is non-empty; -2. Or return 'headline + 'alias otherwise. - -The currently supported symbols are: - - `title' - The \"#+title\" property of org file. - - `alias' - The \"#+roam_alias\" property of the org file, using - space-delimited strings. - - `headline' - The first headline in the org file." - :type '(repeat - (choice - (repeat symbol) - (symbol))) - :group 'org-roam) - -(defcustom org-roam-file-completion-tag-position 'prepend - "Prepend, append, or omit tags from the file titles during completion." - :type '(choice (const :tag "Prepend" prepend) - (const :tag "Append" append) - (const :tag "Omit" omit)) - :group 'org-roam) - -(defcustom org-roam-enable-headline-linking t - "Enable linking to headlines. -This includes automatic :ID: creation and scanning of :ID:s for -org-roam database." - :type 'boolean - :group 'org-roam) - -(defcustom org-roam-verbose t - "Echo messages that are not errors." - :type 'boolean - :group 'org-roam) - -(defvar org-roam-completion-functions nil - "List of functions to be used with `completion-at-point' for Org-roam.") - -;;;; Dynamic variables -(defvar org-roam-last-window nil - "Last window `org-roam' was called from.") - -(defvar-local org-roam-file-name nil - "The corresponding file for a temp buffer. -This is set by `org-roam--with-temp-buffer', to allow throwing of -descriptive warnings when certain operations fail (e.g. parsing).") - -(defvar org-roam--org-link-bracket-typed-re - (rx (seq "[[" - (group (+? anything)) - ":" - (group - (one-or-more - (or (not (any "[]\\")) - (and "\\" (zero-or-more "\\\\") (any "[]")) - (and (one-or-more "\\") (not (any "[]")))))) - "]" - (opt "[" (group (+? anything)) "]") - "]")) - "Matches a typed link in double brackets.") - -;;;; Utilities -(defun org-roam--plist-to-alist (plist) - "Return an alist of the property-value pairs in PLIST." - (let (res) - (while plist - (let ((prop (intern (substring (symbol-name (pop plist)) 1 nil))) - (val (pop plist))) - (push (cons prop val) res))) - res)) - -(defun org-roam--url-p (path) - "Check if PATH is a URL. -Assume the protocol is not present in PATH; e.g. URL `https://google.com' is -passed as `//google.com'." - (string-prefix-p "//" path)) +;;;; ID Utilities +(defun org-roam-id-at-point () + "Return the ID at point, if any. +Recursively traverses up the headline tree to find the +first encapsulating ID." + (org-with-wide-buffer + (org-back-to-heading-or-point-min) + (while (and (not (org-roam-db-node-p)) + (not (bobp))) + (org-roam-up-heading-or-point-min)) + (org-id-get))) ;;;; File functions and predicates (defun org-roam--file-name-extension (filename) @@ -369,23 +163,17 @@ Like `file-name-extension', but does not strip version number." (not (eq 0 (match-beginning 0)))) (substring file (+ (match-beginning 0) 1)))))) -(defun org-roam--org-file-p (path) - "Check if PATH is pointing to an org file." - (let ((ext (org-roam--file-name-extension path))) - (when (string= ext "gpg") ; Handle encrypted files - (setq ext (org-roam--file-name-extension (file-name-sans-extension path)))) - (member ext org-roam-file-extensions))) - -(defun org-roam--org-roam-file-p (&optional file) +(defun org-roam-file-p (&optional file) "Return t if FILE is part of Org-roam system, nil otherwise. If FILE is not specified, use the current buffer's file-path." - (when-let ((path (or file - org-roam-file-name - (-> (buffer-base-buffer) - (buffer-file-name))))) + (let* ((path (or file (buffer-file-name (buffer-base-buffer)))) + (ext (org-roam--file-name-extension path)) + (ext (if (string= ext "gpg") + (org-roam--file-name-extension (file-name-sans-extension path)) + ext))) (save-match-data (and - (org-roam--org-file-p path) + (member ext org-roam-file-extensions) (not (and org-roam-file-exclude-regexp (string-match-p org-roam-file-exclude-regexp path))) (f-descendant-of-p path (expand-file-name org-roam-directory)))))) @@ -401,9 +189,18 @@ If FILE is not specified, use the current buffer's file-path." (defun org-roam--list-files-search-globs (exts) "Given EXTS, return a list of search globs. E.g. (\".org\") => (\"*.org\" \"*.org.gpg\")" - (append - (mapcar (lambda (ext) (s-wrap (concat "*." ext) "\"")) exts) - (mapcar (lambda (ext) (s-wrap (concat "*." ext ".gpg") "\"")) exts))) + (cl-loop for e in exts + append (list (format "\"*.%s\"" e) + (format "\"*.%s.gpg\"" e)))) + +(defun org-roam--list-files-fd (executable dir) + "Return all Org-roam files located recursively within DIR, using fd, provided as EXECUTABLE." + (let* ((globs (org-roam--list-files-search-globs org-roam-file-extensions)) + (extensions (s-join " -e " (mapcar (lambda (glob) (substring glob 2 -1)) globs))) + (command (s-join " " `(,executable "-L" ,dir "--type file" ,extensions)))) + (org-roam--shell-command-files command))) + +(defalias 'org-roam--list-files-fdfind #'org-roam--list-files-fd) (defun org-roam--list-files-rg (executable dir) "Return all Org-roam files located recursively within DIR, using ripgrep, provided as EXECUTABLE." @@ -415,8 +212,8 @@ E.g. (\".org\") => (\"*.org\" \"*.org.gpg\")" (defun org-roam--list-files-find (executable dir) "Return all Org-roam files located recursively within DIR, using find, provided as EXECUTABLE." (let* ((globs (org-roam--list-files-search-globs org-roam-file-extensions)) - (command (s-join " " `(,executable "-L" ,dir "-type f \\(" - ,(s-join " -o " (mapcar (lambda (glob) (concat "-name " glob)) globs)) "\\)")))) + (names (s-join " -o " (mapcar (lambda (glob) (concat "-name " glob)) globs))) + (command (s-join " " `(,executable "-L" ,dir "-type f \\(" ,names "\\)")))) (org-roam--shell-command-files command))) ;; Emacs 26 does not have FOLLOW-SYMLINKS in `directory-files-recursively' @@ -482,11 +279,13 @@ recursion." (defun org-roam--list-files-elisp (dir) "Return all Org-roam files located recursively within DIR, using elisp." - (let ((regex (concat "\\.\\(?:"(mapconcat #'regexp-quote org-roam-file-extensions "\\|" )"\\)\\(?:\\.gpg\\)?\\'")) + (let ((regex (concat "\\.\\(?:"(mapconcat + #'regexp-quote org-roam-file-extensions + "\\|" )"\\)\\(?:\\.gpg\\)?\\'")) result) (dolist (file (org-roam--directory-files-recursively dir regex nil nil t) result) (when (and (file-readable-p file) - (org-roam--org-roam-file-p file)) + (org-roam-file-p file)) (push file result))))) (defun org-roam--list-files (dir) @@ -510,7 +309,7 @@ Use external shell commands if defined in `org-roam-list-files-commands'." (let ((fn (intern (concat "org-roam--list-files-" exe)))) (unless (fboundp fn) (user-error "%s is not an implemented search method" fn)) (funcall fn path (format "\"%s\"" dir))))) - (files (seq-filter #'org-roam--org-roam-file-p files)) + (files (seq-filter #'org-roam-file-p files)) (files (mapcar #'expand-file-name files))) ; canonicalize names files (org-roam--list-files-elisp dir)))) @@ -519,504 +318,15 @@ Use external shell commands if defined in `org-roam-list-files-commands'." "Return a list of all Org-roam files within `org-roam-directory'." (org-roam--list-files (expand-file-name org-roam-directory))) -;;;; Org extraction functions -(defun org-roam--extract-global-props-drawer (props) - "Extract PROPS from the file-level property drawer in Org." - (let (ret) - (org-with-point-at 1 - (dolist (prop props ret) - (when-let ((v (org-entry-get (point) prop))) - (push (cons prop v) ret)))))) +(defun org-roam--nodes-table () + "Return a hash table of node ID to org-roam-nodes." + (let ((ht (make-hash-table :test #'equal))) + (pcase-dolist (`(,id ,file ,title) + (org-roam-db-query [:select [id file title] :from nodes])) + (puthash id (org-roam-node-create :file file :id id :title title) ht)) + ht)) -(defun org-roam--collect-keywords (keywords) - "Collect all Org KEYWORDS in the current buffer." - (if (functionp 'org-collect-keywords) - (org-collect-keywords keywords) - (let ((buf (org-element-parse-buffer)) - res) - (dolist (k keywords) - (let ((p (org-element-map buf 'keyword - (lambda (kw) - (when (string-equal (org-element-property :key kw) k) - (org-element-property :value kw))) - :first-match nil))) - (push (cons k p) res))) - res))) - -(defun org-roam--extract-global-props-keyword (keywords) - "Extract KEYWORDS from the current Org buffer." - (let (ret) - (pcase-dolist (`(,key . ,values) (org-roam--collect-keywords keywords)) - (dolist (value values) - (push (cons key value) ret))) - ret)) - -(defun org-roam--extract-global-props (props) - "Extract PROPS from the current Org buffer. -Props are extracted from both the file-level property drawer (if -any), and Org keywords. Org keywords take precedence." - (append - (org-roam--extract-global-props-keyword props) - (org-roam--extract-global-props-drawer props))) - -(defun org-roam--extract-prop-as-list (prop) - "Extract PROP from the current Org buffer as a list. - -This is the common logic behind the extraction of roam_tags and -roam_alias." - ;; Values are split in two ways: - ;; 1. with spaces and double quotes: - ;; #+prop: a b c \"quoted string\" - ;; -> '(\"a\" \"b\" \"c\" \"quoted string\") - ;; 2. and/or with multiple lines: - ;; #+prop: a b - ;; #+prop: c d - ;; -> '(\"a\" \"b\" \"c\" \"d\") - (--> (org-roam--extract-global-props (list prop)) - ;; so that the returned order is the same as in the buffer - nreverse - ;; '(("ROAM_TAGS" . "a b") ("ROAM_TAGS" . "c d")) - ;; -> '("a b" "c d") - (mapcar #'cdr it) - (mapcar #'split-string-and-unquote it) - ;; We have a list of lists at this point. Join them. - (apply #'append it))) - -(defun org-roam--get-outline-path () - "Return the outline path to the current entry. - -An outline path is a list of ancestors for current headline, as a -list of strings. Statistics cookies are removed and links are -kept. - -When optional argument WITH-SELF is non-nil, the path also -includes the current headline." - (org-with-wide-buffer - (save-match-data - (and (or (condition-case nil - (org-back-to-heading t) - (error nil)) - (org-up-heading-safe)) - (reverse (org-roam--get-outline-path-1)))))) - -(defun org-roam--get-outline-path-1 () - "Return outline path to current headline. - -Outline path is a list of strings, in reverse order. See -`org-roam--get-outline-path' for details. - -Assume buffer is widened and point is on a headline." - (when org-complex-heading-regexp - (let ((heading (let ((case-fold-search nil)) - (looking-at org-complex-heading-regexp) - (if (not (match-end 4)) "" - ;; Remove statistics cookies. - (org-trim - (replace-regexp-in-string - "\\[[0-9]+%\\]\\|\\[[0-9]+/[0-9]+\\]" "" - (match-string-no-properties 4))))))) - (if (org-up-heading-safe) - (cons heading (org-roam--get-outline-path-1)) - (list heading))))) - -(defun org-roam--extract-links (&optional file-path) - "Extracts all link items within the current buffer. -Link items are of the form: - - [source dest type properties] - -This is the format that emacsql expects when inserting into the database. -FILE-FROM is typically the buffer file path, but this may not exist, for example -in temp buffers. In cases where this occurs, we do know the file path, and pass -it as FILE-PATH." - (require 'org-ref nil t) - (setq file-path (or file-path - org-roam-file-name - (buffer-file-name))) - (save-excursion - (let (links) - (org-element-map (org-element-parse-buffer) 'link - (lambda (link) - (goto-char (org-element-property :begin link)) - (let* ((type (org-roam--collate-types (org-element-property :type link))) - (path (org-element-property :path link)) - (properties (list :outline (org-roam--get-outline-path) - :point (point))) - (names (pcase type - ("id" - (when-let ((file-path (org-roam-id-get-file path))) - (list file-path))) - ("cite" (list path)) - ("website" (list path)) - ("fuzzy" (list path)) - ("roam" (list path)) - (_ (if (or (file-remote-p path) - (org-roam--url-p path)) - (list path) - (let ((file-maybe (expand-file-name path (file-name-directory file-path)))) - (if (f-exists? file-maybe) - (list file-maybe) - (list path)))))))) - (dolist (name names) - (when name - (push (vector file-path name type properties) links)))))) - links))) - -(defun org-roam--extract-ids (&optional file-path) - "Extract all IDs within the current buffer. -If FILE-PATH is nil, use the current file." - (setq file-path (or file-path org-roam-file-name (buffer-file-name))) - (let (result) - ;; We need to handle the special case of the file property drawer (at outline level 0) - (org-with-point-at (point-min) - (when-let ((before-first-heading (= 0 (org-outline-level))) - (id (org-entry-get nil "ID"))) - (push (vector id file-path 0) result))) - (org-map-region - (lambda () - (when-let ((id (org-entry-get nil "ID"))) - (push (vector id file-path (org-outline-level)) result))) - (point-min) (point-max)) - result)) - -(defun org-roam--extract-titles-title () - "Return title from \"#+title\" of the current buffer." - (let* ((prop (org-roam--extract-global-props '("TITLE"))) - (title (cdr (assoc "TITLE" prop)))) - (when title - (list title)))) - -(defun org-roam--extract-titles-alias () - "Return the aliases from the current buffer. -Reads from the \"roam_alias\" property." - (condition-case nil - (org-roam--extract-prop-as-list "ROAM_ALIAS") - (error - (progn - (lwarn '(org-roam) :error - "Failed to parse aliases for buffer: %s. Skipping" - (or org-roam-file-name - (buffer-file-name))) - nil)))) - -(defun org-roam--extract-titles-headline () - "Return the first headline of the current buffer." - (let ((headline (save-excursion - (goto-char (point-min)) - ;; "What happens if a heading star was quoted - ;; before the first heading?" - ;; - `org-map-region' also does this - ;; - Org already breaks badly when you do that; - ;; precede the heading star with a ",". - (re-search-forward org-outline-regexp-bol nil t) - (org-entry-get nil "ITEM")))) - (when headline - (list headline)))) - -(defun org-roam--extract-titles (&optional sources nested) - "Extract the titles from current buffer using SOURCES. -If NESTED, return the first successful result from SOURCES." - (org-with-wide-buffer - (let (coll res) - (cl-dolist (source (or sources - org-roam-title-sources)) - (setq res (if (symbolp source) - (funcall (intern (concat "org-roam--extract-titles-" (symbol-name source)))) - (org-roam--extract-titles source t))) - (when res - (if (not nested) - (setq coll (nconc coll res)) - (setq coll res) - (cl-return)))) - (-uniq coll)))) - -(defun org-roam--extract-tags-all-directories (file) - "Extract tags from using the directory path FILE. -All sub-directories relative to `org-roam-directory' are used as tags." - (when-let ((dir-relative (file-name-directory - (file-relative-name file (expand-file-name org-roam-directory))))) - (f-split dir-relative))) - -(defun org-roam--extract-tags-last-directory (file) - "Extract tags from using the directory path FILE. -The final directory component is used as a tag." - (when-let ((dir-relative (file-name-directory - (file-relative-name file (expand-file-name org-roam-directory))))) - (last (f-split dir-relative)))) - -(defun org-roam--extract-tags-first-directory (file) - "Extract tags from path FILE. -The first directory component after `org-roam-directory' is used as a -tag." - (when-let ((dir-relative (file-name-directory - (file-relative-name file (expand-file-name org-roam-directory))))) - (list (car (f-split dir-relative))))) - -(defun org-roam--extract-tags-prop (_file) - "Extract tags from the current buffer's \"#roam_tags\" global property." - (condition-case nil - (org-roam--extract-prop-as-list "ROAM_TAGS") - (error - (progn - (lwarn '(org-roam) :error - "Failed to parse tags for buffer: %s. Skipping" - (or org-roam-file-name - (buffer-file-name))) - nil)))) - -(defun org-roam--extract-tags-vanilla (_file) - "Extract vanilla `org-mode' tags. -This includes all tags used in the buffer." - (org-set-regexps-and-options 'tags-only) - (-flatten (org-get-buffer-tags))) - -(defun org-roam--extract-tags (&optional file) - "Extract tags from the current buffer. -If file-path FILE, use it to determine the directory tags. -Tags are obtained via: - -1. Directory tags: Relative to `org-roam-directory': each folder - path is considered a tag. -2. The key #+roam_tags." - (let* ((file (or file (buffer-file-name (buffer-base-buffer)))) - (tags (-uniq - (mapcan (lambda (source) - (funcall (intern (concat "org-roam--extract-tags-" - (symbol-name source))) - file)) - org-roam-tag-sources)))) - (pcase org-roam-tag-sort - ('nil tags) - ((pred booleanp) (cl-sort tags 'string-lessp :key 'downcase)) - (`(,(pred symbolp) . ,_) - (apply #'cl-sort (push tags org-roam-tag-sort))) - (wrong-type (signal 'wrong-type-argument - `((booleanp (list symbolp)) - ,wrong-type)))))) - -(defun org-roam--collate-types (type) - "Collate TYPE into a parent type. -Packages like `org-ref' introduce many different link prefixes, -but we collate them under the same parent type to clean up -backlinks." - (cond ((and (boundp 'org-ref-cite-types) - (member type org-ref-cite-types)) - "cite") - ((member type '("http" "https")) - "website") - (t type))) - -(defun org-roam--split-ref (ref) - "Processes REF into its type and path. -Returns a cons cell of type and path if ref is a valid ref." - (save-match-data - (when (string-match org-link-plain-re ref) - (cons (org-roam--collate-types (match-string 1 ref)) - (match-string 2 ref))))) - -(defun org-roam--extract-refs () - "Extract all refs (ROAM_KEY statements) from the current buffer. - -Each ref is returned as a cons of its type and its key." - (let (refs) - (pcase-dolist - (`(,_ . ,roam-key) - (org-roam--extract-global-props '("ROAM_KEY"))) - (pcase roam-key - ('nil nil) - ((pred string-empty-p) - (user-error "Org property #+roam_key cannot be empty")) - (ref - (when-let ((r (org-roam--split-ref ref))) - (push r refs))))) - refs)) - -(defun org-roam--extract-ref () - "Extract the ref from current buffer and return the type and the key of the ref." - (car (org-roam--extract-refs))) - -;;;; Title/Path/Slug conversion -(defun org-roam--path-to-slug (path) - "Return a slug from PATH." - (-> path - (file-relative-name (expand-file-name org-roam-directory)) - (file-name-sans-extension))) - -(defun org-roam--title-to-slug (title) - "Convert TITLE to a filename-suitable slug." - (cl-flet* ((nonspacing-mark-p (char) - (memq char org-roam-slug-trim-chars)) - (strip-nonspacing-marks (s) - (ucs-normalize-NFC-string - (apply #'string (seq-remove #'nonspacing-mark-p - (ucs-normalize-NFD-string s))))) - (cl-replace (title pair) - (replace-regexp-in-string (car pair) (cdr pair) title))) - (let* ((pairs `(("[^[:alnum:][:digit:]]" . "_") ;; convert anything not alphanumeric - ("__*" . "_") ;; remove sequential underscores - ("^_" . "") ;; remove starting underscore - ("_$" . ""))) ;; remove ending underscore - (slug (-reduce-from #'cl-replace (strip-nonspacing-marks title) pairs))) - (downcase slug)))) - -(defun org-roam-format-link (target &optional description type link-type) - "Formats an org link for a given file TARGET, link DESCRIPTION and link TYPE. -TYPE defaults to \"file\". LINK-TYPE is the type of file link to -be generated. Here, we also check if there is an ID for the -file." - (setq type (or type "file")) - (when-let ((id (and org-roam-prefer-id-links - (string-equal type "file") - (caar (org-roam-db-query [:select [id] :from ids - :where (= file $s1) - :and (= level 0) - :limit 1] - target))))) - (setq type "id" target id)) - (when (string-equal type "file") - (setq target (org-roam-link-get-path target link-type))) - (setq description - (if (functionp org-roam-link-title-format) - (funcall org-roam-link-title-format description type) - (format org-roam-link-title-format description))) - (org-link-make-string (concat type ":" target) description)) - -(defun org-roam--add-tag-string (str tags) - "Add TAGS to STR. - -Depending on the value of `org-roam-file-completion-tag-position', this function -prepends TAGS to STR, appends TAGS to STR or omits TAGS from STR." - (pcase org-roam-file-completion-tag-position - ('prepend (concat - (when tags (propertize (format "(%s) " (s-join org-roam-tag-separator tags)) - 'face 'org-roam-tag)) - str)) - ('append (concat - str - (when tags (propertize (format " (%s)" (s-join org-roam-tag-separator tags)) - 'face 'org-roam-tag)))) - ('omit str))) - - -(defun org-roam--get-title-path-completions () - "Return an alist for completion. -The car is the displayed title for completion, and the cdr is a -plist containing the path and title for the file." - (let* ((rows (org-roam-db-query [:select [files:file titles:title tags:tags files:meta] :from titles - :left :join tags - :on (= titles:file tags:file) - :left :join files - :on (= titles:file files:file)])) - completions) - (setq rows (seq-sort-by (lambda (x) - (plist-get (nth 3 x) :mtime)) - #'time-less-p - rows)) - (dolist (row rows completions) - (pcase-let ((`(,file-path ,title ,tags) row)) - (let ((k (org-roam--add-tag-string title tags)) - (v (list :path file-path :title title))) - (push (cons k v) completions)))))) - -(defun org-roam--get-index-path () - "Return the path to the index in `org-roam-directory'. -The path to the index can be defined in `org-roam-index-file'. -Otherwise, it is assumed to be a note in `org-roam-directory' -whose title is 'Index'." - (let ((path (pcase org-roam-index-file - ((pred functionp) (funcall org-roam-index-file)) - ((pred stringp) org-roam-index-file) - ('nil (user-error "You need to set `org-roam-index-file' before you can jump to it")) - (wrong-type (signal 'wrong-type-argument - `((functionp stringp) - ,wrong-type)))))) - (expand-file-name path org-roam-directory))) - -;;;; dealing with file-wide properties -(defun org-roam--set-global-prop (name value) - "Set a file property called NAME to VALUE. - -If the property is already set, it's value is replaced." - (org-with-point-at 1 - (let ((case-fold-search t)) - (if (re-search-forward (concat "^#\\+" name ":\\(.*\\)") (point-max) t) - (replace-match (concat " " value) 'fixedcase nil nil 1) - (while (and (not (eobp)) - (looking-at "^[#:]")) - (if (save-excursion (end-of-line) (eobp)) - (progn - (end-of-line) - (insert "\n")) - (forward-line) - (beginning-of-line))) - (insert "#+" name ": " value "\n"))))) - -;;;; org-roam-find-ref -(defun org-roam--get-ref-path-completions (&optional arg filter) - "Return an alist of refs to absolute path of Org-roam files. - -When called interactively (i.e. when ARG is 1), formats the car -of the completion-candidates with extra information: title, tags, -and type \(when `org-roam-include-type-in-ref-path-completions' -is non-nil). - -When called with a `C-u' prefix (i.e. when ARG is 4), forces the -default format without the formatting. - -FILTER can either be a string or a function: - -- If it is a string, it should be the type of refs to include as - candidates \(e.g. \"cite\", \"website\", etc.) - -- If it is a function, it should be the name of a function that - takes three arguments: the type, the ref, and the file of the - current candidate. It should return t if that candidate is to - be included as a candidate." - (let ((rows (org-roam-db-query - [:select [refs:type refs:ref refs:file titles:title tags:tags] - :from titles - :left :join tags - :on (= titles:file tags:file) - :left :join refs :on (= titles:file refs:file) - :where refs:file :is :not :null])) - completions) - (setq rows (seq-sort-by (lambda (x) - (plist-get (nth 3 x) :mtime)) - #'time-less-p - rows)) - (dolist (row rows completions) - (pcase-let ((`(,type ,ref ,file-path ,title ,tags) row)) - (when (pcase filter - ('nil t) - ((pred stringp) (string= type filter)) - ((pred functionp) (funcall filter type ref file-path)) - (wrong-type (signal 'wrong-type-argument - `((stringp functionp) - ,wrong-type)))) - (let ((k (if (eq arg 1) - (concat - (when org-roam-include-type-in-ref-path-completions - (format "{%s} " type)) - (org-roam--add-tag-string (format "%s (%s)" title ref) - tags)) - ref)) - (v (list :path file-path :type type :ref ref))) - (push (cons k v) completions))))))) - -(defun org-roam--find-file (file) - "Open FILE using `org-roam-find-file-function' or `find-file'." - (funcall (or org-roam-find-file-function #'find-file) file)) - -(defun org-roam--find-ref (ref) - "Find and open and Org-roam file from REF if it exists. -REF should be the value of '#+roam_key:' without any -type-information (e.g. 'cite:'). -Return nil if the file does not exist." - (when-let* ((completions (org-roam--get-ref-path-completions)) - (file (plist-get (cdr (assoc ref completions)) :path))) - (org-roam--find-file file))) - -(defun org-roam--org-roam-buffer-p (&optional buffer) +(defun org-roam-buffer-p (&optional buffer) "Return t if BUFFER is accessing a part of Org-roam system. If BUFFER is not specified, use the current buffer." (let ((buffer (or buffer (current-buffer))) @@ -1024,654 +334,552 @@ If BUFFER is not specified, use the current buffer." (with-current-buffer buffer (and (derived-mode-p 'org-mode) (setq path (buffer-file-name (buffer-base-buffer))) - (org-roam--org-roam-file-p path))))) + (org-roam-file-p path))))) -(defun org-roam--get-roam-buffers () +(defun org-roam-buffer-list () "Return a list of buffers that are Org-roam files." - (--filter (org-roam--org-roam-buffer-p it) + (--filter (org-roam-buffer-p it) (buffer-list))) -(defun org-roam--save-buffers (&optional ask update) - "Save all Org-roam buffers. -When ASK is non-nil, ask whether the buffers should be saved. -When UPDATE is non-nil, update the database after." - (save-some-buffers (not ask) #'org-roam--org-roam-buffer-p) - (when update (org-roam-db-update))) - -;;; org-roam-backlinks-mode -(define-minor-mode org-roam-backlinks-mode - "Minor mode for the `org-roam-buffer'. -\\{org-roam-backlinks-mode-map}" - :lighter " Backlinks" - :keymap (let ((map (make-sparse-keymap))) - (define-key map [mouse-1] 'org-open-at-point) - (define-key map (kbd "RET") 'org-open-at-point) - map) - (if org-roam-backlinks-mode - (add-hook 'org-open-at-point-functions - #'org-roam-open-at-point nil 'local) - (remove-hook 'org-open-at-point-functions - #'org-roam-open-at-point 'local))) - -(defun org-roam--in-buffer-p () - "Return t if in the Org-roam buffer." - (and (boundp org-roam-backlinks-mode) - org-roam-backlinks-mode)) - -(defun org-roam--backlink-to-current-p () - "Return t if the link at point is to the current Org-roam file." - (save-match-data - (let ((current-file (buffer-file-name org-roam-buffer--current)) - (backlink-dest (save-excursion - (let* ((context (org-element-context)) - (type (org-element-property :type context)) - (dest (org-element-property :path context))) - (pcase type - ("id" (org-roam-id-get-file dest)) - (_ dest)))))) - (string= current-file backlink-dest)))) - -(defun org-roam-open-at-point () - "Open an Org-roam link or visit the text previewed at point. -When point is on an Org-roam link, open the link in the Org-roam window. -When point is on the Org-roam preview text, open the link in the Org-roam -window, and navigate to the point. -This function hooks into `org-open-at-point' via `org-open-at-point-functions'." - (cond - ;; Org-roam link - ((let* ((context (org-element-context)) - (path (org-element-property :path context))) - (when (and (eq (org-element-type context) 'link) - (org-roam--org-roam-file-p path)) - (org-roam-buffer--find-file path) - (org-show-context) - t))) - ;; Org-roam preview text - ((when-let ((file-from (get-text-property (point) 'file-from)) - (p (get-text-property (point) 'file-from-point))) - (org-roam-buffer--find-file file-from) - (goto-char p) - (org-show-context) - t)) - ;; If called via `org-open-at-point', fall back to default behavior. - (t nil))) - -(defun org-roam--get-backlinks (targets) - "Return the backlinks for TARGETS. -TARGETS is a list of strings corresponding to the TO value in the -Org-roam cache. It may be a file, for Org-roam file links, or a -citation key, for Org-ref cite links." - (unless (listp targets) - (setq targets (list targets))) - (let ((conditions (--> targets - (mapcar (lambda (i) (list '= 'dest i)) it) - (org-roam--list-interleave it :or)))) - (org-roam-db-query `[:select [source dest properties] :from links - :where ,@conditions - :order-by (asc source)]))) - -(defun org-roam-id-get-file (id &optional strict) - "Return the file if ID exists. -When STRICT is non-nil, only consider Org-roam's database. -Return nil otherwise." - (or (caar (org-roam-db-query [:select [file] - :from ids - :where (= id $s1) - :limit 1] - id)) - (and (not strict) - (progn - (unless org-id-locations (org-id-locations-load)) - (or (and org-id-locations - (hash-table-p org-id-locations) - (gethash id org-id-locations))))))) - -(defun org-roam-id-find (id &optional markerp strict keep-buffer-p) - "Return the location of the entry with the id ID. -When MARKERP is non-nil, return a marker pointing to the headline. -Otherwise, return a cons formatted as \(file . pos). -When STRICT is non-nil, only consider Org-roam’s database. -When KEEP-BUFFER-P is non-nil, keep the buffers navigated by Org-roam open." - (let ((file (org-roam-id-get-file id strict))) - (when file - (org-roam-with-file file keep-buffer-p - (org-id-find-id-in-file id file markerp))))) - -(defun org-roam-id-open (id-or-marker &optional strict) - "Go to the entry with ID-OR-MARKER. -Wrapper for `org-id-open' which tries to find the ID in the -Org-roam's database. -ID-OR-MARKER can either be the ID of the entry or the marker -pointing to it if it has already been computed by -`org-roam-id-find'. If the ID-OR-MARKER is not found, it reverts -to the default behaviour of `org-id-open'. -When STRICT is non-nil, only consider Org-roam’s database." - (when-let ((marker (if (markerp id-or-marker) - id-or-marker - (org-roam-id-find id-or-marker t strict t)))) - (org-mark-ring-push) - (org-goto-marker-or-bmk marker) - (set-marker marker nil))) - -(defun org-roam-open-id-at-point () - "Open link, timestamp, footnote or tags at point. -The function tries to open ID-links with Org-roam’s database -before falling back to the default behaviour of -`org-open-at-point'. It also asks the user whether to parse -`org-id-files' when an ID is not found because it might be a slow -process. -This function hooks into `org-open-at-point' via -`org-open-at-point-functions'." - (let* ((context (org-element-context)) - (type (org-element-property :type context)) - (id (org-element-property :path context))) - (when (string= type "id") - (cond ((org-roam-id-open id) - t) - ;; Ask whether to parse `org-id-files' - ((not (y-or-n-p (concat "ID was not found in `org-roam-directory' nor in `org-id-locations'.\n" - "Search in `org-id-files'? "))) - t) - ;; Conditionally fall back to default behaviour - (t - nil))))) - -;;; Completion at point -(defcustom org-roam-completion-everywhere nil - "If non-nil, provide completions from the current word at point." - :group 'org-roam - :type 'boolean) - -;;;; Tags completion -(defun org-roam-complete-tags-at-point () - "`completion-at-point' function for Org-roam tags." - (let ((end (point)) - (start (point)) - (exit-fn (lambda (&rest _) nil)) - collection) - (when (looking-back "^#\\+roam_tags:.*" (line-beginning-position)) - (when (looking-at "\\>") - (setq start (save-excursion (skip-syntax-backward "w") - (point)) - end (point))) - (setq collection #'org-roam-db--get-tags - exit-fn (lambda (str _status) - (delete-char (- (length str))) - (insert "\"" str "\"")))) - (when collection - (let ((prefix (buffer-substring-no-properties start end))) - (list start end - (if (functionp collection) - (completion-table-case-fold - (completion-table-dynamic - (lambda (_) - (cl-remove-if (apply-partially #'string= prefix) - (funcall collection)))) - (not org-roam-completion-ignore-case)) - collection) - :exit-function exit-fn))))) - (defun org-roam--get-titles () - "Return all titles within Org-roam." - (mapcar #'car (org-roam-db-query [:select [titles:title] :from titles]))) + "Return all distinct titles and aliases in the Org-roam database." + (mapcar #'car (org-roam-db-query [:select :distinct title :from nodes + :union :select alias :from aliases]))) -(defun org-roam-complete-everywhere () - "`completion-at-point' function for word at point. -This is active when `org-roam-completion-everywhere' is non-nil." - (let ((end (point)) - (start (point)) - (exit-fn (lambda (&rest _) nil)) - collection) - (when (and org-roam-completion-everywhere - (thing-at-point 'word)) - (let ((bounds (bounds-of-thing-at-point 'word))) - (setq start (car bounds) - end (cdr bounds) - collection #'org-roam--get-titles - exit-fn (lambda (str _status) - (delete-char (- (length str))) - (insert "[[roam:" str "]]"))))) - (when collection - (let ((prefix (buffer-substring-no-properties start end))) - (list start end - (if (functionp collection) - (completion-table-case-fold - (completion-table-dynamic - (lambda (_) - (cl-remove-if (apply-partially #'string= prefix) - (funcall collection)))) - (not org-roam-completion-ignore-case)) - collection) - :exit-function exit-fn))))) +;;; Org-roam setup and teardown +(defvar org-roam-find-file-hook nil + "Hook run when an Org-roam file is visited.") -(add-to-list 'org-roam-completion-functions #'org-roam-complete-tags-at-point) -(add-to-list 'org-roam-completion-functions #'org-roam-complete-everywhere) -(add-to-list 'org-roam-completion-functions #'org-roam-link-complete-at-point) +;;;###autoload +(defun org-roam-setup () + "Setup Org-roam." + (interactive) + (add-hook 'find-file-hook #'org-roam--file-setup) + (add-hook 'kill-emacs-hook #'org-roam-db--close-all) + (advice-add 'rename-file :after #'org-roam--rename-file-advice) + (advice-add 'delete-file :before #'org-roam--delete-file-advice) + (org-roam-db-sync)) -;;; Org-roam-mode -;;;; Function Faces -;; These faces are used by `org-link-set-parameters', which take one argument, -;; which is the path. -(defcustom org-roam-link-use-custom-faces t - "Define where to apply custom faces to Org-roam links. +(defun org-roam-teardown () + "Teardown Org-roam." + (interactive) + (remove-hook 'find-file-hook #'org-roam--file-setup) + (remove-hook 'kill-emacs-hook #'org-roam-db--close-all) + (advice-remove 'rename-file #'org-roam--rename-file-advice) + (advice-remove 'delete-file #'org-roam--delete-file-advice) + (org-roam-db--close-all) + ;; Disable local hooks for all org-roam buffers + (dolist (buf (org-roam-buffer-list)) + (with-current-buffer buf + (remove-hook 'after-save-hook #'org-roam-db-update-file t)))) -Valide values are: - -t Use custom faces inside Org-roam notes (i.e. files in - `org-roam-directory'.) - -everywhere Apply custom faces everywhere. - -Otherwise, do not apply custom faces to Org-roam links." - :type '(choice - (const :tag "Use custom faces inside Org-roam notes" t) - (const :tag "Apply custom faces everywhere" everywhere) - (const :tag "Do not apply custom faces" nil)) - :group 'org-roam) - -(defun org-roam--file-link-face (path) - "Conditional face for file: links. -Applies `org-roam-link-current' if PATH corresponds to the -currently opened Org-roam file in the backlink buffer, or -`org-roam-link-face' if PATH corresponds to any other Org-roam -file." - (save-match-data - (let* ((in-note (-> (buffer-file-name (buffer-base-buffer)) - (org-roam--org-roam-file-p))) - (custom (or (and in-note org-roam-link-use-custom-faces) - (eq org-roam-link-use-custom-faces 'everywhere)))) - (cond ((and custom - (not (file-remote-p path)) ;; Prevent lockups opening Tramp links - (not (file-exists-p path))) - 'org-roam-link-invalid) - ((and (org-roam--in-buffer-p) - (org-roam--backlink-to-current-p)) - 'org-roam-link-current) - ((and custom - (org-roam--org-roam-file-p path)) - 'org-roam-link) - (t - 'org-link))))) - -(defun org-roam--id-link-face (id) - "Conditional face for id links. -Applies `org-roam-link-current' if ID corresponds to the -currently opened Org-roam file in the backlink buffer, or -`org-roam-link-face' if ID corresponds to any other Org-roam -file." - (save-match-data - (let* ((in-note (-> (buffer-file-name (buffer-base-buffer)) - (org-roam--org-roam-file-p))) - (custom (or (and in-note org-roam-link-use-custom-faces) - (eq org-roam-link-use-custom-faces 'everywhere)))) - (cond ((and (org-roam--in-buffer-p) - (org-roam--backlink-to-current-p)) - 'org-roam-link-current) - ((and custom - (org-roam-id-get-file id t)) - 'org-roam-link) - ((and custom - (not (org-roam-id-get-file id))) - 'org-roam-link-invalid) - (t - 'org-link))))) - -;;;; Hooks and Advices -(defcustom org-roam-file-setup-hook nil - "Hook that is run on setting up an Org-roam file." - :group 'org-roam - :type 'hook) - -(defun org-roam--find-file-hook-function () - "Called by `find-file-hook' when mode symbol `org-roam-mode' is on." - (when (org-roam--org-roam-file-p) - (setq org-roam-last-window (get-buffer-window)) - (run-hooks 'org-roam-file-setup-hook) ; Run user hooks - (org-roam--setup-title-auto-update) - (add-hook 'post-command-hook #'org-roam-buffer--update-maybe nil t) - (add-hook 'before-save-hook #'org-roam-link--replace-link-on-save nil t) - (add-hook 'after-save-hook #'org-roam-db-update nil t) - (dolist (fn org-roam-completion-functions) - (add-hook 'completion-at-point-functions fn nil t)) - (org-roam-buffer--update-maybe :redisplay t))) +;;; Hooks and advices +(defun org-roam--file-setup () + "Setup an Org-roam file." + (when (org-roam-file-p) + (run-hooks 'org-roam-find-file-hook))) (defun org-roam--delete-file-advice (file &optional _trash) - "Advice for maintaining cache consistency when FILE is deleted." + "Maintain cache consistency when file deletes. +FILE is removed from the database." (when (and (not (auto-save-file-name-p file)) - (org-roam--org-roam-file-p file)) - (org-roam-db--clear-file (expand-file-name file)))) - -(defun org-roam--get-link-replacement (old-path new-path &optional old-desc new-desc) - "Create replacement text for link at point if OLD-PATH is a match. -Will update link to NEW-PATH. If OLD-DESC is set, and is not the -same as the link description, it is assumed that the user has -modified the description, and the description will not be -updated. Else, update with NEW-DESC." - (let (type path link-type label new-label) - (when-let ((link (org-element-lineage (org-element-context) '(link) t))) - (setq type (org-element-property :type link) - path (org-element-property :path link)) - (when (and (string-equal (expand-file-name path) old-path) - (org-in-regexp org-link-bracket-re 1)) - (setq link-type (when (file-name-absolute-p path) 'absolute) - label (if (match-end 2) - (match-string-no-properties 2) - (org-link-unescape (match-string-no-properties 1)))) - (setq new-label (if (string-equal label old-desc) new-desc label)) - (org-roam-format-link new-path new-label type link-type))))) - -(defun org-roam--replace-link (old-path new-path &optional old-desc new-desc) - "Replace Org-roam file links with path OLD-PATH to path NEW-PATH. -If OLD-DESC is passed, and is not the same as the link -description, it is assumed that the user has modified the -description, and the description will not be updated. Else, -update with NEW-DESC." - (org-with-point-at 1 - (while (re-search-forward org-link-bracket-re nil t) - (when-let ((link (save-match-data (org-roam--get-link-replacement old-path new-path old-desc new-desc)))) - (replace-match link))))) - -(defun org-roam--fix-relative-links (old-path) - "Fix file-relative links in current buffer. -File relative links are assumed to originate from OLD-PATH. The -replaced links are made relative to the current buffer." - (org-with-point-at 1 - (let (link new-link type path) - (while (re-search-forward org-link-bracket-re nil t) - (when (setq link (save-match-data (org-element-lineage (org-element-context) '(link) t))) - (setq type (org-element-property :type link)) - (setq path (org-element-property :path link)) - (when (and (string= type "file") - (f-relative-p path)) - (setq new-link - (concat type ":" (org-roam-link-get-path (expand-file-name path (file-name-directory old-path))))) - (replace-match new-link nil t nil 1))))))) - -(defcustom org-roam-rename-file-on-title-change t - "If non-nil, alter the filename on title change. -The new title is converted into a slug using -`org-roam-title-to-slug-function', and compared with the current -filename." - :group 'org-roam - :type 'boolean) - -(defcustom org-roam-title-change-hook '(org-roam--update-file-name-on-title-change - org-roam--update-links-on-title-change) - "Hook run after detecting a title change. -Each hook is passed two arguments: the old title, and new title -respectively." - :group 'org-roam - :type 'hook) - -(defvar-local org-roam-current-title nil - "The current title of the Org-roam file.") - -(defun org-roam--handle-title-change () - "Detect a title change, and run `org-roam-title-change-hook'." - (let ((new-title (car (org-roam--extract-titles))) - (old-title org-roam-current-title)) - (unless (or (eq old-title nil) - (string-equal old-title new-title)) - (run-hook-with-args 'org-roam-title-change-hook old-title new-title)) - (setq-local org-roam-current-title new-title))) - -(defun org-roam--setup-title-auto-update () - "Setup automatic link description update on title change." - (setq-local org-roam-current-title (car (org-roam--extract-titles))) - (add-hook 'after-save-hook #'org-roam--handle-title-change nil t)) - -(defun org-roam--update-links-on-title-change (old-title new-title) - "Update the link description of other Org-roam files. -Iterate over all Org-roam files that have link description of -OLD-TITLE, and replace the link descriptions with the NEW-TITLE -if applicable. - -To be added to `org-roam-title-change-hook'." - (let* ((current-path (buffer-file-name (buffer-base-buffer))) - (files-affected (org-roam-db-query [:select :distinct [source] - :from links - :where (= dest $s1)] - current-path))) - (dolist (file files-affected) - (org-roam-with-file (car file) nil - (org-roam--replace-link current-path current-path old-title new-title))))) - -(defun org-roam--update-file-name-on-title-change (old-title new-title) - "Update the file name on title change. -The slug is computed from OLD-TITLE using -`org-roam-title-to-slug-function'. If the slug is part of the -current filename, the new slug is computed with NEW-TITLE, and -that portion of the filename is renamed. - -To be added to `org-roam-title-change-hook'." - (org-roam--save-buffers) - (when org-roam-rename-file-on-title-change - (let* ((old-slug (funcall org-roam-title-to-slug-function old-title)) - (file (buffer-file-name (buffer-base-buffer))) - (file-name (file-name-nondirectory file))) - (when (string-match-p old-slug file-name) - (let* ((new-slug (funcall org-roam-title-to-slug-function new-title)) - (new-file-name (replace-regexp-in-string old-slug new-slug file-name))) - (unless (string-equal file-name new-file-name) - (rename-file file-name new-file-name) - (set-visited-file-name new-file-name t t) - (org-roam-db-update) - (org-roam-message "File moved to %S" (abbreviate-file-name new-file-name)))))))) + (not (backup-file-name-p file)) + (org-roam-file-p file)) + (org-roam-db-clear-file (expand-file-name file)))) (defun org-roam--rename-file-advice (old-file new-file-or-dir &rest _args) - "Rename backlinks of OLD-FILE to refer to NEW-FILE-OR-DIR. -When NEW-FILE-OR-DIR is a directory, we use it to compute the new file path." + "Maintain cache consistency of file rename. +OLD-FILE is cleared from the database, and NEW-FILE-OR-DIR is added." (let ((new-file (if (directory-name-p new-file-or-dir) (expand-file-name (file-name-nondirectory old-file) new-file-or-dir) - new-file-or-dir)) - files-affected) + new-file-or-dir))) (setq new-file (expand-file-name new-file)) (setq old-file (expand-file-name old-file)) (when (and (not (auto-save-file-name-p old-file)) (not (auto-save-file-name-p new-file)) (not (backup-file-name-p old-file)) (not (backup-file-name-p new-file)) - (org-roam--org-roam-file-p old-file)) - (org-roam-db--ensure-built) - (setq files-affected (org-roam-db-query [:select :distinct [source] - :from links - :where (= dest $s1)] - old-file)) - ;; Remove database entries for old-file.org - (org-roam-db--clear-file old-file) - ;; If the new path is in a different directory, relative links - ;; will break. Fix all file-relative links: - (unless (string= (file-name-directory old-file) - (file-name-directory new-file)) - (org-roam-with-file new-file nil - (org-roam--fix-relative-links old-file))) - (when (org-roam--org-roam-file-p new-file) - (org-roam-db--update-file new-file)) - ;; Replace links from old-file.org -> new-file.org in all Org-roam files with these links - (mapc (lambda (file) - (setq file (if (string-equal (car file) old-file) - new-file - (car file))) - (org-roam-with-file file nil - (org-roam--replace-link old-file new-file) - (save-buffer) - (org-roam-db--update-file))) - files-affected)))) + (org-roam-file-p old-file)) + (org-roam-db-clear-file old-file)) + (when (org-roam-file-p new-file) + (org-roam-db-update-file new-file)))) -(defun org-roam--id-new-advice (&rest _args) - "Update the database if a new Org ID is created." - (when (and org-roam-enable-headline-linking - (org-roam--org-roam-file-p) - (not (eq org-roam-db-update-method 'immediate)) - (not (org-roam-capture-p))) - (org-roam-db-update))) +;;;; Nodes +(cl-defstruct (org-roam-node (:constructor org-roam-node-create) + (:copier nil)) + file file-hash file-atime file-mtime + id level point todo priority scheduled deadline title properties olp + tags aliases refs) -;;;###autoload -(define-minor-mode org-roam-mode - "Minor mode for Org-roam. +(cl-defmethod org-roam-node-slug ((node org-roam-node)) + "Return the slug of NODE." + (let ((title (org-roam-node-title node)) + (slug-trim-chars '(;; Combining Diacritical Marks https://www.unicode.org/charts/PDF/U0300.pdf + 768 ; U+0300 COMBINING GRAVE ACCENT + 769 ; U+0301 COMBINING ACUTE ACCENT + 770 ; U+0302 COMBINING CIRCUMFLEX ACCENT + 771 ; U+0303 COMBINING TILDE + 772 ; U+0304 COMBINING MACRON + 774 ; U+0306 COMBINING BREVE + 775 ; U+0307 COMBINING DOT ABOVE + 776 ; U+0308 COMBINING DIAERESIS + 777 ; U+0309 COMBINING HOOK ABOVE + 778 ; U+030A COMBINING RING ABOVE + 780 ; U+030C COMBINING CARON + 795 ; U+031B COMBINING HORN + 803 ; U+0323 COMBINING DOT BELOW + 804 ; U+0324 COMBINING DIAERESIS BELOW + 805 ; U+0325 COMBINING RING BELOW + 807 ; U+0327 COMBINING CEDILLA + 813 ; U+032D COMBINING CIRCUMFLEX ACCENT BELOW + 814 ; U+032E COMBINING BREVE BELOW + 816 ; U+0330 COMBINING TILDE BELOW + 817 ; U+0331 COMBINING MACRON BELOW + ))) + (cl-flet* ((nonspacing-mark-p (char) + (memq char slug-trim-chars)) + (strip-nonspacing-marks (s) + (ucs-normalize-NFC-string + (apply #'string (seq-remove #'nonspacing-mark-p + (ucs-normalize-NFD-string s))))) + (cl-replace (title pair) + (replace-regexp-in-string (car pair) (cdr pair) title))) + (let* ((pairs `(("[^[:alnum:][:digit:]]" . "_") ;; convert anything not alphanumeric + ("__*" . "_") ;; remove sequential underscores + ("^_" . "") ;; remove starting underscore + ("_$" . ""))) ;; remove ending underscore + (slug (-reduce-from #'cl-replace (strip-nonspacing-marks title) pairs))) + (downcase slug))))) -This mode sets up several hooks, to ensure that the cache is updated on file -changes, renames and deletes. It is also in charge of graceful termination of -the database connection. +(defvar org-roam-node-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map org-roam-mode-map) + (define-key map [remap org-roam-visit-thing] 'org-roam-node-visit) + map) + "Keymap for Org-roam node sections.") -When called interactively, toggle `org-roam-mode'. with prefix -ARG, enable `org-roam-mode' if ARG is positive, otherwise disable -it. +(defclass org-roam-node-section (magit-section) + ((keymap :initform 'org-roam-node-map) + (node :initform nil))) -When called from Lisp, enable `org-roam-mode' if ARG is omitted, -nil, or positive. If ARG is `toggle', toggle `org-roam-mode'. -Otherwise, behave as if called interactively." - :lighter " Org-roam" - :keymap (let ((map (make-sparse-keymap))) - map) +(defvar org-roam-preview-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map org-roam-mode-map) + (define-key map [remap org-roam-visit-thing] 'org-roam-preview-visit) + map) + "Keymap for Org-roam preview.") + +(defclass org-roam-preview-section (magit-section) + ((keymap :initform 'org-roam-preview-map) + (file :initform nil) + (begin :initform nil) + (end :initform nil))) + +(cl-defmethod org-roam-populate ((node org-roam-node)) + "Populate NODE from database. +Uses the ID, and fetches remaining details from the database. +This can be quite costly: avoid, unless dealing with very few +nodes." + (when-let ((node-info (car (org-roam-db-query [:select [file level pos todo priority + scheduled deadline title properties olp] + :from nodes + :where (= id $s1) + :limit 1] + (org-roam-node-id node))))) + (pcase-let* ((`(,file ,level ,pos ,todo ,priority ,scheduled ,deadline ,title ,properties ,olp) node-info) + (`(,atime ,mtime) (car (org-roam-db-query [:select [atime mtime] + :from files + :where (= file $s1)] + file))) + (tag-info (mapcar #'car (org-roam-db-query [:select [tag] :from tags + :where (= node-id $s1)] + (org-roam-node-id node)))) + (alias-info (mapcar #'car (org-roam-db-query [:select [alias] :from aliases + :where (= node-id $s1)] + (org-roam-node-id node)))) + (refs-info (mapcar #'car (org-roam-db-query [:select [ref] :from refs + :where (= node-id $s1)] + (org-roam-node-id node))))) + (setf (org-roam-node-file node) file + (org-roam-node-file-atime node) atime + (org-roam-node-file-mtime node) mtime + (org-roam-node-level node) level + (org-roam-node-point node) pos + (org-roam-node-todo node) todo + (org-roam-node-priority node) priority + (org-roam-node-scheduled node) scheduled + (org-roam-node-deadline node) deadline + (org-roam-node-title node) title + (org-roam-node-properties node) properties + (org-roam-node-olp node) olp + (org-roam-node-tags node) tag-info + (org-roam-node-refs node) refs-info + (org-roam-node-aliases node) alias-info))) + node) + +(defcustom org-roam-node-display-template + "${title:*} ${tags:10}" + "Configures display formatting for Org-roam node. +Patterns of form \"${field-name:length}\" are interpolated based +on the current node. \"field-name\" is replaced with the +corresponding value of the field of the current node. \"length\" +specifies how many characters are used to display the value of +the field. A \"length\" of \"*\" specifies that as many +characters as possible should be used." :group 'org-roam - :require 'org-roam - :global t - (cond - (org-roam-mode - (unless (or (and (bound-and-true-p emacsql-sqlite3-executable) - (file-executable-p emacsql-sqlite3-executable)) - (executable-find "sqlite3")) - (lwarn '(org-roam) :error "Cannot find executable 'sqlite3'. \ -Ensure it is installed and can be found within `exec-path'. \ -M-x info for more information at Org-roam > Installation > Post-Installation Tasks.")) - (add-to-list 'org-execute-file-search-functions 'org-roam--execute-file-row-col) - (add-hook 'find-file-hook #'org-roam--find-file-hook-function) - (add-hook 'kill-emacs-hook #'org-roam-db--close-all) - (add-hook 'org-open-at-point-functions #'org-roam-open-id-at-point) - (when (and (not org-roam-db-file-update-timer) - (eq org-roam-db-update-method 'idle-timer)) - (setq org-roam-db-file-update-timer (run-with-idle-timer org-roam-db-update-idle-seconds t #'org-roam-db-update-cache-on-timer))) - (advice-add 'rename-file :after #'org-roam--rename-file-advice) - (advice-add 'delete-file :before #'org-roam--delete-file-advice) - (advice-add 'org-id-new :after #'org-roam--id-new-advice) - (when (fboundp 'org-link-set-parameters) - (org-link-set-parameters "file" :face 'org-roam--file-link-face) - (org-link-set-parameters "id" :face 'org-roam--id-link-face)) - (dolist (buf (org-roam--get-roam-buffers)) - (with-current-buffer buf - (add-hook 'post-command-hook #'org-roam-buffer--update-maybe nil t) - (add-hook 'before-save-hook #'org-roam-link--replace-link-on-save nil t) - (add-hook 'after-save-hook #'org-roam-db-update nil t))) - (org-roam-db-build-cache)) - (t - (setq org-execute-file-search-functions (delete 'org-roam--execute-file-row-col org-execute-file-search-functions)) - (remove-hook 'find-file-hook #'org-roam--find-file-hook-function) - (remove-hook 'kill-emacs-hook #'org-roam-db--close-all) - (remove-hook 'org-open-at-point-functions #'org-roam-open-id-at-point) - (when org-roam-db-file-update-timer - (cancel-timer org-roam-db-file-update-timer)) - (advice-remove 'rename-file #'org-roam--rename-file-advice) - (advice-remove 'delete-file #'org-roam--delete-file-advice) - (advice-remove 'org-id-new #'org-roam--id-new-advice) - (when (fboundp 'org-link-set-parameters) - (dolist (face '("file" "id")) - (org-link-set-parameters face :face 'org-link))) - (org-roam-db--close-all) - ;; Disable local hooks for all org-roam buffers - (dolist (buf (org-roam--get-roam-buffers)) - (with-current-buffer buf - (remove-hook 'post-command-hook #'org-roam-buffer--update-maybe t) - (remove-hook 'before-save-hook #'org-roam-link--replace-link-on-save t) - (remove-hook 'after-save-hook #'org-roam-db-update t)))))) + :type 'string) -;;; Interactive Commands -;;;###autoload -(defalias 'org-roam 'org-roam-buffer-toggle-display) +(defun org-roam--tags-to-str (tags) + "Convert list of TAGS into a string." + (mapconcat (lambda (s) (concat "#" s)) tags " ")) + +(defun org-roam-node--format-entry (node width) + "Formats NODE for display in the results list. +WIDTH is the width of the results list. +Uses `org-roam-node-display-template' to format the entry." + (let ((fmt (org-roam--process-display-format org-roam-node-display-template))) + (org-roam-format + (car fmt) + (lambda (field) + (let* ((field (split-string field ":")) + (field-name (car field)) + (field-width (cadr field)) + (getter (intern (concat "org-roam-node-" field-name))) + (field-value (or (funcall getter node) ""))) + (when (and (equal field-name "tags") + field-value) + (setq field-value (org-roam--tags-to-str field-value))) + (when (and (equal field-name "file") + field-value) + (setq field-value (file-relative-name field-value org-roam-directory))) + (when (and (equal field-name "olp") + field-value) + (setq field-value (string-join field-value " > "))) + (if (not field-width) + field-value + (setq field-width (string-to-number field-width)) + (truncate-string-to-width + field-value + (if (> field-width 0) + field-width + (- width (cdr fmt))) + 0 ?\s))))))) + +(defun org-roam-node-preview (file point) + "Get preview content for FILE at POINT." + (save-excursion + (org-roam-with-temp-buffer file + (goto-char point) + (let* ((elem (org-element-at-point)) + (begin (org-element-property :begin elem)) + (end (org-element-property :end elem))) + (list begin end + (or (string-trim (buffer-substring-no-properties begin end)) + (org-element-property :raw-value elem))))))) + +(defun org-roam-node-at-point (&optional assert) + "Return the node at point. +If ASSERT, throw an error if there is no node at point. +This function also returns the node if it has yet to be cached in the +database. In this scenario, only expect `:id' and `:point' to be +populated." + (if-let ((node (magit-section-case + (org-roam-node-section (oref it node)) + (t (org-with-wide-buffer + (org-back-to-heading-or-point-min) + (while (and (not (org-roam-db-node-p)) + (not (bobp))) + (org-roam-up-heading-or-point-min)) + (when-let ((id (org-id-get))) + (org-roam-populate + (org-roam-node-create + :id id + :point (point))))))))) + node + (when assert + (user-error "No node at point")))) + +(defun org-roam-node--find (node) + "Navigate to the point for NODE, and return the buffer." + (unless (org-roam-node-file node) + (user-error "Node does not have corresponding file")) + (let ((buf (find-file-noselect (org-roam-node-file node)))) + (with-current-buffer buf + (goto-char (org-roam-node-point node))) + buf)) + +(defun org-roam-node-visit (node &optional other-window) + "From the buffer, visit NODE. + +Display the buffer in the selected window. With a prefix +argument OTHER-WINDOW display the buffer in another window +instead." + (interactive (list (org-roam-node-at-point t) current-prefix-arg)) + (let ((buf (org-roam-node--find node))) + (funcall (if other-window + #'switch-to-buffer-other-window + #'pop-to-buffer-same-window) buf))) + +(defun org-roam-node-from-id (id) + "Return an `org-roam-node' for the node containing ID. +Return nil if a node with ID does not exist." + (when (> (caar (org-roam-db-query [:select (funcall count) :from nodes + :where (= id $s1)] + id)) 0) + (org-roam-populate (org-roam-node-create :id id)))) + +(defun org-roam-node-from-title-or-alias (s) + "Return an `org-roam-node' for the node with title or alias S. +Return nil if the node does not exist. +Throw an error if multiple choices exist." + (let ((matches (seq-uniq + (append + (org-roam-db-query [:select [id] :from nodes + :where (= title $s1)] + s) + (org-roam-db-query [:select [node-id] :from aliases + :where (= alias $s1)] + s))))) + (cond + ((seq-empty-p matches) + nil) + ((= 1 (length matches)) + (org-roam-populate (org-roam-node-create :id (caar matches)))) + (t + (user-error "Multiple nodes exist with title or alias \"%s\"" s))))) + +(defun org-roam-node-list () + "Return a list of all nodes." + (let ((rows (org-roam-db-query + "SELECT + id, + file, + \"level\", + todo, + pos, + priority , + scheduled , + deadline , + title, + properties , + olp, + atime, + mtime, + '(' || group_concat(tags, ' ') || ')' as tags, + aliases, + refs +FROM + ( + SELECT + id, + file, + \"level\", + todo, + pos, + priority , + scheduled , + deadline , + title, + properties , + olp, + atime, + mtime, + tags, + '(' || group_concat(aliases, ' ') || ')' as aliases, + refs + FROM + ( + SELECT + nodes.id as id, + nodes.file as file, + nodes.\"level\" as \"level\", + nodes.todo as todo, + nodes.pos as pos, + nodes.priority as priority, + nodes.scheduled as scheduled, + nodes.deadline as deadline, + nodes.title as title, + nodes.properties as properties, + nodes.olp as olp, + files.atime as atime, + files.mtime as mtime, + tags.tag as tags, + aliases.alias as aliases, + '(' || group_concat(RTRIM (refs.\"type\", '\"') || ':' || LTRIM(refs.ref, '\"'), ' ') || ')' as refs + FROM nodes + LEFT JOIN files ON files.file = nodes.file + LEFT JOIN tags ON tags.node_id = nodes.id + LEFT JOIN aliases ON aliases.node_id = nodes.id + LEFT JOIN refs ON refs.node_id = nodes.id + GROUP BY nodes.id, tags.tag, aliases.alias ) + GROUP BY id, tags ) +GROUP BY id"))) + (cl-loop for row in rows + append (pcase-let* ((`(,id ,file ,level ,todo ,pos ,priority ,scheduled ,deadline + ,title ,properties ,olp ,atime ,mtime ,tags ,aliases ,refs) + row) + (all-titles (cons title aliases))) + (mapcar (lambda (temp-title) + (org-roam-node-create :id id + :file file + :file-atime atime + :file-mtime mtime + :level level + :point pos + :todo todo + :priority priority + :scheduled scheduled + :deadline deadline + :title temp-title + :properties properties + :olp olp + :tags tags + :refs refs)) + all-titles))))) + +(defun org-roam-node--to-candidate (node) + "Return a minibuffer completion candidate given NODE." + (let ((candidate-main (org-roam-node--format-entry node (1- (frame-width)))) + (tag-str (org-roam--tags-to-str (org-roam-node-tags node)))) + (cons (propertize (concat candidate-main + (propertize tag-str 'invisible t)) + 'node node) + node))) + +(defun org-roam-node--completions () + "Return an alist for node completion. +The car is the displayed title or alias for the node, and the cdr +is the `org-roam-node'. +The displayed title is formatted according to `org-roam-node-display-template'." + (setq org-roam--cached-display-format nil) + (let ((nodes (org-roam-node-list))) + (mapcar #'org-roam-node--to-candidate nodes))) + +(defcustom org-roam-node-annotation-function #'org-roam-node--annotation + "The function used to return annotations in the minibuffer for Org-roam nodes. +This function takes a single argument NODE, which is an `org-roam-node' construct." + :group 'org-roam + :type 'function) + +(defcustom org-roam-node-default-sort 'file-mtime + "Default sort order for Org-roam node completions." + :type 'const + :group 'org-roam) + +(defun org-roam-node-sort-by-file-mtime (completion-a completion-b) + "Sort files such that files modified more recently are shown first. +COMPLETION-A and COMPLETION-B are items in the form of (node-title org-roam-node-struct)" + (let ((node-a (cdr completion-a)) + (node-b (cdr completion-b))) + (time-less-p (org-roam-node-file-mtime node-b) + (org-roam-node-file-mtime node-a)))) + +(defun org-roam-node-sort-by-file-atime (completion-a completion-b) + "Sort files such that files accessed more recently are shown first. +COMPLETION-A and COMPLETION-B are items in the form of (node-title org-roam-node-struct)" + "Sort completions list by file modification time." + (let ((node-a (cdr completion-a)) + (node-b (cdr completion-b))) + (time-less-p (org-roam-node-file-atime node-b) + (org-roam-node-file-atime node-a)))) + +(defun org-roam-node-read (&optional initial-input filter-fn sort-fn require-match) + "Read and return an `org-roam-node'. +INITIAL-INPUT is the initial minibuffer prompt value. FILTER-FN +is a function to filter out nodes: it takes a single argument (an +`org-roam-node'), and when nil is returned the node will be +filtered out. +SORT-FN is a function to sort nodes. See `org-roam-node-sort-by-file-mtime' +for an example sort function. +If REQUIRE-MATCH, the minibuffer prompt will require a match." + (let* ((nodes (org-roam-node--completions)) + (nodes (cl-remove-if-not (lambda (n) + (if filter-fn (funcall filter-fn (cdr n)) t)) nodes)) + (sort-fn (or sort-fn + (when org-roam-node-default-sort + (intern (concat "org-roam-node-sort-by-" (symbol-name org-roam-node-default-sort)))))) + (_ (when sort-fn (setq nodes (seq-sort sort-fn nodes)))) + (node (completing-read + "Node: " + (lambda (string pred action) + (if (eq action 'metadata) + '(metadata + (annotation-function . (lambda (title) + (funcall org-roam-node-annotation-function + (get-text-property 0 'node title)))) + (category . org-roam-node)) + (complete-with-action action nodes string pred))) + nil require-match initial-input))) + (or (cdr (assoc node nodes)) + (org-roam-node-create :title node)))) + +(defun org-roam-node--annotation (_node) + "Dummy function. +Returns empty string for annotations." + "") + +(defun org-roam-preview-visit (file point &optional other-window) + "Visit FILE at POINT. +With prefix argument OTHER-WINDOW, visit the olp in another +window instead." + (interactive (list (org-roam-file-at-point t) + (oref (magit-current-section) begin) + current-prefix-arg)) + (let ((buf (find-file-noselect file))) + (with-current-buffer buf + (widen) + (goto-char point)) + (funcall (if other-window + #'switch-to-buffer-other-window + #'pop-to-buffer-same-window) buf))) + +(cl-defun org-roam-node-insert-section (&key source-node point properties) + "Insert section for NODE. +SOURCE-NODE is the source node. +POINT is the point in buffer for the link. +PROPERTIES contains properties about the link." + (magit-insert-section section (org-roam-node-section) + (let ((outline (if-let ((outline (plist-get properties :outline))) + (mapconcat #'org-link-display-format outline " > ") + "Top"))) + (insert (concat (propertize (org-roam-node-title source-node) + 'font-lock-face 'org-roam-title) + (format " (%s)" + (propertize outline 'font-lock-face 'org-roam-olp))))) + (magit-insert-heading) + (oset section node source-node) + (magit-insert-section section (org-roam-preview-section) + (pcase-let ((`(,begin ,end ,s) (org-roam-node-preview (org-roam-node-file source-node) + point))) + (insert (org-roam-fontify-like-in-org-mode s) "\n") + (oset section file (org-roam-node-file source-node)) + (oset section begin begin) + (oset section end end)) + (insert ?\n)))) ;;;###autoload -(defun org-roam-diagnostics () - "Collect and print info for `org-roam' issues." - (interactive) - (with-current-buffer (switch-to-buffer-other-window (get-buffer-create "*org-roam diagnostics*")) - (erase-buffer) - (insert (propertize "Copy info below this line into issue:\n" 'face '(:weight bold))) - (insert (format "- Emacs: %s\n" (emacs-version))) - (insert (format "- Framework: %s\n" - (condition-case _ - (completing-read "I'm using the following Emacs framework:" - '("Doom" "Spacemacs" "N/A" "I don't know")) - (quit "N/A")))) - (insert (format "- Org: %s\n" (org-version nil 'full))) - (insert (format "- Org-roam: %s" (org-roam-version))))) - -;;;###autoload -(defun org-roam-find-file (&optional initial-prompt completions filter-fn no-confirm) - "Find and open an Org-roam file. -INITIAL-PROMPT is the initial title prompt. -COMPLETIONS is a list of completions to be used instead of -`org-roam--get-title-path-completions`. +(defun org-roam-node-find (&optional other-window initial-input filter-fn) + "Find and open an Org-roam node by its title or alias. +INITIAL-INPUT is the initial input for the prompt. FILTER-FN is the name of a function to apply on the candidates -which takes as its argument an alist of path-completions. See -`org-roam--get-title-path-completions' for details. -If NO-CONFIRM, assume that the user does not want to modify the initial prompt." - (interactive) - (unless org-roam-mode (org-roam-mode)) - (let* ((completions (funcall (or filter-fn #'identity) - (or completions (org-roam--get-title-path-completions)))) - (title-with-tags (if no-confirm - initial-prompt - (org-roam-completion--completing-read "File: " completions - :initial-input initial-prompt))) - (res (cdr (assoc title-with-tags completions))) - (file-path (plist-get res :path))) - (if file-path - (org-roam--find-file file-path) - (let ((org-roam-capture--info `((title . ,title-with-tags) - (slug . ,(funcall org-roam-title-to-slug-function title-with-tags)))) - (org-roam-capture--context 'title)) - (setq org-roam-capture-additional-template-props (list :finalize 'find-file)) - (org-roam-capture--capture))))) +which takes as its argument an alist of path-completions. +If OTHER-WINDOW, visit the NODE in another window." + (interactive current-prefix-arg) + (let ((node (org-roam-node-read initial-input filter-fn))) + (if (org-roam-node-file node) + (org-roam-node-visit node other-window) + (org-roam-capture- + :node node + :props '(:finalize find-file))))) ;;;###autoload -(defun org-roam-find-directory () - "Find and open `org-roam-directory'." - (interactive) - (org-roam--find-file org-roam-directory)) - -;;;###autoload -(defun org-roam-find-ref (arg &optional filter) - "Find and open an Org-roam file from a ref. -ARG is used to forward interactive calls to -`org-roam--get-ref-path-completions' -FILTER can either be a string or a function: -- If it is a string, it should be the type of refs to include as -candidates (e.g. \"cite\" ,\"website\" ,etc.) -- If it is a function, it should be the name of a function that -takes three arguments: the type, the ref, and the file of the -current candidate. It should return t if that candidate is to be -included as a candidate." - (interactive "p") - (unless org-roam-mode (org-roam-mode)) - (let* ((completions (org-roam--get-ref-path-completions arg filter)) - (ref (org-roam-completion--completing-read "Ref: " - completions - :require-match t)) - (file (-> (cdr (assoc ref completions)) - (plist-get :path)))) - (org-roam--find-file file))) - -;;;###autoload -(defun org-roam-random-note () - "Find a random Org-roam file." - (interactive) - (find-file (seq-random-elt (org-roam--list-all-files)))) - -;;;###autoload -(defun org-roam-insert (&optional lowercase completions filter-fn description link-type) +(defun org-roam-node-insert (&optional filter-fn) "Find an Org-roam file, and insert a relative org link to it at point. Return selected file if it exists. If LOWERCASE is non-nil, downcase the link description. -LINK-TYPE is the type of link to be created. It defaults to \"file\". -COMPLETIONS is a list of completions to be used instead of -`org-roam--get-title-path-completions`. FILTER-FN is the name of a function to apply on the candidates -which takes as its argument an alist of path-completions. -If DESCRIPTION is provided, use this as the link label. See -`org-roam--get-title-path-completions' for details." - (interactive "P") - (unless org-roam-mode (org-roam-mode)) - ;; Deactivate the mark on quit since `atomic-change-group' prevents it +which takes as its argument an alist of path-completions." + (interactive) (unwind-protect ;; Group functions together to avoid inconsistent state on quit (atomic-change-group @@ -1681,265 +889,356 @@ If DESCRIPTION is provided, use this as the link label. See (setq beg (set-marker (make-marker) (region-beginning))) (setq end (set-marker (make-marker) (region-end))) (setq region-text (org-link-display-format (buffer-substring-no-properties beg end))))) - (completions (--> (or completions - (org-roam--get-title-path-completions)) - (if filter-fn - (funcall filter-fn it) - it))) - (title-with-tags (org-roam-completion--completing-read "File: " completions - :initial-input region-text)) - (res (cdr (assoc title-with-tags completions))) - (title (or (plist-get res :title) - title-with-tags)) - (target-file-path (plist-get res :path)) - (description (or description region-text title)) - (description (if lowercase - (downcase description) - description))) - (cond ((and target-file-path - (file-exists-p target-file-path)) - (when region-text - (delete-region beg end) - (set-marker beg nil) - (set-marker end nil)) - (insert (org-roam-format-link target-file-path description link-type))) - (t - (let ((org-roam-capture--info `((title . ,title-with-tags) - (slug . ,(funcall org-roam-title-to-slug-function title-with-tags)))) - (org-roam-capture--context 'title)) - (setq org-roam-capture-additional-template-props (list :region (org-roam-shield-region beg end) - :insert-at (point-marker) - :link-type link-type - :link-description description - :finalize 'insert-link)) - (org-roam-capture--capture)))) - res)) + (node (org-roam-node-read region-text filter-fn)) + (description (or region-text + (org-roam-node-title node)))) + (if (org-roam-node-id node) + (progn + (when region-text + (delete-region beg end) + (set-marker beg nil) + (set-marker end nil)) + (insert (org-link-make-string + (concat "id:" (org-roam-node-id node)) + description))) + (org-roam-capture- + :node node + :props (append + (when (and beg end) + (list :region (cons beg end))) + (list :insert-at (point-marker) + :link-description description + :finalize 'insert-link)))))) (deactivate-mark))) ;;;###autoload -(defun org-roam-insert-immediate (arg &rest args) - "Find an Org-roam file, and insert a relative org link to it at point. -This variant of `org-roam-insert' inserts the link immediately by -using the template in `org-roam-capture-immediate-template'. The -interactive ARG and ARGS are passed to `org-roam-insert'. -See `org-roam-insert' for details." - (interactive "P") - (let ((args (push arg args)) - (org-roam-capture-templates (list org-roam-capture-immediate-template))) - (apply #'org-roam-insert args))) +(defun org-roam-node-random (&optional other-window) + "Find a random Org-roam node. +With prefix argument OTHER-WINDOW, visit the node in another +window instead." + (interactive current-prefix-arg) + (let ((random-row (seq-random-elt (org-roam-db-query [:select [id file pos] :from nodes])))) + (org-roam-node-visit (org-roam-node-create :id (nth 0 random-row) + :file (nth 1 random-row) + :point (nth 2 random-row)) + other-window))) + +;;;; Properties +(defun org-roam-add-property (s prop) + "Add S to property PROP." + (let* ((p (org-entry-get (point) prop)) + (lst (when p (split-string-and-unquote p))) + (lst (if (memq s lst) lst (cons s lst))) + (lst (seq-uniq lst))) + (org-set-property prop (combine-and-quote-strings lst)) + s)) + +(defun org-roam-remove-property (prop &optional s) + "Remove S from property PROP. + +If S is not specified, user is prompted to select a value." + (let* ((p (org-entry-get (point) prop)) + (lst (when p (split-string-and-unquote p))) + (prop-to-remove (or s (completing-read "Remove: " lst))) + (lst (delete prop-to-remove lst))) + (if lst + (org-set-property prop (combine-and-quote-strings lst)) + (org-delete-property prop)) + prop-to-remove)) + +(defun org-roam-set-keyword (key value) + "Set keyword KEY to VALUE. +If the property is already set, it's value is replaced." + (org-with-point-at 1 + (let ((case-fold-search t)) + (if (re-search-forward (concat "^#\\+" key ":\\(.*\\)") (point-max) t) + (if (string-blank-p value) + (kill-whole-line) + (replace-match (concat " " value) 'fixedcase nil nil 1)) + (while (and (not (eobp)) + (looking-at "^[#:]")) + (if (save-excursion (end-of-line) (eobp)) + (progn + (end-of-line) + (insert "\n")) + (forward-line) + (beginning-of-line))) + (insert "#+" key ": " value "\n"))))) + +;;;; Tags +(defun org-roam-tag-completions () + "Return list of tags for completions within Org-roam." + (let ((roam-tags (mapcar #'car (org-roam-db-query [:select :distinct [tag] :from tags]))) + (org-tags (cl-loop for tagg in org-tag-alist + nconc (pcase tagg + ('(:newline) + nil) + (`(,tag . ,_) + (list tag)) + (_ nil))))) + (seq-uniq (append roam-tags org-tags)))) + +(defun org-roam-tag-add (tags) + "Add TAGS to the node at point." + (interactive + (list (completing-read-multiple "Tag: " (org-roam-tag-completions)))) + (let ((node (org-roam-node-at-point 'assert))) + (save-excursion + (goto-char (org-roam-node-point node)) + (if (= (org-outline-level) 0) + (let ((current-tags (split-string (or (cadr (assoc "FILETAGS" + (org-collect-keywords '("filetags")))) + "")))) + (org-roam-set-keyword "filetags" (string-join (seq-uniq (append tags current-tags)) " "))) + (org-set-tags (seq-uniq (append tags (org-get-tags))))) + tags))) + +(defun org-roam-tag-remove (&optional tags) + "Remove TAGS from the node at point." + (interactive) + (let ((node (org-roam-node-at-point 'assert))) + (save-excursion + (goto-char (org-roam-node-point node)) + (if (= (org-outline-level) 0) + (let* ((current-tags (split-string (or (cadr (assoc "FILETAGS" + (org-collect-keywords '("filetags")))) + (user-error "No tag to remove")))) + (tags (or tags (completing-read-multiple "Tag: " current-tags)))) + (org-roam-set-keyword "filetags" + (string-join (seq-difference current-tags tags #'string-equal) " "))) + (let* ((current-tags (or (org-get-tags) + (user-error "No tag to remove"))) + (tags (completing-read-multiple "Tag: " current-tags))) + (org-set-tags (seq-difference current-tags tags #'string-equal)))) + tags))) + +;;;; Aliases +(defun org-roam-alias-add (alias) + "Add ALIAS to the node at point." + (interactive "sAlias: ") + (let ((node (org-roam-node-at-point 'assert))) + (save-excursion + (goto-char (org-roam-node-point node)) + (org-roam-add-property alias "ROAM_ALIASES")))) + +(defun org-roam-alias-remove (&optional alias) + "Remove an ALIAS from the node at point." + (interactive) + (let ((node (org-roam-node-at-point 'assert))) + (save-excursion + (goto-char (org-roam-node-point node)) + (org-roam-remove-property "ROAM_ALIASES" alias)))) + +;;;; Refs +(defun org-roam-ref-add (ref) + "Add REF to the node at point." + (interactive "sRef: ") + (let ((node (org-roam-node-at-point 'assert))) + (save-excursion + (goto-char (org-roam-node-point node)) + (org-roam-add-property ref "ROAM_REFS")))) + +(defun org-roam-ref-remove (&optional ref) + "Remove a REF from the node at point." + (interactive) + (let ((node (org-roam-node-at-point 'assert))) + (save-excursion + (goto-char (org-roam-node-point node)) + (org-roam-remove-property "ROAM_REFS" ref)))) + +(defun org-roam-ref--completions () + "Return an alist for ref completion. +The car is the ref, and the cdr is the corresponding node for the ref." + nil + (let ((rows (org-roam-db-query + [:select [id ref type nodes:file pos title] + :from refs + :left-join nodes + :on (= refs:node-id nodes:id)]))) + (cl-loop for row in rows + collect (pcase-let* ((`(,id ,ref ,type ,file ,pos ,title) row) + (node (org-roam-node-create :id id + :file file + :point pos + :title title))) + (cons (propertize ref 'node node 'type type) + node))))) + +(defun org-roam-ref-read (&optional initial-input filter-fn) + "Read an Org-roam ref. +Return a string, is propertized in `meta' with additional properties. +INITIAL-INPUT is the initial prompt value. +FILTER-FN is a function applied to the completion list." + (let* ((refs (org-roam-ref--completions)) + (refs (funcall (or filter-fn #'identity) refs)) + (ref (completing-read "Ref: " + (lambda (string pred action) + (if (eq action 'metadata) + '(metadata + (annotation-function . org-roam-ref--annotation) + (category . org-roam-ref)) + (complete-with-action action refs string pred))) + nil t initial-input))) + (cdr (assoc ref refs)))) + +(defun org-roam-ref--annotation (ref) + "Return the annotation for REF. +REF is assumed to be a propertized string." + (let* ((node (get-text-property 0 'node ref)) + (title (org-roam-node-title node))) + (when title + (concat " " title)))) ;;;###autoload -(defun org-roam-find-file-immediate (arg &rest args) - "Find and open an Org-roam file. -This variant of `org-roam-find-file' uses the template in -`org-roam-capture-immediate-template', avoiding the capture -process. The interactive ARG and ARGS are passed to -`org-roam-find-file'. See `org-roam-find-file' for details." - (interactive "P") - (let ((args (push arg args)) - (org-roam-capture-templates (list org-roam-capture-immediate-template))) - (apply #'org-roam-find-file args))) - -;;;###autoload -(defun org-roam-jump-to-index () - "Find the index file in `org-roam-directory'. -The path to the index can be defined in `org-roam-index-file'. -Otherwise, the function will look in your `org-roam-directory' -for a note whose title is 'Index'. If it does not exist, the -command will offer you to create one." +(defun org-roam-ref-find (&optional initial-input filter-fn) + "Find and open and Org-roam file from REF if it exists. +REF should be the value of '#+roam_key:' without any +type-information (e.g. 'cite:'). +INITIAL-INPUT is the initial input to the prompt. +FILTER-FN is applied to the ref list to filter out candidates." (interactive) - (unless org-roam-mode (org-roam-mode)) - (let ((index (org-roam--get-index-path))) - (if (and index - (file-exists-p index)) - (org-roam--find-file index) - (when (y-or-n-p "Index file does not exist. Would you like to create it? ") - (org-roam-find-file "Index"))))) + (let* ((node (org-roam-ref-read initial-input filter-fn))) + (find-file (org-roam-node-file node)) + (goto-char (org-roam-node-point node)))) -;;;###autoload -(defun org-roam-alias-add () - "Add an alias to Org-roam file. +;;;; roam: link +(defcustom org-roam-link-auto-replace t + "When non-nil, replace Org-roam's roam links with file or id links whenever possible." + :group 'org-roam + :type 'boolean) -Return added alias." +;;; the roam: link +(org-link-set-parameters "roam" :follow #'org-roam-link-follow-link) + +(defun org-roam-link-replace-at-point (&optional link) + "Replace the roam: LINK at point with an id link." + (save-excursion + (save-match-data + (let* ((link (or link (org-element-context))) + (type (org-element-property :type link)) + (path (org-element-property :path link)) + node) + (goto-char (org-element-property :begin link)) + (when (and (org-in-regexp org-link-any-re 1) + (string-equal type "roam") + (setq node (org-roam-node-from-title-or-alias path))) + (replace-match (org-link-make-string + (concat "id:" (org-roam-node-id node)) + path))))))) + +(defun org-roam-link-replace-all () + "Replace all \"roam:\" links in buffer with \"id:\" links." (interactive) - (unless org-roam-mode (org-roam-mode)) - (let ((alias (read-string "Alias: " ))) - (when (string-empty-p alias) - (user-error "Alias can't be empty")) - (org-roam--set-global-prop - "roam_alias" - (combine-and-quote-strings - (seq-uniq (cons alias - (org-roam--extract-titles-alias))))) - (org-roam-db--update-file (buffer-file-name (buffer-base-buffer))) - alias)) + (org-with-point-at 1 + (while (re-search-forward org-link-bracket-re nil t) + (org-roam-link-replace-at-point)))) -;;;###autoload -(defun org-roam-alias-delete () - "Delete an alias from Org-roam file." +(defun org-roam--replace-roam-links-on-save-h () + "Run `org-roam-link-replace-all' before buffer is saved to its file." + (when org-roam-link-auto-replace + (add-hook 'before-save-hook #'org-roam-link-replace-all nil t))) + +(add-hook 'org-roam-find-file-hook #'org-roam--replace-roam-links-on-save-h) + +(defun org-roam-link-follow-link (path) + "Org-roam's roam: link navigation with description PATH. +This function is called by Org when following links of the type +`roam'. While the path is passed, assume that the cursor is on +the link." + (if-let ((node (org-roam-node-from-title-or-alias path))) + (progn + (when org-roam-link-auto-replace + (org-roam-link-replace-at-point)) + (org-id-goto (org-roam-node-id node))) + (org-roam-capture- + :node (org-roam-node-create :title path) + :props '(:finalize find-file)))) + +(defun org-roam-open-id-at-point () + "Navigates to the ID at point. +To be added to `org-open-at-point-functions'." + (let* ((context (org-element-context)) + (type (org-element-property :type context)) + (id (org-element-property :path context))) + (when (string= type "id") + (let ((node (org-roam-populate (org-roam-node-create :id id)))) + (cond + ((org-roam-node-file node) + (org-mark-ring-push) + (org-roam-node-visit node) + t) + (t nil)))))) + +(defun org-roam-open-id-with-org-roam-db-h () + "." + (add-hook 'org-open-at-point-functions #'org-roam-open-id-at-point nil t)) + +(add-hook 'org-roam-find-file-hook #'org-roam-open-id-with-org-roam-db-h) + +;;; Refiling +(defun org-roam-demote-entire-buffer () + "Convert an org buffer with any top level content to a single node. + +All headings are demoted one level. + +The #+TITLE: keyword is converted into a level-1 heading and deleted. +Any tags declared on #+FILETAGS: are transferred to tags on the new top heading. + +Any top level properties drawers are incorporated into the new heading." (interactive) - (unless org-roam-mode (org-roam-mode)) - (if-let ((aliases (org-roam--extract-titles-alias))) - (let ((alias (completing-read "Alias: " aliases nil 'require-match))) - (org-roam--set-global-prop - "roam_alias" - (combine-and-quote-strings (delete alias aliases))) - (org-roam-db--update-file (buffer-file-name (buffer-base-buffer)))) - (user-error "No aliases to delete"))) + (org-with-point-at 1 + (org-map-entries 'org-do-demote) + (insert "* " + (org-roam--file-keyword-get "TITLE") + "\n") + (org-back-to-heading) + (org-set-tags (org-roam--file-keyword-get "FILETAGS")) + (org-roam--file-keyword-kill "TITLE") + (org-roam--file-keyword-kill "FILETAGS"))) -(defun org-roam-tag-add () - "Add a tag to Org-roam file. - -Return added tag." +(defun org-roam-refile () + "Refile to node." (interactive) - (unless org-roam-mode (org-roam-mode)) - (let* ((all-tags (org-roam-db--get-tags)) - (tag (completing-read "Tag: " all-tags)) - (file (buffer-file-name (buffer-base-buffer))) - (existing-tags (org-roam--extract-tags-prop file))) - (when (string-empty-p tag) - (user-error "Tag can't be empty")) - (org-roam--set-global-prop - "roam_tags" - (combine-and-quote-strings (seq-uniq (cons tag existing-tags)))) - (org-roam-db--insert-tags 'update) - tag)) - -(defun org-roam-tag-delete () - "Delete a tag from Org-roam file." - (interactive) - (unless org-roam-mode (org-roam-mode)) - (if-let* ((file (buffer-file-name (buffer-base-buffer))) - (tags (org-roam--extract-tags-prop file))) - (let ((tag (completing-read "Tag: " tags nil 'require-match))) - (org-roam--set-global-prop - "roam_tags" - (combine-and-quote-strings (delete tag tags))) - (org-roam-db--insert-tags 'update)) - (user-error "No tag to delete"))) - -;;;###autoload -(defun org-roam-switch-to-buffer () - "Switch to an existing Org-roam buffer." - (interactive) - (let* ((roam-buffers (org-roam--get-roam-buffers)) - (names-and-buffers (mapcar (lambda (buffer) - (cons (or (org-roam-db--get-title - (buffer-file-name buffer)) - (buffer-name buffer)) - buffer)) - roam-buffers))) - (unless roam-buffers - (user-error "No roam buffers")) - (when-let ((name (org-roam-completion--completing-read "Buffer: " names-and-buffers - :require-match t))) - (switch-to-buffer (cdr (assoc name names-and-buffers)))))) - -(defun org-roam--execute-file-row-col (s) - "Move to row:col if S match the row:col syntax. To be used with `org-execute-file-search-functions'." - (when (string-match (rx (group (1+ digit)) - ":" - (group (1+ digit))) s) - (let ((row (string-to-number (match-string 1 s))) - (col (string-to-number (match-string 2 s)))) - (org-goto-line row) - (move-to-column (- col 1)) - t))) - -;;###autoload -(defun org-roam-unlinked-references () - "Check for unlinked references in the current buffer. - -The check here is naive: it uses a regex that detects for -strict (case-insensitive) occurrences of possible titles (see -`org-roam--extract-titles'), and shows them in a buffer. This -means that the results can be noisy, and may not truly indicate -an unlinked reference. - -Users are encouraged to think hard about whether items should be -linked, lest the network graph get too crowded. - -Requires a version of Ripgrep with PCRE2 support installed, with -the executable 'rg' in variable `exec-path'." - (interactive) - (unless (org-roam--org-roam-file-p) - (user-error "Not in org-roam file")) - (if (not (executable-find "rg")) - (error "Cannot find the ripgrep executable \"rg\". Check that it is installed and available on `exec-path'") - (when (string-match "PCRE2 is not available" (shell-command-to-string "rg --pcre2-version")) - (error "\"rg\" must be compiled with PCRE2 support")) - (let* ((titles (org-roam--extract-titles)) - (rg-command (concat "rg -o --vimgrep -P -i " - (string-join (mapcar (lambda (glob) (concat "-g " glob)) - (org-roam--list-files-search-globs org-roam-file-extensions)) " ") - (format " '\\[([^[]]++|(?R))*\\]%s' " - (mapconcat (lambda (title) - (format "|(\\b%s\\b)" (shell-quote-argument title))) - titles "")) - org-roam-directory)) - (file-loc (buffer-file-name)) - (buf (get-buffer-create "*org-roam unlinked references*")) - (results (split-string (shell-command-to-string rg-command) "\n")) - (result-regex (rx (group (one-or-more anything)) - ":" - (group (one-or-more digit)) - ":" - (group (one-or-more digit)) - ":" - (group (zero-or-more anything))))) - (pop-to-buffer buf) - (let ((inhibit-read-only t)) - (erase-buffer) - (org-mode) - (insert (propertize (car titles) 'font-lock-face 'org-document-title) "\n\n" - "* Unlinked References\n") - (dolist (line results) - (save-match-data - (when (string-match result-regex line) - (let ((file (match-string 1 line)) - (row (match-string 2 line)) - (col (match-string 3 line)) - (match (match-string 4 line))) - (when (and match - (member (downcase match) (mapcar #'downcase titles)) - (not (f-equal-p (expand-file-name file org-roam-directory) - file-loc))) - (let ((rowcol (concat row ":" col))) - (insert "- " - (org-link-make-string (concat "file:" file "::" rowcol) - (format "[%s] %s" rowcol (or (org-roam-db--get-title file) - file)))) - (when (executable-find "sed") ; insert line contents when sed is available - (insert " :: " - (shell-command-to-string - (concat "sed -n " - row - "p " - "\"" - file - "\"")))) - (insert "\n"))))))) - (read-only-mode +1) - (dolist (title titles) - (highlight-phrase (downcase title) 'bold-italic)) - (goto-char (point-min)))))) - -;;;###autoload -(defun org-roam-version (&optional message) - "Return `org-roam' version. -Interactively, or when MESSAGE is non-nil, show in the echo area." - (interactive) - (let* ((version - (with-temp-buffer - (insert-file-contents-literally (locate-library "org-roam.el")) - (goto-char (point-min)) - (save-match-data - (if (re-search-forward "\\(?:;; Version: \\([^z-a]*?$\\)\\)" nil nil) - (substring-no-properties (match-string 1)) - "N/A"))))) - (if (or message (called-interactively-p 'interactive)) - (message "%s" version) - version))) + (let* ((regionp (org-region-active-p)) + (region-start (and regionp (region-beginning))) + (region-end (and regionp (region-end))) + (node (org-roam-node-read nil nil nil 'require-match)) + (file (org-roam-node-file node)) + (nbuf (or (find-buffer-visiting file) + (find-file-noselect file))) + level reversed) + (if regionp + (progn + (org-kill-new (buffer-substring region-start region-end)) + (org-save-markers-in-region region-start region-end)) + (progn + (if (org-before-first-heading-p) + (org-roam-demote-entire-buffer)) + (org-copy-subtree 1 nil t))) + (with-current-buffer nbuf + (org-with-wide-buffer + (goto-char (org-roam-node-point node)) + (setq level (org-get-valid-level (funcall outline-level) 1) + reversed (org-notes-order-reversed-p)) + (goto-char + (if reversed + (or (outline-next-heading) (point-max)) + (or (save-excursion (org-get-next-sibling)) + (org-end-of-subtree t t) + (point-max)))) + (unless (bolp) (newline)) + (org-paste-subtree level nil nil t) + (and org-auto-align-tags + (let ((org-loop-over-headlines-in-active-region nil)) + (org-align-tags))) + (when (fboundp 'deactivate-mark) (deactivate-mark)))) + (if regionp + (delete-region (point) (+ (point) (- region-end region-start))) + (org-preserve-local-variables + (delete-region + (and (org-back-to-heading t) (point)) + (min (1+ (buffer-size)) (org-end-of-subtree t t) (point))))) + (org-roam--kill-empty-buffer))) (provide 'org-roam) ;;; org-roam.el ends here diff --git a/tests/roam-files/alias.org b/tests/roam-files/alias.org deleted file mode 100644 index 3e21811..0000000 --- a/tests/roam-files/alias.org +++ /dev/null @@ -1,2 +0,0 @@ -#+roam_alias: "a1" "a 2" -#+title: t1 diff --git a/tests/roam-files/bar.org b/tests/roam-files/bar.org index 3dd4911..0deb4e6 100644 --- a/tests/roam-files/bar.org +++ b/tests/roam-files/bar.org @@ -1,3 +1,6 @@ +:PROPERTIES: +:ID: 440795d0-70c1-4165-993d-aebd5eef7a24 +:END: #+title: Bar -This is file bar. Bar links to [[file:nested/bar.org][Nested Bar]]. +[[id:884b2341-b7fe-434d-848c-5282c0727861][Foo]] diff --git a/tests/roam-files/base.org b/tests/roam-files/base.org deleted file mode 100644 index 876953a..0000000 --- a/tests/roam-files/base.org +++ /dev/null @@ -1 +0,0 @@ -#+title: Base diff --git a/tests/roam-files/cite_ref.org b/tests/roam-files/cite_ref.org deleted file mode 100644 index ea18813..0000000 --- a/tests/roam-files/cite_ref.org +++ /dev/null @@ -1 +0,0 @@ -#+roam_key: cite:mitsuha2007 diff --git a/tests/roam-files/foo.org b/tests/roam-files/foo.org index 5252e95..8ce5d67 100644 --- a/tests/roam-files/foo.org +++ b/tests/roam-files/foo.org @@ -1,8 +1,4 @@ +:PROPERTIES: +:ID: 884b2341-b7fe-434d-848c-5282c0727861 +:END: #+title: Foo - -This is the foo file. It contains a link to [[file:bar.org][Bar]]. - -To make the tests more robust, here are some arbitrary links: - -- [[https:google.com][Google]] -- [[mailto:foo@john.com][mail to foo]] diff --git a/tests/roam-files/headlines/headline.org b/tests/roam-files/headlines/headline.org deleted file mode 100644 index 563b57e..0000000 --- a/tests/roam-files/headlines/headline.org +++ /dev/null @@ -1,14 +0,0 @@ -#+TITLE: Headline - -* Headline 1 -:PROPERTIES: -:ID: e84d0630-efad-4017-9059-5ef917908823 -:END: - -* No headline here -Oops. - -* Headline 2 -:PROPERTIES: -:ID: 801b58eb-97e2-435f-a33e-ff59a2f0c213 -:END: diff --git a/tests/roam-files/multiple-refs.org b/tests/roam-files/multiple-refs.org deleted file mode 100644 index dea8828..0000000 --- a/tests/roam-files/multiple-refs.org +++ /dev/null @@ -1,2 +0,0 @@ -#+roam_key: https://www.orgroam.com/ -#+roam_key: cite:orgroam2020 diff --git a/tests/roam-files/nested/bar.org b/tests/roam-files/nested/bar.org deleted file mode 100644 index b1c7400..0000000 --- a/tests/roam-files/nested/bar.org +++ /dev/null @@ -1,3 +0,0 @@ -#+title: Nested Bar - -This file is nested, 1 level deeper. It links to both [[file:../foo.org][Foo]] and [[file:foo.org][Nested Foo]]. diff --git a/tests/roam-files/nested/deeply/deeply_nested_file.org b/tests/roam-files/nested/deeply/deeply_nested_file.org deleted file mode 100644 index e64d8d3..0000000 --- a/tests/roam-files/nested/deeply/deeply_nested_file.org +++ /dev/null @@ -1 +0,0 @@ -#+title: Deeply Nested File diff --git a/tests/roam-files/nested/foo.org b/tests/roam-files/nested/foo.org deleted file mode 100644 index b40c6e5..0000000 --- a/tests/roam-files/nested/foo.org +++ /dev/null @@ -1,3 +0,0 @@ -#+title: Nested Foo - -This file has no links. diff --git a/tests/roam-files/no-title.org b/tests/roam-files/no-title.org deleted file mode 100644 index 6af889d..0000000 --- a/tests/roam-files/no-title.org +++ /dev/null @@ -1,5 +0,0 @@ -no title in this file :O - -links to itself, with no title: [[file:no-title.org][no-title]] - -* Headline title diff --git a/tests/roam-files/tags/no_tag.org b/tests/roam-files/tags/no_tag.org deleted file mode 100644 index ad8984f..0000000 --- a/tests/roam-files/tags/no_tag.org +++ /dev/null @@ -1,3 +0,0 @@ -#+title: Tagless File - -This file has no tags, and should not yield any tags on extracting via ~#+roam_tags~. diff --git a/tests/roam-files/tags/tag.org b/tests/roam-files/tags/tag.org deleted file mode 100644 index 3ca8954..0000000 --- a/tests/roam-files/tags/tag.org +++ /dev/null @@ -1,5 +0,0 @@ -#+roam_tags: "t1" "t2 with space" t3 -#+roam_tags: "t4 second-line" -#+title: Tags - -This file is used to test functionality for =(org-roam--extract-tags)= diff --git a/tests/roam-files/titles/aliases.org b/tests/roam-files/titles/aliases.org deleted file mode 100644 index a9431d8..0000000 --- a/tests/roam-files/titles/aliases.org +++ /dev/null @@ -1,2 +0,0 @@ -#+roam_alias: "roam" "alias" -#+roam_alias: "second" "line" diff --git a/tests/roam-files/titles/combination.org b/tests/roam-files/titles/combination.org deleted file mode 100644 index 34ec92f..0000000 --- a/tests/roam-files/titles/combination.org +++ /dev/null @@ -1,4 +0,0 @@ -#+title: TITLE PROP -#+roam_alias: "roam" "alias" - -* Headline diff --git a/tests/roam-files/titles/headline.org b/tests/roam-files/titles/headline.org deleted file mode 100644 index 4d4f027..0000000 --- a/tests/roam-files/titles/headline.org +++ /dev/null @@ -1 +0,0 @@ -* Headline diff --git a/tests/roam-files/titles/title.org b/tests/roam-files/titles/title.org deleted file mode 100644 index a241bc4..0000000 --- a/tests/roam-files/titles/title.org +++ /dev/null @@ -1 +0,0 @@ -#+title: Title diff --git a/tests/roam-files/unlinked.org b/tests/roam-files/unlinked.org deleted file mode 100644 index 0059965..0000000 --- a/tests/roam-files/unlinked.org +++ /dev/null @@ -1,3 +0,0 @@ -#+title: Unlinked - -Nothing links here :( diff --git a/tests/roam-files/web_ref.org b/tests/roam-files/web_ref.org deleted file mode 100644 index c5acfd4..0000000 --- a/tests/roam-files/web_ref.org +++ /dev/null @@ -1 +0,0 @@ -#+roam_key: https://google.com/ diff --git a/tests/test-org-roam-perf.el b/tests/test-org-roam-perf.el deleted file mode 100644 index 667203d..0000000 --- a/tests/test-org-roam-perf.el +++ /dev/null @@ -1,53 +0,0 @@ -;;; test-org-roam-perf.el --- Performance Tests for Org-roam -*- lexical-binding: t; -*- - -;; Copyright (C) 2020 Jethro Kuan - -;; Author: Jethro Kuan -;; Package-Requires: ((buttercup)) - -;; 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 . - -;;; Commentary: -;;; Code: - -(require 'buttercup) -(require 'org-roam) - -(defconst test-org-roam-perf-zip-url "https://github.com/org-roam/test-org-files/archive/master.zip" - "Path to zip for test org-roam files.") - -(defun test-org-roam-perf--abs-path (file-path) - "Get absolute FILE-PATH from `org-roam-directory'." - (expand-file-name file-path org-roam-directory)) - -(defun test-org-roam-perf--init () - "." - (let* ((temp-loc (expand-file-name (make-temp-name "test-org-files-") temporary-file-directory)) - (zip-file-loc (concat temp-loc ".zip")) - (_ (url-copy-file test-org-roam-perf-zip-url zip-file-loc)) - (_ (shell-command (format "mkdir -p %s && unzip -j -qq %s -d %s" temp-loc zip-file-loc temp-loc)))) - (setq org-roam-directory temp-loc))) - -(describe "Cache Build" - (it "cache build from scratch time to be acceptable" - (test-org-roam-perf--init) - (pcase (benchmark-run 1 (org-roam-db-build-cache t)) - (`(,time ,gcs ,time-in-gc) - (message "Elapsed time: %fs (%fs in %d GCs)" time time-in-gc gcs) - (expect time :to-be-less-than 110)))) - (it "builds quickly without change" - (pcase (benchmark-run 1 (org-roam-db-build-cache)) - (`(,time ,gcs ,time-in-gc) - (message "Elapsed time: %fs (%fs in %d GCs)" time time-in-gc gcs) - (expect time :to-be-less-than 5))))) diff --git a/tests/test-org-roam.el b/tests/test-org-roam.el index 4a95b33..7f31e33 100644 --- a/tests/test-org-roam.el +++ b/tests/test-org-roam.el @@ -23,341 +23,31 @@ (require 'buttercup) (require 'org-roam) -(require 'dash) -(defun test-org-roam--abs-path (file-path) - "Get absolute FILE-PATH from `org-roam-directory'." - (expand-file-name file-path org-roam-directory)) - -(defun test-org-roam--find-file (path) - "PATH." - (let ((path (test-org-roam--abs-path path))) - (make-directory (file-name-directory path) t) - (find-file path))) - -(defvar test-org-roam-directory (expand-file-name "tests/roam-files") - "Directory containing org-roam test org files.") - -(defun test-org-roam--init () - "." - (let ((original-dir test-org-roam-directory) - (new-dir (expand-file-name (make-temp-name "org-roam") temporary-file-directory))) - (copy-directory original-dir new-dir) - (setq org-roam-directory new-dir) - (org-roam-mode +1) - (sleep-for 2))) - -(defun test-org-roam--teardown () - (org-roam-mode -1) - (delete-file org-roam-db-location) - (org-roam-db--close)) - -(describe "Ref extraction" +(describe "org-roam-db-sync" (before-all - (test-org-roam--init)) + (setq org-roam-directory (expand-file-name "tests/roam-files") + org-roam-db-location (expand-file-name "org-roam.db" temporary-file-directory)) + (org-roam-setup)) (after-all - (test-org-roam--teardown)) + (org-roam-teardown) + (delete-file org-roam-db-location)) - (cl-flet - ((test (fn file) - (let* ((fname (test-org-roam--abs-path file)) - (buf (find-file-noselect fname))) - (with-current-buffer buf - ;; Unlike tag extraction, it doesn't make sense to - ;; pass a filename. - (funcall fn))))) - ;; Enable "cite:" link parsing - (org-link-set-parameters "cite") - (it "extracts web keys" - (expect (test #'org-roam--extract-ref - "web_ref.org") - :to-equal - '("website" . "//google.com/"))) - (it "extracts cite keys" - (expect (test #'org-roam--extract-ref - "cite_ref.org") - :to-equal - '("cite" . "mitsuha2007"))) - (it "extracts all keys" - (expect (test #'org-roam--extract-refs - "multiple-refs.org") - :to-have-same-items-as - '(("cite" . "orgroam2020") - ("website" . "//www.orgroam.com/")))))) - -(describe "Title extraction" - :var (org-roam-title-sources) - (before-all - (test-org-roam--init)) - - (after-all - (test-org-roam--teardown)) - - (cl-flet - ((test (fn file) - (let ((buf (find-file-noselect - (test-org-roam--abs-path file)))) - (with-current-buffer buf - (funcall fn))))) - (it "extracts title from title property" - (expect (test #'org-roam--extract-titles-title - "titles/title.org") - :to-equal - '("Title")) - (expect (test #'org-roam--extract-titles-title - "titles/aliases.org") - :to-equal - nil) - (expect (test #'org-roam--extract-titles-title - "titles/headline.org") - :to-equal - nil) - (expect (test #'org-roam--extract-titles-title - "titles/combination.org") - :to-equal - '("TITLE PROP"))) - - (it "extracts alias" - (expect (test #'org-roam--extract-titles-alias - "titles/title.org") - :to-equal - nil) - (expect (test #'org-roam--extract-titles-alias - "titles/aliases.org") - :to-equal - '("roam" "alias" "second" "line")) - (expect (test #'org-roam--extract-titles-alias - "titles/headline.org") - :to-equal - nil) - (expect (test #'org-roam--extract-titles-alias - "titles/combination.org") - :to-equal - '("roam" "alias"))) - - (it "extracts headlines" - (expect (test #'org-roam--extract-titles-alias - "titles/title.org") - :to-equal - nil) - (expect (test #'org-roam--extract-titles-headline - "titles/aliases.org") - :to-equal - nil) - (expect (test #'org-roam--extract-titles-headline - "titles/headline.org") - :to-equal - '("Headline")) - (expect (test #'org-roam--extract-titles-headline - "titles/combination.org") - :to-equal - '("Headline"))) - - (describe "uses org-roam-title-sources correctly" - (it "'((title headline) alias)" - (expect (let ((org-roam-title-sources '((title headline) alias))) - (test #'org-roam--extract-titles - "titles/combination.org")) - :to-equal - '("TITLE PROP" "roam" "alias"))) - (it "'((headline title) alias)" - (expect (let ((org-roam-title-sources '((headline title) alias))) - (test #'org-roam--extract-titles - "titles/combination.org")) - :to-equal - '("Headline" "roam" "alias"))) - (it "'(headline alias title)" - (expect (let ((org-roam-title-sources '(headline alias title))) - (test #'org-roam--extract-titles - "titles/combination.org")) - :to-equal - '("Headline" "roam" "alias" "TITLE PROP")))))) - -(describe "Tag extraction" - :var (org-roam-tag-sources) - (before-all - (test-org-roam--init)) - - (after-all - (test-org-roam--teardown)) - - (cl-flet - ((test (fn file) - (let* ((fname (test-org-roam--abs-path file)) - (buf (find-file-noselect fname))) - (with-current-buffer buf - (funcall fn fname))))) - (it "extracts from prop" - (expect (test #'org-roam--extract-tags-prop - "tags/tag.org") - :to-equal - '("t1" "t2 with space" "t3" "t4 second-line")) - (expect (test #'org-roam--extract-tags-prop - "tags/no_tag.org") - :to-equal - nil)) - - (it "extracts from all directories" - (expect (test #'org-roam--extract-tags-all-directories - "base.org") - :to-equal - nil) - (expect (test #'org-roam--extract-tags-all-directories - "tags/tag.org") - :to-equal - '("tags")) - (expect (test #'org-roam--extract-tags-all-directories - "nested/deeply/deeply_nested_file.org") - :to-equal - '("nested" "deeply"))) - - (it "extracts from last directory" - (expect (test #'org-roam--extract-tags-last-directory - "base.org") - :to-equal - nil) - (expect (test #'org-roam--extract-tags-last-directory - "tags/tag.org") - :to-equal - '("tags")) - (expect (test #'org-roam--extract-tags-last-directory - "nested/deeply/deeply_nested_file.org") - :to-equal - '("deeply"))) - - (it "extracts from first directory" - (expect (test #'org-roam--extract-tags-first-directory - "base.org") - :to-equal - nil) - (expect (test #'org-roam--extract-tags-first-directory - "tags/tag.org") - :to-equal - '("tags")) - (expect (test #'org-roam--extract-tags-first-directory - "nested/deeply/deeply_nested_file.org") - :to-equal - '("nested"))) - - (describe "uses org-roam-tag-sources correctly" - (it "'(prop)" - (expect (let ((org-roam-tag-sources '(prop))) - (test #'org-roam--extract-tags - "tags/tag.org")) - :to-equal - '("t1" "t2 with space" "t3" "t4 second-line"))) - (it "'(prop all-directories)" - (expect (let ((org-roam-tag-sources '(prop all-directories))) - (test #'org-roam--extract-tags - "tags/tag.org")) - :to-equal - '("t1" "t2 with space" "t3" "t4 second-line" "tags")))))) - -(describe "ID extraction" - (before-all - (test-org-roam--init)) - - (after-all - (test-org-roam--teardown)) - - (cl-flet - ((test (fn file) - (let* ((fname (test-org-roam--abs-path file)) - (buf (find-file-noselect fname))) - (with-current-buffer buf - (funcall fn fname))))) - (it "extracts ids" - (expect (test #'org-roam--extract-ids - "headlines/headline.org") - :to-have-same-items-as - `(["e84d0630-efad-4017-9059-5ef917908823" ,(test-org-roam--abs-path "headlines/headline.org") 1] - ["801b58eb-97e2-435f-a33e-ff59a2f0c213" ,(test-org-roam--abs-path "headlines/headline.org") 1]))))) - -(describe "Test roam links" - (it "" - (expect (org-roam-link--split-path "") + (it "has the correct number of files" + (expect (caar (org-roam-db-query [:select (funcall count) :from files])) :to-equal - '(title "" "" nil))) - (it "title" - (expect (org-roam-link--split-path "title") + 2)) + + (it "has the correct number of nodes" + (expect (caar (org-roam-db-query [:select (funcall count) :from nodes])) :to-equal - '(title "title" "" nil))) - (it "title*" - (expect (org-roam-link--split-path "title*") + 2)) + + (it "has the correct number of links" + (expect (caar (org-roam-db-query [:select (funcall count) :from links])) :to-equal - '(title+headline "title" "" 5))) - (it "title*headline" - (expect (org-roam-link--split-path "title*headline") - :to-equal - '(title+headline "title" "headline" 5))) - (it "*headline" - (expect (org-roam-link--split-path "*headline") - :to-equal - '(headline "" "headline" 0)))) - -;;; Tests -(xdescribe "org-roam-db-build-cache" - (before-each - (test-org-roam--init)) - - (after-each - (test-org-roam--teardown)) - - (it "initializes correctly" - ;; Cache - (expect (caar (org-roam-db-query [:select (funcall count) :from files])) :to-be 8) - (expect (caar (org-roam-db-query [:select (funcall count) :from links])) :to-be 5) - (expect (caar (org-roam-db-query [:select (funcall count) :from titles])) :to-be 8) - (expect (caar (org-roam-db-query [:select (funcall count) :from titles - :where titles :is-null])) :to-be 1) - (expect (caar (org-roam-db-query [:select (funcall count) :from refs])) :to-be 1) - - ;; Links - (expect (caar (org-roam-db-query [:select (funcall count) :from links - :where (= source $s1)] - (test-org-roam--abs-path "foo.org"))) :to-be 1) - (expect (caar (org-roam-db-query [:select (funcall count) :from links - :where (= source $s1)] - (test-org-roam--abs-path "nested/bar.org"))) :to-be 2) - - ;; Links -- File-to - (expect (caar (org-roam-db-query [:select (funcall count) :from links - :where (= dest $s1)] - (test-org-roam--abs-path "nested/foo.org"))) :to-be 1) - (expect (caar (org-roam-db-query [:select (funcall count) :from links - :where (= dest $s1)] - (test-org-roam--abs-path "nested/bar.org"))) :to-be 1) - (expect (caar (org-roam-db-query [:select (funcall count) :from links - :where (= dest $s1)] - (test-org-roam--abs-path "unlinked.org"))) :to-be 0) - ;; TODO Test titles - (expect (org-roam-db-query [:select * :from titles]) - :to-have-same-items-as - (list (list (test-org-roam--abs-path "alias.org") - (list "t1" "a1" "a 2")) - (list (test-org-roam--abs-path "bar.org") - (list "Bar")) - (list (test-org-roam--abs-path "foo.org") - (list "Foo")) - (list (test-org-roam--abs-path "nested/bar.org") - (list "Nested Bar")) - (list (test-org-roam--abs-path "nested/foo.org") - (list "Nested Foo")) - (list (test-org-roam--abs-path "no-title.org") - (list "Headline title")) - (list (test-org-roam--abs-path "web_ref.org") nil) - (list (test-org-roam--abs-path "unlinked.org") - (list "Unlinked")))) - - (expect (org-roam-db-query [:select * :from refs]) - :to-have-same-items-as - (list (list "https://google.com/" (test-org-roam--abs-path "web_ref.org") "website"))) - - ;; Expect rebuilds to be really quick (nothing changed) - (expect (org-roam-db-build-cache) - :to-equal - (list :files 0 :links 0 :tags 0 :titles 0 :refs 0 :deleted 0)))) + 1))) (provide 'test-org-roam)