[WIP] markdown: Add a test to reproduce the parser's panic (#29479)

Oleksiy Syvokon and Conrad Irwin created

Backtrace of the panic in the Agent pane:
```
Thread "<unnamed>" panicked with "called `Option::unwrap()` on a `None` value" at crates/markdown/src/parser.rs:264:55
https://github.com/zed-industries/zed/blob/3fdbc3090d2cc5c2e24014009cccbe5e7c55d217/src/crates/markdown/src/parser.rs#L264 (may not be uploaded, line may be incorrect if files modified)
   0: zed::reliability::init_panic_hook::{{closure}}
             at /home/silver/develop/zed/crates/zed/src/reliability.rs:56:29
   1: <alloc::boxed::Box<F,A> as core::ops::function::Fn<Args>>::call
             at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/alloc/src/boxed.rs:1990:9
      std::panicking::rust_panic_with_hook
             at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/std/src/panicking.rs:839:13
   2: std::panicking::begin_panic_handler::{{closure}}
             at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/std/src/panicking.rs:697:13
   3: std::sys::backtrace::__rust_end_short_backtrace
             at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/std/src/sys/backtrace.rs:168:18
   4: rust_begin_unwind
             at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/std/src/panicking.rs:695:5
   5: core::panicking::panic_fmt
             at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/core/src/panicking.rs:75:14
   6: core::panicking::panic
             at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/core/src/panicking.rs:145:5
   7: core::option::unwrap_failed
             at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/core/src/option.rs:2015:5
   8: core::option::Option<T>::unwrap
             at /home/silver/.rustup/toolchains/1.86-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/option.rs:978:21
      markdown::parser::parse_markdown
             at /home/silver/develop/zed/crates/markdown/src/parser.rs:264:37
   9: markdown::Markdown::parse::{{closure}}
             at /home/silver/develop/zed/crates/markdown/src/markdown.rs:282:51
  10: <core::pin::Pin<P> as core::future::future::Future>::poll
             at /home/silver/.rustup/toolchains/1.86-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/future/future.rs:124:9
  11: async_task::raw::RawTask<F,T,S,M>::run
             at /home/silver/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-task-4.7.1/src/raw.rs:557:17
  12: async_task::runnable::Runnable<M>::run
             at /home/silver/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-task-4.7.1/src/runnable.rs:781:18
  13: gpui::platform::linux::dispatcher::LinuxDispatcher::new::{{closure}}::{{closure}}
             at /home/silver/develop/zed/crates/gpui/src/platform/linux/dispatcher.rs:44:25
  14: std::sys::backtrace::__rust_begin_short_backtrace
             at /home/silver/.rustup/toolchains/1.86-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/backtrace.rs:152:18
  15: std::thread::Builder::spawn_unchecked_::{{closure}}::{{closure}}
             at /home/silver/.rustup/toolchains/1.86-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/thread/mod.rs:559:17
  16: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
             at /home/silver/.rustup/toolchains/1.86-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/panic/unwind_safe.rs:272:9
  17: std::panicking::try::do_call
             at /home/silver/.rustup/toolchains/1.86-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:587:40
  18: __rust_try
  19: std::panicking::try
             at /home/silver/.rustup/toolchains/1.86-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:550:19
      std::panic::catch_unwind
             at /home/silver/.rustup/toolchains/1.86-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panic.rs:358:14
      std::thread::Builder::spawn_unchecked_::{{closure}}
             at /home/silver/.rustup/toolchains/1.86-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/thread/mod.rs:557:30
  20: core::ops::function::FnOnce::call_once{{vtable.shim}}
             at /home/silver/.rustup/toolchains/1.86-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
  21: <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once
             at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/alloc/src/boxed.rs:1976:9
      <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once
             at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/alloc/src/boxed.rs:1976:9
      std::sys::pal::unix::thread::Thread::new::thread_start
             at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/std/src/sys/pal/unix/thread.rs:106:17
  22: start_thread
             at ./nptl/pthread_create.c:447:8
  23: clone3
             at ./misc/../sysdeps/unix/sysv/linux/x86_64/clone3.S:78:0

Segmentation fault
```


Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

