diff --git a/autoload/maktaba.vim b/autoload/maktaba.vim index db8d19c..eb5392e 100644 --- a/autoload/maktaba.vim +++ b/autoload/maktaba.vim @@ -21,7 +21,7 @@ " :p:h:h is .../maktaba/ let s:plugindir = expand(':p:h:h') if !exists('s:maktaba') - let s:maktaba = maktaba#plugin#GetOrInstall(s:plugindir) + let s:maktaba = maktaba#plugin#Register(s:plugindir) let s:maktaba.globals.installers = [] let s:maktaba.globals.loghandlers = maktaba#reflist#Create() endif diff --git a/autoload/maktaba/plugin.vim b/autoload/maktaba/plugin.vim index de89d70..0ef7197 100644 --- a/autoload/maktaba/plugin.vim +++ b/autoload/maktaba/plugin.vim @@ -4,8 +4,8 @@ if !exists('s:plugins') endif " Mapping from normalized locations to the corresponding plugin object. -" Used to look up plugins by location in maktaba#plugin#Install and -" maktaba#plugin#GetOrInstall. +" Used to look up plugins by location in @function(#Install), +" @function(#Register), and @function(#GetOrInstall). " May have multiple locations mapped to the same plugin in the case of symlinks. if !exists('s:plugins_by_location') let s:plugins_by_location = {} @@ -45,6 +45,16 @@ function! s:CannotEnter(file) abort endfunction +"" +" Gets resolved version of {path}, expanding symlinks. +" Works the same as |resolve()|, but doesn't choke on paths with trailing +" slashes under vim<7.3.194. +function! s:Resolve(path) abort + " Remove trailing slash if there is one, then resolve symlinks. + return resolve(fnamemodify(a:path, ':p:h')) +endfunction + + " This is The Way to store a plugin location, by convention: " Fully expanded path with trailing slash at the end. function! s:Fullpath(location) abort @@ -219,7 +229,7 @@ endfunction " g:loaded_* variable when appropriate. function! maktaba#plugin#Enter(file) abort let [l:plugindir, l:filedir, l:handle] = s:SplitEnteredFile(a:file) - let l:plugin = maktaba#plugin#GetOrInstall(l:plugindir) + let l:plugin = maktaba#plugin#Register(l:plugindir) let l:controller = l:plugin._entered[l:filedir] if l:filedir ==# 'ftplugin' @@ -260,7 +270,7 @@ endfunction " maktaba. May trigger instant/ hooks for newly-registered plugins. function! maktaba#plugin#Detect() abort for [l:name, l:location] in items(maktaba#rtp#LeafDirs()) - call maktaba#plugin#GetOrInstall(l:location) + call maktaba#plugin#Register(l:location) endfor endfunction @@ -367,12 +377,13 @@ endfunction " @throws AlreadyExists if the plugin already exists. " @throws ConfigError if [settings] cannot be applied to this plugin. function! maktaba#plugin#Install(dir, ...) abort - let l:name = s:PluginNameFromDir(a:dir) let l:settings = maktaba#ensure#IsList(get(a:, 1, [])) - if has_key(s:plugins, l:name) - throw s:AlreadyExists('Plugin "%s" already exists.', l:name) + let l:plugin = s:GetOrCreatePluginObject(a:dir) + if has_key(s:plugins, l:plugin.name) + throw s:AlreadyExists('Plugin "%s" already exists.', l:plugin.name) endif - return s:CreatePluginObject(l:name, a:dir, l:settings) + call s:RegisterPlugin(l:plugin, a:dir, l:settings, 1) + return l:plugin endfunction @@ -397,13 +408,33 @@ function! maktaba#plugin#Get(name) abort " Check if any dir on runtimepath is a plugin that hasn't been detected yet. let l:leafdirs = maktaba#rtp#LeafDirs() if has_key(l:leafdirs, a:name) - return maktaba#plugin#GetOrInstall(l:leafdirs[a:name]) + return maktaba#plugin#Register(l:leafdirs[a:name]) endif throw maktaba#error#NotFound('Plugin %s', a:name) endfunction +"" +" Registers the plugin located at {dir} with maktaba, unless it is already +" registered. Should be used for plugins already installed on 'runtimepath'. The +" appropriate maktaba plugin object is returned. +" +" [settings], if given, must be a list of maktaba settings (see +" |maktaba#setting#Create|). If the plugin is new, they will be applied as in +" @function(#Install). Otherwise, they will be applied before returning the +" plugin object. +" +" @throws AlreadyExists if the existing plugin comes from a different directory. +" @throws ConfigError if [settings] cannot be applied to this plugin. +function! maktaba#plugin#Register(dir, ...) abort + let l:settings = maktaba#ensure#IsList(get(a:, 1, [])) + let l:plugin = s:GetOrCreatePluginObject(a:dir) + call s:RegisterPlugin(l:plugin, a:dir, l:settings, 0) + return l:plugin +endfunction + + "" " Installs the plugin located at {dir}, unless it already exists. The " appropriate maktaba plugin object is returned. @@ -417,35 +448,22 @@ endfunction " @throws AlreadyExists if the existing plugin comes from a different directory. " @throws ConfigError if [settings] cannot be applied to this plugin. function! maktaba#plugin#GetOrInstall(dir, ...) abort - let l:name = s:PluginNameFromDir(a:dir) let l:settings = maktaba#ensure#IsList(get(a:, 1, [])) - if has_key(s:plugins, l:name) - let l:plugin = s:plugins[l:name] - " Compare fully resolved paths. Trailing slashes must (see patch 7.3.194) be - " stripped for resolve(), and fnamemodify() with ':p:h' does this safely. - let l:pluginpath = fnamemodify(l:plugin.location, ':p:h') - let l:newpath = s:Fullpath(a:dir) - if resolve(l:pluginpath) !=# resolve(fnamemodify(l:newpath, ':p:h')) - let l:msg = 'Conflict for plugin "%s": %s and %s' - throw s:AlreadyExists(l:msg, l:plugin.name, l:plugin.location, l:newpath) - endif - if !empty(l:settings) - call s:ApplySettings(l:plugin, l:settings) - endif - return l:plugin - endif - return s:CreatePluginObject(l:name, a:dir, l:settings) + let l:plugin = s:GetOrCreatePluginObject(a:dir) + call s:RegisterPlugin(l:plugin, a:dir, l:settings, 1) + return l:plugin endfunction "" -" @dict Plugin -" The maktaba plugin object. Exposes functions that operate on the plugin -" itself. - +" Get a plugin object corresponding to {location}. Returns existing plugin +" object if already registered (but does not register the plugin). +function! s:GetOrCreatePluginObject(location) abort + let l:name = s:PluginNameFromDir(a:location) + if has_key(s:plugins, l:name) + return s:plugins[l:name] + endif -" Common code used by #Install and #GetOrInstall. -function! s:CreatePluginObject(name, location, settings) abort let l:entrycontroller = { \ 'autoload': [], \ 'plugin': [], @@ -453,11 +471,11 @@ function! s:CreatePluginObject(name, location, settings) abort \ 'ftplugin': {} \} let l:plugin = { - \ 'name': a:name, + \ 'name': l:name, \ 'location': s:Fullpath(a:location), \ 'flags': {}, \ 'globals': {}, - \ 'logger': maktaba#log#Logger(a:name), + \ 'logger': maktaba#log#Logger(l:name), \ 'Source': function('maktaba#plugin#Source'), \ 'Load': function('maktaba#plugin#Load'), \ 'AddonInfo': function('maktaba#plugin#AddonInfo'), @@ -482,56 +500,100 @@ function! s:CreatePluginObject(name, location, settings) abort catch /ERROR(BadValue):/ " Couldn't deserialize JSON. endtry - let s:plugins[l:plugin.name] = l:plugin - let s:plugins_by_location[l:plugin.location] = l:plugin + + return l:plugin +endfunction + + +"" +" Register {plugin} at {location} with maktaba, applying {settings}. If +" {force_rtp} is 1, the plugin's location will be added to 'runtimepath' if it's +" not detected there already. +" @throws AlreadyExists if the existing plugin comes from a different directory. +function! s:RegisterPlugin(plugin, location, settings, force_rtp) abort + let l:already_installed = has_key(s:plugins, a:plugin.name) + if l:already_installed + let l:orig_plugin = s:plugins[a:plugin.name] + " Compare fully resolved paths. + let l:pluginpath = s:Resolve(l:orig_plugin.location) + let l:newpath = s:Resolve(s:Fullpath(a:location)) + if l:pluginpath !=# l:newpath + let l:msg = 'Conflict for plugin "%s": %s and %s' + throw s:AlreadyExists(l:msg, a:plugin.name, l:pluginpath, l:newpath) + endif + endif + + let s:plugins[a:plugin.name] = a:plugin + let s:plugins_by_location[a:plugin.location] = a:plugin " If plugin is symlinked, register resolved path as custom location to avoid " conflicts. - let l:resolved_location = s:Fullpath(resolve(l:plugin.location)) - if l:resolved_location !=# l:plugin.location - let s:plugins_by_location[l:resolved_location] = l:plugin + let l:resolved_location = s:Fullpath(s:Resolve(a:plugin.location)) + if l:resolved_location !=# a:plugin.location + let s:plugins_by_location[l:resolved_location] = a:plugin endif - let l:rtp_dirs = maktaba#rtp#Split() - " If the plugin location isn't already on the runtimepath, add it. Check - " for both the raw {location} value and the expanded form. - " Note that this may not detect odd spellings that don't match the raw or - " expanded form, e.g., if it's on rtp with a trailing slash but installed - " using a location without. In such cases, the plugin will end up on the - " runtimepath twice. - if index(l:rtp_dirs, a:location) == -1 && - \ index(l:rtp_dirs, l:plugin.location) == -1 - call maktaba#rtp#Add(l:plugin.location) + if l:already_installed + if !empty(a:settings) + call s:ApplySettings(a:plugin, a:settings) + endif + else + if a:force_rtp + let l:rtp_dirs = maktaba#rtp#Split() + " If the plugin location isn't already on the runtimepath, add it. Check + " for both the {raw_location} value and the expanded form. + " Note that this may not detect odd spellings that don't match the raw or + " expanded form, e.g., if it's on rtp with a trailing slash but installed + " using a location without. In such cases, the plugin will end up on the + " runtimepath twice. + if index(l:rtp_dirs, a:location) == -1 && + \ index(l:rtp_dirs, a:plugin.location) == -1 + call maktaba#rtp#Add(a:plugin.location) + endif + endif + + call s:InitPlugin(a:plugin, a:settings) endif +endfunction + +"" +" Perform one-time initialization of {plugin} (load flags, source instant/ +" files, and define g:installed_PLUGIN variable). Apply {settings} to maktaba +" flags. +function! s:InitPlugin(plugin, settings) abort " These special flags let the user control the loading of parts of the plugin. - if isdirectory(maktaba#path#Join([l:plugin.location, 'plugin'])) - call l:plugin.Flag('plugin', {}) + if isdirectory(maktaba#path#Join([a:plugin.location, 'plugin'])) + call a:plugin.Flag('plugin', {}) endif - if isdirectory(maktaba#path#Join([l:plugin.location, 'instant'])) - call l:plugin.Flag('instant', {}) + if isdirectory(maktaba#path#Join([a:plugin.location, 'instant'])) + call a:plugin.Flag('instant', {}) endif " Load flags file first. - call l:plugin.Source(['instant', 'flags'], 1) + call a:plugin.Source(['instant', 'flags'], 1) " Then apply settings. if !empty(a:settings) - call s:ApplySettings(l:plugin, a:settings) + call s:ApplySettings(a:plugin, a:settings) endif " Then load all instant files in random order. - call call('s:SourceDir', ['instant'], l:plugin) + call call('s:SourceDir', ['instant'], a:plugin) " g:installed_ is set to signal that the plugin has been installed " (though perhaps not loaded). This fills the gap between installation time " (when the plugin is available on the runtimepath) and load time (when the " plugin's files are sourced). This new convention is expected to make it much " easier to build vim dependency managers. - let g:installed_{s:SanitizedName(l:plugin.name)} = 1 - - return l:plugin + let g:installed_{s:SanitizedName(a:plugin.name)} = 1 endfunction +"" +" @dict Plugin +" The maktaba plugin object. Exposes functions that operate on the plugin +" itself. + + " @dict Plugin " Gets a list of all subdirectories in the root plugin directory. " Caches the list for performance, so new paths will not be discovered after the diff --git a/doc/maktaba.txt b/doc/maktaba.txt index a2aff0e..fc0e7e5 100644 --- a/doc/maktaba.txt +++ b/doc/maktaba.txt @@ -1053,16 +1053,12 @@ maktaba#plugin#IsRegistered({plugin}) *maktaba#plugin#IsRegistered* maktaba#plugin#CanonicalName({plugin}) *maktaba#plugin#CanonicalName* The canonical name of {plugin}. This is the name of the plugin directory - with all invalid characters replaced with underscores. Valid characters - include _, [a-z], [A-Z], and [0-9]. For example, the canonical name of - "my-plugin" is "my_plugin". Certain conventions which are common for github - vim projects are also recognized. Specifically, either a "vim-" prefix and a - ".vim" suffix would be disregarded: both "vim-unimpaired" and - "unimpaired.vim" would become simply "unimpaired". + with any "vim-" prefix or ".vim" suffix stripped off: both "vim-unimpaired" + and "unimpaired.vim" would become simply "unimpaired". Note that plugins with different names in the filesystem can conflict in - maktaba. If you've loaded a plugin in the directory "plugins/my-plugin" then - maktaba can't handle a plugin named "plugins/my_plugin". Make sure your + maktaba. If you've loaded a plugin in the directory "plugins/vim-myplugin" + then maktaba can't handle a plugin named "plugins/myplugin". Make sure your plugins have sufficiently different names! maktaba#plugin#Install({dir}, [settings]) *maktaba#plugin#Install* @@ -1121,12 +1117,26 @@ maktaba#plugin#Install({dir}, [settings]) *maktaba#plugin#Install* maktaba#plugin#Get({plugin}) *maktaba#plugin#Get* Gets the plugin object associated with {plugin}. {plugin} may either be the - name of the plugin directory, or the canonicalized plugin name (with invalid - characters converted to underscores). See |maktaba#plugin#CanonicalName|. - Detects plugins added to 'runtimepath' even if they haven't been explicitly - registered with maktaba. + name of the plugin directory, or the canonicalized plugin name (with any + "vim-" prefix or ".vim" suffix stripped off). See + |maktaba#plugin#CanonicalName|. Detects plugins added to 'runtimepath' even + if they haven't been explicitly registered with maktaba. Throws ERROR(NotFound) if the plugin object does not exist. +maktaba#plugin#Register({dir}, [settings]) *maktaba#plugin#Register* + Registers the plugin located at {dir} with maktaba, unless it is already + registered. Should be used for plugins already installed on 'runtimepath'. + The appropriate maktaba plugin object is returned. + + [settings], if given, must be a list of maktaba settings (see + |maktaba#setting#Create|). If the plugin is new, they will be applied as in + |maktaba#plugin#Install|. Otherwise, they will be applied before returning + the plugin object. + + Throws ERROR(AlreadyExists) if the existing plugin comes from a different + directory. + Throws ERROR(ConfigError) if [settings] cannot be applied to this plugin. + maktaba#plugin#GetOrInstall({dir}, [settings]) *maktaba#plugin#GetOrInstall* Installs the plugin located at {dir}, unless it already exists. The appropriate maktaba plugin object is returned. diff --git a/vroom/plugin.vroom b/vroom/plugin.vroom index 042e113..3627b53 100644 --- a/vroom/plugin.vroom +++ b/vroom/plugin.vroom @@ -148,7 +148,7 @@ Now, we install the plugin by giving maktaba the full plugin path: This should generally be done by a plugin manager. A few things have happened. First of all, notice that the flags file was sourced -immediately. (Take a look at fakeplugins/myplugin/plugin/flags.vim to see where +immediately. (Take a look at fakeplugins/myplugin/instant/flags.vim to see where the message came from.) Secondly and most importantly, our plugin is now on the runtimepath and registered with maktaba: @@ -160,6 +160,22 @@ maktaba#plugin#Get to get another: :call maktaba#ensure#IsEqual(maktaba#plugin#Get('myplugin'), g:plugin) +Non-plugin-manager code may also want to ensure that a plugin is registered with +maktaba (and has flags initialized and instant/ code executed) without actually +affecting runtimepath or triggering other side-effects that should be left to +the plugin manager. For these cases, there's maktaba#plugin#Register. + + @messages (STRICT) + :let g:modularpluginpath = maktaba#path#Join( + | [g:thisdir, 'fakeplugins', 'modularplugin']) + :let g:modularplugin = maktaba#plugin#Register(g:modularpluginpath) + ~ The flags file is sourced immediately. + @messages + +Notice that this plugin hasn't been added to runtimepath. + + :call maktaba#ensure#IsFalse(has_key(maktaba#rtp#LeafDirs(), 'modularplugin')) + This all works as you might expect, but none of it is very interesting. Let's see what the plugin object can do. It posesses some boring data, such as name and location: @@ -208,7 +224,8 @@ Call maktaba#plugin#Detect to detect and register plugins. :call maktaba#plugin#Detect() ~ INSTANT LOADED :echomsg string(maktaba#plugin#RegisteredPlugins()) - ~ ['emptyplugin', 'fullplugin', 'loudplugin', 'maktaba', 'myplugin'] + ~ ['emptyplugin', 'fullplugin', 'loudplugin', 'maktaba', 'modularplugin', + | 'myplugin'] Calling maktaba#plugin#RegisteredPlugins will also detect new plugins automatically. @@ -218,7 +235,7 @@ automatically. :echomsg string(maktaba#plugin#RegisteredPlugins()) ~ ['emptyplugin', 'fullplugin', 'library', 'library2', 'loudplugin', - | 'maktaba', 'myplugin'] + | 'maktaba', 'modularplugin', 'myplugin'] That's Maktaba plugin basics for you. There's still a lot more ground to cover. Here's a directory of relevant topics: