将 VS Code 打造成跨平台 Markdown「草稿本」

2023-09-13

A version of this article appears on Sep. 13, 2023 on SSPAI as a member-only post. Learn more or subscribe

The article is permitted to be self-archived in the version as originally submitted for publication on the author’s personal website under CC BY-NC 4.0 pursuant to § 5.2(b) of the SSPAI Fellowship Contributor Agreement.

需求讨论

在电脑上工作时,经常会需要一个暂存文本的空间,用来放置一闪而过的想法、起草相对重要的消息回复,或者汇总来自多处的摘录——相当于案头备着的草稿纸所起的功能。

诚然,可选解决方案不可谓不多,既有 nvALTTotDrafts 等将此作为主要功能的软件,也可以用各类笔记软件、甚至剪贴板管理工具兼任。

Tot (MacWorld)

但是,我对这些方案一直不很满意。打草稿追求的是快进快出、随身携带,但上述工具中很少见到能兼顾收集快捷、导出方便和跨平台(特别是跨 macOS 和 Windows)同步的。将「草稿」和普通笔记混同在一起,也不便于管理。

更重要的是,越是草稿和临时性质的内容,越意味着需要大量编辑,包括调整顺序、提取信息、整理格式等等;笔记软件通常不会对此做特别优化,导致操作起来颇为麻烦。

经过多番尝试和比较后,我开始意识到,文本编辑器其实就是一种特别适合做「草稿本」的工具:由于基于朴素的目录和文本文件,很容易通过终端命令实现自动化,搭车通用的云盘实现跨平台同步;编辑器的定位使然,查找替换、快捷键操作、自动补全和高亮等功能也是必然内置的。

不过,要让文本编辑器真正成为方便好用的「草稿本」,还需要具有以下功能:

  • 支持自动保存。这是「草稿本」让人能放心使用的信任基础,又可以具体细分为自动保存更改和自动保存会话:前者是指自动保存对现有文件的编辑,后者是指在关闭窗口或退出软件时,即使没有保存到文件的临时内容也会被暂存下来,并在下次启动时重现。
  • 支持新文件名模板。没有人会想在打草稿的时候费脑筋起文件名。「草稿本」最好能为每个新建的文件按一定规则拟一个不会重复的文件名,例如通过时间戳或 UUID。
  • 支持 Markdown 语法。对于文本笔记,Markdown 支持几乎是必须的。理想的「草稿本」应该支持 Markdown 语法高亮、常见格式的快捷键,以及同步预览。
  • 支持区隔不同场景下的设置。「草稿本」的上述功能需求与一般的文件查看和代码编辑是非常不同的;如果编辑器只支持一套配置,按此设置也就意味着它几乎不能再用于别的任务了,显得有些小题大做。

调研下来,如果以「出厂状态」为标准,只有 BBEdit 最接近这样的要求。事实上,它的 Notebook 功能完全就是为了这个需求开发的。

BBEdit 的 Notebook 功能(Bare Bones Software)

遗憾的是,BBEdit 只支持 macOS 平台;作为三十年历史的经典软件,有些设计和操作在今天看来略显古旧,对 Markdown 和中文的支持都不完美;Notebook 也不属于其免费功能范围,50 美元的门票有些昂贵了。

于是,我又打起了自己另一个常用软件 VS Code 的主意。经过一番尝试,竟然完全符合需求:

  • VS Code 本身是跨平台、开源的代码编辑器,基础的编辑功能自然不在话下,而且具有特别丰富的插件生态。这使得 VS Code 可能是代码编辑器里最擅长写 Markdown 的,Markdown 工具里编辑功能最强的。
  • 支持多套配置的灵活切换。特别是一月更新的 1.75 版加入了一个非常实用的「配置文件」(profile)功能,可以为各个使用场景选择不同的设置、插件、界面布局和快捷键等,比以往基于工作区(workspace)的方式更灵活,而且支持云端同步。
  • 自动化方便,可以将任何终端命令保存为「任务」;还有完善的命令行界面,方便对接其他自动化工具。

下面,本文将具体介绍怎样将 VS Code 打造成一个跨平台的 Markdown「草稿本」,同时不影响它发挥代码编辑器的本职功能。如果有类似需求,你可以依样操作,也可以挑选适合自己的部分变通采纳。

