跳至主要內容

输入系统

WindilyCloud大约 22 分钟

输入系统

  • Rime 输入法
  • latex-suite 自动补全
  • Vim 编辑跳转及重构

基本原则:上限足够高,我可以不用,但你必须得有

Rime 输入法

中文输入法是一个痛点,目前尚无完美的方案解决。我曾尝试过微软输入法,手心输入法,搜狗输入法,百度输入法,讯飞输入法,rime 输入法等,都不算完美,基于以下几个方面:

  1. 隐私问题:输入法能够窥探到密码等个人隐私,尤其是类似搜狗这样的,每天输入的字符都会被严格记录
  2. 跨平台问题:mac,win,手机使得输入体验会有断层
  3. 定制:如今学习工作离不开打字,定制输入法让自己更舒服是一件终身大事

rime 输入法我是下了又删,下了又删,即使我有一些技术背景,但依然搞得有点头大。原因是他没有一套开箱即用的配置,丢给你一个毛坯房,挺痛苦的。但自从解除了四叶草输入方案,渐渐体会到 rime 输入引擎的强大之处。我并不打算高度定制 rime,但我选择的软件上限一定要高,因为我不能保证以后不会去折腾,不能保证未来没有大佬提供强大且友好的支持。这里给出 windows 小狼毫 Rime 配置的基础概览,最好直接照抄四叶草方案,搞清楚每个文件的定义即可。

配置概览

rime 输入法的配置文件分为两处 (就是安装时你指定的两处位置):程序文件夹、用户文件夹右击托盘图标 可以选择 打开『程序文件夹』和『用户文件夹』

  • 程序文件夹:Rime 的安装目录
  • 用户文件夹:自定义的一些配置,如果需要改用户文件夹目录,需要在 Rime 的安装目录中运行 WeaselSetup.exe,重新选择。

注意:在程序文件夹中的所有文件都不要动,虽然改动这里的文件也能达到自定义的效果,但每次更新都会被重置为默认设置。

我们采用“打补丁”的方式对程序文件夹中的文件进行修改,即在用户文件夹中创建 同名的 custom 文件。比如要对 default.yaml 文件修改,就创建 default.custom.yaml。

一般来说,我们需要创建的文件就三个

  1. default.custom.yaml 全局输入方案补丁,即定制内容对所有的输入方案都有效
  2. weasel.custom.yaml 全局皮肤样式补丁,分为样式和颜色两部分,对所有输入方案有效
  3. luna_pinyin.custom.yaml 朙月拼音 输入方案补丁,定制内容只对该方案有效,如果要修改其他方案,创建“方案名.custom.yaml”文件即可
  4. 方案名.schema.yaml 输入方案,直接覆盖程序文件夹下的该方案
  5. symbols.yaml 符号方案,对引入该文件的输入方案有效,用来定义按键输出的字符。因为是引用生效,所以自己的符号方案可以随意命名
  6. installation.yaml 定义用户资料同步的位置
  7. 词典名.dict.yaml 固定词典文件,对引入该文件的输入方案有效,用来定义能打出的词汇
  8. Custom_phrase.txt 用户词典文件,对所有输入方案有效,用来添加用户想添加的词汇
  9. rime.lua 函数文件,对引入该函数的输入方案有效
//default.custom.yaml
patch:
 # 可选的输入方案列表
 schema_list:
   - {schema: luna_pinyin}
 # 候选栏显示的词语个数 
 "menu/page_size": 6
 # 切换/热键 ctrl+`(tab上面那个键)
 # 这里的切换指 输入方案及其设置的选择
 "switcher/hotkeys":
     -"Control+grave"
 # 处理英文模式及中英文切换
 # 大小写锁定键亮时 输出大写-true 输出小写-false
 ascii_composer/good_old_caps_lock: true
 ascii_composer/switch_key:
     # clear 清除已输入的字符
     # noop 什么也不做
     # commit_code 已输入的字符上屏
     # commit_text 已输入的字符对应的词语上屏
     # inline_ascii 输入法转变并可以继续输入

     # 大小写锁定键
     # 这个键位最好不要设置为noop(可以尝试)
     Caps_Lock: clear
     # 左shift
     Shift_L: inline_ascii
     Shift_R: inline_ascii
     Control_L: noop 
     Control_R: noop 
