很早以前,我注册过一个叫做“黑客派”的论坛,后来这个论坛改名为“链滴”。
当时论坛提供了使用 Github 账号授权登录,由于早期并未注意过 GitHub 登录授权的权限问题。 被站长直接控制 Github 账号去执行 star 和 follow 操作,相关帖子可以看:https://v2ex.com/t/534800
谷歌搜索已无主站入口:
直接搜索最新网址,发现一篇道歉信排名靠前,可想此事当时影响挺大的。
但是非注册用户点击却无法阅读,啥意思?只给注册用户道歉?
使用 web achive 服务拉取到道歉信内容:
最近我尝试登录这个账号并提交账号注销申请,按照他的网站说明:来去自由·关于支持永久停用账号。
但是似乎并不是那么回事。
申请注销时提示更新失败 - Can not deactivate moderator account。于是只能手动将自己以往再次网站发的帖子全部改成:
在体用账号帖子留言后,账号被封禁,而不是被停用。
最后,我们再看下链滴作者 D 的博客 https://88250.b3log.org 评论系统,使用 Github 登录时所需要的权限:
在初次注册登录时,都会出现以下界面,可以点击右侧红框下拉按钮,展开所有的权限。实际上,如果仅用于注册网站,需要一个唯一的注册ID的话,获取邮箱地址的权限就足够了。 图片中显示了 julia 语言论坛授权登录权限信息:
在 Github 账户设置界面依次点击:Integrations
-> Applications
-> Authorized OAuth Apps
:
如果需要看各自的授权权限点开即可查看:
如果认为该权限太高,可以点击右侧的 Revoke access
。
最后,我们来看看号称国内版 Github 的码云使用 Github 授权时需要的权限:
请问要读写用户的 Followers 做什么?
今天使用 Google 搜索自己之前发过的一篇文章“停用 v2ex 账号”,
发现搜索结果如下。奇怪的是,
有一个域名为 jbwqfy.com
的网站居然是全站复制了我的个人博客。
打开其首页,我今日刚发的文章他都有,基本上是实时同步了,目前无法查明原因。
whois 查询结果显示如下,并没有什么有效信息。
大概是 2023 年 9 月份,将网站从 Github Page 服务切换到了 Cloudflare Page。 一开始切换的原因是因为 Github 强制用户使用两步验证才能登录,失去了对 Github 账号的访问权限, 因此将网站移到了 Cloudflare Page 服务上。
登录 Cloudflare 后,在左侧点击 Workers & Pages
-> Create
-> Connect to Git
, 选择对应的仓库,
值得开心的是这里可以选择私有仓库。Github Page 服务免费用户无法使用私有仓库托管 Jekyll 静态网站。
构建设定:
bundle exec jekyll build
/_site
/docs
Enabled
原先使用的 github-pages
对应的 Jekyll 版本太老,语法高亮的插件 rouge
也非常老旧。
在这里既然已经手动执行命令构建,那么可以使用一些比较新的插件,而不受 Github Page 的限制。
修改 docs/Gemfile
为:
source "https://rubygems.org"
gem "jekyll"
gem 'jekyll-redirect-from'
gem 'rouge', '~> 4.2'
gem 'jekyll-paginate'
原先购买的域名已经转移到了 Cloudflare 上面了,这样一来域名的解析及网站的托管都在同一个平台,也方便管理。
前面写过一篇Lua 与 VimL 速度比较的文章, 里面对 Lua 与 VimL 脚本计算速度做了比较。随着 Vim 的更新并且推出了新的脚本语言 Vim9Script, 也想看一下这个新的脚本语言写法及执行效率到底如何。
任然参考上一篇文章中的比较方式,Vim9Script 实现的函数如下:
vim9script
def V9Fibo(N: string)
var start = reltime()
var t = str2nr(N)
var b = 0
while t > 0
t = t - 1
var a = 1
b = 1
var c = 73
while c > 0
c = c - 1
var tmp = a + b
a = b
b = tmp
endwhile
endwhile
var sec = reltimefloat(reltime(start))
echo b
echom printf('Vim9Fibo(%s): %.6g sec', N, sec)
enddef
defcompile
command! -nargs=+ TestV9Func V9Fibo(<f-args>)
新建一个 vim 文件,写入上述内容。使用 :so %
载入脚本,分别执行如下测试命令:
:TestV9Func 1000
:TestV9Func 10000000
本地测试结果:
Vim9Fibo(1000): 0.007455 sec
Vim9Fibo(10000000): 69.937198 sec
在前一篇文章中比较的是 Vim Script 和 Lua 的速度,其结果是:
Fibo(1000): 0.410364 sec
Fibo(10000000): 1470.280914 sec
LuaFibo(1000): 9.052000e-4 sec
LuaFibo(10000000): 1.235385 sec
测试结果不难看出,Vim9Script 相较于老版本的 Vim script 确实有很大的速度提升。
计算参数1000时,从 0.41s
提升到了 0.0074s
;
计算参数一千万时,从 1470s
提升到了 69s
。
但是相较于 Lua 的 9.0e-4s
和 1.23s
的测试结果,还是相差太多。
Neovim 或者 Vim 的插件管理器有很多,最开始的时候我使用的是 Vundle.vim, 后来尝试过 vim-plug 和 neobundle.vim, 目前使用的是 dein.vim。当然了,网上搜一下,其实还有很多其他的插件管理器, 然而并没有发现什么特别吸引我的功能,所以也就没有什么动力切换了。
相较于切换插件管理器,其实我更想好好研究一下插件管理器的实现逻辑,于是看了一些文档及插件管理器的源码,整理这篇文章。
我所理解的 Vim 或者 Neovim 的插件本质是一系列的 Vim 文件按照一定的目录结构组织起来的集合。
Neovim 中增加了 plugin/
、color/
等目录下 lua 文件的识别。
不管是 Vim 还是 Neovim,插件管理器添加一个插件的本质就是将插件所在的目录加入到 runtimepath
选项,
所谓的懒加载(lazy load)实际上是在适当的时机下触发前面这一步骤。
所以,很早以前,还没有插件管理器时,很多时候只是将插件下载到某一个目录,并在 vimrc
中将目录添加到 runtimepath
中。
set runtimepath+=/path/to/plugin_directory
阅读 :h runtimepath
可以看到标准的插件的目录结构包括:
filetype.lua filetypes
autoload/ automatically loaded scripts
colors/ color scheme files
compiler/ compiler files
doc/ documentation
ftplugin/ filetype plugins
indent/ indent scripts
keymap/ key mapping files
lang/ menu translations
lua/ lua modules
menu.vim GUI menus
pack/ packages
parser/ treesitter syntax parsers
plugin/ plugin scripts
queries/ treesitter queries
rplugin/ remote-plugin scripts
spell/ spell checking files
syntax/ syntax files
tutor/ tutorial files
在说明每一个目录的功能之前,我认为有必要了解一下 Vim 启动时的文件载入顺序。
假定,我们有一个 hello-vim 插件,其目录为 ~/wsdjeg/bundle/hello-vim
,示例 vimrc 文件:
set nocompatible
set rtp+=~/wsdjeg/bundle/hello-vim
filetype plugin indent on
syntax on
当启动 Vim 时,它将逐行读取并执行 vimrc
文件。其中 set rtp
这一行就是将插件 hello-vim 的目录加入到 runtimepath 内。
下一行 filetype plugin indent on
,这个命令包含了三层意思:
*.md
文件时,自动识别并设置 filetype
为 markdown
。filetype
时,根据 &filetype
的值,自动读取并执行 runtimepath 中每一个目录下 ftplugin/
子目录里对应的 vim 文件filetype
时,根据 &filetype
的值,自动读取并执行 runtimepath 中每一个目录下 indent/
子目录里对应的 vim 文件关于 :filetype
命令的使用,:h :filetype-overview
显示如下帮助文档:
Overview: *:filetype-overview*
command detection plugin indent ~
:filetype on on unchanged unchanged
:filetype off off unchanged unchanged
:filetype plugin on on on unchanged
:filetype plugin off unchanged off unchanged
:filetype indent on on unchanged on
:filetype indent off unchanged unchanged off
:filetype plugin indent on on on on
:filetype plugin indent off unchanged off off
To see the current status, type: >
:filetype
The output looks something like this: >
filetype detection:ON plugin:ON indent:OFF
那么,使用以上的简单的 vimrc 启动 Vim 时,文件的载入顺序为:
如果 hello-vim/
目录下有 filetype.vim
文件,或者存在 ftdetect/
子目录,其内 *.vim
文件也会被执行。
autoload/
目录关于 autoload/
内的 Vim 文件载入机制,我们来做这样一个测试:
文件名: ~/.SpaceVim.d/autoload/test_autoload.vim
,假定 ~/.SpaceVim.d/
已经加入 runtimepath。
echom "hi one"
function! test_autoload#hi()
echom "hi two"
endfunction
echom "hi three"
启动 Vim 后,发现上述三句 echom
语句都没有执行,这也说明了 autoload
内的文件默认是不会自动执行的。
此时如果执行函数 call test_autoload#hi()
。就会发现如下输出:
hi one
hi three
hi two
注意下输出的顺序,hi three
在 hi two
前面。
这是因为当调用 test_autoload#hi()
函数时,Vim 是根据函数的名称,确认下函数是否已定义,如果未定义,
则去找到对应的 Vim 文件读取并逐行执行,执行完了后才会调用函数。
前面执行过了 call test_autoload#hi()
函数后,如果再次调用。此时输出只会有一行:
hi two
那么问题来了,如果我再次调用一个不存在的函数 call test_autoload#no()
函数,会发生什么呢?
hi one
hi three
E117: Unknown function: test_autoload#no
也很好理解,这个 test_autoload#no()
函数未定义,
Vim 根据函数名称会再次去读取并逐行执行 Vim 文件,也就导致了 Vim 文件中的脚本被重复执行了。不受控的重复执行肯定不行的,那么如何规避呢?
可以像下面这样修改:
if exists('s:loaded')
finish
endif
" 有很多插件喜欢用 g:plugin_xxx_loaded, 个人感觉,非必要不使用暴露给脚本外部的变量跟函数。
let s:loaded = 1
echom "hi one"
function! test_autoload#hi()
echom "hi two"
endfunction
echom "hi three"
plugin/
目录在 Vim 启动时默认的配置文件 vimrc
读取完成后, 此时会对 runtimepath
下每一个目录下的 plugin/
子目录进行遍历。其内所有的 Vim 文件会被逐一读取并执行。
Neovim 增加了 plugin/
目录下 lua 文件的支持。
ftplugin/
目录这个目录就是 Vim 自带的 on_ft
加载机制,当 filetype
被设定时会自动载入 ftplugin/<filetype>.vim
,Neovim 还支持 Lua 格式文件 ftplugin/<filetype>.lua
。
很多插件管理器都实现了一个叫做 Lazy load(懒加载)
的功能,那么这个功能到底是如何实现,其本质逻辑是什么呢?
on_cmd
当执行某个命令时加载,这种实现逻辑实际上新建一个新的命令,在命令中载入插件,并根据其参数再次调用命令。
比如 call dein#add('wsdjeg/FlyGrep.vim', {'on_cmd' : 'FlyGrep'})
此时会定义如下命令:
dein.vim/autoload/dein/parse.vim#L342
function! s:generate_dummy_commands(plugin) abort
let a:plugin.dummy_commands = []
for name in a:plugin.on_cmd
" Define dummy commands.
let raw_cmd = 'command '
\ . '-complete=customlist,dein#autoload#_dummy_complete'
\ . ' -bang -bar -range -nargs=* '. name
\ . printf(" call dein#autoload#_on_cmd(%s, %s, <q-args>,
\ expand('<bang>'), expand('<line1>'), expand('<line2>'))",
\ string(name), string(a:plugin.name))
call add(a:plugin.dummy_commands, [name, raw_cmd])
silent! execute raw_cmd
endfor
endfunction
在上述例子中,FlyGrep.vim 插件载入之前,执行的命令 :FlyGrep
实际上时调用 dein#autoload#_on_cmd
函数
on_func
当调用插件的函数时动态载入插件,其实我感觉这个功能有点多余。因为 autoload/
目录下的 Vim 脚本文件在 Vim 启动时是不会自动执行的。
只有在调用对应的函数时才会载入并执行。
可能是为了更加极致的 Lazy load
, 让非载入的插件完全隐藏吧。那么 on_func
这个功能到底是如何实现的呢?
(Neo)Vim 有一个事件叫做 FuncUndefined
,是在调用不存在的函数时触发. 下面是一段简单的 on_func
实现。
let s:lazy_plugins = []
function! Plug(path, opt)
if !empty(a:opt)
" 直接 添加 rtp
return
endif
call add(s:lazy_plugins, [a:path, a:opt])
endfunction
function! s:on_func(f)
for [path, opt] in s:lazy_plugins
if opt.on_func =~# a:f
" 载入路径 path 的插件
endif
endfor
endfunction
augroup test_on_func
autocmd!
autocmd FuncUndefined * call s:on_func(expand('<afile>'))
augroup END
call Plug('~/.SpaceVim.d/hello-vim', {
\ 'on_func' : 'hello#'
\ })
on_event
(Neo)Vim 有很多事件(event),这些事件在特定的时机会被触发。根据事件加载指的是设置某些插件在特定的事件触发时再载入系统。
相较于前面的 on_cmd
和 on_func
的实现,on_event
的实现相对简单一些,
仅仅是监控事件的发生与否,对于发生时的 <amatch>
及 <afile>
不做判断。
以下为简单的实现逻辑:
let s:lazy_plugins = []
augroup test_on_event
autocmd!
augroup END
function! s:load(path)
" 载入路径为 path 的插件
endfuntion
function! Plug(path, opt)
if !empty(a:opt)
call s:load(a:path)
return
endif
if has_key(a:opt, 'on_event')
for event in a:opt.on_event
exe printf('autocmd test_on_event %s call s:load("%s")', event, a:path)
endfor
endif
endfunction
call Plug('~/.SpaceVim.d/hello-vim', {
\ 'on_event' : ['InsertEnter', 'WinEnter']
\ })
on_ft
根据文件类型加载插件,实际上可以理解为一种特殊的 on_event
懒加载模式。前面也说到 on_event
懒加载不判断触发事件时的 <amatch>
和 <afile>
值。
而 on_ft
模式的懒加载底层逻辑是只监听 FileType
这一种事件,并且在事件触发时判断 <amatch>
值。
on_ft
懒加载的简单实现代码如下:
let s:lazy_plugins = []
augroup test_on_ft
autocmd!
augroup END
function! s:load(path)
" 载入路径为 path 的插件
endfuntion
function! Plug(path, opt)
if !empty(a:opt)
call s:load(a:path)
return
endif
if has_key(a:opt, 'on_ft)
for ft in a:opt.on_ft
exe printf('autocmd test_on_ft FileType %s call s:load("%s")', ft, a:path)
endfor
endif
endfunction
call Plug('~/.SpaceVim.d/hello-vim', {
\ 'on_ft' : ['java', 'python']
\ })
on_map
以下这段摘自 dein.vim 的源码:
function! s:generate_dummy_mappings(plugin) abort
let a:plugin.dummy_mappings = []
let items = type(a:plugin.on_map) == v:t_dict ?
\ map(items(a:plugin.on_map),
\ { _, val -> [split(val[0], '\zs'),
\ dein#util#_convert2list(val[1])]}) :
\ map(copy(a:plugin.on_map),
\ { _, val -> type(val) == v:t_list ?
\ [split(val[0], '\zs'), val[1:]] : [['n', 'x'], [val]] })
for [modes, mappings] in items
if mappings ==# ['<Plug>']
" Use plugin name.
let mappings = ['<Plug>(' . a:plugin.normalized_name]
if stridx(a:plugin.normalized_name, '-') >= 0
" The plugin mappings may use "_" instead of "-".
call add(mappings, '<Plug>(' .
\ substitute(a:plugin.normalized_name, '-', '_', 'g'))
endif
endif
for mapping in mappings
" Define dummy mappings.
let prefix = printf('dein#autoload#_on_map(%s, %s,',
\ string(substitute(mapping, '<', '<lt>', 'g')),
\ string(a:plugin.name))
for mode in modes
let raw_map = mode.'noremap <unique><silent> '.mapping
\ . (mode ==# 'c' ? " \<C-r>=" :
\ mode ==# 'i' ? " \<C-o>:call " : " :\<C-u>call ")
\ . prefix . string(mode) . ')<CR>'
call add(a:plugin.dummy_mappings, [mode, mapping, raw_map])
silent! execute raw_map
endfor
endfor
endfor
endfunction
不难看出,on_map
的实现机制跟 on_cmd
有点类似,实际上时定义了新的快捷键映射,触发时去调用 dein#autoload#on_map
函数。
再在这个函数内载入对应的插件,载入完成后使用 feedkey()
函数去模拟按下触发的快捷键映射。
本文主要介绍了 (neo)vim 插件的基本目录结构以及各个目录下文件的载入机制。 充分利用好这些机制后,即便是没有使用懒加载机制的情况下也可以大大的提高插件的初始化体验。 除此之外,本文还简单介绍了目前常见的几种赖加载机制的实现底层原理。
最后,说明一下,文中示例代码仅限于体现功能实现的基本逻辑,并未做额外的条件判断及错误捕获,并不适用于直接使用。
如果日常使用 Neovim 较多的话,选择一个喜欢的颜色主题还是非常不错的。Github 上面有很多 Neovim 主题插件。 但是在使用过程中会遇到这样情况,比如:“我觉得这个主题某个字段高亮颜色看着不喜欢,想做一个微调”。 一般遇到这种情况大多数人会 Fork 仓库进行修改,或者在配置文件设定主题后方进行覆盖。
以上的这个功能需求,其实很久以前就有了,早期我的解决方法非常简单, 有两种:
早期的配置文件比较简单,我都是在主题设定后方直接修改的,比如:
colorschem one
hi VertSplit guibg=#282828 guifg=#181A1F
这种修改方式也存在弊端,就是在启动 Neovim 后,如果执行 :colorscheme
命令切换主题,那配置文件中的微调将不再生效。
ColorScheme
事件调用覆写函数。autocmd ColorScheme * call s:fix_colorscheme()
function! s:fix_colorscheme()
" 使用 g:colors_name 判断主题名称
" 再此使用 highlight 命令修改高亮组
endfunction
这就解决了前面提到的切换主题后不再生效的问题。但是调整的过着无法在 Vim 里面完成,可能需要借助外部的调色板,调整好后把 hex 值写进配置文件。
前面介绍过我的 cpicker.nvim 插件,随着这个插件做出来后,借助它的调色板,又实现了一个实时调整高亮组的功能。并且,调整的结果会存住下来。 重启 Neovim 后,对应的颜色主题的修改还任然存在。
操作步骤如下:
:CpickerCursorChangeHighlight
命令调出调色板浮窗,此时调色板默认颜色就是刚刚光标所在位置前景色。q
按键关闭调色板浮窗。到此时已经调整完成,如果重启 Neovim 后,对当前这个主题调整任然会生效。
我录制了一个修改颜色及重启 Neovim 并设定颜色主题的动画:
对于调整过的颜色主题,如果需要清除掉调整,只需要执行:CpickerClearColorPatch
命令。