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
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# languageserver 0.3.20

- Keep parameter-name inlay hints for untouched calls during incomplete edits,
moving their positions with incremental changes and refreshing after a valid
parse, so a temporary syntax error does not clear hints across the document.

- Enclose non-syntactic completion names in backticks and preserve escaped names
in function snippets, including completion inside partially typed quoted names (#781).
- Complete chained `$` members using static receiver analysis, including source
Expand Down
26 changes: 23 additions & 3 deletions R/document.R
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Document <- R6::R6Class(
content = NULL,
call_scan_cache = NULL,
parse_data = NULL,
inlay_hint_data = NULL,
inlay_hint_formals = NULL,
is_rmarkdown = NULL,
regions = NULL,
loaded_packages = NULL,
Expand All @@ -35,7 +37,10 @@ Document <- R6::R6Class(
self$is_open <- FALSE
},

set_content = function(version, content) {
set_content = function(version, content, change = NULL) {
if (!identical(self$content, content)) {
self$inlay_hint_data <- inlay_hint_apply_change(self$inlay_hint_data, change, self$content)
}
self$version <- version
self$nline <- length(content)
self$content <- content
Expand Down Expand Up @@ -94,12 +99,18 @@ Document <- R6::R6Class(
} else {
character()
}
self$set_content(version, c(before, changed, after))
self$set_content(version, c(before, changed, after), change)
}
},

update_parse_data = function(parse_data) {
self$parse_data <- parse_data
if (!isTRUE(parse_data$parse_error) && !isTRUE(parse_data$inlay_hint_incomplete)) {
self$inlay_hint_data <- parse_data$range_data[c(
"calls", "arguments", "argument_order", "argument_lines"
)]
self$inlay_hint_formals <- new.env(parent = emptyenv())
}
},

line = function(row) {
Expand Down Expand Up @@ -554,7 +565,14 @@ get_content_hash <- function(content) {

parse_document <- function(uri, content, is_rmarkdown = FALSE,
content_hash = NULL) {
content <- normalize_parse_content(content, is_rmarkdown)
parse_content <- normalize_parse_content(content, is_rmarkdown)
inlay_hint_incomplete_lines <- if (is_rmarkdown) {
which(parse_content != normalize_parse_content(
content, is_rmarkdown, parseable_only = FALSE)) - 1L
} else {
integer()
}
content <- parse_content
if (is.null(content_hash)) content_hash <- get_content_hash(content)

parse_env <- function() {
Expand All @@ -578,6 +596,8 @@ parse_document <- function(uri, content, is_rmarkdown = FALSE,
NULL, content, env$completion_data, uri, env$definitions)
env$content_hash <- content_hash
env$parse_error <- FALSE
env$inlay_hint_incomplete <- length(inlay_hint_incomplete_lines) > 0L
env$inlay_hint_incomplete_lines <- inlay_hint_incomplete_lines
env
}
env <- parse_env()
Expand Down
131 changes: 125 additions & 6 deletions R/inlay_hint.R
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,118 @@ match_named_formal <- function(name, formal_names) {
if (length(partial) == 1L) partial else NA_integer_
}

#' Move untouched calls with an incremental edit, discarding intersecting calls
#' @noRd
inlay_hint_apply_change <- function(indexed, change, content) {
if (is.null(indexed) || is.null(change$range)) return(NULL)
calls <- indexed$calls
if (!length(calls$name)) return(indexed)
start <- change$range$start
end <- change$range$end
before <- calls$end_line < start$line |
calls$end_line == start$line & calls$end_col <= start$character
after <- calls$line > end$line |
calls$line == end$line & calls$col >= end$character
line_text <- function(row) if (row < length(content)) content[[row + 1L]] else ""
# Inserts, replacements and deletions ending at a callee can change its
# name or turn it into a member call, even with spaces or tabs in between.
# Only retain whitespace replacements here; an empty replacement can
# join the callee to preceding text.
if (!nzchar(change$text) || grepl("[^[:space:]]", change$text)) {
end_text <- line_text(end$line)
end_col <- code_point_from_unit(end_text, end$character)
following <- substring(end_text, end_col + 1L)
first_token <- regexpr("[^[:space:]]", following)[[1L]]
boundary <- if (first_token > 0L) {
code_point_to_unit(end_text, end_col + first_token - 1L)
} else {
end$character
}
at_start <- calls$line == end$line & calls$col == boundary
after[at_start] <- FALSE
}
# An edit outside a call can still put it in a comment, string or
# backtick name. Drop following calls when lexical context may change.
removed <- get_range_text(
content, start$line + 1L,
code_point_from_unit(line_text(start$line), start$character) + 1L,
end$line + 1L,
code_point_from_unit(line_text(end$line), end$character)
)
if (any(grepl("[#\"'`\\\\]", c(change$text, removed), useBytes = TRUE), na.rm = TRUE)) {
after[] <- FALSE
}
keep <- before | after
if (!any(keep)) return(NULL)

replacement <- stringi::stri_split_lines(change$text)[[1L]]
new_end_line <- start$line + length(replacement) - 1L
new_end_col <- code_point_to_unit(utils::tail(replacement, 1L), Inf)
if (length(replacement) == 1L) new_end_col <- new_end_col + start$character
shift <- function(lines, cols, move) {
same_line <- move & !is.na(lines) & lines == end$line
cols[same_line] <- cols[same_line] + new_end_col - end$character
lines[move] <- lines[move] + new_end_line - end$line
list(line = lines, col = cols)
}
for (fields in list(c("line", "col"), c("end_line", "end_col"))) {
moved <- shift(calls[[fields[[1L]]]], calls[[fields[[2L]]]], after)
calls[[fields[[1L]]]] <- moved$line
calls[[fields[[2L]]]] <- moved$col
}
arguments <- indexed$arguments
moved <- shift(arguments$line, arguments$col, after[arguments$call])
arguments$line <- moved$line
arguments$col <- moved$col

kept_arguments <- which(keep[arguments$call])
arguments <- lapply(arguments, `[`, kept_arguments)
arguments$call <- match(arguments$call, which(keep))
calls <- lapply(calls, `[`, which(keep))
calls$first_argument <- match(calls$first_argument, kept_arguments)
calls$last_argument <- match(calls$last_argument, kept_arguments)
indexed$calls <- calls
indexed$arguments <- arguments
indexed$argument_order <- which(arguments$present)
indexed$argument_order <- indexed$argument_order[order(
arguments$line[indexed$argument_order], arguments$col[indexed$argument_order]
)]
indexed$argument_lines <- arguments$line[indexed$argument_order]
indexed
}

#' Extract parameter-name inlay hints for calls in a requested range
#' @noRd
inlay_hint_reply <- function(id, uri, workspace, document, request_range) {
parse_data <- current_parse_data(uri, workspace, document)
if (is.null(parse_data)) return(NULL)
incomplete <- isTRUE(parse_data$parse_error) || isTRUE(parse_data$inlay_hint_incomplete)
if (incomplete && !is.null(document$inlay_hint_data)) {
previous <- indexed_inlay_hint_reply(
id, uri, workspace, document$inlay_hint_data, request_range,
document$inlay_hint_formals, use_previous_formals = TRUE,
regions = document$regions,
fallback_lines = if (isTRUE(parse_data$parse_error)) NULL else
parse_data$inlay_hint_incomplete_lines
)
if (isTRUE(parse_data$parse_error)) return(previous)
# Literate documents still parse their other R cells successfully.
current <- indexed_inlay_hint_reply(
id, uri, workspace, parse_data$range_data, request_range
)
hints <- c(current$result, previous$result)
positions <- vapply(hints, function(hint) {
paste(hint$position$line, hint$position$character, sep = ":")
}, character(1L))
return(Response$new(id, result = utils::head(hints[!duplicated(positions)], 200L)))
}
xdoc <- parse_data$xml_doc
if (is.null(xdoc)) return(Response$new(id, result = list()))

if (!is.null(parse_data$range_data)) {
return(indexed_inlay_hint_reply(
id, uri, workspace, parse_data$range_data, request_range
id, uri, workspace, parse_data$range_data, request_range,
document$inlay_hint_formals
))
}

Expand Down Expand Up @@ -177,7 +278,9 @@ inlay_hint_reply <- function(id, uri, workspace, document, request_range) {

#' Resolve hints only for calls with an argument visible in the viewport
#' @noRd
indexed_inlay_hint_reply <- function(id, uri, workspace, indexed, request_range) {
indexed_inlay_hint_reply <- function(id, uri, workspace, indexed, request_range,
previous_formals = NULL, use_previous_formals = FALSE, regions = NULL,
fallback_lines = NULL) {
arguments <- indexed$arguments
visible <- indexed$argument_order[range_line_indices(
indexed$argument_lines,
Expand All @@ -186,6 +289,12 @@ indexed_inlay_hint_reply <- function(id, uri, workspace, indexed, request_range)
visible <- visible[range_position_selected(
arguments$line[visible], arguments$col[visible], request_range
)]
if (!is.null(regions)) {
visible <- visible[regions$line_type[arguments$line[visible] + 1L] == "r"]
}
if (!is.null(fallback_lines)) {
visible <- visible[arguments$line[visible] %in% fallback_lines]
}
if (!length(visible)) return(Response$new(id, result = list()))
calls <- indexed$calls
call_indices <- sort(unique(arguments$call[visible]))
Expand Down Expand Up @@ -215,10 +324,20 @@ indexed_inlay_hint_reply <- function(id, uri, workspace, indexed, request_range)
if (exists(cache_key, envir = formals_cache, inherits = FALSE)) {
formal_names <- get(cache_key, envir = formals_cache, inherits = FALSE)
} else {
function_formals <- tryCatch(call_with_optional_uri(
workspace$get_formals, function_name, package, uri = uri),
error = function(e) NULL)
formal_names <- names(function_formals)
formal_names <- if (use_previous_formals && !is.null(previous_formals)) {
get0(cache_key, envir = previous_formals, inherits = FALSE)
} else {
NULL
}
if (is.null(formal_names)) {
function_formals <- tryCatch(call_with_optional_uri(
workspace$get_formals, function_name, package, uri = uri),
error = function(e) NULL)
formal_names <- names(function_formals)
if (!use_previous_formals && !is.null(previous_formals)) {
assign(cache_key, formal_names, envir = previous_formals)
}
}
assign(cache_key, formal_names, envir = formals_cache)
}
if (!length(formal_names)) next
Expand Down
10 changes: 8 additions & 2 deletions R/provider_index.R
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ range_provider_parse_data <- function(data, content) {
),
calls = list(
name = character(), package = character(),
first_argument = integer(), last_argument = integer()
first_argument = integer(), last_argument = integer(),
line = integer(), col = integer(),
end_line = integer(), end_col = integer()
),
arguments = list(
call = integer(), line = integer(), col = integer(),
Expand Down Expand Up @@ -57,7 +59,11 @@ range_provider_parse_data <- function(data, content) {
name = data$text[index[["function"]]],
package = text_at(index$package),
first_argument = index$first_argument,
last_argument = index$last_argument
last_argument = index$last_argument,
line = as.integer(data$line1[index$call] - 1L),
col = convert_columns(data$line1[index$call] - 1L, data$col1[index$call] - 1L),
end_line = as.integer(data$line2[index$call] - 1L),
end_col = convert_columns(data$line2[index$call] - 1L, data$col2[index$call])
)
first <- index$argument_first
present <- first > 0L
Expand Down
Loading
Loading