//weasel.custom.yaml
patch: 
  "style/color_scheme": Time_water # 应用的配色方案
  "style/font_face": "Microsoft YaHei" # 应用的字体
  "style/font_point": 13 # 字号大小
  "style/horizontal": true # 候选栏横排显示
  "style/inline_preedit": false # 隐藏打字栏
  "style/display_tray_icon": false # 不显示托盘图标
  "style/layout/spacing": 8 # 打字栏与候选栏的间距
  "style/layout/border_width": 2 # 边框宽度
  "style/layout/margin_x": 10 # 候选字左右边距
  "style/layout/margin_y": 10 # 候选字上下边距
  "style/layout/candidate_spacing": 10 # 候选字间隔
  "style/layout/hilite_spacing": 3 # 序号和候选字之间的间隔
  "style/layout/round_corner": 10 # 候选字背景色块圆角幅度
  "style/layout/hilite_padding": 4 # 候选字背景色色块高度和打字栏未选择字背景色块高度 若想候选字背景色块无边界填充候选框,仅需其高度和候选字上下边距一致即可

  # 配色方案名称(用于使用 建议英文)
  "preset_color_schemes/Time_water":
    name: windily # 名称 只用于阅读
    author: cloud # 作者 只用于阅读
    text_color: 0x969483 # 打字栏除正在选择字外的字 字体颜色
    back_color: 0xf2f2f2 # 打字栏与候选栏 背景色
    border_color: 0xffccff # 边框颜色
    #label_color: 0xffffff  # 候选栏 序号颜色
    candidate_text_color: 0x000000 # 候选栏 未候选字颜色
    comment_text_color: 0xd28b26 # 候选栏 补充说明 字体颜色
    hilited_text_color: 0x394bdd # 打字栏 正在选择的字 字体颜色
    hilited_back_color: 0xf2f2f2 # 打字栏 正在选择的字 背景色
    hilited_candidate_text_color: 0xff2288 # 候选栏 候选字颜色
    hilited_candidate_back_color: 0xffccff # 候选栏 候选字背景色

latex-suite 自动补全

自动补全一般分为三类:

  1. 根据笔记库自动补全
  2. 根据上下文自动补全
  3. 根据配置文件自动补全

这三类的定制性依次升高,实际上是精准补全 vs 全面补全的权衡。如果补全的来源是整个笔记库,提示的条数会在 9-10 条左右,随着笔记的增加,精准率会逐渐上升,同时给笔记软件带来的负荷也会上升。基本原理普遍是注册 codemirror 的扩展,稳定性会受到 ob 的影响。之前 ob 对 codemirror 做了一层封装,想要抹平 codemirror 升级带来对插件的影响,但由于很多是不可控的特性(比如把我最爱的光标美化给搞没了),此方案遂被放弃。

obsidian 里有极其多的自动补全方案,包括但不限于:

  • latex-suite:目前使用的插件,开发者友好,且强烈依赖它。最重要的是给了一系列 latex 的输入规则,这是其他插件不可媲美的,功能也足够强大。
  • typing-transformer-obsidianopen in new window:后起之秀,自定义程度也高,是中文开发者,着重解决中文输入痛点,比如¥符号等。核心特性是支持局部格式化。
  • various-complements:全文补全,整库补全的代表,支持自定义词典,足够强大。但我贼烦它时不时跳出来,让我失去了控制感。
  • text-snippets:早年试用过,配置文件补全的代表,但貌似开发者并不强烈依赖它,停更了一段时间,于是我选择了其他插件
  • text-expander:最大的特性是支持脚本补全,是比正则补全更加强大自由的补全方式,试用了一段时间,对脚本的需求不是特别大。我甚至在 latex-suite 下开了 issue,开发者鸽了我,目前来看也无所谓了。
  • completr:扫描全库补全,对我来说 latex 补全远远不及 latex-suite
  • autocomplete:开发者放弃了这个插件

