💨

Vim/NeoVimでMarkdownをプレビューするプラグインを作った

2021/09/26に公開
1

powerd by denops

esm.shの仕様変更で動作しなくなっているかもしれません。ご了承ください。
直していただきました!ありがとうございます😭
使用方法が少し変わっているので注意ください

初めに

Vim/NeoVim(Windows/Mac/Linux(+WSL))対応のMarkdownプレビュープラグインを作成しました。
この記事では作った経緯とこのプラグインについて紹介します。

https://github.com/kat0h/bufpreview.vim

使い方

インストール

このプラグインはDenodenops.vimに依存しています。
初めにDenoを公式のインストールからインストールしてください。

denops.vimは通常のVimプラグインと同じようにインストールしてください。

Plug-Vim:

Plug 'vim-denops/denops.vim'
Plug 'kat0h/bufpreview.vim', { 'do': 'deno task prepare' }

初回起動時にはTypescriptファイルの依存関係のダウンロードが走るため、起動に時間がかかります。
また、フロントエンドのビルドが必要になります。あらかじめ$ deno task prepareを実行してください。

起動

Markdownファイルを開く(filetypeがmarkdownに設定された時)と下記のコマンドがバッファに登録されます。

:PreviewMarkdown
:PreviewMarkdownClose
:PreviewMarkdownToggle

コマンドを実行すると、自動でデフォルトのブラウザでプレビューが開きます。
また、:PreviewMarkdownCloseコマンドや、バッファの削除でプレビューを閉じることができます。

機能

bufpreview.vimは下記の機能を備えています。

  • リアルタイムの内容更新
  • エディタのカーソル位置との同期
動作デモ

リアルタイムの内容更新

このプラグインではキー入力の度にバッファの内容をブラウザに送信しています。
あたかもVSCodeの内蔵プレビューワを利用するかのように変更を確認することができます。

エディタのカーソル位置との同期

エディタのカーソル行のレンダリング結果がブラウザ画面の中央に表示されるようになっています。

作成の動機

Vimには既に素晴しいMarkdownレンダプラグインが存在しています。
いくつか種類がありますが、「依存関係の管理が楽」・「即座に変更を確認できる」・「カーソル位置との同期」が上手に動くプラグインはあまりありません。

https://github.com/iamcco/markdown-preview.nvim

当初は↑のプラグインを利用していた(とても素晴しいプラグインです)のですが、nodejsに依存してしまうことやうまく動作しないことがあることが不便でしたので、自分でプラグインを作成することにしました。

サーバに利用しているdenops.vim/Denoはnodejsのような面倒な依存関係の管理が必要なく、Denoのバイナリが一つあれば依存を自動で解決してくれるため非常に魅力的でした。

仕組み

ここからはプラグインの使用には関係のない話です。

本プラグインでは、ブラウザにバッファを送信するサーバとしてTypescriptランタイムであるDenoを利用しています。
また、Vimとの通信にはdenops.vimを利用しています。
denops.vimの詳細は省略します。

今回、denops.vimを利用するために初めてTypescriptを触ったのですが、型のついた言語はとても便利だとおもいました。

バッファの更新

autocmdでバッファの更新を検知し、バッファ全行をDenoに送信しています。
バッファはbuffer.ts(ソース)で管理しており、server.ts(ソース)はBufferに登録したcallback経由で更新の通知を受取ります。
将来的にdenops.vimにbufferモジュールが追加された際に備えてBufferからのイベントを更新に利用する形としました。

ブラウザとの同期

また、サーバからブラウザへの送信にはWebsocketを利用しています。
Denoのstdにはstd/wsがあるのですが、新しいstd/httpと一緒に上手く動作させることができなかったので、Denoのランタイムライブラリにあるhttpサーバを利用しました。(該当部分)

Markdownのレンダリング

MarkdownのレンダリングにはMarkdown-itを利用しています。
Markdownはブラウザー上でレンダリングされるようになっています。
iamcco/markdown-preview.nvimなどの大抵のレンダラではブラウザ側でレンダリングしていますが、本プラグインではDeno側でレンダリングしたHTMLを送信しています。
通信量が大きく増えてしまう欠点はあるものの、ページのロードが高速になる等の利点があったため現状はこの方式を採っています。

2022/11/23追記、https://github.com/kat0h/bufpreview.vim/pull/32 でブラウザーでレンダラーが動作するように修正していただきました。denoのnpmサポートを利用し、ESBuildでフロントエンドをビルドします。

画面位置の同期

画面位置との同期にはVimのCursorMovedCursorMovedIイベントを利用しています。
レンダリング時にHTMLに行番号の対応を埋め込んでおり、その情報からスクロールを制御しています。

この際、Markdown-itの都合上一部対応が欠損してしまうため、対応するHTML要素が見つからなかった場合は周りの位置からスクロール先を計算しています。
考慮漏れなどでバグらせている可能性があります...

Javascriptからのスクロール制御にはwindow.scrollTo()を利用したのですが、smooth-scrollの設定がSafariで利用できなかったのでPolyfillを利用しました。

サーバで利用するポート

port: 0を指定してサーバを作成しています。ポートはDeno側で自動的に割り当てられます。

今後

iamcco/markdown-preview.nvimの使い心地を目指して9/24頃から開発を初めましたが、ひとまず基本の機能を実装することができました。

Discussion

koheizikoheizi

有益なプラグインを作っていただきありがとうございます。
CORSエラーで動作しなかったのですが、chromeでは以下のプラグインを入れることで回避できました。
Cross Domain - CORS

既にご存知の内容とは思いますが、念の為、コメントさせていただきました。