Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Denote notes show filename rather instead of title #14

Closed
EFLS opened this issue Sep 24, 2022 · 14 comments
Closed

Denote notes show filename rather instead of title #14

EFLS opened this issue Sep 24, 2022 · 14 comments

Comments

@EFLS
Copy link

EFLS commented Sep 24, 2022

Many thanks for your package. I'm testing it with Denote notes, and noticed that the files listed are showing their filename rather than their title.

I was expecting to see titles, but I'm not sure whether that is a correct expectation on my end. The screenshots in the README show both (filenames and note titles) -- but perhaps that's specifically for Org-roam notes?

@mclearc
Copy link
Contributor

mclearc commented Sep 24, 2022

Unfortunately there's no support for titles as opposed to filenames outside of consult-org-roam. The latter only works because I can directly query the roam db. I also use Denote, and for me this is not an issue since the filename mirrors the title and tags. I imagine you could create a custom grep/ripgrep search for title in denote-formatted files. But the default consult-multi listing of notes uses (directory-files), which cannot be made to look inside files for titles.

@EFLS
Copy link
Author

EFLS commented Sep 24, 2022

Ah yes, that makes sense. There's denote-retrieve-title-value that will return a title from a path & file type. Not sure whether that would be usable in practice, as it opens the file in a temporary buffer (so with lots of files that might take a long time...)

@mclearc
Copy link
Contributor

mclearc commented Sep 24, 2022

Yeah, seems like it would be less expensive to pass the value of denote--title-key-regexp to a grep (or related) search. I think Deft uses a regex and grep to display titles? But I haven't used Deft in awhile so I may be wrong about this.

@EFLS
Copy link
Author

EFLS commented Sep 24, 2022