latex-suite 简单易学,前置条件是正则表达式。其目前分为三种模式 (看 issue 还在增加),options 是模式,t 代表文本模式,m 代表 latex 模式,r 代表正则模式,A 代表自动触发。$0 代表光标跳转的位置,[[0]] 代表正则表达式替换的位置。因此可实现:

  1. 正常编辑字符串/正则自动触发补全
  2. 正常编辑 tab 触发字符串/正则补全
  3. latex 模式自动补全,并带有预览框

vim 编辑跳转重构

ob 的编辑跳转比较羸弱,当碰到大文本量时(万字以上),很难优雅的跳转。于是可以采用 vim 模式进行增强,主要场景包括:

  • 选择:当选择 100 行时,vim 可以用 100G 方便的跳转和选择,鼠标选择由于所见即所得,会跳得头疼
  • 重构:把所有二级标题改为一级标题,%s/^##/# 即可处理完成
  • 标记:vim 的标记可以方便的给当前位置做标记,想要的时候跳转
  • 自动居中:zz 使得能让当前光标行居中,虽然有插件可以自动居中,但显然有时候我不想让他一直居中
  • 增强快捷键:vim 的快捷键可以分配给 ob 的命令,不然键盘快捷键属实不够
  • ....

ob 里 vim 增强插件有:

注意:ob 的 vim 模式依然有点小问题,比如打中文打着打着不能实时渲染了,得刷新才行。无报错,无固定复现路径。

ob 支持的 vim 特性

目前 ob 支持的 vim 特性如下,不算太多,但足够使用:

