diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..8db9945 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,41 @@ +--- +name: Bug Report +about: Something's not working. +title: "" +labels: "" +assignees: "" +--- + +### Description + +#### Steps to Reproduce + + + +#### Backtrace + + +#### Expected Results + + + +#### Actual Results + + + +### Environment + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..ad7331f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,49 @@ +## Problem + + + + +## Solution + + + + +## Checklist + +- [ ] I checked the code to make sure that it works on my machine. +- [ ] I checked that the code works without my custom emacs config. +- [ ] I added unit tests. + +## Steps to Test + + + + + +## [Optional] Screenshots + + diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 0000000..64c18ee --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,27 @@ +on: push + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-python@v2 + with: + python-version: '3.10' + architecture: 'x64' + - uses: purcell/setup-emacs@master + with: + version: '28.2' + + - uses: conao3/setup-cask@master + with: + version: 'snapshot' + + - name: Install dependencies + run: cask + + - name: Run tests + run: cask exec buttercup -L . + + - name: Coveralls + uses: coverallsapp/github-action@v2 diff --git a/Cask b/Cask new file mode 100644 index 0000000..e040aaa --- /dev/null +++ b/Cask @@ -0,0 +1,10 @@ +(source gnu) +(source melpa) + +(depends-on "buttercup") +(depends-on "pdf-tools") + +(depends-on "with-simulated-input") + +(depends-on "log4e") +(depends-on "undercover") diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e2d2cef --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +export EMACS ?= $(shell which emacs) +CASK_DIR := $(shell cask package-directory) + +$(CASK_DIR): Cask + cask install + @touch $(CASK_DIR) + +.PHONY: cask +cask: $(CASK_DIR) + +.PHONY: compile +compile: cask + ! (cask eval "(let ((byte-compile-error-on-warn f)) \ + (cask-cli/build))" 2>&1 \ + | egrep -a "(Warning|Error):") ; \ + (ret=$$? ; cask clean-elc && exit $$ret) + +.PHONY: test +test: compile + cask exec buttercup -L . diff --git a/README.org b/README.org index d3b9c81..aba0060 100644 --- a/README.org +++ b/README.org @@ -1,33 +1,87 @@ -[[https://melpa.org/#/org-noter][file:https://melpa.org/packages/org-noter-badge.svg]] -[[https://stable.melpa.org/#/org-noter][file:https://stable.melpa.org/packages/org-noter-badge.svg]] -* Org-noter - A synchronized, Org-mode, document annotator -After using Sebastian Christ's amazing [[https://github.com/rudolfochrist/interleave][Interleave package]] for some time, I got some ideas -on how I could improve upon it, usability and feature-wise. So I created this package from -scratch with those ideas in mind! - -Org-noter's purpose 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 [[http://orgmode.org/][Org-mode]] (the notes may have outlines, latex -fragments, babel, etc...) while acting like notes that are made /inside/ the document. -Also, taking notes is very simple: just press @@html:@@i@@html:@@ and annotate -away! - -*Note*: While this is similar to ~interleave~, it is not intended to be a clone; -therefore, /not every feature of the original mode is available/! You may prefer using the -original, because this is a different take on the same idea. [[#diff][View some differences here.]] - -Org-noter is compatible with [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Document-View.html][DocView]], [[https://github.com/politza/pdf-tools][PDF Tools]], and [[https://github.com/wasamasa/nov.el][Nov.el]]. These modes make it possible -to annotate *PDF*, *EPUB*, *Microsoft Office*, DVI, PS, and OpenDocument. - -On a personal note, if you annotate and read lots of PDFs, give PDF Tools a try! It is -great. +* Org-noter: an interleaving/note-taking package for documents + [[https://melpa.org/#/org-noter][file:https://melpa.org/packages/org-noter-badge.svg]] + [[https://coveralls.io/github/org-noter/org-noter?branch=master][file:https://coveralls.io/repos/github/org-noter/org-noter/badge.svg?branch=master]] + + ~Org-noter~, by [[https://github.com/weirdNox][Gonçalo Santos]], was inspired by the now-orphaned ~Interleave~ + package, by [[https://github.com/rudolfochrist][Sebastian Christ]]. In Sebastian's words (with minor edits): + + #+begin_quote + In the past, textbooks were sometimes published as interleaved editions. That + meant, each page was followed by a blank page and ambitious students/scholars + had the ability to take their notes directly in their copy of the + textbook. Newton and Kant were prominent representatives of this + technique. [find reference] + + Nowadays, textbooks (or lecture materials) come in PDF format. Although almost + every PDF Reader has the ability to add some notes to the PDF itself, it is + not as powerful as it could be. This is what this Emacs minor mode tries to + accomplish. It presents your PDF side by side with an Org Mode buffer of your + notes, narrowed down to just those passages that are relevant to this + particular page in the document viewer. + #+end_quote + + Org-noter's purpose 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 + [[http://orgmode.org/][Org-mode]] (the notes may have outlines, latex fragments, babel, etc...) while + acting like notes that are made /inside/ the document. Also, taking notes is + very simple: just press =i= and annotate away! + + Org-noter is compatible with [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Document-View.html][DocView]], [[https://github.com/politza/pdf-tools][PDF Tools]], [[https://depp.brause.cc/nov.el/][Nov.el]], and + [[DJVU-read][DJVU-image-mode]]. These modes make it possible to annotate *PDF*, *EPUB*, + *Microsoft Office*, DVI, PS, OpenDocument, and DJVU formatted files. Note + that PDF support is our prime goal. Other format have been supported by other + contributors, but we need code contributions from users of other formats to + maintain/progress usability with those formats. + + +** Installation +*** vanilla + 1. clone this repo to a local directory + 2. add to your init file: + #+begin_src elisp + (add-to-list 'load-path "") + (require 'org-noter) + #+end_src + +*** straight.el + In plain ~straight.el~ syntax + #+begin_src elisp + (straight-use-package 'org-noter) + #+end_src + +*** straight.el + use-package + #+begin_src elisp + (straight-use-package 'use-package) + (setq straight-use-package-by-default t) + (use-package org-noter) + #+end_src + + Alternate form cribbed from + https://github.com/dmitrym0/org-noter-plus/issues/3 + #+begin_src elisp + (use-package org-noter + :straight + (:repo "org-noter/org-noter" + :host github + :type git + :files ("*.el" "modules/*.el"))) + #+end_src + +*** MELPA + #+begin_src elisp + (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/")) + (package-refresh-contents) + (package-install 'org-noter) + #+end_src ** Features -- Easy to use annotation interface :: Just press @@html:@@i@@html:@@ in the +*** original org-noter (up to 2019) +- Easy to use annotation interface :: Just press =i= in the document buffer and annotate away! - Keep your notes in sync with the document :: When you take a note by pressing - @@html:@@i@@html:@@, it saves the location where you took it so it is able to show you + =i=, it saves the location where you took it so it is able to show you the notes while you scroll, automatically! - Easy navigation :: You may navigate your document as usual, seeing the notes buffer scroll and show you @@ -50,38 +104,60 @@ great. - Annotate ~org-attach~'ed files :: If you have any attached files, it will let you choose one as the document to annotate. -Many of these features are demonstrated in the screencast, so take a look if you are -confused! +*** new (all formats) + - ~org-noter-enable-update-renames~ :: Optional feature to update document + paths when renaming or moving document files or notes files. This allows + you to change your mind later about the names and locations of your + document files and notes files without having to manually update all the + links. -** Installation -*** MELPA -This package is [[https://melpa.org/#/org-noter][available from MELPA]], so if you want to install it and have added MELPA to -your package archives, you can run -@@html:@@M-x@@html:@@ ~package-install~ @@html:@@RET@@html:@@ ~org-noter~ + - ~org-noter-toggle-notes-window-location~ (~M-T~) :: Toggle between + horizontal and vertical document/notes layout. + + - ~org-noter-start-from-dired~ (Suggested keybinding: ~M-s n~) :: Start + sessions directly from ~dired~. Opens all marked files or the file at + point if none are marked. + +*** new (PDFs only with the [[https://github.com/vedang/pdf-tools][pdftools]] package) + - 2D precise notes :: ([[https://github.com/ahmed-shariff/org-noter][Ahmed Shariff]]) Location tooltip appears at start of + selected text or point of click. -*** Manual installation -You can also install it manually, using =package.el=. -1. Download =org-noter.el= -2. Open it -3. Run @@html:@@M-x@@html:@@ ~package-install-from-buffer~ @@html:@@RET@@html:@@ + - Multicolumn ordering of precise notes :: With the ~COLUMN_EDGES~ property, + you can specify the number of columns in a multicolumn document (or pages + thereof). Precise notes are ordered vertically within columns. Use + ~M-x org-noter-pdf-set-columns~ to interactively set ~COLUMN_EDGES~ for + the current heading. The property is inheritable, so it can be set at the + page, chapter/section, or document level. See [[file:docs/HOWTO.org]] for + more details. + + - Highlighting of selected text :: Default behavior (on/off) is + user-customizable via ~org-noter-highlight-selected-text~. Non-default + behavior is activated with a =C-u= prefix to the note insertion command. + + - Customizable tooltip arrow colors :: ~org-noter-arrow-foreground-color~ (default + orange-red) and ~org-noter-arrow-foreground-color~ (default white) are + both user customizable. + +[TODO: make/link to screencast] ** Usage + If you want to give it a try without much trouble: -- Just have an Org file where you want the notes to go -- Create a root heading to hold the notes -- Run @@html:@@M-x@@html:@@ ~org-noter~ inside! +- Open an Org file where you want the notes to go. +- Create a root heading to hold the notes. +- Run =M-x org-noter= inside the Org file. On the first run, it will ask you for the path of the document and save it in a property. By default, it will also let you annotate an attached file [[https://orgmode.org/manual/Attachments.html][(org-attach documentation)]]. This will open a new dedicated frame where you can use [[#keys][the keybindings described here]]. More generally, there are two modes of operation. You may run -@@html:@@M-x@@html:@@ ~org-noter~: +=M-x org-noter=: - Inside a heading in an Org notes file :: This will associate that heading with a document and open a session with it. This mode is the one described in the example above. -- In a document :: Run @@html:@@M-x@@html:@@ ~org-noter~ when viewing a +- In a document :: Run =M-x org-noter= when viewing a document (eg. PDF, epub...). This will try to find the respective notes file automatically. It will @@ -91,16 +167,14 @@ More generally, there are two modes of operation. You may run There is, of course, more information in the docstrings of each command. -** Screencast -[[https://www.youtube.com/watch?v=Un0ZRXTzufo][Watch the screencast here!]] +** TODO Customization + :PROPERTIES: + :CUSTOM_ID: custom + :END: -Note that this package has had some updates since this screencast was made, so, while the -core functionality is the same, there may be some UX and feature differences. +See [[file:docs/CUSTOMIZATIONS.org]] for examples of user & maintainer customizations +to this package. -The files used to make this screencast are shipped with the package, so you can try this -package even without creating the notes. - -** Customization @@html:@@ There are two kinds of customizations you may do: 1. Global settings, affecting every session 2. Document-specific settings, which override the global settings @@ -135,24 +209,25 @@ permanent, or revert to the default): check the docstring of the variable ~org-noter-closest-tipping-point~ in order to better understand the tipping point. -** Keybindings and commands @@html:@@ -:PROPERTIES: +** Keybindings and commands + :PROPERTIES: :CUSTOM_ID: keys :END: -| Key | Description | Where? | -| @@html:@@i@@html:@@ | Insert note | Document buffer | -| @@html:@@M-i@@html:@@ | Insert precise note | Document buffer | -| @@html:@@q@@html:@@ | Kill session | Document buffer | -| @@html:@@M-p@@html:@@ | Sync previous page/chapter | Document and notes buffer | -| @@html:@@M-.@@html:@@ | Sync current page/chapter | Document and notes buffer | -| @@html:@@M-n@@html:@@ | Sync next page/chapter | Document and notes buffer | -| @@html:@@C-M-p@@html:@@ | Sync previous notes | Document and notes buffer | -| @@html:@@C-M-.@@html:@@ | Sync selected notes | Document and notes buffer | -| @@html:@@C-M-n@@html:@@ | Sync next notes | Document and notes buffer | +| Key | Description | Where? | +| =i= | Insert note | Document buffer | +| =C-i, TAB= | Insert note, toggle no-questions | Document buffer | +| =M-i= | Insert precise note | Document buffer | +| =C-M-i= | Insert precise note, toggle no-questions | Document buffer | +| =q= | Kill session | Document buffer | +| =M-p= | Sync previous page/chapter | Document and notes buffer | +| =M-.= | Sync current page/chapter | Document and notes buffer | +| =M-n= | Sync next page/chapter | Document and notes buffer | +| =C-M-p= | Sync previous notes | Document and notes buffer | +| =C-M-.= | Sync selected notes | Document and notes buffer | +| =C-M-n= | Sync next notes | Document and notes buffer | You can use the usual keybindings to navigate the document -(@@html:@@n@@html:@@, @@html:@@p@@html:@@, -@@html:@@SPC@@html:@@, ...). +(=n=, =p=, =SPC=, ...). There are two types of sync commands: - To sync a page/chapter, means it will find the [previous|current|next] page/chapter and @@ -171,54 +246,47 @@ annotations (or both!) as notes, and it may be used, for example, as a starting You may also want to check the docstrings of the functions associated with the keybindings, because there is some extra functionality in some. -** Why a rewrite from scratch? Why not contribute to the existing Interleave package? -Doing a refactor on a foreign codebase takes a long time because of several factors, like -introducing the ideas to the owner (with which he may even disagree), learning its -internals, proposing pull requests, more back and forth in code review, etc... - -Besides, I like doing things from scratch, not only because it expands my skills, but also -because it is something I find very rewarding! - -*** Non-exhaustive list of differences from Interleave @@html:@@ -:PROPERTIES: -:CUSTOM_ID: diff -:END: -**** New features -- Each session is isolated, which means that it has its own frame with indirect buffers - - Makes it possible to have several sessions simultaneously open - - Doesn't narrow the original buffer, which continues completely accessible -- Has precise notes (attached to a section of a page) -- Also supports nov.el -- Skeleton extraction (outline and/or annotations) -- Being able to use the closest previous note when no notes are present on the current - page -- Closing all notes not related to the notes present in the current view -- Possibility of overriding some global settings in each document or session - -**** Some annoyances fixed -- Notes not sorted -- Notes not synced when executing different page change commands, eg. goto-page or - beginning-of-buffer -- Sometimes it would start narrowing other parts of the buffer, giving errors when trying - to go to notes. - -*** Changes to make in order to be compatible with documents created by Interleave -This package only works like the multi-pdf mode of Interleave - you can't open a session -without having a parent headline. - -For compatibility with existing notes made with Interleave, you can do one of two things: -- Change the following property names inside the your documents: - | Old | New | - |------------------------+------------------| - | =INTERLEAVE_PDF= | =NOTER_DOCUMENT= | - | =INTERLEAVE_PAGE_NOTE= | =NOTER_PAGE= | - -- Set these variables on your init file: - #+BEGIN_SRC emacs-lisp -(setq org-noter-property-doc-file "INTERLEAVE_PDF" - org-noter-property-note-location "INTERLEAVE_PAGE_NOTE") - #+END_SRC - -** Acknowledgments -I must thank [[https://github.com/rudolfochrist][Sebastian]] for the original idea and inspiration. Also, many thanks to everyone who -contributed more ideas, reported bugs and submitted PRs :) +** Changes to note insertion since version 1.4.1 (MELPA version 20191020.1212) + - more consistent use of selected text in title or body + - more consistent primary default title (short-selected-text) + - more consistent secondary default title ("Notes for page ") + - avoids having different notes with the same heading + + Previously, repeated TAB's give multiple notes. Now, new notes in the same + location with the same title are not made, but selected text is added to + the note body. Precise notes are excepted from this rule. + + - *long* text-selections are enclosed in #+BEGIN_QUOTE...#+END_QUOTE + - *short* text-selections are enclosed in ``...'' (LaTeX style) when they are + not used as the title of the note. + - short/long text-selections are differentiated by the custom variable + =org-noter-max-short-selected-text-length= (default: 80 char) + + Previously, long selections were those with more than 3 lines. + +*** Original + | | insert-note =i= | precise note =M-i= | no-questions =C-i, TAB= | + |-----------------+--------------------------------+---------------------------+-------------------------| + | title prompt? | Y | Y | N | + | default title 1 | prior note by location | selected-text | N/A | + | default title 2 | "Notes for page #" | "Notes for page # x y" | "Notes for page #" | + | new note | with new title | always | always | + | body | selected-text on existing note | selected-text (> 3 lines) | none | + |-----------------+--------------------------------+---------------------------+-------------------------| + +*** Refactored + | | insert-note | precise note | insert, no-questions | precise, no-questions | + |---------------------+---------------------------+---------------------------+---------------------------+---------------------------| + | title prompt? | Y | Y | N | N | + | default title 1 | short-selected-text | short-selected-text | short-selected-text | short-selected-text | + | default title 2 | "Notes for page #" | "Notes for page # x y" | "Notes for page #" | "Notes for page # x y" | + | new note | with new title | always | with new title | always | + | body | selected-text (not title) | selected-text (not title) | selected-text (not title) | selected-text (not title) | + |---------------------+---------------------------+---------------------------+---------------------------+---------------------------| + | highlight selection | user setting/toggle | user setting/toggle | user setting/toggle | user setting/toggle | + +* Acknowledgments + - [[https://github.com/rudolfochrist][Sebastian Christ]] :: Author of the ~interleave~ package, inspiration for ~org-noter~ + - [[https://github.com/weirdNox][Gonçalo Santos]] :: Author of ~org-noter~ + - [[https://github.com/ahmed-shariff/org-noter][Ahmed Shariff]] :: Contributor of 2-D precise notes + - [[https://github.com/c1-g/org-noter-plus-djvu][Charlie Gordon]] :: Contributor of DJVU support and document-type modularization diff --git a/docs/CUSTOMIZATIONS.org b/docs/CUSTOMIZATIONS.org new file mode 100644 index 0000000..c430974 --- /dev/null +++ b/docs/CUSTOMIZATIONS.org @@ -0,0 +1,83 @@ +* Customizations + Out of respect for the existing user base of =org-noter=, almost all of the + user-interface features of this fork remain the same those of as the original + =org-noter=. However, users may be interested in our variations on the theme + of Gonçalos Santos. +** Peter's mods + In order of frequency, I use precise notes the most, standard notes next and + no-questions notes the least, so I have bound keys to reflect those + preferences with =i= for precise notes, =TAB= for standard notes; for + no-questions (no-prompt) notes, =I= is for precise notes and =M-i= for + standard notes.: + #+begin_src elisp + (with-eval-after-load 'org-noter + (define-key org-noter-doc-mode-map (kbd "i") 'org-noter-insert-precise-note) + (define-key org-noter-doc-mode-map (kbd "C-i") 'org-noter-insert-note) + (define-key org-noter-doc-mode-map (kbd "I") 'org-noter-insert-precise-note-toggle-no-questions) + (define-key org-noter-doc-mode-map (kbd "M-i") 'org-noter-insert-note-toggle-no-questions)) + #+end_src + + For navigation, I use =..-sync-..-note= more than =..-sync-..-page-..=, so I + bound the =note= commands to the easier-to-type =M-[p.n]= bindings and the + less-used =page-..= commands to the harder-to-type =C-M-[p.n]= bindings. + #+begin_src elisp + (with-eval-after-load 'org-noter + (define-key org-noter-doc-mode-map (kbd "M-p") 'org-noter-sync-prev-note) + (define-key org-noter-doc-mode-map (kbd "M-.") 'org-noter-sync-current-note) + (define-key org-noter-doc-mode-map (kbd "M-n") 'org-noter-sync-next-note) + (define-key org-noter-doc-mode-map (kbd "C-M-p") 'org-noter-sync-prev-page-or-chapter) + (define-key org-noter-doc-mode-map (kbd "C-M-.") 'org-noter-sync-current-page-or-chapter) + (define-key org-noter-doc-mode-map (kbd "C-M-n") 'org-noter-sync-next-page-or-chapter) + + (define-key org-noter-notes-mode-map (kbd "M-p") 'org-noter-sync-prev-note) + (define-key org-noter-notes-mode-map (kbd "M-.") 'org-noter-sync-current-note) + (define-key org-noter-notes-mode-map (kbd "M-n") 'org-noter-sync-next-note) + (define-key org-noter-notes-mode-map (kbd "C-M-p") 'org-noter-sync-prev-page-or-chapter) + (define-key org-noter-notes-mode-map (kbd "C-M-.") 'org-noter-sync-current-page-or-chapter) + (define-key org-noter-notes-mode-map (kbd "C-M-n") 'org-noter-sync-next-page-or-chapter)) + #+end_src + + In the original code, the tooltip arrow on PDFs is *Orange-Red on White*, + which works fine when the arrow is always on the left side of the page. I + found that with the 2D precise notes introduced by Ahmed Shariff, I sometime + had trouble locating the arrow as I navigated through my notes. The *Black + on Cyan* color-scheme that I use is more jarring, hence easier to locate. + #+begin_src elisp + (with-eval-after-load 'org-noter + (setq org-noter-arrow-background-color "cyan" + org-noter-arrow-foreground-color "black")) + #+end_src + +** Dmitry's mods + + [TODO] + +** Lamprinos' mods (user) + @chatziiola prefers "notes.org" as his default notes name, vs the default + default notes name "Notes.org". He also chooses to change the default + ~org-noter-notes-search-path~ from ~~/Documents~ to something else. + +In plain ~straight.el~ syntax +#+begin_src elisp + (straight-use-package 'org-noter) + (setq org-noter-notes-search-path '("~/your/path/to/notes")) + (setq org-noter-default-notes-file-names '("notes.org")) +#+end_src + +*** straight.el + use-package +For all those that use ~straight.el~ along with ~use-package~ +#+begin_src elisp + (straight-use-package 'use-package) + (setq straight-use-package-by-default t) + + (use-package org-noter + :config + (setq org-noter-notes-search-path '("~/your/path/to/notes")) + (setq org-noter-default-notes-file-names '("notes.org")) + ;; Include this only next block only if you use ~evil~ with ~general~ + :general + (general-nmap + :keymaps '(org-noter-mode-map pdf-view-mode-map) + "I" 'org-noter-insert-note-toggle-no-questions + "i" 'org-noter-insert-note)) +#+end_src diff --git a/docs/HOWTO.org b/docs/HOWTO.org new file mode 100644 index 0000000..c1ea60d --- /dev/null +++ b/docs/HOWTO.org @@ -0,0 +1,88 @@ +* HOW TO... + + +** Set up notes for a multicolumn document (~pdf-view-mode~ mode only) + + Most scientific papers are published in two-column format, while other + documents, like IRS instructions, have a mixture of one to four columns on + any given page. + + Multicolumn support requires knowledge of the column edges, specifically, the + right edge of each column. ~org-noter-pdf-set-columns~ helps you + interactively set the column edges for the *current heading* in the *notes + file*. + + For a scientific paper, it usually suffices to set up multicolumn support at + the document level, i.e., at the top header line in the notes file. For a + new paper, run ~org-noter-pdf-set-columns~ before taking any notes. If you + already have notes on the document, then move your point in the notes file to + the top heading, and then run ~org-noter-pdf-set-columns~. If you inspect + the property drawer, then you will see an entry called =:COLUMN_EDGES:= with + a list of the horizontal position of the right edges of the columns. I find + it best to locate the column edges at the *right edge of the text* in the + document, rather than in the whitespace between columns. + + Here is an example of an [[https://www.researchgate.net/publication/222714864_An_introduction_to_PoundDreverHall_laser_frequency_stabilization][/Am. J. Phys./ paper]] with a precise note low in the + first column ("D. Reflection...") and a precise note in the second column + that is vertically higher than the first-column note ("E. Measuring..."). + Without the document-level =:COLUMN_EDGES:= property, the "E" note would be + sorted /before the "D" note. + +#+begin_example org +* BlackED_Intro to PDH laser freq stabilization_AJP_2001 + :PROPERTIES: + :NOTER_DOCUMENT: QR/BlackED_Intro to PDH laser freq stabilization_AJP_2001.pdf + :COLUMN_EDGES: (0.48125585754451733 1) + :END: +** D. Reflection of a modulated beam: The error signal + :PROPERTIES: + :NOTER_PAGE: (5 0.8740043446777697 . 0.08434864104967198) + :END: +** E. Measuring the error signal + :PROPERTIES: + :NOTER_PAGE: (5 0.6307023895727734 . 0.5173383317713215) + :END: +#+end_example + + For a document with varying formats, such as the [[https://www.irs.gov/pub/irs-pdf/i1040gi.pdf][IRS instructions for Form + 1040]], it may make more sense to set up multicolumn support at the section + level, rather than the document level. In this case, there are more steps + involved: + + 1. Create a note on the first page with multiple columns + 2. run ~org-noter-pdf-set-columns~ + 3. After inserting the first *precise* note in this section, *indent the + note* so that it is a sub-heading of the note created in step 1. + + The example below is from the 2022 IRS 1040 General Instructions. The first + 3-column page in the document is at the 6th page ("What's New"). + +#+begin_example org +* i1040gi + :PROPERTIES: + :NOTER_DOCUMENT: i1040gi.pdf + :NOTER_PAGE: 17 + :END: +** What's New + :PROPERTIES: + :NOTER_PAGE: 6 + :COLUMN_EDGES: (0.331302717900656 0.6312089971883786 1) + :END: +*** Due date of return. + :PROPERTIES: + :NOTER_PAGE: (6 0.1448225923244026 . 0.07216494845360825) + :END: +*** Child tax credit + :PROPERTIES: + :NOTER_PAGE: (6 0.667632150615496 . 0.3692596063730084) + :END: +*** Reporting requirements + :PROPERTIES: + :NOTER_PAGE: (6 0.4532947139753801 . 0.6672914714151827) + :END: +** Notes for page 17 + :PROPERTIES: + :NOTER_PAGE: 17 + :COLUMN_EDGES: (0.4887535145267104 1) + :END: +#+end_example diff --git a/docs/README-djvu.org b/docs/README-djvu.org new file mode 100644 index 0000000..b856608 --- /dev/null +++ b/docs/README-djvu.org @@ -0,0 +1,39 @@ +* Org-noter + DJVU - A fork of [[https://github.com/weirdNox/org-noter][org-noter]] with [[https://elpa.gnu.org/packages/djvu.html][djvu.el]] support. +** What is org-noter? +Org-noter is an org-mode document annotator. It is a rewrite of another project, [[https://github.com/rudolfochrist/interleave][Interleave]]. +Read about org-noter’s feature/usage/customization in its README [[file:README-orig.org][here]] or go visit its repository at [[https://github.com/weirdNox/org-noter][github]]. +** Added features + - =org-noter= can be used on djvu file. + - =org-noter-create-skeleton= now works on djvu and pdf file. + - Allow =:NOTER_DOCUMENT:= to be in file-level =:PROPERTIES:= drawer + See the issue https://github.com/weirdNox/org-noter/issues/143 for details. + - There is a module in the [[file:other/][other/]] directory called + [[file:other/org-noter-nov-overlay.el][org-noter-nov-overlay]] that highlights your notes with overlays in + nov-mode, to use it, either add it to your =load-path= or with [[https://github.com/jwiegley/use-package][use-package]], +#+begin_src emacs-lisp + (use-package org-noter-nov-overlay + :ensure nil) +#+end_src + +** Goals + - [ ] Precise location support djvu.el have both graphical (via + =djvu-image-mode=) and textual (default) modes so maybe the textual + mode should handle precise location like nov.el and graphical mode + should do it like pdf-tools. +** Installation +*** Manual installation +1. Clone this repo =git clone https://notabug.org/c1-g/org-noter-plus-djvu.git= +2. Add it to your =load-path=. +** Caveats +*** This is a fork of a fork 🍴 +The branch =djvu= (the only one I’d worked on so far) of this repository is based on Ahmed Shariff’s pull request of org-noter +(See https://github.com/weirdNox/org-noter/pull/129). + +*** Conflicting keybindings +The key @@html:@@i@@html:@@ (lowercase i) for inserting +note in the document buffer will conflict with the default key to +enter =djvu-image-mode=. So either you rebind one of them or you can +just enter =djvu-image-mode= manually via @@html:@@M-x djvu-image-mode@@html:@@ + + + diff --git a/docs/README-interim.org b/docs/README-interim.org new file mode 100644 index 0000000..4cf1211 --- /dev/null +++ b/docs/README-interim.org @@ -0,0 +1,193 @@ +#+STARTUP: hidestars +* ORG-NOTER (fork of a fork) +** If you are new to *ORG-NOTER* + Please first look at [[https://github.com/weirdNox/org-noter/blob/master/README.org][Gonçalo Santos's README]] ([[file:docs/README-orig.org]] in + this repo). + +** This is a refactor of the *c1-g* fork done collaboratively by [[https://github.com/petermao/org-noter][petermao]] and [[https://github.com/dmitrym0/org-noter-plus-djvu][dmitrym0]] + - In essence, this is close to the original *weirdNox* (Gonçalo Santos) + version that you find on MELPA with the refactoring that *c1-g* implemented. + + - djvu and epub support have also been advanced by *c1-g*, but we have not + tested any of that code. See [[https://github.com/c1-g/org-noter-plus-djvu/blob/master/README.org][Charlie Gordon's README]] + ([[file:docs/README-djvu.org]] in this repo). + + - Prior to our work, the main new feature for PDF users was 2-D precise notes + (introduced by *Ahmed Shariff*, which can be inserted in two ways: + 1. "Select-precise": selecting text in the pdf, followed by =M-i=. *c1-g* + changed the position format to use the entire list returned by =edges=. + This breaks the other way of inserting precise notes. On *dmitrym0*'s + fork, select-precise notes auto-fill the note title with the selected + text. This has deleterious side effects for other note insertion methods. + 2. "Click-precise": =M-i=, followed by clicking a location on the page. + + - Standard notes can also be inserted in two ways: + 1. "TAB". Hitting tab creates a note title "Notes for page ##", where the + page number is inserted automatically over the octothorpes. + 2. "i". The fundamental =org-noter-insert-note=. User types note title + into the minibuffer. + +** Stock vs refactored note insertion + - more consistent use of selected text in title or body + - more consistent primary default title (short-selected-text) + - more consistent secondary default title ("Notes for page ") + - avoids having different notes with the same heading + + In Stock org-noter, repeated TAB's give multiple notes. In this refactor, + new notes in the same location with the same title are not made, but + selected text is added to the note body. For now, precise notes are + excepted from this rule. + + - long text-selections are enclosed in #+BEGIN_QUOTE...#+END_QUOTE + - short text-selections are enclosed in ``...'' (LaTeX style) when they are + not used as the title of the note. + - short/long text-selections are differentiated by the custom variable + =org-noter-max-short-selected-text-length= (default: 80 char) + + In Stock org-noter, long selections are those with more than 3 lines. + + - *NEW:* Highlighting of selected text + + Default setting is customizable, see =org-noter-highlight-selected-text=. + Calling insertions with a non-nil prefix (eg, =C-u=) toggles this setting for + individual note insertions. + + - *NEW:* multicolumn precise-notes defined by property NUM_COLUMNS + + Implements issue #153 in weirdNox/org-noter by adding + =org-noter-pdf-convert-to-location-cons= to + =org-noter--convert-to-location-cons-hook=, where a "virtual" vertical + location is calculated by dividing the page into equal width columns. This + is not a perfect solution, and is probably best for low-integer numbers of + columns, since the page is just divided evenly into =NUM_COLUMNS= vertical + strips. + + This can be set using =org-set-property-and-value=, but eventually we will + write a user function to prevent users from mistyping the name of the + property (=NUM_COLUMNS=). + +*** Stock + | | insert-note =i= | precise note =M-i= | no-questions =C-i, TAB= | + |-----------------+--------------------------------+---------------------------+-------------------------| + | title prompt? | Y | Y | N | + | default title 1 | prior note by location | selected-text | N/A | + | default title 2 | "Notes for page #" | "Notes for page # x y" | "Notes for page #" | + | new note | with new title | always | always | + | body | selected-text on existing note | selected-text (> 3 lines) | none | + |-----------------+--------------------------------+---------------------------+-------------------------| + +*** Refactored + | | insert-note | precise note | no-questions | + |---------------------+---------------------------+---------------------------+---------------------------| + | title prompt? | Y | Y | N | + | default title 1 | short-selected-text | short-selected-text | short-selected-text | + | default title 2 | "Notes for page #" | "Notes for page # x y" | "Notes for page #" | + | new note | with new title | always | with new title | + | body | selected-text (not title) | selected-text (not title) | selected-text (not title) | + |---------------------+---------------------------+---------------------------+---------------------------| + | highlight selection | user setting/toggle | user setting/toggle | user setting/toggle | +** Features +*** New + 1. Use pdf-view-current-pagelabel to use the page label instead of page in + default titles + + new function/hook =...-pretty-print-location-for-title= + + 2. Customizable tooltip arrow colors + - =...-arrow-foreground-color= + - =...-arrow-background-color= + + 3. Text-selection higlighting: customizable default behavior, toggle + =...-highlight-selected-text= with =C-u= prefix on note-insertion + commands. + + 4. Rudimentary support for multicolumn PDFs with inheritable =NUM_COLUMNS= + property. See =...-pdf-convert-to-location-cons= + +*** Wishlist + 1. Bind M- to precise-note, no-questions. + + 2. Make background of arrow transparent (see org-noter--show-arrow) + maybe https://emacs.stackexchange.com/questions/45588/how-to-make-tooltip-background-transparent + + 3. Dedicated insert-selected-text-into-page-note + + 4. Internationalize precise notes to handle right-to-left languages. +** Bugs +*** to fix + 1. Sometimes (when?) M-p doesn't pick up the containing note-at-point right + away (or at all), requiring user to manually type in the (existing) title + + 2. With NUM_COLUMNS > 1, point in notes document doesn't land in the correct + place + + 3. With NUM_COLUMNS > 1, columns don't necessarily start at horizontal + positions k/NUM_COLUMNS for k \in {1,..,NUM_COLUMNS}. We need to write a + user interface that builds a list of horizontal fractions to delimit the + columns. + +*** fixed + 1. vertically stacked doc/notes layout fixed + + 2. =org-noter-sync-next-page-or-chapter= navigation fixed + + 3. Navigating up from a nested precise note lands in the prior note at the + next level up (eg level 3 -> level 2). page notes behave properly. + + [file:org-noter-core.el:2179] + =(org-element-property :begin (org-noter--get-containing-element))= returns + the begin of the element one level up when the current note location is of + the form ( . ). It works properly for locations of + the form ( . ). + + It will be one of these two: + - =org-noter--get-containing-heading= + - =org-noter--check-location-property= + found bug: [[file:org-noter-core.el:1023]] change test from integerp to numberp + - =org-noter--get-containing-property-drawer= +** Custom variables + Presently, the custom variables listed under =customize-group org-noter= is a + flat list. I would like to group them into logical categories. + +*** start-stop + - org-noter-supported-modes '(doc-view-mode pdf-view-mode nov-mode djvu-read-mode) + - org-noter-auto-save-last-location nil + - org-noter-default-notes-file-names '("Notes.org") + - org-noter-notes-search-path '("~/Documents") + - org-noter-notes-window-behavior '(start scroll) + - org-noter-suggest-from-attachments t + - org-noter-find-additional-notes-functions nil + - org-noter-always-create-frame t + - org-noter-kill-frame-at-session-end t + - org-noter-use-indirect-buffer t + +*** layout + - org-noter-notes-window-location 'horizontal-split + - org-noter-doc-split-fraction '(0.5 . 0.5) + - org-noter-disable-narrowing nil + - org-noter-swap-window nil + - org-noter-hide-other t + +*** note-insertion + - org-noter-default-heading-title "Notes for page $p$" + - org-noter-separate-notes-from-heading nil + - org-noter-insert-selected-text-inside-note t + - org-noter-highlight-selected-text nil + - org-noter-max-short-selected-text-length 80 + - org-noter-insert-heading-hook nil + - org-noter-insert-note-no-questions nil + +*** navigation-display + - org-noter-arrow-delay 0.2 + - org-noter-arrow-horizontal-offset -0.02 + - org-noter-arrow-foreground-color "orange red" + - org-noter-arrow-background-color "white" + - org-noter-closest-tipping-point 0.3 + - org-noter-no-notes-exist-face + - org-noter-notes-exist-face + +*** other + - org-noter-property-doc-file "NOTER_DOCUMENT" + - org-noter-property-note-location "NOTER_PAGE" + - org-noter-prefer-root-as-file-level nil # used in org-noter--parse-root + - org-noter-doc-property-in-notes nil diff --git a/docs/org-noter-demo.org b/docs/org-noter-demo.org new file mode 100644 index 0000000..fc3ad90 --- /dev/null +++ b/docs/org-noter-demo.org @@ -0,0 +1,70 @@ +* Opening a notes session + - open PDF + - ~M-x org-noter~ + - ~M-x org-noter-set-doc-split-fraction~ + - ~M-x org-noter-set-notes-window-location, M-n~ (org-noter-set-layout?) side-by-side | stacked + - ~M-x org-noter-create-skeleton, M-n~ + +* Navigating the document and the notes + - ~SPC, n, Page-down, down~ to move forward in document + - ~BACKSPACE, p, Page-up, up~ to move back in document + - ~C-M-n, C-M-p~ to move to next/prev note + - ~C-M-.~ sync document to notes + - ~M-n, M-p~ to move to next/prev page with note(s). Always lands on the + first note of the page. + - ~M-.~ sync notes to document + +* Multicolumn setup (as needed) + - Multiple column note ordering can be set up at the document or heading level + - ~org-noter-pdf-set-columns~ inserts "COLUMN_EDGES" into the property drawer + of the current heading. The command requests the number of columns and then + asks you to click on the right edge of all but the last column. The + property is inherited by all sub-headings. + +* Note insertion + +** ~insert-note~ (~i~) + - Inserts a note linked to the current page. If no title is specified, then + default title "Notes for page

" is used, where

is the pagelabel if + it exists or the page number. + - If text is selected AND it is "short" (see ~defcustom + org-noter-max-short-selected-text-length~) the the selected text becomes + the default title. + - If you type in a title, then the selected text is quoted in the body of the + note. Short selected text is set in ``LaTeX-style quotes,'' + #+begin_quote + while long selected text is set inside QUOTE block delimiters. + #+end_quote + - At the title prompt =Note:=, you can use ~M-p~ to "up-arrow" prior note + headings, or ~M-n~ to select from the defaults. + - If you choose a prior note heading, then selected text will be quoted in + that heading. + +** ~insert-precise-note~ (~M-i~) + - Precise notes always create a new note, even if you choose an existing + prior heading. + - Precise notes are linked to a specific point on the page specified with + vertical and horizontal coordinates. + - The multicolumn property ~COLUMN_EDGES~, set by + ~org-noter-pdf-set-columns~, governs the ordering of precise notes on a + page. + - If no title is specified, then default title "Notes for page

V: % H: + %" is used, where

is the pagelabel if it exists or the page number, + is the vertical distance from the top and is the horizontal + position from the left. + - The behavior with selected text (default title, quoting in the body) is the + same as for ~insert-note~. + +** No-questions note insertion + - ~defcustom org-noter-insert-note-no-questions~ is default ~nil~. If set to + ~t~, the note title minibuffer prompt is bypassed and a note is always + create with the default title. Activate this setting if you rarely or + never type in your own titles. + - Both note insertion styles have a ~toggle-no-questions~ variant to get the + non-default behavior. + - Default keybinding for the ~toggle-no-questions~ variant adds the + control-key (~C-i~ and ~C-M-i~, respectively). +** Highlighting + - ~defcustom org-noter-highlight-selected-text~ controls the default + highlighting behavior of selected text. + - ~C-u~ prefix to any note insertion command toggles this behavior diff --git a/docs/org_noter_tech_notes.org b/docs/org_noter_tech_notes.org new file mode 100644 index 0000000..3bd5aef --- /dev/null +++ b/docs/org_noter_tech_notes.org @@ -0,0 +1,172 @@ +:PROPERTIES: +:ID: 4333050B-D293-4A41-8A14-00E6248FD17B +:DRILL_LAST_INTERVAL: -1.0 +:DRILL_REPEATS_SINCE_FAIL: 1 +:DRILL_TOTAL_REPEATS: 1 +:DRILL_FAILURE_COUNT: 1 +:DRILL_AVERAGE_QUALITY: 1.0 +:DRILL_EASE: 2.5 +:NEXT_REVIEW: [2022-12-29 Thu] +:MATURITY: seedling +:LAST_REVIEW: [2022-12-30 Fri] +:END: +#+title: org-noter-tech-notes +#+filetags: :seedling: + +Context for developing org-noter. + +* TOC :TOC: +- [[#brief-history-of-org-noter][Brief history of org-noter]] +- [[#tech-notes][Tech Notes]] + - [[#session][Session]] + - [[#hooks][Hooks]] + - [[#notes][Notes]] + - [[#locations][Locations]] + - [[#note-taking-behavior][Note taking behavior]] + - [[#notes-file][Notes file]] +- [[#solove-nothing-to-hide][solove-nothing-to-hide]] + - [[#note-from-page-1][Note from page 1]] +- [[#development][Development]] + - [[#unit-tests][Unit tests]] + +* Brief history of org-noter + +[[https://github.com/weirdNox/org-noter][org-noter]] (2018-2020), a re-implementation of the [[https://github.com/rudolfochrist/interleave/][interleave packaage]] (2015-2018) by weirdNox: + +#+begin_quote +Yeah, I made org-noter because it is something I need for studying everyday, but I bet that if I didn't use it, then I would probably lose interest too... We don't have time for everything, so decisions must be made :P +#+end_quote + + [[https://github.com/rudolfochrist/interleave/issues/55][source]] + +In early 2022, c1-g created a fork, [[https://github.com/c1-g/org-noter-plus-djvu][org-noter-plus-djvu]] that split up note creation functionality from the underlying document format making it possible to take notes with pdf, epub and djvu documents. + + +* Tech Notes + +** Session +org-noter session contains all the relevant info for the current session. +- notes file (or buffer) +- backing document +- ... + +prereq for getting everything else going see =make-org-noter-session=. + +** Hooks +Hooks are used extensively to manage "non-core" functionality, that is functionality that is mode dependent. + +There are mode specific implementation in =modules/= directory, as well as in =tests/= (for testing). + + +*** Errors +An error like so: +#+begin_src shell + Lisp nesting exceeds ‘max-lisp-eval-depth’ +#+end_src + +possibly indicates that a ~run-hook-with-args-until-success~ has failed: + +#+begin_src elisp + (run-hook-with-args-until-success 'org-noter-set-up-document-hook document-property-value) +#+end_src + +in my experience the error indicates a problem with one of the hooks. For example in the code above one of the hooks in =org-noter-set-up-document-hook= may not be valid elisp code (requires more than one argument or another lisp issue). + +I haven't figured out a good way to identify these. + +** Notes + +There are two types of notes: + +- regular notes +- precise notes + + + +*** Regular Notes + +Notes attached to a "page". + +#+begin_src org-mode +:PROPERTIES: +:NOTER_PAGE: 2 +:END: +#+end_src + + +*** Precise notes + +Precise notes include a coordinate vector that allows to identify the selection in the backing document and act accordingly, ie create a highlight. + +#+begin_src org-mode +:PROPERTIES: +:NOTER_PAGE: (75 0.14417344173441735 0.7955390334572491 0.6834688346883468 0.8199091284593144) +:END: +#+end_src + +** Locations + +Location is a property of every note (see [[Notes]]). + +The concept seems to be poorly defined currently, and most of the code lives in =core=, but maybe it should move to document implementation. + +Currently it's either + +=:NOTER_PAGE: (75 0.14417344173441735 0.7955390334572491 0.6834688346883468 0.8199091284593144)= + +or + +=:NOTER_PAGE: 2= + +** Note taking behavior + +Sophisticated note taking behavior is possible, based on selection size, etc see [[https://github.com/petermao/org-noter/blob/doc/README.org][Peter's matrix]]. + + +** Notes file + +=notes.org= is in this format: + +#+begin_src org-mode +:PROPERTIES: +:ID: FAKE_90283 +:END: +#+TITLE: Test book notes + +* solove-nothing-to-hide +:PROPERTIES: +:NOTER_DOCUMENT: pubs/solove-nothing-to-hide.pdf +:END: +** Note from page 1 +:PROPERTIES: +:NOTER_PAGE: 99 +:END: +#+end_src + +Omitting the header causes =org-noter--parse-root= to work incorrectly. + +* Development +** Unit tests +*** Requirements + +- [[https://github.com/cask/cask][Cask]], a project management tool for Emacs +- [[https://github.com/jorgenschaefer/emacs-buttercup][Buttercup]], behavior driven Emacs testing + +*** Mac + +#+begin_src shell + brew install cask + cask # install dependencies + cask exec buttercup -L . # exec unit tests, in the root of the project +#+end_src + +*** GNU/Linux + +#+begin_src shell + git clone https://github.com/cask/cask.git + make -C cask install + + cd + cask # install dependencies (in root of project, 1 time) + cask exec buttercup -L . # exec unit tests, in the root of the project +#+end_src diff --git a/docs/pre-merge_notes.org b/docs/pre-merge_notes.org new file mode 100644 index 0000000..3ebded9 --- /dev/null +++ b/docs/pre-merge_notes.org @@ -0,0 +1,210 @@ +* Pre-merge deltas w/ *dmitrym0* + In the diffs below the color coding is + #+begin_src diff +- Dmitry [f3f5a05] ++ Peter [6488cc6] + #+end_src +** DONE *-get-buffer-file-name-* +#+begin_src diff +-(defun org-noter-get-buffer-file-name-* (&optional major-mode) ++(defun org-noter-get-buffer-file-name-* (mode) + (bound-and-true-p *-file-name)) + ++(add-to-list 'org-noter-get-buffer-file-name-hook #'org-noter-get-buffer-file-name-*) +#+end_src + + - =major-mode= is a native elisp function, =mode= is a better name + - the arg is not used, so the =&optional= is appropriate + - for the =pdf= variant, we both use =(&optional major-mode)= + + proposal: =(&optional mode)= or remove the argument completely. + + ACTIONS: Do this in our own repos before merge + 1. major-mode -> mode in module files + 2. use &optional when the argument is not used in the function + +** DONE -get-buffer-file-name-hook +#+begin_src diff +-(defcustom org-noter-get-buffer-file-name-hook '(org-noter-get-buffer-file-name-nov org-noter-get-buffer-file-name-pdf) ++(defcustom org-noter-get-buffer-file-name-hook nil +#+end_src + + should be nil in =org-noter-core= and set in modules. + + ACTION: already converged +** DONE *-get-precise-info-* +#+begin_src diff +-(defun org-noter-*--get-precise-info (major-mode) ++(defun org-noter-*--get-precise-info (major-mode window) + (when (eq major-mode 'djvu-read-mode) + (if (region-active-p) + (cons (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))))) ++ (let ((event nil)) ++ (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)))))) +#+end_src + + - calling function already calls =org-noter--get-doc-window= + - =window= is used in all document modes + + proposal: change =major-mode= to =mode=, pass in =window= + + ACTION: (done) Dmitry took mine +** DONE *-goto-location +#+begin_src diff +-(defun org-noter-pdf-goto-location (mode location) ++(defun org-noter-pdf-goto-location (mode location window) + (when (memq mode '(doc-view-mode pdf-view-mode)) + (let ((top (org-noter--get-location-top location)) +- (window (org-noter--get-doc-window)) + (left (org-noter--get-location-left location))) +#+end_src + - calling function already calls =org-noter--get-doc-window= + - nov and djvu don't need the =window= argument + + proposal: we discuss this one, but I think it's better to not call functions + unnecessarily + + ACTION: + pass in window, use &optional as appropriate. + +** DONE *-check-location-property + #+begin_src diff + (defun org-noter-pdf-check-location-property (&optional property) + "Check if PROPERTY is a valid location property" +- (equal 5 (length (read property)))) ++ t) + #+end_src + + location can be + 1. page + 2. page v-pos + 3. page v-pos . h-pos + + neither function works properly. need to read the calling function to + determine course of action.. + + ACTION: done, gone on Dmitry's side. + P: check diff, remove if it's still there. +** DONE -doc--get-precise-info +#+begin_src diff ++(defun org-noter-doc--get-precise-info (major-mode window) ++ (when (eq major-mode 'doc-view-mode) + (let ((event nil)) + (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!"))) +- (let ((col-row (posn-col-row (event-start event)))) +- (org-noter--conv-page-scroll-percentage (+ (window-vscroll) (cdr col-row)) +- (+ (window-hscroll) (car col-row)))))))) ++ (org-noter--conv-page-scroll-percentage (+ (window-vscroll) ++ (cdr (posn-col-row (event-start event)))))))) +#+end_src +Dmitry removed this function at [9d437bf] + +ACTION: Dmitry revive on his side. +** DONE --doc-approx-location-hook + #+begin_src diff + (defcustom org-noter--doc-approx-location-hook nil +- "This returns an approximate location if no precise info is passed: (PAGE 0) +- or if precise info is passed, it's (PAGE 0 0 0 0) where 0s are the precise coords) +-" ++ "TODO" + :group 'org-noter + :type 'hook) + #+end_src + + docstring needs to be updated. + + ACTION: Dmitry reverted +** DONE --note-search-no-recurse :11fc0a8:9dfac53: +#+begin_src diff ++(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.") +#+end_src + + called in =org-noter--get-view-info= by =org-element-map= +#+begin_src diff +- nil nil (delete 'headline (append org-element-all-elements nil)))) ++ nil nil org-noter--note-search-no-recurse) +#+end_src + + but this defconst is used by =org-noter--map-ignore-headings-with-doc-file=, which is + used by all of the sync functions + + probably should keep it, and since we keep it, use it in + =org-noter--get-view-info= + + ACTION: safe for Dmitry to cherry-pick these commits, but + =with-current-buffer= call gets removed. This is the one change I took from + ~cbpnk~ +** DONE org-noter--create-session :9dfac53: + #+begin_src diff + (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)) +- (link-p (or (string-match-p org-bracket-link-regexp document-property-value) ++ (link-p (or (string-match-p org-link-bracket-re document-property-value) + (string-match-p org-noter--url-regexp document-property-value))) + #+end_src + =org-bracket-link-regexp= is obsolete. keep mine. + + ACTION: safe for Dmitry to cherry-pick +** DONE org-noter--narrow-to-root (ast) :dfe7df2: +#+begin_src diff +- (when ast ++ (when (and ast (not (org-noter--no-heading-p))) + (save-excursion + (goto-char (org-element-property :contents-begin ast)) + (org-show-entry) +- (when (org-at-heading-p) (org-narrow-to-subtree)) ++ (org-narrow-to-subtree) + (org-cycle-hide-drawers 'all)))) +#+end_src + "I don't really understand this bit of code, especially what `ast' is, but + it breaks narrowing when multiple documents' notes are stored in a single + file." + + ACTION: safe for Dmitry to cherry-pick +** DONE org-noter--get-location-page (location) :DM:629fbb6: + #+begin_src diff + "Get the page number given a LOCATION of form (page top . left) or (page . top)." +- (message "===> %s" location) +- (if (listp location) +- (car location) +- location)) ++ (car location)) + #+end_src + + ACTION: Peter -- what happens with a page note (no precise location)? does (car location) make an + error? + Answer: No, (car location) works fine because for a page note, location is a + cons cell, e.g. (19 . 0) by the time it reaches this function. + + @DM -- I think we should go back to the original (car location). + + HISTORY: + - 5bc5754 Ahmed Shariff original code + - c1ed245 c1g moved code from org-noter.el to org-noter-core.el, changing + function name + - 629fbb6 introduced by DM + +** DONE org-noter-kill-session :9dfac53: + #+begin_src diff + (with-current-buffer notes-buffer + (remove-hook 'kill-buffer-hook 'org-noter--handle-kill-buffer t) + (restore-buffer-modified-p nil)) +- (unless org-noter-use-indirect-buffer ++ (when org-noter-use-indirect-buffer + (kill-buffer notes-buffer)) + #+end_src + kill the notes buffer **when** an indirect buffer is used, not **unless** it + is used + + ACTION: safe for Dmitry to cherry-pick +** DONE use cl-lib or native elisp hash tables rather than the =ht= package. diff --git a/emacs-devel.el b/emacs-devel.el new file mode 100644 index 0000000..2cecb8e --- /dev/null +++ b/emacs-devel.el @@ -0,0 +1,7 @@ +(require 'cask "/opt/homebrew/share/emacs/site-lisp/cask/cask.el") +(cask-initialize ".") + +(setq mac-option-modifier 'meta) + +(push (expand-file-name ".") load-path) +(require 'org-noter) diff --git a/modules/org-noter-djvu.el b/modules/org-noter-djvu.el new file mode 100644 index 0000000..ed208e0 --- /dev/null +++ b/modules/org-noter-djvu.el @@ -0,0 +1,157 @@ +;;; org-noter-djvu.el --- Module for DJVU -*- lexical-binding: t; -*- + +;; Copyright (C) 2022 c1-g + +;; Author: c1-g +;; Keywords: multimedia + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; + +;;; Code: +(require 'org-noter-core) + +(eval-when-compile ; ensure that the compiled code knows about DJVU, if installed + (condition-case nil + (require 'djvu) + (error (message "`djvu' package not found")))) +(condition-case nil ; run time warning + (require 'djvu) + (error (message "ATTENTION: org-noter-djvu needs the package `djvu'"))) + +(push "djvu" org-noter--doc-extensions) + +(defun org-noter-djvu--pretty-print-location (location) + (org-noter--with-valid-session + (when (eq (org-noter--session-doc-mode session) 'djvu-read-mode) + (format "%s" (if (or (not (org-noter--get-location-top location)) (<= (org-noter--get-location-top location) 0)) + (car location) + location))))) + +(add-to-list 'org-noter--pretty-print-location-hook #'org-noter-djvu--pretty-print-location) +(add-to-list 'org-noter--pretty-print-location-for-title-hook #'org-noter-djvu--pretty-print-location) + +(defun org-noter-djvu--approx-location-cons (mode &optional precise-info _force-new-ref) + (when (eq mode 'djvu-read-mode) + (cons djvu-doc-page (if (or (numberp precise-info) + (and (consp precise-info) + (numberp (car precise-info)) + (numberp (cdr precise-info)))) + precise-info + (max 1 (/ (+ (window-start) (window-end nil t)) 2)))))) + +(add-to-list 'org-noter--doc-approx-location-hook #'org-noter-djvu--approx-location-cons) + +(defun org-noter-djvu--get-precise-info (mode window) + (when (eq mode 'djvu-read-mode) + (if (region-active-p) + (cons (mark) (point)) + (let ((event nil)) + (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)))))) + +(add-to-list 'org-noter--get-precise-info-hook #'org-noter-djvu--get-precise-info) + +(defun org-noter-djvu--setup-handler (mode) + (when (eq mode 'djvu-read-mode) + (advice-add 'djvu-init-page :after 'org-noter--location-change-advice) + t)) + +(add-to-list 'org-noter-set-up-document-hook #'org-noter-djvu--setup-handler) + +(defun org-noter-djvu--goto-location (mode location &optional window) + "DJVU mode function for `org-noter--doc-goto-location-hook'. +MODE is the document mode and LOCATION is the note location. +WINDOW is required by the hook, but not used in this function." + (when (eq mode 'djvu-read-mode) + (djvu-goto-page (car location)) + (goto-char (org-noter--get-location-top location)))) + +(add-to-list 'org-noter--doc-goto-location-hook #'org-noter-djvu--goto-location) + +(defun org-noter-djvu--get-current-view (mode) + (when (eq mode 'djvu-read-mode) + (vector 'paged (car (org-noter-djvu--approx-location-cons mode))))) + +(add-to-list 'org-noter--get-current-view-hook #'org-noter-djvu--get-current-view) + +(defun org-noter-djvu--get-selected-text (mode) + (when (and (eq mode 'djvu-read-mode) + (region-active-p)) + (buffer-substring-no-properties (mark) (point)))) + +(add-to-list 'org-noter-get-selected-text-hook #'org-noter-djvu--get-selected-text) + +(defun org-noter-djvu--create-skeleton (mode) + (when (eq mode 'djvu-read-mode) + (org-noter--with-valid-session + (let* ((ast (org-noter--parse-root)) + (top-level (or (org-element-property :level ast) 0)) + output-data) + (require 'thingatpt) + (with-current-buffer (djvu-ref outline-buf) + (unless (string= (buffer-string) "") + (push (vector "Skeleton" nil 1) output-data) + (save-excursion + (goto-char (point-min)) + (while (not (looking-at "^$")) + (push (vector (string-trim-right (string-trim (thing-at-point 'line t)) " [[:digit:]]+") + (list (string-trim-left (string-trim (thing-at-point 'line t)) ".* ")) + (+ 2 (how-many " " (point-at-bol) (point-at-eol)))) output-data) + (forward-line))))) + + (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 + level) + + (dolist (data (nreverse output-data)) + (setq title (aref data 0) + location (aref data 1) + relative-level (aref data 2)) + + (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")))) + + (setq ast (org-noter--parse-root)) + (org-noter--narrow-to-root ast) + (goto-char (org-element-property :begin ast)) + (when (org-at-heading-p) (outline-hide-subtree)) + (org-show-children 2))) + output-data)))) + +(add-to-list 'org-noter-create-skeleton-functions #'org-noter-djvu--create-skeleton) + +(provide 'org-noter-djvu) +;;; org-noter-djvu.el ends here diff --git a/modules/org-noter-nov.el b/modules/org-noter-nov.el new file mode 100644 index 0000000..9786ffb --- /dev/null +++ b/modules/org-noter-nov.el @@ -0,0 +1,207 @@ +;;; org-noter-nov.el --- Integration with Nov.el -*- lexical-binding: t; -*- + +;; Copyright (C) 2022 c1-g + +;; Author: c1-g +;; Keywords: multimedia + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; + +;;; Code: +(require 'org-noter-core) + +(eval-when-compile ; ensure that the compiled code knows about NOV, if installed + (condition-case nil + (require 'nov) + (error (message "`nov' package not found")))) +(condition-case nil ; run time warning + (require 'nov) + (error (message "ATTENTION: org-noter-nov needs the package `nov'"))) + +(push "epub" org-noter--doc-extensions) + +(defvar nov-documents-index) +(defvar nov-file-name) +(defvar-local org-noter--nov-timer nil + "Timer for synchronizing notes after scrolling.") + +(defun org-noter-nov--get-buffer-file-name (&optional _mode) + (bound-and-true-p nov-file-name)) + +(add-to-list 'org-noter-get-buffer-file-name-hook #'org-noter-nov--get-buffer-file-name) + +(defun org-noter-nov--approx-location-cons (mode &optional precise-info _force-new-ref) + (when (eq mode 'nov-mode) + (cons nov-documents-index (if (or (numberp precise-info) + (and (consp precise-info) + (numberp (car precise-info)) + (numberp (cdr precise-info)))) + precise-info + (max 1 (/ (+ (window-start) (window-end nil t)) 2)))))) + +(add-to-list 'org-noter--doc-approx-location-hook #'org-noter-nov--approx-location-cons) + +(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)))) + +(defun org-noter-nov--setup-handler (mode) + (when (eq mode 'nov-mode) + (advice-add 'nov-render-document :after 'org-noter-nov--scroll-handler) + (add-hook 'window-scroll-functions 'org-noter-nov--scroll-handler nil t) + t)) + +(add-to-list 'org-noter-set-up-document-hook #'org-noter-nov--setup-handler) + +(defun org-noter-nov--no-sessions-remove-advice () + "Remove nov-specific advice when all sessions are closed." + (advice-remove 'nov-render-document 'org-noter-nov--scroll-handler)) + +(add-to-list 'org-noter--no-sessions-remove-advice-hooks #'org-noter-nov--no-sessions-remove-advice) + +(defun org-noter-nov--pretty-print-location (location) + (org-noter--with-valid-session + (when (eq (org-noter--session-doc-mode session) 'nov-mode) + (format "%s" (if (or (not (org-noter--get-location-top location)) (<= (org-noter--get-location-top location) 1)) + (org-noter--get-location-page location) + location))))) + +(add-to-list 'org-noter--pretty-print-location-hook #'org-noter-nov--pretty-print-location) +(add-to-list 'org-noter--pretty-print-location-for-title-hook #'org-noter-nov--pretty-print-location) + +(defun org-noter-nov--get-precise-info (mode window) + (when (eq mode 'nov-mode) + (if (region-active-p) + (cons (mark) (point)) + (let ((event nil)) + (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)))))) + +(add-to-list 'org-noter--get-precise-info-hook #'org-noter-nov--get-precise-info) + +(defun org-noter-nov--goto-location (mode location &optional _window) + (when (eq mode 'nov-mode) + (setq nov-documents-index (org-noter--get-location-page location)) + (nov-render-document) + (goto-char (org-noter--get-location-top location)) + ;; NOTE(nox): This needs to be here, because it would be issued anyway after + ;; everything and would run org-noter--nov-scroll-handler. + (recenter))) + +(add-to-list 'org-noter--doc-goto-location-hook #'org-noter-nov--goto-location) + +(defun org-noter-nov--get-current-view (mode) + (when (eq mode 'nov-mode) + (vector 'nov + (org-noter-nov--approx-location-cons mode (window-start)) + (org-noter-nov--approx-location-cons mode (window-end nil t))))) + +(add-to-list 'org-noter--get-current-view-hook #'org-noter-nov--get-current-view) + +(defun org-noter-nov--get-selected-text (mode) + (when (and (eq mode 'nov-mode) (region-active-p)) + (buffer-substring-no-properties (mark) (point)))) + +(add-to-list 'org-noter-get-selected-text-hook #'org-noter-nov--get-selected-text) + + +;; Shamelessly stolen code from Yuchen Li. +;; This code is originally from org-noter-plus package. +;; At https://github.com/yuchen-lea/org-noter-plus + +(defun org-noter-nov--handle-toc-item (ol depth) + (mapcar (lambda (li) + (mapcar (lambda (a-or-ol) + (pcase-exhaustive (dom-tag a-or-ol) + ('a + (vector :depth depth + :title (dom-text a-or-ol) + :href (esxml-node-attribute 'href a-or-ol))) + ('ol + (org-noter-nov--handle-toc-item a-or-ol + (1+ depth))))) + (dom-children li))) + (dom-children ol))) + +(defun org-noter-nov--create-skeleton-epub (mode) + "Epub outline with nov link." + (when (eq mode 'nov-mode) + (require 'esxml) + (require 'nov) + (require 'dom) + (org-noter--with-valid-session + (let* ((ast (org-noter--parse-root)) + (top-level (or (org-element-property :level ast) 0)) + output-data) + (with-current-buffer (org-noter--session-doc-buffer session) + (let* ((toc-path (cdr (aref nov-documents 0))) + (toc-tree (with-temp-buffer + (insert (nov-ncx-to-html toc-path)) + (goto-char (point-min)) + (while (re-search-forward "\n" nil t) + (replace-match "" nil nil)) + (libxml-parse-html-region (point-min) + (point-max)))) + (origin-index nov-documents-index) + (origin-point (point))) + (dolist (item + (nreverse (flatten-tree (org-noter-nov--handle-toc-item toc-tree 1)))) + (let ((relative-level (aref item 1)) + (title (aref item 3)) + (url (aref item 5))) + (apply 'nov-visit-relative-file + (nov-url-filename-and-target url)) + (when (not (integerp nov-documents-index)) + (setq nov-documents-index 0)) + (push (vector title (list nov-documents-index (point)) relative-level) output-data))) + (push (vector "Skeleton" (list 0) 1) output-data) + + (nov-goto-document origin-index) + (goto-char origin-point))) + (save-excursion + (goto-char (org-element-property :end ast)) + (with-current-buffer (org-noter--session-notes-buffer session) + (dolist (data output-data) + (let* ((title (aref data 0)) + (location (aref data 1)) + (relative-level (aref data 2)) + (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")))) + (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))) + output-data)))) + +(add-to-list 'org-noter-create-skeleton-functions #'org-noter-nov--create-skeleton-epub) + +(provide 'org-noter-nov) +;;; org-noter-nov.el ends here diff --git a/modules/org-noter-pdf.el b/modules/org-noter-pdf.el new file mode 100644 index 0000000..fa91777 --- /dev/null +++ b/modules/org-noter-pdf.el @@ -0,0 +1,521 @@ +;;; org-noter-pdf.el --- Modules for PDF-Tools and DocView mode -*- lexical-binding: t; -*- + +;; Copyright (C) 2022 c1-g + +;; Author: c1-g +;; Keywords: multimedia + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; + +;;; Code: +(eval-when-compile (require 'subr-x)) +(require 'cl-lib) +(require 'org-noter-core) +(eval-when-compile ; ensure that the compiled code knows about PDF-TOOLS, if installed + (condition-case nil + (require 'pdf-tools) + (error (message "`pdf-tools' package not found")))) +(condition-case nil ; inform user at run time if pdf-tools is missing + (require 'pdf-tools) + (error (message "ATTENTION: org-noter-pdf has many featues that depend on the package `pdf-tools'"))) + +(push "pdf" org-noter--doc-extensions) +(cl-defstruct pdf-highlight page coords) + +(defun org-noter-pdf--get-highlight () + "If there's an active pdf selection, returns a that contains all +the relevant info (page, coordinates) + +Otherwise returns nil" + (if-let* ((_ (pdf-view-active-region-p)) + (page (image-mode-window-get 'page)) + (coords (pdf-view-active-region))) + (make-pdf-highlight :page page :coords coords) + nil)) + +(add-to-list 'org-noter--get-highlight-location-hook 'org-noter-pdf--get-highlight) + +(defun org-noter-pdf--pretty-print-highlight (highlight-info) + (format "%s" highlight-info)) + +(add-to-list 'org-noter--pretty-print-highlight-location-hook #'org-noter-pdf--pretty-print-highlight) + +(defun org-noter-pdf--approx-location-cons (mode &optional precise-info _force-new-ref) + "Return location as a cons cell. +Runs when MODE is `doc-view-mode' or `pdf-view-mode' + +Returns page location as (page . 0). When processing +PRECISE-INFO, return (page v-pos) or (page v-pos . h-pos)." + (when (memq mode '(doc-view-mode pdf-view-mode)) + (cons (image-mode-window-get 'page) (if (or (numberp precise-info) + (and (consp precise-info) + (numberp (car precise-info)) + (numberp (cdr precise-info)))) + precise-info 0)))) + +(add-to-list 'org-noter--doc-approx-location-hook #'org-noter-pdf--approx-location-cons) + +(defun org-noter-pdf--get-buffer-file-name (&optional _mode) + "Return the file naming backing the document buffer. + +MODE (unused) is required for this type of hook." + (bound-and-true-p pdf-file-name)) + +(add-to-list 'org-noter-get-buffer-file-name-hook #'org-noter-pdf--get-buffer-file-name) + +(defun org-noter-pdf--pdf-view-setup-handler (mode) + (when (eq 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) + t)) + +(add-to-list 'org-noter-set-up-document-hook #'org-noter-pdf--pdf-view-setup-handler) + +(defun org-noter-pdf--doc-view-setup-handler (mode) + (when (eq 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) + t)) + +(add-to-list 'org-noter-set-up-document-hook #'org-noter-pdf--doc-view-setup-handler) + +(defun org-noter-pdf--no-sessions-remove-advice () + "Remove doc-view-specific advice when all sessions are closed." + (advice-remove 'doc-view-goto-page 'org-noter--location-change-advice)) + +(add-to-list 'org-noter--no-sessions-remove-advice-hooks #'org-noter-pdf--no-sessions-remove-advice) + +(defun org-noter-pdf--pretty-print-location (location) + "Formats LOCATION with full precision for property drawers." + (org-noter--with-valid-session + (when (memq (org-noter--session-doc-mode session) '(doc-view-mode pdf-view-mode)) + (format "%s" (if (or (not (org-noter--get-location-top location)) (<= (org-noter--get-location-top location) 0)) + (car location) + location))))) + +(add-to-list 'org-noter--pretty-print-location-hook #'org-noter-pdf--pretty-print-location) + +(defun org-noter-pdf--pretty-print-location-for-title (location) + "Convert LOCATION to a human readable format. +With `pdf-view-mode', the format uses pagelabel and vertical and +horizontal percentages. With `doc-view-mode', this falls back to +original pretty-print function." + (org-noter--with-valid-session + (let ((mode (org-noter--session-doc-mode session)) + (vpos (org-noter--get-location-top location)) + (hpos (org-noter--get-location-left location)) + (vtxt "") (htxt "") + pagelabel) + (cond ((eq mode 'pdf-view-mode) ; for default title, reference pagelabel instead of page + (if (> hpos 0) + (setq htxt (format " H: %d%%" (round (* 100 hpos))))) + (if (or (> vpos 0) (> hpos 0)) + (setq vtxt (format " V: %d%%" (round (* 100 vpos))))) + (select-window (org-noter--get-doc-window)) + (setq pagelabel (pdf-view-current-pagelabel)) + (select-window (org-noter--get-notes-window)) + (format "%s%s%s" pagelabel vtxt htxt)) + ((eq mode 'doc-view-mode) ; fall back to original pp for doc-mode + (org-noter-pdf--pretty-print-location location)))))) + +(add-to-list 'org-noter--pretty-print-location-for-title-hook #'org-noter-pdf--pretty-print-location-for-title) + +(defun org-noter-pdf--pdf-view-get-precise-info (mode window) + (when (eq mode 'pdf-view-mode) + (let (v-position h-position) + (if (pdf-view-active-region-p) + (let ((edges (car (pdf-view-active-region)))) + (setq v-position (min (nth 1 edges) (nth 3 edges)) + h-position (min (nth 0 edges) (nth 2 edges)))) + + (let ((event nil)) + (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!"))) + (let* ((col-row (posn-col-row (event-start event))) + (click-position (org-noter--conv-page-scroll-percentage (+ (window-vscroll) (cdr col-row)) + (+ (window-hscroll) (car col-row))))) + (setq v-position (car click-position) + h-position (cdr click-position))))) + (cons v-position h-position)))) + +(add-to-list 'org-noter--get-precise-info-hook #'org-noter-pdf--pdf-view-get-precise-info) + +(defun org-noter-pdf--doc-view-get-precise-info (mode window) + (when (eq mode 'doc-view-mode) + (let ((event nil)) + (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)))))))) + +(add-to-list 'org-noter--get-precise-info-hook #'org-noter-pdf--doc-view-get-precise-info) + +(defun org-noter-pdf--goto-location (mode location window) + (when (memq mode '(doc-view-mode pdf-view-mode)) + (let ((top (org-noter--get-location-top location)) + (left (org-noter--get-location-left location))) + + (if (eq mode 'doc-view-mode) + (doc-view-goto-page (org-noter--get-location-page location)) + (pdf-view-goto-page (org-noter--get-location-page 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 + top + left)))) + (image-scroll-up (- (org-noter--conv-page-percentage-scroll top) + (floor (+ (window-vscroll) org-noter-vscroll-buffer))))))) + +(add-to-list 'org-noter--doc-goto-location-hook #'org-noter-pdf--goto-location) + +(defun org-noter-pdf--get-current-view (mode) + (when (memq mode '(doc-view-mode pdf-view-mode)) + (vector 'paged (car (org-noter-pdf--approx-location-cons mode))))) + +(add-to-list 'org-noter--get-current-view-hook #'org-noter-pdf--get-current-view) + +(defun org-noter-pdf--get-selected-text (mode) + (when (and (eq mode 'pdf-view-mode) + (pdf-view-active-region-p)) + (mapconcat 'identity (pdf-view-active-region-text) ? ))) + +(add-to-list 'org-noter-get-selected-text-hook #'org-noter-pdf--get-selected-text) + +;; NOTE(nox): From machc/pdf-tools-org +(defun org-noter-pdf--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)))))) + +(defalias 'org-noter--pdf-tools-edges-to-region 'org-noter-pdf--edges-to-region + "For ORG-NOTER-PDFTOOLS backward compatiblity. The name of the +underlying function is currently under discussion") + +(defun org-noter-pdf--create-skeleton (mode) + "Create notes skeleton with the PDF outline or annotations." + (when (eq mode 'pdf-view-mode) + (org-noter--with-valid-session + (let* ((ast (org-noter--parse-root)) + (top-level (or (org-element-property :level ast) 0)) + (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--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))) + output-data)))) + +(add-to-list 'org-noter-create-skeleton-functions #'org-noter-pdf--create-skeleton) + +(defun org-noter-pdf--create-missing-annotation () + "Add a highlight from a selected note." + (let ((location (org-noter--parse-location-property (org-noter--get-containing-element))) + (window (org-noter--get-doc-window))) + (org-noter-pdf--goto-location 'pdf-view-mode location window) + (pdf-annot-add-highlight-markup-annotation (cdr location)))) + +(defun org-noter-pdf--highlight-location (mode precise-location) + "Highlight a precise location in PDF." + (message "---> %s %s" mode precise-location) + (when (and (memq mode '(doc-view-mode pdf-view-mode)) + (pdf-view-active-region-p)) + (pdf-annot-add-highlight-markup-annotation (pdf-view-active-region)))) + +(add-to-list 'org-noter--add-highlight-hook #'org-noter-pdf--highlight-location) + +(defun org-noter-pdf--convert-to-location-cons (location) + "Encode precise LOCATION as a cons cell for note insertion ordering. +Converts (page v . h) precise locations to (page v') such that +v' represents the fractional distance through the page along +columns, so it takes values between 0 and the number of columns. +Each column is specified by its right edge as a fractional +horizontal position. Output is nil for standard notes and (page +v') for precise notes." + (if-let* ((_ (and (consp location) (consp (cdr location)))) + (column-edges-string (org-entry-get nil "COLUMN_EDGES" t)) + (right-edge-list (car (read-from-string column-edges-string))) + ;;(ncol (length left-edge-list)) + (page (car location)) + (v-pos (cadr location)) + (h-pos (cddr location)) + (column-index (seq-position right-edge-list h-pos #'>=))) + (cons page (+ v-pos column-index)))) + +(add-to-list 'org-noter--convert-to-location-cons-hook #'org-noter-pdf--convert-to-location-cons) + +(defun org-noter-pdf--show-arrow () + ;; From `pdf-util-tooltip-arrow'. + (pdf-util-assert-pdf-window) + (let* (x-gtk-use-system-tooltips + (arrow-top (aref org-noter--arrow-location 2)) ; % of page + (arrow-left (aref org-noter--arrow-location 3)) + (image-top (if (floatp arrow-top) + (round (* arrow-top (cdr (pdf-view-image-size)))))) ; pixel location on page (magnification-dependent) + (image-left (if (floatp arrow-left) + (floor (* arrow-left (car (pdf-view-image-size)))))) + (dx (or image-left + (+ (or (car (window-margins)) 0) + (car (window-fringes))))) + (dy (or image-top 0)) + (pos (list dx dy dx (+ dy (* 2 (frame-char-height))))) + (vscroll (pdf-util-required-vscroll pos)) + (tooltip-frame-parameters + `((border-width . 0) + (internal-border-width . 0) + ,@tooltip-frame-parameters)) + (tooltip-hide-delay 3)) + + (when vscroll + (image-set-window-vscroll vscroll)) + (setq dy (max 0 (- dy + (cdr (pdf-view-image-offset)) + (window-vscroll nil t) + (frame-char-height)))) + (when (overlay-get (pdf-view-current-overlay) 'before-string) + (let* ((e (window-inside-pixel-edges)) + (xw (pdf-util-with-edges (e) e-width)) + (display-left-margin (/ (- xw (car (pdf-view-image-size t))) 2))) + (cl-incf dx display-left-margin))) + (setq dx (max 0 (+ dx org-noter-arrow-horizontal-offset))) + (pdf-util-tooltip-in-window + (propertize + " " 'display (propertize + "\u2192" ;; right arrow + 'display '(height 2) + 'face `(:foreground + ,org-noter-arrow-foreground-color + :background + ,(if (bound-and-true-p pdf-view-midnight-minor-mode) + (cdr pdf-view-midnight-colors) + org-noter-arrow-background-color)))) + dx dy))) + +(add-to-list 'org-noter--show-arrow-hook #'org-noter-pdf--show-arrow) + +(defun org-noter-pdf-set-columns (num-columns) + "Interactively set the COLUMN_EDGES property for the current heading. +NUM-COLUMNS can be given as an integer prefix or in the +minibuffer. The user is then prompted to click on the right edge +of each column, except for the last one. Subheadings of the +current heading inherit the COLUMN_EDGES property." + (interactive "NEnter number of columns: ") + (select-window (org-noter--get-doc-window)) + (let (event + edge-list + (window (car (window-list)))) + (dotimes (ii (1- num-columns)) + (while (not (and (eq 'mouse-1 (car event)) + (eq window (posn-window (event-start event))))) + (setq event (read-event (format "Click on the right boundary of column %d" (1+ ii))))) + (let* ((col-row (posn-col-row (event-start event))) + (click-position (org-noter--conv-page-scroll-percentage (+ (window-vscroll) (cdr col-row)) + (+ (window-hscroll) (car col-row)))) + (h-position (cdr click-position))) + (setq event nil) + (setq edge-list (append edge-list (list h-position))))) + (setq edge-list (append edge-list '(1))) + (select-window (org-noter--get-notes-window)) + (org-entry-put nil "COLUMN_EDGES" (format "%s" (princ edge-list))))) + +;;; override some deleterious keybindings in pdf-view-mode. +(define-key org-noter-doc-mode-map (kbd "C-c C-c") + (defun org-noter-pdf--execute-CcCc-in-notes () + "Override C-c C-c in pdf document buffer." + (interactive) + (select-window (org-noter--get-notes-window)) + (org-ctrl-c-ctrl-c))) + +(define-key org-noter-doc-mode-map (kbd "C-c C-x") + (defun org-noter-pdf--execute-CcCx-in-notes () + "Override C-c C-x in pdf document buffer." + (interactive) + (let ((this-CxCc-cmd (vector (read-event)))) + (select-window (org-noter--get-notes-window)) + (execute-kbd-macro + (vconcat (kbd "C-c C-x") this-CxCc-cmd))))) + +(provide 'org-noter-pdf) +;;; org-noter-pdf.el ends here diff --git a/org-noter-core.el b/org-noter-core.el new file mode 100644 index 0000000..82e736b --- /dev/null +++ b/org-noter-core.el @@ -0,0 +1,2604 @@ +;;; org-noter-core.el --- Core functions of Org-noter -*- lexical-binding: t; -*- + +;; Copyright (C) 2017-2019 Gonçalo Santos + +;; Author: Gonçalo Santos (aka. weirdNox@GitHub) + +;; 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 . + +;;; Code: +(require 'org) +(require 'org-element) +(require 'cl-lib) + +(declare-function org-noter "org-noter") +(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 org-attach-dir "org-attach") +(declare-function org-attach-file-list "org-attach") + +;; -------------------------------------------------------------------------------- +;;; User variables +(defgroup org-noter nil + "A synchronized, external annotator." + :group 'convenience + :version "25.3.1") + +(defgroup org-noter-layout nil + "Org-noter layout and visibility variables." + :group 'org-noter + :version "28.2") + +(defgroup org-noter-navigation nil + "Org-noter navigation and display variables." + :group 'org-noter + :version "28.2") + +(defgroup org-noter-insertion nil + "Org-noter note-insertion variables." + :group 'org-noter + :version "28.2") + +(defcustom org-noter-supported-modes '(doc-view-mode pdf-view-mode nov-mode djvu-read-mode) + "Major modes that are supported by org-noter." + :group 'org-noter + :type '(repeat symbol)) + +(defvar org-noter--doc-extensions nil + "List of extensions handled by org-noter when documents are moved. +Used by `org-noter--update-doc-rename-in-notes'. This variable gets filled in by supported modes, so it is not a `defcustom' variable.") + +(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-insertion + :type 'string) + +(defcustom org-noter-notes-window-behavior '(start scroll) + "Specifies situations for which the notes window is created. + +When the list contains: +- `start', the notes window will be created when starting an + `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 + "The default document/notes window layout. +Options are: \"Horizontal\", \"Vertical\", or \"Other 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-layout + :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-layout + :type '(cons (number :tag "Horizontal fraction") (number :tag "Vertical fraction"))) + +(defcustom org-noter-auto-save-last-location nil + "Option to save document location in notes file. +When non-nil, save the last visited location automatically; when +starting a new session, go to that location. When nil, sessions +start at the beginning of the document." + :group 'org-noter + :type 'boolean) + +(defcustom org-noter-prefer-root-as-file-level nil + "Option to preferentially use the file-level property drawer. + +When non-nil, org-noter will always try to return the file-level +property drawer even when there are headings. + +With the default value nil, org-noter will always use the first +heading as root when there is at least one heading." + :group 'org-noter + :type 'boolean) + +(defcustom org-noter-hide-other t + "Hide notes that are not linked to the current document page. +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-layout + :type 'boolean) + +(defcustom org-noter-always-create-frame t + "Create a new frame for each document session. +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-disable-narrowing nil + "Disable narrowing in notes/org buffer." + :group 'org-noter-layout + :type 'boolean) + +(defcustom org-noter-use-indirect-buffer t + "Use indirect buffer for notes. +When non-nil, org-noter will create an indirect buffer of the +calling org file as a note buffer of the session. When nil, it +will use the real buffer." + :group 'org-noter + :type 'boolean) + +(defcustom org-noter-swap-window nil + "Swap the left/right or top/bottom layout of the doc and notes. + +By default `org-noter' will make a session by setting the buffer +of the selected window to the document buffer then split with the +window of the notes buffer on the right. + +If this variable is non-nil, the buffers of the two windows will +be the other way around." + :group 'org-noter-layout + :type 'boolean) + + +(defcustom org-noter-suggest-from-attachments t + "Suggest document files from attachments (in an Org file). +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-insertion + :type 'boolean) + +(defcustom org-noter-insert-selected-text-inside-note t + "Option to append selected text to existing note. + +When non-nil (default), it will automatically append the selected +text into an existing note. + +When nil, selected text will not be appended to existing +note (not recommended)." + :group 'org-noter-insertion + :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-navigation + :type 'number) + +(defcustom org-noter-default-notes-file-names '("Notes.org") + "List of possible names for the default notes file. +The list is 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 + "Delay (in seconds) afte a sync before showing the tooltip arrow. + +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-navigation + :type 'number) + +(defcustom org-noter-arrow-horizontal-offset -20 + "Horizontal offset of the tooltip arrow relative to a precise location. + +Units are display pixels; positive values move the arrow to the +right, while negative values move it to the left. The intent is +to move the arrow so that it does not cover text of intereest, +but roundoff errors cause the arrow position still to be +dependent upon magnification at the 1-em level" + :group 'org-noter-navigation + :type 'number + :version "28.2") + +(defcustom org-noter-arrow-foreground-color "orange red" + "Default color of the tooltip arrow." + :group 'org-noter-navigation + :type 'string + :version "28.2") + +(defcustom org-noter-arrow-background-color "white" + "Default background color of the tooltip arrow." + :group 'org-noter-navigation + :type 'string + :version "28.2") + +(defcustom org-noter-vscroll-buffer 5 + "Minimum number of document display lines to leave above precise note. +Navigation will scroll precise notes to the top of the buffer. A +value of 0 places the precise note at the top of the window when +possible. A positive number leaves some context above the +precise note location." + :group 'org-noter-navigation + :type 'number + :version "28.2") + +(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 + "Do not prompt for a note title. +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 one of +defaults: the selected text (if it does not exceed +`org-noter-max-short-selected-text-length') or +`org-noter-default-heading-title'." + :group 'org-noter-insertion + :type 'boolean) + +(defcustom org-noter-kill-frame-at-session-end t + "Close the frame when exiting a session. +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-insertion + :type 'hook) + +(defcustom org-noter-create-session-from-document-hook '(org-noter--create-session-from-document-file-default) + "Hook that is invoked when `org-noter' is invoked from a document." + :group 'org-noter + :type 'hook) + +(defcustom org-noter-highlight-selected-text nil + "Highlight selected text when creating notes. +If non-nil, highlight selected-text when creating notes. This +variable is temporarily toggled by prefixing the insertion +command with any non-nil prefix such as \\[universal-argument]." + :group 'org-noter-insertion + :type 'boolean + :version "28.2") + +(defcustom org-noter-max-short-selected-text-length 80 + "Maximum length of a short text selection. +Short text selections are the primary default note title. When +they are quoted in the note, they are quoted as +``short-selected-text'' rather than inside a QUOTE-block." + :group 'org-noter-insertion + :type 'integer + :version "28.2") + +(defcustom org-noter-find-additional-notes-functions nil + "List of functions that map a document to an Org-noter filepath. + +The functions in this list must accept 1 argument, a file name. +The argument will be given by `org-noter'. + +The return value must be a path to an org file. No matter if +it's an absolute or relative path, the file name will be expanded +to each directory set in `org-noter-notes-search-path' to test if +it exists. + +If it exists, it will be listed as a candidate that `org-noter' +will have the user select to use as the note file of the +document." + :group 'org-noter + :type 'hook + :version "28.2") + +(defcustom org-noter-headline-title-decoration "" + "Decoration (emphasis) for the headline title string. + +If you use the Org STARTUP option 'entitiespretty', filenames +with underscores will end up looking ugly. This string is +prepended and appended to the document title in the top-level +headline, making it look nicer. + +Reasonable choices are: /, *, =, ~, _ + +With '/', 'The_Title' would become '/The_Title/'." + :group 'org-noter + :type 'string + :version "28.2") + +(defface org-noter-no-notes-exist-face + '((t + :foreground "chocolate" + :weight bold)) + "Face for modeline note count, when 0." + :group 'org-noter-navigation) + +(defface org-noter-notes-exist-face + '((t + :foreground "SpringGreen" + :weight bold)) + "Face for modeline note count, when not 0." + :group 'org-noter-navigation) + +;; -------------------------------------------------------------------------------- +;;; Integration with other packages +(defgroup org-noter-module-hooks nil + "Hooks for integrating org-noter with other packages (pdfview, nov, djvu)." + :group 'org-noter + :version "28.2") + +(defcustom org-noter--get-location-property-hook nil + "The list of functions that will return the note location of an org element. + +These functions must accept one argument, an org element. +These functions is used by `org-noter--parse-location-property' and +`org-noter--check-location-property' when they can't find the note location +of the org element given to them, that org element will be passed to +the functions in this list." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter--get-containing-element-hook '(org-noter--get-containing-heading + org-noter--get-containing-property-drawer) + "List of functions that return the Org element of a note. + +These functions will be called by +`org-noter--get-containing-element' to get the Org element of the +note at point." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter-parse-document-property-hook nil + "The list of functions that parse NOTER_DOCUMENT for a filename. +Or whatever the property `org-noter-property-doc-file' is set to. + +This is used by `org-noter--get-or-read-document-property' and +`org-noter--doc-file-property'. + +This is added for integration with other packages. + +For example, the module `org-noter-citar' adds the function +`org-noter-citar-find-document-from-refs' to this list which when +the property \"NOTER_DOCUMENT\" (the default value of +`org-noter-property-doc-file') of an org file passed to it is a +citation key, it will return the path to the note file associated +with the citation key and that path will be used for other +operations instead of the real value of the property." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter-get-buffer-file-name-hook nil + "Functions that when passed a major mode, return the current buffer file name. + +This is used by the `org-noter' command to determine the file name when +user calls `org-noter' on a document buffer. + +For example, `nov-mode', a renderer for EPUB documents uses a unique variable +called `nov-file-name' to store the file name of its document while the other +major modes use the variable `buffer-file-name'." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter-set-up-document-hook nil + "TODO." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter-get-selected-text-hook nil + "TODO." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter--check-location-property-hook nil + "TODO." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter--parse-location-property-hook nil + "TODO." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter--pretty-print-location-hook nil + "TODO." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter--pretty-print-location-for-title-hook nil + "TODO." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter--convert-to-location-cons-hook nil + "TODO." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter--doc-goto-location-hook nil + "TODO." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter--pretty-print-highlight-location-hook nil + "Hook that serializes a highlight location so that it can be stored in org." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter--get-highlight-location-hook nil + "Hook that runs to get the location of a highlight." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter--add-highlight-hook nil + "Hook called to highlight selected text when creating notes. +When a note is created this will be given `MAJOR-MODE' and +`PRECISE-INFO'. For example, this hook can be used in pdf-mode +to add a permanent highlight to the document." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter--note-after-tipping-point-hook nil + "TODO." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter--relative-position-to-view-hook nil + "TODO." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter--get-precise-info-hook nil + "TODO." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter--get-current-view-hook nil + "TODO." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter--doc-approx-location-hook nil + "This returns an approximate location if no precise info is passed: (PAGE 0) +or if precise info is passed, it's (PAGE V . H)." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter-create-skeleton-functions nil + "List of functions that convert document outline into noter headlines. + +The functions will be given a major mode of the document and must +return a non-nil value when the outline is created. + +Used by `org-noter-create-skeleton'." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter-open-document-functions nil + "Functions that gives a buffer when passed with a document property. +Used by `org-noter--create-session' when creating a new session." + :group 'org-noter-module-hooks + :type 'hook) + +(defcustom org-noter--show-arrow-hook nil + "List of functions that show precise note location in document. +For example, see `org-noter-pdf--show-arrow'." + :group 'org-noter-module-hooks + :type 'hook) + +;; -------------------------------------------------------------------------------- +;;; 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 org-noter--arrow-location nil + "A vector that shows where the arrow should appear, when idling. +Format: [TIMER WINDOW TOP LEFT]") + +(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--note-search-element-type '(headline) + "List of elements that should be searched for notes.") + +(defconst org-noter--id-text-property 'org-noter-session-id + "Text property used to mark the headings with open sessions.") + +(defvar org-noter--url-regexp + (concat + "\\b\\(\\(www\\.\\|\\(s?https?\\|ftp\\|file\\|gopher\\|" + "nntp\\|news\\|telnet\\|wais\\|mailto\\|info\\):\\)" + "\\(//[-a-z0-9_.]+:[0-9]*\\)?" + (let ((chars "-a-z0-9_=#$@~%&*+\\/[:word:]") + (punct "!?:;.,")) + (concat + "\\(?:" + ;; Match paired parentheses, e.g. in Wikipedia URLs: + ;; http://thread.gmane.org/47B4E3B2.3050402@gmail.com + "[" chars punct "]+" "(" "[" chars punct "]+" ")" + "\\(?:" "[" chars punct "]+" "[" chars "]" "\\)?" + "\\|" + "[" chars punct "]+" "[" chars "]" + "\\)")) + "\\)") + "Regular expression that matches URLs.") + +(defvar org-noter--no-sessions-remove-advice-hooks nil + "List of functions to remove advice when all sessions are closed.") + +;; -------------------------------------------------------------------------------- +;;; Utility functions + +(defun org-noter--no-heading-p () + "Return nil if the current buffer has atleast one heading. +Otherwise return the maximum value for point." + (save-excursion + (and (org-before-first-heading-p) (org-next-visible-heading 1)))) + +(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-parse-link (s) + (pcase (with-temp-buffer + (let ((org-inhibit-startup nil)) + (insert s) + (org-mode) + (goto-char (point-min)) + (org-element-link-parser))) + (`nil nil) + (link link))) + +(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)) + (link-p (or (string-match-p org-link-bracket-re document-property-value) + (string-match-p org-noter--url-regexp document-property-value))) + (display-name (if raw-value-not-empty + (org-element-property :raw-value ast) + (if link-p + document-property-value + (file-name-nondirectory document-property-value)))) + + (frame-name (format "Emacs Org-noter - %s" display-name)) + (document (or (run-hook-with-args-until-success 'org-noter-open-document-functions document-property-value) + (if link-p + (progn (org-link-open-from-string document-property-value) + (current-buffer)) + (find-file-noselect document-property-value)))) + (document-major-mode (if (or link-p (eq document (current-buffer))) + document-property-value + (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 document) + + (notes-buffer + (progn (when (and org-window-config-before-follow-link link-p) + (set-window-configuration org-window-config-before-follow-link)) + (if org-noter-use-indirect-buffer + (make-indirect-buffer + (or (buffer-base-buffer) + (current-buffer)) + (generate-new-buffer-name (concat "Notes of " display-name)) t) + (current-buffer)))) + + (single (eq (or (buffer-base-buffer document-buffer) + document-buffer) + (or (buffer-base-buffer notes-buffer) + notes-buffer))) + + (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 (or (org-element-property :level ast) 0) + :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 + (or (run-hook-with-args-until-success 'org-noter-set-up-document-hook document-major-mode) + (run-hook-with-args-until-success 'org-noter-set-up-document-hook document-property-value) + (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-element t))))) + + ;; NOTE(nox): This timer is for preventing reflowing too soon. + (unless single + (run-with-idle-timer + 0.05 nil + (lambda () + ;; NOTE(ahmed-shariff): setup-window run here to avoid crash when notes buffer not setup in time + (org-noter--setup-windows session) + (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) (or (stringp wanted-prop) + (eq 'link (org-element-type 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 + (while t + (let ((document-property (org-entry-get nil org-noter-property-doc-file t))) + (when (string= (or (run-hook-with-args-until-success 'org-noter-parse-document-property-hook document-property) + document-property) + wanted-prop) + (setq root-pos (copy-marker (if (and org-noter-prefer-root-as-file-level + (save-excursion + (goto-char (point-min)) + (eq 'property-drawer (org-element-type (org-element-at-point))))) + (point-min) + (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 (if (or org-noter-prefer-root-as-file-level (org-noter--no-heading-p)) + (setq root-pos (copy-marker (point-min))) + (org-next-visible-heading 1) + (setq root-pos (copy-marker (point))))) + (with-current-buffer (marker-buffer root-pos) + (org-with-point-at (marker-position root-pos) + (org-back-to-heading-or-point-min t) + (if (org-at-heading-p) + (org-narrow-to-subtree) + (org-hide-drawer-toggle 'force)) + (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 (or (org-element-map section 'property-drawer 'identity nil t) + (org-element-map contents '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 (or (org-element-property :level ast) 0)) + (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))) + (if (= level 0) + (when properties-end + (add-text-properties contents-begin properties-end + `(read-only t rear-nonsticky t ,org-noter--id-text-property ,id)) + (set-buffer-modified-p modified)) + (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)) + (when properties-end + (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))) + (when end + (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. + +When LOCATION is provded, it is written into the property drawer +of the heading under `org-noter-property-note-location' (default: +NOTER_PAGE)." + (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 (and ast (not (org-noter--no-heading-p))) + (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) + "Conjure the notes-window from the void." + (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--relocate-notes-window (notes-buffer) + "Clear the notes-window and (re)locate it. +Used by interactive note-window location functions." + (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)))) + +(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)) + (window-location (org-noter--session-window-location session)) + notes-window) + + (set-window-buffer doc-window doc-buffer) + + (with-current-buffer notes-buffer + (unless org-noter-disable-narrowing + (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)) + + (when org-noter-swap-window + (cl-labels ((swap-windows (window1 window2) + "Swap the buffers of WINDOW1 and WINDOW2." + (let ((buffer1 (window-buffer window1)) + (buffer2 (window-buffer window2))) + (set-window-buffer window1 buffer2) + (set-window-buffer window2 buffer1) + (select-window window2)))) + (let ((frame (window-frame notes-window))) + (when (and (frame-live-p frame) + (not (eq frame (selected-frame)))) + (select-frame-set-input-focus (window-frame notes-window))) + (when (and (window-live-p notes-window) + (not (eq notes-window doc-window))) + (swap-windows notes-window doc-window)))) + + (if (eq window-location 'horizontal-split) + (enlarge-window (- (ceiling (* (- 1 (car (org-noter--session-doc-split-fraction session))) + (frame-width))) + (window-total-width)) t) + (enlarge-window (- (ceiling (* (- 1 (cdr (org-noter--session-doc-split-fraction session))) + (frame-height))) + (window-total-height))))) + + (if org-noter-swap-window + ;; the variable NOTES-WINDOW here is really + ;; the document window since the two got swapped + (set-window-dedicated-p notes-window t) + ;; It's not swapped so set it normally + (set-window-dedicated-p doc-window t)))))) + +(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 (&optional precise-info force-new-ref) + "Return document location as (page . v) or (page v . h). +If PRECISE-INFO is given, return the location in the same format. +FORCE-NEW-REF is not used by PDF, NOV, or DJVU format files." + (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) + (error "Unknown document type %s" major-mode))))) + +(defun org-noter--location-change-advice (&rest _) + (org-noter--with-valid-session (org-noter--doc-location-change-handler))) + +(defsubst org-noter--doc-file-property (headline) + (let ((doc-prop (or (org-element-property (intern (concat ":" org-noter-property-doc-file)) headline) + (org-entry-get nil org-noter-property-doc-file t)))) + (or (run-hook-with-args-until-success 'org-noter-parse-document-property-hook doc-prop) + doc-prop))) + +(defun org-noter--check-location-property (arg) + (let ((property (if (stringp arg) arg + (or (org-element-property + (intern (concat ":" org-noter-property-note-location)) arg) + (run-hook-with-args-until-success + 'org-noter--get-location-property-hook 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))) + (and (consp value) (integerp (car value)) (numberp (cadr value)) (numberp (cddr value))) + (integerp value))))))) + +(defun org-noter--parse-location-property (arg) + (let ((property (if (stringp arg) arg + (or (org-element-property + (intern (concat ":" org-noter-property-note-location)) arg) + (run-hook-with-args-until-success + 'org-noter--get-location-property-hook 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) + ((and (consp value) (integerp (car value)) (consp (cdr value)) (numberp (cadr value)) (numberp (cddr value))) value) + ((integerp value) (cons value 0)))))))) + +(defun org-noter--pretty-print-location (location) + "Original pretty-print for property drawer. +LOCATION contains the page number and, optionally, the vertical +and/or horizontal positions." + (org-noter--with-valid-session + (run-hook-with-args-until-success + 'org-noter--pretty-print-location-hook location))) + +(defun org-noter--pretty-print-location-for-title (location) + "Pretty-print for titles. +Compared to the original functions/hook, this one may present +more human-readable text. LOCATION contains the page number and, +optionally, the vertical and/or horizontal positions." + (org-noter--with-valid-session + (run-hook-with-args-until-success + 'org-noter--pretty-print-location-for-title-hook location))) + +;; TODO: Documentation +(defun org-noter--get-containing-element (&optional include-root) + "Run `org-noter--get-containing-element-hook's until success. + +Runs `org-noter--get-containing-heading', then +`org-noter--get-containing-property-drawer'. This function is +used in `org-noter-sync-current-note', +`org-noter-sync-previous-note', and `org-noter--create-session'. + +When INCLUDE-ROOT is non-nil, the root heading is also eligible +to be returned." + (run-hook-with-args-until-success 'org-noter--get-containing-element-hook include-root)) + +(defun org-noter--get-containing-heading (&optional include-root) + "Return the smallest heading around point with a location property. + +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--get-containing-property-drawer (&optional include-root) + "Return the property drawer of the smallest heading around point with location. + +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-point-at (point-min) + (when (org-before-first-heading-p) + (let ((prop (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)))) + (when (and (org-noter--check-location-property prop) (or include-root (not at-root))) + prop)))))) + +(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-left (float (nth 0 slice))) + (slice-top (float (nth 1 slice))) + (slice-width (float (nth 2 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)))) + (when (or (> slice-width 1) + (> slice-left 1)) + (let ((width (car (image-size (image-mode-window-get 'image) t)))) + (setq slice-width (/ slice-width width) + slice-left (/ slice-left width)))) + (list slice-top slice-height slice-left slice-width))) + +(defun org-noter--conv-page-scroll-percentage (vscroll &optional hscroll) + "Convert VSCROLL, HSCROLL position to percent-base position. +Scroll units are character-based." + (let* ((slice (org-noter--doc-get-page-slice)) + (display-size (image-display-size (image-get-display-property))) ;(width height) + (display-width (car display-size)) + (display-height (cdr display-size)) + (window-geom (window-inside-edges)) ; (L T R B) + (display-left-edge (/ (- (nth 2 window-geom) (nth 0 window-geom) display-width) 2)) + (display-percentage-v (/ vscroll display-height)) + (percentage-v (max 0 (min 1 (+ (nth 0 slice) (* (nth 1 slice) display-percentage-v))))) + (display-percentage-h 0) + (percentage-h 0)) + (when hscroll + (setq display-percentage-h (/ (- hscroll display-left-edge) display-width) + percentage-h (max 0 (min 1 (+ (nth 2 slice) (* (nth 3 slice) display-percentage-h)))))) + (cons percentage-v percentage-h))) + +(defun org-noter--conv-page-percentage-scroll (percentage) + "Convert PERCENTAGE based position to scroll-based position." + (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 (nth 0 slice)) (nth 1 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))) + (with-selected-window window + (run-hook-with-args-until-success 'org-noter--get-precise-info-hook mode window))))) + +(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) + (run-hook-with-args-until-success 'org-noter--show-arrow-hook) + (setq org-noter--arrow-location nil)))) + +(defun org-noter--get-location-top (location) + "Get the top coordinate given a LOCATION. +... when LOCATION has form (page top . left) or (page . top)." + (if (listp (cdr location)) + (cadr location) + (cdr location))) + +(defun org-noter--get-location-page (location) + "Get the page number given a LOCATION of form (page top . left) or (page . top)." + (if (listp location) + (car location) + location)) + +(defun org-noter--get-location-left (location) + "Get the left coordinate given a LOCATION. +... when LOCATION has form (page top . left) or (page . top). If +later form of vector is passed return 0." + (if (listp (cdr location)) + (if (listp (cddr location)) + (caddr location) + (cddr location)) + 0)) + +(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 + (run-hook-with-args-until-success 'org-noter--doc-goto-location-hook mode location window) + (redisplay))))) + +(defun org-noter--compare-location-cons (comp l1 l2) + "Compare L1 and L2, which are location cons. +COMP can be any of the usual comparison operators plus \">f\". +See `org-noter--compare-locations'." + (cl-assert (and (consp l1) (consp l2))) + (cond ((eq comp '=) + (and (= (org-noter--get-location-page l1) (org-noter--get-location-page l2)) + (= (org-noter--get-location-top l1) (org-noter--get-location-top l2)) + (= (org-noter--get-location-left l1) (org-noter--get-location-left l2)))) + ((eq comp '<) + (or (< (org-noter--get-location-page l1) (org-noter--get-location-page l2)) + (and (= (org-noter--get-location-page l1) (org-noter--get-location-page l2)) + (< (org-noter--get-location-top l1) (org-noter--get-location-top l2))) + (and (= (org-noter--get-location-page l1) (org-noter--get-location-page l2)) + (= (org-noter--get-location-top l1) (org-noter--get-location-top l2)) + (< (org-noter--get-location-left l1) (org-noter--get-location-left l2))))) + ((eq comp '<=) + (or (< (org-noter--get-location-page l1) (org-noter--get-location-page l2)) + (and (= (org-noter--get-location-page l1) (org-noter--get-location-page l2)) + (<= (org-noter--get-location-top l1) (org-noter--get-location-top l2))) + (and (= (org-noter--get-location-page l1) (org-noter--get-location-page l2)) + (= (org-noter--get-location-top l1) (org-noter--get-location-top l2)) + (<= (org-noter--get-location-left l1) (org-noter--get-location-left l2))))) + ((eq comp '>) + (or (> (org-noter--get-location-page l1) (org-noter--get-location-page l2)) + (and (= (org-noter--get-location-page l1) (org-noter--get-location-page l2)) + (> (org-noter--get-location-top l1) (org-noter--get-location-top l2))) + (and (= (org-noter--get-location-page l1) (org-noter--get-location-page l2)) + (= (org-noter--get-location-top l1) (org-noter--get-location-top l2)) + (> (org-noter--get-location-left l1) (org-noter--get-location-left l2))))) + ((eq comp '>=) + (or (> (org-noter--get-location-page l1) (org-noter--get-location-page l2)) + (and (= (org-noter--get-location-page l1) (org-noter--get-location-page l2)) + (>= (org-noter--get-location-top l1) (org-noter--get-location-top l2))) + (and (= (org-noter--get-location-page l1) (org-noter--get-location-page l2)) + (= (org-noter--get-location-top l1) (org-noter--get-location-top l2)) + (>= (org-noter--get-location-left l1) (org-noter--get-location-left l2))))) + ((eq comp '>f) + (or (> (org-noter--get-location-page l1) (org-noter--get-location-page l2)) + (and (= (org-noter--get-location-page l1) (org-noter--get-location-page l2)) + (< (org-noter--get-location-top l1) (org-noter--get-location-top l2))) + (and (= (org-noter--get-location-page l1) (org-noter--get-location-page l2)) + (= (org-noter--get-location-top l1) (org-noter--get-location-top l2)) + (< (org-noter--get-location-left l1) (org-noter--get-location-left 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)) + (if (numberp (cdr l2)) + (org-noter--compare-location-cons comp l1 l2) + (org-noter--compare-location-cons comp l1 (cons (car l2) (cadr l2))))))) + +(defun org-noter--show-note-entry (session note) + "Show the NOTE entry and its children for this SESSION. +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))) + (unless (org-before-first-heading-p) + (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) + (or (run-hook-with-args-until-success 'org-noter--get-current-view-hook mode) + (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) + (> (org-noter--get-location-top location) point)) + ((eq (aref view 0) 'nov) + (> (org-noter--get-location-top 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 (org-noter--get-location-page 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 (if (= 0 (org-noter--session-level session)) + (org-element-contents + (org-element-property :parent (org-noter--parse-root))) + (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 org-noter--note-search-element-type + (lambda (element) + (let ((doc-file (org-noter--doc-file-property element)) + (location (org-noter--parse-location-property element))) + (when (and ignore-until-level (<= (org-element-property :level element) 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 element) + (setq ignore-until-level (org-element-property :level element)) + (when (and preamble new-location + (or (not reference-for-insertion) + (>= (org-element-property :begin element) + (org-element-property :end (cdr reference-for-insertion))))) + (setq reference-for-insertion (cons 'after element)))) + + (location + (let ((relative-position (org-noter--relative-position-to-view location view))) + (cond + ((eq relative-position 'inside) + (push (cons element nil) notes-in-view) + + (org-noter--view-region-add current-region-info regions-in-view element) + + (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 element) + (org-element-property :end (car note-cons-to-change))) + (setcdr note-cons-to-change element)))) + + (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 element nil)) + closest-notes-location location + current-region-info nil + closest-notes-regions nil) + (org-noter--view-region-add current-region-info closest-notes-regions element)) + + ((and eligible-for-before (equal location closest-notes-location)) + (push (cons element nil) closest-notes) + (org-noter--view-region-add current-region-info closest-notes-regions element)) + + (t (org-noter--view-region-finish current-region-info element))))))) + + (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 element) + reference-location location)) + + ((and (eq (car reference-for-insertion) 'after) + (< (org-element-property :begin element) + (org-element-property :end (cdr reference-for-insertion))) + (org-noter--compare-locations '>= location new-location)) + (setq reference-for-insertion (cons 'before element) + reference-location location))))) + + (t + (when (and preamble new-location + (or (not reference-for-insertion) + (>= (org-element-property :begin element) + (org-element-property :end (cdr reference-for-insertion))))) + (setq reference-for-insertion (cons 'after element))))))) + 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)))))) + +(defun org-noter--check-if-document-is-annotated-on-file (document-path notes-path) + "Check if NOTES-PATH contains any notes that annotate DOCUMENT-PATH. +NOTES-PATH is a path to a notes files. +DOCUMENT-PATH is a path to a document file." + ;; 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 (string-equal (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 (or (string-match-p org-link-bracket-re doc-prop) + (string-match-p org-noter--url-regexp doc-prop) + (and (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)))) + + (setq doc-prop (or (run-hook-with-args-until-success 'org-noter-parse-document-property-hook doc-prop) + doc-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 + (cond + ((null doc-prop) "No document property found. Please specify a document path: ") + ((file-directory-p doc-prop) + (format "Document property (\"%s\") is a directory. Please specify a document file: " + doc-prop)) + ((not (file-readable-p doc-prop)) + (format "The file specified by the document property \"%s\" is unreadable. Please specify a new document: " + doc-prop))) + 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) + "Return non-nil when there is at least another frame. +This is called in `org-noter-kill-session'. THIS-FRAME can be +specified to override `selected-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))))) + +(defun org-noter--get-highlight-location () + "Return a highlight location. +This is mode specific. In PDF it's a the page number and 4 +coordinates for the highlight. This is delegated to each document +mode." + (with-selected-window (org-noter--get-doc-window) + (run-hook-with-args-until-success 'org-noter--get-highlight-location-hook))) + +(defun org-noter--get-serialized-highlight (highlight-location) + "Return a string representation of the HIGHLIGHT-LOCATION. +This is delegated to each document mode (eg pdf)." + (run-hook-with-args-until-success 'org-noter--pretty-print-highlight-location-hook highlight-location)) + +(defun org-noter--update-doc-rename-in-notes (document-path new-document-path &optional _ok-if-already-exists) + "Update org-noter references to document-file whose name has changed. + +DOCUMENT-PATH is the original filename. +NEW-DOCUMENT-PATH is the new filename. + +Call `org-noter-enable-sync-renames' to enable this feature and +`org-noter-disable-sync-renames' to disable it. + +This advice runs after `dired-rename-file' completes successfully +on files with `file-name-extension' in `org-noter--doc-extensions'. + +For notes files that have the same `file-name-base' as the +document, the notes filename will be changed, but not its +`file-name-directory'. + +If the document is moved to a path above the notes file, a +warning will be issued, but the sync will proceed. The directory +of the notes file will not be changed, as there may be other +documents referenced in the notes file. An `org-noter' session +can still be initiated from the notes file, but not vice-versa, +nor will future renames of the document be synced in the notes +file." + + (when (and (file-name-extension document-path) + (member-ignore-case (file-name-extension document-path) + org-noter--doc-extensions) + (not (file-exists-p document-path)) + (file-exists-p new-document-path)) + ;; continue if the file extension is that of a document + ;; and the rename was successful + (let* ((document-name (file-name-nondirectory document-path)) + (document-base (file-name-base document-name)) + (document-directory (file-name-directory document-path)) + + (search-names (remove nil (append org-noter-default-notes-file-names + (list (concat document-base ".org")) + (list (run-hook-with-args-until-success 'org-noter-find-additional-notes-functions document-path))))) + notes-files ; list of notes files with promising names (Notes.org or .org) + notes-path) ; junk variable when iterating over notes-files + + ;; find promising notes files by name in a few places... + (dolist (name search-names) + ;; check the notes-search-paths + (dolist (path org-noter-notes-search-path) + (setq notes-path (expand-file-name name path)) + (when (file-exists-p notes-path) + (push notes-path notes-files))) + ;; check paths at or above document-directory + (let ((directory (locate-dominating-file document-directory name))) + (when directory + (setq notes-path (expand-file-name name directory)) + (push notes-path notes-files)))) + + (setq notes-files (delete-dups notes-files)) + + ;; in each annotating notes file, find the entry for this file and update + ;; the document's relative path + (dolist (notes-path notes-files) + (when (org-noter--check-if-document-is-annotated-on-file document-path notes-path) + (with-temp-buffer + (insert-file-contents notes-path) + (org-with-point-at (point-min) + (catch 'break ;stop when we find a match + (while (re-search-forward (org-re-property org-noter-property-doc-file) nil) + (let ((property-value (match-string 3)) + (notes-directory (file-name-directory notes-path))) + (when (string-equal (expand-file-name property-value notes-directory) + document-path) + (let ((doc-relative-name (file-relative-name new-document-path notes-directory)) + msg) + ;; sync the new document path in this notes file + (org-set-property org-noter-property-doc-file doc-relative-name) + ;; warn against docs that reside above notes in path + (when (string-prefix-p "../" doc-relative-name) + (setq msg + (format-message "Document file has moved above notes file (%s). `org-noter' will not be able to find the notes file from the new document path (%s)." notes-path doc-relative-name)) + (display-warning 'org-noter msg :warning))) + (write-file notes-path nil) + ;; change the notes filename if it was based on the document filename + (if (string-equal (file-name-base notes-path) document-base) + (let ((new-notes-path (concat (file-name-directory notes-path) + (file-name-base new-document-path) ".org"))) + (rename-file notes-path new-notes-path))) + (throw 'break t)))))))))))) + +(defun org-noter--update-notes-rename-in-notes (notes-path new-notes-path &optional _ok-if-already-exists) + "Update org-noter references to docs when notes file is moved. + +NOTES-PATH is the original filename. +NEW-NOTES-PATH is the new filename. + +Call `org-noter-enable-sync-renames' to enable this feature and +`org-noter-disable-sync-renames' to disable it. + +This advice runs after `dired-rename-file' moves an '.org' file to +a different directory. + +If the notes file is moved to a path below any of its linked +documents, a warning will be issued, but the sync will proceed. +An `org-noter' session can still be initiated from the notes +file, but not vice-versa, but future renames of the notes file +will continue to sync the document references." + + (when (and (string-equal (file-name-extension notes-path) "org") + (not (file-exists-p notes-path)) + (file-exists-p new-notes-path) + (not (string-equal (file-name-directory notes-path) + (file-name-directory new-notes-path)))) + ;; continue if it is an org file + ;; and the rename was successful + ;; and the directory changes + (let* (;;(document-name (file-name-nondirectory document-path)) + ;;(document-base (file-name-base document-name)) + ( notes-directory (file-name-directory notes-path)) + (new-notes-directory (file-name-directory new-notes-path)) + (problem-path-list nil) + (this-org-file-uses-noter nil)) + + ;; update each document's relative path + (with-temp-buffer + (insert-file-contents new-notes-path) + (org-with-point-at (point-min) + (while (re-search-forward (org-re-property org-noter-property-doc-file) nil t) + (let* (( doc-file-rel-path (match-string 3)) + ( doc-file-abs-path (expand-file-name doc-file-rel-path notes-directory)) + (new-doc-file-rel-path (file-relative-name doc-file-abs-path new-notes-directory))) + (setq this-org-file-uses-noter t) + ;; sync the document path to the new notes file + (org-set-property org-noter-property-doc-file new-doc-file-rel-path) + (next-line) + ;; add problematic paths to the list + (when (string-prefix-p "../" new-doc-file-rel-path) + (push new-doc-file-rel-path problem-path-list))))) + ;; warn against docs that reside above notes in path + (when problem-path-list + (let ((msg (format-message + "Notes file has moved below some documents. `org-noter' will not be able to find the notes file from the document path for these files:"))) + (dolist (doc-path problem-path-list) + (setq msg (concat msg (format-message "\n%s" doc-path)))) + (display-warning 'org-noter msg :warning))) + (when this-org-file-uses-noter + (write-file new-notes-path nil)))))) + +;; -------------------------------------------------------------------------------- +;;; 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 + (when (called-interactively-p 'any) '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) + "Toggle saving the last visited location for this document. +With a prefix ARG \\[universal-argument], 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) + "Toggle hiding other headings for the current session. + +- With a prefix ARG \\[universal-argument], set the current setting + permanently for this document. + +- With a prefix ARG \\[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) + "Set the closest note tipping point (see `org-noter-closest-tipping-point'). + +- With a prefix ARG \\[universal-argument], set it permanently for + this document. + +- With a prefix ARG \\[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-toggle-notes-window-location () + "Toggle between side- and bottom-notes window location. +Only acts on the current session." + (interactive) + (org-noter--with-valid-session + (let ((current-notes-location (org-noter--session-window-location session)) + (notes-buffer (org-noter--session-notes-buffer session))) + (cond ((eq current-notes-location 'horizontal-split) + (setf (org-noter--session-window-location session) 'vertical-split)) + ((eq current-notes-location 'vertical-split) + (setf (org-noter--session-window-location session) 'horizontal-split))) + (org-noter--relocate-notes-window notes-buffer)))) + +(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)) + (org-noter--relocate-notes-window notes-buffer) + + (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 ARG \\[universal-argument], set it permanently + for this document. + +- With a prefix ARG \\[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) + (run-hooks 'org-noter--no-sessions-remove-advice-hooks)) + + (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)) + (when org-noter-use-indirect-buffer + (kill-buffer notes-buffer)) + + (when base-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)) + (unless org-noter-kill-frame-at-session-end + (set-window-dedicated-p (get-buffer-window doc-buffer) nil)) + (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 based on the outline of the document." + (interactive) + (org-noter--with-valid-session + (or (run-hook-with-args-until-success 'org-noter-create-skeleton-functions + (org-noter--session-doc-mode session)) + (user-error "This command is not supported for %s" + (org-noter--session-doc-mode session))))) + +(defun org-noter-insert-note (&optional toggle-highlight 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 +either the selected text (if it is <= +`org-noter-max-short-selected-text-length') or +`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. + +The prefix \\[universal-argument] sets TOGGLE-HIGHLIGHT, which +inverts the logic of the custom variable +`org-noter-highlight-selected-text' for this 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. + +Guiding principles for note generation + 1. The preferred title is the one the user enters in the + minibuffer. + 2. Selected text should be used in the note, either as the + title or in the body + 3. Refrain from making notes in the same location with the same + title + 4. Precise notes generally have different locations, so always + make new precise notes" + (interactive "P") + (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 (run-hook-with-args-until-success + 'org-noter-get-selected-text-hook + (org-noter--session-doc-mode session))) + (selected-text-p (> (length selected-text) 0)) + force-new + (location (org-noter--doc-approx-location (or precise-info 'interactive) (gv-ref force-new))) + (current-view (org-noter--get-current-view))) + + (let* ((inhibit-quit t) + (short-selected-text (if (and selected-text-p + (<= (length selected-text) org-noter-max-short-selected-text-length)) + selected-text)) + (org-noter-highlight-selected-text (if toggle-highlight (not org-noter-highlight-selected-text) + org-noter-highlight-selected-text)) + (highlight-location (if org-noter-highlight-selected-text (org-noter--get-highlight-location)))) + + (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 ((view-info (org-noter--get-view-info current-view location)) + (minibuffer-local-completion-map org-noter--completing-read-keymap) + collection title note-body existing-note + (default-title (or short-selected-text + (replace-regexp-in-string (regexp-quote "$p$") + (org-noter--pretty-print-location-for-title location) + org-noter-default-heading-title))) + (empty-lines-number (if org-noter-separate-notes-from-heading 2 1))) + + ;; NOTE(phm): prompt for title unless this is a precise note + (unless precise-info + ;; construct collection for matching existing notes + (dolist (note-cons (org-noter--view-info-notes view-info)) + (let ((display (org-element-property :raw-value (car note-cons)))) + (push (cons display note-cons) collection)))) + + (setq collection (nreverse collection) + ;; prompt for title (unless no-Q's) + title (if org-noter-insert-note-no-questions default-title + (completing-read "Note: " collection nil nil nil nil default-title)) + note-body (if (and selected-text-p + (not (equal title short-selected-text))) + selected-text) + ;; is this an existing note? skip for precise notes + existing-note (unless precise-info (cdr (assoc title collection)))) + + (if existing-note + ;; NOTE(nox): Inserting on an existing note + (let* ((note (car existing-note)) + (insert-before-element (cdr existing-note)) + (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 note-body) + (if short-selected-text + (insert "``" note-body "''") + (insert "#+BEGIN_QUOTE\n" note-body "\n#+END_QUOTE")))) + + ;; NOTE(nox): Inserting a new note + (let ((reference-element-cons (org-noter--view-info-reference-for-insertion view-info)) + level) + (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 (or (org-element-map contents 'section + (lambda (section) (org-element-property :end section)) + nil t org-element-all-elements) + (point-max)))) + + (setq level (or level + (1+ (or (org-element-property :level ast) 0)))) + + ;; NOTE(nox): This is needed to insert in the right place + (unless (org-noter--no-heading-p) (outline-show-entry)) + (org-noter--insert-heading level title empty-lines-number location) + ;; store the highlight in org IF we have a highlight AND can serialize it. + (when-let ((highlight-location) + (serialized-highlight (org-noter--get-serialized-highlight highlight-location))) + (org-set-property "HIGHLIGHT" serialized-highlight)) + (when note-body + (save-excursion + (if short-selected-text + (insert "``" note-body "''") + (insert "#+BEGIN_QUOTE\n" note-body "\n#+END_QUOTE")))) + (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 org-noter-highlight-selected-text ; return to DOC window and highlight text + (select-frame-set-input-focus (org-noter--session-frame session)) + (select-window (get-buffer-window (org-noter--session-doc-buffer session))) + (run-hook-with-args-until-success 'org-noter--add-highlight-hook major-mode highlight-location)))) + (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-highlight) + "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 +default title of the note if the text does not exceed +`org-noter-max-short-selected-text-length'. + +Use prefix [\\universal-argument] to TOGGLE-HIGHLIGHT. + +See `org-noter-insert-note' docstring for more." + (interactive "P") + (org-noter--with-valid-session + (let ((precise-info (org-noter--get-precise-info))) + (org-noter-insert-note toggle-highlight precise-info)))) + +(defun org-noter-insert-note-toggle-no-questions (&optional toggle-highlight) + "Insert note associated with the current location. +This is like `org-noter-insert-note', except it will toggle +`org-noter-insert-note-no-questions'. + +Use prefix [\\universal-argument] to TOGGLE-HIGHLIGHT." + (interactive "P") + (org-noter--with-valid-session + (let ((org-noter-insert-note-no-questions (not org-noter-insert-note-no-questions))) + (org-noter-insert-note toggle-highlight)))) + +(defun org-noter-insert-precise-note-toggle-no-questions (&optional toggle-highlight) + "Insert note associated with the current location. +This is like `org-noter-insert-precise-note', except it will +toggle `org-noter-insert-note-no-questions'. + +Use prefix [\\universal-argument] to TOGGLE-HIGHLIGHT." + (interactive "P") + (org-noter--with-valid-session + (let ((org-noter-insert-note-no-questions (not org-noter-insert-note-no-questions))) + (org-noter-insert-precise-note toggle-highlight)))) + +(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. +This command navigates in relation to the current page or chapter +of the document. 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. +This command navigates in relation to the current page or chapter +of the document. 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 t + (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-element))) + 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-noter--get-or-read-document-property t) + (org-noter--session-property-text session)) + (let ((location (org-noter--parse-location-property (org-noter--get-containing-element)))) + (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))) + +(defun org-noter-enable-update-renames () + "Enable `dired-rename-file' advice for moving docs and notes. +Enables `org-noter--update-doc-rename-in-notes' and +`org-noter--update-notes-rename-in-notes' as advice :after +`dired-rename-file'. + +In dired, this affects the renaming of supported document files +and .org files. + +This feature can be turn off with `org-noter-disable-sync-renames'." + (interactive) + (advice-add 'dired-rename-file :after 'org-noter--update-doc-rename-in-notes) + (advice-add 'dired-rename-file :after 'org-noter--update-notes-rename-in-notes)) + +(defun org-noter-disable-update-renames () + "Disable `dired-rename-file' advice for moving docs and notes. +Run this if you change your mind about using the rename +synchronization features." + (interactive) + (advice-remove 'dired-rename-file 'org-noter--update-doc-rename-in-notes) + (advice-remove 'dired-rename-file 'org-noter--update-notes-rename-in-notes)) + +(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 "C-M-i") . org-noter-insert-precise-note-toggle-no-questions) + (,(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) + (,(kbd "M-T") . org-noter-toggle-notes-window-location)) + + (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) + (,(kbd "M-T") . org-noter-toggle-notes-window-location)) + (if org-noter-doc-mode + (org-noter-doc-mode -1))) + +(provide 'org-noter-core) +;;; org-noter-core.el ends here diff --git a/org-noter-test-utils.el b/org-noter-test-utils.el new file mode 100644 index 0000000..231bc99 --- /dev/null +++ b/org-noter-test-utils.el @@ -0,0 +1,178 @@ +;; we need to load undecover before all the other org-noter modules so that undercover can instrument code to generate test coverage. +(when (require 'undercover nil t) + (setq undercover-force-coverage t) + + (message "Enable test coverage.") + (undercover "*.el" "modules/*.el" + (:exclude "org-noter-test-utils.el") + (:report-format 'lcov) + (:send-report nil))) + + +(require 'log4e) +(add-to-list 'load-path "modules") +(require 'org-noter) +(require 'with-simulated-input) + + +(message "Emacs version: %s" (version)) + + + + + +;; org-noter-test logger = ont +(log4e:deflogger "ont" "ont %t [%l] %m" "%H:%M:%S") +(ont--log-enable-logging) +(ont--log-enable-debugging) +(ont--log-enable-messaging) +(ont--log-set-level 'info) +(ont--log-debug "ont") + + +(defvar mock-contents-simple-notes-file + " +:PROPERTIES: +:ID: FAKE_1 +:END: +#+TITLE: Test book notes (simple) +* solove-nothing-to-hide +:PROPERTIES: +:NOTER_DOCUMENT: /tmp/test.pdf +:END: +") + +(defvar mock-contents-simple-notes-file-with-a-single-note + ":PROPERTIES: +:ID: FAKE_90283 +:END: +#+TITLE: Test book notes + +* solove-nothing-to-hide +:PROPERTIES: +:NOTER_DOCUMENT: pubs/solove-nothing-to-hide.pdf +:END: +** Note from page 1 +:PROPERTIES: +:NOTER_PAGE: 99 +:END: +" +) + + + + +;;;;;;;;;;; +;; helpers +(defun org-noter-core-test-create-session () + "Call this manually with an existing notes buffer to generate a new session" + (org-noter--create-session (org-noter--parse-root) "pubs/solove-nothing-to-hide.pdf" org-noter-test-file)) + + +(defun with-mock-contents (contents lambda) + "Create a real buffer with CONTENTS and then execute the LAMBDA" + (ont--log-debug "\n--------------------------------------------") + + ;; TODO: when an assert fails in buttercup, an exception (??) is thrown, + ;; so temp file isnt being cleaned up. This is the sledgehammer approach. + ;; Needs to be fixed so that it's cleaned up properly. + (when (boundp 'org-noter-test-file) + (progn + (ont--log-debug (format "Removing org-noter-test-file: %s\n" org-noter-test-file)) + (delete-file org-noter-test-file))) + (let* ((tempfile (make-temp-file "Notes" nil ".org" contents))) + (ont--log-debug (format "Creating a tempfile: %s\n" tempfile)) + (setq org-noter-test-file tempfile) + (ont--log-debug "Opening the file..") + (org-mode) + (find-file tempfile) + (org-mode) + (ont--log-debug "Starting the test..") + (ont--log-debug "%s" (buffer-string)) + (funcall lambda) + (ont--log-debug "About to kill buffer..") + (kill-current-buffer) + (ont--log-debug (format "Removing tempfile %s" tempfile)) + (delete-file tempfile) + (ont--log-debug "+++++++++++++++++++++++++++++++++++++++++") + )) + + +;;;;;;;;;;;;;;;;;;;;;;;;;; +;; hooks - org-noter calls these + +(defun org-noter-test-get-selected-text (mode) + "⚠️org-noter-core-test-return-text +org-noter-core-test-return-text +org-noter-core-test-return-text +org-noter-core-test-return-text +org-noter-core-test-return-text +") + + +(defun org-noter-core-test-document-property (&optional param) + org-noter-test-file) + +(defun org-noter-core-test-view-setup-handler (&optional param) + t) + +(defun org-noter-core-test-open-document-functions (&optional doc) + (find-file (org-noter-core-test-document-property))) + +(defun org-noter-core-test-approx-location (major-mode &optional precise-info _force-new-ref) + (cons 99 precise-info)) + +(defun org-noter-core-test-get-current-view (mode) + t) + +;; TODO This doesn't look right +(defun org-noter-core-test-get-precise-info (mode window) + (list 1 2 3 4)) + +(defun org-noter-core-test-pretty-print-location (location) + (format "%s" location)) + +(defun org-noter-core-test-add-highlight (major-mode precise-info) + t) + +(defun org-noter-core-test-get-current-view (mode) + 'org-noter-core-test-view) + +(defun org-noter-core-test-get-highlight-location () + "HARDCODED_HIGHLIGHT_LOCATION") + +(defun org-noter-core-test-pretty-print-location-for-title (location) + "TEST PRETTY PRINT LOCATION") + +(defun create-org-noter-test-session () + + ;; if this is not set; make-session fails and the test crashes with a stack overflow. + (setq org-noter-always-create-frame nil) + + (setq org-noter-highlight-selected-text t) + + ;; setup spies so we can verify that things have been called + (spy-on 'org-noter-test-get-selected-text :and-call-through) + (spy-on 'org-noter-core-test-approx-location :and-call-through) + (spy-on 'org-noter-core-test-get-precise-info :and-call-through) + (spy-on 'org-noter-core-test-add-highlight :and-call-through) + (spy-on 'org-noter-core-test-get-current-view :and-call-through) + + ;; register all the hooks so we can fake a org-noter-test mode + (add-to-list 'org-noter-get-selected-text-hook #'org-noter-test-get-selected-text) + (add-to-list 'org-noter-parse-document-property-hook #'org-noter-core-test-document-property) + (add-to-list 'org-noter-set-up-document-hook #'org-noter-core-test-view-setup-handler) + (add-to-list 'org-noter-open-document-functions #'org-noter-core-test-open-document-functions) + (add-to-list 'org-noter--doc-approx-location-hook #'org-noter-core-test-approx-location) + (add-to-list 'org-noter--get-current-view-hook #'org-noter-core-test-get-current-view) + (add-to-list 'org-noter--get-precise-info-hook #'org-noter-core-test-get-precise-info) + (add-to-list 'org-noter--pretty-print-location-hook #'org-noter-core-test-pretty-print-location) + (add-to-list 'org-noter--pretty-print-location-for-title-hook #'org-noter-core-test-pretty-print-location-for-title) + (add-to-list 'org-noter--add-highlight-hook #'org-noter-core-test-add-highlight) + (add-to-list 'org-noter--get-highlight-location-hook #'org-noter-core-test-get-highlight-location) + ) + + + + +(provide 'org-noter-test-utils) diff --git a/org-noter.el b/org-noter.el index 129d61c..6e15d4c 100644 --- a/org-noter.el +++ b/org-noter.el @@ -1,12 +1,15 @@ ;;; org-noter.el --- A synchronized, Org-mode, document annotator -*- lexical-binding: t; -*- -;; Copyright (C) 2017-2018 Gonçalo Santos +;; Copyright (C) 2017-2019 Gonçalo Santos -;; Author: Gonçalo Santos (aka. weirdNox@GitHub) -;; Homepage: https://github.com/weirdNox/org-noter +;; Author: Gonçalo Santos (github.com/weirdNox) +;; Maintainer Dmitry M +;; Maintainer: Peter Mao +;; Dmitry M +;; Homepage: https://github.com/org-noter/org-noter ;; Keywords: lisp pdf interleave annotate external sync notes documents org-mode ;; Package-Requires: ((emacs "24.4") (cl-lib "0.6") (org "9.0")) -;; Version: 1.4.1 +;; Version: 1.5.0 ;; This file is not part of GNU Emacs. @@ -25,2075 +28,51 @@ ;;; 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. +;; 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) +(require 'org-noter-core) -;; -------------------------------------------------------------------------------- -;; NOTE(nox): User variables -(defgroup org-noter nil - "A synchronized, external annotator" - :group 'convenience - :version "25.3.1") +(declare-function org-entry-put "org") +(declare-function org-with-wide-buffer "org-macs") -(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))) +(add-to-list 'load-path (concat (file-name-directory load-file-name) "modules/")) +(when (or (memq 'doc-view-mode org-noter-supported-modes) + (memq 'pdf-view-mode org-noter-supported-modes)) + (require 'org-noter-pdf)) +(when (memq 'nov-mode org-noter-supported-modes) + (require 'org-noter-nov)) +(when (memq 'djvu-read-mode org-noter-supported-modes) + (require 'org-noter-djvu)) ;;;###autoload (defun org-noter (&optional arg) "Start `org-noter' session. -There are two modes of operation. You may create the session from: +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 ----------------------------------------- +- 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 +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, +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 @@ -2107,14 +86,16 @@ With a prefix number ARG: - 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 --------------------------------------- +- 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. +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 +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 @@ -2123,67 +104,88 @@ notes file, even if it finds one." (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)))) + (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) + (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))) + (session-id (get-text-property (org-element-property :begin ast) org-noter--id-text-property)) + session) + + ;; Check for prefix value + (if (or (numberp arg) (eq arg '-)) + ;; Yes, user's given a prefix value. + (cond ((> (prefix-numeric-value arg) 0) + ;; Is the prefix value greater than 0? + (find-file document-property)) + ;; Open the document like `find-file'. + + ;; Is the prefix value less than 0? + ((< (prefix-numeric-value arg) 0) + ;; Open the folder containing the document. + (find-file (file-name-directory document-property)))) + + ;; No, user didn't give a prefix value + ;; NOTE(nox): Check if it is an existing session + (when session-id + (setq session (cl-loop for session in org-noter--sessions + when (= (org-noter--session-id session) session-id) + return session)))) + + (if session + (let* ((org-noter--session session) + (location (org-noter--parse-location-property + (org-noter--get-containing-element)))) + (org-noter--setup-windows session) + (when location (org-noter--doc-goto-location location)) + (select-frame-set-input-focus (org-noter--session-frame session))) + ;; It's not an existing session, create a new session. (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)) + ;; + ;; eg: M-x org-noter from a pdf document + ((memq major-mode org-noter-supported-modes) + ;; if an org-noter sesseion already exists (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))) + (run-hook-with-args-until-success 'org-noter-create-session-from-document-hook arg buffer-file-name))))) + +(defun org-noter--create-session-from-document-file-default (&optional arg document-file-name) + "Create a new org-noter session from an open document file. +This is the default implementation that is called by +`org-noter-create-session-from-document-hook`. +ARG is the prefix argument passed to `org-noter` +DOCUMENT-FILE-NAME is the document filename." ;; 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 + ;; `document-file-name', and may not have the same results + (let* ((document-file-name (or (run-hook-with-args-until-success 'org-noter-get-buffer-file-name-hook major-mode) + document-file-name)) + (document-path (or document-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) + (document-directory (if document-file-name + (file-name-directory document-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 + ;; be the same as `document-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) + (search-names (remove nil (append org-noter-default-notes-file-names + (list (concat document-base ".org")) + (list (run-hook-with-args-until-success 'org-noter-find-additional-notes-functions document-path))))) + 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))) @@ -2225,8 +227,7 @@ notes file, even if it finds one." (when (file-exists-p file-name) (setq file-name (propertize file-name 'display (concat file-name - (propertize " -- Exists!" - 'face '(foreground-color . "green"))))) + (propertize " -- Exists!" 'face '(:foregorund "green"))))) (push file-name list-of-possible-targets) (throw 'break nil)) @@ -2246,8 +247,7 @@ notes file, even if it finds one." (when (file-exists-p file-name) (setq file-name (propertize file-name 'display (concat file-name - (propertize " -- Exists!" - 'face '(foreground-color . "green")))))) + (propertize " -- Exists!" 'face '(:foreground "green")))))) (push file-name list-of-possible-targets))))) (setq target (completing-read "Where do you want to save it? " list-of-possible-targets @@ -2267,27 +267,46 @@ notes file, even if it finds one." (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-noter-headline-title-decoration + document-base + org-noter-headline-title-decoration) (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) + (when (> (length (delete-dups notes-files-annotating)) 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))))))))))) + (org-with-point-at (point-min) + (catch 'break + (while (re-search-forward (org-re-property org-noter-property-doc-file) nil) + (when (file-equal-p (expand-file-name (match-string 3) + (file-name-directory (car notes-files-annotating))) + document-path) + (if-let ((saved-location (org-entry-get nil org-noter-property-note-location))) + (setq document-location (cons (string-to-number saved-location) 0))) + (let ((org-noter--start-location-override document-location)) + (org-noter arg)) + (throw 'break t)))))))) + +;;;###autoload +(defun org-noter-start-from-dired () + "In Dired, open sessions for marked files or file at point. + +If there are multiple marked files, focus will be on the last +marked file." + (interactive) + (let ((files (or (dired-get-marked-files) + (dired-get-filename)))) + (dolist (filename files) + (find-file filename) + (save-excursion (org-noter)) + (bury-buffer)) + (other-frame 1))) (provide 'org-noter) diff --git a/other/org-noter-citar.el b/other/org-noter-citar.el new file mode 100644 index 0000000..3fc6f2a --- /dev/null +++ b/other/org-noter-citar.el @@ -0,0 +1,87 @@ +;;; org-noter-citar.el --- Module for finding note files from `citar' -*- lexical-binding: t; -*- + +;; Copyright (C) 2021 c1-g + +;; Author: c1-g +;; Keywords: convenience + +;; 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 . + +;;; Code: +(require 'citar) +(require 'org-ref) +(require 'seq) + +;; Regexp stolen from org-roam-bibtex; orb-utils-citekey-re. +(defvar org-noter-citar-cite-key-re + (rx + (or + (seq (group-n 2 (regexp + ;; If Org-ref is available, use its types + ;; default to "cite" + (if (boundp 'org-ref-cite-types) + (regexp-opt + (mapcar + (lambda (el) + ;; Org-ref v3 cite type is a list of strings + ;; Org-ref v2 cite type is a plain string + (or (car-safe el) el)) + org-ref-cite-types)) + "cite"))) + ":" + (or + ;; Org-ref v2 style `cite:links' + (group-n 1 (+ (any "a-zA-Z0-9_:.-"))) + ;; Org-ref v3 style `cite:Some&key' + (seq (*? (not "&")) "&" + (group-n 1 (+ (any "!#-+./:<>-@^-`{-~-" word)))))) + ;; Org-cite [cite/@citations] + (seq "@" (group-n 1 (+ (any "!#-+./:<>-@^-`{-~-" word)))))) + "Universal regexp to match citations in ROAM_REFS. + +Supports Org-ref v2 and v3 and Org-cite.") + +(defun org-noter-citar-find-document-from-refs (cite-key) + "Return a note file associated with CITE-KEY. +When there is more than one note files associated with CITE-KEY, have +user select one of them." + (when (and (stringp cite-key) (string-match org-noter-citar-cite-key-re cite-key)) + (let* ((key (match-string 1 cite-key)) + (entries (citar--ensure-entries (list key))) + (files (citar-file--files-for-multiple-entries + entries + (append citar-library-paths citar-notes-paths) nil)) + (url (list (citar-get-link (car entries)))) + (documents (flatten-list (append (seq-remove #'file-directory-p files) url)))) + (cond ((= (length documents) 1) + (car documents)) + ((> (length documents) 1) + (completing-read (format "Which document from %s?: " key) documents)))))) + +(defun org-noter-citar-find-key-from-this-file (filename) + (let* ((entry-alist (mapcan (lambda (entry) + (when-let ((file (citar-get-value citar-file-variable entry))) + (list (cons file (citar-get-value "=key=" entry))))) + (citar--get-candidates))) + (key (alist-get filename entry-alist nil nil (lambda (s regexp) + (string-match-p regexp s))))) + (when key + (file-name-with-extension key "org")))) + +(add-to-list 'org-noter-parse-document-property-hook #'org-noter-citar-find-document-from-refs) + +(add-to-list 'org-noter-find-additional-notes-functions #'org-noter-citar-find-key-from-this-file) + +(provide 'org-noter-citar) +;;; org-noter-citar.el ends here diff --git a/other/org-noter-dynamic-block.el b/other/org-noter-dynamic-block.el new file mode 100644 index 0000000..a2878f6 --- /dev/null +++ b/other/org-noter-dynamic-block.el @@ -0,0 +1,212 @@ +;;; org-noter-dynamic-block.el --- Use special blocks as notes -*- lexical-binding: t; -*- + +;; Copyright (C) 2021 c1-g + +;; Author: c1-g +;; Keywords: multimedia + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; + +;;; Code: +(require 'org-noter-core) + +(defun org-noter-insert-precise-dynamic-block (&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-dynamic-block (org-noter--get-precise-info))))) + +(defun org-noter-insert-dynamic-block (&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 + (pcase (org-noter--session-doc-mode session) + ('pdf-view-mode + (when (pdf-view-active-region-p) + (mapconcat 'identity (pdf-view-active-region-text) ? ))) + + ((or 'nov-mode 'djvu-read-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 + (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)))))) + + ;; NOTE(nox): Inserting a new note + (let ((reference-element-cons (org-noter--view-info-reference-for-insertion view-info)) + level) + + (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 (or (org-element-map contents 'section + (lambda (section) (org-element-property :end section)) + nil t org-element-all-elements) + (org-element-map ast 'section + (lambda (section) (org-element-property :end section)) + nil t org-element-all-elements)))) + + ;; (setq level (1+ (or (org-element-property :level ast) 0)))) + + ;; NOTE(nox): This is needed to insert in the right place + (unless (org-noter--no-heading-p) (outline-show-entry)) + ;; (org-noter--insert-heading level title empty-lines-number location) + (insert + "\n" + (string-join (list (format "#+BEGIN: note %s" + (if location + (concat ":" org-noter-property-note-location + (format " %S" location)) + "")) + (or selected-text "") + "#+END:") + "\n") + "\n") + + (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-dblock-write:note (params) + (let ((location (plist-get params + (intern (concat ":" org-noter-property-note-location)))) + (content (plist-get params :content)) + (session org-noter--session) + (origin-window (selected-window)) + (origin-location)) + + (org-noter--with-valid-session + (setq origin-location (org-noter--doc-approx-location)) + (when (and location + (org-noter--get-location-top location) + (org-noter--get-location-left location)) + (org-noter--doc-goto-location location) + (with-current-buffer (org-noter--session-doc-buffer session) + (setq content + (pcase major-mode + ('pdf-view-mode (pdf-info-gettext (car location) (cdr location))) + ((or 'nov-mode 'djvu-read-mode) + (buffer-substring (org-noter--get-location-top location) + (org-noter--get-location-left location)))))) + (org-noter--doc-goto-location origin-location) + (select-window origin-window))) + (insert content))) + +(defun org-noter--get-location-dynamic-block (dblock) + (let ((params (read (concat "(" (org-element-property :arguments dblock) ")")))) + (format "%S" (plist-get params (intern (concat ":" org-noter-property-note-location)))))) + +(defun org-noter-get-containing-dynamic-block (&optional _include-root) + (org-noter--with-valid-session + (org-with-wide-buffer + (let ((elt (org-element-at-point))) + (catch 'break + (while (org-element-property :parent elt) + (cond + ((eq (org-element-type elt) 'dynamic-block) + (throw 'break elt)) + (t + (setq elt (org-element-property :parent elt)))))))))) + +(add-hook 'org-noter--get-containing-element-hook #'org-noter-get-containing-dynamic-block) + +(add-hook 'org-noter--get-location-property-hook #'org-noter--get-location-dynamic-block) + +(provide 'org-noter-dynamic-block) +;;; org-noter-dynamic-block.el ends here + diff --git a/other/org-noter-integration.el b/other/org-noter-integration.el index 6cee0cd..496148c 100644 --- a/other/org-noter-integration.el +++ b/other/org-noter-integration.el @@ -1,67 +1,110 @@ -(require 'org-noter) +;;; org-noter-pdftools.el --- Integration between org-pdftools and org-noter +;; Copyright (C) 2020 Alexander Fu Xi + +;; Author: Alexander Fu Xi +;; Maintainer: Alexander Fu Xi +;; Homepage: https://github.com/fuxialexander/org-pdftools +;; Version: 1.0 +;; Keywords: convenience +;; Package-Requires: ((emacs "26.1") (org "9.4") (pdf-tools "0.8") (org-pdftools "1.0") (org-noter "1.4.1")) + +;; 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: +;; Add integration between org-pdftools and org-noter. + + +;;; Code: +(require 'org-id) (require 'org-pdftools) +(require 'org-noter) +(require 'image-mode) -(declare-function pdf-info-editannots "ext:pdf-info") +(declare-function pdf-info-editannot "ext:pdf-info") (declare-function pdf-annot-add-text-annotation "ext:pdf-annot") (declare-function pdf-annot-get-id "ext:pdf-annot") -(defcustom org-noter-markup-pointer-function 'pdf-annot-add-highlight-markup-annotation +(defcustom org-noter-pdftools-markup-pointer-function 'pdf-annot-add-highlight-markup-annotation "Color for markup pointer annotations. Can be one of highlight/underline/strikeout/squiggly." :group 'org-noter :type 'function) -(defcustom org-noter-markup-pointer-color "#A9A9A9" - "Color for markup pointer annotations" +(defcustom org-noter-pdftools-path-generator #'abbreviate-file-name + "Translate your PDF file path the way you like. Take buffer-file-name as the argument." + :group 'org-pdftools + :type 'function) + +(defcustom org-noter-pdftools-markup-pointer-color "#A9A9A9" + "Color for markup pointer annotations." :group 'org-noter :type 'string) -(defcustom org-noter-markup-pointer-opacity 1.0 - "Color for markup pointer annotations" +(defcustom org-noter-pdftools-markup-pointer-opacity 1.0 + "Color for markup pointer annotations." :group 'org-noter :type 'float) -(defcustom org-noter-free-pointer-icon "Circle" +(defcustom org-noter-pdftools-free-pointer-icon "Circle" "Color for free pointer annotations. Refer to `pdf-annot-standard-text-icons`." :group 'org-noter :type 'string) -(defcustom org-noter-free-pointer-color "#FFFFFF" - "Color for free pointer annotations" +(defcustom org-noter-pdftools-free-pointer-color "#FFFFFF" + "Color for free pointer annotations." :group 'org-noter :type 'string) -(defcustom org-noter-free-pointer-opacity 1.0 - "Color for free pointer annotations" +(defcustom org-noter-pdftools-free-pointer-opacity 1.0 + "Color for free pointer annotations." :group 'org-noter :type 'float) -(defcustom org-noter-use-pdftools-link-location t +(defcustom org-noter-pdftools-use-pdftools-link-location t "When non-nil, org-pdftools link is used instead of location-cons when inserting notes." :group 'org-noter :type 'boolean) -(defcustom org-noter-use-org-id t +(defcustom org-noter-pdftools-use-org-id t "When non-nil, an org-id is generated for each heading for linking with PDF annotations and record entry parents." :group 'org-noter :type 'boolean) -(defcustom org-noter-export-to-pdf t - "When non-nil, PDF annotation contents will include both org-id of original notes and org-id of its parent. +(defcustom org-noter-pdftools-export-to-pdf t + "TODO: Whether you want to export the org notes to pdf annotation contents. +To use this, `org-noter-pdftools-use-org-id' has to be t." + :group 'org-noter + :type 'boolean) -To use this, `org-noter-use-org-id' has to be t." +(defcustom org-noter-pdftools-export-to-pdf-with-structure t + "TODO: Whether you want to export the org notes to pdf annotation contents. +To use this, `org-noter-pdftools-use-org-id' has to be t." :group 'org-noter :type 'boolean) -(defcustom org-noter-export-to-pdf-with-structure t - "When non-nil, PDF annotation contents will include both org-id of original notes and org-id of its parent. +(defcustom org-noter-pdftools-use-unique-org-id t + "When non-nil, an org-id is generated for each heading for linking with PDF annotations and record entry parents." + :group 'org-noter + :type 'boolean) -To use this, `org-noter-use-org-id' has to be t." +(defcustom org-noter-pdftools-insert-content-heading t + "When non-nil, insert a \"Content\" heading above the content of an annotation (underline, highlight)" :group 'org-noter :type 'boolean) -(defcustom org-noter-use-unique-org-id t - "When non-nil, an org-id is generated for each heading for linking with PDF annotations and record entry parents." +(defcustom org-noter-pdftools-insert-comment-heading t + "When non-nil, insert a \"Content\" heading above the content of an annotation (underline, highlight)" :group 'org-noter :type 'boolean) @@ -69,61 +112,91 @@ To use this, `org-noter-use-org-id' has to be t." path page height annot-id search-string original-property) (defun org-noter-pdftools--location-link-p (location) + "Check whether LOCATION is a org-pdftools link." (and location (stringp location) - (string-prefix-p "pdftools:" location))) - -(defun org-noter--location-cons-to-link (location) + (or + (string-prefix-p + (concat "[[" org-pdftools-link-prefix ":") + location) + (string-prefix-p + (concat org-pdftools-link-prefix ":") + location)))) + +(defun org-noter-pdftools--location-cons-to-link (location) + "Convert LOCATION cons to link." (cond ((consp location) (concat "::" (number-to-string (car location)) "++" - (format "%.2f" (cdr location)))) + (format "%.2f" (cadr location)))) ((integerp location) (concat "::" (number-to-string (car location)))))) -(defun org-noter--location-link-to-cons (location) - "Convert a org-pdftools link to old location cons." +(defun org-noter-pdftools--location-link-to-cons (location) + "Convert a org-pdftools link to old LOCATION cons." (cons (org-noter-pdftools--location-page location) (or (org-noter-pdftools--location-height location) 0.0))) ;; -------------------------------------------------------------------------------- ;; NOTE(nox): Interface (defun org-noter-pdftools--check-link (property) + "Interface for checking PROPERTY link." (org-noter-pdftools--location-link-p property)) (defun org-noter-pdftools--parse-link (property) + "Interface for parse PROPERTY link." (when (org-noter-pdftools--location-link-p property) - (string-match "\\(.*\\)::\\([0-9]*\\)\\(\\+\\+\\)?\\([[0-9]\\.*[0-9]*\\)?\\(;;\\|\\$\\$\\)?\\(.*\\)?" property) - (let ((path (match-string 1 property)) - (page (match-string 2 property)) - (height (match-string 4 property)) - annot-id search-string) - (cond ((string-equal (match-string 5 property) ";;") - (setq annot-id (match-string 6 property))) - ((string-equal (match-string 5 property) "$$") - (setq search-string (replace-regexp-in-string "%20" " " (match-string 6 property))))) - (make-org-noter-pdftools--location - :path path - :page (and page (string-to-number page)) - :height (and height (string-to-number height)) - :annot-id annot-id - :search-string search-string - :original-property property)))) + (setq property (string-trim property "\\[\\[" "\\]\\]")) + (let ((link-regexp (concat "\\(.*\\)::\\([0-9]*\\)\\(\\+\\+\\)?\\([[0-9]\\.*[0-9]*\\)?\\(;;\\|" + (regexp-quote org-pdftools-search-string-separator) + "\\)?\\(.*\\)?"))) + (string-match link-regexp property) + (let ((path (match-string 1 property)) + (page (match-string 2 property)) + (height (match-string 4 property)) + annot-id search-string) + (condition-case nil + (cond ((string-equal (match-string 5 property) ";;") + (setq annot-id (match-string 6 property))) + ((string-equal (match-string 5 property) org-pdftools-search-string-separator) + (setq search-string (replace-regexp-in-string "%20" " " (match-string 6 property))))) + (error nil)) + (make-org-noter-pdftools--location + :path path + :page (and page (string-to-number page)) + :height (and height (string-to-number height)) + :annot-id annot-id + :search-string search-string + :original-property property))))) (defun org-noter-pdftools--pretty-print-location (location) - (and (org-noter-pdftools--location-p location) - (org-noter-pdftools--location-original-property location))) + "Function for print the LOCATION link." + (org-noter--with-valid-session + (if (memq (org-noter--session-doc-mode session) '(doc-view-mode pdf-view-mode)) + (let ((loc (if (org-noter-pdftools--location-p location) + location + (org-noter-pdftools--parse-link location)))) + (concat "[[" + (org-noter-pdftools--location-original-property loc) + "]]")) + nil))) (defun org-noter-pdftools--convert-to-location-cons (location) - (when (org-noter-pdftools--location-p location) - (org-noter--location-link-to-cons location))) - -(defun org-noter-pdftools--doc-goto-location (mode location) + "Function for converting the LOCATION link to cons." + (if (and location (consp location)) + location + (let ((loc (if (org-noter-pdftools--location-p location) + location + (org-noter-pdftools--parse-link location)))) + (org-noter-pdftools--location-link-to-cons loc)))) + +(defun org-noter-pdftools--doc-goto-location (mode location &optional _window) + "Goto LOCATION in the corresponding MODE." (when (and (eq mode 'pdf-view-mode) (org-noter-pdftools--location-p location)) (when (org-noter-pdftools--location-page location) (pdf-view-goto-page (org-noter-pdftools--location-page location))) @@ -139,29 +212,33 @@ To use this, `org-noter-use-org-id' has to be t." t)) (defun org-noter-pdftools--note-after-tipping-point (point location view) + "Call `org-noter--note-after-tipping-point' relative to POINT based on LOCATION and VIEW." (when (org-noter-pdftools--location-p location) - (cons t (org-noter--note-after-tipping-point point (org-noter--location-link-to-cons location) view)))) + (cons t (org-noter--note-after-tipping-point point (org-noter-pdftools--location-link-to-cons location) view)))) (defun org-noter-pdftools--relative-position-to-view (location view) + "Get relative position based on LOCATION and VIEW." (when (org-noter-pdftools--location-p location) - (org-noter--relative-position-to-view (org-noter--location-link-to-cons location) view))) + (org-noter--relative-position-to-view (org-noter-pdftools--location-link-to-cons location) view))) -(defun org-noter-pdftools--get-precise-info (mode) +(defun org-noter-pdftools--get-precise-info (mode &optional _window) + "Get precise info from MODE." (when (eq mode 'pdf-view-mode) - (let ((org-pdftools-free-pointer-icon org-noter-free-pointer-icon) - (org-pdftools-free-pointer-color org-noter-free-pointer-color) - (org-pdftools-free-pointer-opacity org-noter-free-pointer-opacity) - (org-pdftools-markup-pointer-color org-noter-markup-pointer-color) - (org-pdftools-markup-pointer-opacity org-noter-markup-pointer-opacity) - (org-pdftools-markup-pointer-function org-noter-markup-pointer-function)) - (org-noter-pdftools--parse-link (org-pdftools-get-link t))))) + (let ((org-pdftools-free-pointer-icon org-noter-pdftools-free-pointer-icon) + (org-pdftools-free-pointer-color org-noter-pdftools-free-pointer-color) + (org-pdftools-free-pointer-opacity org-noter-pdftools-free-pointer-opacity) + (org-pdftools-markup-pointer-color org-noter-pdftools-markup-pointer-color) + (org-pdftools-markup-pointer-opacity org-noter-pdftools-markup-pointer-opacity) + (org-pdftools-markup-pointer-function org-noter-pdftools-markup-pointer-function)) + (org-noter-pdftools--parse-link (org-pdftools-get-link))))) (defun org-noter-pdftools--doc-approx-location (mode precise-info force-new-ref) + "Get approximate location in MODE buffer based on PRECISE-INFO and FORCE-NEW-REF." (org-noter--with-valid-session (when (eq mode 'pdf-view-mode) (cond ((or (numberp precise-info) (not precise-info)) (org-noter-pdftools--parse-link - (concat "pdftools:" (expand-file-name (org-noter--session-property-text session)) "::" + (concat org-pdftools-link-prefix ":" (expand-file-name (org-noter--session-property-text session)) "::" (number-to-string (image-mode-window-get 'page)) (when precise-info (concat "++" (number-to-string precise-info)))))) ((org-noter-pdftools--location-p precise-info) precise-info) @@ -172,18 +249,22 @@ To use this, `org-noter-use-org-id' has to be t." (t (error "Invalid pdftools precise-info case: %s" precise-info)))))) (defun org-noter-pdftools--insert-heading () - (let ((location-property (org-entry-get nil org-noter-property-note-location))) - (when (string-match ".*;;\\(.*\\)" location-property) - (org-noter--with-valid-session - (let ((id (match-string 1 location-property))) - (if org-noter-use-org-id - (org-entry-put nil "ID" - (if org-noter-use-unique-org-id - (concat - (org-noter--session-property-text session) - "-" - id) - id)))))))) + "Insert heading in the `org-noter' org document." + (let* ((location-property (org-entry-get nil org-noter-property-note-location))) + (when location-property + (if (string-suffix-p "]]" location-property) + (setq location-property (substring location-property 0 -2))) + (when (string-match ".*;;\\(.*\\)" location-property) + (org-noter--with-valid-session + (let ((id (match-string 1 location-property))) + (if org-noter-pdftools-use-org-id + (org-entry-put nil "ID" + (if org-noter-pdftools-use-unique-org-id + (concat + (org-noter--session-property-text session) + "-" + id) + id))))))))) (dolist (pair '((org-noter--check-location-property-hook . org-noter-pdftools--check-link) (org-noter--parse-location-property-hook . org-noter-pdftools--parse-link) @@ -199,7 +280,7 @@ To use this, `org-noter-use-org-id' has to be t." ;; -------------------------------------------------------------------------------- ;; NOTE(nox): User commands -(defun org-noter-convert-old-org-heading () +(defun org-noter-pdftools-convert-old-org-heading () "Covert an old org heading to a new one for compatiblility." (interactive) (org-noter--with-valid-session @@ -208,7 +289,7 @@ To use this, `org-noter-use-org-id' has to be t." 'pdf-view-mode) (let* ((document-property (org-noter--session-property-text session))) - (let* ((location (org-noter--location-property + (let* ((location (org-noter--parse-location-property (org-entry-get nil org-noter-property-note-location))) @@ -217,7 +298,7 @@ To use this, `org-noter-use-org-id' has to be t." (car location) location)) (height (if (consp location) - (cdr location) + (cadr location) 0.0)) (pos `(0 . ,(round (* @@ -242,25 +323,28 @@ To use this, `org-noter-use-org-id' has to be t." nil org-noter-property-note-location (concat - "pdftools:" + "[[" + org-pdftools-link-prefix + ":" path - (org-noter--location-cons-to-link + (org-noter-pdftools--location-cons-to-link location) ";;" - annot-id)) - (when org-noter-use-org-id + annot-id + "]]")) + (when org-noter-pdftools-use-org-id (org-entry-put nil "ID" - (if org-noter-use-unique-org-id + (if org-noter-pdftools-use-unique-org-id (concat document-property "-" annot-id) annot-id))) - (when org-noter-export-to-pdf + (when org-noter-pdftools-export-to-pdf (let* ((content (if (and (> (org-current-level) 2) - org-noter-export-to-pdf-with-structure) + org-noter-pdftools-export-to-pdf-with-structure) (let ((parent-id (save-excursion (org-up-heading-safe) (org-id-get)))) @@ -288,7 +372,7 @@ To use this, `org-noter-use-org-id' has to be t." (error "This command is only supported on PDF Tools"))))) -(defun org-noter-convert-old-notes () +(defun org-noter-pdftools-convert-old-notes () "Convert old notes (location cons based) to new format (link based)." (interactive) (org-noter--with-valid-session @@ -303,45 +387,36 @@ To use this, `org-noter-use-org-id' has to be t." org-noter-property-note-location))) (if (and prop (not (string-prefix-p - "pdftools:" + org-pdftools-link-prefix ":" prop))) (call-interactively - #'org-noter-convert-old-org-heading)))))) + #'org-noter-pdftools-convert-old-org-heading)))))) -(defun org-noter-jump-to-note (a) +(defun org-noter-pdftools-jump-to-note (a) "Jump from a PDF annotation A to the corresponding org heading." (interactive (list (with-selected-window (org-noter--get-doc-window) (pdf-annot-read-annotation "Left click the annotation ")))) - (when (not org-noter-use-org-id) - "You have to enable `org-noter-use-org-id'!") + (unless org-noter-pdftools-use-org-id + "You have to enable `org-noter-pdftools-use-org-id'!") (org-noter--with-valid-session (pdf-annot-show-annotation a t) (let ((id (symbol-name (pdf-annot-get-id a)))) (select-window (org-noter--get-notes-window)) - (condition-case-unless-debug - nil - (progn - (require 'org-id) - (goto-char - (cdr (org-id-find-id-in-file - (if org-noter-use-unique-org-id - (concat - (org-noter--session-property-text - session) - "-" - id) - id) - buffer-file-name)))) - (error nil)) - t))) + (let ((exist-id (org-id-find-id-in-file + (if org-noter-pdftools-use-unique-org-id + (concat (org-noter--session-property-text session) "-" id) + id) + buffer-file-name))) + (if exist-id (goto-char (cdr exist-id)) + nil))))) ;; TODO(nox): Implement interface for skeleton creation -(defun org-noter-create-skeleton () +(defun org-noter-pdftools-create-skeleton () "Create notes skeleton with the PDF outline or annotations. Only available with PDF Tools." (interactive) @@ -367,29 +442,28 @@ Only available with PDF Tools." pdftools-link path) (when (and (eq type 'goto-dest) (> page 0)) - (when org-noter-use-pdftools-link-location - (setq path (file-relative-name - (expand-file-name - (org-noter--session-property-text - session)) - org-pdftools-root-dir)) + (when org-noter-pdftools-use-pdftools-link-location + (setq path + (funcall org-noter-pdftools-path-generator (buffer-file-name))) (if title (setq pdftools-link (concat - "pdftools:" + org-pdftools-link-prefix ":" path "::" (number-to-string page) "++" - (number-to-string top) - "$$" + (if top + (number-to-string top) + "0") + org-pdftools-search-string-separator (replace-regexp-in-string " " "%20" title))) (setq pdftools-link (concat - "pdftools:" + org-pdftools-link-prefix ":" path "::" (number-to-string page) @@ -398,7 +472,7 @@ Only available with PDF Tools." (push (vector title - (if org-noter-use-pdftools-link-location pdftools-link + (if org-noter-pdftools-use-pdftools-link-location pdftools-link (cons page top)) (1+ depth) nil) @@ -439,16 +513,11 @@ Only available with PDF Tools." (top (nth 1 edges)) (item-subject (alist-get 'subject item)) (item-contents (alist-get 'contents item)) - name contents pdftools-link id path) - (when org-noter-use-pdftools-link-location - (setq path - (file-relative-name - (expand-file-name - (org-noter--session-property-text - session)) - org-pdftools-root-dir)) - (setq id (symbol-name (alist-get 'id item))) - (setq pdftools-link (concat "pdftools:" path "::" + (id (symbol-name (alist-get 'id item))) + name contents pdftools-link path) + (when org-noter-pdftools-use-pdftools-link-location + (setq path (funcall org-noter-pdftools-path-generator (buffer-file-name))) + (setq pdftools-link (concat org-pdftools-link-prefix ":" path "::" (number-to-string page) "++" (number-to-string top) ";;" id))) @@ -470,7 +539,7 @@ Only available with PDF Tools." (if (and item-subject item-contents) "\n" "") (or item-contents "")))))) - (push (vector (format "%s on page %d" name page) (if org-noter-use-pdftools-link-location + (push (vector (format "%s on page %d" name page) (if org-noter-pdftools-use-pdftools-link-location pdftools-link (cons page top)) 'inside contents) output-data))))) @@ -486,14 +555,10 @@ Only available with PDF Tools." (top (nth 1 edges)) (target-page (alist-get 'page link)) target heading-text pdftools-link path) - (when org-noter-use-pdftools-link-location + (when org-noter-pdftools-use-pdftools-link-location (setq path - (file-relative-name - (expand-file-name - (org-noter--session-property-text - session)) - org-pdftools-root-dir)) - (setq pdftools-link (concat "pdftools:" path "::" + (funcall org-noter-pdftools-path-generator (buffer-file-name))) + (setq pdftools-link (concat org-pdftools-link-prefix ":" path "::" (number-to-string page) "++" (number-to-string top)))) (unless (and title (> (length title) 0)) (setq title (pdf-info-gettext page edges))) @@ -514,7 +579,7 @@ Only available with PDF Tools." (push (vector heading-text - (if org-noter-use-pdftools-link-location + (if org-noter-pdftools-use-pdftools-link-location pdftools-link (cons page top)) 'inside @@ -558,10 +623,12 @@ Only available with PDF Tools." (org-noter--insert-heading level title nil location) (when (car contents) - (org-noter--insert-heading (1+ level) "Contents") + (when org-noter-pdftools-insert-content-heading + (org-noter--insert-heading (1+ level) "Contents")) (insert (car contents))) (when (cdr contents) - (org-noter--insert-heading (1+ level) "Comment") + (when org-noter-pdftools-insert-comment-heading + (org-noter--insert-heading (1+ level) "Comment")) (insert (cdr contents))))) (setq ast (org-noter--parse-root)) @@ -570,4 +637,71 @@ Only available with PDF Tools." (outline-hide-subtree) (org-show-children 2))))) - (t (error "This command is only supported on PDF Tools."))))) + (t (error "This command is only supported on PDF Tools"))))) + +(defun org-noter-pdftools-embed-org-note-to-pdf () + "Embed a org subtree to its corresponding PDF annotation." + (interactive) + (org-noter--with-valid-session + (unless (equal (selected-window) (org-noter--get-notes-window)) + (error "You should use this command in an org-noter note buffer")) + (let* ((org-id (org-id-get))) + (unless (and (string-match ".*\\(annot-.*-.*\\)" org-id) + org-noter-pdftools-use-org-id + org-noter-pdftools-use-pdftools-link-location) + (error "This can only be run on an org heading with a valid org-pdftools annotation ID. +Please also make sure `org-noter-pdftools-use-org-id' and `org-noter-pdftools-use-pdftools-link-location' are enabled")) + (let* ((annot-id (match-string 1 org-id)) + note) + (setq kr kill-ring) + (org-copy-subtree nil nil nil t) + (setq note (car kill-ring)) + (setq kill-ring kr) + (with-selected-window + (org-noter--get-doc-window) + (let ((annot (pdf-annot-getannot (intern annot-id)))) + (with-current-buffer (pdf-annot-edit-contents-noselect annot) + (insert note) + (pdf-annot-edit-contents-finalize t))) + (save-buffer)))))) +(defun org-noter-pdftools-embed-all-org-note-to-pdf () + (interactive) + (org-noter--with-valid-session + (with-selected-window (org-noter--get-notes-window) + (save-excursion + (org-map-entries #'org-noter-pdftools-embed-org-note-to-pdf "ID={annot-}"))))) +(defun org-noter-pdftools-embed-org-buffer-to-pdf () + "Embed the whole org-noter doc buffer to a PDF annotation." + (interactive) + (org-noter--with-valid-session + (let* ((note (with-selected-window (org-noter--get-notes-window) + (save-excursion + (buffer-substring-no-properties + (point-min) (point-max))))) + annot-id) + (with-selected-window + (org-noter--get-doc-window) + (save-excursion + (pdf-view-goto-page 1) + (setq annot-id + (pdf-annot-get-id + (let ((annot (ignore-errors (pdf-annot-at-position '(0 . 0))))) + (if annot + annot + (funcall-interactively + #'pdf-annot-add-text-annotation + '(0 . 0) + org-pdftools-free-pointer-icon + `((color . ,org-pdftools-free-pointer-color) + (opacity . ,org-pdftools-free-pointer-opacity)))))))) + (with-selected-window + (org-noter--get-doc-window) + (let ((annot (pdf-annot-getannot annot-id))) + (with-current-buffer (pdf-annot-edit-contents-noselect annot) + (insert note) + (pdf-annot-edit-contents-finalize t))) + (save-buffer)))))) + +(provide 'org-noter-pdftools) + +;;; org-noter-pdftools.el ends here diff --git a/other/org-noter-nov-overlay.el b/other/org-noter-nov-overlay.el new file mode 100644 index 0000000..f4cd0eb --- /dev/null +++ b/other/org-noter-nov-overlay.el @@ -0,0 +1,116 @@ +;;; org-noter-nov-overlay.el --- Module to highlight text in nov-mode with notes -*- lexical-binding: t; -*- + +;; Copyright (C) 2021 Charlie Gordon + +;; Author: Charlie Gordon +;; Keywords: multimedia + +;; 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, +;; 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: + +;; Highlight your precise notes in nov with org-noter-nov-overlay.el + +;;; Code: +(require 'org-noter) +(require 'nov) +(require 'seq) + +(defcustom org-noter-nov-overlay-color-property "NOTER_OVERLAY" + "A property that specifies the overlay color for `org-noter-nov-make-ov'.") + +(defcustom org-noter-nov-overlay-default-color "SkyBlue" + "Name of the default background color of the overlay `org-noter-nov-make-ov' makes. + +Should be one of the element in `defined-colors'.") + +(defun org-noter-nov-make-overlays () + (org-noter--with-selected-notes-window + (let* ((page (buffer-local-value 'nov-documents-index (org-noter--session-doc-buffer session))) + (regexp (org-re-property org-noter-property-note-location t nil + (format (rx "(" (* space) "%d" (+ space) + (+ digit) (+ space) "." (+ space) + (+ digit) (* space) ")") + page)))) + (org-with-wide-buffer + (goto-char (point-min)) + (while (re-search-forward regexp nil t) + (when-let ((location (org-entry-get nil org-noter-property-note-location nil t))) + (org-noter-nov-make-overlay-no-question))))))) + +(defun org-noter-nov-make-overlay () + "TODO" + (org-noter--with-selected-notes-window + "No notes window exists" + (when (eq (org-noter--session-doc-mode session) 'nov-mode) + (let* ((location-property (org-entry-get nil org-noter-property-note-location nil t)) + (location-cons (cdr (read location-property))) + (beg (car location-cons)) + (end (cdr location-cons)) + (ov-pair (list (make-overlay beg end (org-noter--session-doc-buffer session)))) + (hl-color (or (org-entry-get nil org-noter-nov-overlay-color-property nil t) + (if org-noter-insert-note-no-questions + org-noter-nov-overlay-default-color + (read-color "Highlight color: ")))) + (hl-color-alt (color-lighten-name hl-color 15)) + (action-functions (list + #'org-noter-nov-overlay-sync-current-note + #'org-noter-nov-overlay-sync-current-page-or-chapter))) + + (save-excursion + (org-back-to-heading t) + (re-search-forward org-heading-regexp nil t) + (push (make-overlay (match-beginning 1) (match-end 1)) ov-pair)) + + (dolist (ov ov-pair) + (overlay-put ov 'button ov) + (overlay-put ov 'category 'default-button) + (overlay-put ov 'face (list :background hl-color + :foreground (readable-foreground-color hl-color))) + + (org-entry-put nil org-noter-nov-overlay-color-property hl-color) + + (overlay-put ov 'mouse-face (list :background hl-color-alt + :foreground (readable-foreground-color hl-color-alt))) + + (overlay-put ov 'action (pop action-functions))))))) + +(defun org-noter-nov-make-overlay-no-question () + "Like `org-noter-nov-make-ov', but doesn't ask user to select the overlay color." + (org-noter--with-valid-session + (let ((org-noter-insert-note-no-questions t)) + (org-noter-nov-make-overlay)))) + +(defun org-noter-nov-overlay-sync-current-page-or-chapter (_overlay) + "A wrapper function for `org-noter-sync-current-page-or-chapter' +used exclusively with overlays made with `org-noter-nov-make-overlay' + +This wrapper ignores the first argument passed to it and just call +`org-noter-sync-current-page-or-chapter'." + + (org-noter-sync-current-page-or-chapter)) + +(defun org-noter-nov-overlay-sync-current-note (_overlay) + "A wrapper function for `org-noter-nov-overlay-sync-current-note' +used exclusively with overlays made with `org-noter-nov-make-overlay' + +This wrapper ignores the first argument passed to it and just call +`org-noter-nov-overlay-sync-current-note'." + (org-noter-sync-current-note)) + +(add-hook 'nov-post-html-render-hook #'org-noter-nov-make-overlays) + +(provide 'org-noter-nov-overlay) +;;; org-noter-nov-ov.el ends here + diff --git a/tests/MobyDick.pdf b/tests/MobyDick.pdf new file mode 100644 index 0000000..7e2aa23 Binary files /dev/null and b/tests/MobyDick.pdf differ diff --git a/tests/Notes.org b/tests/Notes.org new file mode 100644 index 0000000..1726838 --- /dev/null +++ b/tests/Notes.org @@ -0,0 +1,998 @@ +#+COLUMNS: %25ITEM %NOTER_PAGE %NUM_COLUMNS +#+STARTUP: overview + +* BuzzanitiG_EnricoFermi + :PROPERTIES: + :NOTER_DOCUMENT: _BuzzanitiG_EnricoFermi.pdf + :END: +** Chapter 1: The last Galilean + :PROPERTIES: + :NOTER_PAGE: 17 + :END: +*** Fig 1.1 college entrance exam + :PROPERTIES: + :NOTER_PAGE: 20 + :END: + First page of Fermi’s written exam to enter Scuola Normale Superiore + + Second page of Fermi’s written exam to enter Scuola Normale Superiore +** sin + :PROPERTIES: + :NOTER_PAGE: 45 + :END: +** last trip + :PROPERTIES: + :NOTER_PAGE: 59 + :END: +** Fig 2.4: cathode rays to nuclear atom + :PROPERTIES: + :NOTER_PAGE: 76 + :END: +** tx th + :PROPERTIES: + :NOTER_PAGE: 78 + :END: +** Fig 2.9 nuclear protophysics + :PROPERTIES: + :NOTER_PAGE: 91 + :END: +** beta decay + :PROPERTIES: + :NOTER_PAGE: 117 + :END: +** fig 3.3 Fermi on relativity + :PROPERTIES: + :NOTER_PAGE: 128 + :END: +** Fig 3.4 Fermi transitions to quantum mechanics + :PROPERTIES: + :NOTER_PAGE: 141 + :END: +** Chapter 4 + :PROPERTIES: + :NOTER_PAGE: 172 + :END: +** 4.1 Nuclei and particle accelerators + :PROPERTIES: + :NOTER_PAGE: (172 0.7057971014492753 . 0.10417582417582417) + :END: +** 4.3 The “birth song” + :PROPERTIES: + :NOTER_PAGE: (177 0.4181159420289855 . 0.09758241758241758) + :END: +Nowadays we know that cosmic rays are mainly protons (about 85%) + +#+BEGIN_QUOTE +This whole work constitutes, then, very powerful evidence that the sort of creative, or atom- +building processes discussed above, are continually going on all about us, possibly also +even on the earth, and that each such event is broadcast through the heavens in the form of +the appropriate cosmic ray.14 +#+END_QUOTE +** Fig. 4.6 + :PROPERTIES: + :NOTER_PAGE: (185 0.4623188405797102 . 0.09978021978021978) + :END: + Neutron physics +** Letter from Rutherford + :PROPERTIES: + :NOTER_PAGE: (217 0.2710144927536232 . 0.13494505494505496) + :END: +#+BEGIN_QUOTE +Dear Fermi, +I have to thank you for your kindness in sending me an account of your recent experiments +in causing temporary radioactivity in a number of elements by means of neutrons. Your +results are of great interest, and no doubt later we shall be able to obtain more information +as to the actual mechanism of such transformations. It is by no means clear that in all +cases the process is as simple as appears to be the case in the observations of the Joliot. +I congratulate you on your successful escape from the sphere of theoretical physics! You +seem to have struck a good line to start with. You may be interested to hear that Professor +Dirac also is doing some experiments. This seems to be a good augury for the future of +theoretical physics! +Congratulations and best wishes. +Yours sincerely, Rutherford11 +#+END_QUOTE +** Epilogue + :PROPERTIES: + :NOTER_PAGE: (273 0.09202898550724638 . 0.09868131868131867) + :END: +** Chicago days + :PROPERTIES: + :NOTER_PAGE: 311 + :END: +TD Lee's stories about Fermi and Chicago in the late 40s. + +Nevertheless, since Fermi was not scheduled +to give any courses that quarter, I did register for quantum mechanics with Teller, +electromagnetic theory with Zachariasen, and, later, statistical mechanics with both +Mayers. By attending those classes I felt I was betraying the secret that I was not +an exceptional student. However, that feeling was soon dissipated by my observing +that there were many other students in these classes. +** B.6 T. D. Lee. Reminiscence of Chicago days + :PROPERTIES: + :NOTER_PAGE: (311 0.10144927536231885 . 0.10637362637362636) + :END: + square dancing! +** slide rule + :PROPERTIES: + :NOTER_PAGE: 312 + :END: +radiative transfer equations +** Appendix C Background material + :PROPERTIES: + :NOTER_PAGE: 314 + :END: +** C.1 Newtonian mechanics; inertial and gravitational mass + :PROPERTIES: + :NOTER_PAGE: (314 0.3521739130434783 . 0.10747252747252746) + :END: + +** The identity of the inertial and gravitational mass is a remarkable fact + :PROPERTIES: + :NOTER_PAGE: (314 0.7768115942028986 . 0.12725274725274727) + :END: + +** C.2 Curved space: ... Flatland and Spheriland + :PROPERTIES: + :NOTER_PAGE: (315 0.09927536231884058 . 0.10307692307692308) + :END: +C.2 Curved space: the strange worlds of Flatland +and Spheriland + +Rev. Edwin A. Abbott + +They are thinking beings with a rigid social structure; the circles are at the +vertex of the social pyramid; they are the high priests, who control the power. The +aristocracy is formed by the regular polygons, the midd + +** Fig C.2 + :PROPERTIES: + :NOTER_PAGE: (316 0.16014492753623188 . 0.11428571428571428) + :END: +positive curvature + +** Fig. C.3 neg curv + :PROPERTIES: + :NOTER_PAGE: (316 0.39420289855072466 . 0.2813186813186813) + :END: +sum of the interior angles of a triangle is less than 180 +** C.3 ˛ particle scattering + :PROPERTIES: + :NOTER_PAGE: (316 0.7007246376811594 . 0.10307692307692308) + :END: +#+BEGIN_QUOTE +The passage of particles through matter has been the first tool to investigate the +atomic structure. ˛ particles (having twice the charge of the electron, and a mass four +times bigger than the hydrogen atom mass) are emitted by radioactive substances at +a very high speed (about 107 m/s). Due to their high speed they can travel in air for +several centimeters and cross thick layers of several substances, such as gold. Fermi +in his textbook on atomic physics explained w +#+END_QUOTE + +Let us now suppose that a thin beam of \alpha particles o + +** Let us now suppose that a thin beam of ˛ particles o + :PROPERTIES: + :NOTER_PAGE: (316 0.8775362318840579 . 0.13054945054945055) + :END: + +** C.4 Planck’s constant and the birth of the wave-particle duality + :PROPERTIES: + :NOTER_PAGE: (317 0.7130434782608696 . 0.10197802197802197) + :END: + +** C.5 The electron spin and the exclusion principle + :PROPERTIES: + :NOTER_PAGE: (320 0.6094202898550725 . 0.10307692307692308) + :END: +The failure to provide an explanation of the anomalous Zeeman effect was certainly +the main reason why at the beginning of the 20s a fourth quantum number was +introduced. Uhlenbeck and Goudsmit’s idea was that the difficulties lay in some +unknown structural property of the electron. In particular they made the hypothesis +that the electron rotates around its axis, thus having an angular momentum, and +therefore a magnetic momentum. The empirical evidence of the doubling of the +spectral line of the alkaline metals implied that this intrinsic angular momentum of +the electron (spin) only can have two directions in space with respect to a given +direction. An easy calculation shows that the absolute value of the spin must be 1/2 + +* MahajanS_Art of Insight (navigation timing) + :PROPERTIES: + :NOTER_DOCUMENT: MahajanS_Art of Insight.pdf + :END: +** Skeleton + 8th and 9th notes are precise (pg . v) +*** Preface + :PROPERTIES: + :NOTER_PAGE: 15 + :END: + #+begin_src elisp + (let (ii jj + (note-steps 17) + (repeats 10)) + (measure-time + (other-window 1) + (dotimes (jj repeats) + (dotimes (ii note-steps) (org-noter-sync-next-page-or-chapter)) + (dotimes (ii note-steps) (org-noter-sync-prev-page-or-chapter))) + (other-window 1))) + #+end_src + + #+RESULTS: + : 4.525487 + + org-babel is slow on subsequent runs after B9, but direct execution with C-x + C-e does not suffer the same performance degradation. + +**** summary of results + (PM) 56a45e0: 4.45s (17x10) slower 2nd time in org babel, but consistent w/ C-xC-e + (PM) 7d94dc2: 3.279694, 9.246695 (2 note-steps x 1 repeats) + (B9) f74263f: 5.030582, 13.833845 (3x1) + (B5) 49bc6ee: 11.525995 (7 note-steps x 1 repeat) + (A3) a83a2eb: 3.046663 ( 7 note-steps x 24 repeats), nav broken + (GS) 9ead81d: 3.051076 (17 note-steps x 10 repeats) + +**** raw results + (PM) 56a45e0: 4.45s (17x10) slower 2nd time in org babel, but consistent w/ C-xC-e + (PM) cd3c86c: 22.911387 (7 x 1) 2nd + (PM) cd3c86c: 4.101886, (7 x 24) 1st + (PM) 7d94dc2: 3.279694, 9.246695 (2 note-steps x 1 repeats) + (D1) a494169: 9.266794 (2 note-steps x 1 repeats) + (M2) b30cbaa: 11.612586, 32.265847 (7 note-steps x 1 repeat) + + (B6) b9ddcbd: 32.272890 (7 note-steps x 1 repeat), 2nd time + (B6) b9ddcbd: 11.630274 (7 note-steps x 1 repeat), 1st time + + (B7) 5002732: 11.78...., 32.263172 (7 note-steps x 1 repeat) 1st,2nd + (B8) e417890: 11.627328, 32....... (7 note-steps x 1 repeat) 1st,2nd + + (B9) f74263f: 5.030582 , 13.833845 (3x1) + (B1) e8d3fc1: 11.571044 (7 note-steps x 1 repeat) + (B2) 52a14db: 11.607116 (7 note-steps x 1 repeat) + (B4) bec9767: 11.557839 (7 note-steps x 1 repeat) + (B5) 49bc6ee: 11.525995 (7 note-steps x 1 repeat) + (B3) f38f313: 3.083761 ( 7 note-steps x 24 repeats) + (A6) ced2751: 3.049991 ( 7 note-steps x 24 repeats), nav broken + (A3) a83a2eb: 3.046663 ( 7 note-steps x 24 repeats), nav broken + (A2) 3924fd8: 3.013850 (17 note-steps x 10 repeats), nav OK + (C1) 924dc55: 2.951571 (17 note-steps x 10 repeats) + (GS) 9ead81d: 3.051076 (17 note-steps x 10 repeats) + + + #+begin_src elisp + (let (ii jj + (note-steps 2) + (repeats 1)) + (measure-time + (other-window 1) + (dotimes (jj repeats) + (dotimes (ii note-steps) (org-noter-sync-next-note)) + (dotimes (ii note-steps) (org-noter-sync-prev-note))) + (other-window 1))) + #+end_src + + #+RESULTS: + + (PM) 7d94dc2: 9.246695 (2 note-steps x 1 repeats) + (GS) 9ead81d: 3.051076 (17 note-steps x 10 repeats) + +*** Values for Backs of Envelopes + :PROPERTIES: + :NOTER_PAGE: 19 + :COLUMN_EDGES: (0.27281191806331473 0.5633147113594041 0.6675977653631285 1) + :END: +(car (read-from-string (org-entry-get nil "COLUMN_EDGES" t))) + +**** R + :PROPERTIES: + :NOTER_PAGE: (19 0.2954380883417813 . 0.15597765363128493) + :END: +gas const + +**** Notes for page xvii V: 75% H: 16% + :PROPERTIES: + :NOTER_PAGE: (19 0.7509051412020276 . 0.15597765363128493) + :END: +**** "m" of Boltzmann with W + :PROPERTIES: + :NOTER_PAGE: (19 0.37880184331797234 . 0.3981042654028436) + :END: + -0.0075 with W + -0.018 with P + (* 7.5 (/ 211.0 (- 211 134.25))) + org-noter--arrow-location +**** s in sun + :PROPERTIES: + :NOTER_PAGE: (19 0.4554670528602462 . 0.3547486033519553) + :END: +**** M of moon + :PROPERTIES: + :NOTER_PAGE: (19 0.4677769732078204 . 0.4590316573556797) + :END: +**** Moon or Sun + :PROPERTIES: + :NOTER_PAGE: (19 0.4742939898624185 . 0.46834264432029793) + :HIGHLIGHT: #s(pdf-highlight 19 ((0.46834264432029793 0.4742939898624185 0.5735567970204841 0.47863866763215057))) + :END: +**** human basal metabolic rate + :PROPERTIES: + :NOTER_PAGE: (19 0.6748732802317161 . 0.3063314711359404) + :END: + +**** notes for page xvii V: 22% H: 60% + :PROPERTIES: + :NOTER_PAGE: (19 0.22157856625633598 . 0.6029050279329609) + :END: +i +**** Notes for page xvii V: 32% H: 67% + :PROPERTIES: + :NOTER_PAGE: (19 0.32005792903692976 . 0.6699441340782123) + :END: +*** Part I: Organizing Complexity + :PROPERTIES: + :NOTER_PAGE: 21 + :END: +**** 1. Divide and conquer + :PROPERTIES: + :NOTER_PAGE: 23 + :END: +**** 2. Abstraction + :PROPERTIES: + :NOTER_PAGE: 47 + :END: +*** Part II: Discarding Complexity Without Losing Information + :PROPERTIES: + :NOTER_PAGE: 75 + :END: +**** 3. Symmetry and Conservation + :PROPERTIES: + :NOTER_PAGE: 77 + :END: +**** 4. Proportional Reasoning + :PROPERTIES: + :NOTER_PAGE: 123 + :END: +**** 4.1 Population scaling + :PROPERTIES: + :NOTER_PAGE: (123 . 0.6335988414192614) + :END: +**** 4.2 Finding scaling exponents + :PROPERTIES: + :NOTER_PAGE: (125 . 0.12599565532223025) + :END: + +**** 5. Dimensions + :PROPERTIES: + :NOTER_PAGE: 157 + :END: +*** Part III: Discarding Complexity with Loss of Information + :PROPERTIES: + :NOTER_PAGE: 217 + :END: +**** 6. Lumping + :PROPERTIES: + :NOTER_PAGE: 219 + :END: +**** 7. Probabilistic Reasoning + :PROPERTIES: + :NOTER_PAGE: 255 + :END: +**** 8. Easy Cases + :PROPERTIES: + :NOTER_PAGE: 299 + :END: +***** Wave dispersion diagram + :PROPERTIES: + :NOTER_PAGE: (330 0.2831281679942071 . 0.10383612662942271) + :END: + +**** 9. Spring Models + :PROPERTIES: + :NOTER_PAGE: 337 + :END: +*** Bon Voyage: Long-Lasting Learning + :PROPERTIES: + :NOTER_PAGE: 377 + :END: +*** Bibliography + :PROPERTIES: + :NOTER_PAGE: 379 + :END: +*** Index + :PROPERTIES: + :NOTER_PAGE: 383 + :END: +* MobyDick + :PROPERTIES: + :NOTER_DOCUMENT: MobyDick.pdf + :NOTER_PAGE: 171 + :END: +** Skeleton + To time this code, you need the measure-time macro. + #+begin_src elisp + ;; http://lists.gnu.org/archive/html/help-gnu-emacs/2008-06/msg00087.html + (defmacro measure-time (&rest body) + "Measure the time it takes to evaluate BODY." + `(let ((time (current-time))) + ,@body + (message "%.06f" (float-time (time-since time))))) + + (defmacro measure-time-sexp (&rest body) + "Measure the time it takes to evaluate BODY. + Returns the value of BODY, so it can be used to time any elisp + sexp." + `(let* ((time (current-time)) + (retval ,@body)) + (message "%.06f" (float-time (time-since time))) + retval)) + #+end_src +*** Title page + :PROPERTIES: + :NOTER_PAGE: 1 + :END: + + #+begin_src elisp + (let (ii jj + (note-steps 138) + (repeats 1)) + (measure-time + (other-window 1) + (dotimes (jj repeats) + (dotimes (ii note-steps) (org-noter-sync-next-page-or-chapter)) + (dotimes (ii note-steps) (org-noter-sync-prev-page-or-chapter))) + (other-window -1))) + #+end_src + + #+RESULTS: + : 10.116560 + +*** ETYMOLOGY + :PROPERTIES: + :NOTER_PAGE: (11 . 0.123031) + :END: +*** EXTRACTS + :PROPERTIES: + :NOTER_PAGE: (13 . 0.123031) + :END: +*** LOOMINGS + :PROPERTIES: + :NOTER_PAGE: (25 . 0.123031) + :END: +*** THE CARPET-BAG + :PROPERTIES: + :NOTER_PAGE: (31 . 0.123031) + :END: +*** THE SPOUTER-INN + :PROPERTIES: + :NOTER_PAGE: (37 . 0.123031) + :END: +*** THE COUNTERPANE + :PROPERTIES: + :NOTER_PAGE: (53 . 0.123031) + :END: +*** BREAKFAST + :PROPERTIES: + :NOTER_PAGE: (59 . 0.123031) + :END: +*** THE STREET + :PROPERTIES: + :NOTER_PAGE: (63 . 0.123031) + :END: +*** THE CHAPEL + :PROPERTIES: + :NOTER_PAGE: (67 . 0.123031) + :END: +*** THE PULPIT + :PROPERTIES: + :NOTER_PAGE: (71 . 0.123031) + :END: +*** THE SERMON + :PROPERTIES: + :NOTER_PAGE: (75 . 0.123031) + :END: +*** A BOSOM FRIEND + :PROPERTIES: + :NOTER_PAGE: (85 . 0.123031) + :END: +*** NIGHTGOWN + :PROPERTIES: + :NOTER_PAGE: (91 . 0.123031) + :END: +*** BIOGRAPHICAL + :PROPERTIES: + :NOTER_PAGE: (95 . 0.123031) + :END: +*** WHEELBARROW + :PROPERTIES: + :NOTER_PAGE: (99 . 0.123031) + :END: +*** NANTUCKET + :PROPERTIES: + :NOTER_PAGE: (105 . 0.123031) + :END: +*** CHOWDER + :PROPERTIES: + :NOTER_PAGE: (109 . 0.123031) + :END: +*** THE SHIP + :PROPERTIES: + :NOTER_PAGE: (113 . 0.123031) + :END: +*** THE RAMADAN + :PROPERTIES: + :NOTER_PAGE: (129 . 0.123031) + :END: +*** HIS MARK + :PROPERTIES: + :NOTER_PAGE: (137 . 0.123031) + :END: +*** THE PROPHET + :PROPERTIES: + :NOTER_PAGE: (143 . 0.123031) + :END: +*** ALL ASTIR + :PROPERTIES: + :NOTER_PAGE: (147 . 0.123031) + :END: +*** GOING ABOARD + :PROPERTIES: + :NOTER_PAGE: (151 . 0.123031) + :END: +*** MERRY CHRISTMAS + :PROPERTIES: + :NOTER_PAGE: (155 . 0.123031) + :END: +*** THE LEE SHORE + :PROPERTIES: + :NOTER_PAGE: (161 . 0.123031) + :END: +*** THE ADVOCATE + :PROPERTIES: + :NOTER_PAGE: (163 . 0.123031) + :END: +*** POSTSCRIPT + :PROPERTIES: + :NOTER_PAGE: (169 . 0.123031) + :END: +*** KNIGHTS AND SQUIRES + :PROPERTIES: + :NOTER_PAGE: (171 . 0.123031) + :END: +*** KNIGHTS AND SQUIRES + :PROPERTIES: + :NOTER_PAGE: (175 . 0.123031) + :END: +*** AHAB + :PROPERTIES: + :NOTER_PAGE: (181 . 0.123031) + :END: +*** ENTER AHAB; TO HIM, STUBB + :PROPERTIES: + :NOTER_PAGE: (185 . 0.123031) + :END: +*** THE PIPE + :PROPERTIES: + :NOTER_PAGE: (189 . 0.123031) + :END: +*** QUEEN MAB + :PROPERTIES: + :NOTER_PAGE: (191 . 0.123031) + :END: +*** CETOLOGY + :PROPERTIES: + :NOTER_PAGE: (195 . 0.123031) + :END: +*** THE SPECKSNYDER + :PROPERTIES: + :NOTER_PAGE: (209 . 0.123031) + :END: +*** THE CABIN-TABLE + :PROPERTIES: + :NOTER_PAGE: (213 . 0.123031) + :END: +*** THE MAST-HEAD + :PROPERTIES: + :NOTER_PAGE: (219 . 0.123031) + :END: +*** THE QUARTER-DECK + :PROPERTIES: + :NOTER_PAGE: (227 . 0.123031) + :END: +*** SUNSET + :PROPERTIES: + :NOTER_PAGE: (235 . 0.123031) + :END: +*** DUSK + :PROPERTIES: + :NOTER_PAGE: (237 . 0.123031) + :END: +*** FIRST NIGHT-WATCH FORE-TOP + :PROPERTIES: + :NOTER_PAGE: (239 . 0.123031) + :END: +*** MIDNIGHT, FORECASTLE + :PROPERTIES: + :NOTER_PAGE: (241 . 0.123031) + :END: +*** MOBY DICK + :PROPERTIES: + :NOTER_PAGE: (247 . 0.123031) + :END: +*** THE WHITENESS OF THE WHALE + :PROPERTIES: + :NOTER_PAGE: (259 . 0.123031) + :END: +*** HARK! + :PROPERTIES: + :NOTER_PAGE: (269 . 0.123031) + :END: +*** THE CHART + :PROPERTIES: + :NOTER_PAGE: (271 . 0.123031) + :END: +*** THE AFFIDAVIT + :PROPERTIES: + :NOTER_PAGE: (277 . 0.123031) + :END: +*** SURMISES + :PROPERTIES: + :NOTER_PAGE: (287 . 0.123031) + :END: +*** THE MAT-MAKER + :PROPERTIES: + :NOTER_PAGE: (291 . 0.123031) + :END: +*** THE FIRST LOWERING + :PROPERTIES: + :NOTER_PAGE: (295 . 0.123031) + :END: +*** THE HYENA + :PROPERTIES: + :NOTER_PAGE: (307 . 0.123031) + :END: +*** AHAB’S BOAT AND CREW. FEDALLAH + :PROPERTIES: + :NOTER_PAGE: (311 . 0.123031) + :END: +*** THE SPIRIT-SPOUT + :PROPERTIES: + :NOTER_PAGE: (315 . 0.123031) + :END: +*** THE ALBATROSS + :PROPERTIES: + :NOTER_PAGE: (321 . 0.123031) + :END: +*** THE GAM + :PROPERTIES: + :NOTER_PAGE: (325 . 0.123031) + :END: +*** THE TOWN-HO’S STORY + :PROPERTIES: + :NOTER_PAGE: (331 . 0.123031) + :END: +*** OF THE MONSTROUS PICTURES OF WHALES + :PROPERTIES: + :NOTER_PAGE: (353 . 0.123031) + :END: +*** OF THE LESS ERRONEOUS PICTURES OF WHALES, AND THE TRUE PICTURES OF WHALING SCENES + :PROPERTIES: + :NOTER_PAGE: (359 . 0.123031) + :END: +*** OF WHALES IN PAINT; IN TEETH; IN WOOD; IN SHEET-IRON; IN STONE; IN MOUNTAINS; IN STARS + :PROPERTIES: + :NOTER_PAGE: (365 . 0.123031) + :END: +*** BRIT + :PROPERTIES: + :NOTER_PAGE: (369 . 0.123031) + :END: +*** SQUID + :PROPERTIES: + :NOTER_PAGE: (373 . 0.123031) + :END: +*** THE LINE + :PROPERTIES: + :NOTER_PAGE: (377 . 0.123031) + :END: +*** STUBB KILLS A WHALE + :PROPERTIES: + :NOTER_PAGE: (381 . 0.123031) + :END: +*** THE DART + :PROPERTIES: + :NOTER_PAGE: (387 . 0.123031) + :END: +*** THE CROTCH + :PROPERTIES: + :NOTER_PAGE: (389 . 0.123031) + :END: +*** STUBB’S SUPPER + :PROPERTIES: + :NOTER_PAGE: (391 . 0.123031) + :END: +*** THE WHALE AS A DISH + :PROPERTIES: + :NOTER_PAGE: (401 . 0.123031) + :END: +*** THE SHARK MASSACRE + :PROPERTIES: + :NOTER_PAGE: (405 . 0.123031) + :END: +*** CUTTING IN + :PROPERTIES: + :NOTER_PAGE: (409 . 0.123031) + :END: +*** THE BLANKET + :PROPERTIES: + :NOTER_PAGE: (413 . 0.123031) + :END: +*** THE FUNERAL + :PROPERTIES: + :NOTER_PAGE: (417 . 0.123031) + :END: +*** THE SPHYNX + :PROPERTIES: + :NOTER_PAGE: (419 . 0.123031) + :END: +*** THE JEROBOAM’S STORY + :PROPERTIES: + :NOTER_PAGE: (423 . 0.123031) + :END: +*** THE MONKEY-ROPE + :PROPERTIES: + :NOTER_PAGE: (431 . 0.123031) + :END: +*** STUBB AND FLASK KILL A RIGHT WHALE; AND THEN HAVE A TALK OVER HIM + :PROPERTIES: + :NOTER_PAGE: (437 . 0.123031) + :END: +*** THE SPERM WHALE’S HEAD – CONTRASTED VIEW + :PROPERTIES: + :NOTER_PAGE: (443 . 0.123031) + :END: +*** THE RIGHT WHALE’S HEAD – CONTRASTED VIEW + :PROPERTIES: + :NOTER_PAGE: (449 . 0.123031) + :END: +*** THE BATTERING-RAM + :PROPERTIES: + :NOTER_PAGE: (453 . 0.123031) + :END: +*** THE GREAT HEIDELBURGH TUN + :PROPERTIES: + :NOTER_PAGE: (457 . 0.123031) + :END: +*** CISTERN AND BUCKETS + :PROPERTIES: + :NOTER_PAGE: (461 . 0.123031) + :END: +*** THE PRAIRE + :PROPERTIES: + :NOTER_PAGE: (467 . 0.123031) + :END: +*** THE NUT + :PROPERTIES: + :NOTER_PAGE: (471 . 0.123031) + :END: +*** THE PEQUOD MEETS THE VIRGIN + :PROPERTIES: + :NOTER_PAGE: (475 . 0.123031) + :END: +*** THE HONOR AND GLORY OF WHALING + :PROPERTIES: + :NOTER_PAGE: (487 . 0.123031) + :END: +*** JONAH HISTORICALLY REGARDED + :PROPERTIES: + :NOTER_PAGE: (491 . 0.123031) + :END: +*** PITCHPOLING + :PROPERTIES: + :NOTER_PAGE: (495 . 0.123031) + :END: +*** THE FOUNTAIN + :PROPERTIES: + :NOTER_PAGE: (499 . 0.123031) + :END: +*** THE TAIL + :PROPERTIES: + :NOTER_PAGE: (505 . 0.123031) + :END: +*** THE GRAND ARMADA + :PROPERTIES: + :NOTER_PAGE: (511 . 0.123031) + :END: +*** SCHOOLS AND SCHOOLMASTERS + :PROPERTIES: + :NOTER_PAGE: (525 . 0.123031) + :END: +*** FAST-FISH AND LOOSE-FISH + :PROPERTIES: + :NOTER_PAGE: (529 . 0.123031) + :END: +*** HEADS OR TAILS + :PROPERTIES: + :NOTER_PAGE: (535 . 0.123031) + :END: +*** THE PEQUOD MEETS THE ROSE-BUD + :PROPERTIES: + :NOTER_PAGE: (539 . 0.123031) + :END: +*** AMBERGRIS + :PROPERTIES: + :NOTER_PAGE: (547 . 0.123031) + :END: +*** THE CASTAWAY + :PROPERTIES: + :NOTER_PAGE: (551 . 0.123031) + :END: +*** A SQUEEZE OF THE HAND + :PROPERTIES: + :NOTER_PAGE: (557 . 0.123031) + :END: +*** THE CASSOCK + :PROPERTIES: + :NOTER_PAGE: (561 . 0.123031) + :END: +*** THE TRY-WORKS + :PROPERTIES: + :NOTER_PAGE: (563 . 0.123031) + :END: +*** THE LAMP + :PROPERTIES: + :NOTER_PAGE: (569 . 0.123031) + :END: +*** STOWING DOWN AND CLEARING UP + :PROPERTIES: + :NOTER_PAGE: (571 . 0.123031) + :END: +*** THE DOUBLOON + :PROPERTIES: + :NOTER_PAGE: (575 . 0.123031) + :END: +*** LEG AND ARM. THE PEQUOD, OF NANTUCKET, MEETS THE SAMUEL ENDERBY, OF LONDON + :PROPERTIES: + :NOTER_PAGE: (583 . 0.123031) + :END: +*** THE DECANTER + :PROPERTIES: + :NOTER_PAGE: (591 . 0.123031) + :END: +*** A BOWER IN THE ARSACIDES + :PROPERTIES: + :NOTER_PAGE: (597 . 0.123031) + :END: +*** MEASUREMENT OF THE WHALE’S SKELETON + :PROPERTIES: + :NOTER_PAGE: (603 . 0.123031) + :END: +*** THE FOSSIL WHALE + :PROPERTIES: + :NOTER_PAGE: (607 . 0.123031) + :END: +*** DOES THE WHALE’S MAGNITUDE DIMINISH? – WILL HE PERISH? + :PROPERTIES: + :NOTER_PAGE: (613 . 0.123031) + :END: +*** AHAB’S LEG + :PROPERTIES: + :NOTER_PAGE: (619 . 0.123031) + :END: +*** THE CARPENTER + :PROPERTIES: + :NOTER_PAGE: (623 . 0.123031) + :END: +*** AHAB AND THE CARPENTER + :PROPERTIES: + :NOTER_PAGE: (627 . 0.123031) + :END: +*** AHAB AND STARBUCK IN THE CABIN + :PROPERTIES: + :NOTER_PAGE: (633 . 0.123031) + :END: +*** QUEEQUEG IN HIS COFFIN + :PROPERTIES: + :NOTER_PAGE: (637 . 0.123031) + :END: +*** THE PACIFIC + :PROPERTIES: + :NOTER_PAGE: (645 . 0.123031) + :END: +*** THE BLACKSMITH + :PROPERTIES: + :NOTER_PAGE: (647 . 0.123031) + :END: +*** THE FORGE + :PROPERTIES: + :NOTER_PAGE: (651 . 0.123031) + :END: +*** THE GILDER + :PROPERTIES: + :NOTER_PAGE: (655 . 0.123031) + :END: +*** THE PEQUOD MEETS THE BACHELOR + :PROPERTIES: + :NOTER_PAGE: (659 . 0.123031) + :END: +*** THE DYING WHALE + :PROPERTIES: + :NOTER_PAGE: (663 . 0.123031) + :END: +*** THE WHALE WATCH + :PROPERTIES: + :NOTER_PAGE: (665 . 0.123031) + :END: +*** THE QUADRANT + :PROPERTIES: + :NOTER_PAGE: (667 . 0.123031) + :END: +*** THE CANDLES + :PROPERTIES: + :NOTER_PAGE: (671 . 0.123031) + :END: +*** THE DECK TOWARDS THE END OF THE FIRST NIGHT WATCH + :PROPERTIES: + :NOTER_PAGE: (679 . 0.123031) + :END: +*** MIDNIGHT – THE FORECASTLE BULWARKS + :PROPERTIES: + :NOTER_PAGE: (681 . 0.123031) + :END: +*** MIDNIGHT ALOFT – THUNDER AND LIGHTNING + :PROPERTIES: + :NOTER_PAGE: (685 . 0.123031) + :END: +*** THE MUSKET + :PROPERTIES: + :NOTER_PAGE: (687 . 0.123031) + :END: +*** THE NEEDLE + :PROPERTIES: + :NOTER_PAGE: (691 . 0.123031) + :END: +*** THE LOG AND LINE + :PROPERTIES: + :NOTER_PAGE: (695 . 0.123031) + :END: +*** THE LIFE-BUOY + :PROPERTIES: + :NOTER_PAGE: (699 . 0.123031) + :END: +*** THE DECK + :PROPERTIES: + :NOTER_PAGE: (703 . 0.123031) + :END: +*** THE PEQUOD MEETS THE RACHEL + :PROPERTIES: + :NOTER_PAGE: (707 . 0.123031) + :END: +*** THE CABIN + :PROPERTIES: + :NOTER_PAGE: (713 . 0.123031) + :END: +*** THE HAT + :PROPERTIES: + :NOTER_PAGE: (715 . 0.123031) + :END: +*** THE PEQUOD MEETS THE DELIGHT + :PROPERTIES: + :NOTER_PAGE: (721 . 0.123031) + :END: +*** THE SYMPHONY + :PROPERTIES: + :NOTER_PAGE: (723 . 0.123031) + :END: +*** THE CHASE – FIRST DAY + :PROPERTIES: + :NOTER_PAGE: (729 . 0.123031) + :END: +*** THE CHASE – SECOND DAY + :PROPERTIES: + :NOTER_PAGE: (739 . 0.123031) + :END: +*** THE CHASE – THIRD DAY + :PROPERTIES: + :NOTER_PAGE: (749 . 0.123031) + :END: +*** EPILOGUE. + :PROPERTIES: + :NOTER_PAGE: (761 . 0.123031) + :END: diff --git a/tests/org-noter-core-tests.el b/tests/org-noter-core-tests.el new file mode 100644 index 0000000..147c53a --- /dev/null +++ b/tests/org-noter-core-tests.el @@ -0,0 +1,217 @@ +(add-to-list 'load-path "modules") +(require 'with-simulated-input) +(require 'org-noter-test-utils) + + +(describe "org-noter-core" + (before-each + (create-org-noter-test-session) + ) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (describe "note taking functionality" + ;; checking to make sure that `with-mock-contents` works fine. + (it "can parse a note file ast that is not empty" + (with-mock-contents + mock-contents-simple-notes-file + '(lambda () (let ((mock-ast (org-noter--parse-root))) + (expect mock-ast :not :to-be nil))))) + + ;; basic note should insert a default heading + (it "can take a basic note" + (with-mock-contents + mock-contents-simple-notes-file + '(lambda () + (org-noter-core-test-create-session) + (let ((org-noter-insert-note-no-questions t)) + (org-noter-insert-note nil "NEW NOTE")) + (expect 'org-noter-test-get-selected-text :to-have-been-called) + (expect (string-match "Notes for page" (buffer-string)) :not :to-be nil)))) + + ;; enter a heading when taking a precise note; expect the heading to be there. + (it "can take a precise note" + (with-mock-contents + mock-contents-simple-notes-file + '(lambda () + (org-noter-core-test-create-session) + (with-simulated-input "precise SPC note RET" + (org-noter-insert-precise-note)) + (expect (string-match "precise note" (buffer-string)) :not :to-be nil)))) + + ;; there should be precise data in the note properties when entering a precise note + (it "precise note has precise data" + (with-mock-contents + mock-contents-simple-notes-file + '(lambda () + (org-noter-core-test-create-session) + (with-simulated-input "precise SPC note RET" + (org-noter-insert-precise-note)) + (expect (string-match "NOTER_PAGE:" (buffer-string)) :not :to-be nil) + (expect (string-match "BEGIN_QUOTE" (buffer-string)) :not :to-be nil) + (expect 'org-noter-core-test-get-precise-info :to-have-been-called) + ))) + + ;; highlight code should be called when a precise note is entered + (it "precise note calls the highlight hook" + (with-mock-contents + mock-contents-simple-notes-file + '(lambda () + (org-noter-core-test-create-session) + (with-simulated-input "precise SPC note RET" + (org-noter-insert-precise-note)) + (expect 'org-noter-core-test-add-highlight :to-have-been-called)))) + + ;; hit C-g when entering a note; expect no highlight + (it "precise note DOES NOT call the highlight hook when the note is aborted" + (with-mock-contents + mock-contents-simple-notes-file + '(lambda () + (org-noter-core-test-create-session) + ;; this is how you trap a C-g + (condition-case nil + (with-simulated-input "C-g" (org-noter-insert-precise-note)) + (quit nil)) + (expect 'org-noter-core-test-add-highlight :not :to-have-been-called)))) + + ) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (describe "session creation" + ;; check that the narrowed buffer is named correctly + (it "narrowed buffer is named correctly" + (with-mock-contents + mock-contents-simple-notes-file-with-a-single-note + '(lambda () + (org-noter-core-test-create-session) + (let* ((session org-noter--session)) + (expect (buffer-name (org-noter--session-notes-buffer session)) :to-equal "Notes of solove-nothing-to-hide") + )))) + + ;; check that session properties are set correctly + (it "session properties are set correctly" + (with-mock-contents + mock-contents-simple-notes-file-with-a-single-note + '(lambda () + (org-noter-core-test-create-session) + (let* ((session org-noter--session)) + (expect (org-noter--session-property-text session) :to-equal "pubs/solove-nothing-to-hide.pdf") + (expect (org-noter--session-display-name session) :to-equal "solove-nothing-to-hide") + (expect (org-noter--session-notes-file-path session) :to-equal org-noter-test-file) + (expect (buffer-file-name (org-noter--session-notes-buffer session)) :to-equal org-noter-test-file) + ;; TODO: Need test-specific-major mode somehow? + ;; (expect (org-noter--session-doc-mode session) :to-equal 'org-core-test) + )))) + + ) + + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (describe "view-info" + (it "can get view info" + (with-mock-contents + mock-contents-simple-notes-file-with-a-single-note + '(lambda () + (org-noter-core-test-create-session) + (let* ((view-info (org-noter--get-view-info (org-noter--get-current-view)))) + (expect 'org-noter-core-test-get-current-view :to-have-been-called) + )))) + ) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (describe "locations" + (defvar test-precise-location '(3 1 . 0.1)) + (defvar test-simple-location '(3 1)) + (defvar test-extra-precise-location '(4 1 0.1 0.2 0.3)) + + (describe "precise locations" + (it "can get page from a precise location" + (expect (org-noter--get-location-page test-precise-location) :to-equal 3)) + + (it "can get top from a precise location" + (expect (org-noter--get-location-top test-precise-location) :to-equal 1)) + + (it "can get left from a precise location" + (expect (org-noter--get-location-left test-precise-location) :to-equal 0.1)) + ) + + (describe "simple locations" + + (it "doesn't get a left location for simple location" + (expect (org-noter--get-location-left test-simple-location) :to-equal nil) + ) + + (it "can get top from a simple location" + (expect (org-noter--get-location-top test-simple-location) :to-equal 1)) + + (it "can get page from a simple location" + (expect (org-noter--get-location-page test-simple-location) :to-equal 3)) + ) + + (describe "extra precise locations" + (it "can get page from an extra precise location" + (expect (org-noter--get-location-page test-extra-precise-location) :to-equal 4)) + + (it "can get top from an extra precise location" + (expect (org-noter--get-location-top test-extra-precise-location) :to-equal 1)) + + + (it "can get left from an extra precise location" + (expect (org-noter--get-location-left test-extra-precise-location) :to-equal 0.1))) + ) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (describe "persistent highlights" + (describe "no hooks are setup for precise note highlights" + ;; if no hooks for highlights are setup we expect no :HIGHLIGHT: property + (before-each + (setq org-noter--get-highlight-location-hook '()) + ) + (it "can take a precise note without a highlight appearing" + (with-mock-contents + mock-contents-simple-notes-file + '(lambda () + (org-noter-core-test-create-session) + (with-simulated-input "precise SPC note RET" + (org-noter-insert-precise-note)) + (expect (string-match ":HIGHLIGHT:" (buffer-string)) :to-be nil))))) + + + (describe "hooks for persistent highlights are setup" + ;; setup hooks for highlighting + (before-each + (add-to-list 'org-noter--get-highlight-location-hook #'org-noter-core-test-get-highlight-location) + (spy-on 'org-noter-core-test-get-highlight-location :and-call-through) + ) + ;; now that the hooks for highlights are setup, we expect :HIGHLIGHT: property to appear. + (it "can take a precise note WITH a highlight appearing" + (with-mock-contents + mock-contents-simple-notes-file + '(lambda () + (org-noter-core-test-create-session) + (with-simulated-input "precise SPC note RET" + (org-noter-insert-precise-note)) + (expect (string-match "\\:HIGHLIGHT\\:" (buffer-string)) :not :to-be nil) + (expect (string-match "HARDCODED_HIGHLIGHT_LOCATION" (buffer-string)) :not :to-be nil))))) + ) + + + + + (describe "org-noter basics" + (it "can start org-noter with `org-noter` call" + (with-mock-contents + mock-contents-simple-notes-file + '(lambda () + ;; move to the heading where we're going to invoke org-noter + (search-forward "nothing-to-hide") + (org-noter)))) + + (it "has org-noter-create-session-from-document hook defined" + (expect org-noter-create-session-from-document-hook :not :to-be nil)) + ) +) diff --git a/tests/org-noter-extra-tests.el b/tests/org-noter-extra-tests.el new file mode 100644 index 0000000..06e7eab --- /dev/null +++ b/tests/org-noter-extra-tests.el @@ -0,0 +1,52 @@ + +(add-to-list 'load-path "modules") + +(describe "org-noter very custom behavior" + (before-each + (create-org-noter-test-session) + ) + (describe "with advice" + (before-each + (setq org-noter-max-short-selected-text-length 700000) + + (define-advice org-noter--insert-heading (:after (level title &optional newlines-number location) add-full-body-quote) + "Advice for org-noter--insert-heading. + + When inserting a precise note insert the text of the note in the body as an org mode QUOTE block. + + =org-noter-max-short-length= should be set to a large value to short circuit the normal behavior: + =(setq org-noter-max-short-length 80000)=" + + ;; this tells us it's a precise note that's being invoked. + (if (consp location) + (insert (format "#+BEGIN_QUOTE\n%s\n#+END_QUOTE" title)))) + (create-org-noter-test-session) + ) + (after-each + (setq org-noter-max-short-selected-text-length 80) + (advice-remove #'org-noter--insert-heading 'org-noter--insert-heading@add-full-body-quote) + ) + (it "should insert the highlighted text as an org-mode QUOTE when advice is enabled." + (with-mock-contents + mock-contents-simple-notes-file + '(lambda () + (org-noter-core-test-create-session) + ;; we're not specifying the note title + (with-simulated-input "RET" + (org-noter-insert-precise-note)) + (let* ((expected-heading (regexp-quote (format "** %s" (org-trim (replace-regexp-in-string "\n" " " (org-noter-test-get-selected-text nil))))))) + (expect (string-match "HARDCODED_HIGHLIGHT_LOCATION" (buffer-string)) :not :to-be nil) + (expect (string-match "BEGIN_QUOTE" (buffer-string)) :not :to-be nil) + (expect (string-match "END_QUOTE" (buffer-string)) :not :to-be nil) + (expect (string-match expected-heading (buffer-string)) :not :to-be nil)))))) + + + (describe "without advice" + (it "should revert back to standard title" + (with-mock-contents + mock-contents-simple-notes-file + '(lambda () + (org-noter-core-test-create-session) + (with-simulated-input "RET" + (org-noter-insert-precise-note)) + (expect (string-match "\\*\\* Notes for page" (buffer-string)) :not :to-be nil)))))) diff --git a/tests/org-noter-location-tests.el b/tests/org-noter-location-tests.el new file mode 100644 index 0000000..2f10103 --- /dev/null +++ b/tests/org-noter-location-tests.el @@ -0,0 +1,56 @@ +(add-to-list 'load-path "modules") +(require 'org-noter-test-utils) + +(defvar mock-contents-simple-notes-file-with-locations + " +:PROPERTIES: +:ID: FAKE_1 +:END: +#+TITLE: Test book notes (simple) +* solove-nothing-to-hide +:PROPERTIES: +:NOTER_DOCUMENT: pubs/solove-nothing-to-hide.pdf +:END: +** Heading1 +:PROPERTIES: +:NOTER_PAGE: 40 +:END: +** Heading2 +:PROPERTIES: +:NOTER_PAGE: (41 0.09 . 0.16) +:HIGHLIGHT: #s(pdf-highlight 41 ((0.18050847457627117 0.09406231628453851 0.6957627118644067 0.12110523221634333))) +:END: +#+BEGIN_QUOTE +Test +#+END_QUOTE + +") + + + + +(describe "org-noter locations" + (describe "basic location parsing works" + (before-each + ) + + (describe "page locations" + (before-each + (create-org-noter-test-session) + ) + (it "can parse a page location" + (with-mock-contents + mock-contents-simple-notes-file-with-locations + '(lambda () + (org-noter-core-test-create-session) + (search-forward "Heading2") + (expect (org-noter--get-containing-heading) :not :to-be nil) + (expect (org-noter--parse-location-property (org-noter--get-containing-element)) :to-equal (read "(41 0.09 . 0.16)")) + ) + + )) + + ) + + ) + ) diff --git a/tests/org-noter-pdf-tests.el b/tests/org-noter-pdf-tests.el new file mode 100644 index 0000000..5873993 --- /dev/null +++ b/tests/org-noter-pdf-tests.el @@ -0,0 +1,69 @@ +(add-to-list 'load-path "modules") +(require 'org-noter-test-utils) + + +(defvar expected-highlight-info (make-pdf-highlight :page 747 :coords '(0.1 0.2 0.3 0.4))) + +(describe "org-noter-pdf-functionality" + ;; todo refactor 👇 + (describe "location functionality" + ) + + + (describe "pdf specific highlight functionality" + (before-each + (spy-on 'pdf-view-active-region-p :and-return-value t) + (spy-on 'pdf-view-active-region :and-return-value '(0.1 0.2 0.3 0.4)) + (spy-on 'image-mode-window-get :and-return-value 747) + ) + + (it "can get coordinates from pdf-view" + (let ((highlight-info (org-noter-pdf--get-highlight))) + (expect 'pdf-view-active-region-p :to-have-been-called) + (expect highlight-info :to-equal expected-highlight-info))) + + (describe "highlight persistence" + (before-each + (create-org-noter-test-session) + ;; (create-org-noter-test-session) sets up a highlight hook, so we have to reset it back. + ;; this might be ok for now? maybe filter out all "-core-test-" hooks instead? + (setq org-noter--get-highlight-location-hook '(org-noter-pdf--get-highlight)) + ) + (it "can take a precise note WITH a highlight appearing" + (with-mock-contents + mock-contents-simple-notes-file + '(lambda () + (org-noter-core-test-create-session) + (with-simulated-input "precise SPC note RET" + (org-noter-insert-precise-note)) + (ont--log-debug "%s" (buffer-string)) + (expect (string-match "\\:HIGHLIGHT\\:" (buffer-string)) :not :to-be nil) + (expect (string-match (format "%s" expected-highlight-info) (buffer-string)) :not :to-be nil) + ) + ) + ) + ) + ) + + (describe "pdf keybinding overrides" + (it "C-c C-c called from a PDF document executes in the notes buffer" + ;; open `org-noter' session with PDF and notes + + ;; execute `C-c C-c' from document buffer + + ;; check that current window is notes-window, check that + ;; last command was `org-ctrl-c-ctrl-c' + ) + + (it "C-c C-x called from a PDF document executes in the notes buffer" + ;; open `org-noter' session with PDF and notes + + ;; execute `C-c C-x ' from document buffer, where + ;; \in {C-b, C-v, maybe a few others} + + ;; check that current window is notes-window, check that + ;; last command corresponds to the keybinding of C-c C-x + ;; . + ) + ) + )