From c39d7de783bc03d3fad8108629ff2d48f89de9fe Mon Sep 17 00:00:00 2001 From: KemoNine Date: Thu, 25 Aug 2022 12:12:30 -0400 Subject: [PATCH] integrate org-noter --- .../org-noter-autoloads.el | 65 + .../org-noter-20191020.1212/org-noter-pkg.el | 2 + org/elpa/org-noter-20191020.1212/org-noter.el | 2296 +++++++++++++++++ org/init.el | 2 +- 4 files changed, 2364 insertions(+), 1 deletion(-) create mode 100644 org/elpa/org-noter-20191020.1212/org-noter-autoloads.el create mode 100644 org/elpa/org-noter-20191020.1212/org-noter-pkg.el create mode 100644 org/elpa/org-noter-20191020.1212/org-noter.el diff --git a/org/elpa/org-noter-20191020.1212/org-noter-autoloads.el b/org/elpa/org-noter-20191020.1212/org-noter-autoloads.el new file mode 100644 index 0000000..c856820 --- /dev/null +++ b/org/elpa/org-noter-20191020.1212/org-noter-autoloads.el @@ -0,0 +1,65 @@ +;;; org-noter-autoloads.el --- automatically extracted autoloads -*- lexical-binding: t -*- +;; +;;; Code: + +(add-to-list 'load-path (directory-file-name + (or (file-name-directory #$) (car load-path)))) + + +;;;### (autoloads nil "org-noter" "org-noter.el" (0 0 0 0)) +;;; Generated autoloads from org-noter.el + +(autoload 'org-noter "org-noter" "\ +Start `org-noter' session. + +There are two modes of operation. You may create the session from: +- The Org notes file +- The document to be annotated (PDF, EPUB, ...) + +- Creating the session from notes file ----------------------------------------- +This will open a session for taking your notes, with indirect +buffers to the document and the notes side by side. Your current +window configuration won't be changed, because this opens in a +new frame. + +You only need to run this command inside a heading (which will +hold the notes for this document). If no document path property is found, +this command will ask you for the target file. + +With a prefix universal argument ARG, only check for the property +in the current heading, don't inherit from parents. + +With 2 prefix universal arguments ARG, ask for a new document, +even if the current heading annotates one. + +With a prefix number ARG: +- Greater than 0: Open the document like `find-file' +- Equal to 0: Create session with `org-noter-always-create-frame' toggled +- Less than 0: Open the folder containing the document + +- Creating the session from the document --------------------------------------- +This will try to find a notes file in any of the parent folders. +The names it will search for are defined in `org-noter-default-notes-file-names'. +It will also try to find a notes file with the same name as the +document, giving it the maximum priority. + +When it doesn't find anything, it will interactively ask you what +you want it to do. The target notes file must be in a parent +folder (direct or otherwise) of the document. + +You may pass a prefix ARG in order to make it let you choose the +notes file, even if it finds one. + +\(fn &optional ARG)" t nil) + +(register-definition-prefixes "org-noter" '("org-noter-")) + +;;;*** + +;; Local Variables: +;; version-control: never +;; no-byte-compile: t +;; no-update-autoloads: t +;; coding: utf-8 +;; End: +;;; org-noter-autoloads.el ends here diff --git a/org/elpa/org-noter-20191020.1212/org-noter-pkg.el b/org/elpa/org-noter-20191020.1212/org-noter-pkg.el new file mode 100644 index 0000000..045dcbd --- /dev/null +++ b/org/elpa/org-noter-20191020.1212/org-noter-pkg.el @@ -0,0 +1,2 @@ +;;; Generated package description from org-noter.el -*- no-byte-compile: t -*- +(define-package "org-noter" "20191020.1212" "A synchronized, Org-mode, document annotator" '((emacs "24.4") (cl-lib "0.6") (org "9.0")) :commit "9ead81d42dd4dd5074782d239b2efddf9b8b7b3d" :authors '((nil . "Gonçalo Santos (aka. weirdNox@GitHub)")) :maintainer '(nil . "Gonçalo Santos (aka. weirdNox@GitHub)") :keywords '("lisp" "pdf" "interleave" "annotate" "external" "sync" "notes" "documents" "org-mode") :url "https://github.com/weirdNox/org-noter") diff --git a/org/elpa/org-noter-20191020.1212/org-noter.el b/org/elpa/org-noter-20191020.1212/org-noter.el new file mode 100644 index 0000000..428d0d4 --- /dev/null +++ b/org/elpa/org-noter-20191020.1212/org-noter.el @@ -0,0 +1,2296 @@ +;;; org-noter.el --- A synchronized, Org-mode, document annotator -*- lexical-binding: t; -*- + +;; Copyright (C) 2017-2018 Gonçalo Santos + +;; Author: Gonçalo Santos (aka. weirdNox@GitHub) +;; Homepage: https://github.com/weirdNox/org-noter +;; Keywords: lisp pdf interleave annotate external sync notes documents org-mode +;; Package-Version: 20191020.1212 +;; Package-Commit: 9ead81d42dd4dd5074782d239b2efddf9b8b7b3d +;; Package-Requires: ((emacs "24.4") (cl-lib "0.6") (org "9.0")) +;; Version: 1.4.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 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; The idea is to let you create notes that are kept in sync when you scroll through the +;; document, but that are external to it - the notes themselves live in an Org-mode file. As +;; such, this leverages the power of Org-mode (the notes may have outlines, latex fragments, +;; babel, etc...) while acting like notes that are made /in/ the document. + +;; Also, I must thank Sebastian for the original idea and inspiration! +;; Link to the original Interleave package: +;; https://github.com/rudolfochrist/interleave + +;;; Code: +(require 'org) +(require 'org-element) +(require 'cl-lib) + +(declare-function doc-view-goto-page "doc-view") +(declare-function image-display-size "image-mode") +(declare-function image-get-display-property "image-mode") +(declare-function image-mode-window-get "image-mode") +(declare-function image-scroll-up "image-mode") +(declare-function nov-render-document "ext:nov") +(declare-function org-attach-dir "org-attach") +(declare-function org-attach-file-list "org-attach") +(declare-function pdf-info-getannots "ext:pdf-info") +(declare-function pdf-info-gettext "ext:pdf-info") +(declare-function pdf-info-outline "ext:pdf-info") +(declare-function pdf-info-pagelinks "ext:pdf-info") +(declare-function pdf-util-tooltip-arrow "ext:pdf-util") +(declare-function pdf-view-active-region "ext:pdf-view") +(declare-function pdf-view-active-region-p "ext:pdf-view") +(declare-function pdf-view-active-region-text "ext:pdf-view") +(declare-function pdf-view-goto-page "ext:pdf-view") +(declare-function pdf-view-mode "ext:pdf-view") +(defvar nov-documents-index) +(defvar nov-file-name) + +;; -------------------------------------------------------------------------------- +;; NOTE(nox): User variables +(defgroup org-noter nil + "A synchronized, external annotator" + :group 'convenience + :version "25.3.1") + +(defcustom org-noter-property-doc-file "NOTER_DOCUMENT" + "Name of the property that specifies the document." + :group 'org-noter + :type 'string) + +(defcustom org-noter-property-note-location "NOTER_PAGE" + "Name of the property that specifies the location of the current note. +The default value is still NOTER_PAGE for backwards compatibility." + :group 'org-noter + :type 'string) + +(defcustom org-noter-default-heading-title "Notes for page $p$" + "The default title for headings created with `org-noter-insert-note'. +$p$ is replaced with the number of the page or chapter you are in +at the moment." + :group 'org-noter + :type 'string) + +(defcustom org-noter-notes-window-behavior '(start scroll) + "This setting specifies in what situations the notes window should be created. + +When the list contains: +- `start', the window will be created when starting a `org-noter' session. +- `scroll', it will be created when you go to a location with an associated note. +- `only-prev', it will be created when you go to a location without notes, but that + has previous notes that are shown." + :group 'org-noter + :type '(set (const :tag "Session start" start) + (const :tag "Scroll to location with notes" scroll) + (const :tag "Scroll to location with previous notes only" only-prev))) + +(defcustom org-noter-notes-window-location 'horizontal-split + "Whether the notes should appear in the main frame (horizontal or vertical split) or in a separate frame. + +Note that this will only have effect on session startup if `start' +is member of `org-noter-notes-window-behavior' (which see)." + :group 'org-noter + :type '(choice (const :tag "Horizontal" horizontal-split) + (const :tag "Vertical" vertical-split) + (const :tag "Other frame" other-frame))) + +(define-obsolete-variable-alias 'org-noter-doc-split-percentage 'org-noter-doc-split-fraction "1.2.0") +(defcustom org-noter-doc-split-fraction '(0.5 . 0.5) + "Fraction of the frame that the document window will occupy when split. +This is a cons of the type (HORIZONTAL-FRACTION . VERTICAL-FRACTION)." + :group 'org-noter + :type '(cons (number :tag "Horizontal fraction") (number :tag "Vertical fraction"))) + +(defcustom org-noter-auto-save-last-location nil + "When non-nil, save the last visited location automatically; when starting a new session, go to that location." + :group 'org-noter + :type 'boolean) + +(defcustom org-noter-hide-other t + "When non-nil, hide all headings not related to the command used. +For example, when scrolling to pages with notes, collapse all the +notes that are not annotating the current page." + :group 'org-noter + :type 'boolean) + +(defcustom org-noter-always-create-frame t + "When non-nil, org-noter will always create a new frame for the session. +When nil, it will use the selected frame if it does not belong to any other session." + :group 'org-noter + :type 'boolean) + +(defcustom org-noter-suggest-from-attachments t + "When non-nil, org-noter will suggest files from the attachments +when creating a session, if the document is missing." + :group 'org-noter + :type 'boolean) + +(defcustom org-noter-separate-notes-from-heading nil + "When non-nil, add an empty line between each note's heading and content." + :group 'org-noter + :type 'boolean) + +(defcustom org-noter-insert-selected-text-inside-note t + "When non-nil, it will automatically append the selected text into an existing note." + :group 'org-noter + :type 'boolean) + +(defcustom org-noter-closest-tipping-point 0.3 + "Defines when to show the closest previous note. + +Let x be (this value)*100. The following schematic represents the +view (eg. a page of a PDF): + ++----+ +| | -> If there are notes in here, the closest previous note is not shown ++----+--> Tipping point, at x% of the view +| | -> When _all_ notes are in here, below the tipping point, the closest +| | previous note will be shown. ++----+ + +When this value is negative, disable this feature. + +This setting may be overridden in a document with the function +`org-noter-set-closest-tipping-point', which see." + :group 'org-noter + :type 'number) + +(defcustom org-noter-default-notes-file-names '("Notes.org") + "List of possible names for the default notes file, in increasing order of priority." + :group 'org-noter + :type '(repeat string)) + +(defcustom org-noter-notes-search-path '("~/Documents") + "List of paths to check (non recursively) when searching for a notes file." + :group 'org-noter + :type '(repeat string)) + +(defcustom org-noter-arrow-delay 0.2 + "Number of seconds from when the command was invoked until the tooltip arrow appears. + +When set to a negative number, the arrow tooltip is disabled. +This is needed in order to keep Emacs from hanging when doing many syncs." + :group 'org-noter + :type 'number) + +(defcustom org-noter-doc-property-in-notes nil + "If non-nil, every new note will have the document property too. +This makes moving notes out of the root heading easier." + :group 'org-noter + :type 'boolean) + +(defcustom org-noter-insert-note-no-questions nil + "When non-nil, `org-noter-insert-note' won't ask for a title and will always insert a new note. +The title used will be the default one." + :group 'org-noter + :type 'boolean) + +(defcustom org-noter-kill-frame-at-session-end t + "If non-nil, `org-noter-kill-session' will delete the frame if others exist on the current display.'" + :group 'org-noter + :type 'boolean) + +(defcustom org-noter-insert-heading-hook nil + "Hook being run after inserting a new heading." + :group 'org-noter + :type 'hook) + +(defface org-noter-no-notes-exist-face + '((t + :foreground "chocolate" + :weight bold)) + "Face for modeline note count, when 0." + :group 'org-noter) + +(defface org-noter-notes-exist-face + '((t + :foreground "SpringGreen" + :weight bold)) + "Face for modeline note count, when not 0." + :group 'org-noter) + +;; -------------------------------------------------------------------------------- +;; NOTE(nox): Integration with other packages +(defcustom org-noter--check-location-property-hook nil + "TODO" + :group 'org-noter + :type 'hook) + +(defcustom org-noter--parse-location-property-hook nil + "TODO" + :group 'org-noter + :type 'hook) + +(defcustom org-noter--pretty-print-location-hook nil + "TODO" + :group 'org-noter + :type 'hook) + +(defcustom org-noter--convert-to-location-cons-hook nil + "TODO" + :group 'org-noter + :type 'hook) + +(defcustom org-noter--doc-goto-location-hook nil + "TODO" + :group 'org-noter + :type 'hook) + +(defcustom org-noter--note-after-tipping-point-hook nil + "TODO" + :group 'org-noter + :type 'hook) + +(defcustom org-noter--relative-position-to-view-hook nil + "TODO" + :group 'org-noter + :type 'hook) + +(defcustom org-noter--get-precise-info-hook nil + "TODO" + :group 'org-noter + :type 'hook) + +(defcustom org-noter--doc-approx-location-hook nil + "TODO" + :group 'org-noter + :type 'hook) + +;; -------------------------------------------------------------------------------- +;; NOTE(nox): Private variables or constants +(cl-defstruct org-noter--session + id frame doc-buffer notes-buffer ast modified-tick doc-mode display-name notes-file-path property-text + level num-notes-in-view window-behavior window-location doc-split-fraction auto-save-last-location + hide-other closest-tipping-point) + +(defvar org-noter--sessions nil + "List of `org-noter' sessions.") + +(defvar-local org-noter--session nil + "Session associated with the current buffer.") + +(defvar org-noter--inhibit-location-change-handler nil + "Prevent location change from updating point in notes.") + +(defvar org-noter--start-location-override nil + "Used to open the session from the document in the right page.") + +(defvar-local org-noter--nov-timer nil + "Timer for synchronizing notes after scrolling.") + +(defvar org-noter--arrow-location nil + "A vector [TIMER WINDOW TOP] that shows where the arrow should appear, when idling.") + +(defvar org-noter--completing-read-keymap (make-sparse-keymap) + "A `completing-read' keymap that let's the user insert spaces.") + +(set-keymap-parent org-noter--completing-read-keymap minibuffer-local-completion-map) +(define-key org-noter--completing-read-keymap (kbd "SPC") 'self-insert-command) + +(defconst org-noter--property-behavior "NOTER_NOTES_BEHAVIOR" + "Property for overriding global `org-noter-notes-window-behavior'.") + +(defconst org-noter--property-location "NOTER_NOTES_LOCATION" + "Property for overriding global `org-noter-notes-window-location'.") + +(defconst org-noter--property-doc-split-fraction "NOTER_DOCUMENT_SPLIT_FRACTION" + "Property for overriding global `org-noter-doc-split-fraction'.") + +(defconst org-noter--property-auto-save-last-location "NOTER_AUTO_SAVE_LAST_LOCATION" + "Property for overriding global `org-noter-auto-save-last-location'.") + +(defconst org-noter--property-hide-other "NOTER_HIDE_OTHER" + "Property for overriding global `org-noter-hide-other'.") + +(defconst org-noter--property-closest-tipping-point "NOTER_CLOSEST_TIPPING_POINT" + "Property for overriding global `org-noter-closest-tipping-point'.") + +(defconst org-noter--note-search-no-recurse (delete 'headline (append org-element-all-elements nil)) + "List of elements that shouldn't be recursed into when searching for notes.") + +(defconst org-noter--id-text-property 'org-noter-session-id + "Text property used to mark the headings with open sessions.") + +;; -------------------------------------------------------------------------------- +;; NOTE(nox): Utility functions +(defun org-noter--get-new-id () + (catch 'break + (while t + (let ((id (random most-positive-fixnum))) + (unless (cl-loop for session in org-noter--sessions + when (= (org-noter--session-id session) id) return t) + (throw 'break id)))))) + +(defmacro org-noter--property-or-default (name) + (let ((function-name (intern (concat "org-noter--" (symbol-name name) "-property"))) + (variable (intern (concat "org-noter-" (symbol-name name))))) + `(let ((prop-value (,function-name ast))) + (cond ((eq prop-value 'disable) nil) + (prop-value) + (t ,variable))))) + +(defun org-noter--create-session (ast document-property-value notes-file-path) + (let* ((raw-value-not-empty (> (length (org-element-property :raw-value ast)) 0)) + (display-name (if raw-value-not-empty + (org-element-property :raw-value ast) + (file-name-nondirectory document-property-value))) + (frame-name (format "Emacs Org-noter - %s" display-name)) + + (document (find-file-noselect document-property-value)) + (document-path (expand-file-name document-property-value)) + (document-major-mode (buffer-local-value 'major-mode document)) + (document-buffer-name + (generate-new-buffer-name (concat (unless raw-value-not-empty "Org-noter: ") display-name))) + (document-buffer + (if (eq document-major-mode 'nov-mode) + document + (make-indirect-buffer document document-buffer-name t))) + + (notes-buffer + (make-indirect-buffer + (or (buffer-base-buffer) (current-buffer)) + (generate-new-buffer-name (concat "Notes of " display-name)) t)) + + (session + (make-org-noter--session + :id (org-noter--get-new-id) + :display-name display-name + :frame + (if (or org-noter-always-create-frame + (catch 'has-session + (dolist (test-session org-noter--sessions) + (when (eq (org-noter--session-frame test-session) (selected-frame)) + (throw 'has-session t))))) + (make-frame `((name . ,frame-name) (fullscreen . maximized))) + (set-frame-parameter nil 'name frame-name) + (selected-frame)) + :doc-mode document-major-mode + :property-text document-property-value + :notes-file-path notes-file-path + :doc-buffer document-buffer + :notes-buffer notes-buffer + :level (org-element-property :level ast) + :window-behavior (org-noter--property-or-default notes-window-behavior) + :window-location (org-noter--property-or-default notes-window-location) + :doc-split-fraction (org-noter--property-or-default doc-split-fraction) + :auto-save-last-location (org-noter--property-or-default auto-save-last-location) + :hide-other (org-noter--property-or-default hide-other) + :closest-tipping-point (org-noter--property-or-default closest-tipping-point) + :modified-tick -1)) + + (target-location org-noter--start-location-override) + (starting-point (point))) + + (add-hook 'delete-frame-functions 'org-noter--handle-delete-frame) + (push session org-noter--sessions) + + (with-current-buffer document-buffer + (cond + ;; NOTE(nox): PDF Tools + ((eq document-major-mode 'pdf-view-mode) + (setq buffer-file-name document-path) + (pdf-view-mode) + (add-hook 'pdf-view-after-change-page-hook 'org-noter--doc-location-change-handler nil t)) + + ;; NOTE(nox): DocView + ((eq document-major-mode 'doc-view-mode) + (setq buffer-file-name document-path) + (doc-view-mode) + (advice-add 'doc-view-goto-page :after 'org-noter--location-change-advice)) + + ;; NOTE(nox): Nov.el + ((eq document-major-mode 'nov-mode) + (rename-buffer document-buffer-name) + (advice-add 'nov-render-document :after 'org-noter--nov-scroll-handler) + (add-hook 'window-scroll-functions 'org-noter--nov-scroll-handler nil t)) + + (t (error "This document handler is not supported :/"))) + + (org-noter-doc-mode 1) + (setq org-noter--session session) + (add-hook 'kill-buffer-hook 'org-noter--handle-kill-buffer nil t)) + + (with-current-buffer notes-buffer + (org-noter-notes-mode 1) + ;; NOTE(nox): This is needed because a session created in an indirect buffer would use the point of + ;; the base buffer (as this buffer is indirect to the base!) + (goto-char starting-point) + (setq buffer-file-name notes-file-path + org-noter--session session + fringe-indicator-alist '((truncation . nil))) + (add-hook 'kill-buffer-hook 'org-noter--handle-kill-buffer nil t) + (add-hook 'window-scroll-functions 'org-noter--set-notes-scroll nil t) + (org-noter--set-text-properties (org-noter--parse-root (vector notes-buffer document-property-value)) + (org-noter--session-id session)) + (unless target-location + (setq target-location (org-noter--parse-location-property (org-noter--get-containing-heading t))))) + + (org-noter--setup-windows session) + + ;; NOTE(nox): This timer is for preventing reflowing too soon. + (run-with-idle-timer + 0.05 nil + (lambda () + (with-current-buffer document-buffer + (let ((org-noter--inhibit-location-change-handler t)) + (when target-location (org-noter--doc-goto-location target-location))) + (org-noter--doc-location-change-handler)))))) + +(defun org-noter--valid-session (session) + (when session + (if (and (frame-live-p (org-noter--session-frame session)) + (buffer-live-p (org-noter--session-doc-buffer session)) + (buffer-live-p (org-noter--session-notes-buffer session))) + t + (org-noter-kill-session session) + nil))) + +(defmacro org-noter--with-valid-session (&rest body) + (declare (debug (body))) + `(let ((session org-noter--session)) + (when (org-noter--valid-session session) + (progn ,@body)))) + +(defun org-noter--handle-kill-buffer () + (org-noter--with-valid-session + (let ((buffer (current-buffer)) + (notes-buffer (org-noter--session-notes-buffer session)) + (doc-buffer (org-noter--session-doc-buffer session))) + ;; NOTE(nox): This needs to be checked in order to prevent session killing because of + ;; temporary buffers with the same local variables + (when (or (eq buffer notes-buffer) + (eq buffer doc-buffer)) + (org-noter-kill-session session))))) + +(defun org-noter--handle-delete-frame (frame) + (dolist (session org-noter--sessions) + (when (eq (org-noter--session-frame session) frame) + (org-noter-kill-session session)))) + +(defun org-noter--parse-root (&optional info) + "Parse and return the root AST. +When used, the INFO argument may be an org-noter session or a vector [NotesBuffer PropertyText]. +If nil, the session used will be `org-noter--session'." + (let* ((arg-is-session (org-noter--session-p info)) + (session (or (and arg-is-session info) org-noter--session)) + root-pos ast) + (cond + ((and (not arg-is-session) (vectorp info)) + ;; NOTE(nox): Use arguments to find heading, by trying to find the outermost parent heading with + ;; the specified property + (let ((notes-buffer (aref info 0)) + (wanted-prop (aref info 1))) + (unless (and (buffer-live-p notes-buffer) (stringp wanted-prop) + (eq (buffer-local-value 'major-mode notes-buffer) 'org-mode)) + (error "Error parsing root with invalid arguments")) + + (with-current-buffer notes-buffer + (org-with-wide-buffer + (catch 'break + (org-back-to-heading t) + (while t + (when (string= (org-entry-get nil org-noter-property-doc-file) wanted-prop) + (setq root-pos (copy-marker (point)))) + (unless (org-up-heading-safe) (throw 'break t)))))))) + + ((org-noter--valid-session session) + ;; NOTE(nox): Use session to find heading + (or (and (= (buffer-chars-modified-tick (org-noter--session-notes-buffer session)) + (org-noter--session-modified-tick session)) + (setq ast (org-noter--session-ast session))) ; NOTE(nox): Cached version! + + ;; NOTE(nox): Find session id text property + (with-current-buffer (org-noter--session-notes-buffer session) + (org-with-wide-buffer + (let ((pos (text-property-any (point-min) (point-max) org-noter--id-text-property + (org-noter--session-id session)))) + (when pos (setq root-pos (copy-marker pos))))))))) + + (unless ast + (unless root-pos (error "Root heading not found")) + (with-current-buffer (marker-buffer root-pos) + (org-with-wide-buffer + (goto-char (marker-position root-pos)) + (org-narrow-to-subtree) + (setq ast (car (org-element-contents (org-element-parse-buffer 'greater-element)))) + (when (and (not (vectorp info)) (org-noter--valid-session session)) + (setf (org-noter--session-ast session) ast + (org-noter--session-modified-tick session) (buffer-chars-modified-tick)))))) + ast)) + +(defun org-noter--get-properties-end (ast &optional force-trim) + (when ast + (let* ((contents (org-element-contents ast)) + (section (org-element-map contents 'section 'identity nil t 'headline)) + (properties (org-element-map section 'property-drawer 'identity nil t)) + properties-end) + (if (not properties) + (org-element-property :contents-begin ast) + (setq properties-end (org-element-property :end properties)) + (when (or force-trim + (= (org-element-property :end section) properties-end)) + (while (not (eq (char-before properties-end) ?:)) + (setq properties-end (1- properties-end)))) + properties-end)))) + +(defun org-noter--set-text-properties (ast id) + (org-with-wide-buffer + (when ast + (let* ((level (org-element-property :level ast)) + (begin (org-element-property :begin ast)) + (title-begin (+ 1 level begin)) + (contents-begin (org-element-property :contents-begin ast)) + (properties-end (org-noter--get-properties-end ast t)) + (inhibit-read-only t) + (modified (buffer-modified-p))) + (add-text-properties (max 1 (1- begin)) begin '(read-only t)) + (add-text-properties begin (1- title-begin) `(read-only t front-sticky t ,org-noter--id-text-property ,id)) + (add-text-properties (1- title-begin) title-begin '(read-only t rear-nonsticky t)) + (add-text-properties (1- contents-begin) (1- properties-end) '(read-only t)) + (add-text-properties (1- properties-end) properties-end + '(read-only t rear-nonsticky t)) + (set-buffer-modified-p modified))))) + +(defun org-noter--unset-text-properties (ast) + (when ast + (org-with-wide-buffer + (let* ((begin (org-element-property :begin ast)) + (end (org-noter--get-properties-end ast t)) + (inhibit-read-only t) + (modified (buffer-modified-p))) + (remove-list-of-text-properties (max 1 (1- begin)) end + `(read-only front-sticky rear-nonsticky ,org-noter--id-text-property)) + (set-buffer-modified-p modified))))) + +(defun org-noter--set-notes-scroll (window &rest ignored) + (when window + (with-selected-window window + (org-noter--with-valid-session + (let* ((level (org-noter--session-level session)) + (goal (* (1- level) 2)) + (current-scroll (window-hscroll))) + (when (and (bound-and-true-p org-indent-mode) (< current-scroll goal)) + (scroll-right current-scroll) + (scroll-left goal t))))))) + +(defun org-noter--insert-heading (level title &optional newlines-number location) + "Insert a new heading at LEVEL with TITLE. +The point will be at the start of the contents, after any +properties, by a margin of NEWLINES-NUMBER." + (setq newlines-number (or newlines-number 1)) + (org-insert-heading nil t) + (let* ((initial-level (org-element-property :level (org-element-at-point))) + (changer (if (> level initial-level) 'org-do-demote 'org-do-promote)) + (number-of-times (abs (- level initial-level)))) + (dotimes (_ number-of-times) (funcall changer)) + (insert (org-trim (replace-regexp-in-string "\n" " " title))) + + (org-end-of-subtree) + (unless (bolp) (insert "\n")) + (org-N-empty-lines-before-current (1- newlines-number)) + + (when location + (org-entry-put nil org-noter-property-note-location (org-noter--pretty-print-location location)) + + (when org-noter-doc-property-in-notes + (org-noter--with-valid-session + (org-entry-put nil org-noter-property-doc-file (org-noter--session-property-text session)) + (org-entry-put nil org-noter--property-auto-save-last-location "nil")))) + + (run-hooks 'org-noter-insert-heading-hook))) + +(defun org-noter--narrow-to-root (ast) + (when ast + (save-excursion + (goto-char (org-element-property :contents-begin ast)) + (org-show-entry) + (org-narrow-to-subtree) + (org-cycle-hide-drawers 'all)))) + +(defun org-noter--get-doc-window () + (org-noter--with-valid-session + (or (get-buffer-window (org-noter--session-doc-buffer session) + (org-noter--session-frame session)) + (org-noter--setup-windows org-noter--session) + (get-buffer-window (org-noter--session-doc-buffer session) + (org-noter--session-frame session))))) + +(defun org-noter--get-notes-window (&optional type) + (org-noter--with-valid-session + (let ((notes-buffer (org-noter--session-notes-buffer session)) + (window-location (org-noter--session-window-location session)) + (window-behavior (org-noter--session-window-behavior session)) + notes-window) + (or (get-buffer-window notes-buffer t) + (when (or (eq type 'force) (memq type window-behavior)) + (if (eq window-location 'other-frame) + (let ((restore-frame (selected-frame))) + (switch-to-buffer-other-frame notes-buffer) + (setq notes-window (get-buffer-window notes-buffer t)) + (x-focus-frame restore-frame) + (raise-frame (window-frame notes-window))) + + (with-selected-window (org-noter--get-doc-window) + (let ((horizontal (eq window-location 'horizontal-split))) + (setq + notes-window + (if (window-combined-p nil horizontal) + ;; NOTE(nox): Reuse already existent window + (let ((sibling-window (or (window-next-sibling) (window-prev-sibling)))) + (or (window-top-child sibling-window) (window-left-child sibling-window) + sibling-window)) + + (if horizontal + (split-window-right (ceiling (* (car (org-noter--session-doc-split-fraction session)) + (window-total-width)))) + (split-window-below (ceiling (* (cdr (org-noter--session-doc-split-fraction session)) + (window-total-height))))))))) + + (set-window-buffer notes-window notes-buffer)) + notes-window))))) + +(defun org-noter--setup-windows (session) + "Setup windows when starting session, respecting user configuration." + (when (org-noter--valid-session session) + (with-selected-frame (org-noter--session-frame session) + (delete-other-windows) + (let* ((doc-buffer (org-noter--session-doc-buffer session)) + (doc-window (selected-window)) + (notes-buffer (org-noter--session-notes-buffer session)) + notes-window) + + (set-window-buffer doc-window doc-buffer) + (set-window-dedicated-p doc-window t) + + (with-current-buffer notes-buffer + (org-noter--narrow-to-root (org-noter--parse-root session)) + (setq notes-window (org-noter--get-notes-window 'start)) + (org-noter--set-notes-scroll notes-window)))))) + +(defmacro org-noter--with-selected-notes-window (error-str &rest body) + (declare (debug ([&optional stringp] body))) + (let ((with-error (stringp error-str))) + `(org-noter--with-valid-session + (let ((notes-window (org-noter--get-notes-window))) + (if notes-window + (with-selected-window notes-window + ,(if with-error + `(progn ,@body) + (if body + `(progn ,error-str ,@body) + `(progn ,error-str)))) + ,(when with-error `(user-error "%s" ,error-str))))))) + +(defun org-noter--notes-window-behavior-property (ast) + (let ((property (org-element-property (intern (concat ":" org-noter--property-behavior)) ast)) + value) + (when (and (stringp property) (> (length property) 0)) + (setq value (car (read-from-string property))) + (when (listp value) value)))) + +(defun org-noter--notes-window-location-property (ast) + (let ((property (org-element-property (intern (concat ":" org-noter--property-location)) ast)) + value) + (when (and (stringp property) (> (length property) 0)) + (setq value (intern property)) + (when (memq value '(horizontal-split vertical-split other-frame)) value)))) + +(defun org-noter--doc-split-fraction-property (ast) + (let ((property (org-element-property (intern (concat ":" org-noter--property-doc-split-fraction)) ast)) + value) + (when (and (stringp property) (> (length property) 0)) + (setq value (car (read-from-string property))) + (when (consp value) value)))) + +(defun org-noter--auto-save-last-location-property (ast) + (let ((property (org-element-property (intern (concat ":" org-noter--property-auto-save-last-location)) ast))) + (when (and (stringp property) (> (length property) 0)) + (if (intern property) t 'disable)))) + +(defun org-noter--hide-other-property (ast) + (let ((property (org-element-property (intern (concat ":" org-noter--property-hide-other)) ast))) + (when (and (stringp property) (> (length property) 0)) + (if (intern property) t 'disable)))) + +(defun org-noter--closest-tipping-point-property (ast) + (let ((property (org-element-property (intern (concat ":" org-noter--property-closest-tipping-point)) ast))) + (when (and (stringp property) (> (length property) 0)) + (ignore-errors (string-to-number property))))) + +(defun org-noter--doc-approx-location-cons (&optional precise-info) + (cond + ((memq major-mode '(doc-view-mode pdf-view-mode)) + (cons (image-mode-window-get 'page) (if (numberp precise-info) precise-info 0))) + + ((eq major-mode 'nov-mode) + (cons nov-documents-index (if (integerp precise-info) + precise-info + (max 1 (/ (+ (window-start) (window-end nil t)) 2))))) + + (t (error "Unknown document type %s" major-mode)))) + +(defun org-noter--doc-approx-location (&optional precise-info force-new-ref) + (let ((window (if (org-noter--valid-session org-noter--session) + (org-noter--get-doc-window) + (selected-window)))) + (cl-assert window) + (with-selected-window window + (or (run-hook-with-args-until-success 'org-noter--doc-approx-location-hook major-mode + precise-info force-new-ref) + (org-noter--doc-approx-location-cons precise-info))))) + +(defun org-noter--location-change-advice (&rest _) + (org-noter--with-valid-session (org-noter--doc-location-change-handler))) + +(defun org-noter--nov-scroll-handler (&rest _) + (when org-noter--nov-timer (cancel-timer org-noter--nov-timer)) + (unless org-noter--inhibit-location-change-handler + (setq org-noter--nov-timer (run-with-timer 0.25 nil 'org-noter--doc-location-change-handler)))) + +(defsubst org-noter--doc-file-property (headline) + (org-element-property (intern (concat ":" org-noter-property-doc-file)) headline)) + +(defun org-noter--check-location-property (arg) + (let ((property (if (stringp arg) arg + (org-element-property (intern (concat ":" org-noter-property-note-location)) arg)))) + (when (and (stringp property) (> (length property) 0)) + (or (run-hook-with-args-until-success 'org-noter--check-location-property-hook property) + (let ((value (car (read-from-string property)))) + (or (and (consp value) (integerp (car value)) (numberp (cdr value))) + (integerp value))))))) + +(defun org-noter--parse-location-property (arg) + (let ((property (if (stringp arg) arg + (org-element-property (intern (concat ":" org-noter-property-note-location)) arg)))) + (when (and (stringp property) (> (length property) 0)) + (or (run-hook-with-args-until-success 'org-noter--parse-location-property-hook property) + (let ((value (car (read-from-string property)))) + (cond ((and (consp value) (integerp (car value)) (numberp (cdr value))) value) + ((integerp value) (cons value 0)))))))) + +(defun org-noter--pretty-print-location (location) + (org-noter--with-valid-session + (or (run-hook-with-args-until-success 'org-noter--pretty-print-location-hook location) + (format "%s" (cond + ((memq (org-noter--session-doc-mode session) '(doc-view-mode pdf-view-mode)) + (if (or (not (cdr location)) (<= (cdr location) 0)) + (car location) + location)) + + ((eq (org-noter--session-doc-mode session) 'nov-mode) + (if (or (not (cdr location)) (<= (cdr location) 1)) + (car location) + location))))))) + +(defun org-noter--get-containing-heading (&optional include-root) + "Get smallest containing heading that encloses the point and has location property. +If the point isn't inside any heading with location property, return the outer heading. +When INCLUDE-ROOT is non-nil, the root heading is also eligible to be returned." + (org-noter--with-valid-session + (org-with-wide-buffer + (unless (org-before-first-heading-p) + (org-back-to-heading t) + (let (previous) + (catch 'break + (while t + (let ((prop (org-noter--check-location-property (org-entry-get nil org-noter-property-note-location))) + (at-root (equal (org-noter--session-id session) + (get-text-property (point) org-noter--id-text-property))) + (heading (org-element-at-point))) + (when (and prop (or include-root (not at-root))) + (throw 'break heading)) + + (when (or at-root (not (org-up-heading-safe))) + (throw 'break (if include-root heading previous))) + + (setq previous heading))))))))) + +(defun org-noter--doc-get-page-slice () + "Return (slice-top . slice-height)." + (let* ((slice (or (image-mode-window-get 'slice) '(0 0 1 1))) + (slice-top (float (nth 1 slice))) + (slice-height (float (nth 3 slice)))) + (when (or (> slice-top 1) + (> slice-height 1)) + (let ((height (cdr (image-size (image-mode-window-get 'image) t)))) + (setq slice-top (/ slice-top height) + slice-height (/ slice-height height)))) + (cons slice-top slice-height))) + +(defun org-noter--conv-page-scroll-percentage (scroll) + (let* ((slice (org-noter--doc-get-page-slice)) + (display-height (cdr (image-display-size (image-get-display-property)))) + (display-percentage (/ scroll display-height)) + (percentage (+ (car slice) (* (cdr slice) display-percentage)))) + (max 0 (min 1 percentage)))) + +(defun org-noter--conv-page-percentage-scroll (percentage) + (let* ((slice (org-noter--doc-get-page-slice)) + (display-height (cdr (image-display-size (image-get-display-property)))) + (display-percentage (min 1 (max 0 (/ (- percentage (car slice)) (cdr slice))))) + (scroll (max 0 (floor (* display-percentage display-height))))) + scroll)) + +(defun org-noter--get-precise-info () + (org-noter--with-valid-session + (let ((window (org-noter--get-doc-window)) + (mode (org-noter--session-doc-mode session)) + event) + (with-selected-window window + (cond + ((run-hook-with-args-until-success 'org-noter--get-precise-info-hook mode)) + + ((eq mode 'pdf-view-mode) + (if (pdf-view-active-region-p) + (cadar (pdf-view-active-region)) + (while (not (and (eq 'mouse-1 (car event)) + (eq window (posn-window (event-start event))))) + (setq event (read-event "Click where you want the start of the note to be!"))) + (org-noter--conv-page-scroll-percentage (+ (window-vscroll) + (cdr (posn-col-row (event-start event))))))) + + ((eq mode 'doc-view-mode) + (while (not (and (eq 'mouse-1 (car event)) + (eq window (posn-window (event-start event))))) + (setq event (read-event "Click where you want the start of the note to be!"))) + (org-noter--conv-page-scroll-percentage (+ (window-vscroll) + (cdr (posn-col-row (event-start event)))))) + + ((eq mode 'nov-mode) + (if (region-active-p) + (min (mark) (point)) + (while (not (and (eq 'mouse-1 (car event)) + (eq window (posn-window (event-start event))))) + (setq event (read-event "Click where you want the start of the note to be!"))) + (posn-point (event-start event))))))))) + +(defun org-noter--show-arrow () + (when (and org-noter--arrow-location + (window-live-p (aref org-noter--arrow-location 1))) + (with-selected-window (aref org-noter--arrow-location 1) + (pdf-util-tooltip-arrow (aref org-noter--arrow-location 2)))) + (setq org-noter--arrow-location nil)) + +(defun org-noter--doc-goto-location (location) + "Go to location specified by LOCATION." + (org-noter--with-valid-session + (let ((window (org-noter--get-doc-window)) + (mode (org-noter--session-doc-mode session))) + (with-selected-window window + (cond + ((run-hook-with-args-until-success 'org-noter--doc-goto-location-hook mode location)) + + ((memq mode '(doc-view-mode pdf-view-mode)) + (if (eq mode 'doc-view-mode) + (doc-view-goto-page (car location)) + (pdf-view-goto-page (car location)) + ;; NOTE(nox): This timer is needed because the tooltip may introduce a delay, + ;; so syncing multiple pages was slow + (when (>= org-noter-arrow-delay 0) + (when org-noter--arrow-location (cancel-timer (aref org-noter--arrow-location 0))) + (setq org-noter--arrow-location + (vector (run-with-idle-timer org-noter-arrow-delay nil 'org-noter--show-arrow) + window + (cdr location))))) + (image-scroll-up (- (org-noter--conv-page-percentage-scroll (cdr location)) + (window-vscroll)))) + + ((eq mode 'nov-mode) + (setq nov-documents-index (car location)) + (nov-render-document) + (goto-char (cdr location)) + (recenter))) + ;; NOTE(nox): This needs to be here, because it would be issued anyway after + ;; everything and would run org-noter--nov-scroll-handler. + (redisplay))))) + +(defun org-noter--compare-location-cons (comp l1 l2) + "Compare L1 and L2, which are location cons. +See `org-noter--compare-locations'" + (cl-assert (and (consp l1) (consp l2))) + (cond ((eq comp '=) + (and (= (car l1) (car l2)) + (= (cdr l1) (cdr l2)))) + ((eq comp '<) + (or (< (car l1) (car l2)) + (and (= (car l1) (car l2)) + (< (cdr l1) (cdr l2))))) + ((eq comp '<=) + (or (< (car l1) (car l2)) + (and (= (car l1) (car l2)) + (<= (cdr l1) (cdr l2))))) + ((eq comp '>) + (or (> (car l1) (car l2)) + (and (= (car l1) (car l2)) + (> (cdr l1) (cdr l2))))) + ((eq comp '>=) + (or (> (car l1) (car l2)) + (and (= (car l1) (car l2)) + (>= (cdr l1) (cdr l2))))) + ((eq comp '>f) + (or (> (car l1) (car l2)) + (and (= (car l1) (car l2)) + (< (cdr l1) (cdr l2))))) + (t (error "Comparison operator %s not known" comp)))) + +(defun org-noter--compare-locations (comp l1 l2) + "Compare L1 and L2. +When COMP is '<, '<=, '>, or '>=, it works as expected. +When COMP is '>f, it will return t when L1 is a page greater than +L2 or, when in the same page, if L1 is the _f_irst of the two." + (cond ((not l1) nil) + ((not l2) t) + (t + (setq l1 (or (run-hook-with-args-until-success 'org-noter--convert-to-location-cons-hook l1) l1) + l2 (or (run-hook-with-args-until-success 'org-noter--convert-to-location-cons-hook l2) l2)) + (org-noter--compare-location-cons comp l1 l2)))) + +(defun org-noter--show-note-entry (session note) + "This will show the note entry and its children. +Every direct subheading _until_ the first heading that doesn't +belong to the same view (ie. until a heading with location or +document property) will be opened." + (save-excursion + (goto-char (org-element-property :contents-begin note)) + (org-show-set-visibility t) + (org-element-map (org-element-contents note) 'headline + (lambda (headline) + (let ((doc-file (org-noter--doc-file-property headline))) + (if (or (and doc-file (not (string= doc-file (org-noter--session-property-text session)))) + (org-noter--check-location-property headline)) + t + (goto-char (org-element-property :begin headline)) + (org-show-entry) + (org-show-children) + nil))) + nil t org-element-all-elements))) + +(defun org-noter--focus-notes-region (view-info) + (org-noter--with-selected-notes-window + (if (org-noter--session-hide-other session) + (save-excursion + (goto-char (org-element-property :begin (org-noter--parse-root))) + (outline-hide-subtree)) + (org-cycle-hide-drawers 'all)) + + (let* ((notes-cons (org-noter--view-info-notes view-info)) + (regions (or (org-noter--view-info-regions view-info) + (org-noter--view-info-prev-regions view-info))) + (point-before (point)) + target-region + point-inside-target-region) + (cond + (notes-cons + (dolist (note-cons notes-cons) (org-noter--show-note-entry session (car note-cons))) + + (setq target-region (or (catch 'result (dolist (region regions) + (when (and (>= point-before (car region)) + (or (save-restriction (goto-char (cdr region)) (eobp)) + (< point-before (cdr region)))) + (setq point-inside-target-region t) + (throw 'result region)))) + (car regions))) + + (let ((begin (car target-region)) (end (cdr target-region)) num-lines + (target-char (if point-inside-target-region + point-before + (org-noter--get-properties-end (caar notes-cons)))) + (window-start (window-start)) (window-end (window-end nil t))) + (setq num-lines (count-screen-lines begin end)) + + (cond + ((> num-lines (window-height)) + (goto-char begin) + (recenter 0)) + + ((< begin window-start) + (goto-char begin) + (recenter 0)) + + ((> end window-end) + (goto-char end) + (recenter -2))) + + (goto-char target-char))) + + (t (org-noter--show-note-entry session (org-noter--parse-root))))) + + (org-cycle-show-empty-lines t))) + +(defun org-noter--get-current-view () + "Return a vector with the current view information." + (org-noter--with-valid-session + (let ((mode (org-noter--session-doc-mode session))) + (with-selected-window (org-noter--get-doc-window) + (cond ((memq mode '(doc-view-mode pdf-view-mode)) + (vector 'paged (car (org-noter--doc-approx-location-cons)))) + ((eq mode 'nov-mode) + (vector 'nov + (org-noter--doc-approx-location-cons (window-start)) + (org-noter--doc-approx-location-cons (window-end nil t)))) + (t (error "Unknown document type"))))))) + +(defun org-noter--note-after-tipping-point (point location view) + ;; NOTE(nox): This __assumes__ the note is inside the view! + (let (hook-result) + (cond + ((setq hook-result (run-hook-with-args-until-success 'org-noter--note-after-tipping-point-hook + point location view)) + (cdr hook-result)) + ((eq (aref view 0) 'paged) + (> (cdr location) point)) + ((eq (aref view 0) 'nov) + (> (cdr location) (+ (* point (- (cdr (aref view 2)) (cdr (aref view 1)))) + (cdr (aref view 1)))))))) + +(defun org-noter--relative-position-to-view (location view) + (cond + ((run-hook-with-args-until-success 'org-noter--relative-position-to-view-hook location view)) + + ((eq (aref view 0) 'paged) + (let ((note-page (car location)) + (view-page (aref view 1))) + (cond ((< note-page view-page) 'before) + ((= note-page view-page) 'inside) + (t 'after)))) + + ((eq (aref view 0) 'nov) + (let ((view-top (aref view 1)) + (view-bot (aref view 2))) + (cond ((org-noter--compare-locations '< location view-top) 'before) + ((org-noter--compare-locations '<= location view-bot) 'inside) + (t 'after)))))) + +(defmacro org-noter--view-region-finish (info &optional terminating-headline) + `(when ,info + ,(if terminating-headline + `(push (cons (aref ,info 1) (min (aref ,info 2) (org-element-property :begin ,terminating-headline))) + (gv-deref (aref ,info 0))) + `(push (cons (aref ,info 1) (aref ,info 2)) (gv-deref (aref ,info 0)))) + (setq ,info nil))) + +(defmacro org-noter--view-region-add (info list-name headline) + `(progn + (when (and ,info (not (eq (aref ,info 3) ',list-name))) + (org-noter--view-region-finish ,info ,headline)) + + (if ,info + (setf (aref ,info 2) (max (aref ,info 2) (org-element-property :end ,headline))) + (setq ,info (vector (gv-ref ,list-name) + (org-element-property :begin ,headline) (org-element-property :end ,headline) + ',list-name))))) + +;; NOTE(nox): notes is a list of (HEADING . HEADING-TO-INSERT-TEXT-BEFORE): +;; - HEADING is the root heading of the note +;; - SHOULD-ADD-SPACE indicates if there should be extra spacing when inserting text to the note (ie. the +;; note has contents) +(cl-defstruct org-noter--view-info notes regions prev-regions reference-for-insertion) + +(defun org-noter--get-view-info (view &optional new-location) + "Return VIEW related information. + +When optional NEW-LOCATION is provided, it will be used to find +the best heading to serve as a reference to create the new one +relative to." + (when view + (org-noter--with-valid-session + (let ((contents (org-element-contents (org-noter--parse-root))) + (preamble t) + notes-in-view regions-in-view + reference-for-insertion reference-location + (all-after-tipping-point t) + (closest-tipping-point (and (>= (org-noter--session-closest-tipping-point session) 0) + (org-noter--session-closest-tipping-point session))) + closest-notes closest-notes-regions closest-notes-location + ignore-until-level + current-region-info) ;; NOTE(nox): [REGIONS-LIST-PTR START MAX-END REGIONS-LIST-NAME] + + (org-element-map contents 'headline + (lambda (headline) + (let ((doc-file (org-noter--doc-file-property headline)) + (location (org-noter--parse-location-property headline))) + (when (and ignore-until-level (<= (org-element-property :level headline) ignore-until-level)) + (setq ignore-until-level nil)) + + (cond + (ignore-until-level) ;; NOTE(nox): This heading is ignored, do nothing + + ((and doc-file (not (string= doc-file (org-noter--session-property-text session)))) + (org-noter--view-region-finish current-region-info headline) + (setq ignore-until-level (org-element-property :level headline)) + (when (and preamble new-location + (or (not reference-for-insertion) + (>= (org-element-property :begin headline) + (org-element-property :end (cdr reference-for-insertion))))) + (setq reference-for-insertion (cons 'after headline)))) + + (location + (let ((relative-position (org-noter--relative-position-to-view location view))) + (cond + ((eq relative-position 'inside) + (push (cons headline nil) notes-in-view) + + (org-noter--view-region-add current-region-info regions-in-view headline) + + (setq all-after-tipping-point + (and all-after-tipping-point (org-noter--note-after-tipping-point + closest-tipping-point location view)))) + + (t + (when current-region-info + (let ((note-cons-to-change (cond ((eq (aref current-region-info 3) 'regions-in-view) + (car notes-in-view)) + ((eq (aref current-region-info 3) 'closest-notes-regions) + (car closest-notes))))) + (when (< (org-element-property :begin headline) + (org-element-property :end (car note-cons-to-change))) + (setcdr note-cons-to-change headline)))) + + (let ((eligible-for-before (and closest-tipping-point all-after-tipping-point + (eq relative-position 'before)))) + (cond ((and eligible-for-before + (org-noter--compare-locations '> location closest-notes-location)) + (setq closest-notes (list (cons headline nil)) + closest-notes-location location + current-region-info nil + closest-notes-regions nil) + (org-noter--view-region-add current-region-info closest-notes-regions headline)) + + ((and eligible-for-before (equal location closest-notes-location)) + (push (cons headline nil) closest-notes) + (org-noter--view-region-add current-region-info closest-notes-regions headline)) + + (t (org-noter--view-region-finish current-region-info headline))))))) + + (when new-location + (setq preamble nil) + (cond ((and (org-noter--compare-locations '<= location new-location) + (or (eq (car reference-for-insertion) 'before) + (org-noter--compare-locations '>= location reference-location))) + (setq reference-for-insertion (cons 'after headline) + reference-location location)) + + ((and (eq (car reference-for-insertion) 'after) + (< (org-element-property :begin headline) + (org-element-property :end (cdr reference-for-insertion))) + (org-noter--compare-locations '>= location new-location)) + (setq reference-for-insertion (cons 'before headline) + reference-location location))))) + + (t + (when (and preamble new-location + (or (not reference-for-insertion) + (>= (org-element-property :begin headline) + (org-element-property :end (cdr reference-for-insertion))))) + (setq reference-for-insertion (cons 'after headline))))))) + nil nil org-noter--note-search-no-recurse) + + (org-noter--view-region-finish current-region-info) + + (setf (org-noter--session-num-notes-in-view session) (length notes-in-view)) + + (when all-after-tipping-point (setq notes-in-view (append closest-notes notes-in-view))) + + (make-org-noter--view-info + :notes (nreverse notes-in-view) + :regions (nreverse regions-in-view) + :prev-regions (nreverse closest-notes-regions) + :reference-for-insertion reference-for-insertion))))) + +(defun org-noter--make-view-info-for-single-note (session headline) + (let ((not-belonging-element + (org-element-map (org-element-contents headline) 'headline + (lambda (headline) + (let ((doc-file (org-noter--doc-file-property headline))) + (and (or (and doc-file (not (string= doc-file (org-noter--session-property-text session)))) + (org-noter--check-location-property headline)) + headline))) + nil t))) + + (make-org-noter--view-info + ;; NOTE(nox): The cdr is only used when inserting, doesn't matter here + :notes (list (cons headline nil)) + :regions (list (cons (org-element-property :begin headline) + (or (and not-belonging-element (org-element-property :begin not-belonging-element)) + (org-element-property :end headline))))))) + +(defun org-noter--doc-location-change-handler () + (org-noter--with-valid-session + (let ((view-info (org-noter--get-view-info (org-noter--get-current-view)))) + (force-mode-line-update t) + (unless org-noter--inhibit-location-change-handler + (org-noter--get-notes-window (cond ((org-noter--view-info-regions view-info) 'scroll) + ((org-noter--view-info-prev-regions view-info) 'only-prev))) + (org-noter--focus-notes-region view-info))) + + (when (org-noter--session-auto-save-last-location session) (org-noter-set-start-location)))) + +(defun org-noter--mode-line-text () + (org-noter--with-valid-session + (let* ((number-of-notes (or (org-noter--session-num-notes-in-view session) 0))) + (cond ((= number-of-notes 0) (propertize " 0 notes " 'face 'org-noter-no-notes-exist-face)) + ((= number-of-notes 1) (propertize " 1 note " 'face 'org-noter-notes-exist-face)) + (t (propertize (format " %d notes " number-of-notes) 'face 'org-noter-notes-exist-face)))))) + +;; NOTE(nox): From machc/pdf-tools-org +(defun org-noter--pdf-tools-edges-to-region (edges) + "Get 4-entry region (LEFT TOP RIGHT BOTTOM) from several EDGES." + (when edges + (let ((left0 (nth 0 (car edges))) + (top0 (nth 1 (car edges))) + (bottom0 (nth 3 (car edges))) + (top1 (nth 1 (car (last edges)))) + (right1 (nth 2 (car (last edges)))) + (bottom1 (nth 3 (car (last edges))))) + (list left0 + (+ top0 (/ (- bottom0 top0) 3)) + right1 + (- bottom1 (/ (- bottom1 top1) 3)))))) + +(defun org-noter--check-if-document-is-annotated-on-file (document-path notes-path) + ;; NOTE(nox): In order to insert the correct file contents + (let ((buffer (find-buffer-visiting notes-path))) + (when buffer (with-current-buffer buffer (save-buffer))) + + (with-temp-buffer + (insert-file-contents notes-path) + (catch 'break + (while (re-search-forward (org-re-property org-noter-property-doc-file) nil t) + (when (file-equal-p (expand-file-name (match-string 3) (file-name-directory notes-path)) + document-path) + ;; NOTE(nox): This notes file has the document we want! + (throw 'break t))))))) + +(defsubst org-noter--check-doc-prop (doc-prop) + (and doc-prop (not (file-directory-p doc-prop)) (file-readable-p doc-prop))) + +(defun org-noter--get-or-read-document-property (inherit-prop &optional force-new) + (let ((doc-prop (and (not force-new) (org-entry-get nil org-noter-property-doc-file inherit-prop)))) + (unless (org-noter--check-doc-prop doc-prop) + (setq doc-prop nil) + + (when org-noter-suggest-from-attachments + (require 'org-attach) + (let* ((attach-dir (org-attach-dir)) + (attach-list (and attach-dir (org-attach-file-list attach-dir)))) + (when (and attach-list (y-or-n-p "Do you want to annotate an attached file?")) + (setq doc-prop (completing-read "File to annotate: " attach-list nil t)) + (when doc-prop (setq doc-prop (file-relative-name (expand-file-name doc-prop attach-dir))))))) + + (unless (org-noter--check-doc-prop doc-prop) + (setq doc-prop (expand-file-name + (read-file-name + "Invalid or no document property found. Please specify a document path: " nil nil t))) + (when (or (file-directory-p doc-prop) (not (file-readable-p doc-prop))) (user-error "Invalid file path")) + (when (y-or-n-p "Do you want a relative file name? ") (setq doc-prop (file-relative-name doc-prop)))) + + (org-entry-put nil org-noter-property-doc-file doc-prop)) + doc-prop)) + +(defun org-noter--other-frames (&optional this-frame) + "Returns non-`nil' when there is at least another frame" + (setq this-frame (or this-frame (selected-frame))) + (catch 'other-frame + (dolist (frame (visible-frame-list)) + (unless (or (eq this-frame frame) + (frame-parent frame) + (frame-parameter frame 'delete-before)) + (throw 'other-frame frame))))) + +;; -------------------------------------------------------------------------------- +;; NOTE(nox): User commands +(defun org-noter-set-start-location (&optional arg) + "When opening a session with this document, go to the current location. +With a prefix ARG, remove start location." + (interactive "P") + (org-noter--with-valid-session + (let ((inhibit-read-only t) + (ast (org-noter--parse-root)) + (location (org-noter--doc-approx-location 'interactive))) + (with-current-buffer (org-noter--session-notes-buffer session) + (org-with-wide-buffer + (goto-char (org-element-property :begin ast)) + (if arg + (org-entry-delete nil org-noter-property-note-location) + (org-entry-put nil org-noter-property-note-location + (org-noter--pretty-print-location location)))))))) + +(defun org-noter-set-auto-save-last-location (arg) + "This toggles saving the last visited location for this document. +With a prefix ARG, delete the current setting and use the default." + (interactive "P") + (org-noter--with-valid-session + (let ((inhibit-read-only t) + (ast (org-noter--parse-root)) + (new-setting (if arg + org-noter-auto-save-last-location + (not (org-noter--session-auto-save-last-location session))))) + (setf (org-noter--session-auto-save-last-location session) + new-setting) + (with-current-buffer (org-noter--session-notes-buffer session) + (org-with-wide-buffer + (goto-char (org-element-property :begin ast)) + (if arg + (org-entry-delete nil org-noter--property-auto-save-last-location) + (org-entry-put nil org-noter--property-auto-save-last-location (format "%s" new-setting))) + (unless new-setting (org-entry-delete nil org-noter-property-note-location))))))) + +(defun org-noter-set-hide-other (arg) + "This toggles hiding other headings for the current session. +- With a prefix \\[universal-argument], set the current setting permanently for this document. +- With a prefix \\[universal-argument] \\[universal-argument], remove the setting and use the default." + (interactive "P") + (org-noter--with-valid-session + (let* ((inhibit-read-only t) + (ast (org-noter--parse-root)) + (persistent + (cond ((equal arg '(4)) 'write) + ((equal arg '(16)) 'remove))) + (new-setting + (cond ((eq persistent 'write) (org-noter--session-hide-other session)) + ((eq persistent 'remove) org-noter-hide-other) + ('other-cases (not (org-noter--session-hide-other session)))))) + (setf (org-noter--session-hide-other session) new-setting) + (when persistent + (with-current-buffer (org-noter--session-notes-buffer session) + (org-with-wide-buffer + (goto-char (org-element-property :begin ast)) + (if (eq persistent 'write) + (org-entry-put nil org-noter--property-hide-other (format "%s" new-setting)) + (org-entry-delete nil org-noter--property-hide-other)))))))) + +(defun org-noter-set-closest-tipping-point (arg) + "This sets the closest note tipping point (see `org-noter-closest-tipping-point') +- With a prefix \\[universal-argument], set it permanently for this document. +- With a prefix \\[universal-argument] \\[universal-argument], remove the setting and use the default." + (interactive "P") + (org-noter--with-valid-session + (let* ((ast (org-noter--parse-root)) + (inhibit-read-only t) + (persistent (cond ((equal arg '(4)) 'write) + ((equal arg '(16)) 'remove))) + (new-setting (if (eq persistent 'remove) + org-noter-closest-tipping-point + (read-number "New tipping point: " (org-noter--session-closest-tipping-point session))))) + (setf (org-noter--session-closest-tipping-point session) new-setting) + (when persistent + (with-current-buffer (org-noter--session-notes-buffer session) + (org-with-wide-buffer + (goto-char (org-element-property :begin ast)) + (if (eq persistent 'write) + (org-entry-put nil org-noter--property-closest-tipping-point (format "%f" new-setting)) + (org-entry-delete nil org-noter--property-closest-tipping-point)))))))) + +(defun org-noter-set-notes-window-behavior (arg) + "Set the notes window behaviour for the current session. +With a prefix ARG, it becomes persistent for that document. + +See `org-noter-notes-window-behavior' for more information." + (interactive "P") + (org-noter--with-valid-session + (let* ((inhibit-read-only t) + (ast (org-noter--parse-root)) + (possible-behaviors (list '("Default" . default) + '("On start" . start) + '("On scroll" . scroll) + '("On scroll to location that only has previous notes" . only-prev) + '("Never" . never))) + chosen-behaviors) + + (while (> (length possible-behaviors) 1) + (let ((chosen-pair (assoc (completing-read "Behavior: " possible-behaviors nil t) possible-behaviors))) + (cond ((eq (cdr chosen-pair) 'default) (setq possible-behaviors nil)) + + ((eq (cdr chosen-pair) 'never) (setq chosen-behaviors (list 'never) + possible-behaviors nil)) + + ((eq (cdr chosen-pair) 'done) (setq possible-behaviors nil)) + + (t (push (cdr chosen-pair) chosen-behaviors) + (setq possible-behaviors (delq chosen-pair possible-behaviors)) + (when (= (length chosen-behaviors) 1) + (setq possible-behaviors (delq (rassq 'default possible-behaviors) possible-behaviors) + possible-behaviors (delq (rassq 'never possible-behaviors) possible-behaviors)) + (push (cons "Done" 'done) possible-behaviors)))))) + + (setf (org-noter--session-window-behavior session) + (or chosen-behaviors org-noter-notes-window-behavior)) + + (when arg + (with-current-buffer (org-noter--session-notes-buffer session) + (org-with-wide-buffer + (goto-char (org-element-property :begin ast)) + (if chosen-behaviors + (org-entry-put nil org-noter--property-behavior (format "%s" chosen-behaviors)) + (org-entry-delete nil org-noter--property-behavior)))))))) + +(defun org-noter-set-notes-window-location (arg) + "Set the notes window default location for the current session. +With a prefix ARG, it becomes persistent for that document. + +See `org-noter-notes-window-behavior' for more information." + (interactive "P") + (org-noter--with-valid-session + (let* ((inhibit-read-only t) + (ast (org-noter--parse-root)) + (location-possibilities + '(("Default" . nil) + ("Horizontal split" . horizontal-split) + ("Vertical split" . vertical-split) + ("Other frame" . other-frame))) + (location + (cdr (assoc (completing-read "Location: " location-possibilities nil t) + location-possibilities))) + (notes-buffer (org-noter--session-notes-buffer session))) + + (setf (org-noter--session-window-location session) + (or location org-noter-notes-window-location)) + + (let (exists) + (dolist (window (get-buffer-window-list notes-buffer nil t)) + (setq exists t) + (with-selected-frame (window-frame window) + (if (= (count-windows) 1) + (delete-frame) + (delete-window window)))) + (when exists (org-noter--get-notes-window 'force))) + + (when arg + (with-current-buffer notes-buffer + (org-with-wide-buffer + (goto-char (org-element-property :begin ast)) + (if location + (org-entry-put nil org-noter--property-location + (format "%s" location)) + (org-entry-delete nil org-noter--property-location)))))))) + +(defun org-noter-set-doc-split-fraction (arg) + "Set the fraction of the frame that the document window will occupy when split. +- With a prefix \\[universal-argument], set it permanently for this document. +- With a prefix \\[universal-argument] \\[universal-argument], remove the setting and use the default." + (interactive "P") + (org-noter--with-valid-session + (let* ((ast (org-noter--parse-root)) + (inhibit-read-only t) + (persistent (cond ((equal arg '(4)) 'write) + ((equal arg '(16)) 'remove))) + (current-setting (org-noter--session-doc-split-fraction session)) + (new-setting + (if (eq persistent 'remove) + org-noter-doc-split-fraction + (cons (read-number "Horizontal fraction: " (car current-setting)) + (read-number "Vertical fraction: " (cdr current-setting)))))) + (setf (org-noter--session-doc-split-fraction session) new-setting) + (when (org-noter--get-notes-window) + (with-current-buffer (org-noter--session-doc-buffer session) + (delete-other-windows) + (org-noter--get-notes-window 'force))) + + (when persistent + (with-current-buffer (org-noter--session-notes-buffer session) + (org-with-wide-buffer + (goto-char (org-element-property :begin ast)) + (if (eq persistent 'write) + (org-entry-put nil org-noter--property-doc-split-fraction (format "%s" new-setting)) + (org-entry-delete nil org-noter--property-doc-split-fraction)))))))) + +(defun org-noter-kill-session (&optional session) + "Kill an `org-noter' session. + +When called interactively, if there is no prefix argument and the +buffer has an annotation session, it will kill it; else, it will +show a list of open `org-noter' sessions, asking for which to +kill. + +When called from elisp code, you have to pass in the SESSION you +want to kill." + (interactive "P") + (when (and (called-interactively-p 'any) (> (length org-noter--sessions) 0)) + ;; NOTE(nox): `session' is representing a prefix argument + (if (and org-noter--session (not session)) + (setq session org-noter--session) + (setq session nil) + (let (collection default doc-display-name notes-file-name display) + (dolist (session org-noter--sessions) + (setq doc-display-name (org-noter--session-display-name session) + notes-file-name (file-name-nondirectory + (org-noter--session-notes-file-path session)) + display (concat doc-display-name " - " notes-file-name)) + (when (eq session org-noter--session) (setq default display)) + (push (cons display session) collection)) + (setq session (cdr (assoc (completing-read "Which session? " collection nil t + nil nil default) + collection)))))) + + (when (and session (memq session org-noter--sessions)) + (setq org-noter--sessions (delq session org-noter--sessions)) + + (when (eq (length org-noter--sessions) 0) + (remove-hook 'delete-frame-functions 'org-noter--handle-delete-frame) + (advice-remove 'doc-view-goto-page 'org-noter--location-change-advice) + (advice-remove 'nov-render-document 'org-noter--nov-scroll-handler)) + + (let* ((ast (org-noter--parse-root session)) + (frame (org-noter--session-frame session)) + (notes-buffer (org-noter--session-notes-buffer session)) + (base-buffer (buffer-base-buffer notes-buffer)) + (notes-modified (buffer-modified-p base-buffer)) + (doc-buffer (org-noter--session-doc-buffer session))) + + (dolist (window (get-buffer-window-list notes-buffer nil t)) + (with-selected-frame (window-frame window) + (if (= (count-windows) 1) + (when (org-noter--other-frames) (delete-frame)) + (delete-window window)))) + + (with-current-buffer notes-buffer + (remove-hook 'kill-buffer-hook 'org-noter--handle-kill-buffer t) + (restore-buffer-modified-p nil)) + (kill-buffer notes-buffer) + + (with-current-buffer base-buffer + (org-noter--unset-text-properties ast) + (set-buffer-modified-p notes-modified)) + + (with-current-buffer doc-buffer + (remove-hook 'kill-buffer-hook 'org-noter--handle-kill-buffer t)) + (kill-buffer doc-buffer) + + (when (frame-live-p frame) + (if (and (org-noter--other-frames) org-noter-kill-frame-at-session-end) + (delete-frame frame) + (progn + (delete-other-windows) + (set-frame-parameter nil 'name nil))))))) + +(defun org-noter-create-skeleton () + "Create notes skeleton with the PDF outline or annotations. +Only available with PDF Tools." + (interactive) + (org-noter--with-valid-session + (cond + ((eq (org-noter--session-doc-mode session) 'pdf-view-mode) + (let* ((ast (org-noter--parse-root)) + (top-level (org-element-property :level ast)) + (options '(("Outline" . (outline)) + ("Annotations" . (annots)) + ("Both" . (outline annots)))) + answer output-data) + (with-current-buffer (org-noter--session-doc-buffer session) + (setq answer (assoc (completing-read "What do you want to import? " options nil t) options)) + + (when (memq 'outline answer) + (dolist (item (pdf-info-outline)) + (let ((type (alist-get 'type item)) + (page (alist-get 'page item)) + (depth (alist-get 'depth item)) + (title (alist-get 'title item)) + (top (alist-get 'top item))) + (when (and (eq type 'goto-dest) (> page 0)) + (push (vector title (cons page top) (1+ depth) nil) output-data))))) + + (when (memq 'annots answer) + (let ((possible-annots (list '("Highlights" . highlight) + '("Underlines" . underline) + '("Squigglies" . squiggly) + '("Text notes" . text) + '("Strikeouts" . strike-out) + '("Links" . link) + '("ALL" . all))) + chosen-annots insert-contents pages-with-links) + (while (> (length possible-annots) 1) + (let* ((chosen-string (completing-read "Which types of annotations do you want? " + possible-annots nil t)) + (chosen-pair (assoc chosen-string possible-annots))) + (cond ((eq (cdr chosen-pair) 'all) + (dolist (annot possible-annots) + (when (and (cdr annot) (not (eq (cdr annot) 'all))) + (push (cdr annot) chosen-annots))) + (setq possible-annots nil)) + ((cdr chosen-pair) + (push (cdr chosen-pair) chosen-annots) + (setq possible-annots (delq chosen-pair possible-annots)) + (when (= 1 (length chosen-annots)) (push '("DONE") possible-annots))) + (t + (setq possible-annots nil))))) + + (setq insert-contents (y-or-n-p "Should we insert the annotations contents? ")) + + (dolist (item (pdf-info-getannots)) + (let* ((type (alist-get 'type item)) + (page (alist-get 'page item)) + (edges (or (org-noter--pdf-tools-edges-to-region (alist-get 'markup-edges item)) + (alist-get 'edges item))) + (top (nth 1 edges)) + (item-subject (alist-get 'subject item)) + (item-contents (alist-get 'contents item)) + name contents) + (when (and (memq type chosen-annots) (> page 0)) + (if (eq type 'link) + (cl-pushnew page pages-with-links) + (setq name (cond ((eq type 'highlight) "Highlight") + ((eq type 'underline) "Underline") + ((eq type 'squiggly) "Squiggly") + ((eq type 'text) "Text note") + ((eq type 'strike-out) "Strikeout"))) + + (when insert-contents + (setq contents (cons (pdf-info-gettext page edges) + (and (or (and item-subject (> (length item-subject) 0)) + (and item-contents (> (length item-contents) 0))) + (concat (or item-subject "") + (if (and item-subject item-contents) "\n" "") + (or item-contents "")))))) + + (push (vector (format "%s on page %d" name page) (cons page top) 'inside contents) + output-data))))) + + (dolist (page pages-with-links) + (let ((links (pdf-info-pagelinks page)) + type) + (dolist (link links) + (setq type (alist-get 'type link)) + (unless (eq type 'goto-dest) ;; NOTE(nox): Ignore internal links + (let* ((edges (alist-get 'edges link)) + (title (alist-get 'title link)) + (top (nth 1 edges)) + (target-page (alist-get 'page link)) + target heading-text) + + (unless (and title (> (length title) 0)) (setq title (pdf-info-gettext page edges))) + + (cond + ((eq type 'uri) + (setq target (alist-get 'uri link) + heading-text (format "Link on page %d: [[%s][%s]]" page target title))) + + ((eq type 'goto-remote) + (setq target (concat "file:" (alist-get 'filename link)) + heading-text (format "Link to document on page %d: [[%s][%s]]" page target title)) + (when target-page + (setq heading-text (concat heading-text (format " (target page: %d)" target-page))))) + + (t (error "Unexpected link type"))) + + (push (vector heading-text (cons page top) 'inside nil) output-data)))))))) + + + (when output-data + (if (memq 'annots answer) + (setq output-data + (sort output-data + (lambda (e1 e2) + (or (not (aref e1 1)) + (and (aref e2 1) + (org-noter--compare-locations '< (aref e1 1) (aref e2 1))))))) + (setq output-data (nreverse output-data))) + + (push (vector "Skeleton" nil 1 nil) output-data))) + + (with-current-buffer (org-noter--session-notes-buffer session) + ;; NOTE(nox): org-with-wide-buffer can't be used because we want to reset the + ;; narrow region to include the new headings + (widen) + (save-excursion + (goto-char (org-element-property :end ast)) + + (let (last-absolute-level + title location relative-level contents + level) + (dolist (data output-data) + (setq title (aref data 0) + location (aref data 1) + relative-level (aref data 2) + contents (aref data 3)) + + (if (symbolp relative-level) + (setq level (1+ last-absolute-level)) + (setq last-absolute-level (+ top-level relative-level) + level last-absolute-level)) + + (org-noter--insert-heading level title) + + (when location + (org-entry-put nil org-noter-property-note-location (org-noter--pretty-print-location location))) + + (when org-noter-doc-property-in-notes + (org-entry-put nil org-noter-property-doc-file (org-noter--session-property-text session)) + (org-entry-put nil org-noter--property-auto-save-last-location "nil")) + + (when (car contents) + (org-noter--insert-heading (1+ level) "Contents") + (insert (car contents))) + (when (cdr contents) + (org-noter--insert-heading (1+ level) "Comment") + (insert (cdr contents))))) + + (setq ast (org-noter--parse-root)) + (org-noter--narrow-to-root ast) + (goto-char (org-element-property :begin ast)) + (outline-hide-subtree) + (org-show-children 2))))) + + (t (user-error "This command is only supported on PDF Tools."))))) + +(defun org-noter-insert-note (&optional precise-info) + "Insert note associated with the current location. + +This command will prompt for a title of the note and then insert +it in the notes buffer. When the input is empty, a title based on +`org-noter-default-heading-title' will be generated. + +If there are other notes related to the current location, the +prompt will also suggest them. Depending on the value of the +variable `org-noter-closest-tipping-point', it may also +suggest the closest previous note. + +PRECISE-INFO makes the new note associated with a more +specific location (see `org-noter-insert-precise-note' for more +info). + +When you insert into an existing note and have text selected on +the document buffer, the variable `org-noter-insert-selected-text-inside-note' +defines if the text should be inserted inside the note." + (interactive) + (org-noter--with-valid-session + (let* ((ast (org-noter--parse-root)) (contents (org-element-contents ast)) + (window (org-noter--get-notes-window 'force)) + (selected-text + (cond + ((eq (org-noter--session-doc-mode session) 'pdf-view-mode) + (when (pdf-view-active-region-p) + (mapconcat 'identity (pdf-view-active-region-text) ? ))) + + ((eq (org-noter--session-doc-mode session) 'nov-mode) + (when (region-active-p) + (buffer-substring-no-properties (mark) (point)))))) + force-new + (location (org-noter--doc-approx-location (or precise-info 'interactive) (gv-ref force-new))) + (view-info (org-noter--get-view-info (org-noter--get-current-view) location))) + + (let ((inhibit-quit t)) + (with-local-quit + (select-frame-set-input-focus (window-frame window)) + (select-window window) + + ;; IMPORTANT(nox): Need to be careful changing the next part, it is a bit + ;; complicated to get it right... + + (let ((point (point)) + (minibuffer-local-completion-map org-noter--completing-read-keymap) + collection default default-begin title selection + (empty-lines-number (if org-noter-separate-notes-from-heading 2 1))) + + (cond + ;; NOTE(nox): Both precise and without questions will create new notes + ((or precise-info force-new) + (setq default (and selected-text (replace-regexp-in-string "\n" " " selected-text)))) + (org-noter-insert-note-no-questions) + (t + (dolist (note-cons (org-noter--view-info-notes view-info)) + (let ((display (org-element-property :raw-value (car note-cons))) + (begin (org-element-property :begin (car note-cons)))) + (push (cons display note-cons) collection) + (when (and (>= point begin) (> begin (or default-begin 0))) + (setq default display + default-begin begin)))))) + + (setq collection (nreverse collection) + title (if org-noter-insert-note-no-questions + default + (completing-read "Note: " collection nil nil nil nil default)) + selection (unless org-noter-insert-note-no-questions (cdr (assoc title collection)))) + + (if selection + ;; NOTE(nox): Inserting on an existing note + (let* ((note (car selection)) + (insert-before-element (cdr selection)) + (has-content + (eq (org-element-map (org-element-contents note) org-element-all-elements + (lambda (element) + (if (org-noter--check-location-property element) + 'stop + (not (memq (org-element-type element) '(section property-drawer))))) + nil t) + t))) + (when has-content (setq empty-lines-number 2)) + (if insert-before-element + (goto-char (org-element-property :begin insert-before-element)) + (goto-char (org-element-property :end note))) + + + (if (org-at-heading-p) + (progn + (org-N-empty-lines-before-current empty-lines-number) + (forward-line -1)) + (unless (bolp) (insert "\n")) + (org-N-empty-lines-before-current (1- empty-lines-number))) + + (when (and org-noter-insert-selected-text-inside-note selected-text) (insert selected-text))) + + ;; NOTE(nox): Inserting a new note + (let ((reference-element-cons (org-noter--view-info-reference-for-insertion view-info)) + level) + (when (zerop (length title)) + (setq title (replace-regexp-in-string (regexp-quote "$p$") (number-to-string (car location)) + org-noter-default-heading-title))) + + (if reference-element-cons + (progn + (cond + ((eq (car reference-element-cons) 'before) + (goto-char (org-element-property :begin (cdr reference-element-cons)))) + ((eq (car reference-element-cons) 'after) + (goto-char (org-element-property :end (cdr reference-element-cons))))) + + ;; NOTE(nox): This is here to make the automatic "should insert blank" work better. + (when (org-at-heading-p) (backward-char)) + + (setq level (org-element-property :level (cdr reference-element-cons)))) + + (goto-char (org-element-map contents 'section + (lambda (section) (org-element-property :end section)) + nil t org-element-all-elements)) + (setq level (1+ (org-element-property :level ast)))) + + ;; NOTE(nox): This is needed to insert in the right place + (outline-show-entry) + (org-noter--insert-heading level title empty-lines-number location) + (when (org-noter--session-hide-other session) (org-overview)) + + (setf (org-noter--session-num-notes-in-view session) + (1+ (org-noter--session-num-notes-in-view session))))) + + (org-show-set-visibility t) + (org-cycle-hide-drawers 'all) + (org-cycle-show-empty-lines t))) + (when quit-flag + ;; NOTE(nox): If this runs, it means the user quitted while creating a note, so + ;; revert to the previous window. + (select-frame-set-input-focus (org-noter--session-frame session)) + (select-window (get-buffer-window (org-noter--session-doc-buffer session)))))))) + +(defun org-noter-insert-precise-note (&optional toggle-no-questions) + "Insert note associated with a specific location. +This will ask you to click where you want to scroll to when you +sync the document to this note. You should click on the top of +that part. Will always create a new note. + +When text is selected, it will automatically choose the top of +the selected text as the location and the text itself as the +title of the note (you may change it anyway!). + +See `org-noter-insert-note' docstring for more." + (interactive "P") + (org-noter--with-valid-session + (let ((org-noter-insert-note-no-questions (if toggle-no-questions + (not org-noter-insert-note-no-questions) + org-noter-insert-note-no-questions))) + (org-noter-insert-note (org-noter--get-precise-info))))) + + +(defun org-noter-insert-note-toggle-no-questions () + "Insert note associated with the current location. +This is like `org-noter-insert-note', except it will toggle `org-noter-insert-note-no-questions'" + (interactive) + (org-noter--with-valid-session + (let ((org-noter-insert-note-no-questions (not org-noter-insert-note-no-questions))) + (org-noter-insert-note)))) + +(defmacro org-noter--map-ignore-headings-with-doc-file (contents match-first &rest body) + `(let (ignore-until-level) + (org-element-map ,contents 'headline + (lambda (headline) + (let ((doc-file (org-noter--doc-file-property headline)) + (location (org-noter--parse-location-property headline))) + (when (and ignore-until-level (<= (org-element-property :level headline) ignore-until-level)) + (setq ignore-until-level nil)) + + (cond + (ignore-until-level nil) ;; NOTE(nox): This heading is ignored, do nothing + ((and doc-file (not (string= doc-file (org-noter--session-property-text session)))) + (setq ignore-until-level (org-element-property :level headline)) nil) + (t ,@body)))) + nil ,match-first org-noter--note-search-no-recurse))) + +(defun org-noter-sync-prev-page-or-chapter () + "Show previous page or chapter that has notes, in relation to the current page or chapter. +This will force the notes window to popup." + (interactive) + (org-noter--with-valid-session + (let ((this-location (org-noter--doc-approx-location 0)) + (contents (org-element-contents (org-noter--parse-root))) + target-location) + (org-noter--get-notes-window 'force) + + (org-noter--map-ignore-headings-with-doc-file + contents nil + (when (and (org-noter--compare-locations '< location this-location) + (org-noter--compare-locations '>f location target-location)) + (setq target-location location))) + + (org-noter--get-notes-window 'force) + (select-window (org-noter--get-doc-window)) + (if target-location + (org-noter--doc-goto-location target-location) + (user-error "There are no more previous pages or chapters with notes"))))) + +(defun org-noter-sync-current-page-or-chapter () + "Show current page or chapter notes. +This will force the notes window to popup." + (interactive) + (org-noter--with-valid-session + (let ((window (org-noter--get-notes-window 'force))) + (select-frame-set-input-focus (window-frame window)) + (select-window window) + (org-noter--doc-location-change-handler)))) + +(defun org-noter-sync-next-page-or-chapter () + "Show next page or chapter that has notes, in relation to the current page or chapter. +This will force the notes window to popup." + (interactive) + (org-noter--with-valid-session + (let ((this-location (org-noter--doc-approx-location most-positive-fixnum)) + (contents (org-element-contents (org-noter--parse-root))) + target-location) + + (org-noter--map-ignore-headings-with-doc-file + contents nil + (when (and (org-noter--compare-locations '> location this-location) + (org-noter--compare-locations '< location target-location)) + (setq target-location location))) + + (org-noter--get-notes-window 'force) + (select-window (org-noter--get-doc-window)) + (if target-location + (org-noter--doc-goto-location target-location) + (user-error "There are no more following pages or chapters with notes"))))) + +(defun org-noter-sync-prev-note () + "Go to the location of the previous note, in relation to where the point is. +As such, it will only work when the notes window exists." + (interactive) + (org-noter--with-selected-notes-window + "No notes window exists" + (let ((org-noter--inhibit-location-change-handler t) + (contents (org-element-contents (org-noter--parse-root))) + (current-begin (org-element-property :begin (org-noter--get-containing-heading))) + previous) + (when current-begin + (org-noter--map-ignore-headings-with-doc-file + contents t + (when location + (if (= current-begin (org-element-property :begin headline)) + t + (setq previous headline) + nil)))) + + (if previous + (progn + ;; NOTE(nox): This needs to be manual so we can focus the correct note + (org-noter--doc-goto-location (org-noter--parse-location-property previous)) + (org-noter--focus-notes-region (org-noter--make-view-info-for-single-note session previous))) + (user-error "There is no previous note")))) + (select-window (org-noter--get-doc-window))) + +(defun org-noter-sync-current-note () + "Go the location of the selected note, in relation to where the point is. +As such, it will only work when the notes window exists." + (interactive) + (org-noter--with-selected-notes-window + "No notes window exists" + (if (string= (org-entry-get nil org-noter-property-doc-file t) (org-noter--session-property-text session)) + (let ((location (org-noter--parse-location-property (org-noter--get-containing-heading)))) + (if location + (org-noter--doc-goto-location location) + (user-error "No note selected"))) + (user-error "You are inside a different document"))) + (let ((window (org-noter--get-doc-window))) + (select-frame-set-input-focus (window-frame window)) + (select-window window))) + +(defun org-noter-sync-next-note () + "Go to the location of the next note, in relation to where the point is. +As such, it will only work when the notes window exists." + (interactive) + (org-noter--with-selected-notes-window + "No notes window exists" + (let ((org-noter--inhibit-location-change-handler t) + (contents (org-element-contents (org-noter--parse-root))) + next) + + (org-noter--map-ignore-headings-with-doc-file + contents t + (when (and location (< (point) (org-element-property :begin headline))) + (setq next headline))) + + (if next + (progn + (org-noter--doc-goto-location (org-noter--parse-location-property next)) + (org-noter--focus-notes-region (org-noter--make-view-info-for-single-note session next))) + (user-error "There is no next note")))) + (select-window (org-noter--get-doc-window))) + +(define-minor-mode org-noter-doc-mode + "Minor mode for the document buffer. +Keymap: +\\{org-noter-doc-mode-map}" + :keymap `((,(kbd "i") . org-noter-insert-note) + (,(kbd "C-i") . org-noter-insert-note-toggle-no-questions) + (,(kbd "M-i") . org-noter-insert-precise-note) + (,(kbd "q") . org-noter-kill-session) + (,(kbd "M-p") . org-noter-sync-prev-page-or-chapter) + (,(kbd "M-.") . org-noter-sync-current-page-or-chapter) + (,(kbd "M-n") . org-noter-sync-next-page-or-chapter) + (,(kbd "C-M-p") . org-noter-sync-prev-note) + (,(kbd "C-M-.") . org-noter-sync-current-note) + (,(kbd "C-M-n") . org-noter-sync-next-note)) + + (let ((mode-line-segment '(:eval (org-noter--mode-line-text)))) + (if org-noter-doc-mode + (if (symbolp (car-safe mode-line-format)) + (setq mode-line-format (list mode-line-segment mode-line-format)) + (push mode-line-segment mode-line-format)) + (setq mode-line-format (delete mode-line-segment mode-line-format))))) + +(define-minor-mode org-noter-notes-mode + "Minor mode for the notes buffer. +Keymap: +\\{org-noter-notes-mode-map}" + :keymap `((,(kbd "M-p") . org-noter-sync-prev-page-or-chapter) + (,(kbd "M-.") . org-noter-sync-current-page-or-chapter) + (,(kbd "M-n") . org-noter-sync-next-page-or-chapter) + (,(kbd "C-M-p") . org-noter-sync-prev-note) + (,(kbd "C-M-.") . org-noter-sync-current-note) + (,(kbd "C-M-n") . org-noter-sync-next-note))) + +;;;###autoload +(defun org-noter (&optional arg) + "Start `org-noter' session. + +There are two modes of operation. You may create the session from: +- The Org notes file +- The document to be annotated (PDF, EPUB, ...) + +- Creating the session from notes file ----------------------------------------- +This will open a session for taking your notes, with indirect +buffers to the document and the notes side by side. Your current +window configuration won't be changed, because this opens in a +new frame. + +You only need to run this command inside a heading (which will +hold the notes for this document). If no document path property is found, +this command will ask you for the target file. + +With a prefix universal argument ARG, only check for the property +in the current heading, don't inherit from parents. + +With 2 prefix universal arguments ARG, ask for a new document, +even if the current heading annotates one. + +With a prefix number ARG: +- Greater than 0: Open the document like `find-file' +- Equal to 0: Create session with `org-noter-always-create-frame' toggled +- Less than 0: Open the folder containing the document + +- Creating the session from the document --------------------------------------- +This will try to find a notes file in any of the parent folders. +The names it will search for are defined in `org-noter-default-notes-file-names'. +It will also try to find a notes file with the same name as the +document, giving it the maximum priority. + +When it doesn't find anything, it will interactively ask you what +you want it to do. The target notes file must be in a parent +folder (direct or otherwise) of the document. + +You may pass a prefix ARG in order to make it let you choose the +notes file, even if it finds one." + (interactive "P") + (cond + ;; NOTE(nox): Creating the session from notes file + ((eq major-mode 'org-mode) + (when (org-before-first-heading-p) + (user-error "`org-noter' must be issued inside a heading")) + + (let* ((notes-file-path (buffer-file-name)) + (document-property (org-noter--get-or-read-document-property (not (equal arg '(4))) + (equal arg '(16)))) + (org-noter-always-create-frame + (if (and (numberp arg) (= arg 0)) (not org-noter-always-create-frame) org-noter-always-create-frame)) + (ast (org-noter--parse-root (vector (current-buffer) document-property)))) + + (when (catch 'should-continue + (when (or (numberp arg) (eq arg '-)) + (cond ((> (prefix-numeric-value arg) 0) + (find-file document-property) + (throw 'should-continue nil)) + ((< (prefix-numeric-value arg) 0) + (find-file (file-name-directory document-property)) + (throw 'should-continue nil)))) + + ;; NOTE(nox): Check if it is an existing session + (let ((id (get-text-property (org-element-property :begin ast) org-noter--id-text-property)) + session) + (when id + (setq session (cl-loop for test-session in org-noter--sessions + when (= (org-noter--session-id test-session) id) + return test-session)) + (when session + (let* ((org-noter--session session) + (location (org-noter--parse-location-property (org-noter--get-containing-heading)))) + (org-noter--setup-windows session) + (when location (org-noter--doc-goto-location location)) + (select-frame-set-input-focus (org-noter--session-frame session))) + (throw 'should-continue nil)))) + t) + (org-noter--create-session ast document-property notes-file-path)))) + + ;; NOTE(nox): Creating the session from the annotated document + ((memq major-mode '(doc-view-mode pdf-view-mode nov-mode)) + (if (org-noter--valid-session org-noter--session) + (progn (org-noter--setup-windows org-noter--session) + (select-frame-set-input-focus (org-noter--session-frame org-noter--session))) + + ;; NOTE(nox): `buffer-file-truename' is a workaround for modes that delete + ;; `buffer-file-name', and may not have the same results + (let* ((buffer-file-name (or buffer-file-name (bound-and-true-p nov-file-name))) + (document-path (or buffer-file-name buffer-file-truename + (error "This buffer does not seem to be visiting any file"))) + (document-name (file-name-nondirectory document-path)) + (document-base (file-name-base document-name)) + (document-directory (if buffer-file-name + (file-name-directory buffer-file-name) + (if (file-equal-p document-name buffer-file-truename) + default-directory + (file-name-directory buffer-file-truename)))) + ;; NOTE(nox): This is the path that is actually going to be used, and should + ;; be the same as `buffer-file-name', but is needed for the truename workaround + (document-used-path (expand-file-name document-name document-directory)) + + (search-names (append org-noter-default-notes-file-names (list (concat document-base ".org")))) + notes-files-annotating ; List of files annotating document + notes-files ; List of found notes files (annotating or not) + + (document-location (org-noter--doc-approx-location))) + + ;; NOTE(nox): Check the search path + (dolist (path org-noter-notes-search-path) + (dolist (name search-names) + (let ((file-name (expand-file-name name path))) + (when (file-exists-p file-name) + (push file-name notes-files) + (when (org-noter--check-if-document-is-annotated-on-file document-path file-name) + (push file-name notes-files-annotating)))))) + + ;; NOTE(nox): `search-names' is in reverse order, so we only need to (push ...) + ;; and it will end up in the correct order + (dolist (name search-names) + (let ((directory (locate-dominating-file document-directory name)) + file) + (when directory + (setq file (expand-file-name name directory)) + (unless (member file notes-files) (push file notes-files)) + (when (org-noter--check-if-document-is-annotated-on-file document-path file) + (push file notes-files-annotating))))) + + (setq search-names (nreverse search-names)) + + (when (or arg (not notes-files-annotating)) + (when (or arg (not notes-files)) + (let* ((notes-file-name (completing-read "What name do you want the notes to have? " + search-names nil t)) + list-of-possible-targets + target) + + ;; NOTE(nox): Create list of targets from current path + (catch 'break + (let ((current-directory document-directory) + file-name) + (while t + (setq file-name (expand-file-name notes-file-name current-directory)) + (when (file-exists-p file-name) + (setq file-name (propertize file-name 'display + (concat file-name + (propertize " -- Exists!" + 'face '(foreground-color . "green"))))) + (push file-name list-of-possible-targets) + (throw 'break nil)) + + (push file-name list-of-possible-targets) + + (when (string= current-directory + (setq current-directory + (file-name-directory (directory-file-name current-directory)))) + (throw 'break nil))))) + (setq list-of-possible-targets (nreverse list-of-possible-targets)) + + ;; NOTE(nox): Create list of targets from search path + (dolist (path org-noter-notes-search-path) + (when (file-exists-p path) + (let ((file-name (expand-file-name notes-file-name path))) + (unless (member file-name list-of-possible-targets) + (when (file-exists-p file-name) + (setq file-name (propertize file-name 'display + (concat file-name + (propertize " -- Exists!" + 'face '(foreground-color . "green")))))) + (push file-name list-of-possible-targets))))) + + (setq target (completing-read "Where do you want to save it? " list-of-possible-targets + nil t)) + (set-text-properties 0 (length target) nil target) + (unless (file-exists-p target) (write-region "" nil target)) + + (setq notes-files (list target)))) + + (when (> (length notes-files) 1) + (setq notes-files (list (completing-read "In which notes file should we create the heading? " + notes-files nil t)))) + + (if (member (car notes-files) notes-files-annotating) + ;; NOTE(nox): This is needed in order to override with the arg + (setq notes-files-annotating notes-files) + (with-current-buffer (find-file-noselect (car notes-files)) + (goto-char (point-max)) + (insert (if (save-excursion (beginning-of-line) (looking-at "[[:space:]]*$")) "" "\n") + "* " document-base) + (org-entry-put nil org-noter-property-doc-file + (file-relative-name document-used-path + (file-name-directory (car notes-files))))) + (setq notes-files-annotating notes-files))) + + (when (> (length (cl-delete-duplicates notes-files-annotating :test 'equal)) 1) + (setq notes-files-annotating (list (completing-read "Which notes file should we open? " + notes-files-annotating nil t)))) + + (with-current-buffer (find-file-noselect (car notes-files-annotating)) + (org-with-wide-buffer + (catch 'break + (goto-char (point-min)) + (while (re-search-forward (org-re-property org-noter-property-doc-file) nil t) + (when (file-equal-p (expand-file-name (match-string 3) + (file-name-directory (car notes-files-annotating))) + document-path) + (let ((org-noter--start-location-override document-location)) + (org-noter)) + (throw 'break t))))))))))) + +(provide 'org-noter) + +;;; org-noter.el ends here diff --git a/org/init.el b/org/init.el index 721b254..461868c 100644 --- a/org/init.el +++ b/org/init.el @@ -60,7 +60,7 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; additional packages (add-to-list 'package-selected-packages - '(nov pdf-tools) + '(nov pdf-tools org-noter) ) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;