use crate::{ DEFAULT_THREAD_TITLE, SelectPermissionGranularity, agent_configuration::configure_context_server_modal::default_markdown_style, conversation_view::thread_search_bar::{ThreadSearchBar, ThreadSearchBarEvent}, open_abs_path_at_point, thread_metadata_store::{ThreadId, ThreadMetadataStore},};use agent_client_protocol::schema::v1 as acp;use std::cell::RefCell; use acp_thread::{ Elicitation, ElicitationEntryId, ElicitationStatus, PlanEntry, SandboxAuthorizationDetails, SandboxFallbackAuthorizationDetails, SandboxNotAppliedReason, decode_path_escapes,};use agent::{ SandboxStatusKey, SandboxStatusRefresh, SkillLoadingIssue, SkillLoadingIssueKind, SkillLoadingIssuesUpdated, ThreadSandbox, VerifiedSandboxStatus,};use agent_settings::UserAgentsMd;use agent_skills::MAX_SKILL_DESCRIPTION_LEN;use cloud_api_types::{SubmitAgentThreadFeedbackBody, SubmitAgentThreadFeedbackCommentsBody};use editor::actions::OpenExcerpts;use sandbox::{SandboxFsPolicy, SandboxNetPolicy, SandboxPolicy}; use crate::completion_provider::{AvailableSkill, PromptLocalCommand, pluralize};use crate::message_editor::SharedSessionCapabilities;use crate::ui::{ SandboxGroup, SandboxRow, SandboxSection, SandboxStatusTooltip, TerminalSandboxWarning, TerminalToolHeader,};use crate::unicode_confusables; use db::kvp::KeyValueStore;use gpui::List;use gpui::Stateful;use gpui::TaskExt;use heapless::Vec as ArrayVec;use language_model::{ FastModeConfirmation, LanguageModel, LanguageModelEffortLevel, LanguageModelId, LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Speed,};use notifications::status_toast::StatusToast;use settings::{update_settings_file, update_settings_file_with_completion};use ui::{ ButtonLike, CalloutBorderPosition, Checkbox, SpinnerLabel, SpinnerVariant, SplitButton, SplitButtonStyle, Tab, ToggleState,};use util::markdown::{source_position_from_fragment, split_local_url_fragment};use workspace::{OpenOptions, SERIALIZATION_THROTTLE_TIME}; use super::elicitation::{ ElicitationCard, ElicitationCardHandlers, ElicitationFormState, should_render_elicitation,};use super::*; const DATA_RETENTION_LEARN_MORE_URL: &str = "https://support.claude.com/en/articles/15425996-data-retention-practices-for-mythos-class-models"; #[derive(Default)]struct ThreadFeedbackState { feedback: Option<ThreadFeedback>, comments_editor: Option<Entity<Editor>>,} impl ThreadFeedbackState { pub fn submit( &mut self, thread: Entity<AcpThread>, feedback: ThreadFeedback, window: &mut Window, cx: &mut App, ) { let Some(telemetry) = thread.read(cx).connection().telemetry() else { return; }; let project = thread.read(cx).project().read(cx); let client = project.client(); let user_store = project.user_store(); let organization = user_store.read(cx).current_organization(); if self.feedback == Some(feedback) { return; } self.feedback = Some(feedback); match feedback { ThreadFeedback::Positive => { self.comments_editor = None; } ThreadFeedback::Negative => { self.comments_editor = Some(Self::build_feedback_comments_editor(window, cx)); } } let session_id = thread.read(cx).session_id().clone(); let parent_session_id = thread.read(cx).parent_session_id().cloned(); let agent_telemetry_id = thread.read(cx).connection().telemetry_id(); let task = telemetry.thread_data(&session_id, cx); let rating = match feedback { ThreadFeedback::Positive => "positive", ThreadFeedback::Negative => "negative", }; cx.background_spawn(async move { let thread = task.await?; client .cloud_client() .submit_agent_feedback(SubmitAgentThreadFeedbackBody { organization_id: organization.map(|organization| organization.id.clone()), agent: agent_telemetry_id.to_string(), session_id: session_id.to_string(), parent_session_id: parent_session_id.map(|id| id.to_string()), rating: rating.to_string(), thread, }) .await?; anyhow::Ok(()) }) .detach_and_log_err(cx); } pub fn submit_comments(&mut self, thread: Entity<AcpThread>, cx: &mut App) { let Some(telemetry) = thread.read(cx).connection().telemetry() else { return; }; let Some(comments) = self .comments_editor .as_ref() .map(|editor| editor.read(cx).text(cx)) .filter(|text| !text.trim().is_empty()) else { return; }; self.comments_editor.take(); let project = thread.read(cx).project().read(cx); let client = project.client(); let user_store = project.user_store(); let organization = user_store.read(cx).current_organization(); let session_id = thread.read(cx).session_id().clone(); let agent_telemetry_id = thread.read(cx).connection().telemetry_id(); let task = telemetry.thread_data(&session_id, cx); cx.background_spawn(async move { let thread = task.await?; client .cloud_client() .submit_agent_feedback_comments(SubmitAgentThreadFeedbackCommentsBody { organization_id: organization.map(|organization| organization.id.clone()), agent: agent_telemetry_id.to_string(), session_id: session_id.to_string(), comments, thread, }) .await?; anyhow::Ok(()) }) .detach_and_log_err(cx); } pub fn clear(&mut self) { *self = Self::default() } pub fn dismiss_comments(&mut self) { self.comments_editor.take(); } fn build_feedback_comments_editor(window: &mut Window, cx: &mut App) -> Entity<Editor> { let buffer = cx.new(|cx| { let empty_string = String::new(); MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx) }); let editor = cx.new(|cx| { let mut editor = Editor::new( editor::EditorMode::AutoHeight { min_lines: 1, max_lines: Some(4), }, buffer, None, window, cx, ); editor.set_placeholder_text( "What went wrong? Share your feedback so we can improve.", window, cx, ); editor }); editor.read(cx).focus_handle(cx).focus(window, cx); editor }} struct GeneratingSpinner { variant: SpinnerVariant,} impl GeneratingSpinner { fn new(variant: SpinnerVariant) -> Self { Self { variant } }} impl Render for GeneratingSpinner { fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { SpinnerLabel::with_variant(self.variant).size(LabelSize::Small) }} #[derive(IntoElement)]struct GeneratingSpinnerElement { variant: SpinnerVariant,} impl GeneratingSpinnerElement { fn new(variant: SpinnerVariant) -> Self { Self { variant } }} impl RenderOnce for GeneratingSpinnerElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let id = match self.variant { SpinnerVariant::Dots => "generating-spinner-view", SpinnerVariant::Sand => "confirmation-spinner-view", _ => "spinner-view", }; window.with_id(id, |window| { window.use_state(cx, |_, _| GeneratingSpinner::new(self.variant)) }) }} pub enum AcpThreadViewEvent { Interacted,} impl EventEmitter<AcpThreadViewEvent> for ThreadView {} /// `cat -n`-style numbered code block, already stripped of its line-number/// prefixes and ready to render. Line numbers are guaranteed to be contiguous/// starting at `first_number`, so we only store the first number and the line/// count rather than allocating a per-line `Vec`.struct ParsedCatNumberedCode { code: String, first_number: u32, line_count: usize,} fn parse_cat_numbered_markdown_code_block(markdown: &str) -> Option<ParsedCatNumberedCode> { let (_tag, code) = parse_single_fenced_code_block(markdown)?; parse_cat_numbered_code(code)} fn parse_single_fenced_code_block(markdown: &str) -> Option<(&str, &str)> { let first_non_backtick = markdown.find(|character| character != '`')?; if first_non_backtick < 3 { return None; } let fence = &markdown[..first_non_backtick]; let after_opening_fence = &markdown[first_non_backtick..]; let tag_end = after_opening_fence.find('\n')?; let tag = &after_opening_fence[..tag_end]; let after_tag = &after_opening_fence[tag_end + 1..]; let closing_fence = format!("\n{fence}\n"); let code = after_tag.strip_suffix(&closing_fence)?; Some((tag, code))} /// Walks `code` exactly once: for each line it validates and strips the/// `NNN\t` prefix, then pushes the line's content into the accumulating/// code buffer (with `\n` between lines, no trailing newline). Verifies that/// the line numbers form a contiguous, increasing sequence.fn parse_cat_numbered_code(code: &str) -> Option<ParsedCatNumberedCode> { if code.is_empty() { return None; } let mut output = String::with_capacity(code.len()); let mut first_number = None; let mut expected_number = None; let mut line_count: usize = 0; for raw_line in code.split_inclusive('\n') { let line = strip_line_ending(raw_line); let (number, text) = parse_cat_numbered_line(line)?; if let Some(expected) = expected_number { if number != expected { return None; } } else { first_number = Some(number); } expected_number = number.checked_add(1); if line_count > 0 { output.push('\n'); } output.push_str(text); line_count += 1; } Some(ParsedCatNumberedCode { code: output, first_number: first_number?, line_count, })} fn strip_line_ending(line: &str) -> &str { let without_lf = line.strip_suffix('\n').unwrap_or(line); without_lf.strip_suffix('\r').unwrap_or(without_lf)} fn parse_cat_numbered_line(line: &str) -> Option<(u32, &str)> { let (prefix, text) = line.split_once('\t')?; let number = prefix.trim(); if number.is_empty() || !prefix .chars() .all(|character| character == ' ' || character.is_ascii_digit()) { return None; } Some((number.parse().ok()?, text))} fn render_cat_numbered_code_block( parsed: ParsedCatNumberedCode, language: Option<Arc<Language>>, markdown_style: MarkdownStyle, copy_button_id: String, cx: &App,) -> AnyElement { use std::fmt::Write as _; let ParsedCatNumberedCode { code, first_number, line_count, } = parsed; // Line numbers are contiguous (verified during parsing), so the largest // line number is `first_number + line_count - 1`. Sizing the gutter to // that number's digit count means every rendered line contributes exactly // `gutter_width` bytes to the gutter, plus a newline between adjacent // lines. let last_number = first_number .saturating_add(u32::try_from(line_count.saturating_sub(1)).unwrap_or(u32::MAX)); let gutter_width = last_number.to_string().len().max(1); let gutter_capacity = line_count * gutter_width + line_count.saturating_sub(1); let mut gutter = String::with_capacity(gutter_capacity); for i in 0..line_count { if i > 0 { gutter.push('\n'); } let line_number = first_number.saturating_add(u32::try_from(i).unwrap_or(u32::MAX)); // Writes to a `String` are infallible, so the `Result` can be ignored. let _ = write!(&mut gutter, "{line_number:>gutter_width$}"); } let mut code_text_style = markdown_style.base_text_style.clone(); code_text_style.refine(&markdown_style.code_block.text); let mut gutter_text_style = code_text_style.clone(); gutter_text_style.color = cx.theme().colors().text_muted; let gutter_len = gutter.len(); let gutter = StyledText::new(gutter).with_runs(vec![gutter_text_style.to_run(gutter_len)]); // Share `code` between syntax highlighting, the rendered `StyledText`, and // the copy button via a single `SharedString` (cheap `Arc` clones) instead // of cloning the underlying `String`. let code: SharedString = code.into(); let code_runs = highlight_code_runs(&code, language.as_ref(), code_text_style, &markdown_style); let code_text = StyledText::new(code.clone()).with_runs(code_runs); let code_block_id = format!("read-file-code-block-{copy_button_id}"); let code_scroll_id = format!("read-file-code-scroll-{copy_button_id}"); let mut container = div() .id(code_block_id) .group("read-file-code-block") .relative() .w_full() .whitespace_nowrap(); container.style().refine(&markdown_style.code_block); // `overflow_x_scroll` only actually scrolls when the container is laid out // as a flex container: in GPUI the default `Display` is `Block`, and a // block-level child fills its parent's content width instead of overflowing // it, so there is nothing for the scroll viewport to scroll. Using `flex()` // on the scroll wrapper plus `flex_none()` on the inner item lets the inner // item take its natural width (the unwrapped code), which is what overflows. // `restrict_scroll_to_axis` then keeps vertical wheel events flowing through // to the outer thread scroller. This mirrors the standard markdown // code-block path in `crates/markdown/src/markdown.rs`. let mut code_scroll = div() .id(code_scroll_id) .flex() .flex_1() .min_w_0() .overflow_x_scroll() .child(div().flex_none().child(code_text)); code_scroll.style().restrict_scroll_to_axis = Some(true); container .child( h_flex() .items_start() .min_w_0() .w_full() .child(div().flex_none().pr_3().child(gutter)) .child(code_scroll), ) .child( h_flex() .w_4() .absolute() .top_0() .right_0() .justify_end() .visible_on_hover("read-file-code-block") .child(CopyButton::new(copy_button_id, code).tooltip_label("Copy Code")), ) .into_any_element()} fn highlight_code_runs( code: &str, language: Option<&Arc<Language>>, code_text_style: TextStyle, markdown_style: &MarkdownStyle,) -> Vec<TextRun> { if code.is_empty() { return Vec::new(); } let Some(language) = language else { return vec![code_text_style.to_run(code.len())]; }; let mut runs = Vec::new(); let mut offset = 0; for (range, highlight_id) in language.highlight_text(&Rope::from(code), 0..code.len()) { if range.start > offset { runs.push(code_text_style.to_run(range.start - offset)); } let mut run_style = code_text_style.clone(); if let Some(highlight) = markdown_style.syntax.get(highlight_id).cloned() { run_style = run_style.highlight(highlight); } runs.push(run_style.to_run(range.len())); offset = range.end; } if offset < code.len() { runs.push(code_text_style.to_run(code.len() - offset)); } runs} #[cfg(test)]mod numbered_code_block_tests { use super::*; #[test] fn parses_cat_numbered_markdown_code_block() { let parsed = parse_cat_numbered_markdown_code_block( "```rs zed/crates/example.rs\n 2\tfn main() {\n 3\t println!(\"hi\");\n 4\t}\n```\n", ) .expect("cat-numbered block should parse"); assert_eq!(parsed.line_count, 3); assert_eq!(parsed.first_number, 2); assert_eq!(parsed.code, "fn main() {\n println!(\"hi\");\n}"); } #[test] fn parses_cat_numbered_code_with_crlf_line_endings() { let parsed = parse_cat_numbered_code(" 1\tline one\r\n 2\tline two\r\n") .expect("crlf-terminated cat-numbered code should parse"); assert_eq!(parsed.line_count, 2); assert_eq!(parsed.first_number, 1); assert_eq!(parsed.code, "line one\nline two"); } #[test]Lines 1–500 of 12960. Load more
use crate::{ DEFAULT_THREAD_TITLE, SelectPermissionGranularity, agent_configuration::configure_context_server_modal::default_markdown_style, conversation_view::thread_search_bar::{ThreadSearchBar, ThreadSearchBarEvent}, open_abs_path_at_point, thread_metadata_store::{ThreadId, ThreadMetadataStore}, }; use agent_client_protocol::schema::v1 as acp; use std::cell::RefCell; use acp_thread::{ Elicitation, ElicitationEntryId, ElicitationStatus, PlanEntry, SandboxAuthorizationDetails, SandboxFallbackAuthorizationDetails, SandboxNotAppliedReason, decode_path_escapes, }; use agent::{ SandboxStatusKey, SandboxStatusRefresh, SkillLoadingIssue, SkillLoadingIssueKind, SkillLoadingIssuesUpdated, ThreadSandbox, VerifiedSandboxStatus, }; use agent_settings::UserAgentsMd; use agent_skills::MAX_SKILL_DESCRIPTION_LEN; use cloud_api_types::{SubmitAgentThreadFeedbackBody, SubmitAgentThreadFeedbackCommentsBody}; use editor::actions::OpenExcerpts; use sandbox::{SandboxFsPolicy, SandboxNetPolicy, SandboxPolicy}; use crate::completion_provider::{AvailableSkill, PromptLocalCommand, pluralize}; use crate::message_editor::SharedSessionCapabilities; use crate::ui::{ SandboxGroup, SandboxRow, SandboxSection, SandboxStatusTooltip, TerminalSandboxWarning, TerminalToolHeader, }; use crate::unicode_confusables; use db::kvp::KeyValueStore; use gpui::List; use gpui::Stateful; use gpui::TaskExt; use heapless::Vec as ArrayVec; use language_model::{ FastModeConfirmation, LanguageModel, LanguageModelEffortLevel, LanguageModelId, LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Speed, }; use notifications::status_toast::StatusToast; use settings::{update_settings_file, update_settings_file_with_completion}; use ui::{ ButtonLike, CalloutBorderPosition, Checkbox, SpinnerLabel, SpinnerVariant, SplitButton, SplitButtonStyle, Tab, ToggleState, }; use util::markdown::{source_position_from_fragment, split_local_url_fragment}; use workspace::{OpenOptions, SERIALIZATION_THROTTLE_TIME}; use super::elicitation::{ ElicitationCard, ElicitationCardHandlers, ElicitationFormState, should_render_elicitation, }; use super::*; const DATA_RETENTION_LEARN_MORE_URL: &str = "https://support.claude.com/en/articles/15425996-data-retention-practices-for-mythos-class-models"; #[derive(Default)] struct ThreadFeedbackState { feedback: Option<ThreadFeedback>, comments_editor: Option<Entity<Editor>>, } impl ThreadFeedbackState { pub fn submit( &mut self, thread: Entity<AcpThread>, feedback: ThreadFeedback, window: &mut Window, cx: &mut App, ) { let Some(telemetry) = thread.read(cx).connection().telemetry() else { return; }; let project = thread.read(cx).project().read(cx); let client = project.client(); let user_store = project.user_store(); let organization = user_store.read(cx).current_organization(); if self.feedback == Some(feedback) { return; } self.feedback = Some(feedback); match feedback { ThreadFeedback::Positive => { self.comments_editor = None; } ThreadFeedback::Negative => { self.comments_editor = Some(Self::build_feedback_comments_editor(window, cx)); } } let session_id = thread.read(cx).session_id().clone(); let parent_session_id = thread.read(cx).parent_session_id().cloned(); let agent_telemetry_id = thread.read(cx).connection().telemetry_id(); let task = telemetry.thread_data(&session_id, cx); let rating = match feedback { ThreadFeedback::Positive => "positive", ThreadFeedback::Negative => "negative", }; cx.background_spawn(async move { let thread = task.await?; client .cloud_client() .submit_agent_feedback(SubmitAgentThreadFeedbackBody { organization_id: organization.map(|organization| organization.id.clone()), agent: agent_telemetry_id.to_string(), session_id: session_id.to_string(), parent_session_id: parent_session_id.map(|id| id.to_string()), rating: rating.to_string(), thread, }) .await?; anyhow::Ok(()) }) .detach_and_log_err(cx); } pub fn submit_comments(&mut self, thread: Entity<AcpThread>, cx: &mut App) { let Some(telemetry) = thread.read(cx).connection().telemetry() else { return; }; let Some(comments) = self .comments_editor .as_ref() .map(|editor| editor.read(cx).text(cx)) .filter(|text| !text.trim().is_empty()) else { return; }; self.comments_editor.take(); let project = thread.read(cx).project().read(cx); let client = project.client(); let user_store = project.user_store(); let organization = user_store.read(cx).current_organization(); let session_id = thread.read(cx).session_id().clone(); let agent_telemetry_id = thread.read(cx).connection().telemetry_id(); let task = telemetry.thread_data(&session_id, cx); cx.background_spawn(async move { let thread = task.await?; client .cloud_client() .submit_agent_feedback_comments(SubmitAgentThreadFeedbackCommentsBody { organization_id: organization.map(|organization| organization.id.clone()), agent: agent_telemetry_id.to_string(), session_id: session_id.to_string(), comments, thread, }) .await?; anyhow::Ok(()) }) .detach_and_log_err(cx); } pub fn clear(&mut self) { *self = Self::default() } pub fn dismiss_comments(&mut self) { self.comments_editor.take(); } fn build_feedback_comments_editor(window: &mut Window, cx: &mut App) -> Entity<Editor> { let buffer = cx.new(|cx| { let empty_string = String::new(); MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx) }); let editor = cx.new(|cx| { let mut editor = Editor::new( editor::EditorMode::AutoHeight { min_lines: 1, max_lines: Some(4), }, buffer, None, window, cx, ); editor.set_placeholder_text( "What went wrong? Share your feedback so we can improve.", window, cx, ); editor }); editor.read(cx).focus_handle(cx).focus(window, cx); editor } } struct GeneratingSpinner { variant: SpinnerVariant, } impl GeneratingSpinner { fn new(variant: SpinnerVariant) -> Self { Self { variant } } } impl Render for GeneratingSpinner { fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { SpinnerLabel::with_variant(self.variant).size(LabelSize::Small) } } #[derive(IntoElement)] struct GeneratingSpinnerElement { variant: SpinnerVariant, } impl GeneratingSpinnerElement { fn new(variant: SpinnerVariant) -> Self { Self { variant } } } impl RenderOnce for GeneratingSpinnerElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let id = match self.variant { SpinnerVariant::Dots => "generating-spinner-view", SpinnerVariant::Sand => "confirmation-spinner-view", _ => "spinner-view", }; window.with_id(id, |window| { window.use_state(cx, |_, _| GeneratingSpinner::new(self.variant)) }) } } pub enum AcpThreadViewEvent { Interacted, } impl EventEmitter<AcpThreadViewEvent> for ThreadView {} /// `cat -n`-style numbered code block, already stripped of its line-number /// prefixes and ready to render. Line numbers are guaranteed to be contiguous /// starting at `first_number`, so we only store the first number and the line /// count rather than allocating a per-line `Vec`. struct ParsedCatNumberedCode { code: String, first_number: u32, line_count: usize, } fn parse_cat_numbered_markdown_code_block(markdown: &str) -> Option<ParsedCatNumberedCode> { let (_tag, code) = parse_single_fenced_code_block(markdown)?; parse_cat_numbered_code(code) } fn parse_single_fenced_code_block(markdown: &str) -> Option<(&str, &str)> { let first_non_backtick = markdown.find(|character| character != '`')?; if first_non_backtick < 3 { return None; } let fence = &markdown[..first_non_backtick]; let after_opening_fence = &markdown[first_non_backtick..]; let tag_end = after_opening_fence.find('\n')?; let tag = &after_opening_fence[..tag_end]; let after_tag = &after_opening_fence[tag_end + 1..]; let closing_fence = format!("\n{fence}\n"); let code = after_tag.strip_suffix(&closing_fence)?; Some((tag, code)) } /// Walks `code` exactly once: for each line it validates and strips the /// `NNN\t` prefix, then pushes the line's content into the accumulating /// code buffer (with `\n` between lines, no trailing newline). Verifies that /// the line numbers form a contiguous, increasing sequence. fn parse_cat_numbered_code(code: &str) -> Option<ParsedCatNumberedCode> { if code.is_empty() { return None; } let mut output = String::with_capacity(code.len()); let mut first_number = None; let mut expected_number = None; let mut line_count: usize = 0; for raw_line in code.split_inclusive('\n') { let line = strip_line_ending(raw_line); let (number, text) = parse_cat_numbered_line(line)?; if let Some(expected) = expected_number { if number != expected { return None; } } else { first_number = Some(number); } expected_number = number.checked_add(1); if line_count > 0 { output.push('\n'); } output.push_str(text); line_count += 1; } Some(ParsedCatNumberedCode { code: output, first_number: first_number?, line_count, }) } fn strip_line_ending(line: &str) -> &str { let without_lf = line.strip_suffix('\n').unwrap_or(line); without_lf.strip_suffix('\r').unwrap_or(without_lf) } fn parse_cat_numbered_line(line: &str) -> Option<(u32, &str)> { let (prefix, text) = line.split_once('\t')?; let number = prefix.trim(); if number.is_empty() || !prefix .chars() .all(|character| character == ' ' || character.is_ascii_digit()) { return None; } Some((number.parse().ok()?, text)) } fn render_cat_numbered_code_block( parsed: ParsedCatNumberedCode, language: Option<Arc<Language>>, markdown_style: MarkdownStyle, copy_button_id: String, cx: &App, ) -> AnyElement { use std::fmt::Write as _; let ParsedCatNumberedCode { code, first_number, line_count, } = parsed; // Line numbers are contiguous (verified during parsing), so the largest // line number is `first_number + line_count - 1`. Sizing the gutter to // that number's digit count means every rendered line contributes exactly // `gutter_width` bytes to the gutter, plus a newline between adjacent // lines. let last_number = first_number .saturating_add(u32::try_from(line_count.saturating_sub(1)).unwrap_or(u32::MAX)); let gutter_width = last_number.to_string().len().max(1); let gutter_capacity = line_count * gutter_width + line_count.saturating_sub(1); let mut gutter = String::with_capacity(gutter_capacity); for i in 0..line_count { if i > 0 { gutter.push('\n'); } let line_number = first_number.saturating_add(u32::try_from(i).unwrap_or(u32::MAX)); // Writes to a `String` are infallible, so the `Result` can be ignored. let _ = write!(&mut gutter, "{line_number:>gutter_width$}"); } let mut code_text_style = markdown_style.base_text_style.clone(); code_text_style.refine(&markdown_style.code_block.text); let mut gutter_text_style = code_text_style.clone(); gutter_text_style.color = cx.theme().colors().text_muted; let gutter_len = gutter.len(); let gutter = StyledText::new(gutter).with_runs(vec![gutter_text_style.to_run(gutter_len)]); // Share `code` between syntax highlighting, the rendered `StyledText`, and // the copy button via a single `SharedString` (cheap `Arc` clones) instead // of cloning the underlying `String`. let code: SharedString = code.into(); let code_runs = highlight_code_runs(&code, language.as_ref(), code_text_style, &markdown_style); let code_text = StyledText::new(code.clone()).with_runs(code_runs); let code_block_id = format!("read-file-code-block-{copy_button_id}"); let code_scroll_id = format!("read-file-code-scroll-{copy_button_id}"); let mut container = div() .id(code_block_id) .group("read-file-code-block") .relative() .w_full() .whitespace_nowrap(); container.style().refine(&markdown_style.code_block); // `overflow_x_scroll` only actually scrolls when the container is laid out // as a flex container: in GPUI the default `Display` is `Block`, and a // block-level child fills its parent's content width instead of overflowing // it, so there is nothing for the scroll viewport to scroll. Using `flex()` // on the scroll wrapper plus `flex_none()` on the inner item lets the inner // item take its natural width (the unwrapped code), which is what overflows. // `restrict_scroll_to_axis` then keeps vertical wheel events flowing through // to the outer thread scroller. This mirrors the standard markdown // code-block path in `crates/markdown/src/markdown.rs`. let mut code_scroll = div() .id(code_scroll_id) .flex() .flex_1() .min_w_0() .overflow_x_scroll() .child(div().flex_none().child(code_text)); code_scroll.style().restrict_scroll_to_axis = Some(true); container .child( h_flex() .items_start() .min_w_0() .w_full() .child(div().flex_none().pr_3().child(gutter)) .child(code_scroll), ) .child( h_flex() .w_4() .absolute() .top_0() .right_0() .justify_end() .visible_on_hover("read-file-code-block") .child(CopyButton::new(copy_button_id, code).tooltip_label("Copy Code")), ) .into_any_element() } fn highlight_code_runs( code: &str, language: Option<&Arc<Language>>, code_text_style: TextStyle, markdown_style: &MarkdownStyle, ) -> Vec<TextRun> { if code.is_empty() { return Vec::new(); } let Some(language) = language else { return vec![code_text_style.to_run(code.len())]; }; let mut runs = Vec::new(); let mut offset = 0; for (range, highlight_id) in language.highlight_text(&Rope::from(code), 0..code.len()) { if range.start > offset { runs.push(code_text_style.to_run(range.start - offset)); } let mut run_style = code_text_style.clone(); if let Some(highlight) = markdown_style.syntax.get(highlight_id).cloned() { run_style = run_style.highlight(highlight); } runs.push(run_style.to_run(range.len())); offset = range.end; } if offset < code.len() { runs.push(code_text_style.to_run(code.len() - offset)); } runs } #[cfg(test)] mod numbered_code_block_tests { use super::*; #[test] fn parses_cat_numbered_markdown_code_block() { let parsed = parse_cat_numbered_markdown_code_block( "```rs zed/crates/example.rs\n 2\tfn main() {\n 3\t println!(\"hi\");\n 4\t}\n```\n", ) .expect("cat-numbered block should parse"); assert_eq!(parsed.line_count, 3); assert_eq!(parsed.first_number, 2); assert_eq!(parsed.code, "fn main() {\n println!(\"hi\");\n}"); } #[test] fn parses_cat_numbered_code_with_crlf_line_endings() { let parsed = parse_cat_numbered_code(" 1\tline one\r\n 2\tline two\r\n") .expect("crlf-terminated cat-numbered code should parse"); assert_eq!(parsed.line_count, 2); assert_eq!(parsed.first_number, 1); assert_eq!(parsed.code, "line one\nline two"); } #[test]