├── screenshots
├── folding.png
└── syntax.png
├── .gitignore
├── addon-info.json
├── misc
└── notes
│ ├── shadow
│ ├── New note
│ ├── Note taking syntax
│ └── Note taking commands
│ ├── template.html
│ └── search-notes.py
├── TODO.md
├── INSTALL.md
├── autoload
└── xolox
│ ├── notes
│ ├── markdown.vim
│ ├── html.vim
│ ├── recent.vim
│ ├── mediawiki.vim
│ ├── tags.vim
│ └── parser.vim
│ └── notes.vim
├── plugin
└── notes.vim
├── ftplugin
└── notes.vim
├── syntax
└── notes.vim
├── README.md
└── doc
└── notes.txt
/screenshots/folding.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xolox/vim-notes/HEAD/screenshots/folding.png
--------------------------------------------------------------------------------
/screenshots/syntax.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xolox/vim-notes/HEAD/screenshots/syntax.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | doc/tags
2 | misc/notes/index.pickle
3 | misc/notes/recent.txt
4 | misc/notes/tags.txt
5 | misc/notes/user/
6 |
--------------------------------------------------------------------------------
/addon-info.json:
--------------------------------------------------------------------------------
1 | {"vim_script_nr": 3375, "dependencies": {"vim-misc": {}}, "homepage": "http://peterodding.com/code/vim/notes", "name": "vim-notes"}
--------------------------------------------------------------------------------
/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/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ title }}
6 |
7 |
8 |
9 |
10 |
11 |
30 |
31 |
32 |
33 | {{ content }}
34 |
35 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------