var defaultKeymap = [
    // Key to key mapping. This goes first to make it possible to override
    // existing mappings.
    { keys: '<Left>', type: 'keyToKey', toKeys: 'h' },
    { keys: '<Right>', type: 'keyToKey', toKeys: 'l' },
    { keys: '<Up>', type: 'keyToKey', toKeys: 'k' },
    { keys: '<Down>', type: 'keyToKey', toKeys: 'j' },
    { keys: 'g<Up>', type: 'keyToKey', toKeys: 'gk' },
    { keys: 'g<Down>', type: 'keyToKey', toKeys: 'gj' },
    { keys: '<Space>', type: 'keyToKey', toKeys: 'l' },
    { keys: '<BS>', type: 'keyToKey', toKeys: 'h', context: 'normal'},
    { keys: '<Del>', type: 'keyToKey', toKeys: 'x', context: 'normal'},
    { keys: '<C-Space>', type: 'keyToKey', toKeys: 'W' },
    { keys: '<C-BS>', type: 'keyToKey', toKeys: 'B', context: 'normal' },
    { keys: '<S-Space>', type: 'keyToKey', toKeys: 'w' },
    { keys: '<S-BS>', type: 'keyToKey', toKeys: 'b', context: 'normal' },
    { keys: '<C-n>', type: 'keyToKey', toKeys: 'j' },
    { keys: '<C-p>', type: 'keyToKey', toKeys: 'k' },
    { keys: '<C-[>', type: 'keyToKey', toKeys: '<Esc>' },
    { keys: '<C-c>', type: 'keyToKey', toKeys: '<Esc>' },
    { keys: '<C-[>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' },
    { keys: '<C-c>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' },
    { keys: '<C-Esc>', type: 'keyToKey', toKeys: '<Esc>' }, // ipad keyboard sends C-Esc instead of C-[
    { keys: '<C-Esc>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' },
    { keys: 's', type: 'keyToKey', toKeys: 'cl', context: 'normal' },
    { keys: 's', type: 'keyToKey', toKeys: 'c', context: 'visual'},
    { keys: 'S', type: 'keyToKey', toKeys: 'cc', context: 'normal' },
    { keys: 'S', type: 'keyToKey', toKeys: 'VdO', context: 'visual' },
    { keys: '<Home>', type: 'keyToKey', toKeys: '0' },
    { keys: '<End>', type: 'keyToKey', toKeys: '$' },
    { keys: '<PageUp>', type: 'keyToKey', toKeys: '<C-b>' },
    { keys: '<PageDown>', type: 'keyToKey', toKeys: '<C-f>' },
    { keys: '<CR>', type: 'keyToKey', toKeys: 'j^', context: 'normal' },
    { keys: '<Ins>', type: 'keyToKey', toKeys: 'i', context: 'normal'},
    { keys: '<Ins>', type: 'action', action: 'toggleOverwrite', context: 'insert' },
    // Motions
    { keys: 'H', type: 'motion', motion: 'moveToTopLine', motionArgs: { linewise: true, toJumplist: true }},
    { keys: 'M', type: 'motion', motion: 'moveToMiddleLine', motionArgs: { linewise: true, toJumplist: true }},
    { keys: 'L', type: 'motion', motion: 'moveToBottomLine', motionArgs: { linewise: true, toJumplist: true }},
    { keys: 'h', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: false }},
    { keys: 'l', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: true }},
    { keys: 'j', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, linewise: true }},
    { keys: 'k', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, linewise: true }},
    { keys: 'gj', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: true }},
    { keys: 'gk', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: false }},
    { keys: 'w', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false }},
    { keys: 'W', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false, bigWord: true }},
    { keys: 'e', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, inclusive: true }},
    { keys: 'E', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, bigWord: true, inclusive: true }},
    { keys: 'b', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }},
    { keys: 'B', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false, bigWord: true }},
    { keys: 'ge', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, inclusive: true }},
    { keys: 'gE', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, bigWord: true, inclusive: true }},
    { keys: '{', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: false, toJumplist: true }},
    { keys: '}', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: true, toJumplist: true }},
    { keys: '(', type: 'motion', motion: 'moveBySentence', motionArgs: { forward: false }},
    { keys: ')', type: 'motion', motion: 'moveBySentence', motionArgs: { forward: true }},
    { keys: '<C-f>', type: 'motion', motion: 'moveByPage', motionArgs: { forward: true }},
    { keys: '<C-b>', type: 'motion', motion: 'moveByPage', motionArgs: { forward: false }},
    { keys: '<C-d>', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: true, explicitRepeat: true }},
    { keys: '<C-u>', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: false, explicitRepeat: true }},
    { keys: 'gg', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: false, explicitRepeat: true, linewise: true, toJumplist: true }},
    { keys: 'G', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: true, explicitRepeat: true, linewise: true, toJumplist: true }},
    {keys: "g$", type: "motion", motion: "moveToEndOfDisplayLine"},
    {keys: "g^", type: "motion", motion: "moveToStartOfDisplayLine"},
    {keys: "g0", type: "motion", motion: "moveToStartOfDisplayLine"},
    { keys: '0', type: 'motion', motion: 'moveToStartOfLine' },
    { keys: '^', type: 'motion', motion: 'moveToFirstNonWhiteSpaceCharacter' },
    { keys: '+', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true }},
    { keys: '-', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, toFirstChar:true }},
    { keys: '_', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true, repeatOffset:-1 }},
    { keys: '$', type: 'motion', motion: 'moveToEol', motionArgs: { inclusive: true }},
    { keys: '%', type: 'motion', motion: 'moveToMatchedSymbol', motionArgs: { inclusive: true, toJumplist: true }},
    { keys: 'f<character>', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: true , inclusive: true }},
    { keys: 'F<character>', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: false }},
    { keys: 't<character>', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: true, inclusive: true }},
    { keys: 'T<character>', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: false }},
    { keys: ';', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: true }},
    { keys: ',', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: false }},
    { keys: '\'<character>', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true, linewise: true}},
    { keys: '`<character>', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true}},
    { keys: ']`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true } },
    { keys: '[`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false } },
    { keys: ']\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true, linewise: true } },
    { keys: '[\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false, linewise: true } },
    // the next two aren't motions but must come before more general motion declarations
    { keys: ']p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true, matchIndent: true}},
    { keys: '[p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true, matchIndent: true}},
    { keys: ']<character>', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: true, toJumplist: true}},
    { keys: '[<character>', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: false, toJumplist: true}},
    { keys: '|', type: 'motion', motion: 'moveToColumn'},
    { keys: 'o', type: 'motion', motion: 'moveToOtherHighlightedEnd', context:'visual'},
    { keys: 'O', type: 'motion', motion: 'moveToOtherHighlightedEnd', motionArgs: {sameLine: true}, context:'visual'},
    // Operators
    { keys: 'd', type: 'operator', operator: 'delete' },
    { keys: 'y', type: 'operator', operator: 'yank' },
    { keys: 'c', type: 'operator', operator: 'change' },
    { keys: '=', type: 'operator', operator: 'indentAuto' },
    { keys: '>', type: 'operator', operator: 'indent', operatorArgs: { indentRight: true }},
    { keys: '<', type: 'operator', operator: 'indent', operatorArgs: { indentRight: false }},
    { keys: 'g~', type: 'operator', operator: 'changeCase' },
    { keys: 'gu', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, isEdit: true },
    { keys: 'gU', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, isEdit: true },
    { keys: 'n', type: 'motion', motion: 'findNext', motionArgs: { forward: true, toJumplist: true }},
    { keys: 'N', type: 'motion', motion: 'findNext', motionArgs: { forward: false, toJumplist: true }},
    { keys: 'gn', type: 'motion', motion: 'findAndSelectNextInclusive', motionArgs: { forward: true }},
    { keys: 'gN', type: 'motion', motion: 'findAndSelectNextInclusive', motionArgs: { forward: false }},
    // Operator-Motion dual commands
    { keys: 'x', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorMotionArgs: { visualLine: false }},
    { keys: 'X', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: false }, operatorMotionArgs: { visualLine: true }},
    { keys: 'D', type: 'operatorMotion', operator: 'delete', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
    { keys: 'D', type: 'operator', operator: 'delete', operatorArgs: { linewise: true }, context: 'visual'},
    { keys: 'Y', type: 'operatorMotion', operator: 'yank', motion: 'expandToLine', motionArgs: { linewise: true }, context: 'normal'},
    { keys: 'Y', type: 'operator', operator: 'yank', operatorArgs: { linewise: true }, context: 'visual'},
    { keys: 'C', type: 'operatorMotion', operator: 'change', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
    { keys: 'C', type: 'operator', operator: 'change', operatorArgs: { linewise: true }, context: 'visual'},
    { keys: '~', type: 'operatorMotion', operator: 'changeCase', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorArgs: { shouldMoveCursor: true }, context: 'normal'},
    { keys: '~', type: 'operator', operator: 'changeCase', context: 'visual'},
    { keys: '<C-u>', type: 'operatorMotion', operator: 'delete', motion: 'moveToStartOfLine', context: 'insert' },
    { keys: '<C-w>', type: 'operatorMotion', operator: 'delete', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }, context: 'insert' },
    //ignore C-w in normal mode
    { keys: '<C-w>', type: 'idle', context: 'normal' },
    // Actions
    { keys: '<C-i>', type: 'action', action: 'jumpListWalk', actionArgs: { forward: true }},
    { keys: '<C-o>', type: 'action', action: 'jumpListWalk', actionArgs: { forward: false }},
    { keys: '<C-e>', type: 'action', action: 'scroll', actionArgs: { forward: true, linewise: true }},
    { keys: '<C-y>', type: 'action', action: 'scroll', actionArgs: { forward: false, linewise: true }},
    { keys: 'a', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'charAfter' }, context: 'normal' },
    { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'eol' }, context: 'normal' },
    { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'endOfSelectedArea' }, context: 'visual' },
    { keys: 'i', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'inplace' }, context: 'normal' },
    { keys: 'gi', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'lastEdit' }, context: 'normal' },
    { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'firstNonBlank'}, context: 'normal' },
    { keys: 'gI', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'bol'}, context: 'normal' },
    { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'startOfSelectedArea' }, context: 'visual' },
    { keys: 'o', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: true }, context: 'normal' },
    { keys: 'O', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: false }, context: 'normal' },
    { keys: 'v', type: 'action', action: 'toggleVisualMode' },
    { keys: 'V', type: 'action', action: 'toggleVisualMode', actionArgs: { linewise: true }},
    { keys: '<C-v>', type: 'action', action: 'toggleVisualMode', actionArgs: { blockwise: true }},
    { keys: '<C-q>', type: 'action', action: 'toggleVisualMode', actionArgs: { blockwise: true }},
    { keys: 'gv', type: 'action', action: 'reselectLastSelection' },
    { keys: 'J', type: 'action', action: 'joinLines', isEdit: true },
    { keys: 'gJ', type: 'action', action: 'joinLines', actionArgs: { keepSpaces: true }, isEdit: true },
    { keys: 'p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true }},
    { keys: 'P', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true }},
    { keys: 'r<character>', type: 'action', action: 'replace', isEdit: true },
    { keys: '@<character>', type: 'action', action: 'replayMacro' },
    { keys: 'q<character>', type: 'action', action: 'enterMacroRecordMode' },
    // Handle Replace-mode as a special case of insert mode.
    { keys: 'R', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { replace: true }, context: 'normal'},
    { keys: 'R', type: 'operator', operator: 'change', operatorArgs: { linewise: true, fullLine: true }, context: 'visual', exitVisualBlock: true},
    { keys: 'u', type: 'action', action: 'undo', context: 'normal' },
    { keys: 'u', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, context: 'visual', isEdit: true },
    { keys: 'U', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, context: 'visual', isEdit: true },
    { keys: '<C-r>', type: 'action', action: 'redo' },
    { keys: 'm<character>', type: 'action', action: 'setMark' },
    { keys: '"<character>', type: 'action', action: 'setRegister' },
    { keys: 'zz', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }},
    { keys: 'z.', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
    { keys: 'zt', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }},
    { keys: 'z<CR>', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
    { keys: 'zb', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }},
    { keys: 'z-', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
    { keys: '.', type: 'action', action: 'repeatLastEdit' },
    { keys: '<C-a>', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: true, backtrack: false}},
    { keys: '<C-x>', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: false, backtrack: false}},
    { keys: '<C-t>', type: 'action', action: 'indent', actionArgs: { indentRight: true }, context: 'insert' },
    { keys: '<C-d>', type: 'action', action: 'indent', actionArgs: { indentRight: false }, context: 'insert' },
    // Text object motions
    { keys: 'a<character>', type: 'motion', motion: 'textObjectManipulation' },
    { keys: 'i<character>', type: 'motion', motion: 'textObjectManipulation', motionArgs: { textObjectInner: true }},
    // Search
    { keys: '/', type: 'search', searchArgs: { forward: true, querySrc: 'prompt', toJumplist: true }},
    { keys: '?', type: 'search', searchArgs: { forward: false, querySrc: 'prompt', toJumplist: true }},
    { keys: '*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }},
    { keys: '#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }},
    { keys: 'g*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', toJumplist: true }},
    { keys: 'g#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', toJumplist: true }},
    // Ex command
    { keys: ':', type: 'ex' }
  ];

