1221 lines
46 KiB
VimL
1221 lines
46 KiB
VimL
" Vim auto-load script
|
||
" Author: Peter Odding <peter@peterodding.com>
|
||
" 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('<sfile>: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('<afile>')
|
||
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 <Escape>'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('<amatch>'))
|
||
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, '\\\@<!|')
|
||
let cmdargs = substitute(cmdline[-1], '^\s*\w\+\s\+', '', '')
|
||
let arguments = split(cmdargs)
|
||
let titles = xolox#notes#get_titles(1)
|
||
if a:arglead != '' && len(arguments) == 1
|
||
" If we are completing a single argument and we are able to replace it
|
||
" (the user didn't type <Space><Tab> 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
|
||
" <Space><Tab> 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('<cword>')
|
||
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('<afile>: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 <C-u> clears the complete line
|
||
return "\<C-o>0\<C-o>d$\<C-o>o"
|
||
else
|
||
if exists('s:sol_save')
|
||
let &l:startofline = s:sol_save
|
||
unlet s:sol_save
|
||
endif
|
||
return "\<CR>"
|
||
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
|