1521 lines · 53.0 KB · Rust
RawDownload
use crate::diagnostics::{DiagnosticsOptions, codeblock_fence_for_path, collect_diagnostics};use acp_thread::{MentionUri, selection_name};use agent::{ThreadStore, outline};use agent_client_protocol::schema::v1 as acp;use agent_servers::{AgentServer, AgentServerDelegate};use anyhow::{Context as _, Result, anyhow};use collections::{HashMap, HashSet};use editor::{    Anchor, Editor, EditorSnapshot, FoldPlaceholder, ToOffset,    display_map::{Crease, CreaseId, CreaseMetadata, FoldId},    scroll::Autoscroll,};use futures::{AsyncReadExt as _, FutureExt as _, future::Shared};use gpui::{    AppContext, ClipboardEntry, Context, Empty, Entity, EntityId, Image, ImageFormat, Img,    SharedString, Task, WeakEntity,};use http_client::{AsyncBody, HttpClientWithUrl};use itertools::Either;use language::Buffer;use language_model::{LanguageModelImage, LanguageModelImageExt};use multi_buffer::MultiBufferRow;use postage::stream::Stream as _;use project::{Project, ProjectItem, ProjectPath, Worktree};use rope::Point;use std::{    cell::RefCell,    ffi::OsStr,    fmt::Write,    ops::{Range, RangeInclusive},    path::{Path, PathBuf},    rc::Rc,    sync::Arc,};use text::OffsetRangeExt;use ui::{Disclosure, Toggleable, prelude::*};use util::{ResultExt, debug_panic, rel_path::RelPath};use workspace::{Workspace, notifications::NotifyResultExt as _}; use crate::ui::MentionCrease; pub type MentionTask = Shared<Task<Result<Mention, String>>>; #[derive(Debug, Clone, Eq, PartialEq)]pub enum Mention {    Text {        content: String,        tracked_buffers: Vec<Entity<Buffer>>,    },    Image(MentionImage),    Link,} #[derive(Clone, Debug, Eq, PartialEq)]pub struct MentionImage {    pub data: SharedString,    pub format: ImageFormat,} pub struct MentionSet {    project: WeakEntity<Project>,    thread_store: Option<Entity<ThreadStore>>,    mentions: HashMap<CreaseId, (MentionUri, MentionTask)>,    crease_entities: HashMap<CreaseId, Entity<LoadingContext>>,} impl MentionSet {    pub fn new(project: WeakEntity<Project>, thread_store: Option<Entity<ThreadStore>>) -> Self {        Self {            project,            thread_store,            mentions: HashMap::default(),            crease_entities: HashMap::default(),        }    }     pub fn contents(        &self,        full_mention_content: bool,        cx: &mut App,    ) -> Task<Result<HashMap<CreaseId, (MentionUri, Mention)>>> {        let Some(project) = self.project.upgrade() else {            return Task::ready(Err(anyhow!("Project not found")));        };        let mentions = self.mentions.clone();        cx.spawn(async move |cx| {            let mut contents = HashMap::default();            for (crease_id, (mention_uri, task)) in mentions {                let content = if full_mention_content                    && let MentionUri::Directory { abs_path } = &mention_uri                {                    cx.update(|cx| full_mention_for_directory(&project, abs_path, cx))                        .await?                } else {                    task.await.map_err(|e| anyhow!("{e}"))?                };                 contents.insert(crease_id, (mention_uri, content));            }            Ok(contents)        })    }     pub fn remove_invalid(&mut self, snapshot: &EditorSnapshot) {        for (crease_id, crease) in snapshot.crease_snapshot.creases() {            if !crease.range().start.is_valid(snapshot.buffer_snapshot()) {                self.mentions.remove(&crease_id);                self.crease_entities.remove(&crease_id);            }        }    }     pub fn insert_mention(        &mut self,        crease_id: CreaseId,        uri: MentionUri,        task: MentionTask,        crease_entity: Option<Entity<LoadingContext>>,        cx: &mut App,    ) {        self.mentions.insert(crease_id, (uri, task));        if let Some(entity) = crease_entity {            self.crease_entities.insert(crease_id, entity);        }        self.recompute_disambiguation(cx);    }     /// Creates the appropriate confirmation task for a mention based on its URI type.    /// This is used when pasting mention links to properly load their content.    pub fn confirm_mention_for_uri(        &mut self,        mention_uri: MentionUri,        supports_images: bool,        http_client: Arc<HttpClientWithUrl>,        cx: &mut Context<Self>,    ) -> Task<Result<Mention>> {        match mention_uri {            MentionUri::Fetch { url } => self.confirm_mention_for_fetch(url, http_client, cx),            MentionUri::Directory { .. } => Task::ready(Ok(Mention::Link)),            MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),            MentionUri::File { abs_path } => {                self.confirm_mention_for_file(abs_path, supports_images, cx)            }            MentionUri::Symbol {                abs_path,                line_range,                ..            } => self.confirm_mention_for_symbol(abs_path, line_range, cx),            MentionUri::Skill {                skill_file_path, ..            } => self.confirm_mention_for_skill(skill_file_path, cx),            MentionUri::Diagnostics {                include_errors,                include_warnings,            } => self.confirm_mention_for_diagnostics(include_errors, include_warnings, cx),            MentionUri::GitDiff { base_ref } => {                self.confirm_mention_for_git_diff(base_ref.into(), cx)            }            MentionUri::Selection {                abs_path: Some(abs_path),                line_range,                ..            } => self.confirm_mention_for_symbol(abs_path, line_range, cx),            MentionUri::Selection { abs_path: None, .. } => Task::ready(Err(anyhow!(                "Untitled buffer selection mentions are not supported for paste"            ))),            MentionUri::PastedImage { .. }            | MentionUri::TerminalSelection { .. }            | MentionUri::MergeConflict { .. }            | MentionUri::Rule { .. } => {                Task::ready(Err(anyhow!("Unsupported mention URI type for paste")))            }        }    }     pub fn remove_mention(&mut self, crease_id: &CreaseId, cx: &mut App) {        self.mentions.remove(crease_id);        self.crease_entities.remove(crease_id);        self.recompute_disambiguation(cx);    }     pub fn creases(&self) -> HashSet<CreaseId> {        self.mentions.keys().cloned().collect()    }     pub fn is_empty(&self) -> bool {        self.mentions.is_empty()    }     pub fn mentions(&self) -> HashSet<MentionUri> {        self.mentions.values().map(|(uri, _)| uri.clone()).collect()    }     pub fn mention_uri_for_crease(&self, crease_id: &CreaseId) -> Option<MentionUri> {        self.mentions.get(crease_id).map(|(uri, _)| uri.clone())    }     /// Returns the resolved mention for a crease, if any.    pub fn resolved_mention_for_crease(        &self,        crease_id: &CreaseId,    ) -> Option<(MentionUri, Option<Mention>)> {        let (uri, task) = self.mentions.get(crease_id)?;        let mention = task.clone().now_or_never().and_then(|result| result.ok());        Some((uri.clone(), mention))    }     pub fn set_mentions(&mut self, mentions: HashMap<CreaseId, (MentionUri, MentionTask)>) {        self.crease_entities            .retain(|id, _| mentions.contains_key(id));        self.mentions = mentions;    }     pub fn clear(&mut self) -> impl Iterator<Item = (CreaseId, (MentionUri, MentionTask))> {        self.crease_entities.clear();        self.mentions.drain()    }     fn recompute_disambiguation(&self, cx: &mut App) {        let labels =            compute_disambiguated_labels(self.mentions.iter().map(|(id, (uri, _))| (*id, uri)));         for (crease_id, new_label) in labels {            if let Some(entity) = self.crease_entities.get(&crease_id) {                entity.update(cx, |loading_ctx, cx| {                    if loading_ctx.label != new_label {                        loading_ctx.label = new_label;                        cx.notify();                    }                });            }        }    }     pub fn confirm_mention_completion(        &mut self,        crease_text: SharedString,        start: text::Anchor,        content_len: usize,        mention_uri: MentionUri,        supports_images: bool,        editor: Entity<Editor>,        workspace: &Entity<Workspace>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Task<()> {        let Some(project) = self.project.upgrade() else {            return Task::ready(());        };         let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));        let Some(start_anchor) = snapshot.buffer_snapshot().anchor_in_excerpt(start) else {            return Task::ready(());        };        let end_anchor = snapshot.buffer_snapshot().anchor_before(            start_anchor.to_offset(&snapshot.buffer_snapshot()) + content_len + 1usize,        );         let crease = if let MentionUri::File { abs_path } = &mention_uri            && is_raster_image_path(abs_path)        {            let Some(project_path) = project                .read(cx)                .project_path_for_absolute_path(&abs_path, cx)            else {                log::error!("project path not found for image mention {abs_path:?}");                return Task::ready(());            };            let image_task = project.update(cx, |project, cx| project.open_image(project_path, cx));            let image = cx                .spawn(async move |_, cx| {                    let image = image_task.await.map_err(|e| e.to_string())?;                    let image = image.update(cx, |image, _| image.image.clone());                    Ok(image)                })                .shared();            insert_crease_for_mention(                start,                content_len,                mention_uri.name().into(),                IconName::Image.path().into(),                mention_uri.tooltip_text(),                Some(mention_uri.clone()),                Some(workspace.downgrade()),                Some(image),                editor.clone(),                window,                cx,            )        } else {            insert_crease_for_mention(                start,                content_len,                crease_text,                mention_uri.icon_path(cx),                mention_uri.tooltip_text(),                Some(mention_uri.clone()),                Some(workspace.downgrade()),                None,                editor.clone(),                window,                cx,            )        };        let Some((crease_id, tx, crease_entity)) = crease else {            return Task::ready(());        };         let task = match mention_uri.clone() {            MentionUri::Fetch { url } => {                self.confirm_mention_for_fetch(url, workspace.read(cx).client().http_client(), cx)            }            MentionUri::Directory { .. } => Task::ready(Ok(Mention::Link)),            MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),            MentionUri::File { abs_path } => {                self.confirm_mention_for_file(abs_path, supports_images, cx)            }            MentionUri::Symbol {                abs_path,                line_range,                ..            } => self.confirm_mention_for_symbol(abs_path, line_range, cx),            MentionUri::Skill {                skill_file_path, ..            } => self.confirm_mention_for_skill(skill_file_path, cx),            MentionUri::Diagnostics {                include_errors,                include_warnings,            } => self.confirm_mention_for_diagnostics(include_errors, include_warnings, cx),            MentionUri::PastedImage { .. } => {                debug_panic!("pasted image URI should not be included in completions");                Task::ready(Err(anyhow!(                    "pasted imaged URI should not be included in completions"                )))            }            MentionUri::Selection { .. } => {                debug_panic!("unexpected selection URI");                Task::ready(Err(anyhow!("unexpected selection URI")))            }            MentionUri::TerminalSelection { .. } => {                debug_panic!("unexpected terminal URI");                Task::ready(Err(anyhow!("unexpected terminal URI")))            }            MentionUri::GitDiff { base_ref } => {                self.confirm_mention_for_git_diff(base_ref.into(), cx)            }            MentionUri::MergeConflict { .. } => {                debug_panic!("unexpected merge conflict URI");                Task::ready(Err(anyhow!("unexpected merge conflict URI")))            }            MentionUri::Rule { .. } => {                debug_panic!("unexpected rule URI");                Task::ready(Err(anyhow!("unexpected rule URI")))            }        };        let task = cx            .spawn(async move |_, _| task.await.map_err(|e| e.to_string()))            .shared();        self.mentions.insert(crease_id, (mention_uri, task.clone()));        if let Some(entity) = crease_entity {            self.crease_entities.insert(crease_id, entity);        }        self.recompute_disambiguation(cx);         // Notify the user if we failed to load the mentioned context        let workspace = workspace.downgrade();        cx.spawn(async move |this, mut cx| {            let result = task.await.notify_workspace_async_err(workspace, &mut cx);            drop(tx);            if result.is_none() {                this.update(cx, |this, cx| {                    editor.update(cx, |editor, cx| {                        // Remove mention                        editor.edit([(start_anchor..end_anchor, "")], cx);                    });                    this.mentions.remove(&crease_id);                    this.crease_entities.remove(&crease_id);                })                .ok();            }        })    }     pub fn confirm_mention_for_file(        &self,        abs_path: PathBuf,        supports_images: bool,        cx: &mut Context<Self>,    ) -> Task<Result<Mention>> {        let Some(project) = self.project.upgrade() else {            return Task::ready(Err(anyhow!("project not found")));        };         let Some(project_path) = project            .read(cx)            .project_path_for_absolute_path(&abs_path, cx)        else {            return Task::ready(Err(anyhow!(                "project path not found for file mention {abs_path:?}"            )));        };         if is_raster_image_path(&abs_path) {            if !supports_images {                return Task::ready(Err(anyhow!("This model does not support images yet")));            }            let task = project.update(cx, |project, cx| project.open_image(project_path, cx));            return cx.spawn(async move |_, cx| {                let image = task.await?;                let image = image.update(cx, |image, _| image.image.clone());                let image = cx                    .update(|cx| LanguageModelImage::from_image(image, cx))                    .await;                if let Some(image) = image {                    Ok(Mention::Image(MentionImage {                        data: image.source,                        format: LanguageModelImage::FORMAT,                    }))                } else {                    Err(anyhow!("Failed to convert image"))                }            });        }         let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));        cx.spawn(async move |_, cx| {            let buffer = match buffer.await {                Ok(buffer) => buffer,                Err(_error) => {                    return Ok(Mention::Link);                }            };            let buffer_content = outline::get_buffer_content_or_outline(                buffer.clone(),                Some(&abs_path.to_string_lossy()),                &cx,            )            .await?;             Ok(Mention::Text {                content: buffer_content.text,                tracked_buffers: vec![buffer],            })        })    }     fn confirm_mention_for_fetch(        &self,        url: url::Url,        http_client: Arc<HttpClientWithUrl>,        cx: &mut Context<Self>,    ) -> Task<Result<Mention>> {        cx.background_executor().spawn(async move {            let content = fetch_url_content(http_client, url.to_string()).await?;            Ok(Mention::Text {                content,                tracked_buffers: Vec::new(),            })        })    }     fn confirm_mention_for_symbol(        &self,        abs_path: PathBuf,        line_range: RangeInclusive<u32>,        cx: &mut Context<Self>,    ) -> Task<Result<Mention>> {        let Some(project) = self.project.upgrade() else {            return Task::ready(Err(anyhow!("project not found")));        };        let Some(project_path) = project            .read(cx)            .project_path_for_absolute_path(&abs_path, cx)        else {            return Task::ready(Err(anyhow!(                "project path not found for symbol mention {abs_path:?}"            )));        };        let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));        cx.spawn(async move |_, cx| {            let buffer = buffer.await?;            let mention = buffer.update(cx, |buffer, cx| {                let start = Point::new(*line_range.start(), 0).min(buffer.max_point());                let end = Point::new(*line_range.end() + 1, 0).min(buffer.max_point());                let content = buffer.text_for_range(start..end).collect();                Mention::Text {                    content,                    tracked_buffers: vec![cx.entity()],                }            });            Ok(mention)        })    }     fn confirm_mention_for_skill(        &self,        skill_file_path: PathBuf,        cx: &mut Context<Self>,    ) -> Task<Result<Mention>> {        // Built-in skills have synthetic paths that don't exist on disk;

Lines 1–500 of 1521.