Skip to content

Commit

Permalink
Incremental tags index, improved :ShowTaggedNotes command
Browse files Browse the repository at this point in the history
  • Loading branch information
xolox committed Sep 4, 2011
1 parent 80b6ef5 commit 528fdcb
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 129 deletions.
25 changes: 16 additions & 9 deletions README.md
Expand Up @@ -54,7 +54,7 @@ This option defines the pathname of the Python script that's used to perform acc

### The `g:notes_tagsindex` option

This option defined the pathname of the text file that stores the list of known tags used for tag name completion. The text file is created automatically when you first use tag name completion, after that you can update it manually by executing `:IndexTaggedNotes` (see below).
This option defines the pathname of the text file that stores the list of known tags used for tag name completion and the `:ShowTaggedNotes` command. The text file is created automatically when it's first needed, after that you can recreate it manually by executing `:IndexTaggedNotes` (see below).

## Commands

Expand Down Expand Up @@ -84,14 +84,6 @@ When you execute this command it will start a new note with the selected text as

The `:DeleteNote` command deletes the current note, destroys the buffer and removes the note from the internal cache of filenames and note titles. This fails when changes have been made to the current buffer, unless you use `:DeleteNote!` which discards any changes.

### The `:IndexTaggedNotes` command

The notes plug-in defines an omni completion function that can be used to complete the names of tags. To trigger the omni completion you type Control-X Control-O. When you type `@` in insert mode the plug-in will automatically start omni completion.

The completion menu is populated from a text file listing all your tags, one on each line. The first time omni completion triggers, an index of tag names is generated and saved to the location set by `g:notes_tagsindex`. To update this tags index you need to execute the `:IndexTaggedNotes` command.

If you execute this command with a bang as in `:IndexTaggedNotes!` it wil open a split window with a cross reference of all the tags you've used and the files in which each tag has been used.

### The `:SearchNotes` command

This command wraps [:vimgrep] [vimgrep] and enables you to search through your notes using one or more keywords or a regular expression pattern. To search for a pattern you pass a single argument that starts/ends with a slash:
Expand Down Expand Up @@ -133,6 +125,21 @@ This command makes it easy to find all notes related to the current file: If you

If you execute the `:RecentNotes` command it will open a Vim buffer that lists all your notes grouped by the day they were edited, starting with your most recently edited note. If you pass an argument to `:RecentNotes` it will filter the list of notes by matching the title of each note against the argument which is interpreted as a Vim pattern.

### The `:ShowTaggedNotes` command

To show a list of all notes that contains *@tags@* you can use the `:ShowTaggedNotes` command. If you pass a count to this command it will limit the list of tags to those that have been used at least this many times. For example the following two commands show tags that have been used at least ten times:

:10ShowTaggedNotes
:ShowTaggedNotes 10

### The `:IndexTaggedNotes` command

The notes plug-in defines an omni completion function that can be used to complete the names of tags. To trigger the omni completion you type Control-X Control-O. When you type `@` in insert mode the plug-in will automatically start omni completion.

The completion menu is populated from a text file listing all your tags, one on each line. The first time omni completion triggers, an index of tag names is generated and saved to the location set by `g:notes_tagsindex`. After this file is created, it will be updated automatically as you edit notes and add/remove tags.

If for any reason you want to recreate the list of tags you can execute the `:IndexTaggedNotes` command.

## Other plug-ins that work well with the notes plug-in

* The [utl.vim] [utl] universal text linking plug-in enables links between your notes, other local files and remote resources like web pages
Expand Down
1 change: 1 addition & 0 deletions TODO.md
@@ -1,5 +1,6 @@
# To-do list for the `notes.vim` plug-in

* Add a key mapping to toggle text folding (currently in my `~/.vimrc`)
* Add a key mapping or command to toggle the visibility of `{{{ … }}}` code markers?
* Find a good way to support notes with generates contents, e.g. *'all notes'*.
* When renaming a note, also update references to the note in other notes? (make this optional of course!)
Expand Down
114 changes: 27 additions & 87 deletions autoload/xolox/notes.vim
Expand Up @@ -6,7 +6,7 @@
" 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.10.6'
let g:xolox#notes#version = '0.11'

