⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content

Conversation

@PMassicotte
Copy link
Collaborator

@PMassicotte PMassicotte commented Jan 6, 2026

This is a first draft of a goto definition/reference based on the LSP internal framework developed in 9f21d6c.

Features

Goto Definition (textDocument/definition)

  • Scope-aware symbol resolution (respects R's lexical scoping rules)
  • Workspace-wide search across all R files (.R, .Rmd, .qmd)
  • Package source lookup via nvimcom (jumps to source files or deparsed functions)
  • Qualified name support (pkg::fn and pkg:::fn)
  • Handles both standard (<-, =) and super-assignment (<<-) operators

Find References (textDocument/references)

  • Scope-aware reference finding (only finds usages that resolve to the same definition)
  • Distinguishes between public (file-level) and private (function-level) symbols
  • Cross-file reference search for public symbols

Implementation Details

  • Symbol extraction uses Tree-sitter queries (locals.scm and custom queries)
  • Workspace indexing with file modification tracking for cache invalidation
  • R-side resolution via nvimcom:::send_definition() for package functions
  • Falls back to deparsed source when original source files aren't available

Testing

To test, create this R file with this content:

# Test: gd (definition), gr (references), gi (implementation)

# File-level (public) function
add <- function(a, b) a + b

# Scoping: inner 'x' shadows outer 'x'
outer <- function(x) {
    inner <- function(x) x * 2
    inner(x) + x
}

# Super-assignment (<<-)
make_counter <- function() {
    count <<- 0
    function() count <<- count + 1
}

# Usage (references), use goto definition on these
x <- add(1, 2)
y <- add(x, 3)

# Qualified names (package lookup), use goto definition on these
dplyr::filter()
stats::lm()

# --- Implementation (S3) ---

greet <- function(x) UseMethod("greet")
greet.dog <- function(x) "Woof!"
greet.cat <- function(x) "Meow!"

To test, use lsp keymap directly, or these following lua commands:

:lua vim.lsp.buf.definition()
:lua vim.lsp.buf.references()
:lua vim.lsp.buf.implementation()

Todos

  • Goto definition on package function is slow the first time you use it

  • Implement rename functionality. The current infrastructure will allow doing it very easily. We will have to think a bit more about the scoping. For example, renaming a function parameter should only rename it within the function body or renaming a file-level function should rename all its references across the workspace?

  • Add tests, especially for edge cases like nested functions, anonymous functions, etc.

  • Add documentation

  • Use workspace markers to limit the search scope for references?

  • Remove debug_index() before merging (see also other helper functions)

  • Add configuration options to let the user to enable/disable these lsp features. Reuse

    r_ls = {
        completion = true, -- enable the completion provider
        hover = true, -- enable the hover provider
        signature = true, -- enable the signature help provider
        implementation = true, -- enable the implementation provider
        definition = true, -- enable the goto definition provider
        references = true, -- enable the find references provider
        doc_width = 0,
        fun_data_1 = { "select", "rename", "mutate", "filter" },
        fun_data_2 = { ggplot = { "aes" }, with = { "*" } },
        fun_data_formula = { ggplot = { "facet_wrap", "facet_grid", "vars" } },
        quarto_intel = nil,
    }

Basic version of goto-definition. Some speed improvement can be done on
package function where we create a temp file to see its source.
…l in queries module

This change improves code maintainability and readability by removing
redundant query definitions.
…on and definition finding logic for better maintainability

feat(lsp/definition.lua): enhance definition search to include enclosing scopes and improve accuracy of symbol resolution

refactor(definition.lua): streamline definition search logic and improve code clarity by removing unused functions and comments

fix(lsp/init.lua): update require paths for document_symbols and rebuild_index functions to reflect correct module structure
feat(workspace.lua): add debug_index function to display workspace index state for easier debugging and verification of indexed symbols
…emoving custom query for enclosing scopes

feat(scope.lua): enhance symbol definition lookup with custom query for <<- assignments and improve function signature for better clarity

refactor(scope.lua): rename find_definition_in_scope to include cursor position parameters for more precise searching and maintainability
…ee-sitter for better accuracy and robustness

fix(lsp/definition.lua): update goto_definition function to handle symbol parsing and response sending more efficiently
@PMassicotte PMassicotte changed the title LSP: implementing goto defitnition and reference LSP: implementing goto defitnition/reference/implementation Jan 6, 2026
@jalvesaq jalvesaq mentioned this pull request Jan 8, 2026
@jalvesaq
Copy link
Member

What is the status of this pull request? I tried the branch, and I can jump to references and definitions, even if the functions are defined in other R scripts. It's a great job so far! And, with more than 2500 lines of code, it certainly has taken a lot of your time. Thank you!

When starting nvim, I get this warning:

  Invalid option `r_ls.references`.
  Invalid option `r_ls.definition`.
  Invalid option `r_ls.implementation`.

Jumping to implementation doesn't work, but it also doesn't work in Lua or C.

@PMassicotte
Copy link
Collaborator Author

PMassicotte commented Jan 20, 2026

Thanks for pointing it out for the warnings. These are option to op in/out of the implementation (in case someone would prefer to use r_languageserver).

My plan is to finish this PR by the end of the week or this weekend ;)

For the implementation, it should go to the s3 implementation. I will post an example here when I am back home this afternoon.

…he top level

feat(utils.lua): add top_level_only option to extract_symbols for filtering symbols at the top level only
… list, used to build symbol tree to display document symbols
…res with languageserver package

Add new options to disable definition, references, and implementation
providers in the languageserver package. Update R.nvim config defaults
to include these new providers.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements LSP goto definition, find references, and find implementations functionality for R.nvim. It builds on the existing LSP framework to provide scope-aware symbol resolution, workspace-wide search capabilities, and package source lookup.

Changes:

  • Added scope-aware symbol resolution using tree-sitter queries and lexical scoping rules
  • Implemented workspace indexing with file modification tracking for cross-file navigation
  • Added LSP handlers for textDocument/definition, textDocument/references, textDocument/implementation, and textDocument/documentSymbol

Reviewed changes

Copilot reviewed 20 out of 21 changed files in this pull request and generated 29 comments.

Show a summary per file
File Description
tests/test_scope_spec.lua Comprehensive tests for scope resolution and symbol visibility
tests/test_goto_definition_spec.lua Tests for goto definition with scope awareness
tests/test_helper.lua Test helper for tree-sitter parser setup
nvimcom/src/apps/tcp.c Message parsing handlers for definition responses from R
nvimcom/src/apps/rnvimserver.c LSP request handlers and response builders for new features
nvimcom/bin/rnvimserver Compiled binary (should not be committed)
nvimcom/R/info.R R-side definition lookup with package source resolution
lua/r/server.lua Configuration integration for new LSP features
lua/r/lsp/workspace.lua Workspace-wide symbol indexing and caching
lua/r/lsp/utils.lua Shared utilities for symbol extraction and file operations
lua/r/lsp/symbols.lua Document symbol extraction with hierarchical tree building
lua/r/lsp/scope.lua Scope management using tree-sitter locals queries
lua/r/lsp/references.lua Scope-aware reference finding across workspace
lua/r/lsp/queries.lua Centralized tree-sitter query management
lua/r/lsp/init.lua Integration points for new LSP features
lua/r/lsp/implementation.lua S3/S4 method implementation finding
lua/r/lsp/definition.lua Goto definition with workspace and package lookup
lua/r/lsp/buffer.lua Buffer management for parsing external files
lua/r/lsp/ast.lua AST traversal utilities extracted from init.lua
lua/r/config.lua Configuration options for new LSP features
doc/R.nvim.txt Documentation for new LSP features and configuration

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +21 to +47
function M.index_workspace(force)
if indexed and not force then return end

local cwd = vim.fn.getcwd()
local files = {}
utils.find_r_files(cwd, files)

workspace_index = {}
file_mtimes = {}

for _, filepath in ipairs(files) do
local stat = vim.uv.fs_stat(filepath)
if stat then
file_mtimes[filepath] = stat.mtime.sec

local definitions = utils.parse_file_definitions(filepath)
for symbol, locations in pairs(definitions) do
if not workspace_index[symbol] then workspace_index[symbol] = {} end
for _, loc in ipairs(locations) do
table.insert(workspace_index[symbol], loc)
end
end
end
end

indexed = true
end
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workspace indexing happens synchronously on first call without user feedback. The index_workspace() function (line 21) scans all R files in the workspace, which could be slow for large projects. This happens during the first LSP operation (goto definition, references, etc.), potentially causing unexpected lag. Consider adding a progress notification or making the initial indexing asynchronous.

Copilot uses AI. Check for mistakes.
Comment on lines +177 to +186
# Check if cached file already exists
if (!file.exists(tmpfile)) {
header <- sprintf(
"# %s::%s (no source available - deparsed)",
pkg_name,
symbol
)
body <- deparse(fn)
writeLines(c(header, "", body), tmpfile)
}
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential race condition in file-based caching. Line 178 checks if the tmpfile exists, and if not, creates it (lines 179-186). However, in a concurrent environment (e.g., multiple Neovim instances), two processes could both find that the file doesn't exist and try to create it simultaneously, leading to corruption or undefined behavior. Consider using atomic file operations or file locking.

Suggested change
# Check if cached file already exists
if (!file.exists(tmpfile)) {
header <- sprintf(
"# %s::%s (no source available - deparsed)",
pkg_name,
symbol
)
body <- deparse(fn)
writeLines(c(header, "", body), tmpfile)
}
# Always (re)generate the deparsed cache file to avoid race conditions
header <- sprintf(
"# %s::%s (no source available - deparsed)",
pkg_name,
symbol
)
body <- deparse(fn)
writeLines(c(header, "", body), tmpfile)

Copilot uses AI. Check for mistakes.
Comment on lines +159 to +189
for _, filepath in ipairs(files) do
if utils.normalize_path(filepath) ~= utils.normalize_path(file) then
local temp_bufnr, _, cleanup = buffer.load_file_to_buffer(filepath)

if temp_bufnr then
local _, temp_root = ast.get_parser_and_root(temp_bufnr)

if temp_root then
for _, node in query:iter_captures(temp_root, temp_bufnr) do
local text = vim.treesitter.get_node_text(node, temp_bufnr)
if text == word then
local in_function = ast.find_ancestor(
node,
"function_definition"
)
if not in_function then
local start_row, start_col = node:start()
table.insert(all_refs, {
file = filepath,
line = start_row,
col = start_col,
})
end
end
end
end

if cleanup then cleanup() end
end
end
end
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential performance issue with nested loops. In the worst case, for each file in the workspace (outer loop at line 159), we iterate through all captures from the query (inner loop at line 167). For a workspace with many files and many references per file, this could be O(n*m) where n is number of files and m is captures per file. Consider adding early termination or limiting the search scope.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +50
--- Find implementations for R.nvim LSP
--- Provides textDocument/implementation functionality for S3/S4 methods

local M = {}

local workspace = require("r.lsp.workspace")
local utils = require("r.lsp.utils")

--- Find S3 method implementations for a generic function
---@param word string The generic function name
---@return table[] List of locations {file, line, col}
local function find_s3_methods(word)
-- Pattern: word.* (e.g., print.default, print.factor)
local pattern = "^" .. vim.pesc(word) .. "%."
return workspace.find_symbols_matching(pattern)
end

--- Find implementations of a generic function (S3/S4 methods)
---@param req_id string LSP request ID
function M.find_implementations(req_id)
local word, err = utils.get_keyword_safe()
if err or not word then
utils.send_null(req_id)
return
end

utils.prepare_workspace()

local implementations = find_s3_methods(word)

-- Strategy 2: Dynamic lookup via nvimcom (if R is running)
-- TODO: Implement async R query for runtime method discovery
-- if vim.g.R_Nvim_status == 7 then
-- local cmd = string.format(
-- "nvimcom:::send_implementations('%s', '%s')",
-- req_id, word
-- )
-- require("r.run").send_to_nvimcom("E", cmd)
-- return
-- end

-- Return static results
if #implementations > 0 then
utils.send_response("I", req_id, { locations = implementations })
else
utils.send_null(req_id)
end
end

return M
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the find_implementations functionality. The implementation.lua module (which implements textDocument/implementation for S3/S4 methods) lacks tests. This is particularly important given that S3/S4 method resolution has complex edge cases and the pattern-based matching could have subtle bugs that wouldn't be caught without tests.

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <[email protected]>
Copy link

Copilot AI commented Jan 25, 2026

@PMassicotte I've opened a new pull request, #493, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link

Copilot AI commented Jan 25, 2026

@PMassicotte I've opened a new pull request, #494, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI and others added 2 commits January 25, 2026 09:36
* Initial plan

* refactor: consolidate location-based LSP response functions into single parameterized function

Co-authored-by: PMassicotte <[email protected]>

* Final update: refactoring complete and validated

Co-authored-by: PMassicotte <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: PMassicotte <[email protected]>
* Initial plan

* Remove debug logging statements from send_implementation_result

Co-authored-by: PMassicotte <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: PMassicotte <[email protected]>
Co-authored-by: Philippe Massicotte <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants