Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion R/document.R
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@
# row and col are 0-indexed
text <- self$line0(row)
matches <- scan_token(text, col, forward)
return(list(

Check warning on line 173 in R/document.R

View workflow job for this annotation

GitHub Actions / lint

file=R/document.R,line=173,col=13,[return_linter] Use implicit return behavior; explicit return() is not needed.
full_token = matches$full_token,
right_token = matches$right_token,
package = empty_string_to_null(matches$package),
Expand Down Expand Up @@ -851,6 +851,22 @@
} else if (is.null(doc$requested_packages)) {
doc$requested_packages <- requested_packages
}
} else if (length(parse_data$member_data$packages)) {
# A newly pasted namespace receiver can end at $ or @ before any
# valid parse has requested its metadata. Retain the last valid imports
# while resolving only syntax-confirmed namespace hints from recovery.
requested_packages <- previous_packages
if (is.null(requested_packages)) requested_packages <- list(
packages = normalize_package_request(doc$loaded_packages), namespace_packages = character())
requested_packages$namespace_packages <- normalize_package_request(union(
requested_packages$namespace_packages, parse_data$member_data$packages))
if (!identical(doc$requested_packages, requested_packages) || isTRUE(doc$member_refresh)) {
doc$member_refresh <- FALSE
doc$requested_packages <- requested_packages
self$resolve_task_manager$add_task(uri,
resolve_task(self, uri, doc, requested_packages$packages,
namespace_packages = requested_packages$namespace_packages))
}
}

if (is.function(self$queue_pending_replies)) {
Expand Down Expand Up @@ -882,6 +898,8 @@
}
if (index_context_waiting(self, self$get_workspace(uri), document, point,
hover = identical(method, "textDocument/hover"), call = call)) return(FALSE)
if (identical(method, "textDocument/completion") &&
member_metadata_waiting(self, uri, document, point)) return(FALSE)
if (s3_metadata_waiting(self, uri, document, point,
hover = identical(method, "textDocument/hover"), call = call)) return(FALSE)
}
Expand Down Expand Up @@ -1044,6 +1062,7 @@
protect <- unique(unlist(lapply(open_documents, function(document) {
c(document$requested_packages$packages, document$requested_packages$namespace_packages)
}), use.names = FALSE))
protect <- union(protect, workspace$startup_member_packages)
for (package in names(packages$members)) {
snapshot <- packages$members[[package]]
if (inherits(workspace$member_metadata, "MemberMetadataCache")) {
Expand Down Expand Up @@ -1089,7 +1108,10 @@
callback = function(result) {
if (current()) resolve_callback(self, uri, version, result)
},
error = function(e) logger$info("member metadata task:", e),
error = function(e) {
on.exit(s3_queue_ready(self, uri), add = TRUE)
logger$info("member metadata task:", e)
},
profiles = FALSE
)
}
Expand Down
62 changes: 62 additions & 0 deletions R/handlers-langfeatures.R
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,66 @@ index_context_waiting <- function(self, workspace, document, point, hover = FALS
TRUE
}

# Namespace member models run independently of S3 catalogs. Wait only for
# packages used by this receiver, never unrelated source-member requests.
member_metadata_waiting <- function(self, uri, document, point) {
if (!isTRUE(lsp_settings$get("member_completion"))) return(FALSE)
manager <- if (is.null(self$member_task_manager)) self$resolve_task_manager else self$member_task_manager
if (is.null(manager) || !is.function(manager$has) || !manager$has(uri)) return(FALSE)
cursor <- member_cursor(document, point)
if (is.null(cursor)) return(FALSE)
recovered <- tryCatch(member_recover_scope(document, point, cursor, document$parse_data$member_data),
error = function(e) NULL)
if (is.null(recovered)) return(FALSE)
referenced <- member_source_names(recovered$parsed, recovered$source, recovered$path,
accessor = cursor$accessor)
expressions <- list(recovered$parsed)
seen <- character()
for (pass in seq_len(16L)) {
pending <- setdiff(referenced, seen)
if (!length(pending) || length(seen) > 256L) break
seen <- c(seen, pending)
for (name in pending) {
history <- document$parse_data$member_data$bindings[[name]]
position <- member_history_position(history, recovered$start)
if (position) {
expression <- history[[position]]$expr
referenced <- union(referenced, member_syntax_names(expression))
expressions[[length(expressions) + 1L]] <- expression
}
}
}
packages <- intersect(document$requested_packages$namespace_packages, referenced)
if (!length(packages)) return(FALSE)
metadata <- self$get_workspace(uri)$member_metadata
if (is.null(metadata)) return(TRUE)
requests <- new.env(parent = emptyenv())
requests$ready <- FALSE
requests$objects <- list()
requests$private <- character()
any(vapply(packages, function(package) {
catalog <- if (is.function(metadata$catalog)) metadata$catalog(package) else metadata$get(package, NULL)
if (is.null(catalog) || !is.null(catalog$error)) return(TRUE)
if (!isTRUE(catalog$dataset_only) || isTRUE(catalog$namespace_attempted)) return(FALSE)
# Startup catalogs contain only public datasets. A dataset already in
# that catalog is usable, but regular or private objects must wait for
# the pending namespace upgrade. Failed bounded upgrades are final.
if (!requests$ready) {
for (expression in expressions) member_walk(expression, function(node) {
if (length(node) != 3L || !(member_head(node, "::") || member_head(node, ":::"))) return()
name <- member_name(node[[2L]])
object <- member_name(node[[3L]])
if (is.null(name) || is.null(object)) return()
requests$objects[[name]] <- union(requests$objects[[name]], object)
if (member_head(node, ":::")) requests$private <- union(requests$private, name)
})
requests$ready <- TRUE
}
objects <- requests$objects[[package]]
package %in% requests$private || !length(objects) || !all(objects %in% names(catalog$roots))
}, logical(1L)))
}

