From 53dcf687ef4f905c78edba959160bab2011823a9 Mon Sep 17 00:00:00 2001 From: Jethro Kuan Date: Sat, 27 Feb 2021 23:06:28 +0800 Subject: [PATCH] org-roam v2 --- .dir-locals.el | 16 +- .github/workflows/test.yml | 37 +- .gitignore | 1 + CHANGELOG.md | 9 +- Eldev | 29 + Makefile | 73 +- README.md | 5 +- doc/org-roam.org | 398 ++---- doc/org-roam.texi | 531 ++++---- makem.sh | 1087 ---------------- org-roam-backlinks.el | 92 ++ org-roam-buffer.el | 357 ------ org-roam-capture.el | 113 +- org-roam-compat.el | 77 +- org-roam-completion.el | 64 +- org-roam-dailies.el | 140 +-- org-roam-db.el | 585 ++++----- org-roam-doctor.el | 137 +-- org-roam-graph.el | 309 ----- org-roam-link.el | 306 ++--- org-roam-macs.el | 74 +- org-roam-mode.el | 174 +++ org-roam-node.el | 308 +++++ org-roam-protocol.el | 6 +- org-roam-ref.el | 91 ++ org-roam-reflinks.el | 91 ++ org-roam-dev.el => org-roam-structs.el | 40 +- org-roam-unlinked-references.el | 161 +++ org-roam-utils.el | 139 +++ org-roam.el | 1566 ++---------------------- tests/test-org-roam-perf.el | 4 +- tests/test-org-roam.el | 280 +---- 32 files changed, 2080 insertions(+), 5220 deletions(-) create mode 100644 Eldev delete mode 100755 makem.sh create mode 100644 org-roam-backlinks.el delete mode 100644 org-roam-buffer.el delete mode 100644 org-roam-graph.el create mode 100644 org-roam-mode.el create mode 100644 org-roam-node.el create mode 100644 org-roam-ref.el create mode 100644 org-roam-reflinks.el rename org-roam-dev.el => org-roam-structs.el (51%) create mode 100644 org-roam-unlinked-references.el create mode 100644 org-roam-utils.el diff --git a/.dir-locals.el b/.dir-locals.el index dcf681b..13ef195 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -1,6 +1,12 @@ -;;; 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) + (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 28060f3..035a038 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,19 @@ # 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 ### 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 @@ -18,7 +22,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` @@ -29,6 +33,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. @@ -37,9 +42,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..dbf9190 --- /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" "byte-compile") + 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 3d1f32c..cba75bf 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 @@ -53,8 +53,7 @@ Here's a sample configuration with `use-package`: ("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)))) + (("C-c n i" . org-roam-insert)))) ``` Org-roam requires sqlite to function. Org-roam optionally uses Graphviz for diff --git a/doc/org-roam.org b/doc/org-roam.org index bf12771..bfb5c4c 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 on 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 @@ -294,9 +290,9 @@ 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 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: #+BEGIN_SRC emacs-lisp (executable-find "sqlite3") @@ -313,63 +309,84 @@ your Emacs configuration: * Getting Started -This short tutorial describes the essential commands used in Org-roam, to help -you get started. +** The Org-roam Node -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]]. +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: + +#+begin_quote +A node is any headline or top level file with an ID. +#+end_quote + +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 primary capability comes from its aggressive caching: it crawls all +files within ~org-roam-directory~, keeping a cache of all its links and nodes, +and 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 ~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~: +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") (setq org-roam-directory "~/org-roam") #+END_SRC -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 need to setup Org-roam to maintain cache consistency. This is achieved +by running ~M-x org-roam-setup~. To ensure that Org-roam is available on +startup, one can place this in their 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 -(add-hook 'after-init-hook 'org-roam-mode) -#+END_SRC +#+begin_src emacs-lisp + (require 'org-roam) + (org-roam-setup) +#+end_src 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. -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 +** TODO Creating New Notes + +Let us now create our first node. Call ~M-x org-roam-find-file~. This shows 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. Entering the title of the note you wish to create, and pressing ~RET~ should begin 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~. - 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 @@ -393,165 +410,10 @@ 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]]). -* Files -:PROPERTIES: -:ID: 3edec3e6-8e26-4a43-8a0a-bf204268bbb3 -:END: - -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. - -** File Titles - -To easily find a note, a title needs to be prescribed to a note. - -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 ~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: - -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. - -Take for example the following org file: - -#+BEGIN_SRC org - #+title: World War 2 - #+roam_alias: "WWII" "World War II" - - * Headline -#+END_SRC - -| Method | Titles | -|-------------+--------------------------| -| ~'title~ | '("World War 2") | -| ~'headline~ | '("Headline") | -| ~'alias~ | '("WWII" "World War II") | - -If no title is provided, Org-roam defaults to using the file-path. - -*** Customizing Title Extraction - -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. - -- User Option: org-roam-title-sources - - 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. - -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. - -** 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 ~#+roam_tags~ property. To add -additional extraction methods, see [[id:c986edba-9498-4af1-b033-c94b733d42c8][Customizing Tag Extraction]]. - -*** Customizing Tag Extraction -:PROPERTIES: -:ID: c986edba-9498-4af1-b033-c94b733d42c8 -:END: - -Org-roam calls ~org-roam--extract-tags~ to extract tags from files. The variable -~org-roam-tag-sources~, to control how tags are extracted. - -- User Option: org-roam-tag-sources - -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" - -By default, only the ~'prop~ extraction method is enabled. To enable the other -extraction methods, you may modify ~org-roam-tag-sources~, for example: - -#+BEGIN_SRC emacs-lisp -(setq org-roam-tag-sources '(prop last-directory)) -#+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 - +* TODO Node Properties +** TODO Standard Org properties +** TODO Aliases +** TODO Refs Refs are unique identifiers for files. For example, a note for a website may contain a ref: @@ -581,6 +443,51 @@ 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. +* TODO The Org-roam Buffer +* TODO Styling Org-roam +* TODO Completion +* TODO Encryption +* TODO Org-roam protocol +* TODO Diagnosing and Repair +* TODO Building Extensions +** TODO Public Interface + +Database (for developers) +- querying: (org-roam-db-query) +- updating full database: (org-roam-db-sync) +- update current file: (org-roam-db-update-file) +- remove file from database: (org-roam-db-clear-file) + +Nodes: +- List all nodes +- Read a node in from completion +- Get node at point + +Links: +- Get backlinks for node + +Tags: +- Add tag to node +- delete tag for node +- Get tags for node + +Aliases: +- Add alias to node +- Delete alias for node +- Get aliases for node + +Ref: +- Add ref to node + +Capture: + +Navigation: +- Find or create a node +- + + +* TODO The Org-mode Ecosystem +* TODO Frequently Asked Questions * The Templating System Rather than creating blank files on ~org-roam-insert~ and ~org-roam-find-file~, @@ -760,23 +667,6 @@ 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 @@ -1180,7 +1070,7 @@ in the generated graph. ** 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]] @@ -1489,22 +1379,6 @@ using [[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_SRC The Deft interface can slow down quickly when the number of files get huge. diff --git a/doc/org-roam.texi b/doc/org-roam.texi index 2bf82ad..b6eea54 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,22 @@ General Public License for more details. * A Brief Introduction to the Zettelkasten Method:: * Installation:: * Getting Started:: -* Files:: +* 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:: * The Templating System:: * Concepts and Configuration:: * Inserting Links:: * Completions:: * Navigating Around:: -* Encryption:: +* Encryption: Encryption (1). * Graphing:: * Minibuffer Completion:: * Roam Protocol:: @@ -103,19 +112,22 @@ Installation * Installing from the Git Repository:: * Post-Installation Tasks:: -Files +Getting Started -* File Titles:: -* File Tags:: -* File Refs:: +* The Org-roam Node:: +* Links between Nodes:: +* Setting up Org-roam:: +* Creating New Notes:: -File Titles +Node Properties -* Customizing Title Extraction:: +* Standard Org properties:: +* Aliases:: +* Refs:: -File Tags +Building Extensions -* Customizing Tag Extraction:: +* Public Interface:: The Templating System @@ -125,10 +137,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 +209,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 +242,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 +256,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 on 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 @@ -520,9 +527,9 @@ 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 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: @lisp (executable-find "sqlite3") @@ -540,64 +547,101 @@ 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 New Notes:: +@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 primary capability comes from its aggressive caching: it crawls all +files within @code{org-roam-directory}, keeping a cache of all its links and nodes, +and 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 +@node Creating New Notes +@section @strong{TODO} Creating New Notes + +Let us now create our first node. Call @code{M-x org-roam-find-file}. This shows 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. Entering the title of the note you wish to create, and pressing @code{RET} should begin 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}. - 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 @@ -620,203 +664,23 @@ 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 +713,103 @@ 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 + +@node The Org-mode Ecosystem +@chapter @strong{TODO} The Org-mode Ecosystem + +@node Frequently Asked Questions +@chapter @strong{TODO} Frequently Asked Questions + @node The Templating System @chapter The Templating System @@ -990,10 +951,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 +981,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 +1046,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 @@ -1279,7 +1218,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 +1556,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} 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-backlinks.el b/org-roam-backlinks.el new file mode 100644 index 0000000..91d30eb --- /dev/null +++ b/org-roam-backlinks.el @@ -0,0 +1,92 @@ +;;; org-roam-backlinks.el --- The backlinks section -*- 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") (s "1.12.0") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2") (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 provides functionality dealing with nodes. +;; +;;; Code: +;;;; Library Requires +(require 'magit-section) +(require 'org-roam-structs) +(require 'org-roam-node) + +(defvar org-roam-mode-sections) +(defvar org-roam-mode-map) + +(declare-function org-roam-db-query "org-roam-db") + +;;; Section +;;;; Definition + +;;; Functions +(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)))) + +;;; Section inserter +(cl-defun org-roam-backlinks-insert-section (&key node _file) + "Insert backlinks section for NODE." + (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)))) + +(provide 'org-roam-backlinks) +;;; org-roam-backlinks.el ends here 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 65f528e..ff8fd9c 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") (s "1.12.0") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2") (magit-section "2.90.1")) ;; This file is NOT part of GNU Emacs. @@ -31,8 +31,10 @@ ;;; Code: ;;;; Library Requires (require 'org-capture) -(require 'org-roam-macs) +(eval-when-compile + (require 'org-roam-macs)) (require 'org-roam-db) +(require 'org-roam-structs) (require 'dash) (require 's) (require 'cl-lib) @@ -40,18 +42,9 @@ ;; Declarations (defvar org-roam-encrypt-files) (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.") @@ -168,58 +161,6 @@ Template string :\n%v") ((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)))))) - (defcustom org-roam-capture-ref-templates '(("r" "ref" plain #'org-roam-capture--get-point "%?" @@ -328,7 +269,7 @@ the capture)." (pcase finalize ('find-file (when-let ((file-path (org-roam-capture--get :file-path))) - (org-roam--find-file file-path) + (find-file file-path) (run-hooks 'org-roam-capture-after-find-file-hook))) ('insert-link (when-let* ((mkr (org-roam-capture--get :insert-at)) @@ -535,14 +476,14 @@ This function is used solely in Org-roam's capture templates: see ('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'"))) + ;; ('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"))))) (org-capture-put :template (org-roam-capture--fill-template (org-capture-get :template))) @@ -619,21 +560,17 @@ 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))) + (if (org-roam-node-id node) + (let ((org-roam-capture--info (list (cons 'title (org-roam-node-title node)) + (cons 'slug (funcall org-roam-title-to-slug-function + (org-roam-node-title node))) + (cons 'file (org-roam-node-file node)))) + (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)))))))) (provide 'org-roam-capture) diff --git a/org-roam-compat.el b/org-roam-compat.el index c61fedd..ae6687e 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") (s "1.12.0") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2") (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..2c810f6 100644 --- a/org-roam-completion.el +++ b/org-roam-completion.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") (s "1.12.0") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2") (magit-section "2.90.1")) ;; This file is NOT part of GNU Emacs. @@ -52,66 +52,6 @@ :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))) - -(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))) - (provide 'org-roam-completion) ;;; org-roam-completion.el ends here diff --git a/org-roam-dailies.el b/org-roam-dailies.el index 9110e7f..3f1fcef 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") (s "1.12.0") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2") (magit-section "2.90.1")) ;; This file is NOT part of GNU Emacs. @@ -36,16 +36,19 @@ ;;; 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") + +;;;; 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/" @@ -71,55 +74,55 @@ (choice :value ("d" "default" plain (function org-roam-capture--get-point) "%?" :file-name ,(concat org-roam-dailies-directory - "%<%Y-%m-%d>") + "%<%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 \ + (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)))))))) + (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 () @@ -129,7 +132,7 @@ Template string :\n%v") (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 (org-roam-dailies-directory--get-absolute-path))) (defun org-roam-dailies--daily-note-p (&optional file) "Return t if FILE is an Org-roam daily-note, nil otherwise. @@ -149,9 +152,8 @@ If FILE is not specified, use the current buffer's file-path." "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))) + (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))))) @@ -281,7 +283,7 @@ Prefer past dates, unless PREFER-FUTURE is non-nil." "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))))) + (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) @@ -305,18 +307,18 @@ 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) "Find previous daily-note. diff --git a/org-roam-db.el b/org-roam-db.el index 97aa493..c7d5dc7 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") (s "1.12.0") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2") (magit-section "2.90.1")) ;; This file is NOT part of GNU Emacs. @@ -41,23 +41,15 @@ ;; For `org-with-wide-buffer' (require 'org-macs)) -(defvar org-roam-directory) -(defvar org-roam-enable-headline-linking) -(defvar org-roam-verbose) -(defvar org-roam-file-name) +(require 'org-roam-structs) +(defvar org-roam-directory) +(defvar org-roam-verbose) (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-node-at-point "org-roam-node") ;;;; Options (defcustom org-roam-db-location (expand-file-name "org-roam.db" user-emacs-directory) @@ -71,7 +63,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,33 +76,11 @@ value like `most-positive-fixnum'." :type 'int :group 'org-roam) -(defconst org-roam-db--version 10) +(defconst org-roam-db--version 12) (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 () @@ -156,40 +126,59 @@ SQL can be either the emacsql vector representation, or a string." (apply #'emacsql (org-roam-db) sql args))) ;;;; Schemata +;; NOTE: Foreign key somehow doesn't work! Adding a file column to every table as a workaround. (defconst org-roam-db--table-schemata '((files [(file :unique :primary-key) (hash :not-null) - (meta :not-null)]) + atime + mtime]) - (ids - [(id :unique :primary-key) + (nodes + [(id :primary-key :not-null) (file :not-null) - (level :not-null)]) + (level :not-null) + (pos :not-null) + todo + priority + (scheduled text) + (deadline text) + title] + (: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 + (aliases [(file :not-null) - title]) + (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)]))) + ([(file :not-null) + (node-id :not-null) + (ref :not-null) + (type :not-null)] + (:foreign-key [node-id] :references nodes [id] :on-delete :cascade))) + + (tags + [(file :not-null) + (node-id :not-null) + tag] + (:foreign-key [node-id] :references nodes [id] :on-delete :cascade)) + + (links + [(file :not-null) + (pos :not-null) + (source :not-null) + (dest :not-null) + (type :not-null) + (properties :not-null)] + (:foreign-key [file] :references files [file] :on-delete :cascade)))) (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)))) @@ -201,7 +190,7 @@ SQL can be either the emacsql vector representation, or a string." (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)))) + (org-roam-db-sync t)))) version) (defun org-roam-db--close (&optional db) @@ -218,214 +207,200 @@ 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)] + :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 +(defun org-roam-db-map-headlines (fns) + "Run FNS over all headlines in the current buffer." + (org-with-point-at 1 + (org-map-entries + (lambda () + (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-let ((id (org-id-get))) + (let ((file (buffer-file-name (buffer-base-buffer))) + (title (cadr (assoc "TITLE" (org-collect-keywords '("title")) + #'string-equal))) + (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"))) + (org-roam-db-query + [:insert :into nodes + :values $v1] + (vector id file level pos todo priority + scheduled deadline title)) + (when tags (org-roam-db-query - [:insert :into links + [:insert :into tags :values $v1] - links) - (length links)) - 0))) + (mapcar (lambda (tag) + (vector file id (substring-no-properties tag))) + tags))) + (when aliases + (org-roam-db-query + [:insert :into aliases + :values $v1] + (mapcar (lambda (alias) + (vector file 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 file 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-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 - (org-roam-db-query - [:insert :into ids - :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))) +(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 (nth 4 heading-components))) + (org-roam-db-query + [:insert :into nodes + :values $v1] + (vector id file level pos todo priority + scheduled deadline title))))) -(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-aliases () + "Insert aliases for node at point into Org-roam cache." + (when-let ((file (buffer-file-name (buffer-base-buffer))) + (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 file 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 ((file (buffer-file-name (buffer-base-buffer))) + (node-id (org-id-get)) + (tags (org-get-tags))) + (org-roam-db-query [:insert :into tags + :values $v1] + (mapcar (lambda (tag) + (vector file node-id tag)) tags)))) + +(defun org-roam-db-insert-refs () + "Insert refs for node at point into Org-roam cache." + (when-let* ((file (buffer-file-name (buffer-base-buffer))) + (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 file 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 ((file (buffer-file-name (buffer-base-buffer))) + (type (org-element-property :type link)) + (dest (org-element-property :path link)) + (properties (list :outline (org-get-outline-path))) + source) + (save-excursion + (while (and (not (setq source (org-id-get))) + (not (= (point) (point-min)))) + (org-up-heading-or-point-min))) + (when source + (org-roam-db-query + [:insert :into links + :values $v1] + (vector file (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." @@ -467,7 +442,9 @@ connections, nil is returned." 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 + 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. @@ -491,29 +468,7 @@ 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) +(defun org-roam-db-sync (&optional force) "Build the cache for `org-roam-directory'. If FORCE, force a rebuild of the cache from scratch." (interactive "P") @@ -524,109 +479,47 @@ 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))) + (dolist-with-progress-reporter (file (hash-table-keys current-files)) + "Clearing removed files..." + (org-roam-db-clear-file file)) + (dolist-with-progress-reporter (file modified-files) + "Processing 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-roam-db-clear-file) + (org-roam-db-insert-file) + (org-roam-db-insert-file-node) + (org-roam-db-map-headlines + (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))))) - - ;; 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))) - -(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-doctor.el b/org-roam-doctor.el index 0f3c275..e31b2ef 100644 --- a/org-roam-doctor.el +++ b/org-roam-doctor.el @@ -5,8 +5,8 @@ ;; 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")) +;; Version: 2.0.0 +;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (s "1.12.0") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2") (magit-section "2.90.1")) ;; This file is NOT part of GNU Emacs. @@ -47,16 +47,17 @@ (require 'org-element) (require 's) (require 'dash) -(require 'org-roam-macs) +(eval-when-compile + (require 'org-roam-macs)) +(require 'org-roam-node) + +(defvar org-roam-mode-map) -(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'. @@ -78,79 +79,7 @@ processing multiple files" :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)) + ("r" . ("Replace link" . org-roam-doctor--replace-link)))))) (defun org-roam-doctor-broken-links (ast) "Checker for detecting broken links. @@ -158,18 +87,12 @@ 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)))))) + (when (equal "id" (org-element-property :type l)) + (let ((id (org-element-property :path l))) + (unless (org-id-find id) + (push `(,(org-element-property :begin l) + ,(format "Broken id link \"%s\"" id)) + reports)))))) reports)) (defun org-roam-doctor--check (buffer checkers) @@ -221,25 +144,7 @@ CHECKERS is the list of checkers used." (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))) + (org-roam-node-insert)) (quit (progn (replace-buffer-contents orig) (goto-char p))))))) @@ -258,7 +163,7 @@ CHECKERS is the list of checkers used." (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." +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) @@ -291,12 +196,11 @@ CHECKER is a org-roam-doctor checker instance." "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--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) @@ -304,6 +208,7 @@ If CHECKALL, run the check for all Org-roam files." (save-window-excursion (let ((existing-buffers (org-roam--get-roam-buffers)) (org-inhibit-startup org-roam-doctor-inhibit-startup)) + (org-id-update-id-locations) (dolist (f files) (let ((buf (find-file-noselect f))) (org-roam-doctor--check buf checkers) diff --git a/org-roam-graph.el b/org-roam-graph.el deleted file mode 100644 index e1dbe1c..0000000 --- a/org-roam-graph.el +++ /dev/null @@ -1,309 +0,0 @@ -;;; org-roam-graph.el --- Graphing API -*- 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 graphing functionality for org-roam. -;; -;;; 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") - "Method to view the org-roam graph. -It may be one of the following: - - a string representing the path to the executable for viewing the graph. - - a function accepting a single argument: the graph file path. - - nil uses `view-file' to view the graph." - :type '(choice - (string :tag "Path to executable") - (function :tag "Function to display graph" eww-open-file) - (const :tag "view-file")) - :group 'org-roam) - -(defcustom org-roam-graph-executable "dot" - "Path to graphing executable, or its name." - :type 'string - :group 'org-roam) - -(defcustom org-roam-graph-extra-config nil - "Extra options passed to graphviz. -Example: - '((\"rankdir\" . \"LR\"))" - :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\"))" - :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) - :group 'org-roam) - -(defcustom org-roam-graph-max-title-length 100 - "Maximum length of titles in graph nodes." - :type 'number - :group 'org-roam) - -(defcustom org-roam-graph-shorten-titles 'truncate - "Determines how long titles appear in graph nodes. -Recognized values are the symbols `truncate' and `wrap', in which -cases the title will be truncated or wrapped, respectively, if it -is longer than `org-roam-graph-max-title-length'. - -All other values including nil will have no effect." - :type '(choice - (const :tag "truncate" truncate) - (const :tag "wrap" wrap) - (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. -If WRAP-VAL is non-nil it wraps the VAL." - (concat wrap-key (car option) wrap-key - "=" - 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))) - (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 "}") - (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. -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")) - (unless (executable-find org-roam-graph-executable) - (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)) - (temp-graph (make-temp-file "graph." nil ".svg"))) - (org-roam-message "building graph") - (make-process - :name "*org-roam-graph--build-process*" - :buffer "*org-roam-graph--build-process*" - :command `(,org-roam-graph-executable ,temp-dot "-Tsvg" "-o" ,temp-graph) - :sentinel (when callback - (lambda (process _event) - (when (= 0 (process-exit-status process)) - (funcall callback temp-graph))))))) - -(defun org-roam-graph--open (file) - "Open FILE using `org-roam-graph-viewer' with `view-file' as a fallback." - (pcase org-roam-graph-viewer - ((pred stringp) - (if (executable-find org-roam-graph-viewer) - (condition-case err - (call-process org-roam-graph-viewer nil 0 nil file) - (error (user-error "Failed to open org-roam graph: %s" err))) - (user-error "Executable not found: \"%s\"" org-roam-graph-viewer))) - ((pred functionp) (funcall org-roam-graph-viewer file)) - ('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. -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))))) - -(provide 'org-roam-graph) - -;;; org-roam-graph.el ends here diff --git a/org-roam-link.el b/org-roam-link.el index cc765fb..dcf074c 100644 --- a/org-roam-link.el +++ b/org-roam-link.el @@ -6,8 +6,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") (s "1.12.0") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2") (magit-section "2.90.1")) ;; This file is NOT part of GNU Emacs. @@ -29,255 +29,115 @@ ;;; Commentary: ;; ;; This adds the custom `roam:' link to Org-roam. `roam:' links allow linking to -;; Org-roam files via their titles and headlines. +;; Org-roam files via the node titles. ;; ;;; Code: ;;;; Dependencies (require 'ol) (require 'org-roam-compat) -(require 'org-roam-macs) +(eval-when-compile + (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 or id links whenever possible." :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) +(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. +(defun org-roam-link-follow-link (path) + "Navigates to roam: link 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." - (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))))) + (pcase-let ((`(,id ,file ,pos) (org-roam-link-locate))) + (when org-roam-link-auto-replace + (org-roam-link--replace-link id path)) + (org-id-goto id))) -;;; 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) +(defun org-roam-link--replace-link (id &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. +ID is id for the Org-roam node. 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))))) + (insert (org-link-make-string + (concat "id:" id) + desc))))) -(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-locate () + "Return the location of the roam link at point. +This is a list of three items: the node id, the file, and point +in the file." + (let ((context (org-element-context)) + path matches) + (pcase (org-element-lineage context '(link) t) + ('nil (error "Not at Org link")) + (link + (if (not (string-equal "roam" (org-element-property :type link))) + (error "Not at an Org-roam link") + (setq path (org-element-property :path link)) + (setq matches (seq-uniq + (append + (org-roam-db-query [:select [id file pos] :from nodes + :where (= title $s1)] + path) + (org-roam-db-query [:select [node-id aliases:file nodes:pos] :from aliases + :left :join nodes :on (= nodes:id aliases:node-id) + :where (= aliases:node-id $s1)] + path)))) + (cond + ((seq-empty-p matches) + ;; TODO: prompt to capture new note. + (message "No matches.")) + ((= 1 (length matches)) + (car matches)) + (_ + ;; TODO: need to fix UX somehow + (let ((choice (completing-read "Choose node:" matches nil t))) + (cdr (assoc choice matches #'string-equal)))))))))) -(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))) +;;; Retrieval Functions +(defun org-roam-link--get-nodes () + "Return all node title and aliases." + (append + (mapcar #'car (org-roam-db-query [:select [title] :from nodes])) + (mapcar #'car (org-roam-db-query [:select [alias] :from aliases])))) + +(defun org-roam-link--get-node-from-title (title) + "Return the node id for a given TITLE." + (let ((nodes (seq-uniq + (append + (mapcar #'car (org-roam-db-query [:select [id] :from nodes + :where (= title $s1)] + title)) + (mapcar #'car (org-roam-db-query [:select [node-id] :from aliases + :where (= node-id $s1)] + title)))))) + (pcase nodes + ('nil nil) + (`(,node) node) + (_ + (completing-read "Select node: " nodes))))) ;;; 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) + collection path) (when (org-in-regexp org-link-bracket-re 1) (setq start (match-beginning 1) end (match-end 1)) @@ -285,22 +145,11 @@ DESC is the link description." (pcase (org-element-lineage context '(link) t) (`nil nil) (link - (setq link-type (org-element-property :type link)) + (setq link-type (org-element-property :type link) + path (org-element-property :path 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))))))))) + (setq collection #'org-roam-link--get-nodes)))))) (when collection (let ((prefix (buffer-substring-no-properties start end))) (list start end @@ -314,11 +163,10 @@ DESC is the link description." collection) :exit-function (lambda (str &rest _) - (delete-char (- 0 (length str) - (if headline-only-p 1 0))) + (delete-char (- 0 (length str))) (insert (concat (unless (string= link-type "roam") "roam:") - (when headline-only-p "*") - str)))))))) + str)) + (forward-char 2))))))) (provide 'org-roam-link) ;;; org-roam-link.el ends here diff --git a/org-roam-macs.el b/org-roam-macs.el index f918f5c..ffcd494 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") (s "1.12.0") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2") (magit-section "2.90.1")) ;; This file is NOT part of GNU Emacs. @@ -27,31 +27,9 @@ ;;; 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-with-file (file keep-buf-p &rest body) "Execute BODY within FILE. If FILE is nil, execute BODY in the current buffer. @@ -66,6 +44,8 @@ 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 (org-mode))) (setq res (progn ,@body)) (unless (and new-buf (not ,keep-buf-p)) (save-buffer))) @@ -74,57 +54,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-mode.el b/org-roam-mode.el new file mode 100644 index 0000000..ccd4391 --- /dev/null +++ b/org-roam-mode.el @@ -0,0 +1,174 @@ +;;; 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") (s "1.12.0") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2") (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) + +(declare-function org-roam--org-file-p "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 +;; TODO: make defcustom +(defvar org-roam-last-window nil + "Last window `org-roam' was called from.") + +(defvar org-roam-mode-sections nil + "List of functions that insert sections for Org-roam.") + +;;; 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) + 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 () + "Launch an Org-roam buffer for the current node at point." + (interactive) + (unless (org-roam--org-file-p (buffer-file-name (buffer-base-buffer))) + (user-error "Not in Org-roam file")) + (let ((file (buffer-file-name)) + (buffer (get-buffer-create + (concat "org-roam: " + (file-relative-name (buffer-file-name) org-roam-directory)))) + (node (org-roam-node-at-point))) + (with-current-buffer buffer + (let ((inhibit-read-only t)) + (erase-buffer) + (org-roam-mode) + (org-roam-set-header-line-format + (org-roam-node-title node)) + (magit-insert-section (demo-buffer) + (magit-insert-heading) + (dolist (fn org-roam-mode-sections) + (funcall fn :node node :file file))))) + (switch-to-buffer-other-window buffer))) + +(provide 'org-roam-mode) +;;; org-roam-mode.el ends here diff --git a/org-roam-node.el b/org-roam-node.el new file mode 100644 index 0000000..2114516 --- /dev/null +++ b/org-roam-node.el @@ -0,0 +1,308 @@ +;;; org-roam-node.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") (s "1.12.0") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2") (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 provides functionality dealing with nodes. +;; +;;; Code: +;;;; Library Requires +(require 'magit-section) +(require 'org-roam-structs) +(require 'org-roam-mode) +(eval-when-compile + (require 'org-roam-macs)) + +(defvar org-roam-mode-sections) + +;;; Section +;;;; Definition +(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.") + +(defclass org-roam-node-section (magit-section) + ((keymap :initform org-roam-node-map) + (node :initform nil))) + +;; TODO move to own files +(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))) + +;;; Functions +(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." + (let ((node-info (car (org-roam-db-query [:select [file level pos todo priority scheduled deadline title] + :from nodes + :where (= id $s1) + :limit 1] + (org-roam-node-id node)))) + (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))))) + (pcase-let ((`(,file ,level ,pos ,todo ,priority ,scheduled ,deadline ,title) node-info)) + (setf (org-roam-node-file node) file + (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-tags node) tag-info + (org-roam-node-refs node) refs-info + (org-roam-node-aliases node) alias-info)) + node)) + +(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-let ((node (magit-section-case + (org-roam-node-section (oref it node)) + (t (let (id) + (org-with-wide-buffer + (while (and (not (setq id (org-id-get))) + (not (bobp))) + (org-up-heading-or-point-min)) + (org-roam-populate (org-roam-node-create :id id)))))))) + 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--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'." + (let ((tags-table (org-roam--tags-table))) + (cl-loop for row in (append + (org-roam-db-query [:select [file pos title title id] + :from nodes]) + (org-roam-db-query [:select [nodes:file pos alias title node-id] + :from aliases + :left-join nodes + :on (= aliases:node-id nodes:id)])) + collect (pcase-let* ((`(,file ,pos ,alias ,title ,id) row) + (node (org-roam-node-create :id id + :file file + :title title + :point pos + :tags (gethash id tags-table)))) + (cons (propertize alias 'node node) node))))) + +(defun org-roam-node-read (&optional initial-input filter-fn) + "Read and return an `org-roam-node'. +INITIAL-INPUT is the initial prompt value. +FILTER-FN is a function applied to the completion list." + (let* ((nodes (org-roam-node--completions)) + (nodes (funcall (or filter-fn #'identity) nodes)) + (node (completing-read "Node: " + (lambda (string pred action) + (if (eq action 'metadata) + '(metadata + (annotation-function . org-roam-node--annotation) + (category . org-roam-node)) + (complete-with-action action nodes string pred))) + nil nil initial-input))) + (or (cdr (assoc node nodes)) + (org-roam-node-create :title node)))) + +(defun org-roam-node--annotation (node-title) + "Return the annotation string for a NODE-TITLE." + (let* ((node (get-text-property 0 'node node-title)) + (tags (org-roam-node-tags node))) + (when tags + (format " (%s)" (string-join tags ", "))))) + +(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))) + +;;; Section inserter +(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))) + (string-join (mapcar #'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-fontify-like-in-org-mode s) "\n") + (oset section file (org-roam-node-file source-node)) + (oset section begin begin) + (oset section end end))))) + +;;;Interactives +;;;###autoload +(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. +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) + (let ((org-roam-capture--info `((title . ,(org-roam-node-title node)) + (slug . ,(funcall org-roam-title-to-slug-function + (org-roam-node-title node))))) + (org-roam-capture--context 'title)) + (setq org-roam-capture-additional-template-props (list :finalize 'find-file)) + (org-roam-capture--capture))))) + +(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. +FILTER-FN is the name of a function to apply on the candidates +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 + (let* (region-text + beg end + (_ (when (region-active-p) + (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))))) + (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))) + (let ((org-roam-capture--info + `((title . ,(org-roam-node-title node)) + (slug . ,(funcall org-roam-title-to-slug-function (org-roam-node-title node))))) + (org-roam-capture--context 'title)) + (setq org-roam-capture-additional-template-props + (list :region (when (and beg end) + (cons beg end)) + :insert-at (point-marker) + :link-description description + :finalize 'insert-link)) + (org-roam-capture--capture))))) + (deactivate-mark))) + +;;;###autoload +(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))))) + + +(provide 'org-roam-node) +;;; org-roam-node.el ends here diff --git a/org-roam-protocol.el b/org-roam-protocol.el index c058b28..1cde613 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") (s "1.12.0") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2") (magit-section "2.90.1")) ;; This file is NOT part of GNU Emacs. @@ -98,7 +98,7 @@ It should contain the FILE key, pointing to the path of the file to open. org-protocol://roam-file?file=/path/to/file.org" (when-let ((file (plist-get info :file))) (raise-frame) - (org-roam--find-file file)) + (find-file file)) nil) (push '("org-roam-ref" :protocol "roam-ref" :function org-roam-protocol-open-ref) diff --git a/org-roam-ref.el b/org-roam-ref.el new file mode 100644 index 0000000..40543bd --- /dev/null +++ b/org-roam-ref.el @@ -0,0 +1,91 @@ +;;; org-roam-node.el --- Org-roam references -*- 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") (s "1.12.0") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2") (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 provides functionality dealing with references. +;; +;; An org-roam node can contain references: these are typically sources: URLs, or cite links. +;; +;;; Code: +;;;; Library Requires +(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)))) + +(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) + (let* ((node (org-roam-ref-read initial-input filter-fn))) + (find-file (org-roam-node-file node)) + (goto-char (org-roam-node-point node)))) + +(provide 'org-roam-ref) +;;; org-roam-ref.el ends here diff --git a/org-roam-reflinks.el b/org-roam-reflinks.el new file mode 100644 index 0000000..c719519 --- /dev/null +++ b/org-roam-reflinks.el @@ -0,0 +1,91 @@ +;;; org-roam-reflinks.el --- The reflinks section -*- 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") (s "1.12.0") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2") (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 provides functionality dealing with nodes. +;; +;;; Code: +;;;; Library Requires +(require 'magit-section) +(require 'org-roam-structs) + +(defvar org-roam-mode-sections) +(defvar org-roam-mode-map) + +;;; Section +;;;; Definition + +;;; Functions +(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)))) + +;;; Section inserter +(cl-defun org-roam-reflinks-insert-section (&key node _file) + "Insert 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))))) + +(provide 'org-roam-reflinks) +;;; org-roam-reflinks.el ends here diff --git a/org-roam-dev.el b/org-roam-structs.el similarity index 51% rename from org-roam-dev.el rename to org-roam-structs.el index 47686b0..23f468e 100644 --- a/org-roam-dev.el +++ b/org-roam-structs.el @@ -1,12 +1,12 @@ -;;; org-roam-dev.el --- Org-roam development code -mode -*- coding: utf-8; lexical-binding: t; -*- +;;; org-roam-structs.el --- Structs used in 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: 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") (s "1.12.0") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2") (magit-section "2.90.1")) ;; This file is NOT part of GNU Emacs. @@ -27,20 +27,28 @@ ;;; 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. +;; 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: -(require 'emacsql) +(require 'cl-lib) -;;;###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))) +(cl-defstruct (org-roam-node (:constructor org-roam-node-create) + (:copier nil)) + id file level point todo priority scheduled deadline title + tags aliases refs) -(provide 'org-roam-dev) -;;; org-roam-dev.el ends here +(cl-defstruct (org-roam-backlink (:constructor org-roam-backlink-create) + (:copier nil)) + source-node target-node + point properties) + +(cl-defstruct (org-roam-reflink (:constructor org-roam-reflink-create) + (:copier nil)) + source-node ref + point properties) + +(provide 'org-roam-structs) +;;; org-roam-structs.el ends here diff --git a/org-roam-unlinked-references.el b/org-roam-unlinked-references.el new file mode 100644 index 0000000..7f5a1bb --- /dev/null +++ b/org-roam-unlinked-references.el @@ -0,0 +1,161 @@ +;;; org-roam-unlinked-references.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") (s "1.12.0") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2") (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 provides functionality dealing with unlinked references. +;; +;;; Code: +;;;; Library Requires +(require 'magit-section) + +(defvar org-roam-mode-sections) +(defvar org-roam-mode-map) +(defvar org-roam-file-extensions) +(defvar org-roam-directory) + +;;; Section +;;;; Faces + +;;;; Definition +(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))) + +;;; Functions +;;; TODO: move to own file +(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-olp-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.") + +;;; Section inserter +(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))))) + +(cl-defun org-roam-unlinked-references-insert-section (&key node file) + "Render unlinked references 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 " + (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)) + (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 file 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-fontify-like-in-org-mode (org-roam-unlinked-references-preview-line f row)) + "\n")))))) + (insert ?\n))))) + +(provide 'org-roam-unlinked-references) +;;; org-roam-unlinked-references.el ends here diff --git a/org-roam-utils.el b/org-roam-utils.el new file mode 100644 index 0000000..7226ef3 --- /dev/null +++ b/org-roam-utils.el @@ -0,0 +1,139 @@ +;;; 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") (s "1.12.0") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2") (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) +(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--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--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-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 "\"" "\\\""))) + +(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))) + +;;; Shielding regions +(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)))) + +;;; 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 4e61119..ecd010e 100644 --- a/org-roam.el +++ b/org-roam.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") (s "1.12.0") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite3 "1.0.2") (magit-section "2.90.1")) ;; This file is NOT part of GNU Emacs. @@ -49,20 +49,21 @@ ;;;; Features (require 'org-roam-compat) -(require 'org-roam-macs) +(eval-when-compile + (require 'org-roam-macs)) +(require 'org-roam-utils) ;; 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) +(require 'org-roam-mode) (require 'org-roam-completion) (require 'org-roam-capture) (require 'org-roam-dailies) +(require 'org-roam-ref) (require 'org-roam-db) (require 'org-roam-doctor) -(require 'org-roam-graph) (require 'org-roam-link) ;;;; Declarations @@ -73,7 +74,7 @@ (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 +;;; Customizations (defgroup org-roam nil "Roam Research replica in Org-mode." :group 'org @@ -81,6 +82,12 @@ :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-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." @@ -108,45 +115,6 @@ 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 @@ -174,100 +142,12 @@ 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-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) @@ -275,11 +155,6 @@ The currently supported symbols are: (const :tag "Omit" omit)) :group 'org-roam) -(defcustom org-roam-enable-headline-linking t - "Enable linking to headlines, which 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 @@ -288,44 +163,13 @@ The currently supported symbols are: (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)) +;;;; Faces +(defface org-roam-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) ;;;; File functions and predicates (defun org-roam--file-name-extension (filename) @@ -348,7 +192,6 @@ Like `file-name-extension', but does not strip version number." "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))))) (save-match-data @@ -361,17 +204,17 @@ If FILE is not specified, use the current buffer's file-path." (defun org-roam--shell-command-files (cmd) "Run CMD in the shell and return a list of files. If no files are found, an empty list is returned." (--> cmd - (shell-command-to-string it) - (ansi-color-filter-apply it) - (split-string it "\n") - (seq-filter #'s-present? it))) + (shell-command-to-string it) + (ansi-color-filter-apply it) + (split-string it "\n") + (seq-filter #'s-present? it))) (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-rg (executable dir) "Return all Org-roam files located recursively within DIR, using ripgrep, provided as EXECUTABLE." @@ -383,8 +226,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' @@ -450,10 +293,14 @@ 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\\)?\\'")) - result) - (dolist (file (org-roam--directory-files-recursively dir regex nil nil t) result) - (when (and (file-readable-p file) (org-roam--org-file-p file)) + (let* ((file-regex (concat "\\.\\(?:" + (mapconcat #'regexp-quote org-roam-file-extensions "\\|") + "\\)\\(?:\\.gpg\\)?\\'")) + (files (org-roam--directory-files-recursively dir file-regex nil nil t)) + result) + (dolist (file files result) + (when (and (file-readable-p file) + (org-roam--org-file-p file)) (push file result))))) (defun org-roam--list-files (dir) @@ -486,304 +333,6 @@ 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--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--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." - (let* ((prop (org-roam--extract-global-props '("ROAM_ALIAS"))) - (aliases (or (cdr (assoc "ROAM_ALIAS" prop)) - ""))) - (condition-case nil - (split-string-and-unquote aliases) - (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." - (let* ((prop (or (cdr (assoc "ROAM_TAGS" (org-roam--extract-global-props '("ROAM_TAGS")))) - ""))) - (condition-case nil - (split-string-and-unquote prop) - (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." - (-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." @@ -807,163 +356,14 @@ Each ref is returned as a cons of its type and its key." (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"))))) +(defun org-roam--tags-table () + "Return a hash table of node ID to list of tags." + (let ((ht (make-hash-table :test #'equal))) + (pcase-dolist (`(,node-id ,tag) (org-roam-db-query [:select [node-id tag] :from tags])) + (puthash node-id (cons tag (gethash node-id ht)) ht)) + ht)) ;;;; 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--get-roam-buffers () "Return a list of buffers that are Org-roam files." (--filter (and (with-current-buffer it (derived-mode-p 'org-mode)) @@ -971,144 +371,6 @@ Return nil if the file does not exist." (org-roam--org-roam-file-p (buffer-file-name it))) (buffer-list))) -;;; 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." @@ -1116,37 +378,12 @@ This function hooks into `org-open-at-point' via :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 titles and aliases in the Org-roam database." + (let* ((titles (mapcar #'car (org-roam-db-query [:select title :from nodes]))) + (aliases (mapcar #'car (org-roam-db-query [:select alias :from aliases]))) + (completions (append titles aliases))) + completions)) (defun org-roam-complete-everywhere () "`completion-at-point' function for word at point. @@ -1177,226 +414,63 @@ This is active when `org-roam-completion-everywhere' is non-nil." collection) :exit-function exit-fn))))) -(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) ;;; 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. +;;; Org-roam entry point +(defun org-roam-setup () + "Setup Org-roam." + (interactive) + (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-hook 'find-file-hook #'org-roam--find-file-hook-function) + (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)) -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-teardown () + "Teardown Org-roam." + (interactive) + (remove-hook 'find-file-hook #'org-roam--find-file-hook-function) + (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--get-roam-buffers)) + (with-current-buffer buf + (remove-hook 'after-save-hook #'org-roam-db-update-file t)))) +;;; Hooks and advices (defun org-roam--find-file-hook-function () - "Called by `find-file-hook' when mode symbol `org-roam-mode' is on." + "Setup automatic database update." (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) + (add-hook 'after-save-hook #'org-roam-db-update-file nil t) (dolist (fn org-roam-completion-functions) - (add-hook 'completion-at-point-functions fn nil t)) - (org-roam-buffer--update-maybe :redisplay t))) + (add-hook 'completion-at-point-functions fn nil t)))) (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)) + (not (backup-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)) - (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'." - (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)))))))) + (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)) @@ -1404,474 +478,16 @@ When NEW-FILE-OR-DIR is a directory, we use it to compute the new file path." (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)))) - -(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))) - -;;;###autoload -(define-minor-mode org-roam-mode - "Minor mode for Org-roam. - -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. - -When called interactively, toggle `org-roam-mode'. with prefix -ARG, enable `org-roam-mode' if ARG is positive, otherwise disable -it. - -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) - :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)))))) + (org-roam-db-clear-file old-file)) + (when (org-roam--org-roam-file-p new-file) + (org-roam-db-update-file new-file)))) ;;; Interactive Commands -;;;###autoload -(defalias 'org-roam 'org-roam-buffer-toggle-display) - -;;;###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`. -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))))) - ;;;###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) - "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 - (unwind-protect - ;; Group functions together to avoid inconsistent state on quit - (atomic-change-group - (let* (region-text - beg end - (_ (when (region-active-p) - (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)) - (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))) - -;;;###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." - (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"))))) - -;;;###autoload -(defun org-roam-alias-add () - "Add an alias to Org-roam file. - -Return added alias." - (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)) - -;;;###autoload -(defun org-roam-alias-delete () - "Delete an alias from Org-roam file." - (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"))) - -(defun org-roam-tag-add () - "Add a tag to Org-roam file. - -Return added tag." - (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))) + (find-file org-roam-directory)) (provide 'org-roam) ;;; org-roam.el ends here diff --git a/tests/test-org-roam-perf.el b/tests/test-org-roam-perf.el index 667203d..8182013 100644 --- a/tests/test-org-roam-perf.el +++ b/tests/test-org-roam-perf.el @@ -42,12 +42,12 @@ (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)) + (pcase (benchmark-run 1 (org-roam-db-sync 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)) + (pcase (benchmark-run 1 (org-roam-db-sync)) (`(,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 77cbb87..e545285 100644 --- a/tests/test-org-roam.el +++ b/tests/test-org-roam.el @@ -44,11 +44,11 @@ (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) + (org-roam-setup) (sleep-for 2))) (defun test-org-roam--teardown () - (org-roam-mode -1) + (org-roam-teardown) (delete-file org-roam-db-location) (org-roam-db--close)) @@ -68,236 +68,10 @@ ;; 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")) - (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")) - (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"))) - (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" "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 "") - :to-equal - '(title "" "" nil))) - (it "title" - (expect (org-roam-link--split-path "title") - :to-equal - '(title "title" "" nil))) - (it "title*" - (expect (org-roam-link--split-path "title*") - :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)))) + (org-link-set-parameters "cite"))) ;;; Tests -(xdescribe "org-roam-db-build-cache" +(xdescribe "org-roam-db-sync" (before-each (test-org-roam--init)) @@ -306,56 +80,14 @@ (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")))) + ;; TODO: Write tests (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) + (expect (org-roam-db-sync) :to-equal (list :files 0 :links 0 :tags 0 :titles 0 :refs 0 :deleted 0))))