branch: externals/denote commit 8e6e2736dcc539f54a87879dd0908e76962fa2cd Author: Protesilaos Stavrou <i...@protesilaos.com> Commit: Protesilaos Stavrou <i...@protesilaos.com>
Add generic denote-link-find-file --- README.org | 84 +++++++++++++++++++++----------------------- denote-link.el | 108 +++++++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 132 insertions(+), 60 deletions(-) diff --git a/README.org b/README.org index 5484472d84..fe38dfa360 100644 --- a/README.org +++ b/README.org @@ -540,55 +540,49 @@ participate in the development of Denote. :CUSTOM_ID: h:fc913d54-26c8-4c41-be86-999839e8ad31 :END: -The linking facility is subject to review and there will likely be -breaking changes. This is the only area that needs to be fixed before -we release the first stable version of the package. *What follows is -not up-to-date as of 2022-06-14 17:01 +0300.* +*The linking facility is the area that needs refinements before +releasing the first stable version of Denote.* + +Denote aims to have a simple-yet-effective linking facility to quickly +establish connections between notes. #+findex: denote-link -Denote has a basic linking facility to quickly establish connections -between notes. The command ~denote-link~ prompts for a file name in the -~denote-directory~ (only regular files are considered, not directories). -It then retrieves the path of the given note, inserts it at point using -the appropriate link notation, and creates a backlink entry in the -target file (again using the appropriate notation). +The command ~denote-link~ prompts for a file in the ~denote-directory~ +(only regular files are considered, not directories). It then retrieves +the identifier of the given note and inserts it at point using the +appropriate link notation. What constitutes "appropriate link notation" depends on the file type of -the given entry per ~denote-file-type~ ([[#h:4e9c7512-84dc-4dfb-9fa9-e15d51178e5d][The file naming scheme]]). For -example when linking from an Org file to a Markdown file, the link in -the former will follow Org syntax while the backlink in the latter will -use that of Markdown. Org links use =[[file:TARGET][DESCRIPTION]]=, -those of Markdown are =[DESCRIPTION](file:TARGET)=, while for plain text -we implement our own scheme of =<TYPE: TARGET> [DESCRIPTION]=, where -=TYPE= is either =LINK= or =BACKLINK= (capitalization in the latter two -is literal, because plain text lacks other means of emphasis). - -Plain text links can benefit from Emacs' notion of "future history", -else its ability to read the thing at point for relevant commands. With -point over the =TARGET=, =M-x find-file= followed by =M-n= will fill the -path to that file (this also works with point over just the identifier -of a note). - -#+vindex: denote-link-insert-functions -#+findex: denote-link-backlink -Backlinks are optionally recorded at the end of a note under the heading -with the title =Denote backlinks=. This is an opt-in feature that has -to be set up by adding ~denote-link-backlinks~ to the special hook -~denote-link-insert-functions~. - -The reason backlinks are off by default is because we might still make -breaking changes on how they are implemented. For the time being, -Denote expects that users do not edit the section with the backlinks: it -is controlled by Denote, such as to delete duplicate links (in the -future it might also handle stuff like alphabetic sorting). Suggestions -to improve backlinking are most welcome! - -The section with the backlinks is formatted according to the note's file -type. - -#+findex: denote-link-clear-stale-backlinks -Backlinks that no longer point to available notes can be removed from -the current buffer with the command ~denote-link-clear-stale-backlinks~. +the given entry, per ~denote-file-type~ ([[#h:4e9c7512-84dc-4dfb-9fa9-e15d51178e5d][The file naming scheme]]): + ++ Org: =[[denote:IDENTIFIER][TITLE]]= ++ Markdown: =[TITLE](denote:IDENTIFIER)= ++ Plain text: =[[IDENTIFIER] [TITLE]]= + +The special specifier =denote:= is experimental. The idea is to reserve +for possible integration with Org/Markdown, though we might switch to +another method. + +As a general feature of Emacs, any identifier that appears in a note, +can be expanded into the corresponding file name at the same directory +by leveraging Emacs' notion of "future history" (predicting what the +user wants). With point over the identifier, =M-x find-file= followed +by =M-n= will fill the path to that file. + +#+findex: denote-link-find-file +Based on this principle, Denote has a major-mode-agnostic mechanism to +collects all linked file references in the current buffer and return +them as an appropriately formatted list. This list can then be used in +interactive commands. The ~denote-link-find-file~ is one such command. +It uses minibuffer completion to visit a file that is linked to from the +current note. The candidates have the correct metadata, meaning that a +package such as =marginalia= will display the correct annotations. + +** TODO Finalise first version of denote-link.el [1/3] +*** DONE Add generic linking mechanism +*** TODO Check if denote: is the right link type for Org/Markdown +*** TODO Flesh out the ~denote-link-backlinks~ prototype +*** TODO Document use of Embark with ~denote-link-find-file~ * Fontification in Dired :PROPERTIES: diff --git a/denote-link.el b/denote-link.el index 7ab015d722..4059556053 100644 --- a/denote-link.el +++ b/denote-link.el @@ -38,25 +38,65 @@ ;;;; Link to note -(defconst denote-link--link-format-org "[[file:%s][%s (%s)]]" +;; FIXME 2022-06-14 16:58:24 +0300: Plain text links will use the +;; identifier only. But for Org we need to figure out a better way of +;; integrating with Org/Markdown in a standard way. Relying on +;; org-id.el may be the right course of action for Org. What does +;; markdown-mode require? +;; +;; Whatever we do, we need to consider the implications very carefully. +;; This is not something we can undo once the package gets its first +;; stable release. +;; +;; My principle is to avoid dependencies as much as possible. The +;; `denote-link-find-file' exemplifies this idea. +;; +;; Discussions on the GitHub mirror: +;; +;; * https://github.com/protesilaos/denote/issues/8 +;; * https://github.com/protesilaos/denote/issues/13 +;; +;; And on the mailing list: +;; +;; * https://lists.sr.ht/~protesilaos/denote/%3C9ac1913b-7e8f-7d38-b547-771861a8d641%40eh-is.de%3E +;; * https://lists.sr.ht/~protesilaos/denote/%3C87edzvd5oz.fsf%40cassou.me%3E + +;; Arguments are: FILE-ID FILE-TITLE +(defconst denote-link--format-org "[[denote:%s][%s]]" "Format of Org link to note.") -(defconst denote-link--link-format-markdown "[%2$s (%3$s)](file:%1$s)" +(defconst denote-link--format-markdown "[%2$s](denote:%1$s)" "Format of Markdown link to note.") -(defconst denote-link--link-format-text "<LINK: %s> [NAME %s (%s)]" +(defconst denote-link--format-text "[[%s] [%s]]" "Format of plain text link to note.") +(defconst denote-link--regexp-org + (concat "\\[\\[" "denote:" "\\(?1:" denote--id-regexp "\\)" "]" "\\[.*?]]")) + +(defconst denote-link--regexp-markdown + (concat "\\[.*?]" "(denote:" "\\(?1:" denote--id-regexp "\\)" ")")) + +(defconst denote-link--regexp-text + (concat "\\[\\[" "\\(?1:" denote--id-regexp "\\)" "]" "\s?" "\\[.*?]]")) + (defun denote-link--file-type-format (file) - "Return link pattern based on FILE format." + "Return link format based on FILE format." + (pcase (file-name-extension file) + ("md" denote-link--format-markdown) + ("txt" denote-link--format-text) + (_ denote-link--format-org))) ; Includes backup files. Maybe we can remove them? + +(defun denote-link--file-type-regexp (file) + "Return link regexp based on FILE format." (pcase (file-name-extension file) - ("md" denote-link--link-format-markdown) - ("txt" denote-link--link-format-text) - (_ denote-link--link-format-org))) ; Includes backup files. Maybe we can remove them? + ("md" denote-link--regexp-markdown) + ("txt" denote-link--regexp-text) + (_ denote-link--regexp-org))) (defun denote-link--format-link (file pattern) "Prepare link to FILE using PATTERN." - (let* ((file-id (denote-retrieve--value file denote-retrieve--identifier-regexp)) + (let* ((file-id (denote-retrieve--filename-identifier file)) (file-title (denote-retrieve--value file denote-retrieve--title-front-matter-regexp))) (format pattern file-id file-title))) @@ -65,14 +105,52 @@ "Create Org link to TARGET note in variable `denote-directory'. Run `denote-link-insert-functions' afterwards." (interactive (list (denote-retrieve--read-file-prompt))) - (let* ((origin (buffer-file-name)) - (link (denote-link--format-link target (denote-link--file-type-format origin)))) - (insert link))) + (insert + (denote-link--format-link + target + (denote-link--file-type-format (buffer-file-name))))) + +(defun denote-link--collect-identifiers (regexp) + "Return collection of identifiers in buffer matching REGEXP." + (let (matches) + (save-excursion + (goto-char (point-min)) + (while (re-search-forward regexp nil t) + (push (match-string-no-properties 1) matches))) + matches)) + +(defun denote-link--expand-identifiers (regexp) + "Expend identifiers matching REGEXP into file paths." + (delq nil (mapcar (lambda (i) + (file-name-completion i (denote-directory))) + (denote-link--collect-identifiers regexp)))) + +(defvar denote-link--find-file-history nil + "History for `denote-link-find-file'.") + +(defun denote-link--find-file-prompt (files) + "Prompt for linked file among FILES." + (completing-read "Find linked file " + (denote--completion-table 'file files) + nil t + nil 'denote-link--find-file-history)) + +;; TODO 2022-06-14: We should document the use of Embark for +;; `denote-link-find-file'. Users are gonna love it! + +;; TODO 2022-06-14: Do we need to add any sort of extension to better +;; integrate with Embark? For the minibuffer interaction it is not +;; necessary, but maybe it can be done to immediately recognise the +;; identifiers are links to files? -;; TODO 2022-06-14: Write `denote-link-find-file' command. It should be -;; able to enhance this core idea: -;; -;; (file-name-completion file-id dir) +;;;###autoload +(defun denote-link-find-file () + "Use minibuffer completion to visit linked file." + (interactive) + (if-let* ((regexp (denote-link--file-type-regexp (buffer-file-name))) + (files (denote-link--expand-identifiers regexp))) + (find-file (denote-link--find-file-prompt files)) + (user-error "No links found in the current buffer"))) ;;;; Backlinks' buffer (WORK-IN-PROGRESS)