#' `textDocument/completion` request handler
#'
#' Handler to the `textDocument/completion` [Request].
Expand All @@ -71,9 +131,11 @@ text_document_completion <- function(self, id, params) {
}
if (identical(document$version, document$parse_data$version) &&
(index_context_waiting(self, workspace, document, point) ||
member_metadata_waiting(self, uri, document, point) ||
s3_metadata_waiting(self, uri, document, point, hover = FALSE))) {
enqueue_latest_reply(self, uri, "textDocument/completion", list(id = id, version = document$version, params = params))
if (is.function(self$resolve_task_manager$promote)) self$resolve_task_manager$promote(uri)
if (is.function(self$member_task_manager$promote)) self$member_task_manager$promote(uri)
return(invisible(NULL))
}
# didChange schedules a debounced background parse. Scope and member
Expand Down
7 changes: 7 additions & 0 deletions R/handlers-workspace.R
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,15 @@ workspace_did_change_configuration <- function(self, params) {
))
previous_backends <- backend_settings_identity()
previous_s3 <- isTRUE(lsp_settings$get("s3_dispatch"))
previous_members <- isTRUE(lsp_settings$get("member_completion"))
lsp_settings$update_from_workspace(settings)

if (!previous_members && isTRUE(lsp_settings$get("member_completion"))) {
for (workspace in self$workspaces$values()) {
if (is.function(workspace$load_startup_members)) workspace$load_startup_members()
}
}

