Closed9

NeovimでDenoのファイル保存時にDeno fmtを走らせる

shuntakashuntaka
  1. 編集中のファイルがDenoのプロジェクトか再起的にディレクトリ走査して確かめる
  2. coc.nvimで、cocで現状動作しているLSPの状態を取得し、DenoのLSPが動いている場合はフォーマットをかけるようにする

最初は1で考えていたが、2を思いついたので路線変更

これはcoc.nvimの設定でtsserverをOFFにできるから

{
  "deno.enable": true,
  "deno.lint": false,
  "deno.unstable": true,
  "deno.config": "deno.json",
  "tsserver.enable": false,
  "prettier.enable": false
}
shuntakashuntaka

返却される値が存在しない場合は、null。値の定義が配列の場合配列でそのまま返却される。

  const result = await vars.g.get<string[]>(denops, pVals);

供養コード

const denoFmtFiles = await getDenoFmtFiles(denops);

const getDenoFmtFiles = async (denops: Denops): Promise<string[]> => {
  const result = await vars.g.get<unknown>(denops, pVals.denoRuntimeFiles);

  if (isStringArray(result)) {
    return result;
  } else {
    return [];
  }
};

export const isStringArray = (value: unknown): value is string[]  => {
  return Array.isArray(value) && value.every(item => typeof item === 'string');
}
shuntakashuntaka

CocのStatus

[
  {
    "id": "languageserver.golang",
    "state": "init",
    "languageIds": ["go"]
  },
  {
    "id": "languageserver.ccls",
    "state": "init",
    "languageIds": ["c", "cc", "cpp", "c++", "objc", "objcpp"]
  },
  {
    "id": "eslint",
    "state": "running",
    "languageIds": []
  },
  {
    "id": "tsserver",
    "state": "init",
    "languageIds": ["typescript", "typescriptreact", "typescript.tsx", "typescript.jsx", "javascript", "javascriptreact", "javascript.jsx"]
  },
  {
    "id": "deno",
    "state": "starting",
    "languageIds": ["json", "jsonc", "markdown", "javascript", "javascriptreact", "typescript", "typescriptreact"]
  }
]
shuntakashuntaka

これで、denoのLSPの取得状態は取れそう

import { Denops } from "https://deno.land/x/denops_std@v5.0.1/mod.ts";
import * as fn from "https://deno.land/x/denops_std@v5.0.1/function/mod.ts";

type CocActionResult = {
  id: string;
  state: "init" | "starting" | "runnnig";
}[];

export const main = async (denops: Denops): Promise<void> => {
  denops.dispatcher = {
    echo: async (): Promise<void> => {
      const lspList =
        (await fn.call(denops, "CocAction", ["services"])) as CocActionResult;
      const isRunningDenoLs = lspList.some((lsp) => lsp.id === "deno");

      console.log(`isRunningDenoLs: ${isRunningDenoLs}`);
    },
  };
};

shuntakashuntaka

この機能(deno fmt --check)いいな。今の処理全部無駄になりそう。

  1. echo -n "バッファの内容" | deno fmt - で結果を取得
  2. バッファの内容と1でdiff -uでdiffをとってサードパーティのdiff_parserを利用してdiffパース
  3. diffを元にバッファの書き換え

1の処理で今はまっており、文字列のエスケープ周りがdenoのソースコードとして、deno fmtに食わせるのとbashへのエスケープの2つ考える必要があって悩んでいた。

ここまでのバックアップ
import { Denops } from "https://deno.land/x/denops_std@v5.0.1/mod.ts";
import * as fn from "https://deno.land/x/denops_std@v5.0.1/function/mod.ts";
import * as batch from "https://deno.land/x/denops_std@v5.0.1/batch/mod.ts";
import { execPipedCmd } from "./shell.ts";

type CocActionResult = {
  id: string;
  state: "init" | "starting" | "runnnig";
}[];

type DiffItem = {
  beforeFileName: string;
  afterFileName: string;
  hunks: {
    header: {
      beforeStartLine: number;
      beforeLines: number;
      afterStartLine: number;
      afterLines: number;
    };
    lines: {
      text: string;
      mark: "nomodified" | "add" | "delete";
    }[];
  }[];
};

type DiffType = DiffItem[];

export const main = async (denops: Denops): Promise<void> => {
  denops.dispatcher = {
    fmt: () => {
      return bufDenoFmt();
    },
  };

  const bufDenoFmt = async () => {
    const lspList =
      (await fn.call(denops, "CocAction", ["services"])) as CocActionResult;
    const isRunningDenoLs = lspList.some((lsp) => lsp.id === "deno");

    console.log("start");
    if (!isRunningDenoLs) {
      return;
    }

    const curpos = await fn.getcurpos(denops);
    const bufnr = await fn.bufnr(denops, "%");

    const currentLines = (await denops.call(
      "getbufline",
      bufnr,
      1,
      "$",
    )) as string[];

    const cc = currentLines.join("\n");
    const fc = await execDenoFmt(cc);
    if (fc.stderr !== "") {
      console.log(`stderr ${fc.stderr}`);
      return;
    }

    const diff = await execDiff(cc, fc.stdout.trim());
    if (diff == null || diff.stdout == "" || diff.stderr !== "") {
      return;
    }
    console.log(`diff: ${diff.stdout}`)

    const pDiff = parse(diff.stdout) as DiffType;
    if (pDiff == null || pDiff.length == 0) {
      return;
    }
    console.log(`pdff: ${JSON.stringify(pDiff)}`)

    let promises: Promise<unknown>[] = [];

    const edited = {
      deleted: 0,
      added: 0,
    };

    for (const hunk of pDiff[0].hunks) {
      let lpos = hunk.header.beforeStartLine - edited.deleted + edited.added;

      for (let i = 0; i < hunk.header.beforeLines; i++) {
        if (hunk.lines[i].mark === "delete") {
          console.log(`del :${lpos}`);
          promises = [...promises, fn.deletebufline(denops, bufnr, lpos)];
          edited.deleted += 1;
        } else if (hunk.lines[i].mark === "nomodified") {
          lpos += 1;
        } else if (hunk.lines[i].mark === "add") {
          console.log(`add :${lpos - 1}`);
          promises = [
            ...promises,
            fn.appendbufline(denops, bufnr, lpos - 1, hunk.lines[i].text),
          ];
          edited.added += 1;
          lpos += 1;
        }
      }
    }

    await batch.collect(denops, () => promises);
    await fn.setpos(denops, ".", curpos);
  };
};

const execDiff = async (a: string, b: string) => {
  const diffCmd = [
    "diff",
    "-u",
    `<(echo '${escape(a)}')`,
    `<(echo '${escape(b)}')`,
  ];
  return await execPipedCmd(diffCmd.join(" "));
};

const execDenoFmt = async (content: string) => {
  const fmtCmd = [
    "echo",
    "-n",
    `'${escape(escape(content))}'`,
    "|",
    "deno",
    "fmt",
    "-",
  ];
  Deno.writeTextFile("a", fmtCmd.join(" "));

  return await execPipedCmd(fmtCmd.join(" "));
};

const escape = (text: string): string => {
  return text.replace(/'/g, "''").replace(/\\/g, "\\\\\\\\");
};
shuntakashuntaka

--check用のparserを自前で書かないといけないんだけどね

shuntakashuntaka

stdinってあんま使わないほうがいい気がした
理由は、エスケープ周り。

このスクラップは2023/11/07にクローズされました