搭建基本框架

首先,确定一个「草稿本」的存放目录。具体位置按个人习惯即可,但如果有同步需求,可以选择一个云盘中的位置;如果本来就用纯文本方式做笔记,还可以取现有路径的下级目录,方便统一管理。

例如,我的「草稿本」位于 ~/flashes(取「闪念」之意)。这个目录实际上是指向我 Obsidian 笔记库同名子目录的符号链接,本身 (1) 位于 iCloud 云盘中,(2) 通过 Syncthing 与非苹果设备同步,并且 (3) 自动定期提交到一个私人 GitHub 仓库。

这样,无论在 Mac、Android 手机还是 VPS 服务器上,我都可以随时访问到这些「草稿」;在 Obsidian 中整理笔记时,还可以直接引用或链接到「草稿本」中的内容。

创建完成后,在 VS Code 中通过命令面板(Command/Ctrl+Shift+P)运行 File: Open Folder,将这个笔记目录作为工作区打开。(由于 VS Code 的中文本地化翻译质量不高,本文中提及的命令和操作名称以英文版为准。)

接着,创建一个「草稿本」专用配置文件。 为此,通过命令面板运行 Profiles: Create Profile。

在显示的选项中:

  • 配置文件名称:按喜好填写即可,尽量简洁,方便后面自动化。例如我这里起了一个 flashes,意指「闪念」。
  • 是否复制现有配置或模板的内容:这可以根据你之前的使用情况决定——
    • 如果你从事开发工作,已经对 VS Code 做了开发环境的重度配置,那么显然不适合照搬到「草稿本」中,最好选择「无」,从头开始。
    • 相反,如果你像我一样,主要用 VS Code 码字,配置比较轻量,那么不妨选择「默认」来沿用熟悉的环境。
    • 还有一种选择是 Doc Writer,这是 VS Code 提供的模板之一,内置了一系列为码字优化的设置项和流行插件;如果想偷懒或者没什么思路,它是一个比较好的起点。
  • 配置文件范围:这里列举了配置文件可以独立容纳的内容,如果取消勾选,新的配置文件会直接沿用默认配置的相应部分。如后文将会表明,这几项对于配置「草稿本」都是有用的,保持全选的默认状态即可。

做好设置后,点击 Create 即可完成创建。你可以从 Acticity Bar 上设置按钮的角标判断当前生效的配置文件。此外,工作区会记住上一次切换到的配置文件,因此以后打开「草稿本」文件夹时,都会自动使用刚才新创建的配置文件。

优化「草稿本」的内部环境

如开头提到,配置文件功能的一大优点就是实现了设置、插件的相互隔离。因此,我们可以随意将这个「草稿本」专用的新配置向着最适合 Markdown 随手记的环境改造,而不用担心干扰更适合编辑代码的默认配置,或者因为插件装得太多影响启动速度和性能。

至于具体怎么改造,则是萝卜青菜的问题,读者可以自由发挥。官方帮助中的「Markdown 编辑」一节是很好的起点(读完你很可能会意识到很多功能现在已经不需要装插件)。我之前还介绍过 AutoCorrect(自动添加中英文间的空格)和 markdownlint(自动检测和修复 Markdown 格式)两个实用 Markdown 写作插件,有兴趣的朋友可以回顾

下面只重点介绍一些我认为特别适合本文场景、需要一些注解的配置方式。

精简界面布局

VS Code 默认布局显得比较聒噪,很多功能对于 Markdown 笔记场景也并不适用。因为配置文件也会隔离界面布局,可以放心做些「大刀阔斧」的精简。例如:

  • 将侧边栏的 Explorer、Search 等高频使用的面板放到底栏中(通过拖拽,或者在标题上点击右键,选择 Panel Position > Bottom),然后隐藏 Source Control、Problems 等无关面板(在标题上点击右键选择 Hide Panel)。这样,就可以隐藏左右边栏节约空间了。
  • 点击窗口右上角的「自定义布局」按钮,打开 Zen Mode(但同时关闭 Centered Layout),隐藏无关的界面元素。

这样,界面看起来就清爽多了。

设置自动保存

