@@ -433,10 +433,9 @@ impl JupyterServer {
433433 . await ?;
434434 }
435435
436- JupyterMessageContent :: IsCompleteRequest ( _) => {
437- connection
438- . send ( messaging:: IsCompleteReply :: complete ( ) . as_child_of ( parent) )
439- . await ?;
436+ JupyterMessageContent :: IsCompleteRequest ( req) => {
437+ let reply = check_is_complete ( & req. code ) ;
438+ connection. send ( reply. as_child_of ( parent) ) . await ?;
440439 }
441440 JupyterMessageContent :: KernelInfoRequest ( _) => {
442441 connection. send ( kernel_info ( ) . as_child_of ( parent) ) . await ?;
@@ -755,6 +754,190 @@ async fn publish_result(
755754 Ok ( None )
756755}
757756
757+ /// Check whether code is complete (all brackets/braces/parens balanced
758+ /// and no trailing backslash or unterminated template literal).
759+ /// This is a heuristic — it ignores delimiters inside strings and comments
760+ /// but covers the common interactive cases.
761+ fn check_is_complete ( code : & str ) -> messaging:: IsCompleteReply {
762+ let mut stack: Vec < char > = Vec :: new ( ) ;
763+ let mut chars = code. chars ( ) . peekable ( ) ;
764+ while let Some ( ch) = chars. next ( ) {
765+ match ch {
766+ // Skip single-line comments
767+ '/' if chars. peek ( ) == Some ( & '/' ) => {
768+ for c in chars. by_ref ( ) {
769+ if c == '\n' {
770+ break ;
771+ }
772+ }
773+ }
774+ // Skip multi-line comments
775+ '/' if chars. peek ( ) == Some ( & '*' ) => {
776+ chars. next ( ) ; // consume '*'
777+ let mut closed = false ;
778+ while let Some ( c) = chars. next ( ) {
779+ if c == '*' && chars. peek ( ) == Some ( & '/' ) {
780+ chars. next ( ) ;
781+ closed = true ;
782+ break ;
783+ }
784+ }
785+ if !closed {
786+ return messaging:: IsCompleteReply :: incomplete ( "" . into ( ) ) ;
787+ }
788+ }
789+ // Skip string literals
790+ '\'' | '"' | '`' => {
791+ let quote = ch;
792+ let mut escaped = false ;
793+ let mut closed = false ;
794+ for c in chars. by_ref ( ) {
795+ if escaped {
796+ escaped = false ;
797+ continue ;
798+ }
799+ if c == '\\' {
800+ escaped = true ;
801+ continue ;
802+ }
803+ if c == quote {
804+ closed = true ;
805+ break ;
806+ }
807+ }
808+ if !closed {
809+ return messaging:: IsCompleteReply :: incomplete ( "" . into ( ) ) ;
810+ }
811+ }
812+ '(' | '[' | '{' => stack. push ( ch) ,
813+ ')' => {
814+ if stack. pop ( ) != Some ( '(' ) {
815+ return messaging:: IsCompleteReply :: invalid ( ) ;
816+ }
817+ }
818+ ']' => {
819+ if stack. pop ( ) != Some ( '[' ) {
820+ return messaging:: IsCompleteReply :: invalid ( ) ;
821+ }
822+ }
823+ '}' => {
824+ if stack. pop ( ) != Some ( '{' ) {
825+ return messaging:: IsCompleteReply :: invalid ( ) ;
826+ }
827+ }
828+ _ => { }
829+ }
830+ }
831+ if stack. is_empty ( ) {
832+ messaging:: IsCompleteReply :: complete ( )
833+ } else {
834+ messaging:: IsCompleteReply :: incomplete ( " " . into ( ) )
835+ }
836+ }
837+
838+ #[ cfg( test) ]
839+ mod tests {
840+ use jupyter_protocol:: messaging:: IsCompleteReplyStatus ;
841+
842+ use super :: * ;
843+
844+ #[ test]
845+ fn test_complete_simple_statement ( ) {
846+ let reply = check_is_complete ( "const x = 1" ) ;
847+ assert_eq ! ( reply. status, IsCompleteReplyStatus :: Complete ) ;
848+ }
849+
850+ #[ test]
851+ fn test_incomplete_open_brace ( ) {
852+ let reply = check_is_complete ( "const x = {" ) ;
853+ assert_eq ! ( reply. status, IsCompleteReplyStatus :: Incomplete ) ;
854+ }
855+
856+ #[ test]
857+ fn test_incomplete_open_paren ( ) {
858+ let reply = check_is_complete ( "function foo(" ) ;
859+ assert_eq ! ( reply. status, IsCompleteReplyStatus :: Incomplete ) ;
860+ }
861+
862+ #[ test]
863+ fn test_incomplete_open_bracket ( ) {
864+ let reply = check_is_complete ( "const arr = [1, 2," ) ;
865+ assert_eq ! ( reply. status, IsCompleteReplyStatus :: Incomplete ) ;
866+ }
867+
868+ #[ test]
869+ fn test_complete_balanced_braces ( ) {
870+ let reply = check_is_complete ( "if (true) { console.log(1) }" ) ;
871+ assert_eq ! ( reply. status, IsCompleteReplyStatus :: Complete ) ;
872+ }
873+
874+ #[ test]
875+ fn test_complete_multiline ( ) {
876+ let reply = check_is_complete ( "function foo() {\n return 1;\n }" ) ;
877+ assert_eq ! ( reply. status, IsCompleteReplyStatus :: Complete ) ;
878+ }
879+
880+ #[ test]
881+ fn test_invalid_extra_close ( ) {
882+ let reply = check_is_complete ( "const x = 1 }" ) ;
883+ assert_eq ! ( reply. status, IsCompleteReplyStatus :: Invalid ) ;
884+ }
885+
886+ #[ test]
887+ fn test_invalid_mismatched ( ) {
888+ let reply = check_is_complete ( "const x = (]" ) ;
889+ assert_eq ! ( reply. status, IsCompleteReplyStatus :: Invalid ) ;
890+ }
891+
892+ #[ test]
893+ fn test_ignores_brackets_in_strings ( ) {
894+ let reply = check_is_complete ( r#"const x = "hello { world""# ) ;
895+ assert_eq ! ( reply. status, IsCompleteReplyStatus :: Complete ) ;
896+ }
897+
898+ #[ test]
899+ fn test_ignores_brackets_in_single_line_comment ( ) {
900+ let reply = check_is_complete ( "const x = 1 // {" ) ;
901+ assert_eq ! ( reply. status, IsCompleteReplyStatus :: Complete ) ;
902+ }
903+
904+ #[ test]
905+ fn test_ignores_brackets_in_multiline_comment ( ) {
906+ let reply = check_is_complete ( "const x = 1 /* { */" ) ;
907+ assert_eq ! ( reply. status, IsCompleteReplyStatus :: Complete ) ;
908+ }
909+
910+ #[ test]
911+ fn test_incomplete_unterminated_string ( ) {
912+ let reply = check_is_complete ( r#"const x = "hello"# ) ;
913+ assert_eq ! ( reply. status, IsCompleteReplyStatus :: Incomplete ) ;
914+ }
915+
916+ #[ test]
917+ fn test_incomplete_unterminated_template_literal ( ) {
918+ let reply = check_is_complete ( "const x = `hello ${name}" ) ;
919+ assert_eq ! ( reply. status, IsCompleteReplyStatus :: Incomplete ) ;
920+ }
921+
922+ #[ test]
923+ fn test_incomplete_unterminated_multiline_comment ( ) {
924+ let reply = check_is_complete ( "/* this comment never ends" ) ;
925+ assert_eq ! ( reply. status, IsCompleteReplyStatus :: Incomplete ) ;
926+ }
927+
928+ #[ test]
929+ fn test_complete_empty ( ) {
930+ let reply = check_is_complete ( "" ) ;
931+ assert_eq ! ( reply. status, IsCompleteReplyStatus :: Complete ) ;
932+ }
933+
934+ #[ test]
935+ fn test_escaped_quote_in_string ( ) {
936+ let reply = check_is_complete ( r#"const x = "hello \" world""# ) ;
937+ assert_eq ! ( reply. status, IsCompleteReplyStatus :: Complete ) ;
938+ }
939+ }
940+
758941// TODO(bartlomieju): dedup with repl::editor
759942fn get_expr_from_line_at_pos ( line : & str , cursor_pos : usize ) -> & str {
760943 let start = line[ ..cursor_pos] . rfind ( is_word_boundary) . unwrap_or ( 0 ) ;
0 commit comments