Ensure context inserted via commands is syntax-highlighted (#13133)

Antonio Scandurra created

Release Notes:

- N/A

Change summary

crates/assistant/src/slash_command/active_command.rs | 17 ++--
crates/assistant/src/slash_command/file_command.rs   | 49 ++++++++++---
crates/assistant/src/slash_command/search_command.rs | 25 +++---
crates/assistant/src/slash_command/tabs_command.rs   | 17 +---
4 files changed, 61 insertions(+), 47 deletions(-)

Detailed changes

crates/assistant/src/slash_command/active_command.rs 🔗

@@ -1,10 +1,13 @@
-use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput};
+use super::{
+    file_command::{codeblock_fence_for_path, FilePlaceholder},
+    SlashCommand, SlashCommandOutput,
+};
 use anyhow::{anyhow, Result};
 use assistant_slash_command::SlashCommandOutputSection;
 use editor::Editor;
 use gpui::{AppContext, Task, WeakView};
 use language::LspAdapterDelegate;
-use std::{borrow::Cow, sync::Arc};
+use std::sync::Arc;
 use ui::{IntoElement, WindowContext};
 use workspace::Workspace;
 
@@ -60,14 +63,8 @@ impl SlashCommand for ActiveSlashCommand {
             let text = cx.background_executor().spawn({
                 let path = path.clone();
                 async move {
-                    let path = path
-                        .as_ref()
-                        .map(|path| path.to_string_lossy())
-                        .unwrap_or_else(|| Cow::Borrowed("untitled"));
-
-                    let mut output = String::with_capacity(path.len() + snapshot.len() + 9);
-                    output.push_str("```");
-                    output.push_str(&path);
+                    let mut output = String::new();
+                    output.push_str(&codeblock_fence_for_path(path.as_deref(), None));
                     output.push('\n');
                     for chunk in snapshot.as_rope().chunks() {
                         output.push_str(chunk);

crates/assistant/src/slash_command/file_command.rs 🔗

@@ -6,6 +6,7 @@ use gpui::{AppContext, RenderOnce, SharedString, Task, View, WeakView};
 use language::{LineEnding, LspAdapterDelegate};
 use project::PathMatchCandidateSet;
 use std::{
+    fmt::Write,
     ops::Range,
     path::{Path, PathBuf},
     sync::{atomic::AtomicBool, Arc},
@@ -155,20 +156,20 @@ impl SlashCommand for FileSlashCommand {
         };
 
         let fs = workspace.read(cx).app_state().fs.clone();
-        let argument = argument.to_string();
-        let text = cx.background_executor().spawn(async move {
-            let mut content = fs.load(&abs_path).await?;
-            LineEnding::normalize(&mut content);
-            let mut output = String::with_capacity(argument.len() + content.len() + 9);
-            output.push_str("```");
-            output.push_str(&argument);
-            output.push('\n');
-            output.push_str(&content);
-            if !output.ends_with('\n') {
-                output.push('\n');
+        let text = cx.background_executor().spawn({
+            let path = path.clone();
+            async move {
+                let mut content = fs.load(&abs_path).await?;
+                LineEnding::normalize(&mut content);
+                let mut output = String::new();
+                output.push_str(&codeblock_fence_for_path(Some(&path), None));
+                output.push_str(&content);
+                if !output.ends_with('\n') {
+                    output.push('\n');
+                }
+                output.push_str("```");
+                anyhow::Ok(output)
             }
-            output.push_str("```");
-            anyhow::Ok(output)
         });
         cx.foreground_executor().spawn(async move {
             let text = text.await?;
@@ -224,3 +225,25 @@ impl RenderOnce for FilePlaceholder {
             .on_click(move |_, cx| unfold(cx))
     }
 }
+
+pub fn codeblock_fence_for_path(path: Option<&Path>, row_range: Option<Range<u32>>) -> String {
+    let mut text = String::new();
+    write!(text, "```").unwrap();
+
+    if let Some(path) = path {
+        if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
+            write!(text, "{} ", extension).unwrap();
+        }
+
+        write!(text, "{}", path.display()).unwrap();
+    } else {
+        write!(text, "untitled").unwrap();
+    }
+
+    if let Some(row_range) = row_range {
+        write!(text, ":{}-{}", row_range.start + 1, row_range.end + 1).unwrap();
+    }
+
+    text.push('\n');
+    text
+}

crates/assistant/src/slash_command/search_command.rs 🔗

@@ -1,4 +1,7 @@
-use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput};
+use super::{
+    file_command::{codeblock_fence_for_path, FilePlaceholder},
+    SlashCommand, SlashCommandOutput,
+};
 use anyhow::Result;
 use assistant_slash_command::SlashCommandOutputSection;
 use gpui::{AppContext, Task, WeakView};
@@ -125,9 +128,8 @@ impl SlashCommand for SearchSlashCommand {
                         let range_start = result.range.start.min(file_content.len());
                         let range_end = result.range.end.min(file_content.len());
 
-                        let start_line =
-                            file_content[0..range_start].matches('\n').count() as u32 + 1;
-                        let end_line = file_content[0..range_end].matches('\n').count() as u32 + 1;
+                        let start_row = file_content[0..range_start].matches('\n').count() as u32;
+                        let end_row = file_content[0..range_end].matches('\n').count() as u32;
                         let start_line_byte_offset = file_content[0..range_start]
                             .rfind('\n')
                             .map(|pos| pos + 1)
@@ -138,14 +140,11 @@ impl SlashCommand for SearchSlashCommand {
                             .unwrap_or_else(|| file_content.len());
 
                         let section_start_ix = text.len();
-                        writeln!(
-                            text,
-                            "```{}:{}-{}",
-                            result.path.display(),
-                            start_line,
-                            end_line,
-                        )
-                        .unwrap();
+                        text.push_str(&codeblock_fence_for_path(
+                            Some(&result.path),
+                            Some(start_row..end_row),
+                        ));
+
                         let mut excerpt =
                             file_content[start_line_byte_offset..end_line_byte_offset].to_string();
                         LineEnding::normalize(&mut excerpt);
@@ -159,7 +158,7 @@ impl SlashCommand for SearchSlashCommand {
                                 FilePlaceholder {
                                     id,
                                     path: Some(full_path.clone()),
-                                    line_range: Some(start_line..end_line),
+                                    line_range: Some(start_row..end_row),
                                     unfold,
                                 }
                                 .into_any_element()

crates/assistant/src/slash_command/tabs_command.rs 🔗

@@ -1,11 +1,14 @@
-use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput};
+use super::{
+    file_command::{codeblock_fence_for_path, FilePlaceholder},
+    SlashCommand, SlashCommandOutput,
+};
 use anyhow::{anyhow, Result};
 use assistant_slash_command::SlashCommandOutputSection;
 use collections::HashMap;
 use editor::Editor;
 use gpui::{AppContext, Entity, Task, WeakView};
 use language::LspAdapterDelegate;
-use std::{fmt::Write, path::Path, sync::Arc};
+use std::{fmt::Write, sync::Arc};
 use ui::{IntoElement, WindowContext};
 use workspace::Workspace;
 
@@ -77,15 +80,7 @@ impl SlashCommand for TabsSlashCommand {
                 let mut text = String::new();
                 for (full_path, buffer, _) in open_buffers {
                     let section_start_ix = text.len();
-                    writeln!(
-                        text,
-                        "```{}\n",
-                        full_path
-                            .as_deref()
-                            .unwrap_or(Path::new("untitled"))
-                            .display()
-                    )
-                    .unwrap();
+                    text.push_str(&codeblock_fence_for_path(full_path.as_deref(), None));
                     for chunk in buffer.as_rope().chunks() {
                         text.push_str(chunk);
                     }