" Vim auto-load script " Author: Peter Odding " Last Change: September 2, 2013 " URL: http://peterodding.com/code/vim/notes/ " Note: This file is encoded in UTF-8 including a byte order mark so " that Vim loads the script using the right encoding transparently. let g:xolox#notes#version = '0.23.4' let g:xolox#notes#url_pattern = '\<\(mailto:\|javascript:\|\w\{3,}://\)\(\S*\w\)\+/\?' let s:scriptdir = expand(':p:h') function! xolox#notes#init() " {{{1 " Initialize the configuration of the notes plug-in. This is a bit tricky: " We want to be compatible with Pathogen which installs plug-ins as " "bundles" under ~/.vim/bundle/*/ so we use a relative path to make sure we " 'stay inside the bundle'. However if the notes.vim plug-in is installed " system wide the user probably won't have permission to write inside the " installation directory, so we have to switch to $HOME then. let systemdir = xolox#misc#path#absolute(s:scriptdir . '/../../misc/notes') if filewritable(systemdir) == 2 let localdir = systemdir elseif xolox#misc#os#is_win() let localdir = xolox#misc#path#absolute('~/vimfiles/misc/notes') else let localdir = xolox#misc#path#absolute('~/.vim/misc/notes') endif " Backwards compatibility with old configurations. if exists('g:notes_directory') call xolox#misc#msg#warn("notes.vim %s: Please upgrade your configuration, see :help notes-backwards-compatibility", g:xolox#notes#version) let g:notes_directories = [g:notes_directory] unlet g:notes_directory endif " Define the default location where the user's notes are saved? if !exists('g:notes_directories') let g:notes_directories = [xolox#misc#path#merge(localdir, 'user')] endif call s:create_notes_directories() " Define the default location of the shadow directory with predefined notes? if !exists('g:notes_shadowdir') let g:notes_shadowdir = xolox#misc#path#merge(systemdir, 'shadow') endif " Define the default location for the full text index. if !exists('g:notes_indexfile') let g:notes_indexfile = xolox#misc#path#merge(localdir, 'index.pickle') endif " Define the default location for the keyword scanner script. if !exists('g:notes_indexscript') let g:notes_indexscript = xolox#misc#path#merge(systemdir, 'search-notes.py') endif " Define the default suffix for note filenames. if !exists('g:notes_suffix') let g:notes_suffix = '' endif " Define the default location for the tag name index (used for completion). if !exists('g:notes_tagsindex') let g:notes_tagsindex = xolox#misc#path#merge(localdir, 'tags.txt') endif " Define the default location for the file containing the most recent note's " filename. if !exists('g:notes_recentindex') let g:notes_recentindex = xolox#misc#path#merge(localdir, 'recent.txt') endif " Define the default location of the template for HTML conversion. if !exists('g:notes_html_template') let g:notes_html_template = xolox#misc#path#merge(localdir, 'template.html') endif " Define the default action when a note's filename and title are out of sync. if !exists('g:notes_title_sync') " Valid values are "no", "change_title", "rename_file" and "prompt". let g:notes_title_sync = 'prompt' endif " Smart quotes and such are enabled by default. if !exists('g:notes_smart_quotes') let g:notes_smart_quotes = 1 endif " Tab/Shift-Tab is used to indent/dedent list items by default. if !exists('g:notes_tab_indents') let g:notes_tab_indents = 1 endif " Alt-Left/Alt-Right is used to indent/dedent list items by default. if !exists('g:notes_alt_indents') let g:notes_alt_indents = 1 endif " Text used for horizontal rulers. if !exists('g:notes_ruler_text') let g:notes_ruler_text = repeat(' ', ((&tw > 0 ? &tw : 79) - 5) / 2) . '* * *' endif " Symbols used to denote list items with increasing nesting levels. let g:notes_unicode_bullets = ['•', '◦', '▸', '▹', '▪', '▫'] let g:notes_ascii_bullets = ['*', '-', '+'] if !exists('g:notes_list_bullets') if xolox#notes#unicode_enabled() let g:notes_list_bullets = g:notes_unicode_bullets else let g:notes_list_bullets = g:notes_ascii_bullets endif endif endfunction function! s:create_notes_directories() for directory in xolox#notes#find_directories(0) if !isdirectory(directory) call xolox#misc#msg#info("notes.vim %s: Creating notes directory %s (first run?) ..", g:xolox#notes#version, directory) call mkdir(directory, 'p') endif if filewritable(directory) != 2 call xolox#misc#msg#warn("notes.vim %s: The notes directory %s is not writable!", g:xolox#notes#version, directory) endif endfor endfunction function! xolox#notes#shortcut() " {{{1 " The "note:" pseudo protocol is just a shortcut for the :Note command. let expression = expand('') let bufnr_save = bufnr('%') call xolox#misc#msg#debug("notes.vim %s: Expanding shortcut %s ..", g:xolox#notes#version, string(expression)) let substring = matchstr(expression, 'note:\zs.*') call xolox#misc#msg#debug("notes.vim %s: Editing note based on title substring %s ..", g:xolox#notes#version, string(substring)) call xolox#notes#edit(v:cmdbang ? '!' : '', substring) " Clean up the buffer with the name "note:..."? let pathname = fnamemodify(bufname(bufnr_save), ':p') let basename = fnamemodify(pathname, ':t') if basename =~ '^note:' call xolox#misc#msg#debug("notes.vim %s: Cleaning up buffer #%i - %s", g:xolox#notes#version, bufnr_save, pathname) execute 'bwipeout' bufnr_save endif endfunction function! xolox#notes#edit(bang, title) abort " {{{1 " Edit an existing note or create a new one with the :Note command. let starttime = xolox#misc#timer#start() let title = xolox#misc#str#trim(a:title) if title != '' let fname = xolox#notes#select(title) if fname != '' call xolox#misc#msg#debug("notes.vim %s: Editing existing note: %s", g:xolox#notes#version, fname) execute 'edit' . a:bang fnameescape(fname) if !xolox#notes#unicode_enabled() && xolox#misc#path#equals(fnamemodify(fname, ':h'), g:notes_shadowdir) call s:transcode_utf8_latin1() endif call xolox#notes#set_filetype() call xolox#misc#timer#stop('notes.vim %s: Opened note in %s.', g:xolox#notes#version, starttime) return endif else let title = 'New note' endif " At this point we're dealing with a new note. let fname = xolox#notes#title_to_fname(title) noautocmd execute 'edit' . a:bang fnameescape(fname) if line('$') == 1 && getline(1) == '' let fname = xolox#misc#path#merge(g:notes_shadowdir, 'New note') execute 'silent read' fnameescape(fname) 1delete if !xolox#notes#unicode_enabled() call s:transcode_utf8_latin1() endif setlocal nomodified endif if title != 'New note' call setline(1, title) endif call xolox#notes#set_filetype() doautocmd BufReadPost call xolox#misc#timer#stop('notes.vim %s: Started new note in %s.', g:xolox#notes#version, starttime) endfunction function! xolox#notes#check_sync_title() " {{{1 " Check if the note's title and filename are out of sync. if g:notes_title_sync != 'no' && xolox#notes#buffer_is_note() && &buftype == '' let title = xolox#notes#current_title() let name_on_disk = xolox#misc#path#absolute(expand('%:p')) let name_from_title = xolox#notes#title_to_fname(title) if !xolox#misc#path#equals(name_on_disk, name_from_title) 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) let action = g:notes_title_sync if action == 'prompt' && empty(name_from_title) " There's no point in prompting the user when there's only one choice. let action = 'change_title' elseif action == 'prompt' " Prompt the user what to do (if anything). First we perform a redraw " to make sure the note's content is visible (without this the Vim " window would be blank in my tests). redraw let message = "The note's title and filename do not correspond. What do you want to do?\n\n" let message .= "Current filename: " . s:sync_value(name_on_disk) . "\n" let message .= "Corresponding title: " . s:sync_value(xolox#notes#fname_to_title(name_on_disk)) . "\n\n" let message .= "Current title: " . s:sync_value(title) . "\n" let message .= "Corresponding filename: " . s:sync_value(xolox#notes#title_to_fname(title)) let choice = confirm(message, "Change &title\nRename &file\nDo ¬hing", 3, 'Question') if choice == 1 let action = 'change_title' elseif choice == 2 let action = 'rename_file' else " User chose to do nothing or 'd the prompt. return endif " Intentional fall through here :-) endif if action == 'change_title' let new_title = xolox#notes#fname_to_title(name_on_disk) call setline(1, new_title) setlocal modified call xolox#misc#msg#info("notes.vim %s: Changed note title to match filename.", g:xolox#notes#version) elseif action == 'rename_file' let new_fname = xolox#notes#title_to_fname(xolox#notes#current_title()) if rename(name_on_disk, new_fname) == 0 execute 'edit' fnameescape(new_fname) call xolox#notes#set_filetype() call xolox#misc#msg#info("notes.vim %s: Renamed file to match note title.", g:xolox#notes#version) else call xolox#misc#msg#warn("notes.vim %s: Failed to rename file to match note title?!", g:xolox#notes#version) endif endif endif endif endfunction function! s:sync_value(s) let s = xolox#misc#str#trim(a:s) return empty(s) ? '(none)' : s endfunction function! xolox#notes#from_selection(bang, cmd) " {{{1 " Edit a note with the visually selected text as title. let selection = s:get_visual_selection() if a:cmd != 'edit' | execute a:cmd | endif call xolox#notes#edit(a:bang, selection) endfunction function! s:get_visual_selection() " Why is this not a built-in Vim script function?! See also the question at " http://stackoverflow.com/questions/1533565 but note that none of the code " posted there worked for me so I wrote this function. let [lnum1, col1] = getpos("'<")[1:2] let [lnum2, col2] = getpos("'>")[1:2] let lines = getline(lnum1, lnum2) let lines[-1] = lines[-1][: col2 - (&selection == 'inclusive' ? 1 : 2)] let lines[0] = lines[0][col1 - 1:] return join(lines, ' ') endfunction function! xolox#notes#edit_shadow() " {{{1 " People using latin1 don't like the UTF-8 curly quotes and bullets used in " the predefined notes because there are no equivalent characters in latin1, " resulting in the characters being shown as garbage or a question mark. execute 'edit' fnameescape(expand('')) if !xolox#notes#unicode_enabled() call s:transcode_utf8_latin1() endif call xolox#notes#set_filetype() endfunction function! xolox#notes#unicode_enabled() return &encoding == 'utf-8' endfunction function! s:transcode_utf8_latin1() let view = winsaveview() silent %s/\%xe2\%x80\%x98/`/eg silent %s/\%xe2\%x80\%x99/'/eg silent %s/\%xe2\%x80[\x9c\x9d]/"/eg silent %s/\%xe2\%x80\%xa2/\*/eg setlocal nomodified call winrestview(view) endfunction function! xolox#notes#select(filter) " {{{1 " Interactively select an existing note whose title contains {filter}. let notes = {} let filter = xolox#misc#str#trim(a:filter) for [fname, title] in items(xolox#notes#get_fnames_and_titles(1)) if title ==? filter call xolox#misc#msg#debug("notes.vim %s: Filter %s exactly matches note: %s", g:xolox#notes#version, string(filter), title) return fname elseif title =~? filter let notes[fname] = title endif endfor if len(notes) == 1 let fname = keys(notes)[0] call xolox#misc#msg#debug("notes.vim %s: Filter %s matched one note: %s", g:xolox#notes#version, string(filter), fname) return fname elseif !empty(notes) call xolox#misc#msg#debug("notes.vim %s: Filter %s matched %i notes.", g:xolox#notes#version, string(filter), len(notes)) let choices = ['Please select a note:'] let values = [''] for fname in sort(keys(notes), 1) call add(choices, ' ' . len(choices) . ') ' . notes[fname]) call add(values, fname) endfor let choice = inputlist(choices) if choice > 0 && choice < len(choices) let fname = values[choice] call xolox#misc#msg#debug("notes.vim %s: User selected note: %s", g:xolox#notes#version, fname) return fname endif endif return '' endfunction function! xolox#notes#cmd_complete(arglead, cmdline, cursorpos) " {{{1 " Vim's support for custom command completion is a real mess, specifically " the completion of multi word command arguments. With or without escaping " of spaces, arglead will only contain the last word in the arguments passed " to :Note, and worse, the completion candidates we return only replace the " last word on the command line. " XXX This isn't a real command line parser; it will break on quoted pipes. let cmdline = split(a:cmdline, '\\\@ after the argument) we can select the " completion candidates using a substring match on the first argument " instead of a prefix match (I consider this to be more user friendly). let pattern = xolox#misc#escape#pattern(cmdargs) call filter(titles, "v:val =~ pattern") else " If we are completing more than one argument or the user has typed " after the first argument, we must select completion " candidates using a prefix match on all arguments because Vim doesn't " support replacing previous arguments (selecting completion candidates " using a substring match would result in invalid note titles). let pattern = '^' . xolox#misc#escape#pattern(cmdargs) call filter(titles, "v:val =~ pattern") " Remove the given arguments as the prefix of every completion candidate " because Vim refuses to replace previous arguments. let prevargs = '^' . xolox#misc#escape#pattern(cmdargs[0 : len(cmdargs) - len(a:arglead) - 1]) call map(titles, 'substitute(v:val, prevargs, "", "")') endif " Sort from shortest to longest as a rough approximation of " sorting by similarity to the word that's being completed. return reverse(sort(titles, 's:sort_longest_to_shortest')) endfunction function! xolox#notes#user_complete(findstart, base) " {{{1 " Completion of note titles with Control-X Control-U. if a:findstart let line = getline('.')[0 : col('.') - 2] let words = split(line) if !empty(words) return col('.') - len(words[-1]) - 1 else return -1 endif else let titles = xolox#notes#get_titles(1) if !empty(a:base) let pattern = xolox#misc#escape#pattern(a:base) call filter(titles, 'v:val =~ pattern') endif return titles endif endfunction function! xolox#notes#omni_complete(findstart, base) " {{{1 " Completion of tag names with Control-X Control-O. if a:findstart " For now we assume omni completion was triggered by the mapping for " automatic tag completion. Eventually it might be nice to check for a " leading "@" here and otherwise make it complete e.g. note names, so that " there's only one way to complete inside notes and the plug-in is smart " enough to know what the user wants to complete :-) return col('.') else return sort(keys(xolox#notes#tags#load_index()), 1) endif endfunction function! xolox#notes#save() abort " {{{1 " When the current note's title is changed, automatically rename the file. if xolox#notes#filetype_is_note(&ft) let title = xolox#notes#current_title() let oldpath = expand('%:p') let newpath = xolox#notes#title_to_fname(title) if newpath == '' echoerr "Invalid note title" return endif let bang = v:cmdbang ? '!' : '' execute 'saveas' . bang fnameescape(newpath) " XXX If {oldpath} and {newpath} end up pointing to the same file on disk " yet xolox#misc#path#equals() doesn't catch this, we might end up " deleting the user's one and only note! One way to circumvent this " potential problem is to first delete the old note and then save the new " note. The problem with this approach is that :saveas might fail in which " case we've already deleted the old note... if !xolox#misc#path#equals(oldpath, newpath) if !filereadable(newpath) 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!" call confirm(printf(message, string(newpath), string(oldpath))) return endif call delete(oldpath) endif " Update the tags index on disk and in-memory. call xolox#notes#tags#forget_note(xolox#notes#fname_to_title(oldpath)) call xolox#notes#tags#scan_note(title, join(getline(1, '$'), "\n")) call xolox#notes#tags#save_index() " Update in-memory list of all notes. call xolox#notes#cache_del(oldpath) call xolox#notes#cache_add(newpath, title) endif endfunction function! xolox#notes#delete(bang, title) " {{{1 " Delete the note {title} and close the associated buffer & window. " If no {title} is given the current note is deleted. let title = xolox#misc#str#trim(a:title) if title == '' " Try the current buffer. let title = xolox#notes#fname_to_title(expand('%:p')) endif if !xolox#notes#exists(title) call xolox#misc#msg#warn("notes.vim %s: Failed to delete %s! (not a note)", g:xolox#notes#version, expand('%:p')) else let filename = xolox#notes#title_to_fname(title) if filereadable(filename) && delete(filename) call xolox#misc#msg#warn("notes.vim %s: Failed to delete %s!", g:xolox#notes#version, filename) else call xolox#notes#cache_del(filename) execute 'bdelete' . a:bang . ' ' . bufnr(filename) endif endif endfunction function! xolox#notes#search(bang, input) " {{{1 " Search all notes for the pattern or keywords {input} (current word if none given). try let starttime = xolox#misc#timer#start() let input = a:input if input == '' let input = s:tag_under_cursor() if input == '' call xolox#misc#msg#warn("notes.vim %s: No string under cursor", g:xolox#notes#version) return endif endif if input =~ '^/.\+/$' call s:internal_search(a:bang, input, '', '') call s:set_quickfix_title([], input) else let keywords = split(input) let all_keywords = s:match_all_keywords(keywords) let any_keyword = s:match_any_keyword(keywords) call s:internal_search(a:bang, all_keywords, input, any_keyword) if &buftype == 'quickfix' " Enable line wrapping in the quick-fix window. setlocal wrap " Resize the quick-fix window to 1/3 of the screen height. let max_height = &lines / 3 execute 'resize' max_height " Make it smaller if the content doesn't fill the window. normal G$ let preferred_height = winline() execute 'resize' min([max_height, preferred_height]) normal gg call s:set_quickfix_title(keywords, '') endif endif call xolox#misc#timer#stop("notes.vim %s: Searched notes in %s.", g:xolox#notes#version, starttime) catch /^Vim\%((\a\+)\)\=:E480/ call xolox#misc#msg#warn("notes.vim %s: No matches", g:xolox#notes#version) endtry endfunction function! s:tag_under_cursor() " {{{2 " Get the word or @tag under the text cursor. try let isk_save = &isk set iskeyword+=@-@ return expand('') finally let &isk = isk_save endtry endfunction function! s:match_all_keywords(keywords) " {{{2 " Create a regex that matches when a file contains all {keywords}. let results = copy(a:keywords) call map(results, '''\_^\_.*'' . xolox#misc#escape#pattern(v:val)') return '/' . escape(join(results, '\&'), '/') . '/' endfunction function! s:match_any_keyword(keywords) " {{{2 " Create a regex that matches every occurrence of all {keywords}. let results = copy(a:keywords) call map(results, 'xolox#misc#escape#pattern(v:val)') return '/' . escape(join(results, '\|'), '/') . '/' endfunction function! s:set_quickfix_title(keywords, pattern) " {{{2 " Set the title of the quick-fix window. if &buftype == 'quickfix' let num_notes = len(xolox#misc#list#unique(map(getqflist(), 'v:val["bufnr"]'))) if len(a:keywords) > 0 let keywords = map(copy(a:keywords), '"`" . v:val . "''"') let w:quickfix_title = printf('Found %i note%s containing the word%s %s', \ num_notes, num_notes == 1 ? '' : 's', \ len(keywords) == 1 ? '' : 's', \ len(keywords) > 1 ? (join(keywords[0:-2], ', ') . ' and ' . keywords[-1]) : keywords[0]) else let w:quickfix_title = printf('Found %i note%s containing the pattern %s', \ num_notes, num_notes == 1 ? '' : 's', \ a:pattern) endif endif endfunction function! xolox#notes#related(bang) " {{{1 " Find all notes related to the current note or file. let starttime = xolox#misc#timer#start() let bufname = bufname('%') if bufname == '' call xolox#misc#msg#warn("notes.vim %s: :RelatedNotes only works on named buffers!", g:xolox#notes#version) else let filename = xolox#misc#path#absolute(bufname) if xolox#notes#buffer_is_note() let keywords = xolox#notes#current_title() let pattern = '\<' . s:words_to_pattern(keywords) . '\>' else let pattern = s:words_to_pattern(filename) let keywords = filename if filename[0 : len($HOME)-1] == $HOME let relative = filename[len($HOME) + 1 : -1] let pattern = '\(' . pattern . '\|\~/' . s:words_to_pattern(relative) . '\)' let keywords = relative endif endif let pattern = '/' . escape(pattern, '/') . '/' let friendly_path = fnamemodify(filename, ':~') try call s:internal_search(a:bang, pattern, keywords, '') if &buftype == 'quickfix' let w:quickfix_title = 'Notes related to ' . friendly_path endif catch /^Vim\%((\a\+)\)\=:E480/ call xolox#misc#msg#warn("notes.vim %s: No related notes found for %s", g:xolox#notes#version, friendly_path) endtry endif call xolox#misc#timer#stop("notes.vim %s: Found related notes in %s.", g:xolox#notes#version, starttime) endfunction " Miscellaneous functions. {{{1 function! xolox#notes#find_directories(include_shadow_directory) " {{{2 " Generate a list of absolute pathnames of all notes directories. let directories = copy(g:notes_directories) " Add the shadow directory? if a:include_shadow_directory call add(directories, g:notes_shadowdir) endif " Return the expanded directory pathnames. return map(directories, 'expand(v:val)') endfunction function! xolox#notes#set_filetype() " {{{2 " Load the notes file type if not already loaded. if &filetype != 'notes' " Change the file type. setlocal filetype=notes elseif synID(1, 1, 0) == 0 " Load the syntax. When you execute :RecentNotes, switch to a different " buffer and then return to the buffer created by :RecentNotes, it will " have lost its syntax highlighting. The following line of code solves " this problem. We don't explicitly set the syntax to 'notes' so that we " preserve dot separated composed values. let &syntax = &syntax endif endfunction function! xolox#notes#swaphack() " {{{2 " Selectively ignore the dreaded E325 interactive prompt. if exists('s:swaphack_enabled') let v:swapchoice = 'o' endif endfunction function! xolox#notes#autocmd_pattern(directory, use_extension) " {{{2 " Generate a normalized automatic command pattern. First we resolve the path " to the directory with notes (eliminating any symbolic links) so that the " automatic command also applies to symbolic links pointing to notes (Vim " matches filename patterns in automatic commands after resolving " filenames). let directory = xolox#misc#path#absolute(a:directory) " On Windows we have to replace backslashes with forward slashes, otherwise " the automatic command will never trigger! This has to happen before we " make the fnameescape() call. if xolox#misc#os#is_win() let directory = substitute(directory, '\\', '/', 'g') endif " Escape the directory but not the trailing "*". let pattern = fnameescape(directory) . '/*' if a:use_extension && !empty(g:notes_suffix) let pattern .= g:notes_suffix endif " On Windows the pattern won't match if it contains repeating slashes. return substitute(pattern, '/\+', '/', 'g') endfunction function! xolox#notes#filetype_is_note(ft) " {{{2 " Check whether the given file type value refers to the notes.vim plug-in. return index(split(a:ft, '\.'), 'notes') >= 0 endfunction function! xolox#notes#buffer_is_note() " {{{2 " Check whether the current buffer is a note (with the correct file type and path). let bufpath = expand('%:p:h') if xolox#notes#filetype_is_note(&ft) for directory in xolox#notes#find_directories(1) if xolox#misc#path#equals(bufpath, directory) return 1 endif endfor endif endfunction function! xolox#notes#current_title() " {{{2 " Get the title of the current note. let title = getline(1) let trimmed = xolox#misc#str#trim(title) if title != trimmed call setline(1, trimmed) endif return trimmed endfunction function! xolox#notes#friendly_date(time) " {{{2 " Format a date as a human readable string. let format = '%A, %B %d, %Y' let today = strftime(format, localtime()) let yesterday = strftime(format, localtime() - 60*60*24) let datestr = strftime(format, a:time) if datestr == today return "today" elseif datestr == yesterday return "yesterday" else return datestr endif endfunction function! s:internal_search(bang, pattern, keywords, phase2) " {{{2 " Search notes for {pattern} regex, try to accelerate with {keywords} search. let bufnr_save = bufnr('%') let pattern = a:pattern silent cclose " Find all notes matching the given keywords or regex. let notes = [] let phase2_needed = 1 if a:keywords != '' && s:run_scanner(a:keywords, notes) if a:phase2 != '' let pattern = a:phase2 endif else call s:vimgrep_wrapper(a:bang, a:pattern, xolox#notes#get_fnames(0)) let notes = s:qflist_to_filenames() if a:phase2 != '' let pattern = a:phase2 else let phase2_needed = 0 endif endif if empty(notes) call xolox#misc#msg#warn("notes.vim %s: No matches", g:xolox#notes#version) return endif " If we performed a keyword search using the scanner.py script we need to " run :vimgrep to populate the quick-fix list. If we're emulating keyword " search using :vimgrep we need to run :vimgrep another time to get the " quick-fix list in the right format :-| if phase2_needed call s:vimgrep_wrapper(a:bang, pattern, notes) endif if a:bang == '' && bufnr('%') != bufnr_save " If :vimgrep opens the first matching file while &eventignore is still " set the file will be opened without activating a file type plug-in or " syntax script. Here's a workaround: doautocmd filetypedetect BufRead endif silent cwindow if &buftype == 'quickfix' execute 'match IncSearch' (&ignorecase ? substitute(pattern, '^/', '/\\c', '') : pattern) endif endfunction function! s:vimgrep_wrapper(bang, pattern, files) " {{{2 " Search for {pattern} in {files} using :vimgrep. let starttime = xolox#misc#timer#start() let args = map(copy(a:files), 'fnameescape(v:val)') call insert(args, a:pattern . 'j') let s:swaphack_enabled = 1 try let ei_save = &eventignore set eventignore=syntax,bufread execute 'vimgrep' . a:bang join(args) call xolox#misc#timer#stop("notes.vim %s: Populated quick-fix window in %s.", g:xolox#notes#version, starttime) finally let &eventignore = ei_save unlet s:swaphack_enabled endtry endfunction function! s:qflist_to_filenames() " {{{2 " Get filenames of matched notes from quick-fix list. let names = {} for entry in getqflist() let names[xolox#misc#path#absolute(bufname(entry.bufnr))] = 1 endfor return keys(names) endfunction function! s:run_scanner(keywords, matches) " {{{2 " Try to run scanner.py script to find notes matching {keywords}. call xolox#misc#msg#info("notes.vim %s: Searching notes using keyword index ..", g:xolox#notes#version) let [success, notes] = s:python_command(a:keywords) if success 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') call extend(a:matches, notes) return 1 endif endfunction function! xolox#notes#keyword_complete(arglead, cmdline, cursorpos) " {{{2 " Search keyword completion for the :SearchNotes command. call inputsave() let [success, keywords] = s:python_command('--list=' . a:arglead) call inputrestore() return keywords endfunction function! s:python_command(...) " {{{2 " Vim function to interface with the "search-notes.py" script. let script = xolox#misc#path#absolute(g:notes_indexscript) let python = executable('python2') ? 'python2' : 'python' let output = [] let success = 0 if !(executable(python) && filereadable(script)) call xolox#misc#msg#debug("notes.vim %s: We can't execute the %s script!", g:xolox#notes#version, script) else let options = ['--database', g:notes_indexfile] if &ignorecase call add(options, '--ignore-case') endif for directory in xolox#notes#find_directories(0) call extend(options, ['--notes', directory]) endfor let arguments = map([script] + options + a:000, 'xolox#misc#escape#shell(v:val)') let command = join([python] + arguments) call xolox#misc#msg#debug("notes.vim %s: Executing external command %s", g:xolox#notes#version, command) if !filereadable(xolox#misc#path#absolute(g:notes_indexfile)) call xolox#misc#msg#info("notes.vim %s: Building keyword index (this might take a while) ..", g:xolox#notes#version) endif let result = xolox#misc#os#exec({'command': command, 'check': 0}) if result['exit_code'] != 0 call xolox#misc#msg#warn("notes.vim %s: Search script failed! Context: %s", g:xolox#notes#version, string(result)) else let lines = result['stdout'] call xolox#misc#msg#debug("notes.vim %s: Search script output (raw): %s", g:xolox#notes#version, string(lines)) if !empty(lines) && lines[0] == 'Python works fine!' let output = lines[1:] let success = 1 call xolox#misc#msg#debug("notes.vim %s: Search script output (processed): %s", g:xolox#notes#version, string(output)) else call xolox#misc#msg#warn("notes.vim %s: Search script returned invalid output :-(", g:xolox#notes#version) endif endif endif return [success, output] endfunction " Getters for filenames & titles of existing notes. {{{2 if !exists('s:cache_mtime') let s:have_cached_names = 0 let s:have_cached_titles = 0 let s:have_cached_items = 0 let s:cached_fnames = [] let s:cached_titles = [] let s:cached_pairs = {} let s:cache_mtime = 0 let s:shadow_notes = ['New note', 'Note taking commands', 'Note taking syntax'] endif function! xolox#notes#get_fnames(include_shadow_notes) " {{{3 " Get list with filenames of all existing notes. if !s:have_cached_names let starttime = xolox#misc#timer#start() for directory in xolox#notes#find_directories(0) let pattern = xolox#misc#path#merge(directory, '*') let listing = glob(xolox#misc#path#absolute(pattern)) call extend(s:cached_fnames, filter(split(listing, '\n'), 'filereadable(v:val)')) endfor let s:have_cached_names = 1 call xolox#misc#timer#stop('notes.vim %s: Cached note filenames in %s.', g:xolox#notes#version, starttime) endif let fnames = copy(s:cached_fnames) if a:include_shadow_notes for title in s:shadow_notes call add(fnames, xolox#misc#path#merge(g:notes_shadowdir, title)) endfor endif return fnames endfunction function! xolox#notes#get_titles(include_shadow_notes) " {{{3 " Get list with titles of all existing notes. if !s:have_cached_titles let starttime = xolox#misc#timer#start() for filename in xolox#notes#get_fnames(0) call add(s:cached_titles, xolox#notes#fname_to_title(filename)) endfor let s:have_cached_titles = 1 call xolox#misc#timer#stop('notes.vim %s: Cached note titles in %s.', g:xolox#notes#version, starttime) endif let titles = copy(s:cached_titles) if a:include_shadow_notes call extend(titles, s:shadow_notes) endif return titles endfunction function! xolox#notes#exists(title) " {{{3 " Return true if the note {title} exists. return index(xolox#notes#get_titles(0), a:title, 0, xolox#misc#os#is_win()) >= 0 endfunction function! xolox#notes#get_fnames_and_titles(include_shadow_notes) " {{{3 " Get dictionary of filename => title pairs of all existing notes. if !s:have_cached_items let starttime = xolox#misc#timer#start() let fnames = xolox#notes#get_fnames(0) let titles = xolox#notes#get_titles(0) let limit = len(fnames) let index = 0 while index < limit let s:cached_pairs[fnames[index]] = titles[index] let index += 1 endwhile let s:have_cached_items = 1 call xolox#misc#timer#stop('notes.vim %s: Cached note filenames and titles in %s.', g:xolox#notes#version, starttime) endif let pairs = copy(s:cached_pairs) if a:include_shadow_notes for title in s:shadow_notes let fname = xolox#misc#path#merge(g:notes_shadowdir, title) let pairs[fname] = title endfor endif return pairs endfunction function! xolox#notes#fname_to_title(filename) " {{{3 " Convert absolute note {filename} to title. let fname = a:filename " Strip suffix? if fname[-len(g:notes_suffix):] == g:notes_suffix let fname = fname[0:-len(g:notes_suffix)-1] endif " Strip directory path. let fname = fnamemodify(fname, ':t') " Decode special characters. return xolox#misc#path#decode(fname) endfunction function! xolox#notes#title_to_fname(title) " {{{3 " Convert note {title} to absolute filename. let filename = xolox#misc#path#encode(a:title) if filename != '' let directory = xolox#notes#select_directory() let pathname = xolox#misc#path#merge(directory, filename . g:notes_suffix) return xolox#misc#path#absolute(pathname) endif return '' endfunction function! xolox#notes#select_directory() " {{{3 " Pick the best suited directory for creating a new note. let bufdir = expand('%:p:h') let notes_directories = xolox#notes#find_directories(0) for directory in notes_directories if xolox#misc#path#equals(bufdir, directory) return directory endif endfor return notes_directories[0] endfunction function! xolox#notes#cache_add(filename, title) " {{{3 " Add {filename} and {title} of new note to cache. let filename = xolox#misc#path#absolute(a:filename) if index(s:cached_fnames, filename) == -1 call add(s:cached_fnames, filename) if !empty(s:cached_titles) call add(s:cached_titles, a:title) endif if !empty(s:cached_pairs) let s:cached_pairs[filename] = a:title endif let s:cache_mtime = localtime() endif endfunction function! xolox#notes#cache_del(filename) " {{{3 " Delete {filename} from cache. let filename = xolox#misc#path#absolute(a:filename) let index = index(s:cached_fnames, filename) if index >= 0 call remove(s:cached_fnames, index) if !empty(s:cached_titles) call remove(s:cached_titles, index) endif if !empty(s:cached_pairs) call remove(s:cached_pairs, filename) endif let s:cache_mtime = localtime() endif endfunction function! xolox#notes#unload_from_cache() " {{{3 " Forget deleted notes automatically (called by "BufUnload" automatic command). let bufname = expand(':p') if !filereadable(bufname) call xolox#notes#cache_del(bufname) endif endfunction " Functions called by the file type plug-in and syntax script. {{{2 function! xolox#notes#insert_ruler() " {{{3 " Insert horizontal ruler delimited by empty lines. let lnum = line('.') if getline(lnum) =~ '\S' && getline(lnum + 1) !~ '\S' let lnum += 1 endif let line1 = prevnonblank(lnum) let line2 = nextnonblank(lnum) if line1 < lnum && line2 > lnum execute printf('%i,%idelete', line1 + 1, line2 - 1) endif call append(line1, ['', g:notes_ruler_text, '']) endfunction function! xolox#notes#insert_quote(style) " {{{3 " XXX When I pass the below string constants as arguments from the file type " plug-in the resulting strings contain mojibake (UTF-8 interpreted as " latin1?) even if both scripts contain a UTF-8 BOM! Maybe a bug in Vim?! if xolox#notes#unicode_enabled() let [open_quote, close_quote] = a:style == 1 ? ['‘', '’'] : ['“', '”'] else let [open_quote, close_quote] = a:style == 1 ? ['`', "'"] : ['"', '"'] endif return getline('.')[col('.')-2] =~ '[^\t (]$' ? close_quote : open_quote endfunction function! xolox#notes#insert_bullet(chr) " {{{3 " Insert a UTF-8 list bullet when the user types "*". if getline('.')[0 : max([0, col('.') - 2])] =~ '^\s*$' return xolox#notes#get_bullet(a:chr) endif return a:chr endfunction function! xolox#notes#get_bullet(chr) return xolox#notes#unicode_enabled() ? '•' : a:chr endfunction function! xolox#notes#indent_list(direction, line1, line2) " {{{3 " Change indent of list items from {line1} to {line2} using {command}. let indentstr = repeat(' ', &tabstop) if a:line1 == a:line2 && getline(a:line1) == '' call setline(a:line1, indentstr) else " Regex to match a leading bullet. let leading_bullet = xolox#notes#leading_bullet_pattern() for lnum in range(a:line1, a:line2) let line = getline(lnum) " Calculate new nesting level, should not result in < 0. let level = max([0, xolox#notes#get_list_level(line) + a:direction]) if a:direction == 1 " Indent the line. let line = indentstr . line else " Unindent the line. let line = substitute(line, '^' . indentstr, '', '') endif " Replace the bullet. let bullet = g:notes_list_bullets[level % len(g:notes_list_bullets)] call setline(lnum, substitute(line, leading_bullet, xolox#misc#escape#substitute(bullet), '')) endfor " Regex to match a trailing bullet. if getline('.') =~ xolox#notes#trailing_bullet_pattern() " Restore trailing space after list bullet. call setline('.', getline('.') . ' ') endif endif normal $ endfunction function! xolox#notes#leading_bullet_pattern() " Return a regular expression pattern that matches any leading list bullet. let escaped_bullets = copy(g:notes_list_bullets) call map(escaped_bullets, 'xolox#misc#escape#pattern(v:val)') return '\(\_^\s*\)\@<=\(' . join(escaped_bullets, '\|') . '\)' endfunction function! xolox#notes#trailing_bullet_pattern() " Return a regular expression pattern that matches any trailing list bullet. let escaped_bullets = copy(g:notes_list_bullets) call map(escaped_bullets, 'xolox#misc#escape#pattern(v:val)') return '\(' . join(escaped_bullets, '\|') . '\|\*\)$' endfunction function! xolox#notes#get_comments_option() " Get the value for the &comments option including user defined list bullets. let items = copy(g:notes_list_bullets) call map(items, '": " . v:val . " "') call add(items, ':> ') " <- e-mail style block quotes. return join(items, ',') endfunction function! xolox#notes#get_list_level(line) " Get the nesting level of the list item on the given line. This will only " work with the list item indentation style expected by the notes plug-in " (that is, top level list items are indented with one space, each nested " level below that is indented by pairs of three spaces). return (len(matchstr(a:line, '^\s*')) - 1) / 3 endfunction function! xolox#notes#cleanup_list() " {{{3 " Automatically remove empty list items on Enter. if getline('.') =~ (xolox#notes#leading_bullet_pattern() . '\s*$') let s:sol_save = &startofline setlocal nostartofline " <- so that clears the complete line return "\0\d$\o" else if exists('s:sol_save') let &l:startofline = s:sol_save unlet s:sol_save endif return "\" endif endfunction function! xolox#notes#refresh_syntax() " {{{3 " Update syntax highlighting of note names and code blocks. if xolox#notes#filetype_is_note(&ft) && line('$') > 1 let starttime = xolox#misc#timer#start() call xolox#notes#highlight_names(0) call xolox#notes#highlight_sources(0) call xolox#misc#timer#stop("notes.vim %s: Refreshed highlighting in %s.", g:xolox#notes#version, starttime) endif endfunction function! xolox#notes#highlight_names(force) " {{{3 " Highlight the names of all notes as "notesName" (linked to "Underlined"). if a:force || !(exists('b:notes_names_last_highlighted') && b:notes_names_last_highlighted > s:cache_mtime) let starttime = xolox#misc#timer#start() let current_note = xolox#notes#current_title() let titles = filter(xolox#notes#get_titles(1), '!empty(v:val) && v:val != current_note') call map(titles, 's:words_to_pattern(v:val)') call sort(titles, 's:sort_longest_to_shortest') if hlexists('notesName') syntax clear notesName endif execute 'syntax match notesName /\c\%>1l\%(' . escape(join(titles, '\|'), '/') . '\)/' let b:notes_names_last_highlighted = localtime() call xolox#misc#timer#stop("notes.vim %s: Highlighted note names in %s.", g:xolox#notes#version, starttime) endif endfunction function! s:words_to_pattern(words) " Quote regex meta characters, enable matching of hard wrapped words. return substitute(xolox#misc#escape#pattern(a:words), '\s\+', '\\_s\\+', 'g') endfunction function! s:sort_longest_to_shortest(a, b) " Sort note titles by length, starting with the shortest. return len(a:a) < len(a:b) ? 1 : -1 endfunction function! xolox#notes#highlight_sources(force) " {{{3 " Syntax highlight source code embedded in notes. let starttime = xolox#misc#timer#start() " Look for code blocks in the current note. let filetypes = {} for line in getline(1, '$') let ft = matchstr(line, '{{' . '{\zs\w\+\>') if ft !~ '^\d*$' | let filetypes[ft] = 1 | endif endfor " Don't refresh the highlighting if nothing has changed. if !a:force && exists('b:notes_previous_sources') && b:notes_previous_sources == filetypes return else let b:notes_previous_sources = filetypes endif " Now we're ready to actually highlight the code blocks. if !empty(filetypes) let startgroup = 'notesCodeStart' let endgroup = 'notesCodeEnd' for ft in keys(filetypes) let group = 'notesSnippet' . toupper(ft) let include = s:syntax_include(ft) let command = 'syntax region %s matchgroup=%s start="{{{%s" matchgroup=%s end="}}}" keepend contains=%s%s' execute printf(command, group, startgroup, ft, endgroup, include, has('conceal') ? ' concealends' : '') endfor if &vbs >= 1 call xolox#misc#timer#stop("notes.vim %s: Highlighted embedded %s sources in %s.", g:xolox#notes#version, join(sort(keys(filetypes)), '/'), starttime) endif endif endfunction function! s:syntax_include(filetype) " Include the syntax highlighting of another {filetype}. let grouplistname = '@' . toupper(a:filetype) " Unset the name of the current syntax while including the other syntax " because some syntax scripts do nothing when "b:current_syntax" is set. if exists('b:current_syntax') let syntax_save = b:current_syntax unlet b:current_syntax endif try execute 'syntax include' grouplistname 'syntax/' . a:filetype . '.vim' execute 'syntax include' grouplistname 'after/syntax/' . a:filetype . '.vim' catch /E484/ " Ignore missing scripts. endtry " Restore the name of the current syntax. if exists('syntax_save') let b:current_syntax = syntax_save elseif exists('b:current_syntax') unlet b:current_syntax endif return grouplistname endfunction function! xolox#notes#include_expr(fname) " {{{3 " Translate string {fname} to absolute filename of note. " TODO Use inputlist() when more than one note matches?! let notes = copy(xolox#notes#get_fnames_and_titles(1)) let pattern = xolox#misc#escape#pattern(a:fname) call filter(notes, 'v:val =~ pattern') if !empty(notes) let filtered_notes = items(notes) let lnum = line('.') for range in range(3) let line1 = lnum - range let line2 = lnum + range let text = s:normalize_ws(join(getline(line1, line2), "\n")) for [fname, title] in filtered_notes if text =~? xolox#misc#escape#pattern(s:normalize_ws(title)) return fname endif endfor endfor endif return '' endfunction function! s:normalize_ws(s) " Enable string comparison that ignores differences in whitespace. return xolox#misc#str#trim(substitute(a:s, '\_s\+', '', 'g')) endfunction function! xolox#notes#foldexpr() " {{{3 " Folding expression to fold atx style Markdown headings. let lastlevel = foldlevel(v:lnum - 1) let nextlevel = match(getline(v:lnum), '^#\+\zs') let retval = '=' if lastlevel <= 0 && nextlevel >= 1 let retval = '>' . nextlevel elseif nextlevel >= 1 if lastlevel > nextlevel let retval = '<' . nextlevel else let retval = '>' . nextlevel endif endif if retval != '=' " Check whether the change in folding introduced by 'rv' " is invalidated because we're inside a code block. let pos_save = getpos('.') try call setpos('.', [0, v:lnum, 1, 0]) if search('{{{\|\(}}}\)', 'bnpW') == 1 let retval = '=' endif finally " Always restore the cursor position! call setpos('.', pos_save) endtry endif return retval endfunction function! xolox#notes#foldtext() " {{{3 " Replace atx style "#" markers with "-" fold marker. let line = getline(v:foldstart) if line == '' let line = getline(v:foldstart + 1) endif let matches = matchlist(line, '^\(#\+\)\s*\(.*\)$') if len(matches) >= 3 let prefix = repeat('-', len(matches[1])) return prefix . ' ' . matches[2] . ' ' else return line endif endfunction " }}}1 " Make sure the plug-in configuration has been properly initialized before " any of the auto-load functions in this Vim script can be called. call xolox#notes#init() " vim: ts=2 sw=2 et bomb