Skip to content

Commit 60e95d6

Browse files
Detect being in the browser state when multiline input is sent to R one line at a time (#795)
Co-authored-by: Lionel Henry <[email protected]>
1 parent 271e3a2 commit 60e95d6

File tree

2 files changed

+62
-5
lines changed

2 files changed

+62
-5
lines changed

crates/ark/src/interface.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,7 @@ impl RMain {
707707
buflen: c_int,
708708
_hist: c_int,
709709
) -> ConsoleResult {
710-
let info = Self::prompt_info(prompt);
710+
let info = self.prompt_info(prompt);
711711
log::trace!("R prompt: {}", info.input_prompt);
712712

713713
// Upon entering read-console, finalize any debug call text that we were capturing.
@@ -918,20 +918,27 @@ impl RMain {
918918
// We prefer to panic if there is an error while trying to determine the
919919
// prompt type because any confusion here is prone to put the frontend in a
920920
// bad state (e.g. causing freezes)
921-
fn prompt_info(prompt_c: *const c_char) -> PromptInfo {
921+
fn prompt_info(&self, prompt_c: *const c_char) -> PromptInfo {
922922
let n_frame = harp::session::r_n_frame().unwrap();
923923
log::trace!("prompt_info(): n_frame = '{n_frame}'");
924924

925925
let prompt_slice = unsafe { CStr::from_ptr(prompt_c) };
926926
let prompt = prompt_slice.to_string_lossy().into_owned();
927927

928+
let continuation_prompt: String = harp::get_option("continue").try_into().unwrap();
929+
let matches_continuation = prompt == continuation_prompt;
930+
928931
// Detect browser prompt by matching the prompt string
929932
// https://github.com/posit-dev/positron/issues/4742.
930933
// There are ways to break this detection, for instance setting
931934
// `options(prompt =, continue = ` to something that looks like
932935
// a browser prompt, or doing the same with `readline()`. We have
933936
// chosen to not support these edge cases.
934-
let browser = RE_DEBUG_PROMPT.is_match(&prompt);
937+
// Additionally, we send code to R one line at a time, so even if we are debugging
938+
// it can look like we are in a continuation state. To try and detect that, we
939+
// detect if we matched the continuation prompt while the DAP is active.
940+
let browser =
941+
RE_DEBUG_PROMPT.is_match(&prompt) || (self.dap.is_debugging() && matches_continuation);
935942

936943
// If there are frames on the stack and we're not in a browser prompt,
937944
// this means some user code is requesting input, e.g. via `readline()`
@@ -941,8 +948,6 @@ impl RMain {
941948
// we're in a user request, e.g. `readline("+ ")`. To guard against
942949
// this, we check that we are at top-level (call stack is empty or we
943950
// have a debug prompt).
944-
let continuation_prompt: String = harp::get_option("continue").try_into().unwrap();
945-
let matches_continuation = prompt == continuation_prompt;
946951
let top_level = n_frame == 0 || browser;
947952
let incomplete = matches_continuation && top_level;
948953

crates/ark/tests/kernel.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,58 @@ fn test_execute_request_browser_incomplete() {
250250
assert_eq!(frontend.recv_shell_execute_reply(), input.execution_count);
251251
}
252252

253+
// Test that a multiline input in the browser doesn't throw off our prompt info
254+
// detection logic https://github.com/posit-dev/positron/issues/5928
255+
#[test]
256+
fn test_execute_request_browser_multiline() {
257+
let frontend = DummyArkFrontend::lock();
258+
259+
// Wrap in a function to get a frame on the stack so we aren't at top level.
260+
// Careful to not send any newlines after `fn()`, as that advances the debugger!
261+
let code = "
262+
fn <- function() {
263+
browser()
264+
}
265+
fn()";
266+
frontend.send_execute_request(code, ExecuteRequestOptions::default());
267+
frontend.recv_iopub_busy();
268+
269+
let input = frontend.recv_iopub_execute_input();
270+
assert_eq!(input.code, code);
271+
272+
// We aren't at top level, so this comes as an iopub stream
273+
frontend.recv_iopub_stream_stdout("Called from: fn()\n");
274+
frontend.recv_iopub_idle();
275+
276+
assert_eq!(frontend.recv_shell_execute_reply(), input.execution_count);
277+
278+
// Execute a multiline statement while paused in the debugger
279+
let code = "1 +
280+
1";
281+
frontend.send_execute_request(code, ExecuteRequestOptions::default());
282+
frontend.recv_iopub_busy();
283+
284+
let input = frontend.recv_iopub_execute_input();
285+
assert_eq!(input.code, code);
286+
287+
// Also received as iopub stream because we aren't at top level, we are in the debugger
288+
frontend.recv_iopub_stream_stdout("[1] 2\n");
289+
frontend.recv_iopub_idle();
290+
291+
assert_eq!(frontend.recv_shell_execute_reply(), input.execution_count);
292+
293+
let code = "Q";
294+
frontend.send_execute_request(code, ExecuteRequestOptions::default());
295+
frontend.recv_iopub_busy();
296+
297+
let input = frontend.recv_iopub_execute_input();
298+
assert_eq!(input.code, code);
299+
300+
frontend.recv_iopub_idle();
301+
302+
assert_eq!(frontend.recv_shell_execute_reply(), input.execution_count);
303+
}
304+
253305
#[test]
254306
fn test_execute_request_browser_stdin() {
255307
let frontend = DummyArkFrontend::lock();

0 commit comments

Comments
 (0)