Skip to content

Commit 528fdcb

Browse files
committed
Incremental tags index, improved :ShowTaggedNotes command
1 parent 80b6ef5 commit 528fdcb

8 files changed

Lines changed: 242 additions & 129 deletions

File tree

README.md

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ This option defines the pathname of the Python script that's used to perform acc
5454

5555
### The `g:notes_tagsindex` option
5656

57-
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).
57+
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).
5858

5959
## Commands
6060

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

8585
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.
8686

87-
### The `:IndexTaggedNotes` command
88-
89-
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.
90-
91-
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.
92-
93-
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.
94-
9587
### The `:SearchNotes` command
9688

9789
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:
@@ -133,6 +125,21 @@ This command makes it easy to find all notes related to the current file: If you
133125

134126
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.
135127

128+
### The `:ShowTaggedNotes` command
129+
130+
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:
131+
132+
:10ShowTaggedNotes
133+
:ShowTaggedNotes 10
134+
135+
### The `:IndexTaggedNotes` command
136+
137+
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.
138+
139+
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.
140+
141+
If for any reason you want to recreate the list of tags you can execute the `:IndexTaggedNotes` command.
142+
136143
## Other plug-ins that work well with the notes plug-in
137144

138145
* The [utl.vim] [utl] universal text linking plug-in enables links between your notes, other local files and remote resources like web pages

TODO.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# To-do list for the `notes.vim` plug-in
22

3+
* Add a key mapping to toggle text folding (currently in my `~/.vimrc`)
34
* Add a key mapping or command to toggle the visibility of `{{{ … }}}` code markers?
45
* Find a good way to support notes with generates contents, e.g. *'all notes'*.
56
* When renaming a note, also update references to the note in other notes? (make this optional of course!)

autoload/xolox/notes.vim

Lines changed: 27 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
" Note: This file is encoded in UTF-8 including a byte order mark so
77
" that Vim loads the script using the right encoding transparently.
88

9-
let g:xolox#notes#version = '0.10.6'
9+
let g:xolox#notes#version = '0.11'
1010

1111
function! xolox#notes#shortcut() " {{{1
1212
" The "note:" pseudo protocol is just a shortcut for the :Note command.
@@ -111,7 +111,7 @@ function! xolox#notes#select(filter) " {{{1
111111
elseif !empty(notes)
112112
let choices = ['Please select a note:']
113113
let values = ['']
114-
for fname in sort(keys(notes))
114+
for fname in sort(keys(notes), 1)
115115
call add(choices, ' ' . len(choices) . ') ' . notes[fname])
116116
call add(values, fname)
117117
endfor
@@ -183,77 +183,12 @@ function! xolox#notes#omni_complete(findstart, base) " {{{1
183183
" leading "@" here and otherwise make it complete e.g. note names, so that
184184
" there's only one way to complete inside notes and the plug-in is smart
185185
" enough to know what the user wants to complete :-)
186-
return col('.') - 1
186+
return col('.')
187187
else
188-
let fname = expand(g:notes_tagsindex)
189-
if !filereadable(fname)
190-
return xolox#notes#index_tagged_notes(0)
191-
else
192-
return readfile(fname)
193-
endif
188+
return sort(keys(xolox#notes#tags#load_index()), 1)
194189
endif
195190
endfunction
196191

197-
function! xolox#notes#index_tagged_notes(verbose) " {{{1
198-
let starttime = xolox#misc#timer#start()
199-
let notes = xolox#notes#get_fnames(0)
200-
let num_notes = len(notes)
201-
let known_tags = {}
202-
for idx in range(len(notes))
203-
let fname = notes[idx]
204-
call xolox#misc#msg#info("notes.vim %s: Scanning note %i of %i: %s", g:xolox#notes#version, idx + 1, num_notes, fname)
205-
let text = join(readfile(fname), "\n")
206-
" Strip code blocks from the text.
207-
let text = substitute(text, '{{{\w\+\_.\{-}}}}', '', 'g')
208-
for token in filter(split(text), 'v:val =~ "^@"')
209-
" Strip any trailing punctuation.
210-
let token = substitute(token, '[[:punct:]]*$', '', '')
211-
if token != ''
212-
if !a:verbose
213-
let known_tags[token] = 1
214-
else
215-
" Track the origins of tags.
216-
if !has_key(known_tags, token)
217-
let known_tags[token] = {}
218-
endif
219-
let known_tags[token][fname] = 1
220-
endif
221-
endif
222-
endfor
223-
endfor
224-
" Save the index of known tags as a text file.
225-
let fname = expand(g:notes_tagsindex)
226-
let tagnames = keys(known_tags)
227-
call sort(tagnames, 1)
228-
if writefile(tagnames, fname) != 0
229-
call xolox#misc#msg#warn("notes.vim %s: Failed to save tags index as %s!", g:xolox#notes#version, fname)
230-
else
231-
call xolox#misc#timer#stop('notes.vim %s: Indexed tags in %s.', g:xolox#notes#version, starttime)
232-
endif
233-
if !a:verbose
234-
return tagnames
235-
endif
236-
" If the user executed :IndexTaggedNotes! we show them the origins of tags,
237-
" because after the first time I tried the :IndexTaggedNotes command I was
238-
" immediately wondering where all of those false positives came from... This
239-
" doesn't give a complete picture (doing so would slow down the indexing
240-
" and complicate this code significantly) but it's better than nothing!
241-
let lines = ['All tags', '', printf("You have used %i tags in your notes, they're listed below.", len(known_tags))]
242-
let bullet = s:get_bullet('*')
243-
for tagname in tagnames
244-
call extend(lines, ['', '# ' . tagname, ''])
245-
let fnames = keys(known_tags[tagname])
246-
let titles = map(fnames, 'xolox#notes#fname_to_title(v:val)')
247-
call sort(titles, 1)
248-
for title in titles
249-
call add(lines, ' ' . bullet . ' ' . title)
250-
endfor
251-
endfor
252-
vnew
253-
call setline(1, lines)
254-
setlocal ft=notes nomod
255-
endfunction
256-
257192
function! xolox#notes#save() abort " {{{1
258193
" When the current note's title is changed, automatically rename the file.
259194
if &filetype == 'notes'
@@ -280,6 +215,11 @@ function! xolox#notes#save() abort " {{{1
280215
endif
281216
call delete(oldpath)
282217
endif
218+
" Update the tags index on disk and in-memory.
219+
call xolox#notes#tags#forget_note(xolox#notes#fname_to_title(oldpath))
220+
call xolox#notes#tags#scan_note(title, join(getline(1, '$'), "\n"))
221+
call xolox#notes#tags#save_index()
222+
" Update in-memory list of all notes.
283223
call xolox#notes#cache_del(oldpath)
284224
call xolox#notes#cache_add(newpath, title)
285225
endif
@@ -432,24 +372,14 @@ function! xolox#notes#recent(bang, title_filter) " {{{1
432372
" Sort, group and format list of (matching) notes.
433373
let last_date = ''
434374
let list_item_format = xolox#notes#unicode_enabled() ? ' • %s' : ' * %s'
435-
let date_format = '%A, %B %d:'
436-
let today = strftime(date_format, localtime())
437-
let yesterday = strftime(date_format, localtime() - 60*60*24)
438375
call sort(notes)
439376
call reverse(notes)
440377
let lines = []
441378
for [ftime, title] in notes
442-
let date = strftime(date_format, ftime)
443-
" Add date heading because date changed?
379+
let date = xolox#notes#friendly_date(ftime)
444380
if date != last_date
445381
call add(lines, '')
446-
if date == today
447-
call add(lines, "Today:")
448-
elseif date == yesterday
449-
call add(lines, "Yesterday:")
450-
else
451-
call add(lines, date)
452-
endif
382+
call add(lines, substitute(date, '^\w', '\u\0', '') . ':')
453383
let last_date = date
454384
endif
455385
call add(lines, printf(list_item_format, title))
@@ -461,9 +391,18 @@ endfunction
461391

462392
" Miscellaneous functions. {{{1
463393

464-
function! s:is_empty_buffer() " {{{2
465-
" Check if the buffer is an empty, unchanged buffer which can be reused.
466-
return !&modified && expand('%') == '' && line('$') <= 1 && getline(1) == ''
394+
function! xolox#notes#friendly_date(time) " {{{2
395+
let format = '%A, %B %d, %Y'
396+
let today = strftime(format, localtime())
397+
let yesterday = strftime(format, localtime() - 60*60*24)
398+
let datestr = strftime(format, a:time)
399+
if datestr == today
400+
return "today"
401+
elseif datestr == yesterday
402+
return "yesterday"
403+
else
404+
return datestr
405+
endif
467406
endfunction
468407

469408
function! s:internal_search(bang, pattern, keywords, phase2) " {{{2
@@ -713,12 +652,12 @@ endfunction
713652
function! xolox#notes#insert_bullet(chr) " {{{3
714653
" Insert a UTF-8 list bullet when the user types "*".
715654
if getline('.')[0 : max([0, col('.') - 2])] =~ '^\s*$'
716-
return s:get_bullet(a:chr)
655+
return xolox#notes#get_bullet(a:chr)
717656
endif
718657
return a:chr
719658
endfunction
720659

721-
function! s:get_bullet(chr)
660+
function! xolox#notes#get_bullet(chr)
722661
return xolox#notes#unicode_enabled() ? '' : a:chr
723662
endfunction
724663

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

738678
function! xolox#notes#cleanup_list() " {{{3
739679
" Automatically remove empty list items on Enter.
740-
if getline('.') =~ '^\s*\' . s:get_bullet('*') . '\s*$'
680+
if getline('.') =~ '^\s*\' . xolox#notes#get_bullet('*') . '\s*$'
741681
let s:sol_save = &startofline
742682
setlocal nostartofline " <- so that <C-u> clears the complete line
743683
return "\<C-o>0\<C-o>d$\<C-o>o"

autoload/xolox/notes/tags.vim

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
" Vim auto-load script
2+
" Author: Peter Odding <peter@peterodding.com>
3+
" Last Change: September 4, 2011
4+
" URL: http://peterodding.com/code/vim/notes/
5+
6+
if !exists('s:currently_tagged_notes')
7+
let s:currently_tagged_notes = {} " The in-memory representation of tags and the notes in which they're used.
8+
let s:previously_tagged_notes = {} " Copy of index as it is / should be now on disk (to detect changes).
9+
let s:last_disk_sync = 0 " Whether the on-disk representation of the tags has been read.
10+
let s:buffer_name = 'Tagged Notes' " The buffer name for the list of tagged notes.
11+
endif
12+
13+
function! xolox#notes#tags#load_index() " {{{1
14+
let starttime = xolox#misc#timer#start()
15+
let indexfile = expand(g:notes_tagsindex)
16+
let lastmodified = getftime(indexfile)
17+
if lastmodified == -1
18+
call xolox#notes#tags#create_index()
19+
elseif lastmodified > s:last_disk_sync
20+
let s:currently_tagged_notes = {}
21+
for line in readfile(indexfile)
22+
let filenames = split(line, "\t")
23+
if len(filenames) > 1
24+
let tagname = remove(filenames, 0)
25+
let s:currently_tagged_notes[tagname] = filenames
26+
endif
27+
endfor
28+
let s:previously_tagged_notes = deepcopy(s:currently_tagged_notes)
29+
let s:last_disk_sync = lastmodified
30+
call xolox#misc#timer#stop("notes.vim %s: Loaded tags index in %s.", g:xolox#notes#version, starttime)
31+
endif
32+
return s:currently_tagged_notes
33+
endfunction
34+
35+
function! xolox#notes#tags#create_index() " {{{1
36+
let exists = filereadable(expand(g:notes_tagsindex))
37+
let starttime = xolox#misc#timer#start()
38+
let filenames = xolox#notes#get_fnames(0)
39+
let s:currently_tagged_notes = {}
40+
for idx in range(len(filenames))
41+
let title = xolox#notes#fname_to_title(filenames[idx])
42+
call xolox#misc#msg#info("notes.vim %s: Scanning note %i/%i: %s", g:xolox#notes#version, idx + 1, len(filenames), title)
43+
call xolox#notes#tags#scan_note(title, join(readfile(filenames[idx]), "\n"))
44+
endfor
45+
if xolox#notes#tags#save_index()
46+
let s:previously_tagged_notes = deepcopy(s:currently_tagged_notes)
47+
call xolox#misc#timer#stop('notes.vim %s: %s tags index in %s.', g:xolox#notes#version, exists ? "Updated" : "Created", starttime)
48+
else
49+
call xolox#misc#msg#warn("notes.vim %s: Failed to save tags index as %s!", g:xolox#notes#version, g:notes_tagsindex)
50+
endif
51+
endfunction
52+
53+
function! xolox#notes#tags#save_index() " {{{1
54+
if s:currently_tagged_notes == s:previously_tagged_notes
55+
return 1 " Nothing to be done
56+
else
57+
let lines = []
58+
for [tagname, filenames] in items(s:currently_tagged_notes)
59+
call add(lines, join([tagname] + filenames, "\t"))
60+
endfor
61+
let indexfile = expand(g:notes_tagsindex)
62+
let status = writefile(lines, indexfile) == 0
63+
if status
64+
let s:last_disk_sync = getftime(indexfile)
65+
endif
66+
return status
67+
endif
68+
endfunction
69+
70+
function! xolox#notes#tags#scan_note(title, text) " {{{1
71+
" Add a note to the tags index.
72+
call xolox#notes#tags#load_index()
73+
for token in split(substitute(a:text, '{{{\w\+\_.\{-}}}}', '', 'g'))
74+
if token =~ '^@\w'
75+
let token = substitute(token[1:], '[[:punct:]]*$', '', '')
76+
if token != ''
77+
if !has_key(s:currently_tagged_notes, token)
78+
let s:currently_tagged_notes[token] = [a:title]
79+
elseif index(s:currently_tagged_notes[token], a:title) == -1
80+
call xolox#misc#list#binsert(s:currently_tagged_notes[token], a:title, 1)
81+
endif
82+
endif
83+
endif
84+
endfor
85+
endfunction
86+
87+
function! xolox#notes#tags#forget_note(title) " {{{1
88+
" Remove a note from the tags index.
89+
call xolox#notes#tags#load_index()
90+
for tagname in keys(s:currently_tagged_notes)
91+
call filter(s:currently_tagged_notes[tagname], "v:val != a:title")
92+
if empty(s:currently_tagged_notes[tagname])
93+
unlet s:currently_tagged_notes[tagname]
94+
endif
95+
endfor
96+
endfunction
97+
98+
function! xolox#notes#tags#show_tags(minsize) " {{{1
99+
" TODO Mappings to "zoom" in/out (show only big tags).
100+
call xolox#notes#tags#load_index()
101+
let lines = [s:buffer_name, '']
102+
if empty(s:currently_tagged_notes)
103+
call add(lines, "You haven't used any tags yet!")
104+
else
105+
let bullet = xolox#notes#get_bullet('*')
106+
let numtags = 0
107+
for tagname in sort(keys(s:currently_tagged_notes), 1)
108+
let friendly_name = xolox#notes#tags#friendly_name(tagname)
109+
let numnotes = len(s:currently_tagged_notes[tagname])
110+
if numnotes >= a:minsize
111+
call extend(lines, ['', printf('# %s (%i note%s)', friendly_name, numnotes, numnotes == 1 ? '' : 's'), ''])
112+
for title in s:currently_tagged_notes[tagname]
113+
let lastmodified = xolox#notes#friendly_date(getftime(xolox#notes#title_to_fname(title)))
114+
call add(lines, ' ' . bullet . ' ' . title . ' (last edited ' . lastmodified . ')')
115+
endfor
116+
let numtags += 1
117+
endif
118+
endfor
119+
if a:minsize <= 1
120+
let message = printf("You've used %i %s in your notes",
121+
\ numtags, numtags == 1 ? "tag" : "tags")
122+
else
123+
let message = printf("There %s %i %s that %s been used at least %s times",
124+
\ numtags == 1 ? "is" : "are", numtags,
125+
\ numtags == 1 ? "tag" : "tags",
126+
\ numtags == 1 ? "has" : "have", a:minsize)
127+
endif
128+
let message .= ", "
129+
let message .= numtags == 1 ? "it's" : "they're"
130+
let message .= " listed below. Tags and notes are sorted alphabetically and after each note is the date when it was last modified."
131+
if numtags > 1 && !(&foldmethod == 'expr' && &foldenable)
132+
let message .= " You can enable text folding to get an overview of just the tag names and how many times they've been used."
133+
endif
134+
call insert(lines, message, 2)
135+
endif
136+
call xolox#misc#buffer#prepare(s:buffer_name)
137+
call setline(1, lines)
138+
call xolox#misc#buffer#lock()
139+
setlocal filetype=notes nospell wrap
140+
endfunction
141+
142+
function! xolox#notes#tags#friendly_name(tagname) " {{{1
143+
return substitute(a:tagname, '\(\U\)\(\u\)', '\1 \2', 'g')
144+
endfunction

0 commit comments

Comments
 (0)