Closed8

dduのSourceを作りたい

ピン留めされたアイテム
こまもか🦊こまもか🦊

もうかれこれ一年以上放置してしまったのと、検索すると結構上に出てきてしまうので役に立ちそうな情報を置いておく。


なんやかんやあって(WIPだけども)自作のddu sourceを作ることができた。
その際に参考になった記事と作った際の考え方を簡単に書いていく。

https://github.com/comamoca/memos.vim

参考になった記事

https://zenn.dev/shougo/articles/ddu-vim-make-plugins

作者さん直々に紹介している記事。実際のコードも交えつつ解説されていてkind等を作る際にも参考になった。

https://zenn.dev/vim_jp/articles/c0d75d1f3c7f33

Ddu.vimを俯瞰的にまとめた記事。直接sourceを作る話ではないけど、どのような方向性で作ればいいのか参考になった。

https://trap.jp/post/1870/

部内のSNSクライアントを作るという内容。実際のユースケースとしてどのようなsourceを作ればいいのか考える際に参考になった。

ddu sourceの書き方

基本的には「リスト状のなにか」を用意するスクリプトを書いていく。例えば、

  • ファイル
  • ディレクトリ
  • チャンネル

など。これらを収集し、dduが指定する形式に変換し出力するのがddu sourceの主な仕事になってくる。

以下は先述した自作ddu source memos.vimのコード。sourceを作った際には実質この範囲しかコードを書いていない。(他はAPIとの通信などのユーティリティ関数)

https://github.com/Comamoca/memos.vim/blob/17ba285a47adb61a706fa7b969694e5845636624/denops/%40ddu-sources/memos.ts#L27-L40

また、「自分が用意したsourceが選択された際に任意の挙動を起こさせたい」と考えている場合はkindを作成する。memos.vimの場合は選択された際に実際のファイルではなくサーバー状のデータを操作するので専用のバッファを用意している。また、:wなどの保存時の動作もAPIへの送信に変えている。

https://github.com/Comamoca/memos.vim/blob/17ba285a47adb61a706fa7b969694e5845636624/denops/%40ddu-kinds/memos.ts#L29

バッファの用意などはこちらで処理している。

https://github.com/Comamoca/memos.vim/blob/17ba285a47adb61a706fa7b969694e5845636624/denops/memos/main.ts#L29

また、memos.vimはメモをするという特性上日本語文字列を絞り込む事が多くなってくるけど、migemoを使って検索できるddu-filter-kensakuを絞り込み時に使用するよう設定することで、新規にプログラムを書くことなくその機能を実現している。

こんな感じで、ddu sourceを作る際には、「他のsource/kind/filterを組み合わせて機能を実現出来ないか」考えてから作るのがオススメ。ゆくゆくはmemos.vimをddu-ui-filerにも対応させたい。

こまもか🦊こまもか🦊

ddu.vimのSourceの作り方についてメモしていく
ある程度溜まったら記事にしたい

作りたいSource

  • ghq listの内容を読み込んであいまい検索するSource
  • kindはfileにする。(ディレクトリを開いてもmolderでファイル移動すれば良いと思っている)

まず初めに参考にするSourceをddu-source-lineに決めた。
このSourceは現在開いているファイルの列を検索出来るSourceで、行数もかなり短くシンプルなのでこれが良いと思った。

次に、どの部分でdduに検索対象を渡しているのか調べた。

const lines = await fn.getbufline(args.denops, bufnr, 1, "$");

おそらく上記の部分で対象の読み込みをしている。

そして次にfnについて調べた。これはddu.vimのdeps.tsから読み込まれていて、Denopsの標準ライブラリを読み込んでいる。つまり、これを読み込むと言うことはDenopsの標準ライブラリを読み込むと同義ということなのだろう。

読み込み先のURLにはこのような事が書いてある。

function, function/vim, and function/nvim are modules to provide functions of Vim and Neovim native functions.

(リスト下の引用がインデント付く謎)

Vim/Novimで使える関数はfn、Vim固有の関数はvim、Neovim固有の関数はnvimから呼び出せるらしい。

今回はfn.getbufline()なのでVim/Neovim双方が対応している関数を呼び出している事になる。
getbufline()

export async function getbufline(
  denops: Denops,
  expr: string | number,
  lnum: string | number,
  end?: string | number,
): Promise<string[]> {
  return await denops.call("getbufline", expr, lnum, end) as string[];
}

と定義されている。ddu-source-lineではargs.denops, bufnr, 1, "$"と指定されているので、

  • args.denopsはDenopsオブジェクト
  • bufnrは現在のバッファ番号
  • 1は(おそらく)バッファの先頭
  • "$"はバッファの後尾

ということになる(だろう)。また戻り値は関数名の通り実際のバッファの内容だ。

