1つのプラグインの歴史



それはすべて、 タグバー私のために機能しなくなったときに始まりました 。 プラグインがエラーでクラッシュしました。おそらく、現在のExuberant CtagsのバージョンはExuberantではありません。 ソースを少し調べてみると、最後の外部コマンドがエラーで終了していることがわかりました。v:shell_errorは -1を返しました 。 さらに掘り下げずにfzfをインストールしましたFzfはctrlpと同様に、ファイル、タグ、バッファーなどでファジー検索を実行できますが、後者とは異なり、マイナスなしではなくはるかに高速に動作します。 アプリケーションは端末で直接動作し、毎回入力コマンドの履歴を上書きします。 これは、たとえば、目的のタグを検索するときに、メインバッファーの右側にあるバッファー(一部のスクリーンキャストで判断して、 neovim )に検索結果を表示できないことも意味します。 崇高なものとは異なり、 fzfはファイル名にそれ以上の重みを付けません。そのため、私は期待していた結果がトップに表示されないことがよくありました。 その上、配色を調整する完全な自由の欠如は、一般に平均的なユーザーにとってはあまり重要ではありませんが、私にとっては細部への注意が増しています。 自由とは、少なくとも、通常の(通常の)テキストとクエリ文字列の色の区別を意味します。







これらすべてのことから、標準のディレクトリビューアーnetrwに似た独自のプラグインを作成する必要がありました 。 この経験が誰かに役立つかもしれないと信じて、私が遭遇した問題とそれらを解決する方法を説明します。







Vimスクリプト言語



まず、vimスクリプトで最初の一歩を踏み出している人のために短いツアーを提供したいと思います。 変数には接頭辞があります;あなたはすでにそれらのいくつかを見、書いています。 通常、プラグインは、 g:プレフィックスを持つグローバル変数を使用して構成されます。 プラグインを作成するときは、 s:プレフィックスを使用することが適切です。これにより、変数がスクリプト内でのみ使用可能になります。 関数の引数にアクセスするには、 a:プレフィックスを使用します。 接頭辞のない変数は、宣言された関数に対してローカルです。 プレフィックスの完全なリストは、次のコマンドで表示できます:help internal-variables







バッファを管理するための2つの非常に単純な関数、 getlinesetlineがあります。 彼らの助けを借りて、検索結果をバッファに貼り付けるか、クエリ値を取得できます。 名前から多くの場合それが何をするのかはっきりしているので、私は各関数の説明にはこだわらないでしょう。 この記事のほぼすべてのキーワードはドキュメントに記載されています。したがって、 :help getlineまたは:help setlineですが、全体像については、 セクションごとにグループ化されたすべての関数のリストを含むhelp function-listを参照することをお勧めします。







イベント







Vimはすぐに使用できる多くのイベントを提供しますが、独自のプラグインを作成する場合は、独自のイベントを作成する必要があります。 幸いなことに、これは非常に簡単に行われます。







//   CustomEvent autocmd User CustomEvent call ... //   ,   "No matching autocommands",         if(exists("#User#CustomEvent")) doautocmd User CustomEvent endif
      
      





オートロード







プラグインのすべての関数に、 finder#プレフィックスを設定します。 これは組み込みのvim autoloadメカニズムで、同じ名前の目的のファイルのruntimepathを検索します。 finder関数の呼び出しはruntimepath / finder.vimファイルにあり、 finderファイルのindex関数はruntimepath / finder / files.vimファイルにあります。 次に、プラグインをruntimepathに追加する必要があります。







 set runtimepath+=path/to/plugin
      
      





ただし、 vim-plugなど、これらの目的にはプラグインマネージャーを使用することをお勧めします。







複合チーム







多くの場合、チームが異なる部分から結合されるか、変数の値を挿入する必要がある場合に発生します。 これらの目的のために、vimには実行コマンドがあり、これは多くの場合、 printf関数とともに使用すると便利です。







 execute printf('syntax match finderPrompt /^\%%%il.\{%i\}/', b:queryLine, len(b:prompt))
      
      