crates/markdown/src/parser.rs | 62 ++++++++++++++++++++++++++++--------
1 file changed, 48 insertions(+), 14 deletions(-)

Detailed changes

crates/markdown/src/parser.rs 🔗

@@ -184,6 +184,7 @@ pub fn parse_markdown(
                         (range, MarkdownEvent::SubstitutedText(str.to_owned()))
                     }
                 }
+                #[derive(Debug)]
                 struct TextRange<'a> {
                     source_range: Range<usize>,
                     merged_range: Range<usize>,
@@ -236,7 +237,9 @@ pub fn parse_markdown(
                             events.push(event_for(text, range.source_range, &range.parsed));
                         }
 
-                        let range = ranges.peek_mut().unwrap();
+                        let Some(range) = ranges.peek_mut() else {
+                            continue;
+                        };
                         let prefix_len = link_start_in_merged - range.merged_range.start;
                         if prefix_len > 0 {
                             let (head, tail) = range.parsed.split_at(prefix_len);
@@ -251,6 +254,7 @@ pub fn parse_markdown(
                         }
 
                         let link_start_in_source = range.source_range.start;
+                        let mut link_end_in_source = range.source_range.end;
                         let mut link_events = Vec::new();
 
                         while ranges
@@ -258,23 +262,26 @@ pub fn parse_markdown(
                             .is_some_and(|range| range.merged_range.end <= link_end_in_merged)
                         {
                             let range = ranges.next().unwrap();
+                            link_end_in_source = range.source_range.end;
                             link_events.push(event_for(text, range.source_range, &range.parsed));
                         }
 
-                        let range = ranges.peek_mut().unwrap();
-                        let prefix_len = link_end_in_merged - range.merged_range.start;
-                        if prefix_len > 0 {
-                            let (head, tail) = range.parsed.split_at(prefix_len);
-                            link_events.push(event_for(
-                                text,
-                                range.source_range.start..range.source_range.start + prefix_len,
-                                head,
-                            ));
-                            range.parsed = CowStr::Boxed(tail.into());
-                            range.merged_range.start += prefix_len;
-                            range.source_range.start += prefix_len;
+                        if let Some(range) = ranges.peek_mut() {
+                            let prefix_len = link_end_in_merged - range.merged_range.start;
+                            if prefix_len > 0 {
+                                let (head, tail) = range.parsed.split_at(prefix_len);
+                                link_events.push(event_for(
+                                    text,
+                                    range.source_range.start..range.source_range.start + prefix_len,
+                                    head,
+                                ));
+                                range.parsed = CowStr::Boxed(tail.into());
+                                range.merged_range.start += prefix_len;
+                                range.source_range.start += prefix_len;
+                                link_end_in_source = range.source_range.start;
+                            }
                         }
-                        let link_range = link_start_in_source..range.source_range.start;
+                        let link_range = link_start_in_source..link_end_in_source;
 
                         events.push((
                             link_range.clone(),
@@ -564,6 +571,33 @@ mod tests {
         );
     }
 
+    #[test]
+    fn test_incomplete_link() {
+        assert_eq!(
+            parse_markdown("You can use the [GitHub Search API](https://docs.github.com/en").0,
+            vec![
+                (0..62, Start(Paragraph)),
+                (0..16, Text),
+                (16..17, Text),
+                (17..34, Text),
+                (34..35, Text),
+                (35..36, Text),
+                (
+                    36..62,
+                    Start(Link {
+                        link_type: LinkType::Autolink,
+                        dest_url: "https://docs.github.com/en".into(),
+                        title: "".into(),
+                        id: "".into()
+                    })
+                ),
+                (36..62, Text),
+                (36..62, End(MarkdownTagEnd::Link)),
+                (0..62, End(MarkdownTagEnd::Paragraph))
+            ],
+        );
+    }
+
     #[test]
     fn test_smart_punctuation() {
         assert_eq!(