if (!previous_s3 && isTRUE(lsp_settings$get("s3_dispatch")) &&
!is.null(self$resolve_task_manager)) {
for (workspace in self$workspaces$values()) {
Expand Down
2 changes: 1 addition & 1 deletion R/languageclient.R
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ LanguageClient <- R6::R6Class("LanguageClient",
initialize = function(cmd = NULL, args = NULL) {
if (!is.null(cmd)) {
self$process <- processx::process$new(cmd, args,
stdin = "|", stdout = "|")
stdin = "|", stdout = "|", stderr = "|")
}
self$raw_input_available <- "conn_read_bytes" %in% getNamespaceExports("processx")
self$diagnostics <- collections::dict()
Expand Down
64 changes: 58 additions & 6 deletions R/member-completion.R
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
# Document indexes contain syntax and positions, never user values. Recovery
# parses a bounded current expression; earlier statements come from the index.
member_incomplete_packages <- function(content) {
sentinel <- ".__languageserver_namespace_hint__"
if (length(content) > 129L || sum(nchar(content, type = "bytes")) > 65536L ||
any(grepl(sentinel, content, fixed = TRUE))) return(character())
repaired <- c(content, sentinel)
parsed <- tryCatch(str2expression(paste0(paste(repaired, collapse = "\n"),
missing_closing_delimiters(repaired))), error = function(e) NULL)
packages <- character()
member_walk(parsed, function(node) {
if (length(node) == 3L && (member_head(node, "$") || member_head(node, "@")) &&
identical(member_name(node[[3L]]), sentinel)) {
member_walk(node[[2L]], function(receiver) {
if (length(receiver) == 3L && (member_head(receiver, "::") || member_head(receiver, ":::"))) {
package <- member_name(receiver[[2L]])
if (!is.null(package)) packages <<- union(packages, package)
}
})
}
})
packages
}

member_document_index <- function(content, parsed = NULL, syntax = NULL) {
complete <- !is.null(parsed)
recovered_packages <- character()
items <- list()
append <- function(expr, start, end) {
items[[length(items) + 1L]] <<- list(expr = expr, start = start, end = end)
Expand Down Expand Up @@ -58,6 +81,9 @@ member_document_index <- function(content, parsed = NULL, syntax = NULL) {
NULL
})
if (is.null(parsed)) {
if (grepl("unexpected end of input", message)) {
recovered_packages <- union(recovered_packages, member_incomplete_packages(text))
}
# Discard an invalid single line but retain incomplete calls.
if (!grepl("unexpected end of input|INCOMPLETE_STRING", message)) start <- end + 1L
end <- end + 1L
Expand All @@ -72,7 +98,7 @@ member_document_index <- function(content, parsed = NULL, syntax = NULL) {
s4 <- list()
imports <- list()
effects <- list()
packages <- character()
packages <- recovered_packages
class_scope <- FALSE
symbol_scope <- FALSE
parts <- if (complete) member_function_parts(if (is.null(syntax)) utils::getParseData(parsed) else syntax) else NULL
Expand Down Expand Up @@ -412,17 +438,29 @@ member_context_index <- function(workspace, uri, document, at, parsed = NULL,
if (is.null(data)) {
return(index)
}
# The startup vector is in attachment order (the search path reversed),
# so later packages overwrite earlier ones just as R masks their names.
# A cached namespace alone must not make its bare data names visible.
for (package in workspace$startup_packages) {
package_index <- member_lookup(index$namespace_indices, package)
if (is.null(package_index)) next
index$attached_roots[names(package_index$roots)] <- package_index$roots
if (!is.null(package_index$s4_classes)) {
index$attached_s4[names(package_index$s4_classes)] <- package_index$s4_classes
}
}
for (item in data$imports) {
if (!member_before(item$end, at)) next
expr <- item$expr
if (length(expr) < 2L) next
if (member_head(expr, "library") || member_head(expr, "require")) {
package <- member_name(expr[[2L]])
if (!is.null(package) && package %in% names(index$namespace_indices)) {
index$attached_roots <- utils::modifyList(index$attached_roots, index$namespace_indices[[package]]$roots)
roots <- index$namespace_indices[[package]]$roots
index$attached_roots[names(roots)] <- roots
package_index <- index$namespace_indices[[package]]
if (!is.null(package_index$s4_classes)) {
index$attached_s4 <- utils::modifyList(index$attached_s4, package_index$s4_classes)
index$attached_s4[names(package_index$s4_classes)] <- package_index$s4_classes
}
functions <- if (isTRUE(package_index$.metadata_lazy)) {
package_index$functions
Expand Down Expand Up @@ -513,6 +551,18 @@ member_cursor_value <- function(parsed, sentinel, index, bindings, budget, name
if (member_head(node, accessor) && identical(member_name(node[[3L]]), sentinel)) {
if (is.null(name)) {
result <<- member_infer(node[[2L]], index, env, budget = budget)
if (identical(accessor, "$") && isTRUE(result$default_dollar)) {
context <- member_package_context(index, result$member_package)
if (is.null(context) || !member_default_dollar(result, context, env)) {
if (member_dataframe_columns(result)) {
result$column_names_only <<- TRUE
result$default_dollar <<- NULL
result$reason <<- "unresolved_dollar_dispatch"
} else {
result <<- member_value(reason = "unresolved_dollar_dispatch")
}
}
}
} else {
receiver <- member_infer(node[[2L]], index, env, budget = budget)
result <<- if (identical(accessor, "@")) {
Expand Down Expand Up @@ -572,7 +622,8 @@ member_cursor_value <- function(parsed, sentinel, index, bindings, budget, name
if ((member_head(child, "<-") || member_head(child, "=")) && is.symbol(child[[2L]])) {
assigned <- as.character(child[[2L]])
declaration <- any(member_syntax_names(child[[3L]]) %in% c("setClass", "setClassUnion"))
if (!assigned %in% needed && !declaration) {
dispatch <- startsWith(assigned, "$.") || startsWith(assigned, "[[.")
if (!assigned %in% needed && !declaration && !dispatch) {
keep[[i]] <- FALSE
} else {
needed <- union(setdiff(needed, assigned), member_syntax_names(child[[3L]]))
Expand Down Expand Up @@ -887,10 +938,11 @@ member_completion <- function(uri, workspace, document, point, snippet_support,
return(NULL)
}
value <- resolved$value
index <- resolved$index
index <- member_package_context(resolved$index, value$member_package)
bindings <- resolved$bindings
budget <- resolved$budget
if (is.null(value) || !length(value$type)) {
if (identical(value$reason, "unresolved_dollar_dispatch") && !member_dataframe_columns(value)) return(list())
if (is.null(index) || is.null(value) || !length(value$type)) {
return(NULL)
}
members <- member_members(value, index, bindings, cursor$accessor)
Expand Down
36 changes: 28 additions & 8 deletions R/member-extraction.R
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ member_constructor <- function(fn) {
return(NULL)
}
statements <- as.list(body)[-1L]
if (!length(statements)) return(NULL)
last <- statements[[length(statements)]]
if (member_head(last, "return") && length(last) == 2L) last <- last[[2L]]
target <- member_name(last)
Expand Down Expand Up @@ -215,6 +216,8 @@ member_package_index <- function(input) {
index$definitions <- input$definitions
index$locations <- input$locations
index$exports <- input$exports
index$subset_registry <- input$subset_registry
index$subset_registry_complete <- isTRUE(input$subset_registry_complete)
index$registries <- if (!is.null(input$registries)) input$registries else member_source_registries(input)
index$intrinsics <- if (!is.null(input$intrinsics)) input$intrinsics else character()
index$nonreturning_functions <- intersect(c("stop", "abort"), c(member_base_intrinsics, index$intrinsics))
Expand Down Expand Up @@ -309,21 +312,36 @@ member_package_index <- function(input) {
}
}
}
# Every ordinary environment surface is discovered, including static bundles
# Every static object surface is discovered, including named lists, bundles
# and exported roots. Active names are retained but getters are only analyzed.
for (name in names(input$snapshots)) {
snapshot <- input$snapshots[[name]]
shape <- member_snapshot_shape(snapshot)
shape$type <- name
shape <- member_snapshot_shape(snapshot, input$package)
if (identical(snapshot$kind, "environment")) shape$type <- name
for (field in names(index$registries[[name]])) {
if (is.null(shape$fields[[field]]$s7_generator)) {
shape$fields[field] <- list(member_value(function_key = index$registries[[name]][[field]]))
stored <- shape$fields[[field]]
method <- if (identical(snapshot$kind, "r6_instance") || !is.null(stored$s4_class) ||
length(stored$closure)) stored else member_value()
method$function_key <- index$registries[[name]][[field]]
method$metadata <- input$package
shape$fields[field] <- list(method)
}
}
for (field in names(input$links[[name]])) {
linked <- shape$fields[[field]]
if (identical(linked$type, "environment") && is.null(linked$r6_role)) {
shape$fields[field] <- list(member_value(type = input$links[[name]][[field]],
classes = linked$classes, default_dollar = linked$default_dollar,
member_package = input$package))
}
}
for (field in names(input$links[[name]])) shape$fields[field] <- list(member_value(type = input$links[[name]][[field]]))
index$package_roots[name] <- list(shape)
for (field in names(snapshot$fields)) {
for (field in names(shape$fields)) {
if (!field %in% names(index$members[[name]])) index$members[[name]][field] <- NA_character_
if (identical(snapshot$kind, "environment") && is.null(shape$fields[[field]]$function_key)) {
index$properties[[name]][field] <- list(shape$fields[[field]])
}
}
}
# Registry lookup branches describe membership, precedence, lexical receiver
Expand Down Expand Up @@ -553,9 +571,11 @@ member_package_index <- function(input) {
for (name in names(input$s7_roots)) index$package_roots[name] <- input$s7_roots[name]
for (name in names(input$s4_objects)) {
class <- input$s4_objects[[name]]
index$package_roots[name] <- list(member_s4_shape(class$name, index, list(), class$package))
if (is.null(index$package_roots[[name]]$s4_class)) {
index$package_roots[name] <- list(member_s4_shape(class$name, index, list(), class$package))
}
}
index$roots <- index$package_roots[intersect(names(index$package_roots), index$exports)]
index$roots <- member_public_roots(index, input)
for (name in names(index$roots)) index$namespace_roots[paste(input$package, name, sep = "::")] <- list(index$roots[[name]])
member_prepare_returns(index)
index$cache <- new.env(parent = emptyenv())
Expand Down
Loading
Loading