Skip to content

Commit aa36bfd

Browse files
committed
triple backticks hint upon pressing enter with unclosed backticks
1 parent 7025364 commit aa36bfd

File tree

2 files changed

+137
-23
lines changed

2 files changed

+137
-23
lines changed

crates/chat-cli/src/cli/chat/prompt.rs

Lines changed: 135 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,30 @@ impl PasteState {
9797
}
9898
}
9999

100+
/// Shared state to track when Enter was pressed with unclosed backticks
101+
#[derive(Clone, Debug)]
102+
pub struct MultilineHintState {
103+
inner: Arc<Mutex<bool>>,
104+
}
105+
106+
impl MultilineHintState {
107+
pub fn new() -> Self {
108+
Self {
109+
inner: Arc::new(Mutex::new(false)),
110+
}
111+
}
112+
113+
pub fn set(&self, value: bool) {
114+
let mut inner = self.inner.lock().unwrap();
115+
*inner = value;
116+
}
117+
118+
pub fn get(&self) -> bool {
119+
let inner = self.inner.lock().unwrap();
120+
*inner
121+
}
122+
}
123+
100124
pub const COMMANDS: &[&str] = &[
101125
"/clear",
102126
"/help",
@@ -330,15 +354,23 @@ pub struct ChatHinter {
330354
history_hints_enabled: bool,
331355
history_path: PathBuf,
332356
available_commands: Vec<&'static str>,
357+
/// Shared state to track when to show multiline hint
358+
multiline_hint_state: MultilineHintState,
333359
}
334360

335361
impl ChatHinter {
336362
/// Creates a new ChatHinter instance
337-
pub fn new(history_hints_enabled: bool, history_path: PathBuf, available_commands: Vec<&'static str>) -> Self {
363+
pub fn new(
364+
history_hints_enabled: bool,
365+
history_path: PathBuf,
366+
available_commands: Vec<&'static str>,
367+
multiline_hint_state: MultilineHintState,
368+
) -> Self {
338369
Self {
339370
history_hints_enabled,
340371
history_path,
341372
available_commands,
373+
multiline_hint_state,
342374
}
343375
}
344376

@@ -353,6 +385,16 @@ impl ChatHinter {
353385
return None;
354386
}
355387

388+
// Check if we should show the multiline hint (after Enter was pressed with unclosed backticks)
389+
if self.multiline_hint_state.get() && line.contains("```") {
390+
let triple_backtick_count = line.matches("```").count();
391+
if triple_backtick_count % 2 == 1 {
392+
// Clear the state after showing the hint once
393+
self.multiline_hint_state.set(false);
394+
return Some("in multiline mode, waiting for closing backticks ```".to_string());
395+
}
396+
}
397+
356398
// If line starts with a slash, try to find a command hint
357399
if line.starts_with('/') {
358400
return self
@@ -396,7 +438,15 @@ impl RustylineHinter for ChatHinter {
396438
}
397439

398440
/// Custom validator for multi-line input
399-
pub struct MultiLineValidator;
441+
pub struct MultiLineValidator {
442+
multiline_hint_state: MultilineHintState,
443+
}
444+
445+
impl MultiLineValidator {
446+
pub fn new(multiline_hint_state: MultilineHintState) -> Self {
447+
Self { multiline_hint_state }
448+
}
449+
}
400450

401451
impl Validator for MultiLineValidator {
402452
fn validate(&self, os: &mut ValidationContext<'_>) -> rustyline::Result<ValidationResult> {
@@ -408,7 +458,9 @@ impl Validator for MultiLineValidator {
408458
let triple_backtick_count = input.matches("```").count();
409459

410460
// If we have an odd number of ```, we're in an incomplete code block
461+
// When user presses Enter, set the state to show the hint on next render
411462
if triple_backtick_count % 2 == 1 {
463+
self.multiline_hint_state.set(true);
412464
return Ok(ValidationResult::Incomplete);
413465
}
414466
}
@@ -550,6 +602,43 @@ impl rustyline::ConditionalEventHandler for PasteImageHandler {
550602
}
551603
}
552604

605+
/// Handler for right arrow key that prevents completing hints when in multiline mode
606+
///
607+
/// This handler intercepts the right arrow key press to prevent accidentally completing
608+
/// status hints (like "in multiline mode, waiting for closing backticks ```") that appear
609+
/// when the user presses Enter with unclosed triple backticks.
610+
///
611+
/// When unclosed backticks are detected (odd count of ```), pressing right arrow will:
612+
/// - Just move the cursor forward one character (normal behavior)
613+
/// - NOT complete/accept the hint text
614+
///
615+
/// When no unclosed backticks exist, it returns None to allow default behavior.
616+
struct RightArrowHandler;
617+
618+
impl rustyline::ConditionalEventHandler for RightArrowHandler {
619+
fn handle(
620+
&self,
621+
_evt: &rustyline::Event,
622+
_n: rustyline::RepeatCount,
623+
_positive: bool,
624+
ctx: &rustyline::EventContext<'_>,
625+
) -> Option<Cmd> {
626+
let line = ctx.line();
627+
628+
// Check if we're in multiline mode with unclosed backticks
629+
if line.contains("```") {
630+
let triple_backtick_count = line.matches("```").count();
631+
if triple_backtick_count % 2 == 1 {
632+
// We're in multiline mode - don't complete the hint
633+
// Just move the cursor forward instead
634+
return Some(Cmd::Move(rustyline::Movement::ForwardChar(1)));
635+
}
636+
}
637+
638+
None
639+
}
640+
}
641+
553642
pub fn rl(
554643
os: &Os,
555644
sender: PromptQuerySender,
@@ -577,10 +666,18 @@ pub fn rl(
577666
// Generate available commands based on enabled experiments
578667
let available_commands = get_available_commands(os);
579668

669+
// Create shared state for multiline hint
670+
let multiline_hint_state = MultilineHintState::new();
671+
580672
let h = ChatHelper {
581673
completer: ChatCompleter::new(sender, receiver, available_commands.clone()),
582-
hinter: ChatHinter::new(history_hints_enabled, history_path, available_commands),
583-
validator: MultiLineValidator,
674+
hinter: ChatHinter::new(
675+
history_hints_enabled,
676+
history_path,
677+
available_commands,
678+
multiline_hint_state.clone(),
679+
),
680+
validator: MultiLineValidator::new(multiline_hint_state),
584681
};
585682

586683
let mut rl = Editor::with_config(config)?;
@@ -643,6 +740,12 @@ pub fn rl(
643740
EventHandler::Conditional(Box::new(PasteImageHandler::new(paste_state))),
644741
);
645742

743+
// Override right arrow key to prevent completing multiline status hints
744+
rl.bind_sequence(
745+
KeyEvent(KeyCode::Right, Modifiers::empty()),
746+
EventHandler::Conditional(Box::new(RightArrowHandler)),
747+
);
748+
646749
Ok(rl)
647750
}
648751

@@ -712,14 +815,15 @@ mod tests {
712815
// Create a mock Os for testing
713816
let mock_os = crate::os::Os::new().await.unwrap();
714817
let available_commands = get_available_commands(&mock_os);
818+
let multiline_hint_state = MultilineHintState::new();
715819
let helper = ChatHelper {
716820
completer: ChatCompleter::new(
717821
prompt_request_sender,
718822
prompt_response_receiver,
719823
available_commands.clone(),
720824
),
721-
hinter: ChatHinter::new(true, PathBuf::new(), available_commands),
722-
validator: MultiLineValidator,
825+
hinter: ChatHinter::new(true, PathBuf::new(), available_commands, multiline_hint_state.clone()),
826+
validator: MultiLineValidator::new(multiline_hint_state),
723827
};
724828

725829
// Test basic prompt highlighting
@@ -736,14 +840,15 @@ mod tests {
736840
// Create a mock Os for testing
737841
let mock_os = crate::os::Os::new().await.unwrap();
738842
let available_commands = get_available_commands(&mock_os);
843+
let multiline_hint_state = MultilineHintState::new();
739844
let helper = ChatHelper {
740845
completer: ChatCompleter::new(
741846
prompt_request_sender,
742847
prompt_response_receiver,
743848
available_commands.clone(),
744849
),
745-
hinter: ChatHinter::new(true, PathBuf::new(), available_commands),
746-
validator: MultiLineValidator,
850+
hinter: ChatHinter::new(true, PathBuf::new(), available_commands, multiline_hint_state.clone()),
851+
validator: MultiLineValidator::new(multiline_hint_state),
747852
};
748853

749854
// Test warning prompt highlighting
@@ -763,14 +868,15 @@ mod tests {
763868
// Create a mock Os for testing
764869
let mock_os = crate::os::Os::new().await.unwrap();
765870
let available_commands = get_available_commands(&mock_os);
871+
let multiline_hint_state = MultilineHintState::new();
766872
let helper = ChatHelper {
767873
completer: ChatCompleter::new(
768874
prompt_request_sender,
769875
prompt_response_receiver,
770876
available_commands.clone(),
771877
),
772-
hinter: ChatHinter::new(true, PathBuf::new(), available_commands),
773-
validator: MultiLineValidator,
878+
hinter: ChatHinter::new(true, PathBuf::new(), available_commands, multiline_hint_state.clone()),
879+
validator: MultiLineValidator::new(multiline_hint_state),
774880
};
775881

776882
// Test profile prompt highlighting
@@ -790,14 +896,15 @@ mod tests {
790896
// Create a mock Os for testing
791897
let mock_os = crate::os::Os::new().await.unwrap();
792898
let available_commands = get_available_commands(&mock_os);
899+
let multiline_hint_state = MultilineHintState::new();
793900
let helper = ChatHelper {
794901
completer: ChatCompleter::new(
795902
prompt_request_sender,
796903
prompt_response_receiver,
797904
available_commands.clone(),
798905
),
799-
hinter: ChatHinter::new(true, PathBuf::new(), available_commands),
800-
validator: MultiLineValidator,
906+
hinter: ChatHinter::new(true, PathBuf::new(), available_commands, multiline_hint_state.clone()),
907+
validator: MultiLineValidator::new(multiline_hint_state),
801908
};
802909

803910
// Test profile + warning prompt highlighting
@@ -822,14 +929,15 @@ mod tests {
822929
// Create a mock Os for testing
823930
let mock_os = crate::os::Os::new().await.unwrap();
824931
let available_commands = get_available_commands(&mock_os);
932+
let multiline_hint_state = MultilineHintState::new();
825933
let helper = ChatHelper {
826934
completer: ChatCompleter::new(
827935
prompt_request_sender,
828936
prompt_response_receiver,
829937
available_commands.clone(),
830938
),
831-
hinter: ChatHinter::new(true, PathBuf::new(), available_commands),
832-
validator: MultiLineValidator,
939+
hinter: ChatHinter::new(true, PathBuf::new(), available_commands, multiline_hint_state.clone()),
940+
validator: MultiLineValidator::new(multiline_hint_state),
833941
};
834942

835943
// Test invalid prompt format (should return as-is)
@@ -846,14 +954,15 @@ mod tests {
846954
// Create a mock Os for testing
847955
let mock_os = crate::os::Os::new().await.unwrap();
848956
let available_commands = get_available_commands(&mock_os);
957+
let multiline_hint_state = MultilineHintState::new();
849958
let helper = ChatHelper {
850959
completer: ChatCompleter::new(
851960
prompt_request_sender,
852961
prompt_response_receiver,
853962
available_commands.clone(),
854963
),
855-
hinter: ChatHinter::new(true, PathBuf::new(), available_commands),
856-
validator: MultiLineValidator,
964+
hinter: ChatHinter::new(true, PathBuf::new(), available_commands, multiline_hint_state.clone()),
965+
validator: MultiLineValidator::new(multiline_hint_state),
857966
};
858967

859968
// Test tangent mode prompt highlighting - ↯ yellow, > magenta
@@ -872,14 +981,15 @@ mod tests {
872981
// Create a mock Os for testing
873982
let mock_os = crate::os::Os::new().await.unwrap();
874983
let available_commands = get_available_commands(&mock_os);
984+
let multiline_hint_state = MultilineHintState::new();
875985
let helper = ChatHelper {
876986
completer: ChatCompleter::new(
877987
prompt_request_sender,
878988
prompt_response_receiver,
879989
available_commands.clone(),
880990
),
881-
hinter: ChatHinter::new(true, PathBuf::new(), available_commands),
882-
validator: MultiLineValidator,
991+
hinter: ChatHinter::new(true, PathBuf::new(), available_commands, multiline_hint_state.clone()),
992+
validator: MultiLineValidator::new(multiline_hint_state),
883993
};
884994

885995
// Test tangent mode with warning - ↯ yellow, ! red, > magenta
@@ -903,14 +1013,15 @@ mod tests {
9031013
// Create a mock Os for testing
9041014
let mock_os = crate::os::Os::new().await.unwrap();
9051015
let available_commands = get_available_commands(&mock_os);
1016+
let multiline_hint_state = MultilineHintState::new();
9061017
let helper = ChatHelper {
9071018
completer: ChatCompleter::new(
9081019
prompt_request_sender,
9091020
prompt_response_receiver,
9101021
available_commands.clone(),
9111022
),
912-
hinter: ChatHinter::new(true, PathBuf::new(), available_commands),
913-
validator: MultiLineValidator,
1023+
hinter: ChatHinter::new(true, PathBuf::new(), available_commands, multiline_hint_state.clone()),
1024+
validator: MultiLineValidator::new(multiline_hint_state),
9141025
};
9151026

9161027
// Test profile with tangent mode - [dev] cyan, ↯ yellow, > magenta
@@ -931,7 +1042,8 @@ mod tests {
9311042
// Create a mock Os for testing
9321043
let mock_os = crate::os::Os::new().await.unwrap();
9331044
let available_commands = get_available_commands(&mock_os);
934-
let hinter = ChatHinter::new(true, PathBuf::new(), available_commands);
1045+
let multiline_hint_state = MultilineHintState::new();
1046+
let hinter = ChatHinter::new(true, PathBuf::new(), available_commands, multiline_hint_state);
9351047

9361048
// Test hint for a command
9371049
let line = "/he";
@@ -964,7 +1076,8 @@ mod tests {
9641076
// Create a mock Os for testing
9651077
let mock_os = crate::os::Os::new().await.unwrap();
9661078
let available_commands = get_available_commands(&mock_os);
967-
let hinter = ChatHinter::new(false, PathBuf::new(), available_commands);
1079+
let multiline_hint_state = MultilineHintState::new();
1080+
let hinter = ChatHinter::new(false, PathBuf::new(), available_commands, multiline_hint_state);
9681081

9691082
// Test hint from history - should be None since history hints are disabled
9701083
let line = "How";

crates/chat-cli/src/cli/feed.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"description": "Right arrow key being disabled - [#3439](https://github.com/aws/amazon-q-developer-cli/pull/3439)"
2222
}
2323
]
24-
}{
24+
},
25+
{
2526
"type": "release",
2627
"date": "2025-11-12",
2728
"version": "1.19.5",

0 commit comments

Comments
 (0)