├── .gitignore ├── INSTALL.md ├── README.md ├── TODO.md ├── addon-info.json ├── autoload └── xolox │ ├── notes.vim │ └── notes │ ├── html.vim │ ├── markdown.vim │ ├── mediawiki.vim │ ├── parser.vim │ ├── recent.vim │ └── tags.vim ├── doc └── notes.txt ├── ftplugin └── notes.vim ├── misc └── notes │ ├── search-notes.py │ ├── shadow │ ├── New note │ ├── Note taking commands │ └── Note taking syntax │ └── template.html ├── plugin └── notes.vim ├── screenshots ├── folding.png └── syntax.png └── syntax └── notes.vim /.gitignore: -------------------------------------------------------------------------------- 1 | doc/tags 2 | misc/notes/index.pickle 3 | misc/notes/recent.txt 4 | misc/notes/tags.txt 5 | misc/notes/user/ 6 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Installation instructions 2 | 3 | *Please note that the vim-notes plug-in requires my vim-misc plug-in which is separately distributed.* 4 | 5 | There are two ways to install the vim-notes plug-in and it's up to you which you prefer, both options are explained below. Please note that below are generic installation instructions while some Vim plug-ins may have external dependencies, please refer to the plug-in's [readme](README.md) for details. 6 | 7 | ## Installation using ZIP archives 8 | 9 | Unzip the most recent ZIP archives of the [vim-notes](http://peterodding.com/code/vim/downloads/notes.zip) and [vim-misc](http://peterodding.com/code/vim/downloads/misc.zip) plug-ins inside your Vim profile directory (usually this is `~/.vim` on UNIX and `%USERPROFILE%\vimfiles` on Windows), restart Vim and execute the command `:helptags ~/.vim/doc` (use `:helptags ~\vimfiles\doc` instead on Windows). 10 | 11 | If you get warnings about overwriting existing files while unpacking the ZIP archives you probably don't need to worry about this because it's most likely caused by files like `README.md`, `INSTALL.md` and `addon-info.json`. If these files bother you then you can remove them after unpacking the ZIP archives, they are not required to use the plug-in. 12 | 13 | ## Installation using a Vim plug-in manager 14 | 15 | If you prefer you can also use [Pathogen](http://www.vim.org/scripts/script.php?script_id=2332), [Vundle](https://github.com/gmarik/vundle) or a similar tool to install and update the [vim-notes](https://github.com/xolox/vim-notes) and [vim-misc](https://github.com/xolox/vim-misc) plug-ins using local clones of the git repositories. This takes a bit of work to set up the first time but it makes updating much easier, and it keeps each plug-in in its own directory which helps to keep your Vim profile uncluttered. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Easy note taking in Vim 2 | 3 | The vim-notes plug-in for the [Vim text editor] [vim] makes it easy to manage your notes in Vim: 4 | 5 | * **Starting a new note:** Execute the `:Note` command to create a new buffer and load the appropriate file type and syntax 6 | * You can also start a note with Vim commands like `:edit`, `:tabedit` and `:split` by starting the filename with `note:`, as in `:edit note:todo` (the part after `note:` doesn't have to be the complete note title and if it's empty a new note will be created) 7 | * You can start a new note with the selected text as title in the current window using the `\en` mapping or `:NoteFromSelectedText` command (there are similar mappings and commands for opening split windows and tab pages) 8 | * **Saving notes:** Just use Vim's [:write] [write] and [:update] [update] commands, you don't need to provide a filename because it will be set based on the title (first line) of your note (you also don't need to worry about special characters, they'll be escaped) 9 | * **Editing existing notes:** Execute `:Note anything` to edit a note containing `anything` in its title (if no notes are found a new one is created with its title set to `anything`) 10 | * The `:Note` and `:DeleteNote` commands support tab completion of note titles 11 | * **Deleting notes:** The `:DeleteNote` command enables you to delete the current note 12 | * **Searching notes:** `:SearchNotes keyword …` searches for keywords and `:SearchNotes /pattern/` searches for regular expressions 13 | * The `:SearchNotes` command supports tab completion of keywords and sorts candidates by relevance ([Levenshtein distance] [levenshtein]) 14 | * **Smart defaults:** Without an argument `:SearchNotes` searches for the word under the cursor (if the word starts with `@` that character will be included in the search, this means you can easily search for *@tagged* notes) 15 | * **Back-references:** The `:RelatedNotes` command find all notes referencing the current file 16 | * A [Python 2] [python] script is included that accelerates keyword searches using a keyword index 17 | * The `:RecentNotes` command lists your notes by modification date, starting with the most recently edited note 18 | * **Navigating within notes:** The vim-notes syntax uses atx-style headers just like [Markdown] [markdown] (one to six `#` marks at the start of the line) and supports text folding based on these headers. This allows easy navigation within notes that contain large (and possibly nested) sections of text separated by headers. [Here's a screen shot of text folding] [folding]. 19 | * **Navigating between notes:** The included syntax script highlights note names as hyper links and the file type plug-in redefines [gf] [gf] to jump between notes (the [Control-w f] [ctrlwf] mapping to jump to a note in a split window and the [Control-w gf] [ctrlwgf] mapping to jump to a note in a new tab page also work) 20 | * **Writing aids:** The included file type plug-in contains mappings for automatic curly quotes, arrows and list bullets and supports completion of note titles using Control-X Control-U and completion of tags using Control-X Control-O 21 | * **Embedded file types:** The included syntax script supports embedded highlighting using blocks marked with `{{{type … }}}` (triple back ticks ala [GFM] [gfm] are also supported) which allows you to embed highlighted code and configuration snippets in your notes 22 | 23 | Here's a screen shot of the syntax mode (using the [Slate] [slate] color scheme and the [Monaco] [monaco] font): 24 | 25 | ![Syntax mode screen shot](http://peterodding.com/code/vim/notes/syntax.png) 26 | 27 | ## Install & usage 28 | 29 | Please refer to [the installation instructions] [install-notes] available on GitHub. Once you've installed the plug-in you can get started by executing `:Note` or `:edit note:`, this will start a new note that contains instructions on how to continue from there (and how to use the plug-in in general). 30 | 31 | Make sure `filetype plugin on` (or a variant of that command) is included in your [vimrc script] [vimrc], without that things will not work as intended :-). 32 | 33 | ## Options 34 | 35 | All options have reasonable defaults so if the plug-in works after installation you don't need to change any options. The options are available for people who like to customize how the plug-in works. You can set these options in your [vimrc script] [vimrc] by including a line like this: 36 | 37 | :let g:notes_directories = ['~/Documents/Notes', '~/Dropbox/Shared Notes'] 38 | 39 | Note that after changing an option in your [vimrc script] [vimrc] you have to restart Vim for the changes to take effect. 40 | 41 | ### The `g:notes_directories` option 42 | 43 | Your notes are stored in one or more directories. This option defines where you want to store your notes. Its value should be a list (there's an example above) with one or more pathnames. The default is a single value which depends on circumstances but should work for most people: 44 | 45 | * If the profile directory where the plug-in is installed is writable, the directory `misc/notes/user` under the profile directory is used. This is for compatibility with [Pathogen] [pathogen]; the notes will be stored inside the plug-in's bundle. 46 | 47 | * If the above doesn't work out, the default depends on the platform: `~/vimfiles/misc/notes/user` on Windows and `~/.vim/misc/notes/user` on other platforms. 48 | 49 | #### Backwards compatibility 50 | 51 | In the past the notes plug-in only supported a single directory and the corresponding option was called `g:notes_directory`. When support for multiple notes directories was introduced the option was renamed to `g:notes_directories` to reflect that the value is now a list of directory pathnames. 52 | 53 | For backwards compatibility with old configurations (all of them as of this writing :-) the notes plug-in still uses `g:notes_directory` when it is defined (its no longer defined by the plug-in). However when the plug-in warns you to change your configuration you probably should because this compatibility will be removed at some point. 54 | 55 | ### The `g:notes_suffix` option 56 | 57 | The suffix to add to generated filenames. The plug-in generates filenames for your notes based on the title (first line) of each note and by default these filenames don't include an extension like `.txt`. You can use this option to make the plug-in automatically append an extension without having to embed the extension in the note's title, e.g.: 58 | 59 | :let g:notes_suffix = '.txt' 60 | 61 | ### The `g:notes_title_sync` option 62 | 63 | When you rename a file in your notes directory but don't change the title, the plug-in will notice this the next time you open the note in Vim. Likewise when you change the title in another text editor but don't rename the file. By default the plug-in will prompt you whether you want it to update the title of the note, rename the file on disk or dismiss the prompt without doing anything. 64 | 65 | If you set this option to the string `'no'` this feature will be completely disabled. If you set it to `'change_title'` it will automatically change the title to match the filename. If you set it to `'rename_file'` it will automatically rename the file on disk to match the title. 66 | 67 | This option only concerns the behavior of vim-notes when you open an existing note; it does not change the fact that when you change a note's title in Vim and then save the note, the file is renamed (this is a fundamental feature of the vim-notes plug-in). 68 | 69 | ### The `g:notes_word_boundaries` option 70 | 71 | Old versions of the notes plug-in would highlight note titles without considering word boundaries. This is still the default behavior but the plug-in can now be told to respect word boundaries by changing this option from its default: 72 | 73 | :let g:notes_word_boundaries = 1 74 | 75 | ### The `g:notes_unicode_enabled` option 76 | 77 | By default the vim-notes plug-in uses Unicode characters (e.g. list bullets, arrows, etc.) when Vim's ['encoding'] [enc] option is set to UTF-8. If you don't want Unicode characters in your notes (regardless of the ['encoding'] [enc] option) you can set this option to false (0): 78 | 79 | :let g:notes_unicode_enabled = 0 80 | 81 | ### The `g:notes_smart_quotes` option 82 | 83 | By default the notes plug-in automatically performs several substitutions on the text you type in insert mode, for example regular quote marks are replaced with curly quotes. The full list of substitutions can be found below in the documentation on mappings. If you don't want the plug-in to perform these substitutions, you can set this option to zero like this: 84 | 85 | :let g:notes_smart_quotes = 0 86 | 87 | ### The `g:notes_ruler_text` option 88 | 89 | The text of the ruler line inserted when you type `***` in quick succession. It defaults to three asterisks separated by spaces, center aligned to the text width. 90 | 91 | ### The `g:notes_list_bullets` option 92 | 93 | A list of characters used as list bullets. When you're using a Unicode encoding this defaults to `['•', '◦', '▸', '▹', '▪', '▫']`, otherwise it defaults to `['*', '-', '+']`. 94 | 95 | When you change the nesting level (indentation) of a line containing a bullet point using one of the mappings `Tab`, `Shift-Tab`, `Alt-Left` and `Alt-Right` the bullet point will be automatically changed to correspond to the new nesting level. 96 | 97 | The first level of list items gets the first bullet point in `g:notes_list_bullets`, the second level gets the second, etc. When you're indenting a list item to a level where the `g:notes_list_bullets` doesn't have enough bullets, the plug-in starts again at the first bullet in the list (in other words the selection of bullets wraps around). 98 | 99 | ### The `g:notes_tab_indents` option 100 | 101 | By default `Tab` is mapped to indent list items and `Shift-Tab` is mapped to dedent list items. You can disable these mappings by adding the following to your [vimrc script] [vimrc]: 102 | 103 | :let g:notes_tab_indents = 0 104 | 105 | ### The `g:notes_alt_indents` option 106 | 107 | By default `Alt-Right` is mapped to indent list items and `Alt-Left` is mapped to dedent list items. You can disable these mappings by adding the following to your [vimrc script] [vimrc]: 108 | 109 | :let g:notes_alt_indents = 0 110 | 111 | ### The `g:notes_shadowdir` option 112 | 113 | The notes plug-in comes with some default notes containing documentation about the plug-in. This option defines the path of the directory containing these notes. 114 | 115 | ### The `g:notes_indexfile` option 116 | 117 | This option defines the pathname of the optional keyword index used by the `:SearchNotes` to perform accelerated keyword searching. 118 | 119 | ### The `g:notes_indexscript` option 120 | 121 | This option defines the pathname of the Python script that's used to perform accelerated keyword searching with `:SearchNotes`. 122 | 123 | ### The `g:notes_tagsindex` option 124 | 125 | This option defines the pathname of the text file that stores the list of known tags used for tag name completion and the `:ShowTaggedNotes` command. The text file is created automatically when it's first needed, after that you can recreate it manually by executing `:IndexTaggedNotes` (see below). 126 | 127 | ### The `g:notes_markdown_program` option 128 | 129 | The `:NoteToHtml` command requires the [Markdown] [markdown] program. By default the name of this program is assumed to be simply `markdown`. If you want to use a different program for Markdown to HTML conversion, set this option to the name of the program. 130 | 131 | ### The `g:notes_conceal_code` option 132 | 133 | By default the backticks that mark inline code snippets and the curly quotes that mark code blocks are hidden when your version of Vim supports concealing of text. By setting this option to zero you stop vim-notes from hiding these markers. For example in the following sentence, the backticks would be visible in the editor when this option is set to zero: 134 | 135 | This is a sentence with an `inline code` fragment. 136 | 137 | ### The `g:notes_conceal_italic` option 138 | 139 | By default the underscores that mark italic text are hidden when your version of Vim supports concealing of text. By setting this option to zero you stop vim-notes from hiding those underscores. In the following example, the underscores would be visible in the editor when this option is set to zero: 140 | 141 | This is a sentence with _italic_ text. 142 | 143 | ### The `g:notes_conceal_bold` option 144 | 145 | By default the stars that mark bold text are hidden when your version of Vim supports concealing of text. By setting this option to zero you stop vim-notes from hiding those stars. In the following example, the stars would be visible in the editor when this option is set to zero: 146 | 147 | This is a sentence with *bold* text. 148 | 149 | ### The `g:notes_conceal_url` option 150 | 151 | By default URL schemes (text fragments like `http://`) are hidden when your version of Vim supports concealing of text. By setting this option to zero you stop vim-notes from hiding URL schemes. In the following example, the `https://` text would be visible in the editor when this option is set to zero: 152 | 153 | You can find the vim-notes plug-in at https://github.com/xolox/vim-notes. 154 | 155 | ## Commands 156 | 157 | To edit one of your existing notes (or create a new one) you can use Vim commands such as [:edit] [edit], [:split] [split] and [:tabedit] [tabedit] with a filename that starts with *note:* followed by (part of) the title of one of your notes, e.g.: 158 | 159 | :edit note:todo 160 | 161 | This shortcut also works from the command line: 162 | 163 | $ gvim note:todo 164 | 165 | When you don't follow *note:* with anything a new note is created like when you execute `:Note` without any arguments. If the *note:* shortcut is used from the command line, the environment variable `$VIM_NOTES_TEMPLATE` can be set to the filename of a template for new notes (this will override the default template). 166 | 167 | ### The `:Note` command 168 | 169 | When executed without any arguments this command starts a new note in the current window. If you pass one or more arguments the command will edit an existing note containing the given words in the title. If more than one note is found you'll be asked which note you want to edit. If no notes are found a new note is started with the given word(s) as title. 170 | 171 | This command will fail when changes have been made to the current buffer, unless you use `:Note!` which discards any changes. 172 | 173 | When you are using multiple directories to store your notes and you run `:Note` while editing an existing note, a new note will inherit the directory of the note from which you started. Otherwise the note is created in the first directory in `g:notes_directories`. 174 | 175 | *This command supports tab completion:* If you complete one word, all existing notes containing the given word somewhere in their title are suggested. If you type more than one word separated by spaces, the plug-in will complete only the missing words so that the resulting command line contains the complete note title and nothing more. 176 | 177 | ### The `:NoteFromSelectedText` command 178 | 179 | Start a new note in the current window with the selected text as the title of the note. The name of this command isn't very well suited to daily use, that's because it's intended to be executed from a mapping. The default mapping for this command is `\en` (the backslash is actually the character defined by the [mapleader] [mapleader] variable). 180 | 181 | When you are using multiple directories to store your notes and you run `:NoteFromSelectedText` while editing an existing note, the new note will inherit the directory of the note from which it was created. 182 | 183 | ### The `:SplitNoteFromSelectedText` command 184 | 185 | Same as `:NoteFromSelectedText` but opens the new note in a vertical split window. The default mapping for this command is `\sn`. 186 | 187 | ### The `:TabNoteFromSelectedText` command 188 | 189 | Same as `:NoteFromSelectedText` but opens the new note in a new tab page. The default mapping for this command is `\tn`. 190 | 191 | ### The `:DeleteNote` command 192 | 193 | The `:DeleteNote` command deletes a note file, destroys the buffer and removes the note from the internal cache of filenames and note titles. If you pass a note name as an argument to `:DeleteNote` it will delete the given note, otherwise it will delete the current note. This fails when changes have been made to the buffer, unless you use `:DeleteNote!` which discards any changes. 194 | 195 | ### The `:SearchNotes` command 196 | 197 | This command wraps [:vimgrep] [vimgrep] and enables you to search through your notes using one or more keywords or a regular expression pattern. To search for a pattern you pass a single argument that starts/ends with a slash: 198 | 199 | :SearchNotes /TODO\|FIXME\|XXX/ 200 | 201 | To search for one or more keywords you can just omit the slashes, this matches notes containing all of the given keywords: 202 | 203 | :SearchNotes syntax highlighting 204 | 205 | #### `:SearchNotes` understands @tags 206 | 207 | If you don't pass any arguments to the `:SearchNotes` command it will search for the word under the cursor. If the word under the cursor starts with '@' this character will be included in the search, which makes it possible to easily add *@tags* to your *@notes* and then search for those tags. To make searching for tags even easier you can create key mappings for the `:SearchNotes` command: 208 | 209 | " Make the C-] combination search for @tags: 210 | imap :SearchNotes 211 | nmap :SearchNotes 212 | 213 | " Make double mouse click search for @tags. This is actually quite a lot of 214 | " fun if you don't use the mouse for text selections anyway; you can click 215 | " between notes as if you're in a web browser: 216 | imap <2-LeftMouse> :SearchNotes 217 | nmap <2-LeftMouse> :SearchNotes 218 | 219 | These mappings are currently not enabled by default because they conflict with already useful key mappings, but if you have any suggestions for alternatives feel free to contact me through GitHub or at . 220 | 221 | #### Accelerated searching with Python 222 | 223 | After collecting a fair amount of notes (say more than 5 MB) you will probably start to get annoyed at how long it takes Vim to search through all of your notes. To make searching more scalable the notes plug-in includes a Python script which uses a persistent full text index of your notes stored in a file. 224 | 225 | The first time the Python script is run it will need to build the complete index which can take a moment, but after the index has been initialized updates and searches should be more or less instantaneous. 226 | 227 | ### The `:RelatedNotes` command 228 | 229 | This command makes it easy to find all notes related to the current file: If you are currently editing a note then a search for the note's title is done, otherwise this searches for the absolute path of the current file. 230 | 231 | ### The `:RecentNotes` command 232 | 233 | If you execute the `:RecentNotes` command it will open a Vim buffer that lists all your notes grouped by the day they were edited, starting with your most recently edited note. If you pass an argument to `:RecentNotes` it will filter the list of notes by matching the title of each note against the argument which is interpreted as a Vim pattern. 234 | 235 | ### The `:MostRecentNote` command 236 | 237 | This command edits your most recently edited note (whether you just opened the note or made changes to it). The plug-in will remember the most recent note between restarts of Vim and is shared between all instances of Vim. 238 | 239 | ### The `:ShowTaggedNotes` command 240 | 241 | To show a list of all notes that contains *@tags* you can use the `:ShowTaggedNotes` command. If you pass a count to this command it will limit the list of tags to those that have been used at least this many times. For example the following two commands show tags that have been used at least ten times: 242 | 243 | :10ShowTaggedNotes 244 | :ShowTaggedNotes 10 245 | 246 | ### The `:IndexTaggedNotes` command 247 | 248 | The notes plug-in defines an omni completion function that can be used to complete the names of tags. To trigger the omni completion you type Control-X Control-O. When you type `@` in insert mode the plug-in will automatically start omni completion. 249 | 250 | The completion menu is populated from a text file listing all your tags, one on each line. The first time omni completion triggers, an index of tag names is generated and saved to the location set by `g:notes_tagsindex`. After this file is created, it will be updated automatically as you edit notes and add/remove tags. 251 | 252 | If for any reason you want to recreate the list of tags you can execute the `:IndexTaggedNotes` command. 253 | 254 | ### The `:NoteToHtml` command 255 | 256 | This command converts the current note to HTML. It works by first converting the current note to [Markdown] [markdown] and then using the `markdown` program to convert that to HTML. It requires an external program to convert Markdown to HTML. By default the program `markdown` is used, but you can change the name of the program using the `g:notes_markdown_program` option. To convert your note to HTML and open the generated web page in a browser, you can run: 257 | 258 | :NoteToHtml 259 | 260 | Alternatively, to convert your note to HTML and display it in a new split window in Vim, you can run: 261 | 262 | :NoteToHtml split 263 | 264 | Note that this command can be a bit slow, because the parser for the note taking syntax is written in Vim script (for portability) and has not been optimized for speed (yet). 265 | 266 | ### The `:NoteToMarkdown` command 267 | 268 | Convert the current note to a [Markdown document] [markdown]. The vim-notes syntax shares a lot of similarities with the Markdown text format, but there are some notable differences, which this command takes care of: 269 | 270 | * The first line of a note is an implicit document title. In Markdown format it has to be marked with `#`. This also implies that the remaining headings should be shifted by one level. 271 | 272 | * Preformatted blocks are marked very differently in notes and Markdown (`{{{` and `}}}` markers versus 4 space indentation). 273 | 274 | * The markers and indentation of list items differ between notes and Markdown (dumb bullets vs Unicode bullets and 3 vs 4 spaces). 275 | 276 | Note that this command can be a bit slow, because the parser for the note taking syntax is written in Vim script (for portability) and has not been optimized for speed (yet). 277 | 278 | ### The `:NoteToMediawiki` command 279 | 280 | Convert the current note to a [Mediawiki document] [mediawiki]. This is similar to the `:NoteToMarkdown` command, but it produces wiki text that can be displayed on a Mediawiki site. That being said, the subset of wiki markup that vim-notes actually produces will probably work on other wiki sites. These are the notable transforations: 281 | 282 | * The first line of the note is a title, but it isn't used in the Mediawiki syntax. It could have been put into a `= Title =` tag, but it doesn't really make sense in the context of a wiki. It would make the table of contents nest under the title for every document you create. 283 | 284 | * Preformatted blocks are output into `` tags. This functionality is enabled on Mediawiki through the [SyntaxHighlight GeSHi extention] [geshi]. It is also supported on Wikipedia. 285 | 286 | ## Mappings 287 | 288 | The following key mappings are defined inside notes. 289 | 290 | ### Insert mode mappings 291 | 292 | * `@` automatically triggers tag completion 293 | * `'` becomes `‘` or `’` depending on where you type it 294 | * `"` becomes `“` or `”` (same goes for these) 295 | * `--` becomes `—` 296 | * `->` becomes `→` 297 | * `<-` becomes `←` 298 | * the bullets `*`, `-` and `+` become `•` 299 | * the three characters `***` in insert mode in quick succession insert a horizontal ruler delimited by empty lines 300 | * `Tab` and `Alt-Right` increase indentation of list items (works on the current line and selected lines) 301 | * `Shift-Tab` and `Alt-Left` decrease indentation of list items 302 | * `Enter` on a line with only a list bullet removes the bullet and starts a new line below the current line 303 | * `\en` executes `:NoteFromSelectedText` 304 | * `\sn` executes `:SplitNoteFromSelectedText` 305 | * `\tn` executes `:TabNoteFromSelectedText` 306 | 307 | ## Customizing the syntax highlighting of notes 308 | 309 | The syntax mode for notes is written so you can override styles you don't like. To do so you can add lines such as the following to your [vimrc script] [vimrc]: 310 | 311 | " Don't highlight single quoted strings. 312 | highlight link notesSingleQuoted Normal 313 | 314 | " Show double quoted strings in italic font. 315 | highlight notesDoubleQuoted gui=italic 316 | 317 | See the documentation of the [:highlight] [highlight] command for more information. Below are the names of the syntax items defined by the notes syntax mode: 318 | 319 | * `notesName` - the names of other notes, usually highlighted as a hyperlink 320 | * `notesTagName` - words preceded by an `@` character, also highlighted as a hyperlink 321 | * `notesListBullet` - the bullet characters used for list items 322 | * `notesListNumber` - numbers in front of list items 323 | * `notesDoubleQuoted` - double quoted strings 324 | * `notesSingleQuoted` - single quoted strings 325 | * `notesItalic` - strings between two `_` characters 326 | * `notesBold` - strings between two `*` characters 327 | * `notesTextURL` - plain domain name (recognized by leading `www.`) 328 | * `notesRealURL` - URLs (e.g. ) 329 | * `notesEmailAddr` - e-mail addresses 330 | * `notesUnixPath` - UNIX file paths (e.g. `~/.vimrc` and `/home/peter/.vimrc`) 331 | * `notesPathLnum` - line number following a UNIX path 332 | * `notesWindowsPath` - Windows file paths (e.g. `c:\users\peter\_vimrc`) 333 | * `notesTodo` - `TODO` markers 334 | * `notesXXX` - `XXX` markers 335 | * `notesFixMe` - `FIXME` markers 336 | * `notesInProgress` - `CURRENT`, `INPROGRESS`, `STARTED` and `WIP` markers 337 | * `notesDoneItem` - lines containing the marker `DONE`, usually highlighted as a comment 338 | * `notesDoneMarker` - `DONE` markers 339 | * `notesVimCmd` - Vim commands, words preceded by an `:` character 340 | * `notesTitle` - the first line of each note 341 | * `notesShortHeading` - short sentences ending in a `:` character 342 | * `notesAtxHeading` - lines preceded by one or more `#` characters 343 | * `notesBlockQuote` - lines preceded by a `>` character 344 | * `notesRule` - lines containing only whitespace and `* * *` 345 | * `notesCodeStart` - the `{{{` markers that begin a block of code (including the syntax name) 346 | * `notesCodeEnd` - the `}}}` markers that end a block of code 347 | * `notesModeLine` - Vim [modeline] [modeline] in last line of notes 348 | * `notesLastEdited` - last edited dates in `:ShowTaggedNotes` buffers 349 | 350 | ## Other plug-ins that work well with the notes plug-in 351 | 352 | ### utl.vim 353 | 354 | The [utl.vim] [utl] universal text linking plug-in enables links between your notes, other local files and remote resources like web pages. 355 | 356 | ### vim-shell 357 | 358 | My [vim-shell] [shell] plug-in also enables easy navigation between your notes and environment like local files and directories, web pages and e-mail addresses by providing key mappings and commands to e.g. open the file/URL under the text cursor. This plug-in can also change Vim to full screen which can be really nice for large notes. 359 | 360 | ### VOoM 361 | 362 | The [VOoM] [voom] outlining plug-in should work well for notes if you use the Markdown style headers starting with `#`, however it has been reported that this combination may not always work so well in practice (sometimes losing notes!) 363 | 364 | ### Txtfmt 365 | 366 | If the text formatting supported by the notes plug-in is not enough for you, consider trying the [Txtfmt] [txtfmt] (The Vim Highlighter) plug-in. To use the two plug-ins together, create the file `after/ftplugin/notes.vim` inside your Vim profile with the following contents: 367 | 368 | " Enable Txtfmt formatting inside notes. 369 | setlocal filetype=notes.txtfmt 370 | 371 | ## Using the notes file type for git commit messages 372 | 373 | If you write your git commit messages in Vim and want to use the notes file type (syntax highlighting and editing mode) to edit your git commit messages you can add the following line to your [vimrc script] [vimrc]: 374 | 375 | autocmd BufNewFile,BufRead */.git/COMMIT_EDITMSG setlocal filetype=notes 376 | 377 | This is not a complete solution (there are more types of commit messages that the pattern above won't match) but that is outside the scope of this document. For inspiration you can take a look at the [runtime/filetype.vim] [filetype.vim] file in Vim's Mercurial repository. 378 | 379 | ## Contact 380 | 381 | If you have questions, bug reports, suggestions, etc. the author can be contacted at . The latest version is available at and . If you like the script please vote for it on [Vim Online] [vim_online]. 382 | 383 | ## License 384 | 385 | This software is licensed under the [MIT license] [mit]. 386 | © 2015 Peter Odding <>. 387 | 388 | 389 | [ctrlwf]: http://vimdoc.sourceforge.net/htmldoc/windows.html#CTRL-W_f 390 | [ctrlwgf]: http://vimdoc.sourceforge.net/htmldoc/windows.html#CTRL-W_gf 391 | [edit]: http://vimdoc.sourceforge.net/htmldoc/editing.html#:edit 392 | [enc]: http://vimdoc.sourceforge.net/htmldoc/options.html#'encoding' 393 | [filetype.vim]: https://code.google.com/p/vim/source/browse/runtime/filetype.vim?r=fbc1131f0ba5be4ec74fb2ccdfb3559b446a2b1e#778 394 | [folding]: https://raw.githubusercontent.com/xolox/vim-notes/master/screenshots/folding.png 395 | [geshi]: http://www.mediawiki.org/wiki/Extension:SyntaxHighlight_GeSHi 396 | [gf]: http://vimdoc.sourceforge.net/htmldoc/editing.html#gf 397 | [gfm]: https://help.github.com/articles/github-flavored-markdown/ 398 | [highlight]: http://vimdoc.sourceforge.net/htmldoc/syntax.html#:highlight 399 | [install-notes]: https://github.com/xolox/vim-notes/blob/master/INSTALL.md 400 | [levenshtein]: http://en.wikipedia.org/wiki/Levenshtein_distance 401 | [mapleader]: http://vimdoc.sourceforge.net/htmldoc/map.html#mapleader 402 | [markdown]: http://en.wikipedia.org/wiki/Markdown 403 | [mediawiki]: https://www.mediawiki.org/wiki/MediaWiki 404 | [mit]: http://en.wikipedia.org/wiki/MIT_License 405 | [modeline]: http://vimdoc.sourceforge.net/htmldoc/options.html#modeline 406 | [monaco]: http://en.wikipedia.org/wiki/Monaco_(typeface) 407 | [pathogen]: http://www.vim.org/scripts/script.php?script_id=2332 408 | [python]: http://python.org/ 409 | [shell]: http://www.vim.org/scripts/script.php?script_id=3123 410 | [slate]: http://code.google.com/p/vim/source/browse/runtime/colors/slate.vim 411 | [split]: http://vimdoc.sourceforge.net/htmldoc/windows.html#:split 412 | [tabedit]: http://vimdoc.sourceforge.net/htmldoc/tabpage.html#:tabedit 413 | [txtfmt]: http://www.vim.org/scripts/script.php?script_id=2208 414 | [update]: http://vimdoc.sourceforge.net/htmldoc/editing.html#:update 415 | [utl]: http://www.vim.org/scripts/script.php?script_id=293 416 | [vim]: http://www.vim.org/ 417 | [vim_online]: http://www.vim.org/scripts/script.php?script_id=3375 418 | [vimgrep]: http://vimdoc.sourceforge.net/htmldoc/quickfix.html#:vimgrep 419 | [vimrc]: http://vimdoc.sourceforge.net/htmldoc/starting.html#vimrc 420 | [voom]: http://www.vim.org/scripts/script.php?script_id=2657 421 | [write]: http://vimdoc.sourceforge.net/htmldoc/editing.html#:write 422 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # To-do list for the `notes.vim` plug-in 2 | 3 | * The note name highlighting uses word boundaries so that 'git' inside 'fugitive' is not highlighted, however this breaks highlighting of note names ending in punctuation (or more generically ending in non-word characters). 4 | * The `ftplugin/notes.vim` script used to clear the [matchpairs] [matchpairs] option so that pairs of characters are not highlighted in notes (the irrelevant highlighting was starting to annoy me). Several people have since complained that Vim rings a bell or flashes the screen for every key press in insert mode when editing notes. I've now removed the matchpairs manipulation from the plug-in but I suspect that this may actually be a bug in Vim; to be investigated. See also [issue 10 on GitHub] [issue_10]. 5 | * Override `` to show a quick reference of available commands? 6 | * Define aliases of the available commands that start with `Note` (to help people getting started with the plug-in). 7 | * Add a key mapping to toggle text folding (currently in my `~/.vimrc`) 8 | * Add a key mapping or command to toggle the visibility of `{{{ … }}}` code markers? 9 | * Find a good way to support notes with generates contents, e.g. *'all notes'*. 10 | * When renaming a note, also update references to the note in other notes? (make this optional of course!) 11 | * Improve highlighting of lines below a line with a `DONE` marker; when navigating over such lines, the highlighting will sometimes disappear (except on the first line). See also [issue #2 on GitHub] [issue_2]. 12 | 13 | [issue_2]: https://github.com/xolox/vim-notes/issues/2 14 | [issue_10]: https://github.com/xolox/vim-notes/issues/10 15 | [matchpairs]: http://vimdoc.sourceforge.net/htmldoc/options.html#%27matchpairs%27 16 | -------------------------------------------------------------------------------- /addon-info.json: -------------------------------------------------------------------------------- 1 | {"vim_script_nr": 3375, "dependencies": {"vim-misc": {}}, "homepage": "http://peterodding.com/code/vim/notes", "name": "vim-notes"} -------------------------------------------------------------------------------- /autoload/xolox/notes.vim: -------------------------------------------------------------------------------- 1 | " Vim auto-load script 2 | " Author: Peter Odding 3 | " Last Change: November 4, 2015 4 | " URL: http://peterodding.com/code/vim/notes/ 5 | 6 | " Note: This file is encoded in UTF-8 including a byte order mark so 7 | " that Vim loads the script using the right encoding transparently. 8 | 9 | let g:xolox#notes#version = '0.33.4' 10 | let g:xolox#notes#url_pattern = '\<\(mailto:\|javascript:\|\w\{3,}://\)\(\S*\w\)\+/\?' 11 | let s:scriptdir = expand(':p:h') 12 | 13 | function! xolox#notes#init() " {{{1 14 | " Initialize the configuration of the notes plug-in. This is a bit tricky: 15 | " We want to be compatible with Pathogen which installs plug-ins as 16 | " "bundles" under ~/.vim/bundle/*/ so we use a relative path to make sure we 17 | " 'stay inside the bundle'. However if the notes.vim plug-in is installed 18 | " system wide the user probably won't have permission to write inside the 19 | " installation directory, so we have to switch to $HOME then. 20 | let systemdir = xolox#misc#path#absolute(s:scriptdir . '/../../misc/notes') 21 | if filewritable(systemdir) == 2 22 | let localdir = systemdir 23 | elseif xolox#misc#os#is_win() 24 | let localdir = xolox#misc#path#absolute('~/vimfiles/misc/notes') 25 | else 26 | let localdir = xolox#misc#path#absolute('~/.vim/misc/notes') 27 | endif 28 | " Backwards compatibility with old configurations. 29 | if exists('g:notes_directory') 30 | call xolox#misc#msg#warn("notes.vim %s: Please upgrade your configuration, see :help notes-backwards-compatibility", g:xolox#notes#version) 31 | let g:notes_directories = [g:notes_directory] 32 | unlet g:notes_directory 33 | endif 34 | " Define the default location where the user's notes are saved? 35 | if !exists('g:notes_directories') 36 | let g:notes_directories = [xolox#misc#path#merge(localdir, 'user')] 37 | endif 38 | call s:create_notes_directories() 39 | " Define the default location of the shadow directory with predefined notes? 40 | if !exists('g:notes_shadowdir') 41 | let g:notes_shadowdir = xolox#misc#path#merge(systemdir, 'shadow') 42 | endif 43 | " Define the default location for the full text index. 44 | if !exists('g:notes_indexfile') 45 | let g:notes_indexfile = xolox#misc#path#merge(localdir, 'index.pickle') 46 | endif 47 | " Define the default location for the keyword scanner script. 48 | if !exists('g:notes_indexscript') 49 | let g:notes_indexscript = xolox#misc#path#merge(systemdir, 'search-notes.py') 50 | endif 51 | " Define the default suffix for note filenames. 52 | if !exists('g:notes_suffix') 53 | let g:notes_suffix = '' 54 | endif 55 | " Define the default location for the tag name index (used for completion). 56 | if !exists('g:notes_tagsindex') 57 | let g:notes_tagsindex = xolox#misc#path#merge(localdir, 'tags.txt') 58 | endif 59 | " Define the default location for the file containing the most recent note's 60 | " filename. 61 | if !exists('g:notes_recentindex') 62 | let g:notes_recentindex = xolox#misc#path#merge(localdir, 'recent.txt') 63 | endif 64 | " Define the default location of the template for new notes. 65 | if !exists('g:notes_new_note_template') 66 | if !empty($VIM_NOTES_TEMPLATE) 67 | " Command line override. 68 | let g:notes_new_note_template = xolox#misc#path#absolute($VIM_NOTES_TEMPLATE) 69 | else 70 | let g:notes_new_note_template = xolox#misc#path#merge(g:notes_shadowdir, 'New note') 71 | endif 72 | endif 73 | " Define the default location of the template for HTML conversion. 74 | if !exists('g:notes_html_template') 75 | let g:notes_html_template = xolox#misc#path#merge(localdir, 'template.html') 76 | endif 77 | " Define the default action when a note's filename and title are out of sync. 78 | if !exists('g:notes_title_sync') 79 | " Valid values are "no", "change_title", "rename_file" and "prompt". 80 | let g:notes_title_sync = 'prompt' 81 | endif 82 | " Unicode is enabled by default if Vim's encoding is set to UTF-8. 83 | if !exists('g:notes_unicode_enabled') 84 | let g:notes_unicode_enabled = (&encoding == 'utf-8') 85 | endif 86 | " Smart quotes and such are enabled by default. 87 | if !exists('g:notes_smart_quotes') 88 | let g:notes_smart_quotes = 1 89 | endif 90 | " Tab/Shift-Tab is used to indent/dedent list items by default. 91 | if !exists('g:notes_tab_indents') 92 | let g:notes_tab_indents = 1 93 | endif 94 | " Alt-Left/Alt-Right is used to indent/dedent list items by default. 95 | if !exists('g:notes_alt_indents') 96 | let g:notes_alt_indents = 1 97 | endif 98 | " Text used for horizontal rulers. 99 | if !exists('g:notes_ruler_text') 100 | let g:notes_ruler_text = repeat(' ', ((&tw > 0 ? &tw : 79) - 5) / 2) . '* * *' 101 | endif 102 | " Symbols used to denote list items with increasing nesting levels. 103 | let g:notes_unicode_bullets = ['•', '◦', '▸', '▹', '▪', '▫'] 104 | let g:notes_ascii_bullets = ['*', '-', '+'] 105 | if !exists('g:notes_list_bullets') 106 | if xolox#notes#unicode_enabled() 107 | let g:notes_list_bullets = g:notes_unicode_bullets 108 | else 109 | let g:notes_list_bullets = g:notes_ascii_bullets 110 | endif 111 | endif 112 | " Should note titles only match (be highlighted) on word boundaries? 113 | if !exists('g:notes_word_boundaries') 114 | let g:notes_word_boundaries = 0 115 | endif 116 | endfunction 117 | 118 | function! s:create_notes_directories() 119 | for directory in xolox#notes#find_directories(0) 120 | if !isdirectory(directory) 121 | call xolox#misc#msg#info("notes.vim %s: Creating notes directory %s (first run?) ..", g:xolox#notes#version, directory) 122 | call mkdir(directory, 'p') 123 | endif 124 | if filewritable(directory) != 2 125 | call xolox#misc#msg#warn("notes.vim %s: The notes directory %s is not writable!", g:xolox#notes#version, directory) 126 | endif 127 | endfor 128 | endfunction 129 | 130 | function! xolox#notes#shortcut() " {{{1 131 | " The "note:" pseudo protocol is just a shortcut for the :Note command. 132 | let expression = expand('') 133 | let bufnr_save = bufnr('%') 134 | call xolox#misc#msg#debug("notes.vim %s: Expanding shortcut %s ..", g:xolox#notes#version, string(expression)) 135 | let substring = matchstr(expression, 'note:\zs.*') 136 | call xolox#misc#msg#debug("notes.vim %s: Editing note based on title substring %s ..", g:xolox#notes#version, string(substring)) 137 | call xolox#notes#edit(v:cmdbang ? '!' : '', substring) 138 | " Clean up the buffer with the name "note:..."? 139 | let pathname = fnamemodify(bufname(bufnr_save), ':p') 140 | let basename = fnamemodify(pathname, ':t') 141 | if basename =~ '^note:' 142 | call xolox#misc#msg#debug("notes.vim %s: Cleaning up buffer #%i - %s", g:xolox#notes#version, bufnr_save, pathname) 143 | execute 'bwipeout' bufnr_save 144 | endif 145 | endfunction 146 | 147 | function! xolox#notes#edit(bang, title) abort " {{{1 148 | " Edit an existing note or create a new one with the :Note command. 149 | let starttime = xolox#misc#timer#start() 150 | let title = xolox#misc#str#trim(a:title) 151 | if title != '' 152 | let fname = xolox#notes#select(title) 153 | if fname != '' 154 | call xolox#misc#msg#debug("notes.vim %s: Editing existing note: %s", g:xolox#notes#version, fname) 155 | execute 'edit' . a:bang fnameescape(fname) 156 | if !xolox#notes#unicode_enabled() && xolox#notes#is_shadow() 157 | call s:transcode_utf8_latin1() 158 | endif 159 | call xolox#notes#set_filetype() 160 | call xolox#misc#timer#stop('notes.vim %s: Opened note in %s.', g:xolox#notes#version, starttime) 161 | return 162 | endif 163 | else 164 | let title = 'New note' 165 | endif 166 | " At this point we're dealing with a new note. 167 | let fname = xolox#notes#title_to_fname(title) 168 | noautocmd execute 'edit' . a:bang fnameescape(fname) 169 | if line('$') == 1 && getline(1) == '' 170 | execute 'silent read' fnameescape(g:notes_new_note_template) 171 | 1delete 172 | if !xolox#notes#unicode_enabled() 173 | call s:transcode_utf8_latin1() 174 | endif 175 | setlocal nomodified 176 | endif 177 | if title != 'New note' 178 | call setline(1, title) 179 | endif 180 | call xolox#notes#set_filetype() 181 | doautocmd BufReadPost 182 | call xolox#misc#timer#stop('notes.vim %s: Started new note in %s.', g:xolox#notes#version, starttime) 183 | endfunction 184 | 185 | function! xolox#notes#check_sync_title() " {{{1 186 | " Check if the note's title and filename are out of sync. 187 | if g:notes_title_sync != 'no' && xolox#notes#buffer_is_note() && &buftype == '' 188 | let title = xolox#notes#current_title() 189 | let name_on_disk = xolox#misc#path#absolute(expand('%:p')) 190 | let name_from_title = xolox#notes#title_to_fname(title) 191 | if !xolox#misc#path#equals(name_on_disk, name_from_title) && !xolox#notes#is_shadow() 192 | call xolox#misc#msg#debug("notes.vim %s: Filename (%s) doesn't match note title (%s)", g:xolox#notes#version, name_on_disk, name_from_title) 193 | let action = g:notes_title_sync 194 | if action == 'prompt' && empty(name_from_title) 195 | " There's no point in prompting the user when there's only one choice. 196 | let action = 'change_title' 197 | elseif action == 'prompt' 198 | " Prompt the user what to do (if anything). First we perform a redraw 199 | " to make sure the note's content is visible (without this the Vim 200 | " window would be blank in my tests). 201 | redraw 202 | let message = "The note's title and filename do not correspond. What do you want to do?\n\n" 203 | let message .= "Current filename: " . s:sync_value(name_on_disk) . "\n" 204 | let message .= "Corresponding title: " . s:sync_value(xolox#notes#fname_to_title(name_on_disk)) . "\n\n" 205 | let message .= "Current title: " . s:sync_value(title) . "\n" 206 | let message .= "Corresponding filename: " . s:sync_value(xolox#notes#title_to_fname(title)) 207 | let choice = confirm(message, "Change &title\nRename &file\nDo ¬hing", 3, 'Question') 208 | if choice == 1 209 | let action = 'change_title' 210 | elseif choice == 2 211 | let action = 'rename_file' 212 | else 213 | " User chose to do nothing or 'd the prompt. 214 | return 215 | endif 216 | " Intentional fall through here :-) 217 | endif 218 | if action == 'change_title' 219 | let new_title = xolox#notes#fname_to_title(name_on_disk) 220 | call setline(1, new_title) 221 | setlocal modified 222 | call xolox#misc#msg#info("notes.vim %s: Changed note title to match filename.", g:xolox#notes#version) 223 | elseif action == 'rename_file' 224 | let new_fname = xolox#notes#title_to_fname(xolox#notes#current_title()) 225 | if rename(name_on_disk, new_fname) == 0 226 | execute 'edit' fnameescape(new_fname) 227 | call xolox#notes#set_filetype() 228 | call xolox#misc#msg#info("notes.vim %s: Renamed file to match note title.", g:xolox#notes#version) 229 | else 230 | call xolox#misc#msg#warn("notes.vim %s: Failed to rename file to match note title?!", g:xolox#notes#version) 231 | endif 232 | endif 233 | endif 234 | endif 235 | endfunction 236 | 237 | function! s:sync_value(s) 238 | let s = xolox#misc#str#trim(a:s) 239 | return empty(s) ? '(none)' : s 240 | endfunction 241 | 242 | function! xolox#notes#from_selection(bang, cmd) " {{{1 243 | " Edit a note with the visually selected text as title. 244 | let selection = s:get_visual_selection() 245 | if a:cmd != 'edit' | execute a:cmd | endif 246 | call xolox#notes#edit(a:bang, selection) 247 | endfunction 248 | 249 | function! s:get_visual_selection() 250 | " Why is this not a built-in Vim script function?! See also the question at 251 | " http://stackoverflow.com/questions/1533565 but note that none of the code 252 | " posted there worked for me so I wrote this function. 253 | let [lnum1, col1] = getpos("'<")[1:2] 254 | let [lnum2, col2] = getpos("'>")[1:2] 255 | let lines = getline(lnum1, lnum2) 256 | let lines[-1] = lines[-1][: col2 - (&selection == 'inclusive' ? 1 : 2)] 257 | let lines[0] = lines[0][col1 - 1:] 258 | return join(lines, ' ') 259 | endfunction 260 | 261 | function! xolox#notes#is_shadow() " {{{1 262 | " Check if the current note is a shadow note. 263 | return xolox#misc#path#equals(expand('%:p:h'), g:notes_shadowdir) 264 | endfunction 265 | 266 | function! xolox#notes#edit_shadow() " {{{1 267 | " People using latin1 don't like the UTF-8 curly quotes and bullets used in 268 | " the predefined notes because there are no equivalent characters in latin1, 269 | " resulting in the characters being shown as garbage or a question mark. 270 | execute 'edit' fnameescape(expand('')) 271 | if !xolox#notes#unicode_enabled() 272 | call s:transcode_utf8_latin1() 273 | endif 274 | call xolox#notes#set_filetype() 275 | endfunction 276 | 277 | function! s:transcode_utf8_latin1() 278 | let view = winsaveview() 279 | silent %s/\%xe2\%x80\%x98/`/eg 280 | silent %s/\%xe2\%x80\%x99/'/eg 281 | silent %s/\%xe2\%x80[\x9c\x9d]/"/eg 282 | silent %s/\%xe2\%x80\%xa2/\*/eg 283 | setlocal nomodified 284 | call winrestview(view) 285 | endfunction 286 | 287 | function! xolox#notes#unicode_enabled() " {{{1 288 | " Check if the `g:notes_unicode_enabled` option is set to true (1) and Vim's 289 | " encoding is set to UTF-8. 290 | return g:notes_unicode_enabled && &encoding == 'utf-8' 291 | endfunction 292 | 293 | function! xolox#notes#select(filter) " {{{1 294 | " Interactively select an existing note whose title contains {filter}. 295 | let notes = {} 296 | let filter = xolox#misc#str#trim(a:filter) 297 | for [fname, title] in items(xolox#notes#get_fnames_and_titles(1)) 298 | if title ==? filter 299 | call xolox#misc#msg#debug("notes.vim %s: Filter %s exactly matches note: %s", g:xolox#notes#version, string(filter), title) 300 | return fname 301 | elseif title =~? filter 302 | let notes[fname] = title 303 | endif 304 | endfor 305 | if len(notes) == 1 306 | let fname = keys(notes)[0] 307 | call xolox#misc#msg#debug("notes.vim %s: Filter %s matched one note: %s", g:xolox#notes#version, string(filter), fname) 308 | return fname 309 | elseif !empty(notes) 310 | call xolox#misc#msg#debug("notes.vim %s: Filter %s matched %i notes.", g:xolox#notes#version, string(filter), len(notes)) 311 | let choices = ['Please select a note:'] 312 | let values = [''] 313 | for fname in sort(keys(notes), 1) 314 | call add(choices, ' ' . len(choices) . ') ' . notes[fname]) 315 | call add(values, fname) 316 | endfor 317 | let choice = inputlist(choices) 318 | if choice > 0 && choice < len(choices) 319 | let fname = values[choice] 320 | call xolox#misc#msg#debug("notes.vim %s: User selected note: %s", g:xolox#notes#version, fname) 321 | return fname 322 | endif 323 | endif 324 | return '' 325 | endfunction 326 | 327 | function! xolox#notes#cmd_complete(arglead, cmdline, cursorpos) " {{{1 328 | " Vim's support for custom command completion is a real mess, specifically 329 | " the completion of multi word command arguments. With or without escaping 330 | " of spaces, arglead will only contain the last word in the arguments passed 331 | " to :Note, and worse, the completion candidates we return only replace the 332 | " last word on the command line. 333 | " XXX This isn't a real command line parser; it will break on quoted pipes. 334 | let cmdline = split(a:cmdline, '\\\@ after the argument) we can select the 341 | " completion candidates using a substring match on the first argument 342 | " instead of a prefix match (I consider this to be more user friendly). 343 | let pattern = xolox#misc#escape#pattern(cmdargs) 344 | call filter(titles, "v:val =~ pattern") 345 | else 346 | " If we are completing more than one argument or the user has typed 347 | " after the first argument, we must select completion 348 | " candidates using a prefix match on all arguments because Vim doesn't 349 | " support replacing previous arguments (selecting completion candidates 350 | " using a substring match would result in invalid note titles). 351 | let pattern = '^' . xolox#misc#escape#pattern(cmdargs) 352 | call filter(titles, "v:val =~ pattern") 353 | " Remove the given arguments as the prefix of every completion candidate 354 | " because Vim refuses to replace previous arguments. 355 | let prevargs = '^' . xolox#misc#escape#pattern(cmdargs[0 : len(cmdargs) - len(a:arglead) - 1]) 356 | call map(titles, 'substitute(v:val, prevargs, "", "")') 357 | endif 358 | " Sort from shortest to longest as a rough approximation of 359 | " sorting by similarity to the word that's being completed. 360 | return reverse(sort(titles, 's:sort_longest_to_shortest')) 361 | endfunction 362 | 363 | function! xolox#notes#user_complete(findstart, base) " {{{1 364 | " Completion of note titles with Control-X Control-U. 365 | if a:findstart 366 | let line = getline('.')[0 : col('.') - 2] 367 | let words = split(line) 368 | if !empty(words) 369 | return col('.') - len(words[-1]) - 1 370 | else 371 | return -1 372 | endif 373 | else 374 | let titles = xolox#notes#get_titles(1) 375 | if !empty(a:base) 376 | let pattern = xolox#misc#escape#pattern(a:base) 377 | call filter(titles, 'v:val =~ pattern') 378 | endif 379 | return titles 380 | endif 381 | endfunction 382 | 383 | function! xolox#notes#omni_complete(findstart, base) " {{{1 384 | " Completion of tag names with Control-X Control-O. 385 | if a:findstart 386 | " For now we assume omni completion was triggered by the mapping for 387 | " automatic tag completion. Eventually it might be nice to check for a 388 | " leading "@" here and otherwise make it complete e.g. note names, so that 389 | " there's only one way to complete inside notes and the plug-in is smart 390 | " enough to know what the user wants to complete :-) 391 | return col('.') 392 | else 393 | return sort(keys(xolox#notes#tags#load_index()), 1) 394 | endif 395 | endfunction 396 | 397 | function! xolox#notes#auto_complete_tags() " {{{1 398 | " Automatic completion of tags when the user types "@". 399 | if !xolox#notes#currently_inside_snippet() 400 | return "@\\" 401 | endif 402 | return "@" 403 | endfunction 404 | 405 | function! xolox#notes#save() abort " {{{1 406 | " When the current note's title is changed, automatically rename the file. 407 | if xolox#notes#filetype_is_note(&ft) 408 | let title = xolox#notes#current_title() 409 | let oldpath = expand('%:p') 410 | let newpath = xolox#notes#title_to_fname(title) 411 | if newpath == '' 412 | echoerr "Invalid note title" 413 | return 414 | endif 415 | " Trigger the BufWritePre automatic command event because it provides 416 | " a very unobtrusive way for users to extend the vim-notes plug-in. 417 | execute 'doautocmd BufWritePre' fnameescape(newpath) 418 | " Actually save the user's buffer to the file. 419 | let bang = v:cmdbang ? '!' : '' 420 | execute 'saveas' . bang fnameescape(newpath) 421 | " XXX If {oldpath} and {newpath} end up pointing to the same file on disk 422 | " yet xolox#misc#path#equals() doesn't catch this, we might end up 423 | " deleting the user's one and only note! One way to circumvent this 424 | " potential problem is to first delete the old note and then save the new 425 | " note. The problem with this approach is that :saveas might fail in which 426 | " case we've already deleted the old note... 427 | if !xolox#misc#path#equals(oldpath, newpath) 428 | if !filereadable(newpath) 429 | let message = "The notes plug-in tried to rename your note but failed to create %s so won't delete %s or you could lose your note! This should never happen... If you don't mind me borrowing some of your time, please contact me at peter@peterodding.com and include the old and new filename so that I can try to reproduce the issue. Thanks!" 430 | call confirm(printf(message, string(newpath), string(oldpath))) 431 | return 432 | endif 433 | call delete(oldpath) 434 | endif 435 | " Update the tags index on disk and in-memory. 436 | call xolox#notes#tags#forget_note(xolox#notes#fname_to_title(oldpath)) 437 | call xolox#notes#tags#scan_note(title, join(getline(1, '$'), "\n")) 438 | call xolox#notes#tags#save_index() 439 | " Update in-memory list of all notes. 440 | call xolox#notes#cache_del(oldpath) 441 | call xolox#notes#cache_add(newpath, title) 442 | " Trigger the BufWritePost automatic command event because it provides 443 | " a very unobtrusive way for users to extend the vim-notes plug-in. 444 | execute 'doautocmd BufWritePost' fnameescape(newpath) 445 | endif 446 | endfunction 447 | 448 | function! xolox#notes#delete(bang, title) " {{{1 449 | " Delete the note {title} and close the associated buffer & window. 450 | " If no {title} is given the current note is deleted. 451 | let title = xolox#misc#str#trim(a:title) 452 | if title == '' 453 | " Try the current buffer. 454 | let title = xolox#notes#fname_to_title(expand('%:p')) 455 | endif 456 | if !xolox#notes#exists(title) 457 | call xolox#misc#msg#warn("notes.vim %s: Failed to delete %s! (not a note)", g:xolox#notes#version, expand('%:p')) 458 | else 459 | let filename = xolox#notes#title_to_fname(title) 460 | if filereadable(filename) && delete(filename) 461 | call xolox#misc#msg#warn("notes.vim %s: Failed to delete %s!", g:xolox#notes#version, filename) 462 | else 463 | call xolox#notes#cache_del(filename) 464 | let buffer_number = bufnr(filename) 465 | if buffer_number >= 0 466 | execute 'bdelete' . a:bang . ' ' . buffer_number 467 | endif 468 | endif 469 | endif 470 | endfunction 471 | 472 | function! xolox#notes#search(bang, input) " {{{1 473 | " Search all notes for the pattern or keywords {input} (current word if none given). 474 | let starttime = xolox#misc#timer#start() 475 | let input = a:input 476 | if input == '' 477 | let input = s:tag_under_cursor() 478 | if input == '' 479 | call xolox#misc#msg#warn("notes.vim %s: No string under cursor", g:xolox#notes#version) 480 | return 481 | endif 482 | endif 483 | if input =~ '^/.\+/$' 484 | call xolox#misc#msg#debug("notes.vim %s: Performing pattern search (%s) ..", g:xolox#notes#version, input) 485 | call s:internal_search(a:bang, input, '', '') 486 | call s:set_quickfix_title([], input) 487 | else 488 | let keywords = split(input) 489 | let all_keywords = s:match_all_keywords(keywords) 490 | let any_keyword = s:match_any_keyword(keywords) 491 | call xolox#misc#msg#debug("notes.vim %s: Performing keyword search (%s) ..", g:xolox#notes#version, input) 492 | call s:internal_search(a:bang, all_keywords, input, any_keyword) 493 | if &buftype == 'quickfix' 494 | " Enable line wrapping in the quick-fix window. 495 | setlocal wrap 496 | " Resize the quick-fix window to 1/3 of the screen height. 497 | let max_height = &lines / 3 498 | execute 'resize' max_height 499 | " Make it smaller if the content doesn't fill the window. 500 | normal G$ 501 | let preferred_height = winline() 502 | execute 'resize' min([max_height, preferred_height]) 503 | normal gg 504 | call s:set_quickfix_title(keywords, '') 505 | endif 506 | endif 507 | call xolox#misc#timer#stop("notes.vim %s: Searched notes in %s.", g:xolox#notes#version, starttime) 508 | endfunction 509 | 510 | function! s:tag_under_cursor() " {{{2 511 | " Get the word or @tag under the text cursor. 512 | try 513 | let isk_save = &isk 514 | set iskeyword+=@-@ 515 | return expand('') 516 | finally 517 | let &isk = isk_save 518 | endtry 519 | endfunction 520 | 521 | function! s:match_all_keywords(keywords) " {{{2 522 | " Create a regex that matches when a file contains all {keywords}. 523 | let results = copy(a:keywords) 524 | call map(results, '''\_^\_.*'' . xolox#misc#escape#pattern(v:val)') 525 | return '/' . escape(join(results, '\&'), '/') . '/' 526 | endfunction 527 | 528 | function! s:match_any_keyword(keywords) " {{{2 529 | " Create a regex that matches every occurrence of all {keywords}. 530 | let results = copy(a:keywords) 531 | call map(results, 'xolox#misc#escape#pattern(v:val)') 532 | return '/' . escape(join(results, '\|'), '/') . '/' 533 | endfunction 534 | 535 | function! s:set_quickfix_title(keywords, pattern) " {{{2 536 | " Set the title of the quick-fix window. 537 | if &buftype == 'quickfix' 538 | let num_notes = len(xolox#misc#list#unique(map(getqflist(), 'v:val["bufnr"]'))) 539 | if len(a:keywords) > 0 540 | let keywords = map(copy(a:keywords), '"`" . v:val . "''"') 541 | let w:quickfix_title = printf('Found %i note%s containing the word%s %s', 542 | \ num_notes, num_notes == 1 ? '' : 's', 543 | \ len(keywords) == 1 ? '' : 's', 544 | \ len(keywords) > 1 ? (join(keywords[0:-2], ', ') . ' and ' . keywords[-1]) : keywords[0]) 545 | else 546 | let w:quickfix_title = printf('Found %i note%s containing the pattern %s', 547 | \ num_notes, num_notes == 1 ? '' : 's', 548 | \ a:pattern) 549 | endif 550 | endif 551 | endfunction 552 | 553 | function! xolox#notes#related(bang) " {{{1 554 | " Find all notes related to the current note or file. 555 | let starttime = xolox#misc#timer#start() 556 | let bufname = bufname('%') 557 | if bufname == '' 558 | call xolox#misc#msg#warn("notes.vim %s: :RelatedNotes only works on named buffers!", g:xolox#notes#version) 559 | else 560 | let filename = xolox#misc#path#absolute(bufname) 561 | if xolox#notes#buffer_is_note() 562 | let keywords = xolox#notes#current_title() 563 | let pattern = '\<' . s:words_to_pattern(keywords) . '\>' 564 | else 565 | let pattern = s:words_to_pattern(filename) 566 | let keywords = filename 567 | if filename[0 : len($HOME)-1] == $HOME 568 | let relative = filename[len($HOME) + 1 : -1] 569 | let pattern = '\(' . pattern . '\|\~/' . s:words_to_pattern(relative) . '\)' 570 | let keywords = relative 571 | endif 572 | endif 573 | let pattern = '/' . escape(pattern, '/') . '/' 574 | let friendly_path = fnamemodify(filename, ':~') 575 | try 576 | call s:internal_search(a:bang, pattern, keywords, '') 577 | if &buftype == 'quickfix' 578 | let w:quickfix_title = 'Notes related to ' . friendly_path 579 | endif 580 | catch /^Vim\%((\a\+)\)\=:E480/ 581 | call xolox#misc#msg#warn("notes.vim %s: No related notes found for %s", g:xolox#notes#version, friendly_path) 582 | endtry 583 | endif 584 | call xolox#misc#timer#stop("notes.vim %s: Found related notes in %s.", g:xolox#notes#version, starttime) 585 | endfunction 586 | 587 | " Miscellaneous functions. {{{1 588 | 589 | function! xolox#notes#find_directories(include_shadow_directory) " {{{2 590 | " Generate a list of absolute pathnames of all notes directories. 591 | let directories = copy(g:notes_directories) 592 | " Add the shadow directory? 593 | if a:include_shadow_directory 594 | call add(directories, g:notes_shadowdir) 595 | endif 596 | " Return the expanded directory pathnames. 597 | return map(directories, 'expand(v:val)') 598 | endfunction 599 | 600 | function! xolox#notes#set_filetype() " {{{2 601 | " Load the notes file type if not already loaded. 602 | if &filetype != 'notes' 603 | " Change the file type. 604 | setlocal filetype=notes 605 | elseif synID(1, 1, 0) == 0 606 | " Load the syntax. When you execute :RecentNotes, switch to a different 607 | " buffer and then return to the buffer created by :RecentNotes, it will 608 | " have lost its syntax highlighting. The following line of code solves 609 | " this problem. We don't explicitly set the syntax to 'notes' so that we 610 | " preserve dot separated composed values. 611 | let &syntax = &syntax 612 | endif 613 | endfunction 614 | 615 | function! xolox#notes#swaphack() " {{{2 616 | " Selectively ignore the dreaded E325 interactive prompt. 617 | if exists('s:swaphack_enabled') 618 | let v:swapchoice = 'o' 619 | endif 620 | endfunction 621 | 622 | function! xolox#notes#autocmd_pattern(directory, use_extension) " {{{2 623 | " Generate a normalized automatic command pattern. First we resolve the path 624 | " to the directory with notes (eliminating any symbolic links) so that the 625 | " automatic command also applies to symbolic links pointing to notes (Vim 626 | " matches filename patterns in automatic commands after resolving 627 | " filenames). 628 | let directory = xolox#misc#path#absolute(a:directory) 629 | " On Windows we have to replace backslashes with forward slashes, otherwise 630 | " the automatic command will never trigger! This has to happen before we 631 | " make the fnameescape() call. 632 | if xolox#misc#os#is_win() 633 | let directory = substitute(directory, '\\', '/', 'g') 634 | endif 635 | " Escape the directory but not the trailing "*". 636 | let pattern = fnameescape(directory) . '/*' 637 | if a:use_extension && !empty(g:notes_suffix) 638 | let pattern .= g:notes_suffix 639 | endif 640 | " On Windows the pattern won't match if it contains repeating slashes. 641 | return substitute(pattern, '/\+', '/', 'g') 642 | endfunction 643 | 644 | function! xolox#notes#filetype_is_note(ft) " {{{2 645 | " Check whether the given file type value refers to the notes.vim plug-in. 646 | return index(split(a:ft, '\.'), 'notes') >= 0 647 | endfunction 648 | 649 | function! xolox#notes#buffer_is_note() " {{{2 650 | " Check whether the current buffer is a note (with the correct file type and path). 651 | let buffer_directory = expand('%:p:h') 652 | if xolox#notes#filetype_is_note(&ft) 653 | for directory in xolox#notes#find_directories(1) 654 | if xolox#misc#path#starts_with(buffer_directory, directory) 655 | return 1 656 | endif 657 | endfor 658 | endif 659 | endfunction 660 | 661 | function! xolox#notes#current_title() " {{{2 662 | " Get the title of the current note. 663 | let title = getline(1) 664 | let trimmed = xolox#misc#str#trim(title) 665 | if title != trimmed 666 | call setline(1, trimmed) 667 | endif 668 | return trimmed 669 | endfunction 670 | 671 | function! xolox#notes#friendly_date(time) " {{{2 672 | " Format a date as a human readable string. 673 | let format = '%A, %B %d, %Y' 674 | let today = strftime(format, localtime()) 675 | let yesterday = strftime(format, localtime() - 60*60*24) 676 | let datestr = strftime(format, a:time) 677 | if datestr == today 678 | return "today" 679 | elseif datestr == yesterday 680 | return "yesterday" 681 | else 682 | return datestr 683 | endif 684 | endfunction 685 | 686 | function! s:internal_search(bang, pattern, keywords, phase2) " {{{2 687 | " Search notes for {pattern} regex, try to accelerate with {keywords} search. 688 | let bufnr_save = bufnr('%') 689 | let pattern = a:pattern 690 | silent cclose 691 | " Find all notes matching the given keywords or regex. 692 | let notes = [] 693 | let phase2_needed = 1 694 | if a:keywords != '' && s:run_scanner(a:keywords, notes) 695 | call xolox#misc#msg#debug("notes.vim %s: Skipping phase 1 search (performed using Python script) ..", g:xolox#notes#version) 696 | if a:phase2 != '' 697 | let pattern = a:phase2 698 | endif 699 | else 700 | call xolox#misc#msg#debug("notes.vim %s: Performing phase 1 search to gather matching notes ..", g:xolox#notes#version) 701 | call s:vimgrep_wrapper(a:bang, a:pattern, xolox#notes#get_fnames(0)) 702 | let notes = s:qflist_to_filenames() 703 | if a:phase2 != '' 704 | let pattern = a:phase2 705 | else 706 | let phase2_needed = 0 707 | endif 708 | endif 709 | if empty(notes) 710 | call xolox#misc#msg#warn("notes.vim %s: No matches", g:xolox#notes#version) 711 | return 712 | endif 713 | " If we performed a keyword search using the scanner.py script we need to 714 | " run :vimgrep to populate the quick-fix list. If we're emulating keyword 715 | " search using :vimgrep we need to run :vimgrep another time to get the 716 | " quick-fix list in the right format :-| 717 | if phase2_needed 718 | call setqflist([]) 719 | call xolox#misc#msg#debug("notes.vim %s: Performing phase 2 search to populate quick-fix window ..", g:xolox#notes#version) 720 | call s:vimgrep_wrapper(a:bang, pattern, notes) 721 | if !empty(notes) && empty(getqflist()) 722 | throw "Failed to populate quick-fix window! Looks like you're being bitten by this bug: https://github.com/xolox/vim-notes/issues/53" 723 | endif 724 | endif 725 | if a:bang == '' && bufnr('%') != bufnr_save 726 | " If :vimgrep opens the first matching file while &eventignore is still 727 | " set the file will be opened without activating a file type plug-in or 728 | " syntax script. Here's a workaround: 729 | doautocmd filetypedetect BufRead 730 | endif 731 | silent cwindow 732 | if &buftype == 'quickfix' 733 | execute 'match IncSearch' (&ignorecase ? substitute(pattern, '^/', '/\\c', '') : pattern) 734 | endif 735 | endfunction 736 | 737 | function! s:vimgrep_wrapper(bang, pattern, files) " {{{2 738 | " Search for {pattern} in {files} using :vimgrep. 739 | let starttime = xolox#misc#timer#start() 740 | let args = map(copy(a:files), 'fnameescape(v:val)') 741 | call insert(args, a:pattern . 'j') 742 | let s:swaphack_enabled = 1 743 | let ei_save = &eventignore 744 | try 745 | set eventignore=syntax,bufread 746 | let command = printf('vimgrep%s %s', a:bang, join(args)) 747 | call xolox#misc#msg#debug("notes.vim %s: Populating quick-fix window using command: %s", g:xolox#notes#version, command) 748 | execute command 749 | call xolox#misc#timer#stop("notes.vim %s: Populated quick-fix window in %s.", g:xolox#notes#version, starttime) 750 | finally 751 | let &eventignore = ei_save 752 | unlet s:swaphack_enabled 753 | endtry 754 | endfunction 755 | 756 | function! s:qflist_to_filenames() " {{{2 757 | " Get filenames of matched notes from quick-fix list. 758 | let names = {} 759 | for entry in getqflist() 760 | let names[xolox#misc#path#absolute(bufname(entry.bufnr))] = 1 761 | endfor 762 | return keys(names) 763 | endfunction 764 | 765 | function! s:run_scanner(keywords, matches) " {{{2 766 | " Try to run scanner.py script to find notes matching {keywords}. 767 | call xolox#misc#msg#info("notes.vim %s: Searching notes using keyword index ..", g:xolox#notes#version) 768 | let [success, notes] = s:python_command(a:keywords) 769 | if success 770 | call xolox#misc#msg#debug("notes.vim %s: Search script reported %i matching note%s.", g:xolox#notes#version, len(notes), len(notes) == 1 ? '' : 's') 771 | call extend(a:matches, notes) 772 | return 1 773 | endif 774 | endfunction 775 | 776 | function! xolox#notes#keyword_complete(arglead, cmdline, cursorpos) " {{{2 777 | " Search keyword completion for the :SearchNotes command. 778 | call inputsave() 779 | let [success, keywords] = s:python_command('--list=' . a:arglead) 780 | call inputrestore() 781 | return keywords 782 | endfunction 783 | 784 | function! s:python_command(...) " {{{2 785 | " Vim function to interface with the "search-notes.py" script. 786 | let script = xolox#misc#path#absolute(g:notes_indexscript) 787 | let python = executable('python2') ? 'python2' : 'python' 788 | let output = [] 789 | let success = 0 790 | if !(executable(python) && filereadable(script)) 791 | call xolox#misc#msg#debug("notes.vim %s: We can't execute the %s script!", g:xolox#notes#version, script) 792 | else 793 | let options = ['--database', g:notes_indexfile] 794 | if &ignorecase 795 | call add(options, '--ignore-case') 796 | endif 797 | for directory in xolox#notes#find_directories(0) 798 | call extend(options, ['--notes', directory]) 799 | endfor 800 | let arguments = map([script] + options + a:000, 'xolox#misc#escape#shell(v:val)') 801 | let command = join([python] + arguments) 802 | call xolox#misc#msg#debug("notes.vim %s: Executing external command %s", g:xolox#notes#version, command) 803 | if !filereadable(xolox#misc#path#absolute(g:notes_indexfile)) 804 | call xolox#misc#msg#info("notes.vim %s: Building keyword index (this might take a while) ..", g:xolox#notes#version) 805 | endif 806 | let result = xolox#misc#os#exec({'command': command, 'check': 0}) 807 | if result['exit_code'] != 0 808 | call xolox#misc#msg#warn("notes.vim %s: Search script failed! Context: %s", g:xolox#notes#version, string(result)) 809 | else 810 | let lines = result['stdout'] 811 | call xolox#misc#msg#debug("notes.vim %s: Search script output (raw): %s", g:xolox#notes#version, string(lines)) 812 | if !empty(lines) && lines[0] == 'Python works fine!' 813 | let output = lines[1:] 814 | let success = 1 815 | call xolox#misc#msg#debug("notes.vim %s: Search script output (processed): %s", g:xolox#notes#version, string(output)) 816 | else 817 | call xolox#misc#msg#warn("notes.vim %s: Search script returned invalid output :-(", g:xolox#notes#version) 818 | endif 819 | endif 820 | endif 821 | return [success, output] 822 | endfunction 823 | 824 | " Getters for filenames & titles of existing notes. {{{2 825 | 826 | if !exists('s:cache_mtime') 827 | let s:have_cached_names = 0 828 | let s:have_cached_titles = 0 829 | let s:have_cached_items = 0 830 | let s:cached_fnames = [] 831 | let s:cached_titles = [] 832 | let s:cached_pairs = {} 833 | let s:cache_mtime = 0 834 | let s:shadow_notes = ['New note', 'Note taking commands', 'Note taking syntax'] 835 | endif 836 | 837 | function! xolox#notes#get_fnames(include_shadow_notes) " {{{3 838 | " Get list with filenames of all existing notes. 839 | if !s:have_cached_names 840 | let starttime = xolox#misc#timer#start() 841 | for directory in xolox#notes#find_directories(0) 842 | let pattern = xolox#misc#path#merge(directory, '**') 843 | let listing = glob(xolox#misc#path#absolute(pattern)) 844 | call extend(s:cached_fnames, filter(split(listing, '\n'), 'filereadable(v:val)')) 845 | endfor 846 | let s:have_cached_names = 1 847 | call xolox#misc#timer#stop('notes.vim %s: Cached note filenames in %s.', g:xolox#notes#version, starttime) 848 | endif 849 | let fnames = copy(s:cached_fnames) 850 | if a:include_shadow_notes 851 | for title in s:shadow_notes 852 | call add(fnames, xolox#misc#path#merge(g:notes_shadowdir, title)) 853 | endfor 854 | endif 855 | return fnames 856 | endfunction 857 | 858 | function! xolox#notes#get_titles(include_shadow_notes) " {{{3 859 | " Get list with titles of all existing notes. 860 | if !s:have_cached_titles 861 | let starttime = xolox#misc#timer#start() 862 | for filename in xolox#notes#get_fnames(0) 863 | call add(s:cached_titles, xolox#notes#fname_to_title(filename)) 864 | endfor 865 | let s:have_cached_titles = 1 866 | call xolox#misc#timer#stop('notes.vim %s: Cached note titles in %s.', g:xolox#notes#version, starttime) 867 | endif 868 | let titles = copy(s:cached_titles) 869 | if a:include_shadow_notes 870 | call extend(titles, s:shadow_notes) 871 | endif 872 | return titles 873 | endfunction 874 | 875 | function! xolox#notes#exists(title) " {{{3 876 | " Return true if the note {title} exists. 877 | return index(xolox#notes#get_titles(0), a:title, 0, xolox#misc#os#is_win()) >= 0 878 | endfunction 879 | 880 | function! xolox#notes#get_fnames_and_titles(include_shadow_notes) " {{{3 881 | " Get dictionary of filename => title pairs of all existing notes. 882 | if !s:have_cached_items 883 | let starttime = xolox#misc#timer#start() 884 | let fnames = xolox#notes#get_fnames(0) 885 | let titles = xolox#notes#get_titles(0) 886 | let limit = len(fnames) 887 | let index = 0 888 | while index < limit 889 | let s:cached_pairs[fnames[index]] = titles[index] 890 | let index += 1 891 | endwhile 892 | let s:have_cached_items = 1 893 | call xolox#misc#timer#stop('notes.vim %s: Cached note filenames and titles in %s.', g:xolox#notes#version, starttime) 894 | endif 895 | let pairs = copy(s:cached_pairs) 896 | if a:include_shadow_notes 897 | for title in s:shadow_notes 898 | let fname = xolox#misc#path#merge(g:notes_shadowdir, title) 899 | let pairs[fname] = title 900 | endfor 901 | endif 902 | return pairs 903 | endfunction 904 | 905 | function! xolox#notes#fname_to_title(filename) " {{{3 906 | " Convert absolute note {filename} to title. 907 | let fname = a:filename 908 | " Strip suffix? 909 | if fname[-len(g:notes_suffix):] == g:notes_suffix 910 | let fname = fname[0:-len(g:notes_suffix)-1] 911 | endif 912 | " Strip directory path. 913 | let fname = fnamemodify(fname, ':t') 914 | " Decode special characters. 915 | return xolox#misc#path#decode(fname) 916 | endfunction 917 | 918 | function! xolox#notes#title_to_fname(title) " {{{3 919 | " Convert note {title} to absolute filename. 920 | let filename = xolox#misc#path#encode(a:title) 921 | if filename != '' 922 | let directory = xolox#notes#select_directory() 923 | let pathname = xolox#misc#path#merge(directory, filename . g:notes_suffix) 924 | return xolox#misc#path#absolute(pathname) 925 | endif 926 | return '' 927 | endfunction 928 | 929 | function! xolox#notes#select_directory() " {{{3 930 | " Pick the best suited directory for creating a new note. 931 | let buffer_directory = expand('%:p:h') 932 | let notes_directories = xolox#notes#find_directories(0) 933 | for directory in notes_directories 934 | if xolox#misc#path#starts_with(buffer_directory, directory) 935 | return buffer_directory 936 | endif 937 | endfor 938 | return notes_directories[0] 939 | endfunction 940 | 941 | function! xolox#notes#cache_add(filename, title) " {{{3 942 | " Add {filename} and {title} of new note to cache. 943 | let filename = xolox#misc#path#absolute(a:filename) 944 | if index(s:cached_fnames, filename) == -1 945 | call add(s:cached_fnames, filename) 946 | if s:have_cached_titles 947 | call add(s:cached_titles, a:title) 948 | endif 949 | if s:have_cached_items 950 | let s:cached_pairs[filename] = a:title 951 | endif 952 | let s:cache_mtime = localtime() 953 | endif 954 | endfunction 955 | 956 | function! xolox#notes#cache_del(filename) " {{{3 957 | " Delete {filename} from cache. 958 | let filename = xolox#misc#path#absolute(a:filename) 959 | let index = index(s:cached_fnames, filename) 960 | if index >= 0 961 | call remove(s:cached_fnames, index) 962 | if s:have_cached_titles 963 | call remove(s:cached_titles, index) 964 | endif 965 | if s:have_cached_items 966 | call remove(s:cached_pairs, filename) 967 | endif 968 | let s:cache_mtime = localtime() 969 | endif 970 | endfunction 971 | 972 | function! xolox#notes#unload_from_cache() " {{{3 973 | " Forget deleted notes automatically (called by "BufUnload" automatic command). 974 | let bufname = expand(':p') 975 | if !filereadable(bufname) 976 | call xolox#notes#cache_del(bufname) 977 | endif 978 | endfunction 979 | 980 | " Functions called by the file type plug-in and syntax script. {{{2 981 | 982 | function! xolox#notes#insert_ruler() " {{{3 983 | " Insert horizontal ruler delimited by empty lines. 984 | let lnum = line('.') 985 | if getline(lnum) =~ '\S' && getline(lnum + 1) !~ '\S' 986 | let lnum += 1 987 | endif 988 | let line1 = prevnonblank(lnum) 989 | let line2 = nextnonblank(lnum) 990 | if line1 < lnum && line2 > lnum 991 | execute printf('%i,%idelete', line1 + 1, line2 - 1) 992 | endif 993 | call append(line1, ['', g:notes_ruler_text, '']) 994 | endfunction 995 | 996 | function! xolox#notes#insert_quote(chr) " {{{3 997 | " XXX When I pass the below string constants as arguments from the file type 998 | " plug-in the resulting strings contain mojibake (UTF-8 interpreted as 999 | " latin1?) even if both scripts contain a UTF-8 BOM! Maybe a bug in Vim?! 1000 | if g:notes_smart_quotes && !xolox#notes#currently_inside_snippet() 1001 | if xolox#notes#unicode_enabled() 1002 | let [open_quote, close_quote] = (a:chr == "'") ? ['‘', '’'] : ['“', '”'] 1003 | else 1004 | let [open_quote, close_quote] = (a:chr == "'") ? ['`', "'"] : ['"', '"'] 1005 | endif 1006 | return getline('.')[col('.')-2] =~ '[^\t (]$' ? close_quote : open_quote 1007 | endif 1008 | return a:chr 1009 | endfunction 1010 | 1011 | function! xolox#notes#insert_em_dash() " {{{3 1012 | " Change double-dash (--) to em-dash (—) as it is typed. 1013 | return (g:notes_smart_quotes && xolox#notes#unicode_enabled() && !xolox#notes#currently_inside_snippet()) ? '—' : '--' 1014 | endfunction 1015 | 1016 | function! xolox#notes#insert_left_arrow() " {{{3 1017 | " Change ASCII left arrow (<-) to Unicode arrow (←) as it is typed. 1018 | return (g:notes_smart_quotes && xolox#notes#unicode_enabled() && !xolox#notes#currently_inside_snippet()) ? '←' : "<-" 1019 | endfunction 1020 | 1021 | function! xolox#notes#insert_right_arrow() " {{{3 1022 | " Change ASCII right arrow (->) to Unicode arrow (→) as it is typed. 1023 | return (g:notes_smart_quotes && xolox#notes#unicode_enabled() && !xolox#notes#currently_inside_snippet()) ? '→' : '->' 1024 | endfunction 1025 | 1026 | function! xolox#notes#insert_bidi_arrow() " {{{3 1027 | " Change bidirectional ASCII arrow (->) to Unicode arrow (→) as it is typed. 1028 | return (g:notes_smart_quotes && xolox#notes#unicode_enabled() && !xolox#notes#currently_inside_snippet()) ? '↔' : "<->" 1029 | endfunction 1030 | 1031 | function! xolox#notes#insert_bullet(chr) " {{{3 1032 | " Insert a UTF-8 list bullet when the user types "*". 1033 | if !xolox#notes#currently_inside_snippet() 1034 | if getline('.')[0 : max([0, col('.') - 2])] =~ '^\s*$' 1035 | return xolox#notes#get_bullet(a:chr) 1036 | endif 1037 | endif 1038 | return a:chr 1039 | endfunction 1040 | 1041 | function! xolox#notes#get_bullet(chr) 1042 | return xolox#notes#unicode_enabled() ? '•' : a:chr 1043 | endfunction 1044 | 1045 | function! xolox#notes#indent_list(direction, line1, line2) " {{{3 1046 | " Change indent of list items from {line1} to {line2} using {command}. 1047 | let indentstr = repeat(' ', &tabstop) 1048 | if a:line1 == a:line2 && getline(a:line1) == '' 1049 | call setline(a:line1, indentstr) 1050 | else 1051 | " Regex to match a leading bullet. 1052 | let leading_bullet = xolox#notes#leading_bullet_pattern() 1053 | for lnum in range(a:line1, a:line2) 1054 | let line = getline(lnum) 1055 | " Calculate new nesting level, should not result in < 0. 1056 | let level = max([0, xolox#notes#get_list_level(line) + a:direction]) 1057 | if a:direction == 1 1058 | " Indent the line. 1059 | let line = indentstr . line 1060 | else 1061 | " Unindent the line. 1062 | let line = substitute(line, '^' . indentstr, '', '') 1063 | endif 1064 | " Replace the bullet. 1065 | let bullet = g:notes_list_bullets[level % len(g:notes_list_bullets)] 1066 | call setline(lnum, substitute(line, leading_bullet, xolox#misc#escape#substitute(bullet), '')) 1067 | endfor 1068 | " Regex to match a trailing bullet. 1069 | if getline('.') =~ xolox#notes#trailing_bullet_pattern() 1070 | " Restore trailing space after list bullet. 1071 | call setline('.', getline('.') . ' ') 1072 | endif 1073 | endif 1074 | normal $ 1075 | endfunction 1076 | 1077 | function! xolox#notes#leading_bullet_pattern() 1078 | " Return a regular expression pattern that matches any leading list bullet. 1079 | let escaped_bullets = copy(g:notes_list_bullets) 1080 | call map(escaped_bullets, 'xolox#misc#escape#pattern(v:val)') 1081 | return '\(\_^\s*\)\@<=\(' . join(escaped_bullets, '\|') . '\)' 1082 | endfunction 1083 | 1084 | function! xolox#notes#trailing_bullet_pattern() 1085 | " Return a regular expression pattern that matches any trailing list bullet. 1086 | let escaped_bullets = copy(g:notes_list_bullets) 1087 | call map(escaped_bullets, 'xolox#misc#escape#pattern(v:val)') 1088 | return '\(' . join(escaped_bullets, '\|') . '\|\*\)$' 1089 | endfunction 1090 | 1091 | function! xolox#notes#get_comments_option() 1092 | " Get the value for the &comments option including user defined list bullets. 1093 | let items = copy(g:notes_list_bullets) 1094 | call map(items, '": " . v:val . " "') 1095 | call add(items, ':> ') " <- e-mail style block quotes. 1096 | return join(items, ',') 1097 | endfunction 1098 | 1099 | function! xolox#notes#get_list_level(line) 1100 | " Get the nesting level of the list item on the given line. This will only 1101 | " work with the list item indentation style expected by the notes plug-in 1102 | " (that is, top level list items are indented with one space, each nested 1103 | " level below that is indented by pairs of three spaces). 1104 | return (len(matchstr(a:line, '^\s*')) - 1) / 3 1105 | endfunction 1106 | 1107 | function! xolox#notes#cleanup_list() " {{{3 1108 | " Automatically remove empty list items on Enter. 1109 | if getline('.') =~ (xolox#notes#leading_bullet_pattern() . '\s*$') 1110 | let s:sol_save = &startofline 1111 | setlocal nostartofline " <- so that clears the complete line 1112 | return "\0\d$\o" 1113 | else 1114 | if exists('s:sol_save') 1115 | let &l:startofline = s:sol_save 1116 | unlet s:sol_save 1117 | endif 1118 | return "\" 1119 | endif 1120 | endfunction 1121 | 1122 | function! xolox#notes#refresh_syntax() " {{{3 1123 | " Update syntax highlighting of note names and code blocks. 1124 | if xolox#notes#filetype_is_note(&ft) && line('$') > 1 1125 | let starttime = xolox#misc#timer#start() 1126 | call xolox#notes#highlight_names(0) 1127 | call xolox#notes#highlight_sources(0) 1128 | call xolox#misc#timer#stop("notes.vim %s: Refreshed highlighting in %s.", g:xolox#notes#version, starttime) 1129 | endif 1130 | endfunction 1131 | 1132 | function! xolox#notes#highlight_names(force) " {{{3 1133 | " Highlight the names of all notes as "notesName" (linked to "Underlined"). 1134 | if a:force || !(exists('b:notes_names_last_highlighted') && b:notes_names_last_highlighted > s:cache_mtime) 1135 | let starttime = xolox#misc#timer#start() 1136 | let current_note = xolox#notes#current_title() 1137 | let titles = filter(xolox#notes#get_titles(1), '!empty(v:val) && v:val != current_note') 1138 | call map(titles, 's:words_to_pattern(v:val)') 1139 | call sort(titles, 's:sort_longest_to_shortest') 1140 | if hlexists('notesName') 1141 | syntax clear notesName 1142 | endif 1143 | let pattern = '\%(' . escape(join(titles, '\|'), '/') . '\)' 1144 | if g:notes_word_boundaries 1145 | let pattern = '\<' . pattern . '\>' 1146 | endif 1147 | execute 'syntax match notesName /\c\%>1l' . pattern . '/' 1148 | let b:notes_names_last_highlighted = localtime() 1149 | call xolox#misc#timer#stop("notes.vim %s: Highlighted note names in %s.", g:xolox#notes#version, starttime) 1150 | endif 1151 | endfunction 1152 | 1153 | function! s:words_to_pattern(words) 1154 | " Quote regex meta characters, enable matching of hard wrapped words. 1155 | return substitute(xolox#misc#escape#pattern(a:words), '\s\+', '\\_s\\+', 'g') 1156 | endfunction 1157 | 1158 | function! s:sort_longest_to_shortest(a, b) 1159 | " Sort note titles by length, starting with the shortest. 1160 | return len(a:a) < len(a:b) ? 1 : -1 1161 | endfunction 1162 | 1163 | function! xolox#notes#highlight_sources(force) " {{{3 1164 | " Syntax highlight source code embedded in notes. 1165 | let starttime = xolox#misc#timer#start() 1166 | " Look for code blocks in the current note. 1167 | let filetypes = {} 1168 | for line in getline(1, '$') 1169 | let ft = matchstr(line, '\({{[{]\|```\)\zs\w\+\>') 1170 | if ft !~ '^\d*$' | let filetypes[ft] = 1 | endif 1171 | endfor 1172 | " Don't refresh the highlighting if nothing has changed. 1173 | if !a:force && exists('b:notes_previous_sources') && b:notes_previous_sources == filetypes 1174 | return 1175 | else 1176 | let b:notes_previous_sources = filetypes 1177 | endif 1178 | " Now we're ready to actually highlight the code blocks. 1179 | if !empty(filetypes) 1180 | let startgroup = 'notesCodeStart' 1181 | let endgroup = 'notesCodeEnd' 1182 | for ft in keys(filetypes) 1183 | let group = 'notesSnippet' . toupper(ft) 1184 | let include = s:syntax_include(ft) 1185 | for [startmarker, endmarker] in [['{{{', '}}}'], ['```', '```']] 1186 | let conceal = has('conceal') && xolox#misc#option#get('notes_conceal_code', 1) 1187 | let command = 'syntax region %s matchgroup=%s start="%s%s \?" matchgroup=%s end="%s" keepend contains=%s%s' 1188 | execute printf(command, group, startgroup, startmarker, ft, endgroup, endmarker, include, conceal ? ' concealends' : '') 1189 | endfor 1190 | endfor 1191 | if &vbs >= 1 1192 | call xolox#misc#timer#stop("notes.vim %s: Highlighted embedded %s sources in %s.", g:xolox#notes#version, join(sort(keys(filetypes)), '/'), starttime) 1193 | endif 1194 | endif 1195 | endfunction 1196 | 1197 | function! s:syntax_include(filetype) 1198 | " Include the syntax highlighting of another {filetype}. 1199 | let grouplistname = '@' . toupper(a:filetype) 1200 | " Unset the name of the current syntax while including the other syntax 1201 | " because some syntax scripts do nothing when "b:current_syntax" is set. 1202 | if exists('b:current_syntax') 1203 | let syntax_save = b:current_syntax 1204 | unlet b:current_syntax 1205 | endif 1206 | try 1207 | execute 'syntax include' grouplistname 'syntax/' . a:filetype . '.vim' 1208 | execute 'syntax include' grouplistname 'after/syntax/' . a:filetype . '.vim' 1209 | catch /E403/ 1210 | " Ignore errors about syntax scripts that can't be loaded more than once. 1211 | " See also: https://github.com/xolox/vim-notes/issues/68 1212 | catch /E484/ 1213 | " Ignore missing scripts. 1214 | endtry 1215 | " Restore the name of the current syntax. 1216 | if exists('syntax_save') 1217 | let b:current_syntax = syntax_save 1218 | elseif exists('b:current_syntax') 1219 | unlet b:current_syntax 1220 | endif 1221 | return grouplistname 1222 | endfunction 1223 | 1224 | function! xolox#notes#include_expr(fname) " {{{3 1225 | " Translate string {fname} to absolute filename of note. 1226 | " TODO Use inputlist() when more than one note matches?! 1227 | let notes = copy(xolox#notes#get_fnames_and_titles(1)) 1228 | let pattern = xolox#misc#escape#pattern(a:fname) 1229 | call filter(notes, 'v:val =~ pattern') 1230 | if !empty(notes) 1231 | let filtered_notes = items(notes) 1232 | let lnum = line('.') 1233 | for range in range(3) 1234 | let line1 = lnum - range 1235 | let line2 = lnum + range 1236 | let text = s:normalize_ws(join(getline(line1, line2), "\n")) 1237 | for [fname, title] in filtered_notes 1238 | if text =~? xolox#misc#escape#pattern(s:normalize_ws(title)) 1239 | return fname 1240 | endif 1241 | endfor 1242 | endfor 1243 | endif 1244 | return '' 1245 | endfunction 1246 | 1247 | function! s:normalize_ws(s) 1248 | " Enable string comparison that ignores differences in whitespace. 1249 | return xolox#misc#str#trim(substitute(a:s, '\_s\+', '', 'g')) 1250 | endfunction 1251 | 1252 | function! xolox#notes#foldexpr() " {{{3 1253 | " Folding expression to fold atx style Markdown headings. 1254 | let lastlevel = foldlevel(v:lnum - 1) 1255 | let nextlevel = match(getline(v:lnum), '^#\+\zs') 1256 | let retval = '=' 1257 | if lastlevel <= 0 && nextlevel >= 1 1258 | let retval = '>' . nextlevel 1259 | elseif nextlevel >= 1 1260 | if lastlevel > nextlevel 1261 | let retval = '<' . nextlevel 1262 | else 1263 | let retval = '>' . nextlevel 1264 | endif 1265 | endif 1266 | " Check whether the change in folding introduced by 'rv' 1267 | " is invalidated because we're inside a code block. 1268 | if retval != '=' && xolox#notes#inside_snippet(v:lnum, 1) 1269 | let retval = '=' 1270 | endif 1271 | return retval 1272 | endfunction 1273 | 1274 | function! xolox#notes#inside_snippet(lnum, col) " {{{3 1275 | " Check if the given line and column position is inside a snippet (a code 1276 | " block enclosed by triple curly brackets or triple back ticks). This 1277 | " function temporarily changes the cursor position in the current buffer in 1278 | " order to search backwards efficiently. 1279 | let pos_save = getpos('.') 1280 | try 1281 | call setpos('.', [0, a:lnum, a:col, 0]) 1282 | let matching_subpattern = search('{{{\|\(}}}\)\|```\w\|\(```\)', 'bnpW') 1283 | return matching_subpattern == 1 1284 | finally 1285 | call setpos('.', pos_save) 1286 | endtry 1287 | endfunction 1288 | 1289 | function! xolox#notes#currently_inside_snippet() " {{{3 1290 | " Check if the current cursor position is inside a snippet (a code block 1291 | " enclosed by triple curly brackets). 1292 | return xolox#notes#inside_snippet(line('.'), col('.')) 1293 | endfunction 1294 | 1295 | function! xolox#notes#foldtext() " {{{3 1296 | " Replace atx style "#" markers with "-" fold marker. 1297 | let line = getline(v:foldstart) 1298 | if line == '' 1299 | let line = getline(v:foldstart + 1) 1300 | endif 1301 | let matches = matchlist(line, '^\(#\+\)\s*\(.*\)$') 1302 | if len(matches) >= 3 1303 | let prefix = repeat('-', len(matches[1])) 1304 | return prefix . ' ' . matches[2] . ' ' 1305 | else 1306 | return line 1307 | endif 1308 | endfunction 1309 | 1310 | " }}}1 1311 | 1312 | " Make sure the plug-in configuration has been properly initialized before 1313 | " any of the auto-load functions in this Vim script can be called. 1314 | call xolox#notes#init() 1315 | 1316 | " vim: ts=2 sw=2 et bomb 1317 | -------------------------------------------------------------------------------- /autoload/xolox/notes/html.vim: -------------------------------------------------------------------------------- 1 | " Vim auto-load script 2 | " Author: Peter Odding 3 | " Last Change: December 29, 2014 4 | " URL: http://peterodding.com/code/vim/notes/ 5 | 6 | if !exists('g:notes_markdown_program') 7 | let g:notes_markdown_program = 'markdown' 8 | endif 9 | 10 | function! xolox#notes#html#view(open_in) " {{{1 11 | " Convert the current note to a web page and show the web page in a browser. 12 | " Requires [Markdown] [markdown] to be installed; you'll get a warning if it 13 | " isn't. 14 | " 15 | " [markdown]: http://en.wikipedia.org/wiki/Markdown 16 | try 17 | " Convert the note's text to HTML using Markdown. 18 | let starttime = xolox#misc#timer#start() 19 | let note_title = xolox#notes#current_title() 20 | let filename = xolox#notes#title_to_fname(note_title) 21 | let note_text = join(getline(1, '$'), "\n") 22 | let raw_html = xolox#notes#html#convert_note(note_text) 23 | let styled_html = xolox#notes#html#apply_template({ 24 | \ 'encoding': &encoding, 25 | \ 'title': note_title, 26 | \ 'content': raw_html, 27 | \ 'version': g:xolox#notes#version, 28 | \ 'date': strftime('%A %B %d, %Y at %H:%M'), 29 | \ 'filename': fnamemodify(filename, ':~'), 30 | \ }) 31 | if a:open_in == "split" 32 | " Open the generated HTML in a :split window. 33 | vnew 34 | call setline(1, split(styled_html, "\n")) 35 | setlocal filetype=html 36 | else 37 | " Open the generated HTML in a web browser. 38 | let filename = s:create_temporary_file(note_title) 39 | if writefile(split(styled_html, "\n"), filename) != 0 40 | throw printf("Failed to write HTML file! (%s)", filename) 41 | endif 42 | call xolox#misc#open#url('file://' . filename) 43 | endif 44 | call xolox#misc#timer#stop("notes.vim %s: Rendered HTML preview in %s.", g:xolox#notes#version, starttime) 45 | catch 46 | call xolox#misc#msg#warn("notes.vim %s: %s at %s", g:xolox#notes#version, v:exception, v:throwpoint) 47 | endtry 48 | endfunction 49 | 50 | function! xolox#notes#html#convert_note(note_text) " {{{1 51 | " Convert a note's text to a web page (HTML) using the [Markdown text 52 | " format] [markdown] as an intermediate format. This function takes the text 53 | " of a note (the first argument) and converts it to HTML, returning a 54 | " string. 55 | if !executable(g:notes_markdown_program) 56 | throw "HTML conversion requires the `markdown' program! On Debian/Ubuntu you can install it by executing `sudo apt-get install markdown'." 57 | endif 58 | let markdown = xolox#notes#markdown#convert_note(a:note_text) 59 | let result = xolox#misc#os#exec({'command': g:notes_markdown_program, 'stdin': markdown}) 60 | let html = join(result['stdout'], "\n") 61 | return html 62 | endfunction 63 | 64 | function! xolox#notes#html#apply_template(variables) " {{{1 65 | " The vim-notes plug-in contains a web page template that's used to provide 66 | " a bit of styling when a note is converted to a web page and presented to 67 | " the user. This function takes the original HTML produced by [Markdown] 68 | " [markdown] (the first argument) and wraps it in the configured template, 69 | " returning the final HTML as a string. 70 | let filename = expand(g:notes_html_template) 71 | call xolox#misc#msg#debug("notes.vim %s: Reading web page template from %s ..", g:xolox#notes#version, filename) 72 | let template = join(readfile(filename), "\n") 73 | let output = substitute(template, '{{\(.\{-}\)}}', '\= s:template_callback(a:variables)', 'g') 74 | return output 75 | endfunction 76 | 77 | function! s:template_callback(variables) " {{{1 78 | " Callback for xolox#notes#html#apply_template(). 79 | let key = xolox#misc#str#trim(submatch(1)) 80 | return get(a:variables, key, '') 81 | endfunction 82 | 83 | function! s:create_temporary_file(note_title) " {{{1 84 | " Create a temporary filename for a note converted to an HTML document, 85 | " based on the title of the note. 86 | if !exists('s:temporary_directory') 87 | let s:temporary_directory = xolox#misc#path#tempdir() 88 | endif 89 | let filename = xolox#misc#str#slug(a:note_title) . '.html' 90 | return xolox#misc#path#merge(s:temporary_directory, filename) 91 | endfunction 92 | -------------------------------------------------------------------------------- /autoload/xolox/notes/markdown.vim: -------------------------------------------------------------------------------- 1 | " Vim auto-load script 2 | " Author: Peter Odding 3 | " Last Change: November 27, 2014 4 | " URL: http://peterodding.com/code/vim/notes/ 5 | 6 | function! xolox#notes#markdown#view() " {{{1 7 | " Convert the current note to a Markdown document and show the converted text. 8 | let note_text = join(getline(1, '$'), "\n") 9 | let markdown_text = xolox#notes#markdown#convert_note(note_text) 10 | vnew 11 | call setline(1, split(markdown_text, "\n")) 12 | setlocal filetype=markdown 13 | endfunction 14 | 15 | function! xolox#notes#markdown#convert_note(note_text) " {{{1 16 | " Convert a note's text to the [Markdown text format] [markdown]. The syntax 17 | " used by vim-notes has a lot of similarities with Markdown, but there are 18 | " some notable differences like the note title and the way code blocks are 19 | " represented. This function takes the text of a note (the first argument) 20 | " and converts it to the Markdown format, returning a string. 21 | " 22 | " [markdown]: http://en.wikipedia.org/wiki/Markdown 23 | let starttime = xolox#misc#timer#start() 24 | let blocks = xolox#notes#parser#parse_note(a:note_text) 25 | call map(blocks, 'xolox#notes#markdown#convert_block(v:val)') 26 | let markdown = join(blocks, "\n\n") 27 | call xolox#misc#timer#stop("notes.vim %s: Converted note to Markdown in %s.", g:xolox#notes#version, starttime) 28 | return markdown 29 | endfunction 30 | 31 | function! xolox#notes#markdown#convert_block(block) " {{{1 32 | " Convert a single block produced by `xolox#misc#notes#parser#parse_note()` 33 | " (the first argument, expected to be a dictionary) to the [Markdown text 34 | " format] [markdown]. Returns a string. 35 | if a:block.type == 'title' 36 | let text = s:make_urls_explicit(a:block.text) 37 | return printf("# %s", text) 38 | elseif a:block.type == 'heading' 39 | let marker = repeat('#', 1 + a:block.level) 40 | let text = s:make_urls_explicit(a:block.text) 41 | return printf("%s %s", marker, text) 42 | elseif a:block.type == 'code' 43 | let comment = "" 44 | let text = xolox#misc#str#indent(xolox#misc#str#dedent(a:block.text), 4) 45 | return join([comment, text], "\n\n") 46 | elseif a:block.type == 'divider' 47 | return '---' 48 | elseif a:block.type == 'list' 49 | let items = [] 50 | if a:block.ordered 51 | let counter = 1 52 | for item in a:block.items 53 | let indent = repeat(' ', item.indent * 4) 54 | let text = s:make_urls_explicit(item.text) 55 | call add(items, printf("%s%d. %s", indent, counter, text)) 56 | let counter += 1 57 | endfor 58 | else 59 | for item in a:block.items 60 | let indent = repeat(' ', item.indent * 4) 61 | let text = s:make_urls_explicit(item.text) 62 | call add(items, printf("%s- %s", indent, text)) 63 | endfor 64 | endif 65 | return join(items, "\n\n") 66 | elseif a:block.type == 'block-quote' 67 | let lines = [] 68 | for line in a:block.lines 69 | let prefix = repeat('>', line.level) 70 | call add(lines, printf('%s %s', prefix, line.text)) 71 | endfor 72 | return join(lines, "\n") 73 | elseif a:block.type == 'paragraph' 74 | let text = s:make_urls_explicit(a:block.text) 75 | if len(text) <= 50 && text =~ ':$' 76 | let text = printf('**%s**', text) 77 | endif 78 | return text 79 | else 80 | let msg = "Encountered unsupported block: %s!" 81 | throw printf(msg, string(a:block)) 82 | endif 83 | endfunction 84 | 85 | function! s:make_urls_explicit(text) " {{{1 86 | " In the vim-notes syntax, URLs are implicitly hyperlinks. 87 | " In Markdown syntax they have to be wrapped in . 88 | return substitute(a:text, g:xolox#notes#url_pattern, '\= s:url_callback(submatch(0))', 'g') 89 | endfunction 90 | 91 | function! s:url_callback(url) 92 | let label = substitute(a:url, '^\w\+:\(//\)\?', '', '') 93 | return printf('[%s](%s)', label, a:url) 94 | endfunction 95 | -------------------------------------------------------------------------------- /autoload/xolox/notes/mediawiki.vim: -------------------------------------------------------------------------------- 1 | " Vim auto-load script 2 | " Author: Anthony Naddeo 3 | " Last Change: December 29, 2014 4 | " URL: https://github.com/naddeoa 5 | 6 | function! xolox#notes#mediawiki#view() " {{{1 7 | " Convert the current note to a Mediawiki document and show the converted text. 8 | let note_text = join(getline(1, '$'), "\n") 9 | let mediawiki_text = xolox#notes#mediawiki#convert_note(note_text) 10 | vnew 11 | call setline(1, split(mediawiki_text, "\n")) 12 | setlocal filetype=mediawiki 13 | endfunction 14 | 15 | function! xolox#notes#mediawiki#convert_note(note_text) " {{{1 16 | " Convert a note's text to the [Mediawiki text format] [mediawiki]. The syntax 17 | " used by vim-notes has a lot of similarities with Mediawiki, but there are 18 | " some notable differences like the note title and the way code blocks are 19 | " represented. This function takes the text of a note (the first argument) 20 | " and converts it to the Mediawiki format, returning a string. 21 | " 22 | " [mediawiki]: https://www.mediawiki.org/wiki/MediaWiki 23 | let starttime = xolox#misc#timer#start() 24 | let blocks = xolox#notes#parser#parse_note(a:note_text) 25 | call map(blocks, 'xolox#notes#mediawiki#convert_block(v:val)') 26 | let mediawiki = join(blocks, "\n\n") 27 | call xolox#misc#timer#stop("notes.vim %s: Converted note to Mediawiki syntax in %s.", g:xolox#notes#version, starttime) 28 | return mediawiki 29 | endfunction 30 | 31 | function! xolox#notes#mediawiki#convert_block(block) " {{{1 32 | " Convert a single block produced by `xolox#misc#notes#parser#parse_note()` 33 | " (the first argument, expected to be a dictionary) to the [Mediawiki text 34 | " format] [mediawiki]. Returns a string. 35 | if a:block.type == 'title' 36 | let text = s:make_urls_explicit(a:block.text) 37 | return "" 38 | elseif a:block.type == 'heading' 39 | let marker = repeat('=', 1 + a:block.level) 40 | let text = s:make_urls_explicit(a:block.text) 41 | return printf("%s %s %s", marker, text, marker) 42 | elseif a:block.type == 'code' 43 | return printf('%s', a:block.language, a:block.text) 44 | elseif a:block.type == 'divider' 45 | return '----' 46 | elseif a:block.type == 'list' 47 | let items = [] 48 | if a:block.ordered 49 | for item in a:block.items 50 | let indent = repeat('#', item.indent + 1) 51 | let text = s:make_urls_explicit(item.text) 52 | let text = s:highlight_task_markers(text) 53 | if text =~# "DONE" 54 | call add(items, printf("%s %s", indent, text)) 55 | else 56 | call add(items, printf("%s %s", indent, text)) 57 | endif 58 | endfor 59 | else 60 | for item in a:block.items 61 | let indent = repeat('*', item.indent + 1) 62 | let text = s:make_urls_explicit(item.text) 63 | let text = s:highlight_task_markers(text) 64 | if text =~# "DONE" 65 | call add(items, printf("%s %s", indent, text)) 66 | else 67 | call add(items, printf("%s %s", indent, text)) 68 | endif 69 | endfor 70 | endif 71 | return join(items, "\n") 72 | elseif a:block.type == 'block-quote' 73 | let lines = [] 74 | for line in a:block.lines 75 | let prefix = repeat('>', line.level) 76 | call add(lines, printf('%s %s', prefix, line.text)) 77 | endfor 78 | return join(lines, "\n") 79 | elseif a:block.type == 'paragraph' 80 | let text = s:make_urls_explicit(a:block.text) 81 | if len(text) <= 50 && text =~ ':$' 82 | let text = printf("'''%s'''", text) 83 | endif 84 | return text 85 | else 86 | let msg = "Encountered unsupported block: %s!" 87 | throw printf(msg, string(a:block)) 88 | endif 89 | endfunction 90 | 91 | function! s:highlight_task_markers(text) 92 | " Highlight `TODO`, `DONE` and `XXX` markers with color in the Mediawiki 93 | " output similar to how the markers are highlighted by vim-notes. 94 | let highlighted = a:text 95 | let highlighted = substitute(highlighted, '\C\', 'XXX', "") 96 | let highlighted = substitute(highlighted, '\C\', 'TODO', "") 97 | let highlighted = substitute(highlighted, '\C\', 'DONE', "") 98 | return highlighted 99 | endfunction 100 | 101 | function! s:make_urls_explicit(text) " {{{1 102 | " In the vim-notes syntax, URLs are implicitly hyperlinks. 103 | " In Mediawiki syntax they have to be wrapped in [[markers]]. 104 | return substitute(a:text, g:xolox#notes#url_pattern, '\= s:url_callback(submatch(0))', 'g') 105 | endfunction 106 | 107 | function! s:url_callback(url) 108 | let label = substitute(a:url, '^\w\+:\(//\)\?', '', '') 109 | return printf('[%s %s]', a:url, label) 110 | endfunction 111 | -------------------------------------------------------------------------------- /autoload/xolox/notes/parser.vim: -------------------------------------------------------------------------------- 1 | " Vim auto-load script 2 | " Author: Peter Odding 3 | " Last Change: November 27, 2014 4 | " URL: http://peterodding.com/code/vim/notes/ 5 | 6 | function! xolox#notes#parser#parse_note(text) " {{{1 7 | " Parser for the note taking syntax used by vim-notes. 8 | let starttime = xolox#misc#timer#start() 9 | let context = s:create_parse_context(a:text) 10 | let note_title = context.next_line() 11 | let blocks = [{'type': 'title', 'text': note_title}] 12 | while context.has_more() 13 | let chr = context.peek(1) 14 | if chr == "\n" 15 | " Ignore empty lines. 16 | call context.next(1) 17 | continue 18 | elseif chr == '#' 19 | let block = s:parse_heading(context) 20 | elseif chr == '>' 21 | let block = s:parse_block_quote(context) 22 | elseif chr == '{' && context.peek(3) == "\{\{\{" 23 | let block = s:parse_code_block(context, 1) 24 | elseif chr == '`' && context.peek(3) == "```" 25 | let block = s:parse_code_block(context, 2) 26 | else 27 | let lookahead = s:match_bullet_or_divider(context, 0) 28 | if !empty(lookahead) 29 | if lookahead.type =~ 'list' 30 | let block = s:parse_list(context) 31 | elseif lookahead.type == 'divider' 32 | let block = s:parse_divider(context) 33 | else 34 | let msg = "Programming error! Unsupported lookahead: %s." 35 | throw printf(msg, string(lookahead)) 36 | endif 37 | else 38 | let block = s:parse_paragraph(context) 39 | endif 40 | endif 41 | " Don't include empty blocks in the output. 42 | if !empty(block) 43 | call add(blocks, block) 44 | endif 45 | endwhile 46 | call xolox#misc#timer#stop("notes.vim %s: Parsed note into %i blocks in %s.", g:xolox#notes#version, len(blocks), starttime) 47 | return blocks 48 | endfunction 49 | 50 | function! xolox#notes#parser#view_parse_nodes() " {{{1 51 | " Parse the current note and show the parse nodes in a temporary buffer. 52 | let note_text = join(getline(1, '$'), "\n") 53 | let parse_nodes = xolox#notes#parser#parse_note(note_text) 54 | vnew 55 | call setline(1, map(parse_nodes, 'string(v:val)')) 56 | setlocal filetype=vim nomodified nowrap 57 | endfunction 58 | 59 | function! s:create_parse_context(text) " {{{1 60 | " Create an object to encapsulate the lowest level of parser state. 61 | let context = {'text': a:text, 'index': 0} 62 | " The has_more() method returns 1 (true) when more input is available, 0 63 | " (false) otherwise. 64 | function context.has_more() 65 | return self.index < len(self.text) 66 | endfunction 67 | " The peek() method returns the next character without consuming it. 68 | function context.peek(n) 69 | if self.has_more() 70 | return self.text[self.index : self.index + (a:n - 1)] 71 | endif 72 | return '' 73 | endfunction 74 | " The next() method returns the next character and consumes it. 75 | function context.next(n) 76 | let result = self.peek(a:n) 77 | let self.index += a:n 78 | return result 79 | endfunction 80 | " The next_line() method returns the current line and consumes it. 81 | function context.next_line() 82 | let line = '' 83 | while self.has_more() 84 | let chr = self.next(1) 85 | if chr == "\n" || chr == "" 86 | " We hit the end of line or input. 87 | return line 88 | else 89 | " The line continues. 90 | let line .= chr 91 | endif 92 | endwhile 93 | return line 94 | endfunction 95 | return context 96 | endfunction 97 | 98 | function! s:match_bullet_or_divider(context, consume_lookahead) " {{{1 99 | " Check whether the current line starts with a list bullet. 100 | let result = {} 101 | let context = copy(a:context) 102 | let line = context.next_line() 103 | let bullet = matchstr(line, s:bullet_pattern) 104 | if !empty(bullet) 105 | " Disambiguate list bullets from horizontal dividers. 106 | if line =~ '^\s\+\*\s\*\s\*$' 107 | let result.type = 'divider' 108 | else 109 | " We matched a bullet! Now we still need to distinguish ordered from 110 | " unordered list items. 111 | if bullet =~ '\d' 112 | let result.type = 'ordered-list' 113 | else 114 | let result.type = 'unordered-list' 115 | endif 116 | let indent = matchstr(bullet, '^\s*') 117 | let result.indent = len(indent) 118 | " Since we already skipped the whitespace and matched the bullet, it's 119 | " very little work to mark our position for the benefit of the caller. 120 | if a:consume_lookahead 121 | let a:context.index += len(bullet) 122 | endif 123 | endif 124 | endif 125 | return result 126 | endfunction 127 | 128 | function! s:match_line(context) " {{{1 129 | " Get the text of the current line, stopping at end of the line or just 130 | " before the start of a code block marker, whichever comes first. 131 | let line = '' 132 | while a:context.has_more() 133 | let chr = a:context.peek(1) 134 | if chr == '{' && a:context.peek(3) == "\{\{\{" 135 | " XXX The start of a code block implies the end of whatever came before. 136 | " The marker above contains back slashes so that Vim doesn't apply 137 | " folding because of the marker :-). 138 | return line 139 | elseif chr == '`' && a:context.peek(3) =~ '```' 140 | " A second form of code blocks. 141 | return line 142 | elseif chr == "\n" 143 | call a:context.next(1) 144 | return line . "\n" 145 | else 146 | let line .= a:context.next(1) 147 | endif 148 | endwhile 149 | " We hit the end of the input. 150 | return line 151 | endfunction 152 | 153 | function! s:parse_heading(context) " {{{1 154 | " Parse the upcoming heading in the input stream. 155 | let level = 0 156 | while a:context.peek(1) == '#' 157 | let level += 1 158 | call a:context.next(1) 159 | endwhile 160 | let text = xolox#misc#str#trim(s:match_line(a:context)) 161 | return {'type': 'heading', 'level': level, 'text': text} 162 | endfunction 163 | 164 | function! s:parse_block_quote(context) " {{{1 165 | " Parse the upcoming block quote in the input stream. 166 | let lines = [] 167 | while a:context.has_more() 168 | if a:context.peek(1) != '>' 169 | break 170 | endif 171 | let line = s:match_line(a:context) 172 | let level = len(matchstr(line, '^>\+')) 173 | let text = matchstr(line, '^>\+\s*\zs.\{-}\ze\_s*$') 174 | call add(lines, {'level': level, 'text': text}) 175 | endwhile 176 | return {'type': 'block-quote', 'lines': lines} 177 | endfunction 178 | 179 | function! s:parse_code_block(context, style) " {{{1 180 | " Parse the upcoming code block in the input stream. 181 | let language = '' 182 | let text = '' 183 | " Skip the start marker. 184 | call a:context.next(3) 185 | " Get the optional language name. 186 | while a:context.peek(1) =~ '\w' 187 | let language .= a:context.next(1) 188 | endwhile 189 | " Skip the whitespace separating the start marker and/or language name from 190 | " the text. 191 | while a:context.peek(1) =~ '[ \t]' 192 | call a:context.next(1) 193 | endwhile 194 | " Get the text inside the code block. 195 | while a:context.has_more() 196 | let chr = a:context.next(1) 197 | if (a:style == 1 && chr == '}' && a:context.peek(2) == '}}') || (a:style == 2 && chr == '`' && a:context.peek(2) == '``') 198 | call a:context.next(2) 199 | break 200 | endif 201 | let text .= chr 202 | endwhile 203 | " Strip trailing whitespace. 204 | let text = substitute(text, '\_s\+$', '', '') 205 | return {'type': 'code', 'language': language, 'text': text} 206 | endfunction 207 | 208 | function! s:parse_divider(context) " {{{1 209 | " Parse the upcoming horizontal divider in the input stream. 210 | call a:context.next_line() 211 | return {'type': 'divider'} 212 | endfunction 213 | 214 | function! s:parse_list(context) " {{{1 215 | " Parse the upcoming sequence of list items in the input stream. 216 | let list_type = 'unknown' 217 | let items = [] 218 | let lines = [] 219 | let indent = 0 220 | " Outer loop to consume one or more list items. 221 | while a:context.has_more() 222 | let lookahead = s:match_bullet_or_divider(a:context, 1) 223 | if !empty(lookahead) 224 | " Save the previous list item with the old indent level. 225 | call s:save_item(items, lines, indent) 226 | let lines = [] 227 | " Set the new indent level (three spaces -> one level). 228 | let indent = lookahead.indent / 3 229 | " The current line starts with a list bullet. 230 | if list_type == 'unknown' 231 | " The first bullet determines the type of list. 232 | let list_type = lookahead.type 233 | endif 234 | endif 235 | let line = s:match_line(a:context) 236 | call add(lines, line) 237 | if line[-1:] != "\n" 238 | " XXX When match_line() returns a line that doesn't end in a newline 239 | " character, it means either we hit the end of the input or the current 240 | " line continues in a code block (which is not ours to parse :-). 241 | break 242 | elseif line =~ '^\_s*$' 243 | " For now an empty line terminates the list item. 244 | " TODO Add support for list items with multiple paragraphs of text. 245 | break 246 | endif 247 | endwhile 248 | call s:save_item(items, lines, indent) 249 | return {'type': 'list', 'ordered': (list_type == 'ordered-list'), 'items': items} 250 | endfunction 251 | 252 | function! s:save_item(items, lines, indent) 253 | let text = join(a:lines, "\n") 254 | if text =~ '\S' 255 | let text = xolox#misc#str#compact(text) 256 | call add(a:items, {'text': text, 'indent': a:indent}) 257 | endif 258 | endfunction 259 | 260 | function! s:parse_paragraph(context) " {{{1 261 | " Parse the upcoming paragraph in the input stream. 262 | let lines = [] 263 | while a:context.has_more() 264 | if !empty(s:match_bullet_or_divider(a:context, 0)) 265 | " If the next line starts with a list bullet it shouldn't 266 | " be included in the paragraph we're currently parsing. 267 | break 268 | else 269 | let line = s:match_line(a:context) 270 | call add(lines, line) 271 | if line =~ '^\_s*$' 272 | " An empty line marks the end of the paragraph. 273 | break 274 | elseif line[-1:] != "\n" 275 | " XXX When match_line() returns a line that doesn't end in a newline 276 | " character, it means either we hit the end of the input or the current 277 | " line continues in a code block (which is not ours to parse :-). 278 | break 279 | endif 280 | endif 281 | endwhile 282 | " Don't include empty paragraphs in the output. 283 | let text = join(lines, "\n") 284 | if text =~ '\S' 285 | return {'type': 'paragraph', 'text': xolox#misc#str#compact(text)} 286 | else 287 | return {} 288 | endif 289 | endfunction 290 | 291 | function! s:generate_list_item_bullet_pattern() " {{{1 292 | " Generate a regular expression that matches any kind of list bullet. 293 | let choices = copy(g:notes_unicode_bullets) 294 | for bullet in g:notes_ascii_bullets 295 | call add(choices, xolox#misc#escape#pattern(bullet)) 296 | endfor 297 | call add(choices, '\d\+[[:punct:]]\?') 298 | return join(choices, '\|') 299 | endfunction 300 | 301 | let s:bullet_pattern = '^\s*\(' . s:generate_list_item_bullet_pattern() . '\)\s\+' 302 | 303 | function! xolox#notes#parser#run_tests() " {{{1 304 | " Tests for the note taking syntax parser. 305 | call xolox#misc#test#reset() 306 | call xolox#misc#test#wrap('xolox#notes#parser#test_parsing_of_note_titles') 307 | call xolox#misc#test#wrap('xolox#notes#parser#test_parsing_of_headings') 308 | call xolox#misc#test#wrap('xolox#notes#parser#test_parsing_of_paragraphs') 309 | call xolox#misc#test#wrap('xolox#notes#parser#test_parsing_of_code_blocks') 310 | call xolox#misc#test#wrap('xolox#notes#parser#test_parsing_of_list_items') 311 | call xolox#misc#test#wrap('xolox#notes#parser#test_parsing_of_block_quotes') 312 | call xolox#misc#test#summarize() 313 | endfunction 314 | 315 | function! xolox#notes#parser#test_parsing_of_note_titles() 316 | call xolox#misc#test#assert_equals([{'type': 'title', 'text': 'Just the title'}], xolox#notes#parser#parse_note('Just the title')) 317 | endfunction 318 | 319 | function! xolox#notes#parser#test_parsing_of_headings() 320 | call xolox#misc#test#assert_equals([{'type': 'title', 'text': 'Just the title'}, {'type': 'heading', 'level': 1, 'text': 'This is a heading'}], xolox#notes#parser#parse_note("Just the title\n\n# This is a heading")) 321 | endfunction 322 | 323 | function! xolox#notes#parser#test_parsing_of_paragraphs() 324 | call xolox#misc#test#assert_equals([{'type': 'title', 'text': 'Just the title'}, {'type': 'paragraph', 'text': 'This is a paragraph'}], xolox#notes#parser#parse_note("Just the title\n\nThis is a paragraph")) 325 | call xolox#misc#test#assert_equals([{'type': 'title', 'text': 'Just the title'}, {'type': 'paragraph', 'text': 'This is a paragraph'}, {'type': 'paragraph', 'text': "And here's another paragraph!"}], xolox#notes#parser#parse_note("Just the title\n\nThis is a paragraph\n\n\n\nAnd here's another paragraph!")) 326 | endfunction 327 | 328 | function! xolox#notes#parser#test_parsing_of_code_blocks() 329 | call xolox#misc#test#assert_equals([{'type': 'title', 'text': 'Just the title'}, {'type': 'code', 'language': '', 'text': "This is a code block\nwith two lines"}], xolox#notes#parser#parse_note("Just the title\n\n{{{ This is a code block\nwith two lines }}}")) 330 | endfunction 331 | 332 | function! xolox#notes#parser#test_parsing_of_list_items() 333 | call xolox#misc#test#assert_equals([{'type': 'title', 'text': 'Just the title'}, {'type': 'list', 'ordered': 1, 'items': [{'indent': 0, 'text': 'item one'}, {'indent': 0, 'text': 'item two'}, {'indent': 0, 'text': 'item three'}]}], xolox#notes#parser#parse_note("Just the title\n\n1. item one\n2. item two\n3. item three")) 334 | endfunction 335 | 336 | function! xolox#notes#parser#test_parsing_of_block_quotes() 337 | call xolox#misc#test#assert_equals([{'type': 'title', 'text': 'Just the title'}, {'type': 'block-quote', 'lines': [{'level': 1, 'text': 'block'}, {'level': 2, 'text': 'quoted'}, {'level': 1, 'text': 'text'}]}], xolox#notes#parser#parse_note("Just the title\n\n> block\n>> quoted\n> text")) 338 | endfunction 339 | -------------------------------------------------------------------------------- /autoload/xolox/notes/recent.vim: -------------------------------------------------------------------------------- 1 | " Vim auto-load script 2 | " Author: Peter Odding 3 | " Last Change: May 16, 2013 4 | " URL: http://peterodding.com/code/vim/notes/ 5 | 6 | function! xolox#notes#recent#show(bang, title_filter) " {{{1 7 | call xolox#misc#msg#info("notes.vim %s: Generating overview of recent notes ..", g:xolox#notes#version) 8 | " Show generated note listing all notes by last modified time. 9 | let starttime = xolox#misc#timer#start() 10 | let bufname = '[Recent Notes]' 11 | " Prepare a buffer to hold the list of recent notes. 12 | call xolox#misc#buffer#prepare({ 13 | \ 'name': bufname, 14 | \ 'path': xolox#misc#path#merge($HOME, bufname)}) 15 | " Filter notes by pattern (argument)? 16 | let notes = [] 17 | let title_filter = '\v' . a:title_filter 18 | for [fname, title] in items(xolox#notes#get_fnames_and_titles(0)) 19 | if title =~? title_filter 20 | call add(notes, [getftime(fname), title]) 21 | endif 22 | endfor 23 | " Start note with "You have N note(s) [matching filter]". 24 | let readme = "You have " 25 | if empty(notes) 26 | let readme .= "no notes" 27 | elseif len(notes) == 1 28 | let readme .= "one note" 29 | else 30 | let readme .= len(notes) . " notes" 31 | endif 32 | if a:title_filter != '' 33 | let quote_format = xolox#notes#unicode_enabled() ? '‘%s’' : "`%s'" 34 | let readme .= " matching " . printf(quote_format, a:title_filter) 35 | endif 36 | " Explain the sorting of the notes. 37 | if empty(notes) 38 | let readme .= "." 39 | elseif len(notes) == 1 40 | let readme .= ", it's listed below." 41 | else 42 | let readme .= ". They're listed below grouped by the day they were edited, starting with your most recently edited note." 43 | endif 44 | " Add the generated text to the buffer. 45 | call setline(1, ["Recent notes", "", readme]) 46 | " Reformat the text in the buffer to auto-wrap. 47 | normal Ggqq 48 | " Sort, group and format the list of (matching) notes. 49 | let last_date = '' 50 | let list_item_format = xolox#notes#unicode_enabled() ? ' • %s' : ' * %s' 51 | call sort(notes) 52 | call reverse(notes) 53 | let lines = [] 54 | for [ftime, title] in notes 55 | let date = xolox#notes#friendly_date(ftime) 56 | if date != last_date 57 | call add(lines, '') 58 | call add(lines, substitute(date, '^\w', '\u\0', '') . ':') 59 | let last_date = date 60 | endif 61 | call add(lines, printf(list_item_format, title)) 62 | endfor 63 | " Add the formatted list of notes to the buffer. 64 | call setline(line('$') + 1, lines) 65 | " Load the notes file type. 66 | call xolox#notes#set_filetype() 67 | let &l:statusline = bufname 68 | " Change the status line 69 | " Lock the buffer contents. 70 | call xolox#misc#buffer#lock() 71 | " And we're done! 72 | call xolox#misc#timer#stop("notes.vim %s: Generated %s in %s.", g:xolox#notes#version, bufname, starttime) 73 | endfunction 74 | 75 | function! xolox#notes#recent#track() " {{{1 76 | let fname = expand('%:p') 77 | let indexfile = expand(g:notes_recentindex) 78 | call xolox#misc#msg#debug("notes.vim %s: Recording '%s' as most recent note in %s ..", g:xolox#notes#version, fname, indexfile) 79 | if writefile([fname], indexfile) == -1 80 | call xolox#misc#msg#warn("notes.vim %s: Failed to record most recent note in %s!", g:xolox#notes#version, indexfile) 81 | endif 82 | endfunction 83 | 84 | function! xolox#notes#recent#edit(bang) " {{{1 85 | " Edit the most recently edited (not necessarily changed) note. 86 | let indexfile = expand(g:notes_recentindex) 87 | call xolox#misc#msg#debug("notes.vim %s: Recalling most recent note from %s ..", g:xolox#notes#version, indexfile) 88 | try 89 | let fname = readfile(indexfile)[0] 90 | if empty(fname) 91 | throw "The index of recent notes is empty?!" 92 | endif 93 | catch 94 | call xolox#misc#msg#warn("notes.vim %s: Failed to recall most recent note from %s: %s", g:xolox#notes#version, indexfile, v:exception) 95 | return 96 | endtry 97 | call xolox#misc#msg#info("notes.vim %s: Editing most recent note '%s' ..", g:xolox#notes#version, fname) 98 | execute 'edit' . a:bang fnameescape(fname) 99 | call xolox#notes#set_filetype() 100 | endfunction 101 | -------------------------------------------------------------------------------- /autoload/xolox/notes/tags.vim: -------------------------------------------------------------------------------- 1 | " Vim auto-load script 2 | " Author: Peter Odding 3 | " Last Change: May 5, 2013 4 | " URL: http://peterodding.com/code/vim/notes/ 5 | 6 | if !exists('s:currently_tagged_notes') 7 | let s:currently_tagged_notes = {} " The in-memory representation of tags and the notes in which they're used. 8 | let s:previously_tagged_notes = {} " Copy of index as it is / should be now on disk (to detect changes). 9 | let s:last_disk_sync = 0 " Whether the on-disk representation of the tags has been read. 10 | let s:buffer_name = 'Tagged Notes' " The buffer name for the list of tagged notes. 11 | let s:loading_index = 0 12 | endif 13 | 14 | function! xolox#notes#tags#load_index() " {{{1 15 | if s:loading_index 16 | " Guard against recursive calls. 17 | return s:currently_tagged_notes 18 | endif 19 | let starttime = xolox#misc#timer#start() 20 | let indexfile = expand(g:notes_tagsindex) 21 | let lastmodified = getftime(indexfile) 22 | if lastmodified == -1 23 | let s:loading_index = 1 24 | call xolox#notes#tags#create_index() 25 | let s:loading_index = 0 26 | elseif lastmodified > s:last_disk_sync 27 | let s:currently_tagged_notes = {} 28 | for line in readfile(indexfile) 29 | let filenames = split(line, "\t") 30 | if len(filenames) > 1 31 | let tagname = remove(filenames, 0) 32 | let s:currently_tagged_notes[tagname] = filenames 33 | endif 34 | endfor 35 | let s:previously_tagged_notes = deepcopy(s:currently_tagged_notes) 36 | let s:last_disk_sync = lastmodified 37 | call xolox#misc#timer#stop("notes.vim %s: Loaded tags index in %s.", g:xolox#notes#version, starttime) 38 | endif 39 | return s:currently_tagged_notes 40 | endfunction 41 | 42 | function! xolox#notes#tags#create_index() " {{{1 43 | let exists = filereadable(expand(g:notes_tagsindex)) 44 | let starttime = xolox#misc#timer#start() 45 | let filenames = xolox#notes#get_fnames(0) 46 | let s:currently_tagged_notes = {} 47 | for idx in range(len(filenames)) 48 | if filereadable(filenames[idx]) 49 | let title = xolox#notes#fname_to_title(filenames[idx]) 50 | call xolox#misc#msg#info("notes.vim %s: Scanning note %i/%i: %s", g:xolox#notes#version, idx + 1, len(filenames), title) 51 | call xolox#notes#tags#scan_note(title, join(readfile(filenames[idx]), "\n")) 52 | endif 53 | endfor 54 | if xolox#notes#tags#save_index() 55 | let s:previously_tagged_notes = deepcopy(s:currently_tagged_notes) 56 | call xolox#misc#timer#stop('notes.vim %s: %s tags index in %s.', g:xolox#notes#version, exists ? "Updated" : "Created", starttime) 57 | else 58 | call xolox#misc#msg#warn("notes.vim %s: Failed to save tags index as %s!", g:xolox#notes#version, g:notes_tagsindex) 59 | endif 60 | endfunction 61 | 62 | function! xolox#notes#tags#save_index() " {{{1 63 | let indexfile = expand(g:notes_tagsindex) 64 | let existingfile = filereadable(indexfile) 65 | let nothingchanged = (s:currently_tagged_notes == s:previously_tagged_notes) 66 | if existingfile && nothingchanged 67 | call xolox#misc#msg#debug("notes.vim %s: Index not dirty so not saved.", g:xolox#notes#version) 68 | return 1 " Nothing to be done 69 | else 70 | let lines = [] 71 | for [tagname, filenames] in items(s:currently_tagged_notes) 72 | call add(lines, join([tagname] + filenames, "\t")) 73 | endfor 74 | let status = writefile(lines, indexfile) == 0 75 | if status 76 | call xolox#misc#msg#debug("notes.vim %s: Index saved to %s.", g:xolox#notes#version, g:notes_tagsindex) 77 | let s:last_disk_sync = getftime(indexfile) 78 | else 79 | call xolox#misc#msg#debug("notes.vim %s: Failed to save index to %s.", g:xolox#notes#version, g:notes_tagsindex) 80 | endif 81 | return status 82 | endif 83 | endfunction 84 | 85 | function! xolox#notes#tags#scan_note(title, text) " {{{1 86 | " Add a note to the tags index. 87 | call xolox#notes#tags#load_index() 88 | " Don't scan tags inside code blocks. 89 | let text = substitute(a:text, '{{{\w\+\_.\{-}}}}', '', 'g') 90 | " Split everything on whitespace. 91 | for token in split(text) 92 | " Match words that start with @ and don't contain { (BibTeX entries). 93 | if token =~ '^@\w' && token !~ '{' 94 | " Strip any trailing punctuation. 95 | let token = substitute(token[1:], '[[:punct:]]*$', '', '') 96 | if token != '' 97 | if !has_key(s:currently_tagged_notes, token) 98 | let s:currently_tagged_notes[token] = [a:title] 99 | elseif index(s:currently_tagged_notes[token], a:title) == -1 100 | " Keep the tags sorted. 101 | call xolox#misc#list#binsert(s:currently_tagged_notes[token], a:title, 1) 102 | endif 103 | endif 104 | endif 105 | endfor 106 | endfunction 107 | 108 | function! xolox#notes#tags#forget_note(title) " {{{1 109 | " Remove a note from the tags index. 110 | call xolox#notes#tags#load_index() 111 | for tagname in keys(s:currently_tagged_notes) 112 | call filter(s:currently_tagged_notes[tagname], "v:val != a:title") 113 | if empty(s:currently_tagged_notes[tagname]) 114 | unlet s:currently_tagged_notes[tagname] 115 | endif 116 | endfor 117 | endfunction 118 | 119 | function! xolox#notes#tags#show_tags(minsize) " {{{1 120 | " TODO Mappings to "zoom" in/out (show only big tags). 121 | let starttime = xolox#misc#timer#start() 122 | call xolox#notes#tags#load_index() 123 | let lines = [s:buffer_name, ''] 124 | if empty(s:currently_tagged_notes) 125 | call add(lines, "You haven't used any tags yet!") 126 | else 127 | " Create a dictionary with note titles as keys. 128 | let unmatched = {} 129 | for title in xolox#notes#get_titles(0) 130 | let unmatched[title] = 1 131 | endfor 132 | let totalnotes = len(unmatched) 133 | " Group matching notes and remove them from the dictionary. 134 | let grouped_notes = [] 135 | let numtags = 0 136 | for tagname in sort(keys(s:currently_tagged_notes), 1) 137 | let numnotes = len(s:currently_tagged_notes[tagname]) 138 | if numnotes >= a:minsize 139 | let matched_notes = s:currently_tagged_notes[tagname] 140 | for title in matched_notes 141 | if has_key(unmatched, title) 142 | unlet unmatched[title] 143 | endif 144 | endfor 145 | call add(grouped_notes, {'name': tagname, 'notes': matched_notes}) 146 | let numtags += 1 147 | endif 148 | endfor 149 | " Add a "fake tag" with all unmatched notes. 150 | if !empty(unmatched) 151 | call add(grouped_notes, {'name': "Unmatched notes", 'notes': keys(unmatched)}) 152 | endif 153 | " Format the results as a note. 154 | let bullet = xolox#notes#get_bullet('*') 155 | for group in grouped_notes 156 | let tagname = group['name'] 157 | let friendly_name = xolox#notes#tags#friendly_name(tagname) 158 | let numnotes = len(group['notes']) 159 | if numnotes >= a:minsize 160 | call extend(lines, ['', printf('# %s (%i note%s)', friendly_name, numnotes, numnotes == 1 ? '' : 's'), '']) 161 | for title in group['notes'] 162 | let lastmodified = xolox#notes#friendly_date(getftime(xolox#notes#title_to_fname(title))) 163 | call add(lines, ' ' . bullet . ' ' . title . ' (last edited ' . lastmodified . ')') 164 | endfor 165 | endif 166 | endfor 167 | if a:minsize <= 1 168 | let message = printf("You've used %i %s in %i %s", 169 | \ numtags, numtags == 1 ? "tag" : "tags", 170 | \ totalnotes, totalnotes == 1 ? "note" : "notes") 171 | else 172 | let message = printf("There %s %i %s that %s been used at least %s times", 173 | \ numtags == 1 ? "is" : "are", numtags, 174 | \ numtags == 1 ? "tag" : "tags", 175 | \ numtags == 1 ? "has" : "have", a:minsize) 176 | endif 177 | let message .= ", " . (numtags == 1 ? "it's" : "they're") 178 | let message .= " listed below. Tags and notes are sorted alphabetically and after each note is the date when it was last modified." 179 | if !empty(unmatched) 180 | if a:minsize <= 1 181 | let message .= " At the bottom is a list of untagged notes." 182 | else 183 | let message .= " At the bottom is a list of unmatched notes." 184 | endif 185 | endif 186 | if numtags > 1 && !(&foldmethod == 'expr' && &foldenable) 187 | let message .= " You can enable text folding to get an overview of just the tag names and how many times they've been used." 188 | endif 189 | call insert(lines, message, 2) 190 | endif 191 | call xolox#misc#buffer#prepare(s:buffer_name) 192 | call setline(1, lines) 193 | call xolox#misc#buffer#lock() 194 | call xolox#notes#set_filetype() 195 | setlocal nospell wrap 196 | call xolox#misc#timer#stop('notes.vim %s: Generated [%s] in %s.', g:xolox#notes#version, s:buffer_name, starttime) 197 | endfunction 198 | 199 | function! xolox#notes#tags#friendly_name(tagname) " {{{1 200 | return substitute(a:tagname, '\(\U\)\(\u\)', '\1 \2', 'g') 201 | endfunction 202 | -------------------------------------------------------------------------------- /doc/notes.txt: -------------------------------------------------------------------------------- 1 | *notes.txt* Easy note taking in Vim 2 | 3 | =============================================================================== 4 | Contents ~ 5 | 6 | 1. Introduction |notes-introduction| 7 | 2. Install & usage |notes-install-usage| 8 | 3. Options |notes-options| 9 | 1. The |g:notes_directories| option 10 | 1. Backwards compatibility |notes-backwards-compatibility| 11 | 2. The |g:notes_suffix| option 12 | 3. The |g:notes_title_sync| option 13 | 4. The |g:notes_word_boundaries| option 14 | 5. The |g:notes_unicode_enabled| option 15 | 6. The |g:notes_smart_quotes| option 16 | 7. The |g:notes_ruler_text| option 17 | 8. The |g:notes_list_bullets| option 18 | 9. The |g:notes_tab_indents| option 19 | 10. The |g:notes_alt_indents| option 20 | 11. The |g:notes_shadowdir| option 21 | 12. The |g:notes_indexfile| option 22 | 13. The |g:notes_indexscript| option 23 | 14. The |g:notes_tagsindex| option 24 | 15. The |g:notes_markdown_program| option 25 | 16. The |g:notes_conceal_code| option 26 | 17. The |g:notes_conceal_italic| option 27 | 18. The |g:notes_conceal_bold| option 28 | 19. The |g:notes_conceal_url| option 29 | 4. Commands |notes-commands| 30 | 1. The |:Note| command 31 | 2. The |:NoteFromSelectedText| command 32 | 3. The |:SplitNoteFromSelectedText| command 33 | 4. The |:TabNoteFromSelectedText| command 34 | 5. The |:DeleteNote| command 35 | 6. The |:SearchNotes| command 36 | 1. |:SearchNotes| understands @tags |searchnotes-understands-tags| 37 | 2. Accelerated searching with Python |notes-accelerated-searching-with-python| 38 | 7. The |:RelatedNotes| command 39 | 8. The |:RecentNotes| command 40 | 9. The |:MostRecentNote| command 41 | 10. The |:ShowTaggedNotes| command 42 | 11. The |:IndexTaggedNotes| command 43 | 12. The |:NoteToHtml| command 44 | 13. The |:NoteToMarkdown| command 45 | 14. The |:NoteToMediawiki| command 46 | 5. Mappings |notes-mappings| 47 | 1. Insert mode mappings |notes-insert-mode-mappings| 48 | 6. Customizing the syntax highlighting of notes |customizing-syntax-highlighting-of-notes| 49 | 7. Other plug-ins that work well with the notes plug-in |other-plug-ins-that-work-well-with-notes-plug-in| 50 | 1. utl.vim |notes-utl.vim| 51 | 2. vim-shell |notes-vim-shell| 52 | 3. VOoM |notes-voom| 53 | 4. Txtfmt |notes-txtfmt| 54 | 8. Using the notes file type for git commit messages |using-notes-file-type-for-git-commit-messages| 55 | 9. Contact |notes-contact| 56 | 10. License |notes-license| 57 | 11. References |notes-references| 58 | 59 | =============================================================================== 60 | *notes-introduction* 61 | Introduction ~ 62 | 63 | The vim-notes plug-in for the Vim text editor makes it easy to manage your 64 | notes in Vim: 65 | 66 | - **Starting a new note:** Execute the |:Note| command to create a new buffer 67 | and load the appropriate file type and syntax 68 | 69 | - You can also start a note with Vim commands like ':edit', ':tabedit' and 70 | ':split' by starting the filename with 'note:', as in ':edit note:todo' 71 | (the part after 'note:' doesn't have to be the complete note title and if 72 | it's empty a new note will be created) 73 | 74 | - You can start a new note with the selected text as title in the current 75 | window using the '\en' mapping or |:NoteFromSelectedText| command (there 76 | are similar mappings and commands for opening split windows and tab pages) 77 | 78 | - **Saving notes:** Just use Vim's |:write| and |:update| commands, you don't 79 | need to provide a filename because it will be set based on the title (first 80 | line) of your note (you also don't need to worry about special characters, 81 | they'll be escaped) 82 | 83 | - **Editing existing notes:** Execute ':Note anything' to edit a note 84 | containing 'anything' in its title (if no notes are found a new one is 85 | created with its title set to 'anything') 86 | 87 | - The |:Note| and |:DeleteNote| commands support tab completion of note 88 | titles 89 | 90 | - **Deleting notes:** The |:DeleteNote| command enables you to delete the 91 | current note 92 | 93 | - **Searching notes:**':SearchNotes keyword …' searches for keywords and 94 | ':SearchNotes /pattern/' searches for regular expressions 95 | 96 | - The |:SearchNotes| command supports tab completion of keywords and sorts 97 | candidates by relevance (Levenshtein distance [1]) 98 | 99 | - **Smart defaults:** Without an argument |:SearchNotes| searches for the 100 | word under the cursor (if the word starts with '@' that character will be 101 | included in the search, this means you can easily search for _@tagged_ 102 | notes) 103 | 104 | - **Back-references:** The |:RelatedNotes| command find all notes referencing 105 | the current file 106 | 107 | - A Python 2 [2] script is included that accelerates keyword searches using a 108 | keyword index 109 | 110 | - The |:RecentNotes| command lists your notes by modification date, starting 111 | with the most recently edited note 112 | 113 | - **Navigating within notes:** The vim-notes syntax uses atx-style headers 114 | just like Markdown [3] (one to six '#' marks at the start of the line) and 115 | supports text folding based on these headers. This allows easy navigation 116 | within notes that contain large (and possibly nested) sections of text 117 | separated by headers. Here's a screen shot of text folding [4]. 118 | 119 | - **Navigating between notes:** The included syntax script highlights note 120 | names as hyper links and the file type plug-in redefines |gf| to jump 121 | between notes (the Control-w f (see |CTRL-W_f|) mapping to jump to a note 122 | in a split window and the Control-w gf (see |CTRL-W_gf|) mapping to jump to 123 | a note in a new tab page also work) 124 | 125 | - **Writing aids:** The included file type plug-in contains mappings for 126 | automatic curly quotes, arrows and list bullets and supports completion of 127 | note titles using Control-X Control-U and completion of tags using 128 | Control-X Control-O 129 | 130 | - **Embedded file types:** The included syntax script supports embedded 131 | highlighting using blocks marked with '{{{type … }}}' (triple back ticks 132 | ala GFM [5] are also supported) which allows you to embed highlighted code 133 | and configuration snippets in your notes 134 | 135 | Here's a screen shot of the syntax mode (using the Slate [6] color scheme and 136 | the Monaco [7] font): 137 | 138 | Image: Syntax mode screen shot (see reference [8]) 139 | 140 | =============================================================================== 141 | *notes-install-usage* 142 | Install & usage ~ 143 | 144 | Please refer to the installation instructions [9] available on GitHub. Once 145 | you've installed the plug-in you can get started by executing |:Note| or ':edit 146 | note:', this will start a new note that contains instructions on how to 147 | continue from there (and how to use the plug-in in general). 148 | 149 | Make sure 'filetype plugin on' (or a variant of that command) is included in 150 | your |vimrc| script, without that things will not work as intended :-). 151 | 152 | =============================================================================== 153 | *notes-options* 154 | Options ~ 155 | 156 | All options have reasonable defaults so if the plug-in works after installation 157 | you don't need to change any options. The options are available for people who 158 | like to customize how the plug-in works. You can set these options in your 159 | |vimrc| script by including a line like this: 160 | > 161 | :let g:notes_directories = ['~/Documents/Notes', '~/Dropbox/Shared Notes'] 162 | < 163 | Note that after changing an option in your |vimrc| script you have to restart 164 | Vim for the changes to take effect. 165 | 166 | ------------------------------------------------------------------------------- 167 | The *g:notes_directories* option 168 | 169 | Your notes are stored in one or more directories. This option defines where you 170 | want to store your notes. Its value should be a list (there's an example above) 171 | with one or more pathnames. The default is a single value which depends on 172 | circumstances but should work for most people: 173 | 174 | - If the profile directory where the plug-in is installed is writable, the 175 | directory 'misc/notes/user' under the profile directory is used. This is 176 | for compatibility with Pathogen [10]; the notes will be stored inside the 177 | plug-in's bundle. 178 | 179 | - If the above doesn't work out, the default depends on the platform: 180 | '~/vimfiles/misc/notes/user' on Windows and '~/.vim/misc/notes/user' on 181 | other platforms. 182 | 183 | ------------------------------------------------------------------------------- 184 | *notes-backwards-compatibility* 185 | Backwards compatibility ~ 186 | 187 | In the past the notes plug-in only supported a single directory and the 188 | corresponding option was called 'g:notes_directory'. When support for multiple 189 | notes directories was introduced the option was renamed to 190 | |g:notes_directories| to reflect that the value is now a list of directory 191 | pathnames. 192 | 193 | For backwards compatibility with old configurations (all of them as of this 194 | writing :-) the notes plug-in still uses 'g:notes_directory' when it is defined 195 | (its no longer defined by the plug-in). However when the plug-in warns you to 196 | change your configuration you probably should because this compatibility will 197 | be removed at some point. 198 | 199 | ------------------------------------------------------------------------------- 200 | The *g:notes_suffix* option 201 | 202 | The suffix to add to generated filenames. The plug-in generates filenames for 203 | your notes based on the title (first line) of each note and by default these 204 | filenames don't include an extension like '.txt'. You can use this option to 205 | make the plug-in automatically append an extension without having to embed the 206 | extension in the note's title, e.g.: 207 | > 208 | :let g:notes_suffix = '.txt' 209 | < 210 | ------------------------------------------------------------------------------- 211 | The *g:notes_title_sync* option 212 | 213 | When you rename a file in your notes directory but don't change the title, the 214 | plug-in will notice this the next time you open the note in Vim. Likewise when 215 | you change the title in another text editor but don't rename the file. By 216 | default the plug-in will prompt you whether you want it to update the title of 217 | the note, rename the file on disk or dismiss the prompt without doing anything. 218 | 219 | If you set this option to the string "'no'" this feature will be completely 220 | disabled. If you set it to "'change_title'" it will automatically change the 221 | title to match the filename. If you set it to "'rename_file'" it will 222 | automatically rename the file on disk to match the title. 223 | 224 | This option only concerns the behavior of vim-notes when you open an existing 225 | note; it does not change the fact that when you change a note's title in Vim 226 | and then save the note, the file is renamed (this is a fundamental feature of 227 | the vim-notes plug-in). 228 | 229 | ------------------------------------------------------------------------------- 230 | The *g:notes_word_boundaries* option 231 | 232 | Old versions of the notes plug-in would highlight note titles without 233 | considering word boundaries. This is still the default behavior but the plug-in 234 | can now be told to respect word boundaries by changing this option from its 235 | default: 236 | > 237 | :let g:notes_word_boundaries = 1 238 | < 239 | ------------------------------------------------------------------------------- 240 | The *g:notes_unicode_enabled* option 241 | 242 | By default the vim-notes plug-in uses Unicode characters (e.g. list bullets, 243 | arrows, etc.) when Vim's |'encoding'| option is set to UTF-8. If you don't want 244 | Unicode characters in your notes (regardless of the |'encoding'| option) you 245 | can set this option to false (0): 246 | > 247 | :let g:notes_unicode_enabled = 0 248 | < 249 | ------------------------------------------------------------------------------- 250 | The *g:notes_smart_quotes* option 251 | 252 | By default the notes plug-in automatically performs several substitutions on 253 | the text you type in insert mode, for example regular quote marks are replaced 254 | with curly quotes. The full list of substitutions can be found below in the 255 | documentation on mappings. If you don't want the plug-in to perform these 256 | substitutions, you can set this option to zero like this: 257 | > 258 | :let g:notes_smart_quotes = 0 259 | < 260 | ------------------------------------------------------------------------------- 261 | The *g:notes_ruler_text* option 262 | 263 | The text of the ruler line inserted when you type '***' in quick succession. It 264 | defaults to three asterisks separated by spaces, center aligned to the text 265 | width. 266 | 267 | ------------------------------------------------------------------------------- 268 | The *g:notes_list_bullets* option 269 | 270 | A list of characters used as list bullets. When you're using a Unicode encoding 271 | this defaults to "['•', '◦', '▸', '▹', '▪', '▫']", otherwise it defaults to 272 | "['*', '-', '+']". 273 | 274 | When you change the nesting level (indentation) of a line containing a bullet 275 | point using one of the mappings 'Tab', 'Shift-Tab', 'Alt-Left' and 'Alt-Right' 276 | the bullet point will be automatically changed to correspond to the new nesting 277 | level. 278 | 279 | The first level of list items gets the first bullet point in 280 | |g:notes_list_bullets|, the second level gets the second, etc. When you're 281 | indenting a list item to a level where the |g:notes_list_bullets| doesn't have 282 | enough bullets, the plug-in starts again at the first bullet in the list (in 283 | other words the selection of bullets wraps around). 284 | 285 | ------------------------------------------------------------------------------- 286 | The *g:notes_tab_indents* option 287 | 288 | By default 'Tab' is mapped to indent list items and 'Shift-Tab' is mapped to 289 | dedent list items. You can disable these mappings by adding the following to 290 | your |vimrc| script: 291 | > 292 | :let g:notes_tab_indents = 0 293 | < 294 | ------------------------------------------------------------------------------- 295 | The *g:notes_alt_indents* option 296 | 297 | By default 'Alt-Right' is mapped to indent list items and 'Alt-Left' is mapped 298 | to dedent list items. You can disable these mappings by adding the following to 299 | your |vimrc| script: 300 | > 301 | :let g:notes_alt_indents = 0 302 | < 303 | ------------------------------------------------------------------------------- 304 | The *g:notes_shadowdir* option 305 | 306 | The notes plug-in comes with some default notes containing documentation about 307 | the plug-in. This option defines the path of the directory containing these 308 | notes. 309 | 310 | ------------------------------------------------------------------------------- 311 | The *g:notes_indexfile* option 312 | 313 | This option defines the pathname of the optional keyword index used by the 314 | |:SearchNotes| to perform accelerated keyword searching. 315 | 316 | ------------------------------------------------------------------------------- 317 | The *g:notes_indexscript* option 318 | 319 | This option defines the pathname of the Python script that's used to perform 320 | accelerated keyword searching with |:SearchNotes|. 321 | 322 | ------------------------------------------------------------------------------- 323 | The *g:notes_tagsindex* option 324 | 325 | This option defines the pathname of the text file that stores the list of known 326 | tags used for tag name completion and the |:ShowTaggedNotes| command. The text 327 | file is created automatically when it's first needed, after that you can 328 | recreate it manually by executing |:IndexTaggedNotes| (see below). 329 | 330 | ------------------------------------------------------------------------------- 331 | The *g:notes_markdown_program* option 332 | 333 | The |:NoteToHtml| command requires the Markdown [3] program. By default the 334 | name of this program is assumed to be simply 'markdown'. If you want to use a 335 | different program for Markdown to HTML conversion, set this option to the name 336 | of the program. 337 | 338 | ------------------------------------------------------------------------------- 339 | The *g:notes_conceal_code* option 340 | 341 | By default the backticks that mark inline code snippets and the curly quotes 342 | that mark code blocks are hidden when your version of Vim supports concealing 343 | of text. By setting this option to zero you stop vim-notes from hiding these 344 | markers. For example in the following sentence, the backticks would be visible 345 | in the editor when this option is set to zero: 346 | > 347 | This is a sentence with an `inline code` fragment. 348 | < 349 | ------------------------------------------------------------------------------- 350 | The *g:notes_conceal_italic* option 351 | 352 | By default the underscores that mark italic text are hidden when your version 353 | of Vim supports concealing of text. By setting this option to zero you stop 354 | vim-notes from hiding those underscores. In the following example, the 355 | underscores would be visible in the editor when this option is set to zero: 356 | > 357 | This is a sentence with _italic_ text. 358 | < 359 | ------------------------------------------------------------------------------- 360 | The *g:notes_conceal_bold* option 361 | 362 | By default the stars that mark bold text are hidden when your version of Vim 363 | supports concealing of text. By setting this option to zero you stop vim-notes 364 | from hiding those stars. In the following example, the stars would be visible 365 | in the editor when this option is set to zero: 366 | > 367 | This is a sentence with *bold* text. 368 | < 369 | ------------------------------------------------------------------------------- 370 | The *g:notes_conceal_url* option 371 | 372 | By default URL schemes (text fragments like 'http://') are hidden when your 373 | version of Vim supports concealing of text. By setting this option to zero you 374 | stop vim-notes from hiding URL schemes. In the following example, the 375 | 'https://' text would be visible in the editor when this option is set to zero: 376 | > 377 | You can find the vim-notes plug-in at https://github.com/xolox/vim-notes. 378 | < 379 | =============================================================================== 380 | *notes-commands* 381 | Commands ~ 382 | 383 | To edit one of your existing notes (or create a new one) you can use Vim 384 | commands such as |:edit|, |:split| and |:tabedit| with a filename that starts 385 | with _note:_ followed by (part of) the title of one of your notes, e.g.: 386 | > 387 | :edit note:todo 388 | < 389 | This shortcut also works from the command line: 390 | > 391 | $ gvim note:todo 392 | < 393 | When you don't follow _note:_ with anything a new note is created like when you 394 | execute |:Note| without any arguments. If the _note:_ shortcut is used from the 395 | command line, the environment variable '$VIM_NOTES_TEMPLATE' can be set to the 396 | filename of a template for new notes (this will override the default template). 397 | 398 | ------------------------------------------------------------------------------- 399 | The *:Note* command 400 | 401 | When executed without any arguments this command starts a new note in the 402 | current window. If you pass one or more arguments the command will edit an 403 | existing note containing the given words in the title. If more than one note is 404 | found you'll be asked which note you want to edit. If no notes are found a new 405 | note is started with the given word(s) as title. 406 | 407 | This command will fail when changes have been made to the current buffer, 408 | unless you use ':Note!' which discards any changes. 409 | 410 | When you are using multiple directories to store your notes and you run |:Note| 411 | while editing an existing note, a new note will inherit the directory of the 412 | note from which you started. Otherwise the note is created in the first 413 | directory in |g:notes_directories|. 414 | 415 | _This command supports tab completion:_ If you complete one word, all existing 416 | notes containing the given word somewhere in their title are suggested. If you 417 | type more than one word separated by spaces, the plug-in will complete only the 418 | missing words so that the resulting command line contains the complete note 419 | title and nothing more. 420 | 421 | ------------------------------------------------------------------------------- 422 | The *:NoteFromSelectedText* command 423 | 424 | Start a new note in the current window with the selected text as the title of 425 | the note. The name of this command isn't very well suited to daily use, that's 426 | because it's intended to be executed from a mapping. The default mapping for 427 | this command is '\en' (the backslash is actually the character defined by the 428 | |mapleader| variable). 429 | 430 | When you are using multiple directories to store your notes and you run 431 | |:NoteFromSelectedText| while editing an existing note, the new note will 432 | inherit the directory of the note from which it was created. 433 | 434 | ------------------------------------------------------------------------------- 435 | The *:SplitNoteFromSelectedText* command 436 | 437 | Same as |:NoteFromSelectedText| but opens the new note in a vertical split 438 | window. The default mapping for this command is '\sn'. 439 | 440 | ------------------------------------------------------------------------------- 441 | The *:TabNoteFromSelectedText* command 442 | 443 | Same as |:NoteFromSelectedText| but opens the new note in a new tab page. The 444 | default mapping for this command is '\tn'. 445 | 446 | ------------------------------------------------------------------------------- 447 | The *:DeleteNote* command 448 | 449 | The |:DeleteNote| command deletes a note file, destroys the buffer and removes 450 | the note from the internal cache of filenames and note titles. If you pass a 451 | note name as an argument to |:DeleteNote| it will delete the given note, 452 | otherwise it will delete the current note. This fails when changes have been 453 | made to the buffer, unless you use ':DeleteNote!' which discards any changes. 454 | 455 | ------------------------------------------------------------------------------- 456 | The *:SearchNotes* command 457 | 458 | This command wraps |:vimgrep| and enables you to search through your notes 459 | using one or more keywords or a regular expression pattern. To search for a 460 | pattern you pass a single argument that starts/ends with a slash: 461 | > 462 | :SearchNotes /TODO\|FIXME\|XXX/ 463 | < 464 | To search for one or more keywords you can just omit the slashes, this matches 465 | notes containing all of the given keywords: 466 | > 467 | :SearchNotes syntax highlighting 468 | < 469 | ------------------------------------------------------------------------------- 470 | *searchnotes-understands-tags* 471 | :SearchNotes understands @tags ~ 472 | 473 | If you don't pass any arguments to the |:SearchNotes| command it will search 474 | for the word under the cursor. If the word under the cursor starts with '@' 475 | this character will be included in the search, which makes it possible to 476 | easily add _@tags_ to your _@notes_ and then search for those tags. To make 477 | searching for tags even easier you can create key mappings for the 478 | |:SearchNotes| command: 479 | > 480 | " Make the C-] combination search for @tags: 481 | imap :SearchNotes 482 | nmap :SearchNotes 483 | 484 | " Make double mouse click search for @tags. This is actually quite a lot of 485 | " fun if you don't use the mouse for text selections anyway; you can click 486 | " between notes as if you're in a web browser: 487 | imap <2-LeftMouse> :SearchNotes 488 | nmap <2-LeftMouse> :SearchNotes 489 | < 490 | These mappings are currently not enabled by default because they conflict with 491 | already useful key mappings, but if you have any suggestions for alternatives 492 | feel free to contact me through GitHub or at peter@peterodding.com. 493 | 494 | ------------------------------------------------------------------------------- 495 | *notes-accelerated-searching-with-python* 496 | Accelerated searching with Python ~ 497 | 498 | After collecting a fair amount of notes (say more than 5 MB) you will probably 499 | start to get annoyed at how long it takes Vim to search through all of your 500 | notes. To make searching more scalable the notes plug-in includes a Python 501 | script which uses a persistent full text index of your notes stored in a file. 502 | 503 | The first time the Python script is run it will need to build the complete 504 | index which can take a moment, but after the index has been initialized updates 505 | and searches should be more or less instantaneous. 506 | 507 | ------------------------------------------------------------------------------- 508 | The *:RelatedNotes* command 509 | 510 | This command makes it easy to find all notes related to the current file: If 511 | you are currently editing a note then a search for the note's title is done, 512 | otherwise this searches for the absolute path of the current file. 513 | 514 | ------------------------------------------------------------------------------- 515 | The *:RecentNotes* command 516 | 517 | If you execute the |:RecentNotes| command it will open a Vim buffer that lists 518 | all your notes grouped by the day they were edited, starting with your most 519 | recently edited note. If you pass an argument to |:RecentNotes| it will filter 520 | the list of notes by matching the title of each note against the argument which 521 | is interpreted as a Vim pattern. 522 | 523 | ------------------------------------------------------------------------------- 524 | The *:MostRecentNote* command 525 | 526 | This command edits your most recently edited note (whether you just opened the 527 | note or made changes to it). The plug-in will remember the most recent note 528 | between restarts of Vim and is shared between all instances of Vim. 529 | 530 | ------------------------------------------------------------------------------- 531 | The *:ShowTaggedNotes* command 532 | 533 | To show a list of all notes that contains _@tags_ you can use the 534 | |:ShowTaggedNotes| command. If you pass a count to this command it will limit 535 | the list of tags to those that have been used at least this many times. For 536 | example the following two commands show tags that have been used at least ten 537 | times: 538 | > 539 | :10ShowTaggedNotes 540 | :ShowTaggedNotes 10 541 | < 542 | ------------------------------------------------------------------------------- 543 | The *:IndexTaggedNotes* command 544 | 545 | The notes plug-in defines an omni completion function that can be used to 546 | complete the names of tags. To trigger the omni completion you type Control-X 547 | Control-O. When you type '@' in insert mode the plug-in will automatically 548 | start omni completion. 549 | 550 | The completion menu is populated from a text file listing all your tags, one on 551 | each line. The first time omni completion triggers, an index of tag names is 552 | generated and saved to the location set by |g:notes_tagsindex|. After this file 553 | is created, it will be updated automatically as you edit notes and add/remove 554 | tags. 555 | 556 | If for any reason you want to recreate the list of tags you can execute the 557 | |:IndexTaggedNotes| command. 558 | 559 | ------------------------------------------------------------------------------- 560 | The *:NoteToHtml* command 561 | 562 | This command converts the current note to HTML. It works by first converting 563 | the current note to Markdown [3] and then using the 'markdown' program to 564 | convert that to HTML. It requires an external program to convert Markdown to 565 | HTML. By default the program 'markdown' is used, but you can change the name of 566 | the program using the |g:notes_markdown_program| option. To convert your note 567 | to HTML and open the generated web page in a browser, you can run: 568 | > 569 | :NoteToHtml 570 | < 571 | Alternatively, to convert your note to HTML and display it in a new split 572 | window in Vim, you can run: 573 | > 574 | :NoteToHtml split 575 | < 576 | Note that this command can be a bit slow, because the parser for the note 577 | taking syntax is written in Vim script (for portability) and has not been 578 | optimized for speed (yet). 579 | 580 | ------------------------------------------------------------------------------- 581 | The *:NoteToMarkdown* command 582 | 583 | Convert the current note to a Markdown document [3]. The vim-notes syntax 584 | shares a lot of similarities with the Markdown text format, but there are some 585 | notable differences, which this command takes care of: 586 | 587 | - The first line of a note is an implicit document title. In Markdown format 588 | it has to be marked with '#'. This also implies that the remaining headings 589 | should be shifted by one level. 590 | 591 | - Preformatted blocks are marked very differently in notes and Markdown 592 | ('{{{' and '}}}' markers versus 4 space indentation). 593 | 594 | - The markers and indentation of list items differ between notes and Markdown 595 | (dumb bullets vs Unicode bullets and 3 vs 4 spaces). 596 | 597 | Note that this command can be a bit slow, because the parser for the note 598 | taking syntax is written in Vim script (for portability) and has not been 599 | optimized for speed (yet). 600 | 601 | ------------------------------------------------------------------------------- 602 | The *:NoteToMediawiki* command 603 | 604 | Convert the current note to a Mediawiki document [11]. This is similar to the 605 | |:NoteToMarkdown| command, but it produces wiki text that can be displayed on a 606 | Mediawiki site. That being said, the subset of wiki markup that vim-notes 607 | actually produces will probably work on other wiki sites. These are the notable 608 | transforations: 609 | 610 | - The first line of the note is a title, but it isn't used in the Mediawiki 611 | syntax. It could have been put into a '= Title =' tag, but it doesn't 612 | really make sense in the context of a wiki. It would make the table of 613 | contents nest under the title for every document you create. 614 | 615 | - Preformatted blocks are output into '' tags. 616 | This functionality is enabled on Mediawiki through the SyntaxHighlight 617 | GeSHi extention [12]. It is also supported on Wikipedia. 618 | 619 | =============================================================================== 620 | *notes-mappings* 621 | Mappings ~ 622 | 623 | The following key mappings are defined inside notes. 624 | 625 | ------------------------------------------------------------------------------- 626 | *notes-insert-mode-mappings* 627 | Insert mode mappings ~ 628 | 629 | - '@' automatically triggers tag completion 630 | - "'" becomes '‘' or '’' depending on where you type it 631 | - '"' becomes '“' or '”' (same goes for these) 632 | - '--' becomes '—' 633 | - '->' becomes '→' 634 | - '<-' becomes '←' 635 | - the bullets '*', '-' and '+' become '•' 636 | - the three characters '***' in insert mode in quick succession insert a 637 | horizontal ruler delimited by empty lines 638 | - 'Tab' and 'Alt-Right' increase indentation of list items (works on the 639 | current line and selected lines) 640 | - 'Shift-Tab' and 'Alt-Left' decrease indentation of list items 641 | - 'Enter' on a line with only a list bullet removes the bullet and starts a 642 | new line below the current line 643 | - '\en' executes |:NoteFromSelectedText| 644 | - '\sn' executes |:SplitNoteFromSelectedText| 645 | - '\tn' executes |:TabNoteFromSelectedText| 646 | 647 | =============================================================================== 648 | *customizing-syntax-highlighting-of-notes* 649 | Customizing the syntax highlighting of notes ~ 650 | 651 | The syntax mode for notes is written so you can override styles you don't like. 652 | To do so you can add lines such as the following to your |vimrc| script: 653 | > 654 | " Don't highlight single quoted strings. 655 | highlight link notesSingleQuoted Normal 656 | 657 | " Show double quoted strings in italic font. 658 | highlight notesDoubleQuoted gui=italic 659 | < 660 | See the documentation of the |:highlight| command for more information. Below 661 | are the names of the syntax items defined by the notes syntax mode: 662 | 663 | - 'notesName' - the names of other notes, usually highlighted as a hyperlink 664 | - 'notesTagName' - words preceded by an '@' character, also highlighted as a 665 | hyperlink 666 | - 'notesListBullet' - the bullet characters used for list items 667 | - 'notesListNumber' - numbers in front of list items 668 | - 'notesDoubleQuoted' - double quoted strings 669 | - 'notesSingleQuoted' - single quoted strings 670 | - 'notesItalic' - strings between two '_' characters 671 | - 'notesBold' - strings between two '*' characters 672 | - 'notesTextURL' - plain domain name (recognized by leading 'www.') 673 | - 'notesRealURL' - URLs (e.g. http://vim.org/) 674 | - 'notesEmailAddr' - e-mail addresses 675 | - 'notesUnixPath' - UNIX file paths (e.g. '~/.vimrc' and 676 | '/home/peter/.vimrc') 677 | - 'notesPathLnum' - line number following a UNIX path 678 | - 'notesWindowsPath' - Windows file paths (e.g. 'c:\users\peter\_vimrc') 679 | - 'notesTodo' - 'TODO' markers 680 | - 'notesXXX' - 'XXX' markers 681 | - 'notesFixMe' - 'FIXME' markers 682 | - 'notesInProgress' - 'CURRENT', 'INPROGRESS', 'STARTED' and 'WIP' markers 683 | - 'notesDoneItem' - lines containing the marker 'DONE', usually highlighted 684 | as a comment 685 | - 'notesDoneMarker' - 'DONE' markers 686 | - 'notesVimCmd' - Vim commands, words preceded by an ':' character 687 | - 'notesTitle' - the first line of each note 688 | - 'notesShortHeading' - short sentences ending in a ':' character 689 | - 'notesAtxHeading' - lines preceded by one or more '#' characters 690 | - 'notesBlockQuote' - lines preceded by a '>' character 691 | - 'notesRule' - lines containing only whitespace and '* * *' 692 | - 'notesCodeStart' - the '{{{' markers that begin a block of code (including 693 | the syntax name) 694 | - 'notesCodeEnd' - the '}}}' markers that end a block of code 695 | - 'notesModeLine' - Vim |modeline| in last line of notes 696 | - 'notesLastEdited' - last edited dates in |:ShowTaggedNotes| buffers 697 | 698 | =============================================================================== 699 | *other-plug-ins-that-work-well-with-notes-plug-in* 700 | Other plug-ins that work well with the notes plug-in ~ 701 | 702 | ------------------------------------------------------------------------------- 703 | *notes-utl.vim* 704 | utl.vim ~ 705 | 706 | The utl.vim [13] universal text linking plug-in enables links between your 707 | notes, other local files and remote resources like web pages. 708 | 709 | ------------------------------------------------------------------------------- 710 | *notes-vim-shell* 711 | vim-shell ~ 712 | 713 | My vim-shell [14] plug-in also enables easy navigation between your notes and 714 | environment like local files and directories, web pages and e-mail addresses by 715 | providing key mappings and commands to e.g. open the file/URL under the text 716 | cursor. This plug-in can also change Vim to full screen which can be really 717 | nice for large notes. 718 | 719 | ------------------------------------------------------------------------------- 720 | *notes-voom* 721 | VOoM ~ 722 | 723 | The VOoM [15] outlining plug-in should work well for notes if you use the 724 | Markdown style headers starting with '#', however it has been reported that 725 | this combination may not always work so well in practice (sometimes losing 726 | notes!) 727 | 728 | ------------------------------------------------------------------------------- 729 | *notes-txtfmt* 730 | Txtfmt ~ 731 | 732 | If the text formatting supported by the notes plug-in is not enough for you, 733 | consider trying the Txtfmt [16] (The Vim Highlighter) plug-in. To use the two 734 | plug-ins together, create the file 'after/ftplugin/notes.vim' inside your Vim 735 | profile with the following contents: 736 | > 737 | " Enable Txtfmt formatting inside notes. 738 | setlocal filetype=notes.txtfmt 739 | < 740 | =============================================================================== 741 | *using-notes-file-type-for-git-commit-messages* 742 | Using the notes file type for git commit messages ~ 743 | 744 | If you write your git commit messages in Vim and want to use the notes file 745 | type (syntax highlighting and editing mode) to edit your git commit messages 746 | you can add the following line to your |vimrc| script: 747 | > 748 | autocmd BufNewFile,BufRead */.git/COMMIT_EDITMSG setlocal filetype=notes 749 | < 750 | This is not a complete solution (there are more types of commit messages that 751 | the pattern above won't match) but that is outside the scope of this document. 752 | For inspiration you can take a look at the runtime/filetype.vim [17] file in 753 | Vim's Mercurial repository. 754 | 755 | =============================================================================== 756 | *notes-contact* 757 | Contact ~ 758 | 759 | If you have questions, bug reports, suggestions, etc. the author can be 760 | contacted at peter@peterodding.com. The latest version is available at 761 | http://peterodding.com/code/vim/notes/ and http://github.com/xolox/vim-notes. 762 | If you like the script please vote for it on Vim Online [18]. 763 | 764 | =============================================================================== 765 | *notes-license* 766 | License ~ 767 | 768 | This software is licensed under the MIT license [19]. © 2015 Peter Odding 769 | . 770 | 771 | =============================================================================== 772 | *notes-references* 773 | References ~ 774 | 775 | [1] http://en.wikipedia.org/wiki/Levenshtein_distance 776 | [2] http://python.org/ 777 | [3] http://en.wikipedia.org/wiki/Markdown 778 | [4] https://raw.githubusercontent.com/xolox/vim-notes/master/screenshots/folding.png 779 | [5] https://help.github.com/articles/github-flavored-markdown/ 780 | [6] http://code.google.com/p/vim/source/browse/runtime/colors/slate.vim 781 | [7] http://en.wikipedia.org/wiki/Monaco_(typeface) 782 | [8] http://peterodding.com/code/vim/notes/syntax.png 783 | [9] https://github.com/xolox/vim-notes/blob/master/INSTALL.md 784 | [10] http://www.vim.org/scripts/script.php?script_id=2332 785 | [11] https://www.mediawiki.org/wiki/MediaWiki 786 | [12] http://www.mediawiki.org/wiki/Extension:SyntaxHighlight_GeSHi 787 | [13] http://www.vim.org/scripts/script.php?script_id=293 788 | [14] http://www.vim.org/scripts/script.php?script_id=3123 789 | [15] http://www.vim.org/scripts/script.php?script_id=2657 790 | [16] http://www.vim.org/scripts/script.php?script_id=2208 791 | [17] https://code.google.com/p/vim/source/browse/runtime/filetype.vim?r=fbc1131f0ba5be4ec74fb2ccdfb3559b446a2b1e#778 792 | [18] http://www.vim.org/scripts/script.php?script_id=3375 793 | [19] http://en.wikipedia.org/wiki/MIT_License 794 | 795 | vim: ft=help 796 | -------------------------------------------------------------------------------- /ftplugin/notes.vim: -------------------------------------------------------------------------------- 1 | " Vim file type plug-in 2 | " Author: Peter Odding 3 | " Last Change: September 14, 2014 4 | " URL: http://peterodding.com/code/vim/notes/ 5 | 6 | if exists('b:did_ftplugin') 7 | finish 8 | else 9 | let b:did_ftplugin = 1 10 | endif 11 | 12 | " Add dash to keyword characters so it can be used in tags. {{{1 13 | setlocal iskeyword+=- 14 | let b:undo_ftplugin = 'set iskeyword<' 15 | 16 | " Copy indent from previous line. {{{1 17 | setlocal autoindent 18 | let b:undo_ftplugin = 'set autoindent<' 19 | 20 | " Set &tabstop and &shiftwidth options for bulleted lists. {{{1 21 | setlocal tabstop=3 shiftwidth=3 expandtab 22 | let b:undo_ftplugin .= ' | set tabstop< shiftwidth< expandtab<' 23 | 24 | " Automatic formatting for bulleted lists. {{{1 25 | let &l:comments = xolox#notes#get_comments_option() 26 | setlocal formatoptions=tcron 27 | let b:undo_ftplugin .= ' | set comments< formatoptions<' 28 | 29 | " Automatic text folding based on headings. {{{1 30 | setlocal foldmethod=expr 31 | setlocal foldexpr=xolox#notes#foldexpr() 32 | setlocal foldtext=xolox#notes#foldtext() 33 | let b:undo_ftplugin .= ' | set foldmethod< foldexpr< foldtext<' 34 | 35 | " Enable concealing of notes syntax markers? {{{1 36 | if has('conceal') 37 | setlocal conceallevel=3 38 | let b:undo_ftplugin .= ' | set conceallevel<' 39 | endif 40 | 41 | " Change to jump to notes by name. {{{1 42 | setlocal includeexpr=xolox#notes#include_expr(v:fname) 43 | let b:undo_ftplugin .= ' | set includeexpr<' 44 | 45 | " Enable completion of note titles using C-x C-u. {{{1 46 | setlocal completefunc=xolox#notes#user_complete 47 | let b:undo_ftplugin .= ' | set completefunc<' 48 | 49 | " Enable completion of tag names using C-x C-o. {{{1 50 | setlocal omnifunc=xolox#notes#omni_complete 51 | let b:undo_ftplugin .= ' | set omnifunc<' 52 | 53 | " Automatic completion of tag names after typing "@". {{{1 54 | inoremap @ xolox#notes#auto_complete_tags() 55 | let b:undo_ftplugin .= ' | execute "iunmap @"' 56 | 57 | " Automatic completion of tag names should not interrupt the flow of typing, 58 | " for this we have to change the (unfortunately) global option &completeopt. 59 | set completeopt+=longest 60 | 61 | " Change double-dash to em-dash as it is typed. {{{1 62 | inoremap -- xolox#notes#insert_em_dash() 63 | let b:undo_ftplugin .= ' | execute "iunmap --"' 64 | 65 | " Change plain quotes to curly quotes as they're typed. {{{1 66 | inoremap ' xolox#notes#insert_quote("'") 67 | inoremap " xolox#notes#insert_quote('"') 68 | let b:undo_ftplugin .= ' | execute "iunmap ''"' 69 | let b:undo_ftplugin .= ' | execute ''iunmap "''' 70 | 71 | " Change ASCII style arrows to Unicode arrows. {{{1 72 | inoremap <- xolox#notes#insert_left_arrow() 73 | inoremap -> xolox#notes#insert_right_arrow() 74 | inoremap <-> xolox#notes#insert_bidi_arrow() 75 | let b:undo_ftplugin .= ' | execute "iunmap ->"' 76 | let b:undo_ftplugin .= ' | execute "iunmap <-"' 77 | let b:undo_ftplugin .= ' | execute "iunmap <->"' 78 | 79 | " Convert ASCII list bullets to Unicode bullets. {{{1 80 | if g:notes_smart_quotes 81 | inoremap * xolox#notes#insert_bullet('*') 82 | inoremap - xolox#notes#insert_bullet('-') 83 | inoremap + xolox#notes#insert_bullet('+') 84 | let b:undo_ftplugin .= ' | execute "iunmap *"' 85 | let b:undo_ftplugin .= ' | execute "iunmap -"' 86 | let b:undo_ftplugin .= ' | execute "iunmap +"' 87 | endif 88 | 89 | " Format three asterisks as a horizontal ruler. {{{1 90 | inoremap *** :call xolox#notes#insert_ruler() 91 | let b:undo_ftplugin .= ' | execute "iunmap ***"' 92 | 93 | " Indent list items using and ? {{{1 94 | if g:notes_tab_indents 95 | inoremap :call xolox#notes#indent_list(1, line('.'), line('.')) 96 | snoremap :call xolox#notes#indent_list(1, line("'<"), line("'>"))gv 97 | let b:undo_ftplugin .= ' | execute "iunmap "' 98 | let b:undo_ftplugin .= ' | execute "sunmap "' 99 | inoremap :call xolox#notes#indent_list(-1, line('.'), line('.')) 100 | snoremap :call xolox#notes#indent_list(-1, line("'<"), line("'>"))gv 101 | let b:undo_ftplugin .= ' | execute "iunmap "' 102 | let b:undo_ftplugin .= ' | execute "sunmap "' 103 | endif 104 | 105 | " Indent list items using and ? {{{1 106 | if g:notes_alt_indents 107 | inoremap :call xolox#notes#indent_list(1, line('.'), line('.')) 108 | snoremap :call xolox#notes#indent_list(1, line("'<"), line("'>"))gv 109 | let b:undo_ftplugin .= ' | execute "iunmap "' 110 | let b:undo_ftplugin .= ' | execute "sunmap "' 111 | inoremap :call xolox#notes#indent_list(-1, line('.'), line('.')) 112 | snoremap :call xolox#notes#indent_list(-1, line("'<"), line("'>"))gv 113 | let b:undo_ftplugin .= ' | execute "iunmap "' 114 | let b:undo_ftplugin .= ' | execute "sunmap "' 115 | endif 116 | 117 | " Automatically remove empty list items on Enter. {{{1 118 | inoremap xolox#notes#cleanup_list() 119 | let b:undo_ftplugin .= ' | execute "iunmap "' 120 | 121 | " Shortcuts to create new notes from the selected text. {{{1 122 | 123 | vnoremap en :NoteFromSelectedText 124 | let b:undo_ftplugin .= ' | execute "vunmap en"' 125 | 126 | vnoremap sn :SplitNoteFromSelectedText 127 | let b:undo_ftplugin .= ' | execute "vunmap sn"' 128 | 129 | vnoremap tn :TabNoteFromSelectedText 130 | let b:undo_ftplugin .= ' | execute "vunmap tn"' 131 | 132 | " }}}1 133 | 134 | " This is currently the only place where a command is guaranteed to be 135 | " executed when the user edits a note. Maybe I shouldn't abuse this (it 136 | " doesn't feel right ;-) but for now it will do. 137 | call xolox#notes#recent#track() 138 | call xolox#notes#check_sync_title() 139 | 140 | " vim: ts=2 sw=2 et 141 | -------------------------------------------------------------------------------- /misc/notes/search-notes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Python script for fast text file searching using keyword index on disk. 4 | # 5 | # Author: Peter Odding 6 | # Last Change: November 1, 2015 7 | # URL: http://peterodding.com/code/vim/notes/ 8 | # License: MIT 9 | # 10 | # This Python script can be used by the notes.vim plug-in to perform fast 11 | # keyword searches in the user's notes. It has two advantages over just 12 | # using Vim's internal :vimgrep command to search all of the user's notes: 13 | # 14 | # - Very large notes don't slow searching down so much; 15 | # - Hundreds of notes can be searched in less than a second. 16 | # 17 | # The keyword index is a Python dictionary that's persisted using the pickle 18 | # module. The structure of the dictionary may seem very naive but it's quite 19 | # fast. Also the pickle protocol makes sure repeating strings are stored only 20 | # once, so it's not as bad as it may appear at first sight :-). 21 | # 22 | # For more information about the Vim plug-in see http://peterodding.com/code/vim/notes/. 23 | 24 | """ 25 | Usage: search-notes.py [OPTIONS] KEYWORD... 26 | 27 | Search one or more directories of plain text files using a full text index, 28 | updated automatically during each invocation of the program. 29 | 30 | Valid options include: 31 | 32 | -i, --ignore-case ignore case of keyword(s) 33 | -l, --list=SUBSTR list keywords matching substring 34 | -d, --database=FILE set path to keywords index file 35 | -n, --notes=DIR set directory with user notes (can be repeated) 36 | -e, --encoding=NAME set character encoding of notes 37 | -v, --verbose make more noise 38 | -h, --help show this message and exit 39 | 40 | For more information see http://peterodding.com/code/vim/notes/ 41 | """ 42 | 43 | # Standard library modules. 44 | import codecs 45 | import fnmatch 46 | import getopt 47 | import logging 48 | import os 49 | import re 50 | import sys 51 | import time 52 | 53 | # Load the faster C variant of the pickle module where possible, but 54 | # fall back to the Python implementation that's always available. 55 | try: 56 | import cPickle as pickle 57 | except ImportError: 58 | import pickle 59 | 60 | # Compatibility between Python 2 and 3. 61 | try: 62 | # Python 2. 63 | unicode_string = unicode 64 | byte_string = str 65 | except NameError: 66 | # Python 3. 67 | unicode_string = str 68 | byte_string = bytes 69 | 70 | # Compatibility with Python 2.6 which doesn't have logging.NullHandler. 71 | try: 72 | from logging import NullHandler 73 | except ImportError: 74 | 75 | # This class was copied from the Python standard library, specifically 76 | # https://hg.python.org/cpython/file/771f28686022/Lib/logging/__init__.py#l1670 77 | class NullHandler(logging.Handler): 78 | 79 | def handle(self, record): 80 | pass 81 | 82 | def emit(self, record): 83 | pass 84 | 85 | def createLock(self): 86 | self.lock = None 87 | 88 | # Try to import the Levenshtein module, don't error out if it's not installed. 89 | try: 90 | import Levenshtein 91 | LEVENSHTEIN_SUPPORTED = True 92 | except ImportError: 93 | LEVENSHTEIN_SUPPORTED = False 94 | 95 | # The version of the index format that's supported by this revision of the 96 | # `search-notes.py' script; if an existing index file is found with an 97 | # unsupported version, the script knows that it should rebuild the index. 98 | INDEX_VERSION = 2 99 | 100 | # Filename matching patterns of files to ignore during scans. 101 | PATTERNS_TO_IGNORE = ('.swp', '.s??', '.*.s??', '*~') 102 | 103 | 104 | class NotesIndex(object): 105 | 106 | def __init__(self): 107 | """Entry point to the notes search.""" 108 | global_timer = Timer() 109 | self.init_logging() 110 | keywords = self.parse_args() 111 | self.load_index() 112 | self.update_index() 113 | if self.dirty: 114 | self.save_index() 115 | print("Python works fine!") 116 | if self.keyword_filter is not None: 117 | self.list_keywords(self.keyword_filter) 118 | self.logger.debug("Finished listing keywords in %s", global_timer) 119 | else: 120 | matches = self.search_index(keywords) 121 | if matches: 122 | print('\n'.join(sorted(matches))) 123 | self.logger.debug("Finished searching index in %s", global_timer) 124 | 125 | def init_logging(self): 126 | """Initialize the logging subsystem.""" 127 | self.logger = logging.getLogger('search-notes') 128 | self.logger.setLevel(logging.INFO) 129 | if all(map(os.isatty, (0, 1, 2))): 130 | self.logger.addHandler(logging.StreamHandler(sys.stderr)) 131 | else: 132 | self.logger.addHandler(NullHandler()) 133 | 134 | def parse_args(self): 135 | """Parse the command line arguments.""" 136 | try: 137 | opts, keywords = getopt.getopt(sys.argv[1:], 'il:d:n:e:vh', [ 138 | 'ignore-case', 'list=', 'database=', 'notes=', 'encoding=', 139 | 'verbose', 'help', 140 | ]) 141 | except getopt.GetoptError as error: 142 | print(str(error)) 143 | self.usage() 144 | sys.exit(2) 145 | # Define the command line option defaults. 146 | self.database_file = '~/.vim/misc/notes/index.pickle' 147 | self.user_directories = ['~/.vim/misc/notes/user/'] 148 | self.character_encoding = 'UTF-8' 149 | self.case_sensitive = True 150 | self.keyword_filter = None 151 | # Map command line options to variables. 152 | for opt, arg in opts: 153 | if opt in ('-i', '--ignore-case'): 154 | self.case_sensitive = False 155 | self.logger.debug("Disabling case sensitivity") 156 | elif opt in ('-l', '--list'): 157 | self.keyword_filter = arg.strip().lower() 158 | elif opt in ('-d', '--database'): 159 | self.database_file = arg 160 | elif opt in ('-n', '--notes'): 161 | self.user_directories.append(arg) 162 | elif opt in ('-e', '--encoding'): 163 | self.character_encoding = arg 164 | elif opt in ('-v', '--verbose'): 165 | self.logger.setLevel(logging.DEBUG) 166 | elif opt in ('-h', '--help'): 167 | self.usage() 168 | sys.exit(0) 169 | else: 170 | assert False, "Unhandled option" 171 | self.logger.debug("Index file: %s", self.database_file) 172 | self.logger.debug("Notes directories: %r", self.user_directories) 173 | self.logger.debug("Character encoding: %s", self.character_encoding) 174 | if self.keyword_filter is not None: 175 | self.keyword_filter = self.decode(self.keyword_filter) 176 | # Canonicalize pathnames, check validity. 177 | self.database_file = self.munge_path(self.database_file) 178 | self.user_directories = map(self.munge_path, self.user_directories) 179 | self.user_directories = filter(os.path.isdir, self.user_directories) 180 | if not any(os.path.isdir(p) for p in self.user_directories): 181 | sys.stderr.write("None of the notes directories exist!\n") 182 | sys.exit(1) 183 | # Return tokenized keyword arguments. 184 | return [self.normalize(k) for k in self.tokenize(' '.join(keywords))] 185 | 186 | def load_index(self): 187 | """Load the keyword index or start with an empty one.""" 188 | try: 189 | load_timer = Timer() 190 | self.logger.debug("Loading index from %s ..", self.database_file) 191 | with open(self.database_file, 'rb') as handle: 192 | self.index = pickle.load(handle) 193 | self.logger.debug("Format version of index loaded from disk: %i", self.index['version']) 194 | assert self.index['version'] == INDEX_VERSION, "Incompatible index format detected!" 195 | self.first_use = False 196 | self.dirty = False 197 | self.logger.debug("Loaded %i notes from index in %s", len(self.index['files']), load_timer) 198 | except Exception: 199 | self.logger.warn("Failed to load index from file!", exc_info=True) 200 | self.first_use = True 201 | self.dirty = True 202 | self.index = {'keywords': {}, 'files': {}, 'version': INDEX_VERSION} 203 | 204 | def save_index(self): 205 | """Save the keyword index to disk.""" 206 | save_timer = Timer() 207 | with open(self.database_file, 'wb') as handle: 208 | pickle.dump(self.index, handle) 209 | self.logger.debug("Saved index to disk in %s", save_timer) 210 | 211 | def update_index(self): 212 | """Update the keyword index by scanning the notes directory.""" 213 | update_timer = Timer() 214 | # First we find the filenames and last modified times of the notes on disk. 215 | notes_on_disk = {} 216 | last_count = 0 217 | for directory in self.user_directories: 218 | for root, dirs, files in os.walk(directory): 219 | for filename in files: 220 | if not any(fnmatch.fnmatch(filename, pattern) for pattern in PATTERNS_TO_IGNORE): 221 | abspath = os.path.join(root, filename) 222 | notes_on_disk[abspath] = os.path.getmtime(abspath) 223 | self.logger.info("Found %i notes in %s ..", len(notes_on_disk) - last_count, directory) 224 | last_count = len(notes_on_disk) 225 | self.logger.info("Found a total of %i notes ..", len(notes_on_disk)) 226 | # Check for updated and/or deleted notes since the last run? 227 | if not self.first_use: 228 | for filename in self.index['files'].keys(): 229 | if filename not in notes_on_disk: 230 | # Forget a deleted note. 231 | self.delete_note(filename) 232 | else: 233 | # Check whether previously seen note has changed? 234 | last_modified_on_disk = notes_on_disk[filename] 235 | last_modified_in_db = self.index['files'][filename] 236 | if last_modified_on_disk > last_modified_in_db: 237 | self.delete_note(filename) 238 | self.add_note(filename, last_modified_on_disk) 239 | # Already checked this note, we can forget about it. 240 | del notes_on_disk[filename] 241 | # Add new notes to index. 242 | for filename, last_modified in notes_on_disk.items(): 243 | self.add_note(filename, last_modified) 244 | self.logger.debug("Updated index in %s", update_timer) 245 | 246 | def add_note(self, filename, last_modified): 247 | """Add a note to the index (assumes the note is not already indexed).""" 248 | self.logger.info("Adding file to index: %s", filename) 249 | self.index['files'][filename] = last_modified 250 | with open(filename) as handle: 251 | for kw in self.tokenize(handle.read()): 252 | if kw not in self.index['keywords']: 253 | self.index['keywords'][kw] = [filename] 254 | else: 255 | self.index['keywords'][kw].append(filename) 256 | self.dirty = True 257 | 258 | def delete_note(self, filename): 259 | """Remove a note from the index.""" 260 | self.logger.info("Removing file from index: %s", filename) 261 | del self.index['files'][filename] 262 | for kw in self.index['keywords']: 263 | self.index['keywords'][kw] = [x for x in self.index['keywords'][kw] if x != filename] 264 | self.dirty = True 265 | 266 | def search_index(self, keywords): 267 | """Return names of files containing all of the given keywords.""" 268 | matches = None 269 | normalized_db_keywords = [(k, self.normalize(k)) for k in self.index['keywords']] 270 | for usr_kw in keywords: 271 | submatches = set() 272 | for original_db_kw, normalized_db_kw in normalized_db_keywords: 273 | # Yes I'm using a nested for loop over all keywords in the index. If 274 | # I really have to I'll probably come up with something more 275 | # efficient, but really it doesn't seem to be needed -- I have over 276 | # 850 notes (about 8 MB) and 25000 keywords and it's plenty fast. 277 | if usr_kw in normalized_db_kw: 278 | submatches.update(self.index['keywords'][original_db_kw]) 279 | if matches is None: 280 | matches = submatches 281 | else: 282 | matches &= submatches 283 | return list(matches) if matches else [] 284 | 285 | def list_keywords(self, substring, limit=25): 286 | """Print all (matching) keywords to standard output.""" 287 | decorated = [] 288 | substring = self.normalize(substring) 289 | for kw, filenames in self.index['keywords'].items(): 290 | normalized_kw = self.normalize(kw) 291 | if substring in normalized_kw: 292 | if LEVENSHTEIN_SUPPORTED: 293 | decorated.append((Levenshtein.distance(normalized_kw, substring), -len(filenames), kw)) 294 | else: 295 | decorated.append((-len(filenames), kw)) 296 | decorated.sort() 297 | selection = [d[-1] for d in decorated[:limit]] 298 | print(self.encode(u'\n'.join(selection))) 299 | 300 | def tokenize(self, text): 301 | """Tokenize a string into a list of normalized, unique keywords.""" 302 | words = set() 303 | text = self.decode(text) 304 | for word in re.findall(r'\w+', text, re.UNICODE): 305 | word = word.strip() 306 | if word != '' and not word.isspace() and len(word) >= 2: 307 | words.add(word) 308 | return words 309 | 310 | def normalize(self, keyword): 311 | """Normalize the case of a keyword if configured to do so.""" 312 | return keyword if self.case_sensitive else keyword.lower() 313 | 314 | def encode(self, text): 315 | """Encode a string in the user's preferred character encoding.""" 316 | if isinstance(text, unicode_string): 317 | text = codecs.encode(text, self.character_encoding, 'ignore') 318 | return text 319 | 320 | def decode(self, text): 321 | """Decode a string in the user's preferred character encoding.""" 322 | if isinstance(text, byte_string): 323 | text = codecs.decode(text, self.character_encoding, 'ignore') 324 | return text 325 | 326 | def munge_path(self, path): 327 | """Canonicalize user-defined path, making it absolute.""" 328 | return os.path.abspath(os.path.expanduser(path)) 329 | 330 | def usage(self): 331 | print(__doc__.strip()) 332 | 333 | 334 | class Timer(object): 335 | 336 | """Easy to use timer to keep track of long during operations.""" 337 | 338 | def __init__(self): 339 | self.start_time = time.time() 340 | 341 | def __str__(self): 342 | return "%.2f seconds" % self.elapsed_time 343 | 344 | @property 345 | def elapsed_time(self): 346 | return time.time() - self.start_time 347 | 348 | 349 | if __name__ == '__main__': 350 | NotesIndex() 351 | -------------------------------------------------------------------------------- /misc/notes/shadow/New note: -------------------------------------------------------------------------------- 1 | New note 2 | 3 | To get started enter a title for your note above. When you’re ready to save 4 | your note just use Vim’s :write or :update commands, a filename will be picked 5 | automatically based on the title. 6 | 7 | * * * 8 | 9 | The notes plug-in comes with self hosting documentation. To jump to these notes 10 | position your cursor on the highlighted name and press ‘gf’ in normal mode: 11 | 12 | • Note taking syntax 13 | • Note taking commands 14 | -------------------------------------------------------------------------------- /misc/notes/shadow/Note taking commands: -------------------------------------------------------------------------------- 1 | Note taking commands 2 | 3 | To edit existing notes you can use Vim commands such as :edit, :split and 4 | :tabedit with a filename that starts with ‘note:’ followed by (part of) the 5 | title of one of your notes, e.g.: 6 | {{{vim 7 | :edit note:todo 8 | }}} 9 | When you don’t follow ‘note:’ with anything a new note is created. 10 | The following commands can be used to manage your notes: 11 | 12 | # :Note starts new notes and edits existing ones 13 | 14 | If you don’t pass any arguments to the :Note command it will start editing a 15 | new note. If you pass (part of) of the title of one of your existing notes that 16 | note will be edited. If no notes match the given argument then a new note is 17 | created with its title set to the text you passed to :Note. This command will 18 | fail when changes have been made to the current buffer, unless you use :Note! 19 | which discards any changes. 20 | 21 | To start a new note and use the currently selected text as the title for the 22 | note you can use the :NoteFromSelectedText command. The name of this command 23 | isn’t very well suited to daily use, however the idea is that users will define 24 | their own mapping to invoke this command. For example: 25 | {{{vim 26 | " Map \ns in visual mode to start new note with selected text as title. 27 | vmap ns :NoteFromSelectedText 28 | }}} 29 | # :DeleteNote deletes the current note 30 | 31 | The :DeleteNote command deletes the current note, destroys the buffer and 32 | removes the note from the internal cache of filenames and note titles. This 33 | fails when changes have been made to the current buffer, unless you use 34 | :DeleteNote! which discards any changes. 35 | 36 | # :SearchNotes searches your notes 37 | 38 | This command wraps :vimgrep and enables you to search through your notes using 39 | a regular expression pattern or keywords. To search for a pattern you pass a 40 | single argument that starts & ends with a slash: 41 | 42 | :SearchNotes /TODO\|FIXME\|XXX/ 43 | 44 | To search for one or more keywords you can just omit the slashes, this matches 45 | notes containing all of the given keywords: 46 | 47 | :SearchNotes syntax highlighting 48 | 49 | ## :SearchNotes understands @tags 50 | 51 | If you don’t pass any arguments to the :SearchNotes command it will search for 52 | the word under the cursor. If the word under the cursor starts with ‘@’ this 53 | character will be included in the search, which makes it possible to easily 54 | add @tags to your @notes and then search for those tags. To make searching for 55 | tags even easier you can create key mappings for the :SearchNotes command: 56 | {{{vim 57 | " Make the C-] combination search for @tags: 58 | imap :SearchNotes 59 | nmap :SearchNotes 60 | 61 | " Make double mouse click search for @tags. This is actually quite a lot of 62 | " fun if you don’t use the mouse for text selections anyway; you can click 63 | " between notes as if you’re in a web browser: 64 | imap <2-LeftMouse> :SearchNotes 65 | nmap <2-LeftMouse> :SearchNotes 66 | }}} 67 | These mappings are currently not enabled by default because they conflict with 68 | already useful key mappings, but if you have any suggestions for alternatives 69 | feel free to contact me through GitHub or at peter@peterodding.com. 70 | 71 | ## Accelerated searching with Python 72 | 73 | After collecting a fair amount of notes (say >= 5 MB) you will probably start 74 | to get annoyed at how long it takes Vim to search through all of your notes. To 75 | make searching more scalable the notes plug-in includes a Python script which 76 | uses a persistent keyword index of your notes stored in a file. 77 | 78 | The first time the Python script is run it will need to build the complete 79 | index which can take a moment, but after the index has been initialized 80 | updates and searches should be more or less instantaneous. 81 | 82 | # :RelatedNotes finds related notes 83 | 84 | This command makes it easy to find all notes related to the current file: If 85 | you are currently editing a note then a search for the note’s title is done, 86 | otherwise this searches for the absolute path of the current file. 87 | 88 | # :RecentNotes lists notes by modification date 89 | 90 | If you execute the :RecentNotes command it will open a Vim buffer that lists 91 | all your notes grouped by the day they were edited, starting with your most 92 | recently edited note. If you pass an argument to :RecentNotes it will filter 93 | the list of notes by matching the title of each note against the argument which 94 | is interpreted as a Vim pattern. 95 | -------------------------------------------------------------------------------- /misc/notes/shadow/Note taking syntax: -------------------------------------------------------------------------------- 1 | Note taking syntax 2 | 3 | This note contains examples of the syntax highlighting styles supported by the 4 | notes plug-in. When your Vim configuration supports concealing of text, the 5 | markers which enable the syntax highlighting won’t be visible. In this case you 6 | can make the markers visible by selecting the text. 7 | 8 | # Headings 9 | 10 | Lines prefixed with one or more ‘#’ symbols are headings which can be used for 11 | automatic text folding. There’s also an alternative heading format which isn’t 12 | folded, it consists of a line shorter than 60 letters that starts with an 13 | uppercase letter and ends in a colon (the hard wrapping in this paragraph 14 | illustrates why the “starts with uppercase” rule is needed): 15 | 16 | # Inline formatting 17 | 18 | Text styles: 19 | • _italic text_ 20 | • *bold text* 21 | 22 | Hyper links and such: 23 | • Hyper links: http://www.vim.org/, sftp://server/file 24 | • Domain names: www.python.org 25 | • E-mail addresses: user@host.ext 26 | • UNIX filenames: ~/relative/to/home, /absolute/path 27 | • Windows filenames: ~\relative\to\home, c:\absolute\path, \\server\share 28 | 29 | # Lists 30 | 31 | Bulleted lists can be used for to-do lists: 32 | • DONE Publish my notes.vim plug-in 33 | • TODO Write an indent script for atx headings 34 | • XXX This one is really important 35 | 36 | Numbered lists are also supported: 37 | 1. And You can 38 | 2) use any type 39 | 3/ of marker 40 | 41 | # Block quotes 42 | 43 | > Quotes are written using 44 | > the convention from e-mail 45 | 46 | # Embedded syntax highlighting 47 | 48 | If you type three ‘{’ characters followed by the name of a Vim file type, all 49 | text until the three closing ‘}’ characters will be highlighted using the 50 | indicated file type. Here are some examples of the Fibonacci sequence: 51 | 52 | Lua: {{{lua function fib(n) return n < 2 and n or fib(n - 1) + fib(n - 2) end }}} 53 | Vim script: {{{vim function fib(n) | return n < 2 ? n : fib(n - 1) + fib(n - 2) | endfunction }}} 54 | Python: {{{python def fib(n): return n < 2 and n or fib(n - 1) + fib(n - 2) }}} 55 | -------------------------------------------------------------------------------- /misc/notes/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | 8 | 9 | 10 | 11 | 30 | 31 | 32 |
33 | {{ content }} 34 |
35 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /plugin/notes.vim: -------------------------------------------------------------------------------- 1 | " Vim plug-in 2 | " Author: Peter Odding 3 | " Last Change: August 19, 2013 4 | " URL: http://peterodding.com/code/vim/notes/ 5 | 6 | " Support for automatic update using the GLVS plug-in. 7 | " GetLatestVimScripts: 3375 1 :AutoInstall: notes.zip 8 | 9 | " Don't source the plug-in when it's already been loaded or &compatible is set. 10 | if &cp || exists('g:loaded_notes') 11 | finish 12 | endif 13 | 14 | " Make sure vim-misc is installed. 15 | try 16 | " The point of this code is to do something completely innocent while making 17 | " sure the vim-misc plug-in is installed. We specifically don't use Vim's 18 | " exists() function because it doesn't load auto-load scripts that haven't 19 | " already been loaded yet (last tested on Vim 7.3). 20 | call type(g:xolox#misc#version) 21 | catch 22 | echomsg "Warning: The vim-notes plug-in requires the vim-misc plug-in which seems not to be installed! For more information please review the installation instructions in the readme (also available on the homepage and on GitHub). The vim-notes plug-in will now be disabled." 23 | let g:loaded_notes = 1 24 | finish 25 | endtry 26 | 27 | " Initialize the configuration defaults. 28 | call xolox#notes#init() 29 | 30 | " User commands to create, delete and search notes. 31 | command! -bar -bang -nargs=? -complete=customlist,xolox#notes#cmd_complete Note call xolox#notes#edit(, ) 32 | command! -bar -bang -nargs=? -complete=customlist,xolox#notes#cmd_complete DeleteNote call xolox#notes#delete(, ) 33 | command! -bang -nargs=? -complete=customlist,xolox#notes#keyword_complete SearchNotes call xolox#notes#search(, ) 34 | command! -bar -bang RelatedNotes call xolox#notes#related() 35 | command! -bar -bang -nargs=? RecentNotes call xolox#notes#recent#show(, ) 36 | command! -bar -bang MostRecentNote call xolox#notes#recent#edit() 37 | command! -bar -count=1 ShowTaggedNotes call xolox#notes#tags#show_tags() 38 | command! -bar IndexTaggedNotes call xolox#notes#tags#create_index() 39 | command! -bar NoteToMarkdown call xolox#notes#markdown#view() 40 | command! -bar NoteToMediawiki call xolox#notes#mediawiki#view() 41 | command! -bar -nargs=? NoteToHtml call xolox#notes#html#view() 42 | 43 | " TODO Generalize this so we have one command + modifiers (like :tab)? 44 | command! -bar -bang -range NoteFromSelectedText call xolox#notes#from_selection(, 'edit') 45 | command! -bar -bang -range SplitNoteFromSelectedText call xolox#notes#from_selection(, 'vsplit') 46 | command! -bar -bang -range TabNoteFromSelectedText call xolox#notes#from_selection(, 'tabnew') 47 | 48 | " Automatic commands to enable the :edit note:… shortcut and load the notes file type. 49 | 50 | augroup PluginNotes 51 | autocmd! 52 | au SwapExists * call xolox#notes#swaphack() 53 | au BufUnload * call xolox#notes#unload_from_cache() 54 | au BufReadPost,BufWritePost * call xolox#notes#refresh_syntax() 55 | au InsertEnter,InsertLeave * call xolox#notes#refresh_syntax() 56 | au CursorHold,CursorHoldI * call xolox#notes#refresh_syntax() 57 | " NB: "nested" is used here so that SwapExists automatic commands apply 58 | " to notes (which is IMHO better than always showing the E325 prompt). 59 | au BufReadCmd note:* nested call xolox#notes#shortcut() 60 | " Automatic commands to read/write notes (used for automatic renaming). 61 | exe 'au BufReadCmd' xolox#notes#autocmd_pattern(g:notes_shadowdir, 0) 'call xolox#notes#edit_shadow()' 62 | for s:directory in xolox#notes#find_directories(0) 63 | exe 'au BufWriteCmd' xolox#notes#autocmd_pattern(s:directory, 1) 'call xolox#notes#save()' 64 | endfor 65 | unlet s:directory 66 | augroup END 67 | 68 | augroup filetypedetect 69 | let s:template = 'au BufNewFile,BufRead %s if &bt == "" | setl ft=notes | end' 70 | for s:directory in xolox#notes#find_directories(0) 71 | execute printf(s:template, xolox#notes#autocmd_pattern(s:directory, 1)) 72 | endfor 73 | unlet s:directory 74 | execute printf(s:template, xolox#notes#autocmd_pattern(g:notes_shadowdir, 0)) 75 | augroup END 76 | 77 | " Make sure the plug-in is only loaded once. 78 | let g:loaded_notes = 1 79 | 80 | " vim: ts=2 sw=2 et 81 | -------------------------------------------------------------------------------- /screenshots/folding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xolox/vim-notes/e465a0a987dbacdf7291688215b8545f8584d409/screenshots/folding.png -------------------------------------------------------------------------------- /screenshots/syntax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xolox/vim-notes/e465a0a987dbacdf7291688215b8545f8584d409/screenshots/syntax.png -------------------------------------------------------------------------------- /syntax/notes.vim: -------------------------------------------------------------------------------- 1 | " Vim syntax script 2 | " Author: Peter Odding 3 | " Last Change: March 15, 2015 4 | " URL: http://peterodding.com/code/vim/notes/ 5 | 6 | " Note: This file is encoded in UTF-8 including a byte order mark so 7 | " that Vim loads the script using the right encoding transparently. 8 | 9 | " Quit when a syntax file was already loaded. 10 | if exists('b:current_syntax') 11 | finish 12 | endif 13 | 14 | " Tell Vim to start redrawing by rescanning all previous text. This isn't 15 | " exactly optimal for performance but it enables accurate syntax highlighting. 16 | " Ideally we'd find a way to get accurate syntax highlighting without the 17 | " nasty performance implications, but for now I'll accept the performance 18 | " impact in order to have accurate highlighting. For more discussion please 19 | " refer to https://github.com/xolox/vim-notes/issues/2. 20 | syntax sync fromstart 21 | 22 | " Check for spelling errors in all text. 23 | syntax spell toplevel 24 | 25 | " Inline elements. {{{1 26 | 27 | " Cluster of elements which never contain a newline character. 28 | syntax cluster notesInline contains=notesName 29 | 30 | " Default highlighting style for notes syntax markers. 31 | highlight def link notesHiddenMarker Ignore 32 | 33 | " Highlight note names as hyperlinks. {{{2 34 | call xolox#notes#highlight_names(1) 35 | syntax cluster notesInline add=notesName 36 | highlight def link notesName Underlined 37 | 38 | " Highlight @tags as hyperlinks. {{{2 39 | syntax match notesTagName /\(^\|\s\)\@<=@\k\+/ 40 | highlight def link notesTagName Underlined 41 | 42 | " Highlight list bullets and numbers. {{{2 43 | execute 'syntax match notesListBullet /' . escape(xolox#notes#leading_bullet_pattern(), '/') . '/' 44 | highlight def link notesListBullet Comment 45 | syntax match notesListNumber /^\s*\zs\d\+[[:punct:]]\?\ze\s/ 46 | highlight def link notesListNumber Comment 47 | 48 | " Highlight quoted fragments. {{{2 49 | if xolox#notes#unicode_enabled() 50 | syntax match notesDoubleQuoted /\w\@\|\n/ contains=@Spell concealends 72 | highlight link notesItalicMarker notesHiddenMarker 73 | else 74 | syntax match notesItalic /\<_\k[^_]*\k_\>/ 75 | endif 76 | syntax cluster notesInline add=notesItalic 77 | highlight notesItalic gui=italic cterm=italic 78 | 79 | " Highlight text emphasized in bold font. {{{2 80 | if has('conceal') && xolox#misc#option#get('notes_conceal_bold', 1) 81 | syntax region notesBold matchgroup=notesBoldMarker start=/\*\k\@=/ end=/\S\@<=\*/ contains=@Spell concealends 82 | highlight link notesBoldMarker notesHiddenMarker 83 | else 84 | syntax match notesBold /\*\k[^*]*\k\*/ 85 | endif 86 | syntax cluster notesInline add=notesBold 87 | highlight notesBold gui=bold cterm=bold 88 | 89 | " Highlight domain names, URLs, e-mail addresses and filenames. {{{2 90 | 91 | " FIXME This setting is lost once the user switches color scheme! 92 | highlight notesSubtleURL gui=underline guifg=fg 93 | 94 | syntax match notesTextURL @\/ 105 | syntax cluster notesInline add=notesEmailAddr 106 | highlight def link notesEmailAddr notesSubtleURL 107 | syntax match notesUnixPath /\k\@/ 118 | syntax match notesXXX /\/ 119 | syntax match notesFixMe /\/ 120 | syntax match notesInProgress /\<\(CURRENT\|INPROGRESS\|STARTED\|WIP\)\>/ 121 | syntax match notesDoneItem /^\(\s\+\).*\.*\(\n\1\s.*\)*/ contains=@notesInline 122 | syntax match notesDoneMarker /\/ containedin=notesDoneItem 123 | highlight def link notesTodo WarningMsg 124 | highlight def link notesXXX WarningMsg 125 | highlight def link notesFixMe WarningMsg 126 | highlight def link notesDoneItem Comment 127 | highlight def link notesDoneMarker Question 128 | highlight def link notesInProgress Directory 129 | 130 | " Highlight Vim command names in :this notation. {{{2 131 | syntax match notesVimCmd /\w\@\)/ contains=ALLBUT,@Spell 132 | syntax cluster notesInline add=notesVimCmd 133 | highlight def link notesVimCmd Special 134 | 135 | " Block level elements. {{{1 136 | 137 | " The first line of each note contains the title. {{{2 138 | syntax match notesTitle /^.*\%1l.*$/ contains=@notesInline 139 | highlight def link notesTitle ModeMsg 140 | 141 | " Short sentences ending in a colon are considered headings. {{{2 142 | syntax match notesShortHeading /^\s*\zs\u.\{1,50}\k:\ze\(\s\|$\)/ contains=@notesInline 143 | highlight def link notesShortHeading Title 144 | 145 | " Atx style headings are also supported. {{{2 146 | syntax match notesAtxHeading /^#\+.*/ contains=notesAtxMarker,@notesInline 147 | highlight def link notesAtxHeading Title 148 | syntax match notesAtxMarker /^#\+/ contained 149 | highlight def link notesAtxMarker Comment 150 | 151 | " E-mail style block quotes are highlighted as comments. {{{2 152 | syntax match notesBlockQuote /\(^\s*>.*\n\)\+/ contains=@notesInline 153 | highlight def link notesBlockQuote Comment 154 | 155 | " Horizontal rulers. {{{2 156 | syntax match notesRule /\(^\s\+\)\zs\*\s\*\s\*$/ 157 | highlight def link notesRule Comment 158 | 159 | " Highlight embedded blocks of source code, log file messages, basically anything Vim can highlight. {{{2 160 | " NB: I've escaped these markers so that Vim doesn't interpret them when editing this file… 161 | syntax match notesCodeStart /```\w*/ 162 | syntax match notesCodeEnd /```\W/ 163 | syntax match notesCodeStart /{{[{]\w*/ 164 | syntax match notesCodeEnd /}}[}]/ 165 | highlight def link notesCodeStart Ignore 166 | highlight def link notesCodeEnd Ignore 167 | call xolox#notes#highlight_sources(1) 168 | 169 | " Hide mode line at end of file. {{{2 170 | syntax match notesModeLine /\_^vim:.*\_s*\%$/ 171 | highlight def link notesModeLine LineNr 172 | 173 | " Last edited dates in :ShowTaggedNotes buffers. 174 | syntax match notesLastEdited /(last edited \(today\|yesterday\|\w\+, \w\+ \d\+, \d\+\))/ 175 | highlight def link notesLastEdited LineNr 176 | 177 | " }}}1 178 | 179 | " Set the currently loaded syntax mode. 180 | let b:current_syntax = 'notes' 181 | 182 | " vim: ts=2 sw=2 et bomb fdl=1 183 | --------------------------------------------------------------------------------