Skip to content

Commit

Permalink
feat: Implements go to references (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
nabekou29 authored Nov 18, 2024
1 parent 9cbc052 commit 19c7d6d
Show file tree
Hide file tree
Showing 17 changed files with 311 additions and 58 deletions.
89 changes: 59 additions & 30 deletions lua/js-i18n/analyzer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ local library_query = {
--- @param file string ファイルパス
--- @return string|nil クエリ
function M.load_query_from_file(file)
local cache = {}
local query_cache = {}

local function load_query_from_file(file)
if cache[file] ~= nil then
return cache[file]
if query_cache[file] ~= nil then
return query_cache[file]
end

local f = io.open(file, "r")
Expand All @@ -34,7 +34,7 @@ function M.load_query_from_file(file)
local query = f:read("*a")
f:close()

cache[file] = query
query_cache[file] = query
return query
end

Expand Down Expand Up @@ -71,13 +71,19 @@ local function calculate_node_depth(node)
end

--- Treesitterパーサーをセットアップしてキーにマッチするノードを取得する関数
--- @param bufnr number 文言ファイルのバッファ番号
--- @param source integer|string バッファ番号 or ソース
--- @param keys string[] キー
--- @return TSNode | nil, string | nil
function M.get_node_for_key(bufnr, keys)
function M.get_node_for_key(source, keys)
local ts = vim.treesitter

local parser = ts.get_parser(bufnr, "json")
local parser = (function()
if type(source) == "string" then
return ts.get_string_parser(source, "json")
else
return ts.get_parser(source)
end
end)()
local tree = parser:parse()[1]
local root = tree:root()

Expand All @@ -86,15 +92,15 @@ function M.get_node_for_key(bufnr, keys)
local query = ts.query.parse("json", [[
(pair
key: (string) @key (#eq? @key "\"]] .. k .. [[\"")
value: [(object)(string)] @value
value: [(object)(array)(string)] @value
)
]])

local key_node = nil
local key_node_depth = 9999
local value_node = nil
local value_node_depth = 9999
for id, node, _ in query:iter_captures(json_node, bufnr) do
for id, node, _ in query:iter_captures(json_node, source) do
local name = query.captures[id]
local depth = calculate_node_depth(node)
if name == "key" and depth < key_node_depth then
Expand Down Expand Up @@ -164,20 +170,20 @@ end

--- t関数の取得に関する情報を解析する
--- @param target_node TSNode t関数取得のノード
--- @param bufnr integer バッファ番号
--- @param source (integer|string) バッファ番号 or ソース
--- @param query vim.treesitter.Query クエリ
--- @return GetTDetail|nil
local function parse_get_t(target_node, bufnr, query)
local function parse_get_t(target_node, source, query)
local namespace = ""
local key_prefix = ""

for id, node, _ in query:iter_captures(target_node, bufnr, 0, -1) do
for id, node, _ in query:iter_captures(target_node, source, 0, -1) do
local name = query.captures[id]

if name == "i18n.namespace" then
namespace = vim.treesitter.get_node_text(node, bufnr)
namespace = vim.treesitter.get_node_text(node, source)
elseif name == "i18n.key_prefix" then
key_prefix = vim.treesitter.get_node_text(node, bufnr)
key_prefix = vim.treesitter.get_node_text(node, source)
end
end

Expand All @@ -199,30 +205,30 @@ end

--- t関数の呼び出しに関する情報を解析する
--- @param target_node TSNode t関数呼び出しのノード
--- @param bufnr integer バッファ番号
--- @param source (integer|string) バッファ番号 or ソース
--- @param query vim.treesitter.Query クエリ
--- @return CallTDetail|nil
local function parse_call_t(target_node, bufnr, query)
local function parse_call_t(target_node, source, query)
local key = nil
local key_node = nil
local key_arg_node = nil
local namespace = nil
local key_prefix = nil

for id, node, _ in query:iter_captures(target_node, bufnr, 0, -1) do
for id, node, _ in query:iter_captures(target_node, source, 0, -1) do
local name = query.captures[id]

-- t関数の呼び出しがネストしている場合があるため、最初に見つかったものを採用する
-- そのため key = ke or ... のような形にしている
if name == "i18n.key" then
key = key or vim.treesitter.get_node_text(node, bufnr)
key = key or vim.treesitter.get_node_text(node, source)
key_node = key_node or node
elseif name == "i18n.key_arg" then
key_arg_node = key_arg_node or node
elseif name == "i18n.namespace" then
namespace = namespace or vim.treesitter.get_node_text(node, bufnr)
namespace = namespace or vim.treesitter.get_node_text(node, source)
elseif name == "i18n.key_prefix" then
key_prefix = key_prefix or vim.treesitter.get_node_text(node, bufnr)
key_prefix = key_prefix or vim.treesitter.get_node_text(node, source)
end
end

Expand All @@ -239,6 +245,14 @@ local function parse_call_t(target_node, bufnr, query)
}
end

--- t関数を含むノードを検索する
--- @param bufnr integer バッファ番号
--- @return FindTExpressionResultItem[]
function M.find_call_t_expressions_from_buf(bufnr)
local workspace_dir = utils.get_workspace_root(bufnr)
return M.find_call_t_expressions(bufnr, utils.detect_library(workspace_dir))
end

--- @class FindTExpressionResultItem
--- @field node TSNode t関数のノード
--- @field key_node TSNode t関数のキーのノード
Expand All @@ -249,24 +263,39 @@ end
--- @field namespace? string t 関数の namespace

--- t関数を含むノードを検索する
--- @param bufnr integer バッファ番号
--- @param source integer|string バッファ番号 or ソース
--- @param lib? string ライブラリ
--- @param lang? string 言語
--- @return FindTExpressionResultItem[]
function M.find_call_t_expressions(bufnr)
local ok, parser = pcall(vim.treesitter.get_parser, bufnr)
function M.find_call_t_expressions(source, lib, lang)
local ok, parser = pcall(function()
if type(source) == "string" then
vim.validate({ lang = { lang, "string", true } })
if lang == nil then
error("lang is required when source is string")
end
return vim.treesitter.get_string_parser(source, lang)
else
return vim.treesitter.get_parser(source)
end
end)
if not ok then
return {}
end

local tree = parser:parse()[1]
local root_node = tree:root()
local language = parser:lang()
local language = lang or parser:lang()

if not vim.tbl_contains({ "javascript", "typescript", "jsx", "tsx" }, language) then
return {}
end

local library = utils.detect_library(bufnr) or utils.Library.I18Next
local query_str = ""
for _, query_file in ipairs(library_query[library][language] or library_query[library]["*"]) do
if type(library_query[lib]) ~= "table" then
return {}
end
for _, query_file in ipairs(library_query[lib][language] or library_query[lib]["*"]) do
local str = M.load_query_from_file(query_file)
if str and type(str) == "string" and str ~= "" then
query_str = query_str .. "\n" .. str
Expand Down Expand Up @@ -303,7 +332,7 @@ function M.find_call_t_expressions(bufnr)
--- @type FindTExpressionResultItem[]
local result = {}

for id, node, _ in query:iter_captures(root_node, bufnr) do
for id, node, _ in query:iter_captures(root_node, source) do
local name = query.captures[id]

-- 現在のスコープから抜けたかどうかを判定する
Expand All @@ -313,7 +342,7 @@ function M.find_call_t_expressions(bufnr)
end

if name == "i18n.get_t" then
local get_t_detail = parse_get_t(node, bufnr, query)
local get_t_detail = parse_get_t(node, source, query)
if get_t_detail then
-- 同一のスコープ内で get_t が呼ばれた場合はスコープを上書きする形になるように、一度 leave_scope してから enter_scope する
if get_t_detail.scope_node == current_scope().scope_node then
Expand All @@ -323,7 +352,7 @@ function M.find_call_t_expressions(bufnr)
end
elseif name == "i18n.call_t" then
local scope = current_scope()
local call_t_detail = parse_call_t(node, bufnr, query)
local call_t_detail = parse_call_t(node, source, query)

if call_t_detail == nil then
goto continue
Expand Down Expand Up @@ -363,7 +392,7 @@ end
--- @return boolean ok カーソルが t 関数の引数内にあるかどうか
--- @return FindTExpressionResultItem | nil result カーソルが t 関数の引数内にある場合は t 関数の情報
function M.check_cursor_in_t_argument(bufnr, position)
local t_calls = M.find_call_t_expressions(bufnr)
local t_calls = M.find_call_t_expressions_from_buf(bufnr)

for _, t_call in ipairs(t_calls) do
local key_arg_node = t_call.key_arg_node
Expand Down
1 change: 1 addition & 0 deletions lua/js-i18n/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ i18n.setup = function(opts)
"typescriptreact",
"javascript.jsx",
"typescript.tsx",
"json",
},
single_file_support = true,
},
Expand Down
4 changes: 2 additions & 2 deletions lua/js-i18n/lsp/checker.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function M.check(client, uri)
local bufnr = vim.uri_to_bufnr(uri)
local workspace_dir = utils.get_workspace_root(bufnr)
local t_source = client.t_source_by_workspace[workspace_dir]
local library = utils.detect_library(bufnr)
local library = utils.detect_library(workspace_dir)

local dispatchers = require("js-i18n.lsp.config").dispatchers
if not dispatchers then
Expand All @@ -28,7 +28,7 @@ function M.check(client, uri)
--- @type lsp.Diagnostic[]
local diagnostics = {}

local t_calls = analyzer.find_call_t_expressions(bufnr)
local t_calls = analyzer.find_call_t_expressions_from_buf(bufnr)
for _, t_call in ipairs(t_calls) do
local key = t_call.key
local keys = vim.split(key, c.config.key_separator, { plain = true })
Expand Down
3 changes: 3 additions & 0 deletions lua/js-i18n/lsp/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ local M = {}
--- @type vim.lsp.rpc.Dispatchers
M.dispatchers = nil

--- @type table<string, I18n.ReferenceTable>
M.ref_table_by_workspace = {}

return M
19 changes: 12 additions & 7 deletions lua/js-i18n/lsp/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,14 @@ function M.create_rpc(dispatchers, client)
--- @type I18n.lsp.ProtocolModule
local protocol_module = module

local err, result = protocol_module.handler(params, client)
if err then
callback(err, nil)
else
callback(nil, result)
end
vim.schedule(function()
local err, result = protocol_module.handler(params, client)
if err then
callback(err, nil)
else
callback(nil, result)
end
end)
return true
end,
notify = function(method, params)
Expand All @@ -52,7 +54,10 @@ function M.create_rpc(dispatchers, client)
end
--- @type I18n.lsp.NotifyProtocolModule
local protocol_module = module
protocol_module.handler(params, client)

vim.schedule(function()
protocol_module.handler(params, client)
end)
return true
end,
is_closing = function()
Expand Down
17 changes: 17 additions & 0 deletions lua/js-i18n/lsp/protocol/notify/text_document_did_change.lua
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
local lsp_config = require("js-i18n.lsp.config")
local reference_table = require("js-i18n.reference_table")

--- ハンドラ
--- @param params lsp.DidChangeTextDocumentParams
--- @param client I18n.Client
local function handler(params, client)
if #params.contentChanges ~= 0 then
local uri = params.textDocument.uri

local bufnr = vim.uri_to_bufnr(uri)
local workspace_dir = require("js-i18n.utils").get_workspace_root(bufnr)
local ref_table = lsp_config.ref_table_by_workspace[workspace_dir]
if lsp_config.ref_table_by_workspace[workspace_dir] == nil then
local ref_table = reference_table.ReferenceTable.new({
workspace_dir = workspace_dir,
})
lsp_config.ref_table_by_workspace[workspace_dir] = ref_table
ref_table:load_all()
else
ref_table:load_path(vim.uri_to_fname(uri), params.contentChanges[1].text)
end

vim.schedule(function()
require("js-i18n.lsp.checker").check(client, uri)
end)
Expand Down
15 changes: 15 additions & 0 deletions lua/js-i18n/lsp/protocol/notify/text_document_did_open.lua
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
local lsp_config = require("js-i18n.lsp.config")
local reference_table = require("js-i18n.reference_table")
local utils = require("js-i18n.utils")

--- ハンドラ
--- @param params lsp.DidOpenTextDocumentParams
--- @param client I18n.Client
local function handler(params, client)
local uri = params.textDocument.uri

local bufnr = vim.uri_to_bufnr(uri)
local workspace_dir = require("js-i18n.utils").get_workspace_root(bufnr)
if lsp_config.ref_table_by_workspace[workspace_dir] == nil then
local ref_table = reference_table.ReferenceTable.new({
workspace_dir = workspace_dir,
})
lsp_config.ref_table_by_workspace[workspace_dir] = ref_table
ref_table:load_all()
end

vim.schedule(function()
require("js-i18n.lsp.checker").check(client, uri)
end)
Expand Down
1 change: 1 addition & 0 deletions lua/js-i18n/lsp/protocol/request/initialize.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ local function handler(params, _client)
capabilities = {
textDocumentSync = 1,
definitionProvider = true,
referencesProvider = true,
hoverProvider = true,
completionProvider = {},
codeActionProvider = {},
Expand Down
5 changes: 3 additions & 2 deletions lua/js-i18n/lsp/protocol/request/text_document_completion.lua
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ end
--- @return lsp.CompletionItem[]
local function get_completion_items(client, bufnr, t_call)
local lang = client:get_language(bufnr)
local t_source = client.t_source_by_workspace[utils.get_workspace_root(bufnr)]
local library = utils.detect_library(bufnr)
local workspace_dir = utils.get_workspace_root(bufnr)
local t_source = client.t_source_by_workspace[workspace_dir]
local library = utils.detect_library(workspace_dir)

local key_prefix = t_call.key_prefix or ""

Expand Down
Loading

0 comments on commit 19c7d6d

Please sign in to comment.