357 lines · 11.6 KB · Rust
RawDownload
use anyhow::{Result, anyhow};use assistant_slash_command::{    ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,    SlashCommandOutputSection, SlashCommandResult,};use editor::{BufferOffset, Editor, MultiBufferSnapshot};use futures::StreamExt;use gpui::{App, SharedString, Task, WeakEntity, Window};use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate}; use rope::Point;use std::ops::Range;use std::sync::Arc;use std::sync::atomic::AtomicBool;use ui::IconName;use workspace::Workspace; use crate::file_command::codeblock_fence_for_path; pub struct SelectionCommand; impl SlashCommand for SelectionCommand {    fn name(&self) -> String {        "selection".into()    }     fn label(&self, _cx: &App) -> CodeLabel {        CodeLabel::plain(self.name(), None)    }     fn description(&self) -> String {        "Insert editor selection".into()    }     fn icon(&self) -> IconName {        IconName::Quote    }     fn menu_text(&self) -> String {        self.description()    }     fn requires_argument(&self) -> bool {        false    }     fn accepts_arguments(&self) -> bool {        true    }     fn complete_argument(        self: Arc<Self>,        _arguments: &[String],        _cancel: Arc<AtomicBool>,        _workspace: Option<WeakEntity<Workspace>>,        _window: &mut Window,        _cx: &mut App,    ) -> Task<Result<Vec<ArgumentCompletion>>> {        Task::ready(Err(anyhow!("this command does not require argument")))    }     fn run(        self: Arc<Self>,        _arguments: &[String],        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],        _context_buffer: BufferSnapshot,        workspace: WeakEntity<Workspace>,        _delegate: Option<Arc<dyn LspAdapterDelegate>>,        _window: &mut Window,        cx: &mut App,    ) -> Task<SlashCommandResult> {        let mut events = vec![];         let Some(creases) = workspace            .update(cx, |workspace, cx| {                let editor = workspace                    .active_item(cx)                    .and_then(|item| item.act_as::<Editor>(cx))?;                 editor.update(cx, |editor, cx| {                    let selection_ranges = editor                        .selections                        .all_adjusted(&editor.display_snapshot(cx))                        .iter()                        .map(|selection| selection.range())                        .collect::<Vec<_>>();                    let snapshot = editor.buffer().read(cx).snapshot(cx);                    Some(selections_creases(selection_ranges, snapshot, cx))                })            })            .unwrap_or_else(|e| {                events.push(Err(e));                None            })        else {            return Task::ready(Err(anyhow!("no active selection")));        };         for (text, title) in creases {            events.push(Ok(SlashCommandEvent::StartSection {                icon: IconName::TextSnippet,                label: SharedString::from(title),                metadata: None,            }));            events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {                text,                run_commands_in_text: false,            })));            events.push(Ok(SlashCommandEvent::EndSection));            events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {                text: "\n".to_string(),                run_commands_in_text: false,            })));        }         let result = futures::stream::iter(events).boxed();         Task::ready(Ok(result))    }} pub fn selections_creases(    selection_ranges: Vec<Range<Point>>,    snapshot: MultiBufferSnapshot,    cx: &App,) -> Vec<(String, String)> {    let mut creases = Vec::new();    for range in selection_ranges {        let buffer_ranges = snapshot.range_to_buffer_ranges(range.clone());         if buffer_ranges.is_empty() {            creases.extend(crease_for_range(range, &snapshot, cx));            continue;        }         for (buffer_snapshot, buffer_range, _excerpt_id) in buffer_ranges {            creases.extend(crease_for_buffer_range(buffer_snapshot, buffer_range, cx));        }    }    creases} /// Creates a crease for a range within a specific buffer (excerpt)./// This is used when we know the exact buffer and range within it.fn crease_for_buffer_range(    buffer: &BufferSnapshot,    Range { start, end }: Range<BufferOffset>,    cx: &App,) -> Option<(String, String)> {    let selected_text: String = buffer.text_for_range(start.0..end.0).collect();     if selected_text.is_empty() {        return None;    }     let start_point = buffer.offset_to_point(start.0);    let end_point = buffer.offset_to_point(end.0);    let start_buffer_row = start_point.row;    let end_buffer_row = end_point.row;     let language = buffer.language_at(start.0);    let language_name_arc = language.map(|l| l.code_fence_block_name());    let language_name = language_name_arc.as_deref().unwrap_or_default();     let filename = buffer        .file()        .map(|file| file.full_path(cx).to_string_lossy().into_owned());     let text = if language_name == "markdown" {        selected_text            .lines()            .map(|line| format!("> {}", line))            .collect::<Vec<_>>()            .join("\n")    } else {        let start_symbols = buffer.symbols_containing(start, None);        let end_symbols = buffer.symbols_containing(end, None);         let outline_text = if !start_symbols.is_empty() && !end_symbols.is_empty() {            Some(                start_symbols                    .into_iter()                    .zip(end_symbols)                    .take_while(|(a, b)| a == b)                    .map(|(a, _)| a.text)                    .collect::<Vec<_>>()                    .join(" > "),            )        } else {            None        };         let line_comment_prefix =            language.and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());         let fence =            codeblock_fence_for_path(filename.as_deref(), Some(start_buffer_row..=end_buffer_row));         if let Some((line_comment_prefix, outline_text)) = line_comment_prefix.zip(outline_text) {            let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");            format!("{fence}{breadcrumb}{selected_text}\n```")        } else {            format!("{fence}{selected_text}\n```")        }    };     let crease_title = if let Some(path) = filename {        let start_line = start_buffer_row + 1;        let end_line = end_buffer_row + 1;        if start_line == end_line {            format!("{path}, Line {start_line}")        } else {            format!("{path}, Lines {start_line} to {end_line}")        }    } else {        "Quoted selection".to_string()    };     Some((text, crease_title))} /// Fallback function to create a crease from a multibuffer range when we can't split by excerpt.fn crease_for_range(    range: Range<Point>,    snapshot: &MultiBufferSnapshot,    cx: &App,) -> Option<(String, String)> {    let selected_text = snapshot.text_for_range(range.clone()).collect::<String>();    if selected_text.is_empty() {        return None;    }     // Get actual file line numbers (not multibuffer row numbers)    let start_buffer_row = snapshot        .point_to_buffer_point(range.start)        .map(|(_, point, _)| point.row)        .unwrap_or(range.start.row);    let end_buffer_row = snapshot        .point_to_buffer_point(range.end)        .map(|(_, point, _)| point.row)        .unwrap_or(range.end.row);     let start_language = snapshot.language_at(range.start);    let end_language = snapshot.language_at(range.end);    let language_name = if start_language == end_language {        start_language.map(|language| language.code_fence_block_name())    } else {        None    };    let language_name = language_name.as_deref().unwrap_or("");     let filename = snapshot        .file_at(range.start)        .map(|file| file.full_path(cx).to_string_lossy().into_owned());     let text = if language_name == "markdown" {        selected_text            .lines()            .map(|line| format!("> {}", line))            .collect::<Vec<_>>()            .join("\n")    } else {        let start_symbols = snapshot            .symbols_containing(range.start, None)            .map(|(_, symbols)| symbols);        let end_symbols = snapshot            .symbols_containing(range.end, None)            .map(|(_, symbols)| symbols);         let outline_text =            if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) {                Some(                    start_symbols                        .into_iter()                        .zip(end_symbols)                        .take_while(|(a, b)| a == b)                        .map(|(a, _)| a.text)                        .collect::<Vec<_>>()                        .join(" > "),                )            } else {                None            };         let line_comment_prefix =            start_language.and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());         let fence =            codeblock_fence_for_path(filename.as_deref(), Some(start_buffer_row..=end_buffer_row));         if let Some((line_comment_prefix, outline_text)) = line_comment_prefix.zip(outline_text) {            let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");            format!("{fence}{breadcrumb}{selected_text}\n```")        } else {            format!("{fence}{selected_text}\n```")        }    };     let crease_title = if let Some(path) = filename {        let start_line = start_buffer_row + 1;        let end_line = end_buffer_row + 1;        if start_line == end_line {            format!("{path}, Line {start_line}")        } else {            format!("{path}, Lines {start_line} to {end_line}")        }    } else {        "Quoted selection".to_string()    };     Some((text, crease_title))} #[cfg(test)]mod tests {    use super::*;    use gpui::TestAppContext;    use multi_buffer::MultiBuffer;     #[gpui::test]    fn test_selections_creases_single_excerpt(cx: &mut TestAppContext) {        let buffer = cx.update(|cx| {            MultiBuffer::build_multi(                [("a\nb\nc\n", vec![Point::new(0, 0)..Point::new(3, 0)])],                cx,            )        });        let creases = cx.update(|cx| {            let snapshot = buffer.read(cx).snapshot(cx);            selections_creases(vec![Point::new(0, 0)..Point::new(2, 1)], snapshot, cx)        });        assert_eq!(creases.len(), 1);        assert_eq!(creases[0].0, "```untitled:1-3\na\nb\nc\n```");        assert_eq!(creases[0].1, "Quoted selection");    }     #[gpui::test]    fn test_selections_creases_spans_multiple_excerpts(cx: &mut TestAppContext) {        let buffer = cx.update(|cx| {            MultiBuffer::build_multi(                [                    ("aaa\nbbb\n", vec![Point::new(0, 0)..Point::new(2, 0)]),                    ("111\n222\n", vec![Point::new(0, 0)..Point::new(2, 0)]),                ],                cx,            )        });        let creases = cx.update(|cx| {            let snapshot = buffer.read(cx).snapshot(cx);            let end = snapshot.offset_to_point(snapshot.len());            selections_creases(vec![Point::new(0, 0)..end], snapshot, cx)        });        assert_eq!(creases.len(), 2);        assert!(creases[0].0.contains("aaa") && !creases[0].0.contains("111"));        assert!(creases[1].0.contains("111") && !creases[1].0.contains("aaa"));    }}