diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 9f2295e650..003012439c 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2666,6 +2666,7 @@ impl ChatWidget { } self.stream_controller = Some(StreamController::new( self.last_rendered_width.get().map(|w| w.saturating_sub(2)), + self.config.cwd.clone(), )); } if let Some(controller) = self.stream_controller.as_mut() diff --git a/codex-rs/tui/src/markdown.rs b/codex-rs/tui/src/markdown.rs index 2ea307066b..4acbc1a34e 100644 --- a/codex-rs/tui/src/markdown.rs +++ b/codex-rs/tui/src/markdown.rs @@ -1,10 +1,25 @@ use ratatui::text::Line; +use std::path::Path; + pub(crate) fn append_markdown( markdown_source: &str, width: Option, lines: &mut Vec>, ) { - let rendered = crate::markdown_render::render_markdown_text_with_width(markdown_source, width); + append_markdown_with_cwd(markdown_source, width, None, lines); +} + +pub(crate) fn append_markdown_with_cwd( + markdown_source: &str, + width: Option, + cwd: Option<&Path>, + lines: &mut Vec>, +) { + let rendered = crate::markdown_render::render_markdown_text_with_width_and_cwd( + markdown_source, + width, + cwd, + ); crate::render::line_utils::push_owned_lines(&rendered.lines, lines); } diff --git a/codex-rs/tui/src/markdown_render.rs b/codex-rs/tui/src/markdown_render.rs index 2bbe19b6f7..80dd689672 100644 --- a/codex-rs/tui/src/markdown_render.rs +++ b/codex-rs/tui/src/markdown_render.rs @@ -1,3 +1,4 @@ +use crate::diff_render::display_path_for; use crate::render::highlight::highlight_code_to_lines; use crate::render::line_utils::line_to_static; use crate::wrapping::RtOptions; @@ -16,7 +17,10 @@ use ratatui::text::Line; use ratatui::text::Span; use ratatui::text::Text; use regex_lite::Regex; +use std::path::Path; +use std::path::PathBuf; use std::sync::LazyLock; +use url::Url; struct MarkdownStyles { h1: Style, @@ -80,10 +84,18 @@ pub fn render_markdown_text(input: &str) -> Text<'static> { } pub(crate) fn render_markdown_text_with_width(input: &str, width: Option) -> Text<'static> { + render_markdown_text_with_width_and_cwd(input, width, None) +} + +pub(crate) fn render_markdown_text_with_width_and_cwd( + input: &str, + width: Option, + cwd: Option<&Path>, +) -> Text<'static> { let mut options = Options::empty(); options.insert(Options::ENABLE_STRIKETHROUGH); let parser = Parser::new_ext(input, options); - let mut w = Writer::new(parser, width); + let mut w = Writer::new(parser, width, cwd); w.run(); w.text } @@ -92,9 +104,7 @@ pub(crate) fn render_markdown_text_with_width(input: &str, width: Option) struct LinkState { destination: String, show_destination: bool, - hidden_location_suffix: Option, - label_start_span_idx: usize, - label_styled: bool, + local_display_text: Option, } fn should_render_link_destination(dest_url: &str) -> bool { @@ -130,6 +140,49 @@ fn is_local_path_like_link(dest_url: &str) -> bool { ) } +fn local_link_display_text(dest_url: &str, cwd: Option<&Path>) -> Option { + if !is_local_path_like_link(dest_url) { + return None; + } + let (path, location_suffix) = split_local_link_destination(dest_url)?; + let display_path = cwd.map_or_else( + || path.display().to_string(), + |cwd| display_path_for(&path, cwd), + ); + Some(format!("{display_path}{location_suffix}")) +} + +fn split_local_link_destination(dest_url: &str) -> Option<(PathBuf, String)> { + if dest_url.starts_with("file://") { + let url = Url::parse(dest_url).ok()?; + let path = url.to_file_path().ok()?; + let location_suffix = url + .fragment() + .map(|fragment| format!("#{fragment}")) + .and_then(|suffix| normalize_markdown_hash_location_suffix(&suffix)) + .unwrap_or_default(); + return Some((path, location_suffix)); + } + + if let Some((path_part, fragment)) = dest_url.rsplit_once('#') + && HASH_LOCATION_SUFFIX_RE.is_match(fragment) + { + let location_suffix = + normalize_markdown_hash_location_suffix(&format!("#{fragment}")).unwrap_or_default(); + return Some((PathBuf::from(path_part), location_suffix)); + } + + if let Some(location_match) = COLON_LOCATION_SUFFIX_RE.find(dest_url) { + let path_part = &dest_url[..location_match.start()]; + return Some(( + PathBuf::from(path_part), + location_match.as_str().to_string(), + )); + } + + Some((PathBuf::from(dest_url), String::new())) +} + struct Writer<'a, I> where I: Iterator>, @@ -153,13 +206,14 @@ where current_subsequent_indent: Vec>, current_line_style: Style, current_line_in_code_block: bool, + cwd: Option, } impl<'a, I> Writer<'a, I> where I: Iterator>, { - fn new(iter: I, wrap_width: Option) -> Self { + fn new(iter: I, wrap_width: Option, cwd: Option<&Path>) -> Self { Self { iter, text: Text::default(), @@ -180,9 +234,17 @@ where current_subsequent_indent: Vec::new(), current_line_style: Style::default(), current_line_in_code_block: false, + cwd: cwd.map(Path::to_path_buf), } } + fn suppress_local_link_label(&self) -> bool { + self.link + .as_ref() + .and_then(|link| link.local_display_text.as_ref()) + .is_some() + } + fn run(&mut self) { while let Some(ev) = self.iter.next() { self.handle_event(ev); @@ -194,10 +256,26 @@ where match event { Event::Start(tag) => self.start_tag(tag), Event::End(tag) => self.end_tag(tag), - Event::Text(text) => self.text(text), - Event::Code(code) => self.code(code), - Event::SoftBreak => self.soft_break(), - Event::HardBreak => self.hard_break(), + Event::Text(text) => { + if !self.suppress_local_link_label() { + self.text(text); + } + } + Event::Code(code) => { + if !self.suppress_local_link_label() { + self.code(code); + } + } + Event::SoftBreak => { + if !self.suppress_local_link_label() { + self.soft_break(); + } + } + Event::HardBreak => { + if !self.suppress_local_link_label() { + self.hard_break(); + } + } Event::Rule => { self.flush_current_line(); if !self.text.lines.is_empty() { @@ -206,8 +284,16 @@ where self.push_line(Line::from("———")); self.needs_newline = true; } - Event::Html(html) => self.html(html, false), - Event::InlineHtml(html) => self.html(html, true), + Event::Html(html) => { + if !self.suppress_local_link_label() { + self.html(html, false); + } + } + Event::InlineHtml(html) => { + if !self.suppress_local_link_label() { + self.html(html, true); + } + } Event::FootnoteReference(_) => {} Event::TaskListMarker(_) => {} } @@ -231,9 +317,21 @@ where } Tag::List(start) => self.start_list(start), Tag::Item => self.start_item(), - Tag::Emphasis => self.push_inline_style(self.styles.emphasis), - Tag::Strong => self.push_inline_style(self.styles.strong), - Tag::Strikethrough => self.push_inline_style(self.styles.strikethrough), + Tag::Emphasis => { + if !self.suppress_local_link_label() { + self.push_inline_style(self.styles.emphasis); + } + } + Tag::Strong => { + if !self.suppress_local_link_label() { + self.push_inline_style(self.styles.strong); + } + } + Tag::Strikethrough => { + if !self.suppress_local_link_label() { + self.push_inline_style(self.styles.strikethrough); + } + } Tag::Link { dest_url, .. } => self.push_link(dest_url.to_string()), Tag::HtmlBlock | Tag::FootnoteDefinition(_) @@ -257,7 +355,11 @@ where self.indent_stack.pop(); self.pending_marker_line = false; } - TagEnd::Emphasis | TagEnd::Strong | TagEnd::Strikethrough => self.pop_inline_style(), + TagEnd::Emphasis | TagEnd::Strong | TagEnd::Strikethrough => { + if !self.suppress_local_link_label() { + self.pop_inline_style(); + } + } TagEnd::Link => self.pop_link(), TagEnd::HtmlBlock | TagEnd::FootnoteDefinition @@ -513,36 +615,9 @@ where fn push_link(&mut self, dest_url: String) { let show_destination = should_render_link_destination(&dest_url); - let label_styled = !show_destination; - let label_start_span_idx = self - .current_line_content - .as_ref() - .map(|line| line.spans.len()) - .unwrap_or(0); - if label_styled { - self.push_inline_style(self.styles.code); - } self.link = Some(LinkState { show_destination, - hidden_location_suffix: if is_local_path_like_link(&dest_url) { - dest_url - .rsplit_once('#') - .and_then(|(_, fragment)| { - HASH_LOCATION_SUFFIX_RE - .is_match(fragment) - .then(|| format!("#{fragment}")) - }) - .and_then(|suffix| normalize_markdown_hash_location_suffix(&suffix)) - .or_else(|| { - COLON_LOCATION_SUFFIX_RE - .find(&dest_url) - .map(|m| m.as_str().to_string()) - }) - } else { - None - }, - label_start_span_idx, - label_styled, + local_display_text: local_link_display_text(&dest_url, self.cwd.as_deref()), destination: dest_url, }); } @@ -550,39 +625,11 @@ where fn pop_link(&mut self) { if let Some(link) = self.link.take() { if link.show_destination { - if link.label_styled { - self.pop_inline_style(); - } self.push_span(" (".into()); self.push_span(Span::styled(link.destination, self.styles.link)); self.push_span(")".into()); - } else if let Some(location_suffix) = link.hidden_location_suffix.as_deref() { - let label_text = self - .current_line_content - .as_ref() - .and_then(|line| { - line.spans.get(link.label_start_span_idx..).map(|spans| { - spans - .iter() - .map(|span| span.content.as_ref()) - .collect::() - }) - }) - .unwrap_or_default(); - if label_text - .rsplit_once('#') - .is_some_and(|(_, fragment)| HASH_LOCATION_SUFFIX_RE.is_match(fragment)) - || COLON_LOCATION_SUFFIX_RE.find(&label_text).is_some() - { - // The label already carries a location suffix; don't duplicate it. - } else { - self.push_span(Span::styled(location_suffix.to_string(), self.styles.code)); - } - if link.label_styled { - self.pop_inline_style(); - } - } else if link.label_styled { - self.pop_inline_style(); + } else if let Some(local_display_text) = link.local_display_text { + self.push_span(Span::styled(local_display_text, self.styles.code)); } } } diff --git a/codex-rs/tui/src/markdown_render_tests.rs b/codex-rs/tui/src/markdown_render_tests.rs index 9981246093..d34ed47ddf 100644 --- a/codex-rs/tui/src/markdown_render_tests.rs +++ b/codex-rs/tui/src/markdown_render_tests.rs @@ -3,12 +3,18 @@ use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::text::Span; use ratatui::text::Text; +use std::path::Path; use crate::markdown_render::COLON_LOCATION_SUFFIX_RE; use crate::markdown_render::HASH_LOCATION_SUFFIX_RE; use crate::markdown_render::render_markdown_text; +use crate::markdown_render::render_markdown_text_with_width_and_cwd; use insta::assert_snapshot; +fn render_markdown_text_for_cwd(input: &str, cwd: &Path) -> Text<'static> { + render_markdown_text_with_width_and_cwd(input, None, Some(cwd)) +} + #[test] fn empty() { assert_eq!(render_markdown_text(""), Text::default()); @@ -661,106 +667,109 @@ fn load_location_suffix_regexes() { #[test] fn file_link_hides_destination() { - let text = render_markdown_text( - "[codex-rs/tui/src/markdown_render.rs](/Users/example/code/codex/codex-rs/tui/src/markdown_render.rs)", + let text = render_markdown_text_for_cwd( + "[ignored label](/Users/example/code/codex/codex-rs/tui/src/markdown_render.rs)", + Path::new("/Users/example/code/codex"), ); let expected = Text::from(Line::from_iter(["codex-rs/tui/src/markdown_render.rs".cyan()])); assert_eq!(text, expected); } #[test] -fn file_link_appends_line_number_when_label_lacks_it() { - let text = render_markdown_text( +fn file_link_uses_target_path_relative_to_cwd() { + let text = render_markdown_text_for_cwd( "[markdown_render.rs](/Users/example/code/codex/codex-rs/tui/src/markdown_render.rs:74)", + Path::new("/Users/example/code/codex"), ); - let expected = Text::from(Line::from_iter([ - "markdown_render.rs".cyan(), - ":74".cyan(), - ])); + let expected = Text::from(Line::from_iter(["codex-rs/tui/src/markdown_render.rs:74".cyan()])); assert_eq!(text, expected); } #[test] -fn file_link_uses_label_for_line_number() { - let text = render_markdown_text( +fn file_link_ignores_label_when_rendering_visible_target() { + let text = render_markdown_text_for_cwd( "[markdown_render.rs:74](/Users/example/code/codex/codex-rs/tui/src/markdown_render.rs:74)", + Path::new("/Users/example/code/codex"), ); - let expected = Text::from(Line::from_iter(["markdown_render.rs:74".cyan()])); + let expected = Text::from(Line::from_iter(["codex-rs/tui/src/markdown_render.rs:74".cyan()])); assert_eq!(text, expected); } #[test] -fn file_link_appends_hash_anchor_when_label_lacks_it() { - let text = render_markdown_text( +fn file_link_normalizes_hash_anchor_for_terminal_clickability() { + let text = render_markdown_text_for_cwd( "[markdown_render.rs](file:///Users/example/code/codex/codex-rs/tui/src/markdown_render.rs#L74C3)", + Path::new("/Users/example/code/codex"), ); - let expected = Text::from(Line::from_iter([ - "markdown_render.rs".cyan(), - ":74:3".cyan(), - ])); + let expected = Text::from(Line::from_iter(["codex-rs/tui/src/markdown_render.rs:74:3".cyan()])); assert_eq!(text, expected); } #[test] -fn file_link_uses_label_for_hash_anchor() { - let text = render_markdown_text( +fn file_link_ignores_hash_anchor_label() { + let text = render_markdown_text_for_cwd( "[markdown_render.rs#L74C3](file:///Users/example/code/codex/codex-rs/tui/src/markdown_render.rs#L74C3)", + Path::new("/Users/example/code/codex"), ); - let expected = Text::from(Line::from_iter(["markdown_render.rs#L74C3".cyan()])); + let expected = Text::from(Line::from_iter(["codex-rs/tui/src/markdown_render.rs:74:3".cyan()])); assert_eq!(text, expected); } #[test] -fn file_link_appends_range_when_label_lacks_it() { - let text = render_markdown_text( +fn file_link_shows_range_from_target() { + let text = render_markdown_text_for_cwd( "[markdown_render.rs](/Users/example/code/codex/codex-rs/tui/src/markdown_render.rs:74:3-76:9)", + Path::new("/Users/example/code/codex"), ); - let expected = Text::from(Line::from_iter([ - "markdown_render.rs".cyan(), - ":74:3-76:9".cyan(), - ])); + let expected = + Text::from(Line::from_iter(["codex-rs/tui/src/markdown_render.rs:74:3-76:9".cyan()])); assert_eq!(text, expected); } #[test] -fn file_link_uses_label_for_range() { - let text = render_markdown_text( +fn file_link_ignores_range_label() { + let text = render_markdown_text_for_cwd( "[markdown_render.rs:74:3-76:9](/Users/example/code/codex/codex-rs/tui/src/markdown_render.rs:74:3-76:9)", + Path::new("/Users/example/code/codex"), ); - let expected = Text::from(Line::from_iter(["markdown_render.rs:74:3-76:9".cyan()])); + let expected = + Text::from(Line::from_iter(["codex-rs/tui/src/markdown_render.rs:74:3-76:9".cyan()])); assert_eq!(text, expected); } #[test] -fn file_link_appends_hash_range_when_label_lacks_it() { - let text = render_markdown_text( +fn file_link_normalizes_hash_range() { + let text = render_markdown_text_for_cwd( "[markdown_render.rs](file:///Users/example/code/codex/codex-rs/tui/src/markdown_render.rs#L74C3-L76C9)", + Path::new("/Users/example/code/codex"), ); - let expected = Text::from(Line::from_iter([ - "markdown_render.rs".cyan(), - ":74:3-76:9".cyan(), - ])); + let expected = + Text::from(Line::from_iter(["codex-rs/tui/src/markdown_render.rs:74:3-76:9".cyan()])); assert_eq!(text, expected); } #[test] fn multiline_file_link_label_after_styled_prefix_does_not_panic() { - let text = render_markdown_text( + let text = render_markdown_text_for_cwd( "**bold** plain [foo\nbar](file:///Users/example/code/codex/codex-rs/tui/src/markdown_render.rs#L74C3)", + Path::new("/Users/example/code/codex"), ); - let expected = Text::from_iter([ - Line::from_iter(["bold".bold(), " plain ".into(), "foo".cyan()]), - Line::from_iter(["bar".cyan(), ":74:3".cyan()]), - ]); + let expected = Text::from(Line::from_iter([ + "bold".bold(), + " plain ".into(), + "codex-rs/tui/src/markdown_render.rs:74:3".cyan(), + ])); assert_eq!(text, expected); } #[test] -fn file_link_uses_label_for_hash_range() { - let text = render_markdown_text( +fn file_link_ignores_hash_range_label() { + let text = render_markdown_text_for_cwd( "[markdown_render.rs#L74C3-L76C9](file:///Users/example/code/codex/codex-rs/tui/src/markdown_render.rs#L74C3-L76C9)", + Path::new("/Users/example/code/codex"), ); - let expected = Text::from(Line::from_iter(["markdown_render.rs#L74C3-L76C9".cyan()])); + let expected = + Text::from(Line::from_iter(["codex-rs/tui/src/markdown_render.rs:74:3-76:9".cyan()])); assert_eq!(text, expected); } @@ -778,8 +787,9 @@ fn url_link_shows_destination() { #[test] fn markdown_render_file_link_snapshot() { - let text = render_markdown_text( + let text = render_markdown_text_for_cwd( "See [markdown_render.rs:74](/Users/example/code/codex/codex-rs/tui/src/markdown_render.rs:74).", + Path::new("/Users/example/code/codex"), ); let rendered = text .lines diff --git a/codex-rs/tui/src/markdown_stream.rs b/codex-rs/tui/src/markdown_stream.rs index 6ac457eee2..94ec9b035f 100644 --- a/codex-rs/tui/src/markdown_stream.rs +++ b/codex-rs/tui/src/markdown_stream.rs @@ -1,4 +1,5 @@ use ratatui::text::Line; +use std::path::PathBuf; use crate::markdown; @@ -8,14 +9,16 @@ pub(crate) struct MarkdownStreamCollector { buffer: String, committed_line_count: usize, width: Option, + cwd: Option, } impl MarkdownStreamCollector { - pub fn new(width: Option) -> Self { + pub fn new(width: Option, cwd: Option) -> Self { Self { buffer: String::new(), committed_line_count: 0, width, + cwd, } } @@ -41,7 +44,7 @@ impl MarkdownStreamCollector { return Vec::new(); }; let mut rendered: Vec> = Vec::new(); - markdown::append_markdown(&source, self.width, &mut rendered); + markdown::append_markdown_with_cwd(&source, self.width, self.cwd.as_deref(), &mut rendered); let mut complete_line_count = rendered.len(); if complete_line_count > 0 && crate::render::line_utils::is_blank_line_spaces_only( @@ -82,7 +85,7 @@ impl MarkdownStreamCollector { tracing::trace!("markdown finalize (raw source):\n---\n{source}\n---"); let mut rendered: Vec> = Vec::new(); - markdown::append_markdown(&source, self.width, &mut rendered); + markdown::append_markdown_with_cwd(&source, self.width, self.cwd.as_deref(), &mut rendered); let out = if self.committed_line_count >= rendered.len() { Vec::new() @@ -101,7 +104,7 @@ pub(crate) fn simulate_stream_markdown_for_tests( deltas: &[&str], finalize: bool, ) -> Vec> { - let mut collector = MarkdownStreamCollector::new(None); + let mut collector = MarkdownStreamCollector::new(None, None); let mut out = Vec::new(); for d in deltas { collector.push_delta(d); @@ -122,7 +125,7 @@ mod tests { #[tokio::test] async fn no_commit_until_newline() { - let mut c = super::MarkdownStreamCollector::new(None); + let mut c = super::MarkdownStreamCollector::new(None, None); c.push_delta("Hello, world"); let out = c.commit_complete_lines(); assert!(out.is_empty(), "should not commit without newline"); @@ -133,7 +136,7 @@ mod tests { #[tokio::test] async fn finalize_commits_partial_line() { - let mut c = super::MarkdownStreamCollector::new(None); + let mut c = super::MarkdownStreamCollector::new(None, None); c.push_delta("Line without newline"); let out = c.finalize_and_drain(); assert_eq!(out.len(), 1); @@ -253,7 +256,7 @@ mod tests { async fn heading_starts_on_new_line_when_following_paragraph() { // Stream a paragraph line, then a heading on the next line. // Expect two distinct rendered lines: "Hello." and "Heading". - let mut c = super::MarkdownStreamCollector::new(None); + let mut c = super::MarkdownStreamCollector::new(None, None); c.push_delta("Hello.\n"); let out1 = c.commit_complete_lines(); let s1: Vec = out1 @@ -309,7 +312,7 @@ mod tests { // Paragraph without trailing newline, then a chunk that starts with the newline // and the heading text, then a final newline. The collector should first commit // only the paragraph line, and later commit the heading as its own line. - let mut c = super::MarkdownStreamCollector::new(None); + let mut c = super::MarkdownStreamCollector::new(None, None); c.push_delta("Sounds good!"); // No commit yet assert!(c.commit_complete_lines().is_empty()); diff --git a/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__markdown_render_file_link_snapshot.snap b/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__markdown_render_file_link_snapshot.snap index 1b1f1210f4..73d2e21e6b 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__markdown_render_file_link_snapshot.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__markdown_render_file_link_snapshot.snap @@ -1,6 +1,6 @@ --- source: tui/src/markdown_render_tests.rs -assertion_line: 714 +assertion_line: 776 expression: rendered --- -See markdown_render.rs:74. +See codex-rs/tui/src/markdown_render.rs:74. diff --git a/codex-rs/tui/src/streaming/controller.rs b/codex-rs/tui/src/streaming/controller.rs index 6117485adf..9c57bf0aa9 100644 --- a/codex-rs/tui/src/streaming/controller.rs +++ b/codex-rs/tui/src/streaming/controller.rs @@ -4,6 +4,7 @@ use crate::render::line_utils::prefix_lines; use crate::style::proposed_plan_style; use ratatui::prelude::Stylize; use ratatui::text::Line; +use std::path::PathBuf; use std::time::Duration; use std::time::Instant; @@ -18,9 +19,9 @@ pub(crate) struct StreamController { } impl StreamController { - pub(crate) fn new(width: Option) -> Self { + pub(crate) fn new(width: Option, cwd: PathBuf) -> Self { Self { - state: StreamState::new(width), + state: StreamState::new(width, Some(cwd)), finishing_after_drain: false, header_emitted: false, } @@ -117,7 +118,7 @@ pub(crate) struct PlanStreamController { impl PlanStreamController { pub(crate) fn new(width: Option) -> Self { Self { - state: StreamState::new(width), + state: StreamState::new(width, None), header_emitted: false, top_padding_emitted: false, } @@ -248,7 +249,7 @@ mod tests { #[tokio::test] async fn controller_loose_vs_tight_with_commit_ticks_matches_full() { - let mut ctrl = StreamController::new(None); + let mut ctrl = StreamController::new(None, PathBuf::from("/workspace")); let mut lines = Vec::new(); // Exact deltas from the session log (section: Loose vs. tight list items) diff --git a/codex-rs/tui/src/streaming/mod.rs b/codex-rs/tui/src/streaming/mod.rs index c783f27ae9..36452f0437 100644 --- a/codex-rs/tui/src/streaming/mod.rs +++ b/codex-rs/tui/src/streaming/mod.rs @@ -10,6 +10,7 @@ //! arrival timestamp so policy code can reason about oldest queued age without peeking into text. use std::collections::VecDeque; +use std::path::PathBuf; use std::time::Duration; use std::time::Instant; @@ -34,9 +35,9 @@ pub(crate) struct StreamState { impl StreamState { /// Creates an empty stream state with an optional target wrap width. - pub(crate) fn new(width: Option) -> Self { + pub(crate) fn new(width: Option, cwd: Option) -> Self { Self { - collector: MarkdownStreamCollector::new(width), + collector: MarkdownStreamCollector::new(width, cwd), queued_lines: VecDeque::new(), has_seen_delta: false, } @@ -105,7 +106,7 @@ mod tests { #[test] fn drain_n_clamps_to_available_lines() { - let mut state = StreamState::new(None); + let mut state = StreamState::new(None, None); state.enqueue(vec![Line::from("one")]); let drained = state.drain_n(8);