function! xolox#notes#shortcut() " {{{1
" The "note:" pseudo protocol is just a shortcut for the :Note command.
Expand Down Expand Up @@ -111,7 +111,7 @@ function! xolox#notes#select(filter) " {{{1
elseif !empty(notes)
let choices = ['Please select a note:']
let values = ['']
for fname in sort(keys(notes))
for fname in sort(keys(notes), 1)
call add(choices, ' ' . len(choices) . ') ' . notes[fname])
call add(values, fname)
endfor
Expand Down Expand Up @@ -183,77 +183,12 @@ function! xolox#notes#omni_complete(findstart, base) " {{{1
" 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('.') - 1
return col('.')
else
let fname = expand(g:notes_tagsindex)
if !filereadable(fname)
return xolox#notes#index_tagged_notes(0)
else
return readfile(fname)
endif
return sort(keys(xolox#notes#tags#load_index()), 1)
endif
endfunction

function! xolox#notes#index_tagged_notes(verbose) " {{{1
let starttime = xolox#misc#timer#start()
let notes = xolox#notes#get_fnames(0)
let num_notes = len(notes)
let known_tags = {}
for idx in range(len(notes))
let fname = notes[idx]
call xolox#misc#msg#info("notes.vim %s: Scanning note %i of %i: %s", g:xolox#notes#version, idx + 1, num_notes, fname)
let text = join(readfile(fname), "\n")
" Strip code blocks from the text.
let text = substitute(text, '{{{\w\+\_.\{-}}}}', '', 'g')
for token in filter(split(text), 'v:val =~ "^@"')
" Strip any trailing punctuation.
let token = substitute(token, '[[:punct:]]*$', '', '')
if token != ''
if !a:verbose
let known_tags[token] = 1
else
" Track the origins of tags.
if !has_key(known_tags, token)
let known_tags[token] = {}
endif
let known_tags[token][fname] = 1
endif
endif
endfor
endfor
" Save the index of known tags as a text file.
let fname = expand(g:notes_tagsindex)
let tagnames = keys(known_tags)
call sort(tagnames, 1)
if writefile(tagnames, fname) != 0
call xolox#misc#msg#warn("notes.vim %s: Failed to save tags index as %s!", g:xolox#notes#version, fname)
else
call xolox#misc#timer#stop('notes.vim %s: Indexed tags in %s.', g:xolox#notes#version, starttime)
endif
if !a:verbose
return tagnames
endif
" If the user executed :IndexTaggedNotes! we show them the origins of tags,
" because after the first time I tried the :IndexTaggedNotes command I was
" immediately wondering where all of those false positives came from... This
" doesn't give a complete picture (doing so would slow down the indexing
" and complicate this code significantly) but it's better than nothing!
let lines = ['All tags', '', printf("You have used %i tags in your notes, they're listed below.", len(known_tags))]
let bullet = s:get_bullet('*')
for tagname in tagnames
call extend(lines, ['', '# ' . tagname, ''])
let fnames = keys(known_tags[tagname])
let titles = map(fnames, 'xolox#notes#fname_to_title(v:val)')
call sort(titles, 1)
for title in titles
call add(lines, ' ' . bullet . ' ' . title)
endfor
endfor
vnew
call setline(1, lines)
setlocal ft=notes nomod
endfunction

function! xolox#notes#save() abort " {{{1
" When the current note's title is changed, automatically rename the file.
if &filetype == 'notes'
Expand All @@ -280,6 +215,11 @@ function! xolox#notes#save() abort " {{{1
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
Expand Down Expand Up @@ -432,24 +372,14 @@ function! xolox#notes#recent(bang, title_filter) " {{{1
" Sort, group and format list of (matching) notes.
let last_date = ''
let list_item_format = xolox#notes#unicode_enabled() ? ' • %s' : ' * %s'
let date_format = '%A, %B %d:'
let today = strftime(date_format, localtime())
let yesterday = strftime(date_format, localtime() - 60*60*24)
call sort(notes)
call reverse(notes)
let lines = []
for [ftime, title] in notes
let date = strftime(date_format, ftime)
" Add date heading because date changed?
let date = xolox#notes#friendly_date(ftime)
if date != last_date
call add(lines, '')
if date == today
call add(lines, "Today:")
elseif date == yesterday
call add(lines, "Yesterday:")
else
call add(lines, date)
endif
call add(lines, substitute(date, '^\w', '\u\0', '') . ':')
let last_date = date
endif
call add(lines, printf(list_item_format, title))
Expand All @@ -461,9 +391,18 @@ endfunction

" Miscellaneous functions. {{{1

function! s:is_empty_buffer() " {{{2
" Check if the buffer is an empty, unchanged buffer which can be reused.
return !&modified && expand('%') == '' && line('$') <= 1 && getline(1) == ''
function! xolox#notes#friendly_date(time) " {{{2
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
Expand Down Expand Up @@ -713,12 +652,12 @@ 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 s:get_bullet(a:chr)
return xolox#notes#get_bullet(a:chr)
endif
return a:chr
endfunction

function! s:get_bullet(chr)
function! xolox#notes#get_bullet(chr)
return xolox#notes#unicode_enabled() ? '' : a:chr
endfunction

Expand All @@ -729,6 +668,7 @@ function! xolox#notes#indent_list(command, line1, line2) " {{{3
else
execute a:line1 . ',' . a:line2 . 'normal' a:command
if getline('.') =~ '\(•\|\*\)$'
" Restore trailing space after list bullet.
call setline('.', getline('.') . ' ')
endif
endif
Expand All @@ -737,7 +677,7 @@ endfunction

function! xolox#notes#cleanup_list() " {{{3
" Automatically remove empty list items on Enter.
if getline('.') =~ '^\s*\' . s:get_bullet('*') . '\s*$'
if getline('.') =~ '^\s*\' . xolox#notes#get_bullet('*') . '\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"
Expand Down
144 changes: 144 additions & 0 deletions autoload/xolox/notes/tags.vim
@@ -0,0 +1,144 @@
" Vim auto-load script
" Author: Peter Odding <peter@peterodding.com>
" Last Change: September 4, 2011
" URL: http://peterodding.com/code/vim/notes/

if !exists('s:currently_tagged_notes')
let s:currently_tagged_notes = {} " The in-memory representation of tags and the notes in which they're used.
let s:previously_tagged_notes = {} " Copy of index as it is / should be now on disk (to detect changes).
let s:last_disk_sync = 0 " Whether the on-disk representation of the tags has been read.
let s:buffer_name = 'Tagged Notes' " The buffer name for the list of tagged notes.
endif

function! xolox#notes#tags#load_index() " {{{1
let starttime = xolox#misc#timer#start()
let indexfile = expand(g:notes_tagsindex)
let lastmodified = getftime(indexfile)
if lastmodified == -1
call xolox#notes#tags#create_index()
elseif lastmodified > s:last_disk_sync
let s:currently_tagged_notes = {}
for line in readfile(indexfile)
let filenames = split(line, "\t")
if len(filenames) > 1
let tagname = remove(filenames, 0)
let s:currently_tagged_notes[tagname] = filenames
endif
endfor
let s:previously_tagged_notes = deepcopy(s:currently_tagged_notes)
let s:last_disk_sync = lastmodified
call xolox#misc#timer#stop("notes.vim %s: Loaded tags index in %s.", g:xolox#notes#version, starttime)
endif
return s:currently_tagged_notes
endfunction

function! xolox#notes#tags#create_index() " {{{1
let exists = filereadable(expand(g:notes_tagsindex))
let starttime = xolox#misc#timer#start()
let filenames = xolox#notes#get_fnames(0)
let s:currently_tagged_notes = {}
for idx in range(len(filenames))
let title = xolox#notes#fname_to_title(filenames[idx])
call xolox#misc#msg#info("notes.vim %s: Scanning note %i/%i: %s", g:xolox#notes#version, idx + 1, len(filenames), title)
call xolox#notes#tags#scan_note(title, join(readfile(filenames[idx]), "\n"))
endfor
if xolox#notes#tags#save_index()
let s:previously_tagged_notes = deepcopy(s:currently_tagged_notes)
call xolox#misc#timer#stop('notes.vim %s: %s tags index in %s.', g:xolox#notes#version, exists ? "Updated" : "Created", starttime)
else
call xolox#misc#msg#warn("notes.vim %s: Failed to save tags index as %s!", g:xolox#notes#version, g:notes_tagsindex)
endif
endfunction

function! xolox#notes#tags#save_index() " {{{1
if s:currently_tagged_notes == s:previously_tagged_notes
return 1 " Nothing to be done
else
let lines = []
for [tagname, filenames] in items(s:currently_tagged_notes)
call add(lines, join([tagname] + filenames, "\t"))
endfor
let indexfile = expand(g:notes_tagsindex)
let status = writefile(lines, indexfile) == 0
if status
let s:last_disk_sync = getftime(indexfile)
endif
return status
endif
endfunction

function! xolox#notes#tags#scan_note(title, text) " {{{1
" Add a note to the tags index.
call xolox#notes#tags#load_index()
for token in split(substitute(a:text, '{{{\w\+\_.\{-}}}}', '', 'g'))
if token =~ '^@\w'
let token = substitute(token[1:], '[[:punct:]]*$', '', '')
if token != ''
if !has_key(s:currently_tagged_notes, token)
let s:currently_tagged_notes[token] = [a:title]
elseif index(s:currently_tagged_notes[token], a:title) == -1
call xolox#misc#list#binsert(s:currently_tagged_notes[token], a:title, 1)
endif
endif
endif
endfor
endfunction

function! xolox#notes#tags#forget_note(title) " {{{1
" Remove a note from the tags index.
call xolox#notes#tags#load_index()
for tagname in keys(s:currently_tagged_notes)
call filter(s:currently_tagged_notes[tagname], "v:val != a:title")
if empty(s:currently_tagged_notes[tagname])
unlet s:currently_tagged_notes[tagname]
endif
endfor
endfunction

function! xolox#notes#tags#show_tags(minsize) " {{{1
" TODO Mappings to "zoom" in/out (show only big tags).
call xolox#notes#tags#load_index()
let lines = [s:buffer_name, '']
if empty(s:currently_tagged_notes)
call add(lines, "You haven't used any tags yet!")
else
let bullet = xolox#notes#get_bullet('*')
let numtags = 0
for tagname in sort(keys(s:currently_tagged_notes), 1)
let friendly_name = xolox#notes#tags#friendly_name(tagname)
let numnotes = len(s:currently_tagged_notes[tagname])
if numnotes >= a:minsize
call extend(lines, ['', printf('# %s (%i note%s)', friendly_name, numnotes, numnotes == 1 ? '' : 's'), ''])
for title in s:currently_tagged_notes[tagname]
let lastmodified = xolox#notes#friendly_date(getftime(xolox#notes#title_to_fname(title)))
call add(lines, ' ' . bullet . ' ' . title . ' (last edited ' . lastmodified . ')')
endfor
let numtags += 1
endif
endfor
if a:minsize <= 1
let message = printf("You've used %i %s in your notes",
\ numtags, numtags == 1 ? "tag" : "tags")
else
let message = printf("There %s %i %s that %s been used at least %s times",
\ numtags == 1 ? "is" : "are", numtags,
\ numtags == 1 ? "tag" : "tags",
\ numtags == 1 ? "has" : "have", a:minsize)
endif
let message .= ", "
let message .= numtags == 1 ? "it's" : "they're"
let message .= " listed below. Tags and notes are sorted alphabetically and after each note is the date when it was last modified."
if numtags > 1 && !(&foldmethod == 'expr' && &foldenable)
let message .= " You can enable text folding to get an overview of just the tag names and how many times they've been used."
endif
call insert(lines, message, 2)
endif
call xolox#misc#buffer#prepare(s:buffer_name)
call setline(1, lines)
call xolox#misc#buffer#lock()
setlocal filetype=notes nospell wrap
endfunction

function! xolox#notes#tags#friendly_name(tagname) " {{{1
return substitute(a:tagname, '\(\U\)\(\u\)', '\1 \2', 'g')
endfunction

0 comments on commit 528fdcb

Please sign in to comment.