## 経緯
[[📝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
```