定位

|300
|300
操作快捷键备注
下一个单词开头w非标点也算单词,vimrc 中可调
以空格为分隔向后移动W
上一个单词的开头b非标点也算单词,vimrc 中可调
以空格为分隔向前移动B
下一个单词的结尾e非标点也算单词,vimrc 中可调
上一个单词的结尾ge非标点也算单词,vimrc 中可调
移动到行首第一个字符0
移动到行首非空字符^我改到了 H
移动到行尾$2$ 代表下一行的行尾,我改到了 L
行内向后移动到一个指定字符f{char}计数前缀 ; 重复,, 反向重复
行内向前移动到一个指定字符F{char}计数前缀 ; 重复,, 反向重复
行内向后移动到目标字符的前一个字符t{char}计数前缀 ; 重复,, 反向重复
行内向前移动到目标后一个字符T{char}计数前缀 ; 重复,, 反向重复
单词匹配*
括号匹配%
移动到指定的行{num}G 或者 :{num}
移动到第一行gg
移动到最后一行G
百分号定位{num}%
可视区域定位H M L定位到可视区域的高 hign,中 middle,低 low
向下滚半屏ctrl + ddown
向上滚半屏ctrl + uup
向下滚整屏ctrl + fforward
向上滚整屏ctrl + bbackward
让光标所处行在顶部zttop
让光标所处行在中间zz
让光标所处行在底部zb
跳转到上一个位置``
上次修改的地方`.
上次插入的地方`^
上次复制和修改的起始位置`[
标记位置m标记一个 char 位置
跳转标记位置`
在行内跳转到下一指定字符前t
在行内跳转到上一指定字符上f
在行内跳转到上一指定字符后t

编辑

操作快捷键备注
增加从当前行到文档末尾处的缩进层级>G
行首插入I
行尾插入A
修改当前光标至行尾的内容C
修改当前字符r
进入替换模式R
修改当前字符s
修改当前整行内容S
在下一行插入o
在上一行插入O
连续修改相同单词* -> cw{new char} -> n -> .obsidian 中中英文有问题
删除一个单词daw
数字递增ctrl + a不必在数字上, 180 + ctrl + a,对数字加 180
数字递减ctrl + x不必在数字上
删除一个段落dap
自动缩进=
增加缩进>
减少缩进<

改变大小写

操作快捷键备注
大写gU
小写gu
反转大小写g~

查找

操作快捷键备注
页内向后查找/{string}特殊字符用 \ 转义, n 下一个 , N 上一个
页内向前查找?{string}特殊字符用 \ 转义, n 下一个 , N 上一个
查找已有单词*在单词下面按 *
匹配单词末尾/{word}\
匹配单词开始/\<{word}
全字匹配/\<{word}\
匹配行首/^{word}正则匹配
匹配行尾/{word}$正则匹配
正则匹配

替换

操作快捷键备注
行内替换s/from/to/flag&重复,u 回退
全局替换%s/from/to/flag
指定行替换{num},{num}s/from/to/flag
标记行内替换
指定范围替换.+{num},$-{num}s/from/to/flag