始めましょう



したがって、必要なのはクエリ文字列と検索結果だけです。 入力関数はvimのユーザー入力を担当しますが、私が知る限り、入力行を一番上に配置することはできません。タグで検索することは、ファイルに表示されている順序でタグを表示する方が便利なので、これは非常に重要です。 さらに、時間が経つにつれて、netrwを示す同様の帽子を作ることにしました 。 入力行はバッファに実装する必要があり、ここで最初の問題が発生しました。







リクエスト







リクエストの値を取得するには、入力フィールドとツールチップに相対的なオフセットが配置されている行を知り、 TextChangedIイベントのハンドラーを設定する必要があります。 以前にプログラミングしたことがある人は、この段階で複雑なものは何もないはずなので、コードは省略します。 ハンドラーを<buffer>属性でハングアップする必要があることを追加します。







 autocmd TextChangedI <buffer> call ...
      
      





プロンプト







ツールチップはユーザー入力と同じ行にあるため、何らかの方法で修正する必要があります。 これらの目的のために、<BS>や<Del>などのキーの動作を担当するバックスペースオプションの値をクリアすることができます。 特に、私はeolstartにのみ興味がありました。 Eolは行末記号の削除を許可し、それに応じて文字列のマージを許可しますが、startは挿入モードの開始後に入力されたテキストのみの削除を許可します。 それは非常に便利で簡単に判明しました。たとえば、「Files>」というプロンプトを挿入し、テキストを入力し始めると、テキストを削除してもプロンプトはそのまま残りました。 確かに、私は1つの点を考慮しませんでした-そのようなプラグインの操作のために、多くのロジックが必要であり、通常モードに行くことは一般的な習慣でした。 どのマッピングでも新しい「セッション」を簡単に開始でき、以前に入力されたテキストは削除されなくなりました。 たとえば、<Esc>を押すだけでした。







 inoremap <Cj> <Esc>:call ...
      
      





<BS>のマッピングを作成し、テキストを手動で削除する必要がありました。







 inoremap <buffer><BS> <Esc>:call finder#backspace()<CR>
      
      





カーソルの奇妙なちらつきがいくつかあり、時間が経つにつれてひどく迷惑になりました。 障害がコマンドラインモードへの移行であることに気づくまでに長い時間がかかりました通常は、 を押して開始します。 この瞬間、テキストの上のカーソルが消えます。 ちらつき効果は強く、呼び出される関数は「より重い」。 TextChangedIイベントでハンドラーをハングさせる試みがあり、現在のカーソル位置を確認しました。カーソルがツールチップに危険なほど近づいた場合、必要なのは<BS>が何もしないようにすることだけでした。 残念ながら、時々1文字が削除されます。 しばらくして、解決策が見つかりました-属性<expr>







 map {lhs} {rhs}
      
      





{rhs}は有効な式(:help expression-syntax)であり、その結果はバッファーに挿入されます。 <BS>や<Ch>などの特殊キーは二重引用符で囲み、 \ (:help expr-quote)文字でエスケープする必要があります。







 inoremap <expr><buffer><BS> finder#canGoLeft() ? "\<BS>" : "" inoremap <expr><buffer><Ch> finder#canGoLeft() ? "\<BS>" : "" inoremap <expr><buffer><Del> col(".") == col("$") ? "" : "\<Del>" inoremap <expr><buffer><Left> finder#canGoLeft() ? "\<Left>" : ""
      
      





出口