以上のことからgetbuflineにリスト形式でデータを流し込めば検索が出来そうという事が分かった。

こまもか🦊こまもか🦊

Sourceが書かれているクラスの概要がなんとなく理解ってきたのでメモ

export class Source extends BaseSource<Params> {
  kind = "file";

  gather(args: {
    denops: Denops;
    context: Context;
    sourceParams: Params;
  }): ReadableStream<Item<ActionData>[]> {
    return new ReadableStream({
      async start(controller) {
        const bufnr = args.context.bufNr;
        const lines = await fn.getbufline(args.denops, bufnr, 1, "$");
        controller.enqueue(lines.map((line, i) => {
          return {
            word: line,
            action: {
              bufNr: bufnr,
              lineNr: i + 1,
            },
          };
        }));

        controller.close();
      },
    });
  }

これはddu-source-lineのコードの一部だ。ここから飛べる

まず初めの行の

export class Source extends BaseSource<Params>

でベースになるSourceクラスを継承したクラスを作成し、それを外部に公開している。こうすることでddu.vimがファイルを読み込んだ時に実行出来るようになるのだろう。

次のkind = "file";で選択されたときに実行されるkindを実行している。filekindは恐らく選択された要素をファイルとして開いてくれるのだろう。

真ん中あたりのコードは既に述べているので飛ばして、最後の

return {
            word: line,
            action: {
              bufNr: bufnr,
              lineNr: i + 1,
            },

ここは実際に出力するコンテンツと選択された要素をどのような方法で返すのかを指定している。
wordに表示するコンテンツ、actionに返す値を指定する。
Sourceについて調べている時に選択された要素をそのまま解釈して実行していたらUIが見づらくなるなと感じていたのでその疑問もここで解決した。うまく出来ているなと感じた。

こまもか🦊こまもか🦊

また、今回は外部のコマンドから値を取得しているのだが、とても良い例を見つけたので紹介する。
ddu-source-man

このSourceはmanページを選択して開けるSourceである(実際に使用していなので推測ではあるが)
またこのSourceではkindも組み込まれているのでkindを作ろうとしている人の参考にもなるだろう。

さて、このSourceの今回の要はここだ

async function getOutput(
  cmds: string[],
  cwd?: string,
): Promise<string[]> {
  try {
    const proc = Deno.run({
      cmd: cmds,
      stdout: "piped",
      stderr: "piped",
      cwd: cwd,
    });
    const [status, stdout, stderr] = await Promise.all([
      proc.status(),
      proc.output(),
      proc.stderrOutput(),
    ]);
    proc.close();

    if (!status.success) {
      console.error(new TextDecoder().decode(stderr));
      return [];
    }
    return (new TextDecoder().decode(stdout)).split("\n");
  } catch (e: unknown) {
    console.error(e);
    return [];
  }
}

ここではDeno.run()を実行して出力された文字列をsplit()してリスト状にして返している。
Deno.run()についてはここが分かりやすいだろう。

こうして書いた関数をawaitを使って呼び出せばリストを渡すことが出来る。関数の戻り値を`Promise<string[]>にするのを忘れずに。

こまもか🦊こまもか🦊

https://twitter.com/Comamoca_/status/1622223127672668161?s=20

なんやかんやあって任意の文字列リストを表示させることに成功した
ddu-source-custom-listをベースに書いた(初めの投稿と言ってることが全然違う)

最小限の構成は以下の通り

import { BaseSource, Item } from "https://deno.land/x/ddu_vim@v2.0.0/types.ts";
import { Denops } from "https://deno.land/x/ddu_vim@v2.0.0/deps.ts";

type ActionData = {
  text: string;
};

type Params = {
  // texts: string[];
};

const texts = ["Hi", "this", "is", "my", "ddu", "source!"];
export class Source extends BaseSource<Params> {
  kind = "file";

  gather(args: {
    denops: Denops;
    sourceParams: Params;
  }): ReadableStream<Item<ActionData>[]> {
    return new ReadableStream({
      start(controller) {
        controller.enqueue(texts.map((text) => {
          return {
            word: text,
            action: {
              text: text,
            },
          };
        }));

        controller.close();
      },
    });
  }

  params(): Params {
    return {
      texts: [],
    };
  }
}
こまもか🦊こまもか🦊

これはtextsに格納されている文字列リストを表示させる単純なsourceになっている
kindにfileを指定しているけど、Enterを押しても何も起こらないので色々調べてみる。どうもActionDataが怪しい

こまもか🦊こまもか🦊

色々いじってみた結果、何も起こっていないのではなくただファイルが作られている事に気が付いていなかっただけだった...
ActionDataで任意のファイルが開けそうなのでこの調子で色々いじってみる

このスクラップは2ヶ月前にクローズされました