复制粘贴

操作快捷键备注
复制到系统剪贴板"+yshift + insert

折叠

操作快捷键备注
折叠zc
展开zo

可视模式与选区

v 可以进入可视模式

操作快捷键备注
激活面向字符的可视模式v
激活面向行的可视模式V
激活面向列块的可视模式ctrl + v
重选上次选区gv
切换选区活动端o
可视选择一个单词viw
选择标签内内容vitobsidian 内不适用
选区高亮的第一行(用于命令模式)`<
选区高亮的最后行(用于命令模式)`>
插入I A
修改整个可视列块c
修改整个可视列块即块后内容C
连接行J

Vim 的文本对象由两个字符组成,第一个字符永远是 i 或是 a。一 般以 i 开头的文本对象会选择分隔符内部的文本,而以 a 开头的文本对 象会选择包括分隔符在内的整个文本。为了便于记忆,可以把 i 成“inside”,而把 a 想成“around”或“all”。

|600
|600
|600
|600

记录与回放/宏录制

  1. "q{register}" 命令启动一次击键记录,结果保存到 {register} 指定的寄存器中。寄存器名可以用 a 到 z 中任一个字母表示。
  2. 输入你的命令。
  3. 键入 q (后面不用跟任何字符) 命令结束记录。

现在,你可以用 "@{register}" 命令执行这个宏。

快捷键管理

心得体会:别 tm 想着快捷键管理,常用的快捷键自然而然会记住,冲突了自然而然会替换,分配啥的随当时心意就好。

文本格式化

格式化的目的不仅仅是强迫症为了好看,更多的是为了文本一致性。一致的文本有利于检索。这里的一致性有很多可折腾的,但现在我并不想展开,它和整个笔记系统相关(这不是我故作高深,详情可以参考前端 linter,prettier 的爱恨情仇,每一个语句后面需不需要引号都能写篇长文论证。数以百计的 linter 规则,还有 git commit 自动化, html,css 等规则,足以说明健全的笔记系统,格式化的重要性足够重要)。只给出一致性带给我们的优势:==给整个笔记库应用统一规则,给不同笔记应用不同的规则。==这里的规则一方面限制我们书写笔记的自由度,使得文本检索,文本排版,文本索引,文本属性得以更好发挥,另一方面,给我们更大的自由度,即自动化。

  • 限制自由度:每篇笔记都按照特定格式,特定文本,甚至特定文件夹组织,笔记完全是一个规则化的系统。
  • 更大自由度:正是因为限制了自由度,才能用脚本或者插件,在规则的基础上找到修改,重构,检索的方法。md 一般以 # 为标题 (其实 --- 也行),这样就能自动检索,修改标题,这是非格式化的文本做不到的。

目前 ob 的格式化插件也很多:

  1. platers/obsidian-linter: An Obsidian plugin that formats and styles your notes with a focus on configurability and extensibility. (github.com)open in new window:强大,高度定制化,下限高,上限不低,能自定义运行命令。这个自定义运行命令是我加的,但现在看来多此一举,因为我可以用 quickadd 先运行 linter,再运行自定义命令,完全不用写一行代码。我人傻了。
  2. hipstersmoothie/obsidian-plugin-prettier: Format obsidian.md notes using prettier (github.com)open in new window:功能羸弱
  3. cristianvasquez/obsidian-prettify: A markdown prettifier for obsidian (github.com)open in new window:不再维护

其实文本格式化还有一个用途,就是 lint。比如笔记少些了 tag,哪个位置差点什么,能在 lint 时提醒自己。当然,这属于高度定制化功能,得 用 linter 插件写脚本。

版本管理

git 的重要性不言而喻,但非程序员很少有人能很好的利用起来这个强大的工具,具体这是个啥玩意儿,我又不是写教程,自己去学去。这里给出我的心得,git 教程应该不会有:

一般的 git 工作流:

  1. 创建新的分支 branch
  2. 创建提交 commit 保存你的笔记,使得该阶段可回溯
  3. 进行工作,完成工作,并提交 commit
  4. 合并分支到主分支

这种工作流往往会产生一些不必要的 commit 节点,使得 git 的历史是不整洁,不利于检查的。偶尔还需要 rebase,squash merge 等操作。

这里推荐一个新的工作流:

  1. 创建新的分支 branch
  2. 提交 commit 保存笔记,不必在意注释内容
  3. 重置 reset 会初始状态,重新 add 并 commit,写注释
  4. 合并分支到主分支
$ git reset origin/main
Unstaged changes after reset:
M       src/components/Footer/Footer.tsx
M       src/components/Nav/Nav.css
M       src/components/Nav/Nav.tsx
M       src/components/Posts/Post.tsx
M       src/components/Posts/PostList.tsx

$ git status
On branch feature-branch
Your branch is behind 'origin/feature-branch' by 3 commits, and can be fast-forwarded.
  (use "git pull" to update your local branch)

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   src/components/Footer/Footer.tsx
        modified:   src/components/Nav/Nav.css
        modified:   src/components/Nav/Nav.tsx
        modified:   src/components/Posts/Post.tsx
        modified:   src/components/Posts/PostList.tsx

$ git add src/components/Nav/Nav.css
$ git add src/components/Nav/Nav.tsx
$ git commit -m"Added new styles to navigation"

$ git add src/components/Posts/Post.tsx
$ git add src/components/Posts/PostList.tsx
$ git commit -m"Updated author images on posts"

$ git add src/components/Footer/Footer.tsx
$ git commit -m"Fixed responsive bug in footer"

当然也可借助一些前端命令行工具应用一定的规则进行提交,比如 enhancestudy 等关键词,方便以后通过 git 回溯学习记录。