Skip to content

Add maktaba#plugin#Register that never touches runtimepath #60

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion autoload/maktaba.vim
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
" <sfile>:p:h:h is .../maktaba/
let s:plugindir = expand('<sfile>: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
Expand Down
180 changes: 121 additions & 59 deletions autoload/maktaba/plugin.vim
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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


Expand All @@ -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.
Expand All @@ -417,47 +448,34 @@ 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': [],
\ 'instant': [],
\ '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'),
Expand All @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When are the settings applied if we aren't l:already_installed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In s:InitPlugin (or, as you would have it, s:LoadInstantFilesAndApplySettings). They're applied in a precise sequence, after the flags are defined but before the instant/ files are sourced.

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_<plugin> 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
Expand Down
34 changes: 22 additions & 12 deletions doc/maktaba.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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*
Expand Down Expand Up @@ -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.
Expand Down
23 changes: 20 additions & 3 deletions vroom/plugin.vroom
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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:
Expand Down Expand Up @@ -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.
Expand All @@ -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:
Expand Down