## 経緯 [[📝telescope-coc.nvimでTelescope coc referencesコマンドの実行結果からファイル名が読み取れない]] 問題を解決したいので。 ## コードの調査をする 以下を調べる。 <div class="link-card"> <div class="link-card-header"> <img src="https://github.githubassets.com/favicons/favicon.svg" class="link-card-site-icon"/> <span class="link-card-site-name">GitHub</span> </div> <div class="link-card-body"> <div class="link-card-content"> <div> <p class="link-card-title">telescope-coc.nvim/lua/telescope/_extensions/coc.lua at master · fannheyward/telescope-coc.nvim</p> </div> <div class="link-card-description"> coc.nvim integration for telescope.nvim. Contribute to fannheyward/telescope-coc.nvim development by... </div> </div> <img src="https://opengraph.githubassets.com/449d2feefa22bd14f010619dc7f71fbe8fb7f121a852a313ba339b586e252c71/fannheyward/telescope-coc.nvim" class="link-card-image" /> </div> <a href="https://github.com/fannheyward/telescope-coc.nvim/blob/master/lua/telescope/_extensions/coc.lua"></a> </div> ### [[Ubuntu]]でログを仕込んでみる `make_display`関数の中にログを仕込んでみた。 ```lua local make_display = function(entry) local line_info = { table.concat({ entry.lnum, entry.col }, ':'), 'TelescopeResultsLineNr' } print(entry.filename) local filename = utils.transform_path(opts, entry.filename) print(filename) return displayer({ line_info, filename, entry.text:gsub('.* | ', ''), }) end ``` その結果が以下。 ```console /home/tadashi-aikawa/tmp/ts-sandbox/main.ts main.ts /home/tadashi-aikawa/tmp/ts-sandbox/main.ts main.ts /home/tadashi-aikawa/tmp/ts-sandbox/main.ts main.ts /home/tadashi-aikawa/tmp/ts-sandbox/main.ts main.ts ``` [[Ubuntu]]環境では `utils.transform_path` にて相対パスに変換されていることが分かる。 ### [[Windows]]でログを仕込んでみる コードは一緒なので割愛。パスが変換されていないことは確認できる。 ```console c:\Users\syoum\tmp\ts-sandbox\main.ts c:\Users\syoum\tmp\ts-sandbox\main.ts c:\Users\syoum\tmp\ts-sandbox\main.ts c:\Users\syoum\tmp\ts-sandbox\main.ts c:\Users\syoum\tmp\ts-sandbox\main.ts c:\Users\syoum\tmp\ts-sandbox\main.ts c:\Users\syoum\tmp\ts-sandbox\main.ts c:\Users\syoum\tmp\ts-sandbox\main.ts ``` この時点で以下の推測もできる。 - [[telescope-coc.nvim]]は絶対パスを相対パスに変換するロジックをもっている - [[tsserver]]以外の[[LSP]]はカレントディレクトリも考慮して相対パスを返しているか、異なる方法で正規化してそう ### `utils.transform_path`の確認 話題のfunctionをもう少し追ってみる。 ```lua local utils = require('telescope.utils') ``` `utils`は`telescope`の`utils`を使っていそうなので、[[telescope.nvim]]のコードを追う必要がある。 `lua/telescope/utils.lua`の`utils.transform_path`を見る。何やら長い説明が... ```lua --- Transform path is a util function that formats a path based on path_display --- found in `opts` or the default value from config. --- It is meant to be used in make_entry to have a uniform interface for --- builtins as well as extensions utilizing the same user configuration --- Note: It is only supported inside `make_entry`/`make_display` the use of --- this function outside of telescope might yield to undefined behavior and will --- not be addressed by us ---@param opts table: The opts the users passed into the picker. Might contains a path_display key ---@param path string: The path that should be formatted ---@return string: The transformed path ready to be displayed utils.transform_path = function(opts, path) ``` あまりピンとこないのでコードを読み進めていく。 ```lua local path_display = vim.F.if_nil(opts.path_display, require("telescope.config").values.path_display) local transformed_path = path ``` `path_display`はtruncateで使うものだから直接は関係ないはず。`transform_path`は変換予定の`path`が入る。なお、configで設定はしていないで`path_display`は空tableみたいだった。 重要なのは、この関数内のどこで相対パスに変換されるのかということ。 ```lua if type(path_display) == "function" then return path_display(opts, transformed_path) elseif utils.is_path_hidden(nil, path_display) then return "" elseif type(path_display) == "table" then if vim.tbl_contains(path_display, "tail") or path_display.tail then transformed_path = utils.path_tail(transformed_path) elseif vim.tbl_contains(path_display, "smart") or path_display.smart then transformed_path = utils.path_smart(transformed_path) else if not vim.tbl_contains(path_display, "absolute") or path_display.absolute == false then ① end if vim.tbl_contains(path_display, "shorten") or path_display["shorten"] ~= nil then ② end if vim.tbl_contains(path_display, "truncate") or path_display.truncate then ③ end ``` 条件からして①②③のいずれかに入るはず。それぞれがor条件なので吟味が必要。 - ルート① - `true` not vim.tbl_contains(path_display, "absolute") - `nil` path_display.absolute == false - ルート② - `false` vim.tbl_contains(path_display, "shorten") - `false` path_display["shorten"] ~= nil - ルート③ - `false` vim.tbl_contains(path_display, "truncate") - `false` path_display.truncate よってルート①に入る。(この条件の妥当性は少し気になるけど) 内部の実装と挙動は以下の通り。 ```lua local cwd if opts.cwd then cwd = opts.cwd if not vim.in_fast_event() then cwd = vim.fn.expand(opts.cwd) end else -- ここで代入される cwd = vim.loop.cwd() end -- cwd = "/home/tadashi-aikawa/tmp/ts-sandbox" transformed_path = Path:new(transformed_path):make_relative(cwd) ``` `vim.loop.cwd()`がカレントディレクトリを取得しており、`Path:new...`の結果が相対パスになっているところまでは確認できた。**[[Ubuntu]]では**。 [[Windows]]で確認したところ`main.ts`は相対パスと絶対パスのそれぞれで存在する...。 ``` main.ts c:\Users\syoum\tmp\ts-sandbox\main.ts c:\Users\syoum\tmp\ts-sandbox\main.ts c:\Users\syoum\tmp\ts-sandbox\main.ts c:\Users\syoum\tmp\ts-sandbox\main.ts ``` いずれにせよこれはおかしい。詳細を見てみる。 ```lua print("") print("[cwd]") print(cwd) print("[before]") print(transformed_path) transformed_path = Path:new(transformed_path):make_relative(cwd) print("[after]") print(transformed_path) print("") ``` ``` [cwd] C:\Users\syoum\tmp\ts-sandbox [before] C:\Users\syoum\tmp\ts-sandbox\main.ts [after] main.ts ``` ``` [cwd] C:\Users\syoum\tmp\ts-sandbox [before] c:\Users\syoum\tmp\ts-sandbox\main.ts [after] c:\Users\syoum\tmp\ts-sandbox\main.ts ``` 大文字と小文字か。。。 `c:\Users\syoum\tmp\ts-sandbox\main.ts`だと`cwd`が`C:`だから相対パス判定されないのね...。 リポジトリを確認したところ、`transform_path`に最近変更は加わっていないので、デグレとかではなさそう。 ### なぜ大文字のCと小文字のcが混ざるのか? 話を[[telescope-coc.nvim]]に戻そう。 ```lua local make_display = function(entry) local line_info = { table.concat({ entry.lnum, entry.col }, ':'), 'TelescopeResultsLineNr' } local filename = utils.transform_path(opts, entry.filename) return displayer({ line_info, filename, entry.text:gsub('.* | ', ''), }) end ``` 問題は`make_display`の `entry.filename` に `c:` が小文字のpathが入ってしまうことだ。そもそも論として、`main.ts`は複数回受け付けてしまっている。本来なら `C:` と大文字はじまりのパスが必要回数だけ来て欲しい。 デバッグを仕込んで分かったが、先の調査では[[telescope-coc.nvim]]以外に[[telescope.nvim]]の関数を呼んでいたのも拾われてしまっていた。実際にログを出してみた感じだと、[[telescope-coc.nvim]]の結果は以下だけと言える。 ``` 👺 table: 0x01f1b2d9c450 🐈 table: 0x01f1b322f368 [cwd] C:\Users\syoum\tmp\ts-sandbox [before] c:\Users\syoum\tmp\ts-sandbox\main.ts [after] c:\Users\syoum\tmp\ts-sandbox\main.ts [cwd] C:\Users\syoum\tmp\ts-sandbox [before] c:\Users\syoum\tmp\ts-sandbox\main.ts [after] c:\Users\syoum\tmp\ts-sandbox\main.ts [cwd] C:\Users\syoum\tmp\ts-sandbox [before] c:\Users\syoum\tmp\ts-sandbox\main.ts [after] c:\Users\syoum\tmp\ts-sandbox\main.ts ``` それならば、[[telescope-coc.nvim]]の修正で今回の問題が解決する可能性もありそう。ログを仕込んでtableの中身を表示する。 ```lua function print_table(t, indent) indent = indent or '' for k, v in pairs(t) do if type(v) == "table" then print(indent .. k .. ":") print_table(v, indent .. ' ') else print(indent .. k .. ": " .. tostring(v)) end end end ``` ```lua local refs = CocAction('references', excludeDeclaration) if type(refs) ~= 'table' or vim.tbl_isempty(refs) then return end print("👺") print_table(refs) local results = locations_to_items(refs) print("🐈") print_table(results) ``` 結果。 ``` 👺 1: uri: file:///c%3A/Users/syoum/tmp/ts-sandbox/main.ts range: end: line: 8 character: 15 start: line: 8 character: 12 2: uri: file:///c%3A/Users/syoum/tmp/ts-sandbox/main.ts range: end: line: 9 character: 16 start: line: 9 character: 13 🐈 1: lnum: 9 text: const z = add(1, 10); filename: c:\Users\syoum\tmp\ts-sandbox\main.ts col: 13 2: lnum: 10 text: const z2 = add(2, 20); filename: c:\Users\syoum\tmp\ts-sandbox\main.ts col: 14 ``` CocActionの結果 `refs` が既に小文字になってしまっている。それならば、 ```lua local locations_to_items = function(locs) if not locs then return end local items = {} for _, l in ipairs(locs) do if l.targetUri and l.targetRange then -- LocationLink l.uri = l.targetUri l.range = l.targetRange end local bufnr = vim.uri_to_bufnr(l.uri) vim.fn.bufload(bufnr) local filename = vim.uri_to_fname(l.uri) local row = l.range.start.line local line = (api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { '' })[1] items[#items + 1] = { filename = filename, lnum = row + 1, col = l.range.start.character + 1, text = line, } end return items end ``` の中から以下の部分でファイル名に変換しているはず。 ```lua local filename = vim.uri_to_fname(l.uri) ``` この後に **先頭が `c:` だったら `C:` に置換する** 方法もありそう。 ## 対応策 ### [[telescope.nvim]]を修正するのはキツそう... [[ChatGPT]]に聞いてみたところ、以下のようなコードに至った。[[Windows]]だったら小文字と大文字を同一視する。 ```lua local function normalizePath(path) if os.getenv("OS") ~= nil and os.getenv("OS"):match("Windows") then -- Windows環境の場合、パスを小文字に変換 return string.lower(path) else -- それ以外の環境では、パスをそのまま使用 return path end end local transformed_path = normalizePath(original_transformed_path) local cwd = normalizePath(original_cwd) -- ここで Path:new() と make_relative() を使用 ``` 変換をしている部分は以下なので... ```lua transformed_path = Path:new(transformed_path):make_relative(cwd) ``` 以下のようにする。 ```lua local make_relative = function(path, cwd) if os.getenv("OS") ~= nil and os.getenv("OS"):match("Windows") then return Path:new(string.lower(path)):make_relative(string.lower(cwd)) else return path end end ``` これでいけると思ったが、よくよく考えたらこれだとすべて小文字になってしまう...。`make_relative`を調べたほうがよさそう。 ```lua local Path = require "plenary.path" ``` [[plenary.nvim]]のコードを見る。 `lua/plenary/path.lua` ```lua function Path:make_relative(cwd) if is_uri(self.filename) then return self.filename end self.filename = clean(self.filename) cwd = clean(F.if_nil(cwd, self._cwd, cwd)) if self.filename == cwd then self.filename = "." else if cwd:sub(#cwd, #cwd) ~= path.sep then cwd = cwd .. path.sep end if self.filename:sub(1, #cwd) == cwd then self.filename = self.filename:sub(#cwd + 1, -1) end end return self.filename end ``` このファイルに付け入る隙はなさそう。ただ、その下に... ```lua function Path:normalize(cwd) if is_uri(self.filename) then return self.filename end self:make_relative(cwd) -- Substitute home directory w/ "~" -- string.gsub is not useful here because usernames with dashes at the end -- will be seen as a regexp pattern rather than a raw string local home = path.home if string.sub(path.home, -1) ~= path.sep then home = home .. path.sep end local start, finish = string.find(self.filename, home, 1, true) if start == 1 then self.filename = "~" .. path.sep .. string.sub(self.filename, (finish + 1), -1) end return _normalize_path(clean(self.filename), self._cwd) end ``` なんかすごそうなfunctionがある。最後の`_normalize_path`も中見てみたけど、色々やってそう...。これを試しに使ってみたけどダメだった。 ### [[telescope-coc.nvim]]を修正する [[telescope.nvim]]は更新頻度も高そうだし、[[telescope-coc.nvim]]を修正する方が安全そう。 ```lua local make_display = function(entry) local line_info = { table.concat({ entry.lnum, entry.col }, ':'), 'TelescopeResultsLineNr' } local filename = utils.transform_path(opts, entry.filename) return displayer({ line_info, filename, entry.text:gsub('.* | ', ''), }) end ``` この `entry.filename` のドライブ名を大文字にすれば回避策としては十分。 ```diff + local normalizeDriveLetter = function(str) + return str:match("^[a-z]:") and str:sub(1, 1):upper() .. str:sub(2) or str + end local make_display = function(entry) local line_info = { table.concat({ entry.lnum, entry.col }, ':'), 'TelescopeResultsLineNr' } - local filename = utils.transform_path(opts, entry.filename) + local filename = utils.transform_path(opts, normalizeDriveLetter(entry.filename)) return displayer({ line_info, filename, entry.text:gsub('.* | ', ''), }) end ```