バッファーを終了するには、<Esc>をバインドできます。 不快な瞬間は、いくつかのキーの組み合わせが<Esc>と同じ文字のシーケンスで始まることです。 入力モードに入り、<Cv>を押してから矢印のいずれかを押すと、 ^ [がプレフィックスとして表示されます。 たとえば、左矢印の場合、端末は^ [ODを vimに送信します。 したがって、矢印または<S-Tab>を押すと、vimは<Esc>によって割り当てられたアクションを実行し、残りの文字を解釈しようとします。左矢印の場合、これは上部(O)に空行を挿入し、同じ文字に大文字の「D」を挿入します行。 esckeysオプションは、シーケンスが入力モードで<Esc>で始まる場合、つまり^ [で始まる場合、新しい文字が到着することを期待するかどうかを示します。 何が必要なように見えますが、<Esc>の動作を変更しない場合にのみ機能します。







-

これはあなたのジョーク、親愛なるIDEユーザーかもしれません。







おそらく何かを見逃したかもしれませんが、さまざまなリソースでこのキーの動作を変更しないことをお勧めすることは無駄ではありません。 <S-Tab>がそれほど重要でない場合は、矢印をバインドして次/前の出現を選択するとよいでしょう。 したがって、<Esc>の代わりに、 InsertLeaveイベントを使用します。 これには新たな課題が伴います。 入力モードを終了せずに関数を呼び出す方法は? ドキュメントを読んでいると、興味深い点が1つ見つかりました。InsertLeaveイベントを発生させずに挿入モードを終了する<Ctrl-c>の組み合わせですが、マッピングに<Cc>が存在する場合、これは機能せず、 InsertLeaveがポップアップします。 インターネットをサーフィンしているときに、一般的な見解を示す解決策が見つかりました。







 inoremap <BS> <Cr>=expr<CR>
      
      





ドキュメントから、これは式レジスタであることがわかります 。 式の結果がバッファに挿入されたことを除いて、これはまさに私が必要としたものです。 実際、すべての身体の動きは挿入モードで発生するため、これはプラグイン全体が構築される場所です。 各関数で空の文字列を返さないように(これが行われない場合、関数は0を返します)、目的の関数を呼び出す中間体を使用することにしました。







 function! finder#call(fn, ...) call call(a:fn, a:000) return "" endfunction
      
      





a:名前空間は関数の引数へのアクセスを担当し、 a:000変数にはオプションのパラメーターのリストが含まれます。 入力モードを終了せずにアプリケーションロジックを記述できるようになったため、 backspaceオプションを使用できます。 ただし、後で学んだように、このオプションの値をリセットするとdelimitMateが怒り 、そのため正常に機能しなくなり、これらの試みを中止することにしました。







バックエンド







ただ何もありません、私たちはすでにたくさんの無駄なピクセルを持っています。 バッファに寿命を追加します。 vimスクリプトを高速言語や複雑なものを書くのが良い言語と呼ぶのは難しいので、Dでバックエンドを書くことにしました。ファジー検索が必要なので 実装するのが面倒 必要ありません。正確な発生を考慮した検索です。ソース文字列とユーザーのリクエストを文字ごとに比較することを決定しました。これは正規表現を使用するよりもはるかに高速だと信じていました。 実際に4つのモードがあったことを考えると、^クエリ、クエリ、クエリ$、^クエリ$、コードは少し魅力的ではありませんでした。 私が書いたものを見て、すべてを削除して定期的に検索したいという要望がありました。 しばらくして、私は自分が書いたものが標準のUnixツールを使用して実行できることに気づき、最初から考えていたgrepの使用に戻ることにしましたが、「複雑な」ロジックの存在により破棄しました。 問題は、ファイル名で検索し、ファイルパスの長さで並べ替え、元の文字列ではなく、そのインデックスを表示する必要があることでした。 UnixのgrepがDにあるstd.regexよりも4倍高速であることが判明したことは注目に値します。







  1. ファイル名を取得するには、プログラムbasenameを使用できますが、残念なことに、標準入力ストリームを読み取らず、パラメータを直接操作するだけです。 sed 's !. * / !!'も使用できます すべてを最後までカットします。 vim組み込み関数fnamemodifyも適しています。







  2. 独自の拡張機能を実装して作成する方が簡単なので、vimツールを使用して並べ替えを行うことにしました。 sort関数は、 コンパレータを記述する必要があるソートを担当します。







  3. インデックスを表示するには、 grep-nフラグを使用します。これは、行番号を表示します。形式はn:lineであり、解析が容易です。


カーソルのちらつき







一般的に、これは私がかなり嫌いなことです。 カーソルのちらつきは、 incsearchオプションを設定することで確認できます。 バッファ内で何かを探して、入力中にカーソルをたどってください。 <BS>の動作が変更されてすべてが明確になった場合、どこにでも<expr>を書き込むことはできません。 このフラグは、カーソルが置かれている行以外の行の変更を禁止します。 したがって、ロジックの残りの部分では、上記の式レジスタが使用されます 。これは、 :のように、式の期間中カーソルを現在の位置から削除します。 数千のファイルの検索には時間がかかるため、各文字を印刷するときにカーソルが点滅する効果が現れます。 非ブロッキングvim、特にtimer_start関数が時間内に到着したことを言わなければなりません。 バッファが非同期的に描画され始めたとき、問題はなくなりました。 最良の解決策ではない、と言わざるを得ないが、より適切なものは見つけられなかった。 これが、プラグインがvim 8バージョンを必要とする唯一の理由です。







プレビューをしなければならなかったときに、問題が3回目に追いついた。 いずれかのバッファのカーソル位置が変更され、画面が描画された瞬間にカーソルが点滅しました。 「カーソルの下のキャラクターを強調する構文」が不可欠であり、そのような詐欺をより良い時まで残すことに決めました。







痕跡に気づく







バッファーの内容は絶えず変更されているため、 タブラインはバッファーが変更されたこと、および入力モードでの操作に対応する左下の表記が表示されることを示します。 あなたのことは知りませんが、私はミニマルなデザインが好きです。そのようなものを削除したいと思います。 また、 ルーラーstatuslineを非表示にすると便利です。 vimがバッファ内の変更を監視しないようにするには、 buftypeオプションを使用できます。







 setlocal buftype=nofile
      
      





ステータスライン、ルーラー、碑文-- INSERT --



では、表示を担当するオプションがグローバルであるため、少し複雑です。つまり、バッファーを終了するときに以前の値を復元する必要があるためです。 OptionSetイベントをリッスンすると便利です。







 set noshowmode set laststatus=0 set rulerformat=%0(%) redraw
      
      





rulerformatの代わりにnorulerを使用することもできますが、後者は事前のクリーニング(再描画!)で画面を再描画する必要があり、これは目に不快な影響を与えます。







構文



ここで、プラグインの操作で重要な役割を果たした構文に関する最も重要なポイントを要約したいと思います。







アイテム 説明
\ c \ c。* 検索時に大文字と小文字を区別しません。
。{-} 。{-} p 欲張りな相手ではありません
\ zs\ ze 。{-} \ zsfoo \ ze。* エントリの最初と最後。
\ @ =\ @ <= \(非表示の\)\ @ <=テキスト いわゆるゼロ幅アトム -エントリから前のアトムを切り取ります。
\%l \%1l 特定の行を検索します。
\& p1 \&p2 \&.. 接続詞演算子。


ベースネーム







ファイル名を使用しているため、エントリの強調表示を制限する領域を設定する必要があります。







 \(^\|\%(\/\)\@<=\)[^\/]\+$
      
      





config / foob​​ar.php

foob​​ar.php







次に、目的の文字を強調表示する必要があります。 これらの目的のために、結合演算子\& (:help branch)を使用できます。







 \(^\|\%(\/\)\@<=\)[^\/]\+$\&.\{-}f
      
      





config / f oobar.php

f oobar.php







ヒント :これは通常のパターン (:help pattern)であるため、 /を押すことにより、個別のバッファーですべてをテストできます。







コメント







fzfには便利な視覚的機能があります -検索結果、つまりコメントに影響を与えないテキストの存在。 最初は、コメントの始まりを示すために、ある種の不可視のUnicode文字を使用したかった(明らかな理由により、スペースが収まらない)が、後に構文グループ-concealの有用なプロパティに出会いました。 要するに、 隠蔽はテキストを隠し、バッファに残します。 隠蔽動作には、 conceallevelconcealcursorの 2つのオプションがあります。 特定の設定では、テキストが非表示にならない場合がありますので、読むことをお勧めします。 私のプラグインでは、行は次のとおりです。







 text#finderendline...
      
      





ここで、 ...はオプションのコメントで、 #finderendline非表示です。 隠しテキストの例:







 syntax match hidden /pattern/ conceal
      
      





スクロール







入力モードでプラグインを実行すると、多くの問題が発生します。その1つはスクロールです。 リクエストの入力位置にカーソルが必要なので、カーソルを移動して目的の行を強調表示することはできません。 検索結果をナビゲートするには、適切なグループを作成して構文を使用できます。 通常の原子\%lは完全に適合します。 たとえば、 \ ^%2l。* $ 2行目が強調表示されます。







私の画面には63行のテキストが含まれており、さらに多くのエントリが存在する可能性があるため、64行目以降に到達する方法についての疑問が生じます。 ヘッダーとクエリ行は常に画面の表示部分にあるはずなので、画面の最後に近づくと、最後に達するまで最初(2番目、3番目、...)の発生を切り取ります(一時配列に入れます)。 上に移動するとき-すべてがまったく逆です。







まとめ



この記事の存在下では、vimはほのめかしています- 入力を使用する必要がありましたが、すべてがすでに遅れている場合、私は非標準的な方法で行って、そのような貴重な経験を得たことがうれしいです。 それだけです 。独自の拡張機能のインストール、使用、作成に関する情報はリポジトリで見つけることができます







有用なささいなこと



プラグインを書いた後に特定のベースを受け取ったので、私は私の人生を少し簡素化したかったです。







挿入モードを終了







私の前の記事を読んだ人は、私が以前に崇高なものを使ってテキストとコードを編集したことを知っています。 sublimeとvimには、キーボードショートカットの処理方法に大きな違いがあります。 崇高な場合、組み合わせを入力するときに遅延なしでテキストを挿入し、vimは最初に特定の時間を期待し、組み合わせが「壊れる」場合にのみ目的の文字を挿入します。 一般的にvimモード、特にvimモードを使用する最初から、 dfを使用して挿入モードを終了しました。 たとえばjjでの再トレーニングの試みは失敗するほど習慣的になりました。 df以外の文字を入力するたびに、不快な動きが観察されました。 私は崇高な振る舞いを繰り返すことにしました。







 let g:lastInsertedChar = '' function! LeaveInsertMode() let reltime = reltime() let timePressed = reltime[0] * 1000 + reltime[1] / 1000 if(g:lastInsertedChar == 'd' && v:char == 'f' && timePressed - g:lastTimePressed < 500) let v:char = '' call feedkeys("\<Esc>x") endif let g:lastInsertedChar = v:char let g:lastTimePressed = timePressed endfunction autocmd InsertCharPre * call LeaveInsertMode()
      
      





これは最良のコードではないかもしれませんが、タスクを実行します。 一番下の行は、 dを押した後、 fを押すのに0.5秒かかります。 後者が真の場合、 fは出力されず、 dはバッファから削除されます。 その後、エディターは通常モードになります。







読み取り専用ファイル







残りのマイナーな追加は、特定のファイルの編集の禁止になります。







 function! PHPVendorFiles() let path = expand("%:p") if(stridx(path, "/vendor/") != -1) setlocal nomodifiable endif endfunction autocmd Filetype php call PHPVendorFiles()
      
      





このコードは、 ベンダーディレクトリにある場合、 .phpファイルの編集を禁止します。







追記



最初の記事の発行以降の私の環境の変更のリスト。










All Articles