Wagomu no Akibako

闇の力への入門 ddc.vim編


この​記事はVim駅伝2023年09月22日(金)の​記事です。

前回の​記事は​ kawarimidoll さんの​「Vimで​インデント幅の​単位で​左右移動する」と​いう​記事でした。

次回の​記事は​ 9月25日(月) に​投稿される​予定です。


はじめに

こんに​ちは!​次の​式年遷宮は​暗黒美夢王(以下​Shougoさん)の​新規開発中の​プラグインマネージャーdpp.vimが​完成時に​なるだろうと​思っている​輪ごむです。

この​記事は​闇の​プラグインである​ddc.vim(Dark deno-powered completion)に​luaと​tsで​入門した​作業ログ的な​記事です。

環境

OS:m1 mac
Terminal:WezTerm
Editor:Neovim
Plugin Manager:Lazy.nvim
:version
NVIM v0.10.0-dev-4258f4d
Build type: RelWithDebInfo
LuaJIT 2.1.1694316387
Run ":verbose version" for more info

補完プラグインに​求める​こと

  • 周辺文字列や​単語の​補完
  • lspとの​連携
  • コマンドライン補完​(履歴補完も​含む)

要件を​整理すると、​非常に​シンプルでした。​この​要望を​ただ満た​すためで​あれば、​今まで​使っていた​vim-cmpの​ほうが​簡単で​直ぐに​使えます。

ただ、​男の​子は​闇の​力に​憧れを​抱く​ものです。​最近​tsでも​設定できると​聞いた​ことも​あり、​闇の​力へ​入門する​こととなりました。

ソースの​選定

以下の​ソースを​使います

他の​ソースを​探したい​人は​以下の​リンクを​参照してください。
ddc-source · GitHub Topics · GitHub

フィルターの​選定

以下の​フィルターを​使います。​いまいち理解できていないので、​各フィルターの​説明は​割愛します。

他の​フィルターを​探したい​人は​以下の​リンクを​参照してください。
ddc-filter · GitHub Topics · GitHub

い​ざ設定開始!

使いたい​ソースと​フィルターが​そろったので、​設定を​書き始めます。​Shougoさんの​プラグインの​設定を​書くに​あたって必要な​ものあると​便利な​ものが​あります。
必要な​ものは、​導入した​プラグインの​ヘルプです。
あると​便利な​ものは​Shougoさん本人の​dotfiles通称リファレンス実装と​言われている​ものです。​導入した​プラグインの​ヘルプを​読むことは​勿論大事ですが、​リファレンス実装を​読むことで​それぞれの​機能が​どう​使われているのか等の​大枠を​ぱっと​確認できて​非常に​有用です。

tsファイル読み込みで​詰まった

これは​リファレンス実装を​頼りにしすぎた​ことと、​Vimscriptに​関する​知識不足に​よって​発生した​問題なのですが、​ddc#custom#load_config関数で​tsファイルが​読み込めませんでした。​Shougoさんは​Vimscriptで​tsファイルの​ある​ディレクトリを​環境変数と​して​利用していました。​しかし、​自分が​luaで​設定を​書いているのが​影響したのかvim.fs.joinpath関数内で​環境変数が​展開されず、​うまく​読み込むことができないと​いう​問題が​起こりました。
この​問題は、​stdpath(‘config’)起点で​パスを​結合させる​ことに​よって​解決しました。

-- utils.lua
M.plugins_path = vim.fs.joinpath(vim.fn.stdpath("config"), "lua", "plugins")

-- ddc/init.lua
vim.fn["ddc#custom#load_config"](vim.fs.joinpath(require("utils").plugins_path, "ddc", "ddc.ts"))

その​際には​vim-jpの​皆様に​大変お世話に​なりました。

参考に​しつつカスタマイズ

tsファイルの​読み込みさえできてしまえば、​あとは​リファレンス実装と​ヘルプを​頼りに​自分の​好きなように​設定していくだけです。

全体の​ソースコード

luaと​tsファイルは​同一階層に​配置しています。

init.lua
function _G.CommandlinePre(mode) 
  vim.b["prev_buffer_config"] = vim.fn["ddc#custom#get_buffer"]()
  if mode == ":" then
    vim.fn["ddc#custom#patch_buffer"]("sourceOptions", { _ = {keywordPattern = "[0-9a-zA-Z_:#-]*", minAutoCompleteLength = 2, }})
  end

vim.api.nvim_create_autocmd("User", {
pattern = "DDCCmdlineLeave",
callback = function()
if vim.fn.exists("b:prev_buffer_config") then
vim.fn["ddc#custom#set_buffer"](vim.b["prev_buffer_config"])
vim.b["prev_buffer_config"] = nil
end
end,
once = true,
})

vim.fn["ddc#enable_cmdline_completion"]()
end

return {
"Shougo/ddc.vim",
dependencies = {
"vim-denops/denops.vim",
"Shougo/pum.vim",
"Shougo/ddc-ui-pum",
-- source
"LumaKernel/ddc-source-file",
"Shougo/ddc-source-around",
"Shougo/ddc-source-cmdline",
"Shougo/ddc-source-cmdline-history",
"Shougo/ddc-source-copilot",
"Shougo/ddc-source-input",
"Shougo/ddc-source-nvim-lsp",
"Shougo/ddc-source-rg",
"Shougo/ddc-source-shell",
"Shougo/ddc-source-shell-native",
"matsui54/ddc-buffer",
"uga-rosa/ddc-source-nvim-lua",
-- filter
"Shougo/ddc-filter-converter_remove_overlap",
"Shougo/ddc-filter-matcher_head",
"Shougo/ddc-filter-matcher_length",
"Shougo/ddc-filter-matcher_prefix",
"Shougo/ddc-filter-sorter_head",
"Shougo/ddc-filter-sorter_rank",
},
config = function()
-- keymaps
vim.keymap.set({"i", "c"}, "<C-n>", "<Cmd>call pum#map#insert_relative(+1, 'loop')<CR>")
vim.keymap.set({"i", "c"}, "<C-p>", "<Cmd>call pum#map#insert_relative(-1, 'loop')<CR>")
vim.keymap.set({"i", "c"}, "<C-e>", function()
if vim.fn["ddc#visible"]() then
return vim.fn["ddc#hide"]("Manual")
else
return "<End>"
end
end, { remap = true })
vim.keymap.set({"i", "c"}, "<C-l>", function()
return vim.fn["ddc#map#manual_complete"]()
end, { expr = true, desc="Refresh the completion" })
vim.keymap.set({"n", "x"}, ":", "<Cmd>call v:lua.CommandlinePre(':')<CR>:")
vim.keymap.set({"n"}, "?", "<Cmd>call v:lua.CommandlinePre('/')<CR>?")

    -- options
    vim.fn["pum#set_option"]({
      auto_confirm_time = 3000,
      auto_select = false,
      border = "singlle",
      horizontal_menu = false,
      max_width = 80,
      max_height = 20,
      offset_cmdcol = 0,
      padding = true,
      preview = true,
      preview_border = "single",
      preview_width = 80,
      reversed = false,
      use_complete = true,
      use_setline = false,
    })
    vim.fn["pum#set_local_option"]("c",{ { horizontal_menu = false, }, })
    vim.fn["ddc#custom#load_config"](vim.fs.joinpath(require("utils").plugins_path, "ddc", "ddc.ts"))
    vim.fn["ddc#enable"]({context_filetype = "treesitter"})
    vim.fn["ddc#enable_terminal_completion"]()

end
}

ddc.ts
import { BaseConfig } from "https://deno.land/x/[email protected]/types.ts";
import { fn } from "https://deno.land/x/[email protected]/deps.ts";
import { ConfigArguments } from "https://deno.land/x/[email protected]/base/config.ts";

export class Config extends BaseConfig {
  override async config(args: ConfigArguments): Promise<void> {
    const hasWindows = await fn.has(args.denops, "win32");

    args.contextBuilder.patchGlobal({
      ui: "pum",
      autoCompleteEvents: [
        "InsertEnter",
        "TextChangedI",
        "TextChangedP",
        "CmdlineEnter",
        "CmdlineChanged",
        "TextChangedT",
      ],
      sources: ["around", "file", "skkeleton"],
      cmdlineSources: {
        ":": ["cmdline", "cmdline-history", "around"],
        "/": ["around"],
        "?": ["around"],
        "=": ["input"],
      },
      sourceOptions: {
        _: {
          ignoreCase: true,
          matchers: ["matcher_head", "matcher_prefix", "matcher_length"],
          sorters: ["sorter_rank"],
          converters: ["converter_remove_overlap"],
          timeout: 1000,
        },
        around: {
          mark: "A",
        },
        buffer: {
          mark: "B",
        },
        "nvim-lua": {
          mark: "",
          forceCompletionPattern: "\\.\\w*",
        },
        cmdline: {
          mark: "󰆍",
          forceCompletionPattern: "\\S/\\S*|\\.\\w*",
        },
        "cmdline-history": {
          mark: "󰆍 his",
          sorters: [],
        },
        copilot: {
          mark: "",
          matchers: [],
          minAutoCompleteLength: 0,
          isVolatile: false,
        },
        input: {
          mark: "input",
          forceCompletionPattern: "\\S/\\S*",
          isVolatile: true,
        },
        "nvim-lsp": {
          mark: "lsp",
          forceCompletionPattern: "\\.\\w*|::\\w*|->\\w*",
          dup: "force",
        },
        file: {
          mark: "F",
          isVolatile: true,
          minAutoCompleteLength: 1000,
          forceCompletionPattern: "\\S/\\S*",
        },
        shell: {
          mark: "sh",
          isVolatile: true,
          forceCompletionPattern: "\\S/\\S*",
        },
        "shell-native": {
          mark: "sh",
          isVolatile: true,
          forceCompletionPattern: "\\S/\\S*",
        },
        rg: {
          mark: "rg",
          minAutoCompleteLength: 5,
          enabledIf: "finddir('.git', ';') != ''",
        },
        skkeleton: {
          mark: "sk",
          matchers: ["skkeleton"],
          sorters: [],
          minAutoCompleteLength: 2,
          isVolatile: true,
        },
      },
      sourceParams: {
        buffer: {
          requireSameFiletype: false,
          limitBytes: 50000,
          fromAltBuf: true,
          forceCollect: true,
        },
        file: {
          filenameChars: "[:keyword:].",
        },
        "shell-native": {
          shell: "zsh",
        },
      },
      postFilters: ["sorter_head"],
    });

    for (
      const filetype of [
        "help",
        "vimdoc",
        "markdown",
        "markdown_inline",
        "gitcommit",
        "comment",
      ]
    ) {
      args.contextBuilder.patchFiletype(filetype, {
        sources: ["around", "copilot"],
      });
    }

    for (const filetype of ["html", "css"]) {
      args.contextBuilder.patchFiletype(filetype, {
        sourceOptions: {
          _: {
            keywordPattern: "[0-9a-zA-Z_:#-]*",
          },
        },
      });
    }

    for (const filetype of ["zsh", "sh", "bash"]) {
      args.contextBuilder.patchFiletype(filetype, {
        sourceOptions: {
          _: {
            keywordPattern: "[0-9a-zA-Z_./#:-]*",
          },
        },
        sources: [
          hasWindows ? "shell" : "shell-native",
          "around",
        ],
      });
    }
    args.contextBuilder.patchFiletype("ddu-ff-filter", {
      sources: ["buffer"],
      sourceOptions: {
        _: {
          keywordPattern: "[0-9a-zA-Z_:#-]*",
        },
      },
      specialBufferCompletion: true,
    });

    for (
      const filetype of [
        "css",
        "go",
        "html",
        "python",
        "ruby",
        "typescript",
        "typescriptreact",
        "tsx",
        "graphql",
        "astro",
        "svelte",
      ]
    ) {
      args.contextBuilder.patchFiletype(filetype, {
        sources: ["copilot", "nvim-lsp", "around"],
      });
    }

    args.contextBuilder.patchFiletype("lua", {
      sources: ["copilot", "nvim-lsp", "nvim-lua", "around"],
    });

    // Enable specialBufferCompletion for cmdwin.
    args.contextBuilder.patchFiletype("vim", {
      specialBufferCompletion: true,
    });
  }
}

うまく​いかなくて​詰まったら

この​vim駅伝を​企画している​vim-jpと​いう​素敵な​コミュニティが​あります。​Slack上で​自分よりも​圧倒的に​Vimや​Neovimに​詳しい​方が​たくさん​いる​なんとも​素晴しい​コミュニティです。​なに​よりも​ddc.vimの​作者の​Shougoさんも​所属している​コミュニティでも​あるので、​困った​ときの​頼り先と​して​これ以上​心強い​場所は​ありません。​もし、​この​稚拙な​記事を​読んで​vim-jpに​興味を​持たれた​方が​いましたら​どう​ぞ​おいでください。

2023/09/22追記:
vim-jpの​Slackには​#tech-shougowareと​いう​チャンネルが​あり、​そこでは​Shougoさん作の​プラグインに​関連する​相談や​話が​されています。​もしSlackには​入った​際には​是非この​チャンネルもの​ぞいてみてください。

まとめ

これらの​手順を​踏めば、​macでは​コマンドライン補完が​動作すると​思います。
一緒に​闇の​力に​入門しませんか?

※windowsでは​ddcの​読みこみが​よく​失敗してしまって、​うまく​動作しません。​解決次第追記します。