So I hacked this together (with my limited elisp skills) in order to test this:

    (setq consult--source-denotes
      `(:name     "denote"
        :items ,(lambda ()
                  (mapcar (lambda (f) (denote-retrieve-title-value f 'org))
                          (directory-files
                            denote-directory
                            'full-path
                            consult-notes-file-match)))))

    (consult-notes '(consult--source-denotes))

... this shows title notes, but I guess I then need to come up with an appropriate :action to open the notes based on their title? That seems difficult to achieve. Or what is the canonical way to deal with this? (Fairly new to `consult', so I might be missing something.)

@EFLS
Copy link
Author

EFLS commented Sep 24, 2022

I think Deft uses a regex and grep to display titles? But I haven't used Deft in awhile so I may be wrong about this.

I think Deft has some sort of hashing of contents, including the title. Don't think it is grep. But the strength of Denote is that it doesn't use any hashing at all, so that’s not really the way to go.

@mclearc
Copy link
Contributor

mclearc commented Sep 24, 2022

... this shows title notes, but I guess I then need to come up with an appropriate :action to open the notes based on their title? That seems difficult to achieve. Or what is the canonical way to deal with this? (Fairly new to `consult', so I might be missing something.)

Yes, :action will provide a function to act on the candidate. The question is whether Denote has a way to go from title back to file or id? It isn't immediately apparent to me that it does.

You might also look at the function consult-notes-org-roam--refs and related in consul-notes-org-roam.el to get a sense of how I was doing this for org-roam. Again, the sql db makes this a lot easier.

@mclearc
Copy link
Contributor

mclearc commented Sep 24, 2022

This seems to work, so long as you don't mind seeing the id # in the list:

(with-eval-after-load 'consult-notes
  (defvar consult--source-denote
    (list :name     "Denote notes"
          :narrow   ?d
          :category 'files
          :items    (lambda ()
                      (mapcar (lambda (f) (concat (denote-retrieve-filename-identifier f) "  " (denote-retrieve-title-value f 'org)))
                              (directory-files
                               path-to-files.....
                               'full-path
                               consult-notes-file-match)))
          :action   (lambda (f)
                      (let* ((file-id (denote-retrieve-filename-identifier f))
                             (id-path (denote-get-path-by-id file-id)))
                        (denote-open-or-create id-path))))))

(defun consult-denote ()
  (interactive)
  (require 'denote)
  (require 'consult-notes)
  (consult-notes '(consult--source-denote)))

@EFLS
Copy link
Author

EFLS commented Sep 25, 2022

This is great! Thanks for your help.

I'll be looking at the code more closely. One thing that can be simplified is to use (denote-directory-files) (which returns full paths to all notes) rather than (directory-files ...).

I was thinking that another way to achieve this, might be to use invisible text (like you do in consult-notes--make-source to include the directory in the items list). Maybe the identifiers can be hidden as well. I'll look into it a bit more and report back at some point.

@EFLS
Copy link
Author

EFLS commented Sep 25, 2022

This is what I came up with for the time being: I include the full path in the :items but with the invisible text property, and then use only that hidden text in :action and :annotate. (To conveniently extract only the hidden text, I had to write a little helper function.)

The only drawback of this approach is that any Embark actions still use the full :items value (i.e., path and title together).

(defun efls/return-invisible-substring (string)
  "Return the first invisible substring from STRING.
With 'invisible substring' is meant: text with the invisible property.
If no invisible text is found, return nil."
  (with-temp-buffer
    (insert string)
    (if-let ((match (text-property-search-backward 'invisible)))
      (buffer-substring-no-properties
        (prop-match-beginning match)
        (prop-match-end match)))))

(with-eval-after-load 'consult-notes
  (setq consult--source-denote
     (list :name     "Denote notes"
           :narrow   ?d
           :category 'files
           :annotate (lambda (c) (consult-notes-annotate-denote c))
           :items    (lambda ()
                      (mapcar (lambda (f)
                                (concat
                                   ;; Prepend the full path as hidden text
                                   (propertize f 'invisible t)
                                   ;; and append the title value
                                   (denote-retrieve-filename-title f)))
                                (denote-directory-files)))
          :action   (lambda (f)
                      (find-file (efls/return-invisible-substring f))))))
  
(defun consult-notes-annotate-denote (cand)
  (let* ((path (efls/return-invisible-substring cand))
        (attrs (file-attributes path))
        (ftime (consult-notes--time (file-attribute-modification-time attrs)))
        (fsize (file-size-human-readable (file-attribute-size attrs)))
        (keywords (denote-retrieve-keywords-value path 'org)))
    (put-text-property 0 (length fsize) 'face 'consult-notes-size fsize)
    (put-text-property 0 (length ftime) 'face 'consult-notes-time ftime)
    (format "%-30s  %5s  %8s"
      (if keywords
          (concat "#: "
                  (mapconcat 'identity keywords " "))
        "")
      fsize
      ftime)))

(defun consult-denote ()
  (interactive)
  (require 'denote)
  (require 'consult-notes)
  (consult-notes '(consult--source-denote)))

@gagbo
Copy link

gagbo commented Oct 4, 2022

Can't you propertize the title text with the path directly ? Like

(with-eval-after-load 'consult-notes
  (setq consult--source-denote
     (list :name     "Denote notes"
           :narrow   ?d
           :category 'files
           :annotate (lambda (c) (consult-notes-annotate-denote c))
           :items    (lambda ()
                       (mapcar (lambda (f)
                                 (propertize (denote-retrieve-filename-title f) 'denote-path f))
                                (denote-directory-files)))
           :action   (lambda (f)
                       (find-file (get-text-property 0 'denote-path f))))))

(defun consult-notes-annotate-denote (cand)
  (let* ((path (get-text-property 0 'denote-path cand))
        (attrs (file-attributes path))
        (ftime (consult-notes--time (file-attribute-modification-time attrs)))
        (fsize (file-size-human-readable (file-attribute-size attrs)))
        (keywords (denote-retrieve-keywords-value path 'org)))
    (put-text-property 0 (length fsize) 'face 'consult-notes-size fsize)
    (put-text-property 0 (length ftime) 'face 'consult-notes-time ftime)
    (format "%-30s  %5s  %8s"
      (if keywords
          (concat "#: "
                  (mapconcat 'identity keywords " "))
        "")
      fsize
      ftime)))

(defun consult-denote ()
  (interactive)
  (require 'denote)
  (require 'consult-notes)
  (consult-notes '(consult--source-denote)))

with this you don't need your helper anymore. Text-properties are very useful to attach arbitrary data to any string

@EFLS
Copy link
Author

EFLS commented Oct 4, 2022

Ooh that's even better. Great suggestion, thanks.

@ssl19
Copy link

ssl19 commented Oct 17, 2022

I have modified the snippets above a little bit, it now could filter by denote-subdirectory, keywords. And it can create new node if no match being found.

;;; consult-denote.el --- find or create denote notes using consult -*- lexical-binding: t -*-
;;; Commentary:
;;; Code:

(require 'denote)

;; This dependency is  not necessary.
(require 'consult-notes)

(defvar consult-denote--history nil)

(defconst consult-denote--source
      (list :name     "Denote notes"
            :narrow   ?d
            :category 'files
            :annotate #'consult-denote--annotate
            :items    (lambda ()
                        (let* ((max-width 0)
                               (cands (mapcar (lambda (f)
                                                (let* ((title (denote-retrieve-filename-title f))
                                                       (folder (file-relative-name (file-name-directory f) denote-directory))
                                                       (folder+title (concat (if (equal "./" folder)
                                                                                 ""
                                                                               (concat (propertize folder 'face 'shadow) " - "))
                                                                             title))
                                                       (keywords (denote-extract-keywords-from-path f)))
                                                  (let ((current-width (string-width folder+title)))
                                                    (when (> current-width max-width)
                                                      (setq max-width current-width)))
                                                  (propertize folder+title 'denote-path f 'denote-keywords keywords)))
                                              (denote-directory-files))))
                          (mapcar (lambda (c)
                                    (let ((keywords (get-text-property 0 'denote-keywords c)))
                                      (concat c
                                              ;; align keywords
                                              (propertize " " 'display `(space :align-to (+ left ,(+ 2 max-width))))
                                              (format "%-30s"
                                                      (if keywords
                                                          (concat "#: "
                                                                  (propertize (mapconcat 'identity keywords " ") 'face 'org-tag))
                                                        "")))))
                                  cands)))
            :action   (lambda (f)
                        (find-file (get-text-property 0 'denote-path f)))
            ;; create new if not exist
            :new     (lambda (cand)
                       (let* ((f (expand-file-name cand denote-directory))
                              (f-dir (file-name-directory f))
                              (f-name-base (file-name-base f))
                              (file-type (consult-denote--extension-file-type f))
                              keywords  date template)
                         (dolist (prompt denote-prompts)
                           (pcase prompt
                             ('keywords (setq keywords (denote-keywords-prompt)))
                             ('file-type (setq file-type (denote-file-type-prompt)))
                             ('date (setq date (denote-date-prompt)))
                             ('template (setq template (denote-template-prompt)))))
                         (denote (string-trim f-name-base) keywords file-type f-dir date template)))))

(defun consult-denote--extension-file-type (f)
  "Return denote file-type of F."
  (pcase (file-name-extension f)
    ("org" "org")
    ("md" "markdown-toml")
    ("txt" "text")))

(defun consult-denote--annotate (cand)
  "Annotate CAND in `consult-denote'."
  (let* ((path (get-text-property 0 'denote-path cand))
         (attrs (file-attributes path))
         (ftime (consult-notes--time (file-attribute-modification-time attrs)))
         (fsize (file-size-human-readable (file-attribute-size attrs))))
    (put-text-property 0 (length fsize) 'face 'consult-notes-size fsize)
    (put-text-property 0 (length ftime) 'face 'consult-notes-time ftime)
    (format "%5s  %8s"
            fsize
            ftime)))


;;;###autoload
(defun consult-denote ()
  "Find or create denote notes with consult.

Find notes: filter by denote-subdirectory, notes title and keywords.

Create notes: when a note not found in completion or exit minibufer with raw input.

Three scenarios:

    Input \"foo\", then create \"id-foo\", file type is determined by
    `denote-file-type' or choose manually when `denote-prompts'
    includes 'file-type. For example, if `denote-file-type' is
    nil , then create \"id-foo.org\".

    Input \"foo.txt\", create \"id-foo.txt\", check
    `consult-denote--extension-file-type' for more information.

    Input \"bar/foo.txt\", create \"id-foo.txt\" in denote-subdirectory named \"bar\"."
  (interactive)
  (consult--multi '(consult-denote--source)
                  :prompt "Denote: "
                  :history consult-denote--history))

(provide 'consult-denote)
;;; consult-denote.el ends here

@mclearc
Copy link
Contributor

mclearc commented Oct 17, 2022

When I get some time (hopefully soon) I'll provide a denote back-end like I do for org-roam.

@mclearc
Copy link
Contributor

mclearc commented Nov 5, 2022

Closed by 7451e51

@mclearc mclearc closed this as completed Nov 5, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants