diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..dd6d453f --- /dev/null +++ b/.clang-format @@ -0,0 +1,8 @@ +--- +BasedOnStyle: LLVM +IndentWidth: 4 +ColumnLimit: 80 +PointerAlignment: Right +SpaceAfterCStyleCast: false +SortIncludes: Never +ReflowComments: true diff --git a/lua/r/config.lua b/lua/r/config.lua index f65e6749..7b04f9a3 100644 --- a/lua/r/config.lua +++ b/lua/r/config.lua @@ -853,7 +853,7 @@ local check_readme = function() -- Create or update the README (objls_ files will be regenerated if older than -- the README). local need_readme = false - local first_line = "Last change in this file: 2026-01-17" + local first_line = "Last change in this file: 2026-03-01" if vim.fn.filereadable(config.compldir .. "/README") == 0 or vim.fn.readfile(config.compldir .. "/README")[1] ~= first_line diff --git a/lua/r/lsp/definition.lua b/lua/r/lsp/definition.lua index d346da6d..cc0848c0 100644 --- a/lua/r/lsp/definition.lua +++ b/lua/r/lsp/definition.lua @@ -99,26 +99,6 @@ local function parse_qualified_name_at_cursor(bufnr, row, col) return nil, word, false end ---- Find definition in R package source ---- Communicates with nvimcom to get source location ----@param pkg string Package name ----@param symbol string Function/object name ----@param req_id string Request ID for async response -function M.find_in_package(pkg, symbol, req_id) - if vim.g.R_Nvim_status ~= 7 then - -- R is not running, can't query packages; respond immediately - utils.send_null(req_id) - return "pending" - end - - -- Send request to nvimcom to get source reference - local cmd = - string.format("nvimcom:::send_definition('%s', '%s', '%s')", req_id, pkg, symbol) - require("r.run").send_to_nvimcom("E", cmd) - -- Response will be sent back asynchronously - return "pending" -end - --- Main entry point for goto definition --- Called from rnvimserver via client/exeRnvimCmd ---@param req_id string LSP request ID @@ -167,34 +147,19 @@ function M.goto_definition(req_id) return end - -- 3. Try package lookup if R is running - if vim.g.R_Nvim_status == 7 then - -- If qualified (pkg::fn), use that package - -- Otherwise, let nvimcom search loaded packages + -- 3. Try rnvimserver lookup + if vim.g.R_Nvim_status >= 2 then local target_pkg = pkg or "" - M.find_in_package(target_pkg, symbol, req_id) + require("r.lsp").send_msg({ + code = "G", + orig_id = req_id, + symbol = symbol, + pkg = target_pkg, + }) return end utils.send_null(req_id) end ---- Handle definition response from nvimcom ---- Called when nvimcom sends back source location ----@param req_id string Request ID ----@param filepath string? File path or nil ----@param line integer? Line number (1-indexed from R) ----@param col integer? Column number -function M.handle_definition_response(req_id, filepath, line, col) - if filepath and filepath ~= "" then - utils.send_response("D", req_id, { - uri = "file://" .. filepath, - line = (line or 1) - 1, -- Convert to 0-indexed - col = (col or 1) - 1, - }) - else - utils.send_null(req_id) - end -end - return M diff --git a/nvimcom/DESCRIPTION b/nvimcom/DESCRIPTION index d675fe3b..8a591164 100644 --- a/nvimcom/DESCRIPTION +++ b/nvimcom/DESCRIPTION @@ -1,9 +1,12 @@ Package: nvimcom -Version: 0.9.86 -Date: 2026-01-31 +Version: 0.9.87 +Date: 2026-03-01 Title: Intermediate the Communication Between R and Neovim -Author: Jakson Aquino -Maintainer: Jakson Alves de Aquino +Authors@R: c( + person("Jakson", "Aquino", email = "jalvesaq@gmail.com", + role = c("aut", "cre")), + person("Philippe", "Massicotte", email = "pmassicotte@hotmail.com", + role = "aut")) Depends: R (>= 4.1.0) Suggests: knitr, rmarkdown, quarto Imports: methods, tools diff --git a/nvimcom/NAMESPACE b/nvimcom/NAMESPACE index e5d68324..785ca34c 100644 --- a/nvimcom/NAMESPACE +++ b/nvimcom/NAMESPACE @@ -8,8 +8,8 @@ importFrom("methods", "existsFunction", "isGeneric", "slotNames") importFrom("graphics", "boxplot", "hist", "par", "plot") importFrom("stats", "runif") importFrom("utils", "browseURL", "capture.output", "example", "find", - "getAnywhere", "getS3method", "isS3stdGeneric", - "installed.packages", "methods", "packageDescription", - "read.table", "str", "Sweave", "write.table") + "getAnywhere", "getS3method", "getSrcFilename", "getSrcref", + "isS3stdGeneric", "installed.packages", "methods", + "packageDescription", "read.table", "str", "Sweave", "write.table") importFrom("parallel", "detectCores", "mclapply") useDynLib(nvimcom, .registration = TRUE) diff --git a/nvimcom/R/bol.R b/nvimcom/R/bol.R index 21399263..a42ec600 100644 --- a/nvimcom/R/bol.R +++ b/nvimcom/R/bol.R @@ -531,11 +531,37 @@ nvim.buildargs <- function(afile, pkg) { return(invisible(NULL)) } +#' Build source reference cache for all functions in a package. +#' @param srcref_file Full path of the `srcref_` file to be built. +#' @param libname Library name. +nvim.build.srcref <- function(srcref_file, libname) { + packname <- paste0("package:", libname) + obj.list <- objects(packname, all.names = TRUE) + sink(srcref_file, append = FALSE) + for (obj in obj.list) { + fn <- try(get(obj, envir = as.environment(packname)), silent = TRUE) + if (inherits(fn, "try-error") || !is.function(fn)) { + next + } + sr <- getSrcref(fn) + if (is.null(sr)) { + next + } + srcfile <- getSrcFilename(sr, full.names = TRUE) + if (!nzchar(srcfile) || !file.exists(srcfile)) { + next + } + cat(obj, "\006", srcfile, "\006", sr[1], "\006", sr[5], "\n", sep = "") + } + sink() +} + #' Build data files for auto completion and for the Object Browser in the #' cache directory: #' - `alias_` : for finding the appropriate function during auto completion. #' - `objls_` : for auto completion and object browser #' - `args_` : for describing selected arguments during auto completion. +#' - `srcref_`: for source reference of functions (goto definition). #' @param cmpllist Full path of `objls_` file to be built. #' @param libname Library name. nvim.bol <- function(cmpllist, libname) { @@ -611,6 +637,7 @@ nvim.build.cmplls <- function() { unlink(file.path(bdir, paste("objls", u$pkg, u$cvrs, sep = "_"))) unlink(file.path(bdir, paste("alias", u$pkg, sep = "_"))) unlink(file.path(bdir, paste("args", u$pkg, sep = "_"))) + unlink(file.path(bdir, paste("srcref", u$pkg, sep = "_"))) } # Delete outdated cache files @@ -619,6 +646,7 @@ nvim.build.cmplls <- function() { unlink(file.path(bdir, paste("objls", o$pkg, o$cvrs, sep = "_"))) unlink(file.path(bdir, paste("alias", o$pkg, sep = "_"))) unlink(file.path(bdir, paste("args", o$pkg, sep = "_"))) + unlink(file.path(bdir, paste("srcref", o$pkg, sep = "_"))) } # Build missing or outdated cache files @@ -639,6 +667,7 @@ nvim.build.cmplls <- function() { t2 <- Sys.time() nvim.buildargs(paste0(bdir, "/args_", p), p) t3 <- Sys.time() + nvim.build.srcref(paste0(bdir, "/srcref_", p), p) msg <- paste0( "INFO: ", p, diff --git a/nvimcom/R/interlace.R b/nvimcom/R/interlace.R index 1d0aa2c1..6029afda 100644 --- a/nvimcom/R/interlace.R +++ b/nvimcom/R/interlace.R @@ -428,7 +428,7 @@ nvim.interlace.rmd <- function(Rmdfile, outform = NULL, rmddir, ...) { } else { if (exists("params", envir = .GlobalEnv)) { old_params <- get("params", envir = .GlobalEnv) - rm(params, envir = .GlobalEnv) + rm("params", envir = .GlobalEnv) } res <- rmarkdown::render(Rmdfile, outform, ...) if (exists("old_params", inherits = FALSE)) { diff --git a/nvimcom/src/apps/Makefile b/nvimcom/src/apps/Makefile index a2f71a70..aa986f5e 100644 --- a/nvimcom/src/apps/Makefile +++ b/nvimcom/src/apps/Makefile @@ -1,7 +1,7 @@ CC ?= gcc CFLAGS = -pthread -O2 -Wall TARGET = rnvimserver -SRCS = complete.c resolve.c hover.c signature.c rhelp.c chunk.c data_structures.c logging.c rnvimserver.c obbr.c tcp.c utilities.c ../common.c +SRCS = complete.c resolve.c hover.c definition.c signature.c rhelp.c chunk.c data_structures.c logging.c rnvimserver.c obbr.c tcp.c utilities.c ../common.c all: $(TARGET) diff --git a/nvimcom/src/apps/Makefile.win b/nvimcom/src/apps/Makefile.win index a88f6629..83e7a80d 100644 --- a/nvimcom/src/apps/Makefile.win +++ b/nvimcom/src/apps/Makefile.win @@ -1,7 +1,7 @@ CC=gcc TARGET=rnvimserver.exe CFLAGS = -mwindows -std=gnu99 -O3 -Wall -DWIN32 -SRCS = complete.c resolve.c hover.c signature.c rhelp.c chunk.c data_structures.c logging.c rnvimserver.c obbr.c tcp.c utilities.c ../common.c +SRCS = complete.c resolve.c hover.c definition.c signature.c rhelp.c chunk.c data_structures.c logging.c rnvimserver.c obbr.c tcp.c utilities.c ../common.c LIBS=-lWs2_32 ifeq "$(WIN)" "64" diff --git a/nvimcom/src/apps/data_structures.c b/nvimcom/src/apps/data_structures.c index 46328607..15c4809a 100644 --- a/nvimcom/src/apps/data_structures.c +++ b/nvimcom/src/apps/data_structures.c @@ -69,6 +69,8 @@ static void delete_pkg(PkgData *pd) { free(pd->objls); if (pd->args) free(pd->args); + if (pd->srcref) + free(pd->srcref); if (pd->title) // free title, descr and alias free(pd->title); free(pd); @@ -194,6 +196,52 @@ static void *read_alias_file(PkgData *pd) { return b; } +static char *read_srcref_file(const char *nm) { + char fnm[512]; + snprintf(fnm, 511, "%s/srcref_%s", cmp_dir, nm); + char *b = read_file(fnm, 0); + if (!b) + return NULL; + + int size = strlen(b); + if (size == 0) + return b; + + // Validate: expect exactly 3 \006 per line + const char *s0 = b; + char *s1 = b; + int n = 0; + while (*s1) { + if (*s1 == '\006') + n++; + if (*s1 == '\n') { + if (n == 3) { + n = 0; + s0 = s1 + 1; + } else { + char buf[64]; + strncpy(buf, s0, 63); + buf[63] = '\0'; + fprintf(stderr, "srcref: bad separator count: %d (%s)\n", n, + buf); + fflush(stderr); + free(b); + return NULL; + } + } + s1++; + } + + // Convert \006 to \0 + char *p = b; + while (*p) { + if (*p == '\006') + *p = 0; + p++; + } + return b; +} + static char *read_args_file(const char *nm) { char fnm[512]; snprintf(fnm, 511, "%s/args_%s", cmp_dir, nm); @@ -214,6 +262,7 @@ static void load_pkg_data(PkgData *pd, const char *fname) { int size; read_alias_file(pd); pd->args = read_args_file(pd->name); + pd->srcref = read_srcref_file(pd->name); if (!pd->objls) { pd->nobjs = 0; pd->objls = read_objls_file(fname, &size); diff --git a/nvimcom/src/apps/data_structures.h b/nvimcom/src/apps/data_structures.h index b109e794..50223e66 100644 --- a/nvimcom/src/apps/data_structures.h +++ b/nvimcom/src/apps/data_structures.h @@ -19,6 +19,7 @@ typedef struct pkg_data_ { char *alias; // A copy of the alias_ file char *objls; // A copy of the objls_ file char *args; // A copy of the args_ file + char *srcref; // A copy of the srcref_ file (source references) int nobjs; // Number of objects in objls } PkgData; diff --git a/nvimcom/src/apps/definition.c b/nvimcom/src/apps/definition.c new file mode 100644 index 00000000..83a3b1be --- /dev/null +++ b/nvimcom/src/apps/definition.c @@ -0,0 +1,171 @@ +#include +#include +#include + +#include "definition.h" +#include "global_vars.h" +#include "logging.h" +#include "lsp.h" +#include "utilities.h" +#include "tcp.h" + +/** + * @brief Search a package's srcref buffer for a symbol and extract + * file path, line, and column. + * + * The srcref buffer has the format (with \0 as separator, converted from \006): + * funcname\0filepath\0line\0col\n + * + * @param srcref The srcref buffer. + * @param symbol The symbol name to find. + * @param file Output: pointer to file path string within the buffer. + * @param line Output: line number (1-indexed from R). + * @param col Output: column number. + * @return 1 if found, 0 otherwise. + */ +static int seek_srcref(const char *srcref, const char *symbol, + const char **file, int *line, int *col) { + const char *s = srcref; + while (*s) { + if (strcmp(s, symbol) == 0) { + while (*s) + s++; + s++; + *file = s; + while (*s) + s++; + s++; + *line = atoi(s); + while (*s) + s++; + s++; + *col = atoi(s); + return 1; + } + while (*s != '\n') + s++; + s++; + } + return 0; +} + +static void send_definition_location(const char *req_id, const char *filepath, + int line, int col) { + const char *fmt = "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":" + "{\"uri\":\"file://%s\",\"range\":{\"start\":" + "{\"line\":%d,\"character\":%d},\"end\":" + "{\"line\":%d,\"character\":%d}}}}"; + + size_t len = strlen(filepath) + strlen(req_id) + 256; + char *res = (char *)malloc(len); + int lsp_line = line - 1; + if (lsp_line < 0) + lsp_line = 0; + snprintf(res, len - 1, fmt, req_id, filepath, lsp_line, col, lsp_line, col); + send_ls_response(req_id, res); + free(res); +} + +/** + * @brief Try to resolve a symbol's definition from a package's cached data. + * + * If the srcref cache has a hit, sends the location directly. + * If the srcref cache was built but has no entry, falls back to R with the + * specific package name (fast single-namespace lookup). + * If no srcref cache exists, falls back to R with the specific package name. + * + * @return 1 if handled (response sent or R fallback dispatched), 0 otherwise. + */ +static int try_resolve(const char *id, const char *symbol, const char *pkg_name, + PkgData *pkg) { + const char *file; + int line, col; + + if (pkg->srcref && seek_srcref(pkg->srcref, symbol, &file, &line, &col)) { + send_definition_location(id, file, line, col); + return 1; + } + if (r_running) { + char cmd[512]; + snprintf(cmd, 511, "nvimcom:::send_definition('%s', '%s', '%s')", id, + pkg_name, symbol); + nvimcom_eval(cmd); + return 1; + } + send_null(id); + return 1; +} + +void definition(const char *params) { + Log("definition: %s", params); + + char *id = strstr(params, "\"orig_id\":"); + char *symbol = strstr(params, "\"symbol\":\""); + char *pkg = strstr(params, "\"pkg\":\""); + + cut_json_int(&id, 10); + cut_json_str(&symbol, 10); + cut_json_str(&pkg, 7); + + if (!id || !symbol || !*symbol) { + if (id) + send_null(id); + return; + } + + if (pkg && *pkg) { + LibList *lib = inst_libs; + while (lib) { + if (strcmp(lib->pkg->name, pkg) == 0) { + try_resolve(id, symbol, pkg, lib->pkg); + return; + } + lib = lib->next; + } + if (r_running) { + char cmd[512]; + snprintf(cmd, 511, "nvimcom:::send_definition('%s', '%s', '%s')", + id, pkg, symbol); + nvimcom_eval(cmd); + return; + } + send_null(id); + return; + } + + LibList *lib = loaded_libs; + while (lib) { + if (lib->pkg->objls) { + const char *s = seek_word(lib->pkg->objls, symbol); + if (s) { + try_resolve(id, symbol, lib->pkg->name, lib->pkg); + return; + } + } + lib = lib->next; + } + + // Not in loaded_libs — search inst_libs + lib = inst_libs; + while (lib) { + if (lib->pkg->objls) { + const char *s = seek_word(lib->pkg->objls, symbol); + if (s) { + try_resolve(id, symbol, lib->pkg->name, lib->pkg); + return; + } + } + lib = lib->next; + } + + // Not found — full R search as last resort + if (r_running) { + char cmd[512]; + snprintf(cmd, 511, "nvimcom:::send_definition('%s', '', '%s')", id, + symbol); + nvimcom_eval(cmd); + return; + } + + send_null(id); +} diff --git a/nvimcom/src/apps/definition.h b/nvimcom/src/apps/definition.h new file mode 100644 index 00000000..8b5a4cd8 --- /dev/null +++ b/nvimcom/src/apps/definition.h @@ -0,0 +1,6 @@ +#ifndef DEFINITION_H +#define DEFINITION_H + +void definition(const char *params); + +#endif diff --git a/nvimcom/src/apps/rnvimserver.c b/nvimcom/src/apps/rnvimserver.c index c07aefff..c006fbaf 100644 --- a/nvimcom/src/apps/rnvimserver.c +++ b/nvimcom/src/apps/rnvimserver.c @@ -9,6 +9,7 @@ #include "complete.h" #include "resolve.h" #include "hover.h" +#include "definition.h" #include "signature.h" #include "tcp.h" #include "obbr.h" @@ -299,6 +300,9 @@ static void handle_exe_cmd(const char *params) { case 'H': hover(params); break; + case 'G': + definition(params); + break; case 'S': signature(params); break; @@ -455,7 +459,8 @@ static void handle_implementation(const char *id) { send_cmd_to_nvim(i_cmd); } -// Generic function to handle location-based LSP responses (definition, references, implementation) +// Generic function to handle location-based LSP responses (definition, +// references, implementation) static void send_location_result(const char *params) { // IMPORTANT: Search for ALL fields BEFORE calling cut_json_* functions, // because those functions NULL-terminate and modify the params string! @@ -482,22 +487,26 @@ static void send_location_result(const char *params) { size_t result_size = 4096; char *result = (char *)malloc(result_size); char *p = result; - p += snprintf(p, result_size, "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":[", id); + p += snprintf(p, result_size, + "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":[", id); char *loc = arr_start + 1; int first = 1; while (loc < arr_end) { char *obj_start = strchr(loc, '{'); - if (!obj_start || obj_start >= arr_end) break; + if (!obj_start || obj_start >= arr_end) + break; char *obj_end = strchr(obj_start, '}'); - if (!obj_end || obj_end > arr_end) break; + if (!obj_end || obj_end > arr_end) + break; char *file = strstr(obj_start, "\"file\":\""); char *line = strstr(obj_start, "\"line\":"); char *col = strstr(obj_start, "\"col\":"); - if (file && line && col && file < obj_end && line < obj_end && col < obj_end) { + if (file && line && col && file < obj_end && line < obj_end && + col < obj_end) { file += 8; char *file_end = strchr(file, '"'); if (file_end && file_end < obj_end) { @@ -517,9 +526,12 @@ static void send_location_result(const char *params) { first = 0; p += snprintf(p, result_size - (p - result), - "{\"uri\":\"file://%s\",\"range\":{\"start\":{\"line\":%d,\"character\":%d}," - "\"end\":{\"line\":%d,\"character\":%d}}}", - file_str, line_num, col_num, line_num, col_num); + "{\"uri\":\"file://" + "%s\",\"range\":{\"start\":{\"line\":%d," + "\"character\":%d}," + "\"end\":{\"line\":%d,\"character\":%d}}}", + file_str, line_num, col_num, line_num, + col_num); free(file_str); } @@ -541,14 +553,16 @@ static void send_location_result(const char *params) { cut_json_int(&col_field, 6); // Build the LSP Location response - const char *fmt = - "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":" - "{\"uri\":\"%s\",\"range\":{\"start\":{\"line\":%s,\"character\":%s}," - "\"end\":{\"line\":%s,\"character\":%s}}}}"; + const char *fmt = "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":" + "{\"uri\":\"%s\",\"range\":{\"start\":{\"line\":%s," + "\"character\":%s}," + "\"end\":{\"line\":%s,\"character\":%s}}}}"; - size_t len = strlen(uri) + strlen(id) + strlen(line_field) * 2 + strlen(col_field) * 2 + 256; + size_t len = strlen(uri) + strlen(id) + strlen(line_field) * 2 + + strlen(col_field) * 2 + 256; char *res = (char *)malloc(len); - snprintf(res, len - 1, fmt, id, uri, line_field, col_field, line_field, col_field); + snprintf(res, len - 1, fmt, id, uri, line_field, col_field, line_field, + col_field); send_ls_response(id, res); free(res); } @@ -581,7 +595,8 @@ static void send_document_symbols_result(const char *params) { return; } - // Build the result - we'll pass through the symbols array as-is since Lua already formatted it correctly + // Build the result - we'll pass through the symbols array as-is since Lua + // already formatted it correctly. // The Lua code sends DocumentSymbol objects with all required fields size_t result_size = (arr_end - arr_start) + 256; char *result = (char *)malloc(result_size); @@ -592,7 +607,9 @@ static void send_document_symbols_result(const char *params) { strncpy(array_content, arr_start, array_len); array_content[array_len] = '\0'; - snprintf(result, result_size, "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}", id, array_content); + snprintf(result, result_size, + "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}", id, + array_content); send_ls_response(id, result); free(array_content); diff --git a/nvimcom/src/apps/tcp.c b/nvimcom/src/apps/tcp.c index 2a7f4be3..1e580972 100644 --- a/nvimcom/src/apps/tcp.c +++ b/nvimcom/src/apps/tcp.c @@ -138,7 +138,8 @@ static void ParseMsg(char *b) { "{\"line\":%d,\"character\":%d}}}}"; size_t len = strlen(def_file) + strlen(def_id) + 256; char *res = (char *)malloc(len); - snprintf(res, len - 1, fmt, def_id, def_file, line, col, line, col); + snprintf(res, len - 1, fmt, def_id, def_file, line, col, + line, col); send_ls_response(def_id, res); free(res); } @@ -162,18 +163,21 @@ static void ParseMsg(char *b) { char *result = (char *)malloc(result_size); char *p = result; p += snprintf(p, result_size, - "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":[", multi_id); + "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":[", + multi_id); for (int i = 0; i < count; i++) { char *m_file = b; b = strstr(b, "|"); - if (!b) break; + if (!b) + break; *b = '\0'; b++; char *m_line_str = b; b = strstr(b, "|"); - if (!b) break; + if (!b) + break; *b = '\0'; b++; @@ -184,13 +188,15 @@ static void ParseMsg(char *b) { b = next + 1; } - int m_line = atoi(m_line_str) - 1; // 1-indexed to 0-indexed + int m_line = + atoi(m_line_str) - 1; // 1-indexed to 0-indexed int m_col = atoi(m_col_str); if (i > 0) { p += snprintf(p, result_size - (p - result), ","); } - p += snprintf(p, result_size - (p - result), + p += snprintf( + p, result_size - (p - result), "{\"uri\":\"file://%s\",\"range\":{\"start\":" "{\"line\":%d,\"character\":%d},\"end\":" "{\"line\":%d,\"character\":%d}}}",