From a523c285f06cd6fa07b7ed323b85487a9cd440de Mon Sep 17 00:00:00 2001 From: Peter Odding Date: Sun, 12 May 2013 20:10:01 +0200 Subject: [PATCH] Support for multiple directories with notes (related to issue #18) Issue #18 on GitHub: https://github.com/xolox/vim-notes/issues/18 --- README.md | 20 ++++++--- autoload/xolox/notes.vim | 83 +++++++++++++++++++++++++++++--------- doc/notes.txt | 48 +++++++++++++++++----- misc/notes/search-notes.py | 38 +++++++++-------- plugin/notes.vim | 12 ++++-- 5 files changed, 146 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 964c4b2..54d1382 100644 --- a/README.md +++ b/README.md @@ -31,18 +31,24 @@ Unzip the most recent [ZIP archive] [download] file inside your Vim profile dire All options have reasonable defaults so if the plug-in works after installation you don't need to change any options. They're available for people who like to customize their directory layout. These options can be configured in your [vimrc script] [vimrc] by including a line like this: - :let g:notes_directory = '~/Documents/Notes' + :let g:notes_directories = ['~/Documents/Notes', '~/Dropbox/Shared Notes'] Note that after changing an option in your [vimrc script] [vimrc] you have to restart Vim for the changes to take effect. -### The `g:notes_directory` option +### The `g:notes_directories` option -All your notes are stored together in one directory. This option defines the path of this directory. The default value depends on circumstances but should work for most people: +Your notes are stored in one or more directories. This option defines where you want to store your notes. Its value should be a list (there's an example above) with one or more pathnames. The default is a single value which depends on circumstances but should work for most people: * If the profile directory where the plug-in is installed is writable, the directory `misc/notes/user` under the profile directory is used. This is for compatibility with [Pathogen] [pathogen]; the notes will be stored inside the plug-in's bundle. * If the above doesn't work out, the default depends on the platform: `~/vimfiles/misc/notes/user` on Windows and `~/.vim/misc/notes/user` on other platforms. +#### Backwards compatibility + +In the past the notes plug-in only supported a single directory and the corresponding option was called `g:notes_directory`. When support for multiple notes directories was introduced the option was renamed to `g:notes_directories` to reflect that the value is now a list of directory pathnames. + +For backwards compatibility with old configurations (all of them as of this writing :-) the notes plug-in still uses `g:notes_directory` when it is defined (its no longer defined by the plug-in). However when the plug-in warns you to change your configuration you probably should because this compatibility will be removed at some point. + ### The `g:notes_suffix` option The suffix to add to generated filenames. The plug-in generates filenames for your notes based on the title (first line) of each note and by default these filenames don't include an extension like `.txt`. You can use this option to make the plug-in automatically append an extension without having to embed the extension in the note's title, e.g.: @@ -103,7 +109,7 @@ This option defines the pathname of the text file that stores the list of known ## Commands -To edit one of your existing notes you can use Vim commands such as [:edit] [edit], [:split] [split] and [:tabedit] [tabedit] with a filename that starts with *note:* followed by (part of) the title of one of your notes, e.g.: +To edit one of your existing notes (or create a new one) you can use Vim commands such as [:edit] [edit], [:split] [split] and [:tabedit] [tabedit] with a filename that starts with *note:* followed by (part of) the title of one of your notes, e.g.: :edit note:todo @@ -111,7 +117,7 @@ This shortcut also works from the command line: $ gvim note:todo -When you don't follow *note:* with anything a new note is created. +When you don't follow *note:* with anything a new note is created like when you execute `:Note` without any arguments. ### The `:Note` command @@ -119,12 +125,16 @@ When executed without any arguments this command starts a new note in the curren This command will fail when changes have been made to the current buffer, unless you use `:Note!` which discards any changes. +When you are using multiple directories to store your notes and you run `:Note` while editing an existing note, a new note will inherit the directory of the note from which you started. Otherwise the note is created in the first directory in `g:notes_directories`. + *This command supports tab completion:* If you complete one word, all existing notes containing the given word somewhere in their title are suggested. If you type more than one word separated by spaces, the plug-in will complete only the missing words so that the resulting command line contains the complete note title and nothing more. ### The `:NoteFromSelectedText` command Start a new note in the current window with the selected text as the title of the note. The name of this command isn't very well suited to daily use, that's because it's intended to be executed from a mapping. The default mapping for this command is `\en` (the backslash is actually the character defined by the [mapleader] [mapleader] variable). +When you are using multiple directories to store your notes and you run `:NoteFromSelectedText` while editing an existing note, the new note will inherit the directory of the note from which it was created. + ### The `:SplitNoteFromSelectedText` command Same as `:NoteFromSelectedText` but opens the new note in a vertical split window. The default mapping for this command is `\sn`. diff --git a/autoload/xolox/notes.vim b/autoload/xolox/notes.vim index b4fde9f..b7cd074 100644 --- a/autoload/xolox/notes.vim +++ b/autoload/xolox/notes.vim @@ -1,12 +1,12 @@ " Vim auto-load script " Author: Peter Odding -" Last Change: May 6, 2013 +" Last Change: May 12, 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.18.3' +let g:xolox#notes#version = '0.19' let s:scriptdir = expand(':p:h') call xolox#misc#compat#check('notes', 2) @@ -26,11 +26,17 @@ function! xolox#notes#init() " {{{1 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_directory') - let g:notes_directory = xolox#misc#path#merge(localdir, 'user') + if !exists('g:notes_directories') + let g:notes_directories = [xolox#misc#path#merge(localdir, 'user')] endif - call s:create_notes_directory() + 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') @@ -82,15 +88,16 @@ function! xolox#notes#init() " {{{1 endif endfunction -function! s:create_notes_directory() - let notes_directory = expand(g:notes_directory) - if !isdirectory(notes_directory) - call xolox#misc#msg#info("notes.vim %s: Creating notes directory (first run?) ..", g:xolox#notes#version) - call mkdir(notes_directory, 'p') - endif - if filewritable(notes_directory) != 2 - call xolox#misc#msg#warn("notes.vim %s: The notes directory (%s) is not writable!", g:xolox#notes#version, notes_directory) - endif +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 @@ -595,6 +602,17 @@ 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' @@ -646,7 +664,14 @@ endfunction function! xolox#notes#buffer_is_note() " {{{2 " Check whether the current buffer is a note (with the correct file type and path). - return xolox#notes#filetype_is_note(&ft) && xolox#misc#path#equals(expand('%:p:h'), g:notes_directory) + 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 @@ -771,10 +796,13 @@ function! s:python_command(...) " {{{2 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, '--notes', g:notes_directory] + 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) @@ -812,9 +840,11 @@ 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() - let pattern = xolox#misc#path#merge(g:notes_directory, '*') - let listing = glob(xolox#misc#path#absolute(pattern)) - call extend(s:cached_fnames, filter(split(listing, '\n'), 'filereadable(v:val)')) + 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 @@ -891,12 +921,25 @@ 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 pathname = xolox#misc#path#merge(g:notes_directory, filename . g:notes_suffix) + 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) diff --git a/doc/notes.txt b/doc/notes.txt index 60a92a4..151785e 100644 --- a/doc/notes.txt +++ b/doc/notes.txt @@ -7,7 +7,8 @@ Contents ~ 1. Introduction |notes-introduction| 2. Install & usage |notes-install-usage| 3. Options |notes-options| - 1. The |g:notes_directory| option + 1. The |g:notes_directories| option + 1. Backwards compatibility |notes-backwards-compatibility| 2. The |g:notes_suffix| option 3. The |g:notes_title_sync| option 4. The |g:notes_smart_quotes| option @@ -134,17 +135,18 @@ installation you don't need to change any options. They're available for people who like to customize their directory layout. These options can be configured in your |vimrc| script by including a line like this: > - :let g:notes_directory = '~/Documents/Notes' + :let g:notes_directories = ['~/Documents/Notes', '~/Dropbox/Shared Notes'] Note that after changing an option in your |vimrc| script you have to restart Vim for the changes to take effect. ------------------------------------------------------------------------------- -The *g:notes_directory* option +The *g:notes_directories* option -All your notes are stored together in one directory. This option defines the -path of this directory. The default value depends on circumstances but should -work for most people: +Your notes are stored in one or more directories. This option defines where +you want to store your notes. Its value should be a list (there's an example +above) with one or more pathnames. The default is a single value which depends +on circumstances but should work for most people: - If the profile directory where the plug-in is installed is writable, the directory 'misc/notes/user' under the profile directory is used. This is @@ -155,6 +157,22 @@ work for most people: '~/vimfiles/misc/notes/user' on Windows and '~/.vim/misc/notes/user' on other platforms. +------------------------------------------------------------------------------- + *notes-backwards-compatibility* +Backwards compatibility ~ + +In the past the notes plug-in only supported a single directory and the +corresponding option was called 'g:notes_directory'. When support for multiple +notes directories was introduced the option was renamed to +|g:notes_directories| to reflect that the value is now a list of directory +pathnames. + +For backwards compatibility with old configurations (all of them as of this +writing :-) the notes plug-in still uses 'g:notes_directory' when it is +defined (its no longer defined by the plug-in). However when the plug-in warns +you to change your configuration you probably should because this +compatibility will be removed at some point. + ------------------------------------------------------------------------------- The *g:notes_suffix* option @@ -266,9 +284,9 @@ can recreate it manually by executing |:IndexTaggedNotes| (see below). *notes-commands* Commands ~ -To edit one of your existing notes you can use Vim commands such as |:edit|, -|:split| and |:tabedit| with a filename that starts with note: followed by (part -of) the title of one of your notes, e.g.: +To edit one of your existing notes (or create a new one) you can use Vim +commands such as |:edit|, |:split| and |:tabedit| with a filename that starts with +note: followed by (part of) the title of one of your notes, e.g.: > :edit note:todo @@ -276,7 +294,8 @@ This shortcut also works from the command line: > $ gvim note:todo -When you don't follow note: with anything a new note is created. +When you don't follow note: with anything a new note is created like when you +execute |:Note| without any arguments. ------------------------------------------------------------------------------- The *:Note* command @@ -290,6 +309,11 @@ new note is started with the given word(s) as title. This command will fail when changes have been made to the current buffer, unless you use ':Note!' which discards any changes. +When you are using multiple directories to store your notes and you run +|:Note| while editing an existing note, a new note will inherit the directory +of the note from which you started. Otherwise the note is created in the first +directory in |g:notes_directories|. + This command supports tab completion: If you complete one word, all existing notes containing the given word somewhere in their title are suggested. If you type more than one word separated by spaces, the plug-in will complete only @@ -305,6 +329,10 @@ because it's intended to be executed from a mapping. The default mapping for this command is '\en' (the backslash is actually the character defined by the |mapleader| variable). +When you are using multiple directories to store your notes and you run +|:NoteFromSelectedText| while editing an existing note, the new note will +inherit the directory of the note from which it was created. + ------------------------------------------------------------------------------- The *:SplitNoteFromSelectedText* command diff --git a/misc/notes/search-notes.py b/misc/notes/search-notes.py index 140c788..d477de7 100755 --- a/misc/notes/search-notes.py +++ b/misc/notes/search-notes.py @@ -3,7 +3,7 @@ # Python script for fast text file searching using keyword index on disk. # # Author: Peter Odding -# Last Change: April 21, 2013 +# Last Change: May 12, 2013 # URL: http://peterodding.com/code/vim/notes/ # License: MIT # @@ -24,7 +24,7 @@ """ Usage: search-notes.py [OPTIONS] KEYWORD... -Search a directory of plain text files using a full text index, +Search one or more directories of plain text files using a full text index, updated automatically during each invocation of the program. Valid options include: @@ -32,7 +32,7 @@ -i, --ignore-case ignore case of keyword(s) -l, --list=SUBSTR list keywords matching substring -d, --database=FILE set path to keywords index file - -n, --notes=DIR set directory with user notes + -n, --notes=DIR set directory with user notes (can be repeated) -e, --encoding=NAME set character encoding of notes -v, --verbose make more noise -h, --help show this message and exit @@ -100,7 +100,7 @@ def parse_args(self): sys.exit(2) # Define the command line option defaults. self.database_file = '~/.vim/misc/notes/index.pickle' - self.user_directory = '~/.vim/misc/notes/user/' + self.user_directories = ['~/.vim/misc/notes/user/'] self.character_encoding = 'UTF-8' self.case_sensitive = True self.keyword_filter = None @@ -114,7 +114,7 @@ def parse_args(self): elif opt in ('-d', '--database'): self.database_file = arg elif opt in ('-n', '--notes'): - self.user_directory = arg + self.user_directories.append(arg) elif opt in ('-e', '--encoding'): self.character_encoding = arg elif opt in ('-v', '--verbose'): @@ -125,15 +125,16 @@ def parse_args(self): else: assert False, "Unhandled option" self.logger.debug("Index file: %s", self.database_file) - self.logger.debug("Notes directory: %s", self.user_directory) + self.logger.debug("Notes directories: %r", self.user_directories) self.logger.debug("Character encoding: %s", self.character_encoding) if self.keyword_filter is not None: self.keyword_filter = self.decode(self.keyword_filter) # Canonicalize pathnames, check validity. self.database_file = self.munge_path(self.database_file) - self.user_directory = self.munge_path(self.user_directory) - if not os.path.isdir(self.user_directory): - sys.stderr.write("Notes directory %s doesn't exist!\n" % self.user_directory) + self.user_directories = map(self.munge_path, self.user_directories) + self.user_directories = filter(os.path.isdir, self.user_directories) + if not any(os.path.isdir(p) for p in self.user_directories): + sys.stderr.write("None of the notes directories exist!\n") sys.exit(1) # Return tokenized keyword arguments. return [self.normalize(k) for k in self.tokenize(' '.join(keywords))] @@ -168,14 +169,17 @@ def update_index(self): update_timer = Timer() # First we find the filenames and last modified times of the notes on disk. notes_on_disk = {} - for filename in os.listdir(self.user_directory): - # Vim swap files are ignored. - if (filename != '.swp' and not fnmatch.fnmatch(filename, '.s??') - and not fnmatch.fnmatch(filename, '.*.s??')): - abspath = os.path.join(self.user_directory, filename) - if os.path.isfile(abspath): - notes_on_disk[abspath] = os.path.getmtime(abspath) - self.logger.info("Found %i notes in %s ..", len(notes_on_disk), self.user_directory) + last_count = 0 + for directory in self.user_directories: + for filename in os.listdir(directory): + # Vim swap files are ignored. + if (filename != '.swp' and not fnmatch.fnmatch(filename, '.s??') + and not fnmatch.fnmatch(filename, '.*.s??')): + abspath = os.path.join(directory, filename) + if os.path.isfile(abspath): + notes_on_disk[abspath] = os.path.getmtime(abspath) + self.logger.info("Found %i notes in %s ..", len(notes_on_disk) - last_count, directory) + last_count = len(notes_on_disk) # Check for updated and/or deleted notes since the last run? if not self.first_use: for filename in self.index['files'].keys(): diff --git a/plugin/notes.vim b/plugin/notes.vim index 292bf4b..7c2282f 100644 --- a/plugin/notes.vim +++ b/plugin/notes.vim @@ -1,6 +1,6 @@ " Vim plug-in " Author: Peter Odding -" Last Change: December 13, 2011 +" Last Change: May 12, 2013 " URL: http://peterodding.com/code/vim/notes/ " Support for automatic update using the GLVS plug-in. @@ -42,12 +42,18 @@ augroup PluginNotes au BufReadCmd note:* nested call xolox#notes#shortcut() " Automatic commands to read/write notes (used for automatic renaming). exe 'au BufReadCmd' xolox#notes#autocmd_pattern(g:notes_shadowdir, 0) 'call xolox#notes#edit_shadow()' - exe 'au BufWriteCmd' xolox#notes#autocmd_pattern(g:notes_directory, 1) 'call xolox#notes#save()' + for s:directory in xolox#notes#find_directories(0) + exe 'au BufWriteCmd' xolox#notes#autocmd_pattern(s:directory, 1) 'call xolox#notes#save()' + endfor + unlet s:directory augroup END augroup filetypedetect let s:template = 'au BufNewFile,BufRead %s if &bt == "" | setl ft=notes | end' - execute printf(s:template, xolox#notes#autocmd_pattern(g:notes_directory, 1)) + for s:directory in xolox#notes#find_directories(0) + execute printf(s:template, xolox#notes#autocmd_pattern(s:directory, 1)) + endfor + unlet s:directory execute printf(s:template, xolox#notes#autocmd_pattern(g:notes_shadowdir, 0)) augroup END