好的「草稿本」应该尽量让人忘掉「保存」这回事,自己默默保存更改就好,不要用「是否保存」的弹窗来骚扰用户。为此,相关的设置是:

  • files.autoSave: 设为 afterDelay,即每隔一段时间自动保存,具体间隔通过 files.autoSaveDelay 指定(单位为毫秒),例如 5000 即为每 5 秒保存一次。
  • files.hotExit: 设为 onExitAndWindowClose,即在关闭窗口或退出应用时均不额外提醒,下次打开时自动恢复未保存内容。

一键插入格式模版

很多第三方笔记工具会提供快速插入 Markdown 模板的菜单项和快捷键,而这在 VS Code 中也很好实现,并且用自带的「代码片段」(snippet)和快捷键映射功能即可,不需要任何插件。

下面以自制插入 Markdown 任务列表(- [ ])的模板并绑定快捷键为例演示,配置好的效果如下图所示。

首先,将任务列表的语法创建为一个代码片段。 为此,用命令面板运行 Snippets: Configure User Snippets,然后选择 markdown。将打开的 markdown.json 文件修改如下:

{
    // omitted
    "Insert task list": {
        "prefix": "task",
        "body": [
            "- [${1| ,x|}] $2",
            "$0"
        ],
        "description": "Insert task list"
    },
    // omitted
}

由此,我们创建了一个名为 Insert task list,简称为 task 的代码片段,其格式定义位于 body 键。其中:

  • $1$2……$n 称为 tabstop,是模板中待填写的「空档」,在插入后可以按 Tab 键快速跳到下一个。
  • ${1| ,x|} 是指在光标位于这个空档时,用列表提供空格和 x 两个选项供用户选择,对应任务列表的两种状态。
  • $0 是填完所有空档后,光标最后停在的位置,这里设置为下一行的行首。

这时,就可以用命令面板运行 Snippets: Insert Snippet,然后选择 task 来调用这个模板了。

接着,我们为它指定一个快捷键,以便更快调用。 用命令面板运行 Preferences: Open Keyboard Shortcuts (JSON),将打开的 keybindings.json 文件修改如下:

[
    // omitted
    {
        "when": "editorTextFocus && isMac",
        "key": "cmd+k t",
        "command": "editor.action.insertSnippet",
        "args": {
            "name": "Insert task list",
            "langId": "markdown"
        }
    },
    {
        "when": "editorTextFocus && !isMac",
        "key": "ctrl+k t",
        "command": "editor.action.insertSnippet",
        "args": {
            "name": "Insert task list",
            "langId": "markdown"
        }
    },
    // omitted
]

这里:

  • key 键用于指定快捷键,可以是组合式、两段式或其混合(完整语法见官方帮助)。例如 cmd+k t 指先按 Command + K,然后再按 T
  • when 键用于指定启用快捷键的环境,可用条件包括操作状态、操作系统等,且支持逻辑运算(完整列举见官方帮助)。例如 editorTextFocus && !isMac 表示仅在输入焦点位于编辑区域,且当前系统不是 macOS 时才启用。由于 VS Code 是一个跨平台编辑器,这特别适合针对 macOS 和 Windows 设置不同的修饰键。
  • commandargs 键用于指定快捷键要执行的命令。对于代码片段而言,命令总是 editor.action.insertSnippetargs.nameargs.langId 则分别是之前在创建代码片段时设置的名称和适用语言。

通过同样的方法,你可以创建其他自己常用的 Markdown 格式模板并绑定快捷键。和配置文件中的其他设置一样,这都不会和默认配置相冲突,并且可以通过设置同步功能跨端同步。

这里再附一个「插入 Markdown 表格模板」的代码片段供参考:

{
    // omitted
    "Insert 3x2 table": {
        "prefix": "table",
        "body": [
            "| ${1:Column1}  | ${2:Column2}   | ${3:Column3}   |",
            "|-------------- | -------------- | -------------- |",
            "| ${4:Item1}    | ${5:Item1}     | ${6:Item1}     |",
            "${0}"
        ],
        "description": "Insert table with 2 rows and 3 columns."
    },
    // omitted
}

让新建文件名附带时间戳

默认设置下,VS Code 的 Command+N(或 Windows 上的 Ctrl+N)快捷键会新建一个名字毫无新意的 Untitled-1.txt,并且在手动保存之前都是临时文件。对于「草稿本」的场景而言,这既不直观也不方便。

联想到很多采用「卡片笔记」方法的软件都支持自动在文件名中加入时间戳(例如 Obsidian 的「ZK 卡片」按钮),既不会重复,又方便事后检索。这能否在 VS Code 中实现呢?

当然可以,方法是挪用 VS Code 的「任务」(task)功能。任务原本是针对开发场景的自动化功能,每项任务其实就是一条预先定义的命令,可以通过命令面板或快捷键调用,实现代码检查、编译等功能。

这样,问题就转化为「如何用一行命令按模板名称新建文件并打开」。如果有一定终端操作经验,答案想必是很直接的(没有经验也不用担心,今年是 2023 年,有个东西叫 GPT)。

例如,对于 macOS 和 Linux,可以这样写:

note="/path/to/note_$(date +%Y%m%d_%H%M%S).md"; \
    touch "$note" && \
    code --reuse-window "$note"

其中,--reuse-window (-r) 选项告诉 VS Code 尽量「复用」现有的窗口,从而避免每新建一条「草稿」就新开一个窗口。$(date +%Y%m%d_%H%M%S) 使用 date 命令创建一个时间戳,得到的结果形如 20230102_123456,可以按需修改。

而在 Windows 上,等效的 PowerShell 版本则可以写成:

$note='X:\path\to\note_' + $(Get-Date -Format yyyyMMdd_HHmmss) + '.md'; `
    New-Item "$note" && `
    code --reuse-window "$note"

下面我们把这些命令做成 VS Code 任务。用命令面板运行 Tasks: Open User Tasks,然后选择 Others。将打开的 tasks.json 文件编辑为如下内容:

{
    "version": "2.0.0",
    "tasks": [
        // omitted
        {
            "label": "New note",
            "type": "shell",
            "command": "note=\"${workspaceFolder}/note_$(date +%Y%m%d_%H%M%S).md\"; touch \"$note\" && code --reuse-window \"$note\"",
            "windows": {
                "command": "$note='${workspaceFolder}\\note_' + $(Get-Date -Format yyyyMMdd_HHmmss) + '.md'; New-Item \"$note\" && code --reuse-window \"$note\""
            },
            "presentation": {
                "reveal": "silent"
            },
            "problemMatcher": []
        },
        // omitted
    ]
}

其中:

  • type 键用于设定该任务的类型,此处的 shell 即指运行终端命令。具体的终端环境在 macOS 和 Linux 上默认为当前用户的登录 shell($SHELL),在 Windows 上默认为 PowerShell,但也可以自行设置
  • command 键用于设定任务要执行的终端命令。如果同时存在 windows.command 键(如本例),则前者只会在 macOS 和 Linux 上执行任务时调用,Windows 上则会调用后者中的命令。注意由于 JSON 语法,命令中的 "\ 都需要用前缀 \ 转义。
  • 命令路径中的 ${workspaceFolder} 是 VS Code 内置变量之一,会在运行时被替换为当前目录的实际路径,这就实现了定向在「草稿本」目录中创建文件。
  • presentation.revealproblemMatcher 键分别用于设置执行任务时是否显示终端输出,以及是否扫描输出检测问题。这都是针对开发环境有用的功能,而我们只需要「默默」新建文件即可,故分别设置为 silent 和空来禁用。

这样,就可以用命令面板执行 Tasks: Run Task,然后选择 New note 来获得文件名带时间戳的新「草稿」了。

但这显然还不够方便,我们可以在上面介绍过的 keybindings.json 中追加一个快捷键定义,把默认的新建文件快捷键 Command/Ctrl+N「绑架」到我们的自定义任务上:

[
    // omitted
    {
        "key": "ctrl+n",
        "command": "workbench.action.tasks.runTask",
        "args": "New note",
        "when": "!isMac"
    },
    {
        "key": "cmd+n",
        "command": "workbench.action.tasks.runTask",
        "args": "New note",
        "when": "isMac"
    },
    // omitted
]

这样,以后直接按最符合直觉的 Command/Ctrl+N,就能执行 New note 任务,新建并打开时间戳命名的文件了。

这里再附一个「删除正在编辑的文件」的任务定义供参考:

{
    "label": "Delete current",
    "type": "shell",
    "command": "rm -f '${file}'",
    "presentation": {
        "reveal": "silent"
    },
    "problemMatcher": []
}

快速启动和抓取文本

到目前为止,我们已经为「草稿本」打造了一个比较便捷的「内部环境」。接下来,还可以从外部着手,让「草稿本」的启动和内容录入更加流畅。这就是 VS Code 的命令行界面发挥用处的时候了。

在接着往下看之前,请了解:

  • 作为准备步骤,先确保 VS Code 的主程序 code 位于终端环境的搜索路径中。这在 Windows 和 Linux 上一般会在安装时自动配置,在 macOS 上则需要手动通过命令面板运行一次 Shell Command: Install 'code' command in PATH 命令。(具体可参阅官方帮助的初始设置一节。)
  • 由于自动化方案五花八门,下面示例的方法都只是「最大公分母」,优先说明如何使用 macOS 和 Windows 系统的自带功能实现需求。显然,读者可以根据原理改用到自己习惯的工具中。主力用 Linux 的朋友……可能轮不到我教你用什么工具 :)

快速启动「草稿本」

既然花了一番功夫打造出这个独立的「草稿本」环境,自然会期望为它设置一个专用的启动方式。为此,VS Code 提供了下面的命令:

code /path/to/workspace --profile PROFILE_NAME

因此,只需将这个命令打包成一个快捷方式即可:

在 macOS 上,最简单的方法是用快捷指令 app 创建一个只包含一步「执行终端脚本」的快捷指令,在其中按上述格式填好命令。(注意要使用 VS Code 命令行的完整路径 /usr/local/bin/code。)

然后,选择「文件」>「添加到程序坞」菜单项,即可在 Dock 中添加一键打开「草稿本」的图标。

在 Windows 上,直接用快捷方式即可。在桌面空白处单击右键,选择「新建」>「快捷方式」,然后填写其目标为 "%LOCALAPPDATA%\Programs\Microsoft VS Code\Code.exe" "X:\path\to\workspace" --profile PROFILE_NAME(按实际情况更换其中的路径和配置文件名),即可在桌面添加一键打开「草稿本」的图标。

快速捕获别处内容

很多第三方笔记工具都支持不同形式的「快速捕获」功能,可以一键将当前选中的内容添加创建为笔记。这在我们的 VS Code「草稿本」中也很容易实现。

在 macOS 上, 仍然可以用快捷指令为载体,制作一条内容如下图所示的快捷指令(下载,注意按实际情况替换其中的路径和配置文件名)。

然后,在侧边栏中点击「快捷指令详细信息」,勾选「作为快速操作使用」下的「服务菜单」,并点击「添加键盘快捷键」按钮,为其分配一个快捷键。

这样,以后就可以在其他应用中选择文本,再从右键菜单的「服务」中选择这个快捷指令(或者按下设置的快捷键),就可以将其 Markdown 版本新建为一条「草稿」了。(你也可以根据自己的习惯调整是否转为 Markdown、文件名模板等具体步骤。)

根据相同的原理,还可以再制作一条「将剪贴板内容保存为草稿」的快捷指令(下载)。

运行效果如下图所示:

在 Windows 上,没有像 macOS 那样可以直接跨应用选中内容的内置机制,但捕获剪贴板还是可以通过 PowerShell 实现的。为此,将下列脚本(注意按实际情况替换其中的路径和配置文件名)保存到一个固定位置:

$note = "X:\path\to\workspace" + $(Get-Date -Format "yyyyMMdd_HHmmss") + ".md"
Get-Clipboard | Out-File -FilePath "$note"
code -r "$note" --profile PROFILE_NAME

例如,我按通例放在用户主目录下的 bin 目录,命名为 clipboard2scratchpad.ps1

然后,在桌面上新建一个快捷方式,填写其目标为:powershell.exe -ExecutionPolicy Bypass -File "X:\path\to\clipboard2scratchpad.ps1"(路径按实际情况替换)。

之后双击即可将剪贴板内容保存为「草稿」。你还可以在快捷方式的属性中为其分配一个快捷键。