5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

よりそうAdvent Calendar 2023

Day 10

入門:denops.vimでNeovimプラグインを作ろう

Last updated at Posted at 2023-12-09

きっかけ

Neovim向けのプラグインを作ってみたいけど
Vim scriptもLuaもよくわからん!!!!

TypeScriptなら書ける! => Denops.vimで作ってみよう

Denops.vimについて

denops.vim とは
denops.vim は JavaScript/TypeScript のランタイムである Deno を利用して Vim/Neovim 双方で動作するプラグインを作るためのエコシステムです。

環境構築についても詳しく書いてあるので助かりました。

成果物

今後、Neovimを触っているときに突然雪を見たくなる日があるかも知れません。
なので雪を降らせました。
作りたいものが思いつかなかったわけではありません

Videotogif (1).gif

コード全体

main.ts

import { Denops } from "https://deno.land/x/denops_std@v5.0.2/mod.ts";
import { colors, createSnowWindow, highlightGroups } from "./snow.ts";

export async function main(denops: Denops): Promise<void> {
  for (let i = 0; i < highlightGroups.length; i++) {
    await denops.cmd(
      `hi ${highlightGroups[i]} guifg=${colors[i]} guibg=NONE gui=NONE`,
    );
  }
  denops.dispatcher = {
    async snowfall(): Promise<void> {
      const winId = await createSnowWindow(denops);
      setTimeout(async () => {
        await denops.call("nvim_win_close", winId, true);
      }, 10000);
    },
  };

  await denops.cmd(
    `command! Snowfall call denops#request("${denops.name}", "snowfall", [])`,
  );
}

snow.ts

import { Denops } from "https://deno.land/x/denops_std@v5.0.2/mod.ts";

type options = {
  speed: number;
  chars: string[];
  count: number;
  size: {
    w: number;
    h: number;
  };
};

type flake = {
  speed: number;
  velY: number;
  x: number;
  y: number;
  char: string;
  highlightGroup: string;
};

export const highlightGroups = ["SnowFlake1", "SnowFlake2", "SnowFlake3"];
export const colors = ["#aecacf", "#d8bfd8", "#f0f8ff"];

export async function createSnowWindow(denops: Denops): Promise<number> {
  const maxWidth = await denops.eval("max([winwidth(0), 80])") as number;
  const maxHeight = await denops.eval("max([winheight(0), 24])") as number;

  const width = 80;
  const height = 24;

  const col = Math.floor((maxWidth - width) / 2);
  const row = Math.floor((maxHeight - height) / 2);

  const bufnr = await denops.call("nvim_create_buf", false, true) as number;
  const windid = await denops.call("nvim_open_win", bufnr, true, {
    relative: "win",
    width: width,
    height: height,
    col: col,
    row: row,
    style: "minimal",
  }) as number;
  const options: options = {
    speed: 0.5,
    chars: [".", "*", "+", "", "", ""],
    count: 100,
    size: {
      w: width,
      h: height,
    },
  };
  const flakes = initializeFlakes(options);
  let keepSnowing = true;
  const snow = () => {
    if (!keepSnowing) {
      return;
    }
    updateFlakes(flakes, options);
    drawFlakes(denops, bufnr, flakes, options);
    setTimeout(snow, 100);
  };
  snow();

  setTimeout(() => {
    keepSnowing = false;
  }, 10000);

  return windid;
}

function initializeFlakes(options: options): flake[] {
  const flakes: flake[] = [];
  for (let i = 0; i < options.count; i++) {
    const x = Math.floor(Math.random() * options.size.w),
      y = Math.floor(Math.random() * options.size.h),
      char = options.chars[Math.floor(Math.random() * options.chars.length)],
      speed = Math.random() * 1 + options.speed;
    const highlightGroup =
      highlightGroups[Math.floor(Math.random() * highlightGroups.length)];
    const flake: flake = {
      speed: speed,
      velY: speed,
      x: x,
      y: y,
      char: char,
      highlightGroup: highlightGroup,
    };
    flakes.push(flake);
  }
  return flakes;
}

function updateFlakes(flakes: flake[], options: options) {
  for (const flake of flakes) {
    flake.velY = flake.speed;
    flake.y += flake.velY;

    if (
      flake.y >= options.size.h || flake.y <= 0
    ) {
      resetFlake(flake, options);
    }
  }
}

async function drawFlakes(
  denops: Denops,
  bufnr: number,
  flakes: flake[],
  options: options,
) {
  const screen: string[][] = Array.from(
    { length: options.size.h },
    () => new Array(options.size.w).fill(" "),
  );
  for (const flake of flakes) {
    const x = Math.floor(flake.x);
    const y = Math.floor(flake.y);
    if (y >= 0 && y < options.size.h && x >= 0 && x < options.size.w) {
      screen[y][x] = flake.char;
    }
  }
  const lines = screen.map((row) => row.join(""));
  await denops.call("nvim_buf_set_lines", bufnr, 0, -1, false, lines);

  for (const flake of flakes) {
    const x = Math.floor(flake.x);
    const y = Math.floor(flake.y);
    if (y >= 0 && y < options.size.h && x >= 0 && x < options.size.w) {
      await denops.call(
        "nvim_buf_add_highlight",
        bufnr,
        -1,
        flake.highlightGroup,
        y,
        x,
        x + 1,
      );
    }
  }
}

function resetFlake(flake: flake, options: options) {
  flake.x = Math.floor(Math.random() * options.size.w);
  flake.y = 0;
  flake.char = options.chars[Math.floor(Math.random() * options.chars.length)];
  flake.speed = Math.random() * 1 + options.speed;
  flake.velY = flake.speed;
}

学んだこと

main.ts

import { Denops } from "https://deno.land/x/denops_std@v5.0.2/mod.ts";

Denoの標準ライブラリ(denops_std)をインポートします。

denops.dispatcher = {
    async snowfall(): Promise<void> {
      const winId = await createSnowWindow(denops);
      setTimeout(async () => {
        await denops.call("nvim_win_close", winId, true);
      }, 10000);
    },
  };

await denops.cmd(
    `command! Snowfall call denops#request("${denops.name}", "snowfall", [])`,
  );

denops.dispatcherを設定して、Vimからのコマンドに応答するようにします。
今回はSnowFallコマンドが実行されると、createSnowWindow関数を呼び出し、設定された時間(ここでは10秒)後にウィンドウを閉じます。

snowFlake.ts

バッファの作成

const bufnr = await denops.call("nvim_create_buf", false, true) as number;

Neovimのnvim_create_buf関数を呼び出しています。

  • 第一引数のfalse
    • バッファがテキスト編集用でないことを意味しています。
  • 第二引数のtrue
    • バッファが一時的であることを指します。

この関数は新しいバッファの番号を返します。

const windid = await denops.call("nvim_open_win", bufnr, true, {
    relative: "win",
    width: width,
    height: height,
    col: col,
    row: row,
    style: "minimal",
  }) as number;

Neovim のnvim_open_win関数を呼び出しています。

  • 第一引数のbufnr
    • 新しく作成したバッファの番号を指しています。
  • 第二引数のtrue
    • ウィンドウがフォーカスを受け取るかどうかを指します。
  • 第三引数
    • ウィンドウの位置やサイズ、スタイルを設定するオブジェクトです。
    • relative
      • "win" はウィンドウの位置を現在のウィンドウの相対的な位置で指定することを意味します。
    • widthとheight
      • ウィンドウの幅と高さを指定します。
    • colとrow
      • ウィンドウを配置する列と行を指定します。
    • style
      • "minimal" はウィンドウのスタイルを最小限にすることを意味し、余分なUI要素(ステータスライン、行番号など)を隠します。

新しく作成したウィンドウのID(windid)を返します。
これにより、Neovim内に新しい描画用の領域(ウィンドウ)が作成され、そのウィンドウ内に雪のアニメーションが表示されるようになります。

await denops.call("nvim_buf_set_lines", bufnr, 0, -1, false, lines);

  for (const flake of flakes) {
    const x = Math.floor(flake.x);
    const y = Math.floor(flake.y);
    if (y >= 0 && y < options.size.h && x >= 0 && x < options.size.w) {
      await denops.call(
        "nvim_buf_add_highlight",
        bufnr,
        -1,
        flake.highlightGroup,
        y,
        x,
        x + 1,
      );
    }
  }

バッファに行をセットする

Neovimのnvim_buf_set_linesを呼び出しています。

  • 第一引数のbufnr
    • 操作するバッファの番号です。
  • 第二引数の0と第三引数の-1
    • 操作する行の範囲を指定しており、ここではバッファの最初から最後までを意味します。
  • 第四引数のfalse
    • バッファの変更を通知しないことを指定します。
  • 第五引数のlines
    • バッファにセットする行の内容を含む配列です。これには画面に表示される雪の粒の位置が含まれます。

雪の粒にハイライトを適用する

各雪の粒(flake)に対してハイライトを設定してキラキラさせて映えさせたいと考えました。

Neovimのnvim_buf_add_highlightを呼び出して、特定の雪の粒にハイライトを適用します。

  • 第一引数のbufnr
    • 操作するバッファの番号です。
  • 第二引数の-1
    • ハイライトグループのID
  • 第三引数のflake.highlightGroup
    • 適用するハイライトグループの名前です。
  • y,x,x+1
    • ハイライトを適用する範囲を指定しており、ここでは雪の粒の位置を指します。

これにより、画面上に雪の粒が描画され各粒にハイライトが適用されます。

感想

簡単に作れてすぐに試すことができ、開発体験はかなりよかったです。

今後は自作プラグインを公開できるように引き続き学習を続けていきたいと思います。

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?