assistant2: Add support for referencing symbols as context (#27513)
TODO Release Notes: - N/A
This commit is contained in:
parent
da47013e56
commit
a916bbf00c
9 changed files with 838 additions and 20 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -482,6 +482,7 @@ dependencies = [
|
||||||
"markdown",
|
"markdown",
|
||||||
"menu",
|
"menu",
|
||||||
"multi_buffer",
|
"multi_buffer",
|
||||||
|
"ordered-float 2.10.1",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"paths",
|
"paths",
|
||||||
"picker",
|
"picker",
|
||||||
|
|
|
@ -55,6 +55,7 @@ lsp.workspace = true
|
||||||
markdown.workspace = true
|
markdown.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
multi_buffer.workspace = true
|
multi_buffer.workspace = true
|
||||||
|
ordered-float.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
paths.workspace = true
|
paths.workspace = true
|
||||||
picker.workspace = true
|
picker.workspace = true
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
use std::path::Path;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use std::{ops::Range, path::Path};
|
||||||
|
|
||||||
use file_icons::FileIcons;
|
use file_icons::FileIcons;
|
||||||
use gpui::{App, Entity, SharedString};
|
use gpui::{App, Entity, SharedString};
|
||||||
use language::Buffer;
|
use language::Buffer;
|
||||||
use language_model::{LanguageModelRequestMessage, MessageContent};
|
use language_model::{LanguageModelRequestMessage, MessageContent};
|
||||||
|
use project::ProjectPath;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use text::BufferId;
|
use text::{Anchor, BufferId};
|
||||||
use ui::IconName;
|
use ui::IconName;
|
||||||
use util::post_inc;
|
use util::post_inc;
|
||||||
|
|
||||||
|
@ -38,6 +39,7 @@ pub struct ContextSnapshot {
|
||||||
pub enum ContextKind {
|
pub enum ContextKind {
|
||||||
File,
|
File,
|
||||||
Directory,
|
Directory,
|
||||||
|
Symbol,
|
||||||
FetchedUrl,
|
FetchedUrl,
|
||||||
Thread,
|
Thread,
|
||||||
}
|
}
|
||||||
|
@ -47,6 +49,7 @@ impl ContextKind {
|
||||||
match self {
|
match self {
|
||||||
ContextKind::File => IconName::File,
|
ContextKind::File => IconName::File,
|
||||||
ContextKind::Directory => IconName::Folder,
|
ContextKind::Directory => IconName::Folder,
|
||||||
|
ContextKind::Symbol => IconName::Code,
|
||||||
ContextKind::FetchedUrl => IconName::Globe,
|
ContextKind::FetchedUrl => IconName::Globe,
|
||||||
ContextKind::Thread => IconName::MessageCircle,
|
ContextKind::Thread => IconName::MessageCircle,
|
||||||
}
|
}
|
||||||
|
@ -57,6 +60,7 @@ impl ContextKind {
|
||||||
pub enum AssistantContext {
|
pub enum AssistantContext {
|
||||||
File(FileContext),
|
File(FileContext),
|
||||||
Directory(DirectoryContext),
|
Directory(DirectoryContext),
|
||||||
|
Symbol(SymbolContext),
|
||||||
FetchedUrl(FetchedUrlContext),
|
FetchedUrl(FetchedUrlContext),
|
||||||
Thread(ThreadContext),
|
Thread(ThreadContext),
|
||||||
}
|
}
|
||||||
|
@ -66,6 +70,7 @@ impl AssistantContext {
|
||||||
match self {
|
match self {
|
||||||
Self::File(file) => file.id,
|
Self::File(file) => file.id,
|
||||||
Self::Directory(directory) => directory.snapshot.id,
|
Self::Directory(directory) => directory.snapshot.id,
|
||||||
|
Self::Symbol(symbol) => symbol.id,
|
||||||
Self::FetchedUrl(url) => url.id,
|
Self::FetchedUrl(url) => url.id,
|
||||||
Self::Thread(thread) => thread.id,
|
Self::Thread(thread) => thread.id,
|
||||||
}
|
}
|
||||||
|
@ -85,6 +90,12 @@ pub struct DirectoryContext {
|
||||||
pub snapshot: ContextSnapshot,
|
pub snapshot: ContextSnapshot,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SymbolContext {
|
||||||
|
pub id: ContextId,
|
||||||
|
pub context_symbol: ContextSymbol,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct FetchedUrlContext {
|
pub struct FetchedUrlContext {
|
||||||
pub id: ContextId,
|
pub id: ContextId,
|
||||||
|
@ -113,11 +124,30 @@ pub struct ContextBuffer {
|
||||||
pub text: SharedString,
|
pub text: SharedString,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ContextSymbol {
|
||||||
|
pub id: ContextSymbolId,
|
||||||
|
pub buffer: Entity<Buffer>,
|
||||||
|
pub buffer_version: clock::Global,
|
||||||
|
/// The range that the symbol encloses, e.g. for function symbol, this will
|
||||||
|
/// include not only the signature, but also the body
|
||||||
|
pub enclosing_range: Range<Anchor>,
|
||||||
|
pub text: SharedString,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ContextSymbolId {
|
||||||
|
pub path: ProjectPath,
|
||||||
|
pub name: SharedString,
|
||||||
|
pub range: Range<Anchor>,
|
||||||
|
}
|
||||||
|
|
||||||
impl AssistantContext {
|
impl AssistantContext {
|
||||||
pub fn snapshot(&self, cx: &App) -> Option<ContextSnapshot> {
|
pub fn snapshot(&self, cx: &App) -> Option<ContextSnapshot> {
|
||||||
match &self {
|
match &self {
|
||||||
Self::File(file_context) => file_context.snapshot(cx),
|
Self::File(file_context) => file_context.snapshot(cx),
|
||||||
Self::Directory(directory_context) => Some(directory_context.snapshot()),
|
Self::Directory(directory_context) => Some(directory_context.snapshot()),
|
||||||
|
Self::Symbol(symbol_context) => symbol_context.snapshot(cx),
|
||||||
Self::FetchedUrl(fetched_url_context) => Some(fetched_url_context.snapshot()),
|
Self::FetchedUrl(fetched_url_context) => Some(fetched_url_context.snapshot()),
|
||||||
Self::Thread(thread_context) => Some(thread_context.snapshot(cx)),
|
Self::Thread(thread_context) => Some(thread_context.snapshot(cx)),
|
||||||
}
|
}
|
||||||
|
@ -197,6 +227,27 @@ impl DirectoryContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SymbolContext {
|
||||||
|
pub fn snapshot(&self, cx: &App) -> Option<ContextSnapshot> {
|
||||||
|
let buffer = self.context_symbol.buffer.read(cx);
|
||||||
|
let name = self.context_symbol.id.name.clone();
|
||||||
|
let path = buffer_path_log_err(buffer)?
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
.into();
|
||||||
|
|
||||||
|
Some(ContextSnapshot {
|
||||||
|
id: self.id,
|
||||||
|
name,
|
||||||
|
parent: Some(path),
|
||||||
|
tooltip: None,
|
||||||
|
icon_path: None,
|
||||||
|
kind: ContextKind::Symbol,
|
||||||
|
text: Box::new([self.context_symbol.text.clone()]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FetchedUrlContext {
|
impl FetchedUrlContext {
|
||||||
pub fn snapshot(&self) -> ContextSnapshot {
|
pub fn snapshot(&self) -> ContextSnapshot {
|
||||||
ContextSnapshot {
|
ContextSnapshot {
|
||||||
|
@ -232,6 +283,7 @@ pub fn attach_context_to_message(
|
||||||
) {
|
) {
|
||||||
let mut file_context = Vec::new();
|
let mut file_context = Vec::new();
|
||||||
let mut directory_context = Vec::new();
|
let mut directory_context = Vec::new();
|
||||||
|
let mut symbol_context = Vec::new();
|
||||||
let mut fetch_context = Vec::new();
|
let mut fetch_context = Vec::new();
|
||||||
let mut thread_context = Vec::new();
|
let mut thread_context = Vec::new();
|
||||||
|
|
||||||
|
@ -241,6 +293,7 @@ pub fn attach_context_to_message(
|
||||||
match context.kind {
|
match context.kind {
|
||||||
ContextKind::File => file_context.push(context),
|
ContextKind::File => file_context.push(context),
|
||||||
ContextKind::Directory => directory_context.push(context),
|
ContextKind::Directory => directory_context.push(context),
|
||||||
|
ContextKind::Symbol => symbol_context.push(context),
|
||||||
ContextKind::FetchedUrl => fetch_context.push(context),
|
ContextKind::FetchedUrl => fetch_context.push(context),
|
||||||
ContextKind::Thread => thread_context.push(context),
|
ContextKind::Thread => thread_context.push(context),
|
||||||
}
|
}
|
||||||
|
@ -251,6 +304,9 @@ pub fn attach_context_to_message(
|
||||||
if !directory_context.is_empty() {
|
if !directory_context.is_empty() {
|
||||||
capacity += 1;
|
capacity += 1;
|
||||||
}
|
}
|
||||||
|
if !symbol_context.is_empty() {
|
||||||
|
capacity += 1;
|
||||||
|
}
|
||||||
if !fetch_context.is_empty() {
|
if !fetch_context.is_empty() {
|
||||||
capacity += 1 + fetch_context.len();
|
capacity += 1 + fetch_context.len();
|
||||||
}
|
}
|
||||||
|
@ -281,6 +337,15 @@ pub fn attach_context_to_message(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !symbol_context.is_empty() {
|
||||||
|
context_chunks.push("The following symbols are available:\n");
|
||||||
|
for context in &symbol_context {
|
||||||
|
for chunk in &context.text {
|
||||||
|
context_chunks.push(&chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !fetch_context.is_empty() {
|
if !fetch_context.is_empty() {
|
||||||
context_chunks.push("The following fetched results are available:\n");
|
context_chunks.push("The following fetched results are available:\n");
|
||||||
for context in &fetch_context {
|
for context in &fetch_context {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
mod completion_provider;
|
mod completion_provider;
|
||||||
mod fetch_context_picker;
|
mod fetch_context_picker;
|
||||||
mod file_context_picker;
|
mod file_context_picker;
|
||||||
|
mod symbol_context_picker;
|
||||||
mod thread_context_picker;
|
mod thread_context_picker;
|
||||||
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
@ -16,6 +17,7 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use multi_buffer::MultiBufferRow;
|
use multi_buffer::MultiBufferRow;
|
||||||
use project::ProjectPath;
|
use project::ProjectPath;
|
||||||
|
use symbol_context_picker::SymbolContextPicker;
|
||||||
use thread_context_picker::{render_thread_context_entry, ThreadContextEntry};
|
use thread_context_picker::{render_thread_context_entry, ThreadContextEntry};
|
||||||
use ui::{
|
use ui::{
|
||||||
prelude::*, ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor,
|
prelude::*, ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor,
|
||||||
|
@ -39,6 +41,7 @@ pub enum ConfirmBehavior {
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum ContextPickerMode {
|
enum ContextPickerMode {
|
||||||
File,
|
File,
|
||||||
|
Symbol,
|
||||||
Fetch,
|
Fetch,
|
||||||
Thread,
|
Thread,
|
||||||
}
|
}
|
||||||
|
@ -49,6 +52,7 @@ impl TryFrom<&str> for ContextPickerMode {
|
||||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||||
match value {
|
match value {
|
||||||
"file" => Ok(Self::File),
|
"file" => Ok(Self::File),
|
||||||
|
"symbol" => Ok(Self::Symbol),
|
||||||
"fetch" => Ok(Self::Fetch),
|
"fetch" => Ok(Self::Fetch),
|
||||||
"thread" => Ok(Self::Thread),
|
"thread" => Ok(Self::Thread),
|
||||||
_ => Err(format!("Invalid context picker mode: {}", value)),
|
_ => Err(format!("Invalid context picker mode: {}", value)),
|
||||||
|
@ -60,6 +64,7 @@ impl ContextPickerMode {
|
||||||
pub fn mention_prefix(&self) -> &'static str {
|
pub fn mention_prefix(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::File => "file",
|
Self::File => "file",
|
||||||
|
Self::Symbol => "symbol",
|
||||||
Self::Fetch => "fetch",
|
Self::Fetch => "fetch",
|
||||||
Self::Thread => "thread",
|
Self::Thread => "thread",
|
||||||
}
|
}
|
||||||
|
@ -68,6 +73,7 @@ impl ContextPickerMode {
|
||||||
pub fn label(&self) -> &'static str {
|
pub fn label(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::File => "Files & Directories",
|
Self::File => "Files & Directories",
|
||||||
|
Self::Symbol => "Symbols",
|
||||||
Self::Fetch => "Fetch",
|
Self::Fetch => "Fetch",
|
||||||
Self::Thread => "Thread",
|
Self::Thread => "Thread",
|
||||||
}
|
}
|
||||||
|
@ -76,6 +82,7 @@ impl ContextPickerMode {
|
||||||
pub fn icon(&self) -> IconName {
|
pub fn icon(&self) -> IconName {
|
||||||
match self {
|
match self {
|
||||||
Self::File => IconName::File,
|
Self::File => IconName::File,
|
||||||
|
Self::Symbol => IconName::Code,
|
||||||
Self::Fetch => IconName::Globe,
|
Self::Fetch => IconName::Globe,
|
||||||
Self::Thread => IconName::MessageCircle,
|
Self::Thread => IconName::MessageCircle,
|
||||||
}
|
}
|
||||||
|
@ -86,6 +93,7 @@ impl ContextPickerMode {
|
||||||
enum ContextPickerState {
|
enum ContextPickerState {
|
||||||
Default(Entity<ContextMenu>),
|
Default(Entity<ContextMenu>),
|
||||||
File(Entity<FileContextPicker>),
|
File(Entity<FileContextPicker>),
|
||||||
|
Symbol(Entity<SymbolContextPicker>),
|
||||||
Fetch(Entity<FetchContextPicker>),
|
Fetch(Entity<FetchContextPicker>),
|
||||||
Thread(Entity<ThreadContextPicker>),
|
Thread(Entity<ThreadContextPicker>),
|
||||||
}
|
}
|
||||||
|
@ -205,6 +213,18 @@ impl ContextPicker {
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
ContextPickerMode::Symbol => {
|
||||||
|
self.mode = ContextPickerState::Symbol(cx.new(|cx| {
|
||||||
|
SymbolContextPicker::new(
|
||||||
|
context_picker.clone(),
|
||||||
|
self.workspace.clone(),
|
||||||
|
self.context_store.clone(),
|
||||||
|
self.confirm_behavior,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
}
|
||||||
ContextPickerMode::Fetch => {
|
ContextPickerMode::Fetch => {
|
||||||
self.mode = ContextPickerState::Fetch(cx.new(|cx| {
|
self.mode = ContextPickerState::Fetch(cx.new(|cx| {
|
||||||
FetchContextPicker::new(
|
FetchContextPicker::new(
|
||||||
|
@ -416,6 +436,7 @@ impl Focusable for ContextPicker {
|
||||||
match &self.mode {
|
match &self.mode {
|
||||||
ContextPickerState::Default(menu) => menu.focus_handle(cx),
|
ContextPickerState::Default(menu) => menu.focus_handle(cx),
|
||||||
ContextPickerState::File(file_picker) => file_picker.focus_handle(cx),
|
ContextPickerState::File(file_picker) => file_picker.focus_handle(cx),
|
||||||
|
ContextPickerState::Symbol(symbol_picker) => symbol_picker.focus_handle(cx),
|
||||||
ContextPickerState::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
|
ContextPickerState::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
|
||||||
ContextPickerState::Thread(thread_picker) => thread_picker.focus_handle(cx),
|
ContextPickerState::Thread(thread_picker) => thread_picker.focus_handle(cx),
|
||||||
}
|
}
|
||||||
|
@ -430,6 +451,7 @@ impl Render for ContextPicker {
|
||||||
.map(|parent| match &self.mode {
|
.map(|parent| match &self.mode {
|
||||||
ContextPickerState::Default(menu) => parent.child(menu.clone()),
|
ContextPickerState::Default(menu) => parent.child(menu.clone()),
|
||||||
ContextPickerState::File(file_picker) => parent.child(file_picker.clone()),
|
ContextPickerState::File(file_picker) => parent.child(file_picker.clone()),
|
||||||
|
ContextPickerState::Symbol(symbol_picker) => parent.child(symbol_picker.clone()),
|
||||||
ContextPickerState::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
|
ContextPickerState::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
|
||||||
ContextPickerState::Thread(thread_picker) => parent.child(thread_picker.clone()),
|
ContextPickerState::Thread(thread_picker) => parent.child(thread_picker.clone()),
|
||||||
})
|
})
|
||||||
|
@ -446,7 +468,11 @@ enum RecentEntry {
|
||||||
fn supported_context_picker_modes(
|
fn supported_context_picker_modes(
|
||||||
thread_store: &Option<WeakEntity<ThreadStore>>,
|
thread_store: &Option<WeakEntity<ThreadStore>>,
|
||||||
) -> Vec<ContextPickerMode> {
|
) -> Vec<ContextPickerMode> {
|
||||||
let mut modes = vec![ContextPickerMode::File, ContextPickerMode::Fetch];
|
let mut modes = vec![
|
||||||
|
ContextPickerMode::File,
|
||||||
|
ContextPickerMode::Symbol,
|
||||||
|
ContextPickerMode::Fetch,
|
||||||
|
];
|
||||||
if thread_store.is_some() {
|
if thread_store.is_some() {
|
||||||
modes.push(ContextPickerMode::Thread);
|
modes.push(ContextPickerMode::Thread);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ use gpui::{App, Entity, Task, WeakEntity};
|
||||||
use http_client::HttpClientWithUrl;
|
use http_client::HttpClientWithUrl;
|
||||||
use language::{Buffer, CodeLabel, HighlightId};
|
use language::{Buffer, CodeLabel, HighlightId};
|
||||||
use lsp::CompletionContext;
|
use lsp::CompletionContext;
|
||||||
use project::{Completion, CompletionIntent, ProjectPath, WorktreeId};
|
use project::{Completion, CompletionIntent, ProjectPath, Symbol, WorktreeId};
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use text::{Anchor, ToPoint};
|
use text::{Anchor, ToPoint};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
|
@ -308,6 +308,66 @@ impl ContextPickerCompletionProvider {
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn completion_for_symbol(
|
||||||
|
symbol: Symbol,
|
||||||
|
excerpt_id: ExcerptId,
|
||||||
|
source_range: Range<Anchor>,
|
||||||
|
editor: Entity<Editor>,
|
||||||
|
context_store: Entity<ContextStore>,
|
||||||
|
workspace: Entity<Workspace>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Option<Completion> {
|
||||||
|
let path_prefix = workspace
|
||||||
|
.read(cx)
|
||||||
|
.project()
|
||||||
|
.read(cx)
|
||||||
|
.worktree_for_id(symbol.path.worktree_id, cx)?
|
||||||
|
.read(cx)
|
||||||
|
.root_name();
|
||||||
|
|
||||||
|
let (file_name, _) = super::file_context_picker::extract_file_name_and_directory(
|
||||||
|
&symbol.path.path,
|
||||||
|
path_prefix,
|
||||||
|
);
|
||||||
|
|
||||||
|
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
|
||||||
|
let mut label = CodeLabel::plain(symbol.name.clone(), None);
|
||||||
|
label.push_str(" ", None);
|
||||||
|
label.push_str(&file_name, comment_id);
|
||||||
|
|
||||||
|
let new_text = format!("@symbol {}:{}", file_name, symbol.name);
|
||||||
|
let new_text_len = new_text.len();
|
||||||
|
Some(Completion {
|
||||||
|
old_range: source_range.clone(),
|
||||||
|
new_text,
|
||||||
|
label,
|
||||||
|
documentation: None,
|
||||||
|
source: project::CompletionSource::Custom,
|
||||||
|
icon_path: Some(IconName::Code.path().into()),
|
||||||
|
confirm: Some(confirm_completion_callback(
|
||||||
|
IconName::Code.path().into(),
|
||||||
|
symbol.name.clone().into(),
|
||||||
|
excerpt_id,
|
||||||
|
source_range.start,
|
||||||
|
new_text_len,
|
||||||
|
editor.clone(),
|
||||||
|
move |cx| {
|
||||||
|
let symbol = symbol.clone();
|
||||||
|
let context_store = context_store.clone();
|
||||||
|
let workspace = workspace.clone();
|
||||||
|
super::symbol_context_picker::add_symbol(
|
||||||
|
symbol.clone(),
|
||||||
|
false,
|
||||||
|
workspace.clone(),
|
||||||
|
context_store.downgrade(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CompletionProvider for ContextPickerCompletionProvider {
|
impl CompletionProvider for ContextPickerCompletionProvider {
|
||||||
|
@ -350,14 +410,10 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||||
cx.spawn(async move |_, cx| {
|
cx.spawn(async move |_, cx| {
|
||||||
let mut completions = Vec::new();
|
let mut completions = Vec::new();
|
||||||
|
|
||||||
let MentionCompletion {
|
let MentionCompletion { mode, argument, .. } = state;
|
||||||
mode: category,
|
|
||||||
argument,
|
|
||||||
..
|
|
||||||
} = state;
|
|
||||||
|
|
||||||
let query = argument.unwrap_or_else(|| "".to_string());
|
let query = argument.unwrap_or_else(|| "".to_string());
|
||||||
match category {
|
match mode {
|
||||||
Some(ContextPickerMode::File) => {
|
Some(ContextPickerMode::File) => {
|
||||||
let path_matches = cx
|
let path_matches = cx
|
||||||
.update(|cx| {
|
.update(|cx| {
|
||||||
|
@ -392,6 +448,35 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some(ContextPickerMode::Symbol) => {
|
||||||
|
if let Some(editor) = editor.upgrade() {
|
||||||
|
let symbol_matches = cx
|
||||||
|
.update(|cx| {
|
||||||
|
super::symbol_context_picker::search_symbols(
|
||||||
|
query,
|
||||||
|
Arc::new(AtomicBool::default()),
|
||||||
|
&workspace,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.await?;
|
||||||
|
cx.update(|cx| {
|
||||||
|
completions.extend(symbol_matches.into_iter().filter_map(
|
||||||
|
|(_, symbol)| {
|
||||||
|
Self::completion_for_symbol(
|
||||||
|
symbol,
|
||||||
|
excerpt_id,
|
||||||
|
source_range.clone(),
|
||||||
|
editor.clone(),
|
||||||
|
context_store.clone(),
|
||||||
|
workspace.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
));
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some(ContextPickerMode::Fetch) => {
|
Some(ContextPickerMode::Fetch) => {
|
||||||
if let Some(editor) = editor.upgrade() {
|
if let Some(editor) = editor.upgrade() {
|
||||||
if !query.is_empty() {
|
if !query.is_empty() {
|
||||||
|
@ -792,6 +877,7 @@ mod tests {
|
||||||
"five.txt dir/b/",
|
"five.txt dir/b/",
|
||||||
"four.txt dir/a/",
|
"four.txt dir/a/",
|
||||||
"Files & Directories",
|
"Files & Directories",
|
||||||
|
"Symbols",
|
||||||
"Fetch"
|
"Fetch"
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
438
crates/assistant2/src/context_picker/symbol_context_picker.rs
Normal file
438
crates/assistant2/src/context_picker/symbol_context_picker.rs
Normal file
|
@ -0,0 +1,438 @@
|
||||||
|
use std::cmp::Reverse;
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::{Context as _, Result};
|
||||||
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
|
use gpui::{
|
||||||
|
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
|
||||||
|
};
|
||||||
|
use ordered_float::OrderedFloat;
|
||||||
|
use picker::{Picker, PickerDelegate};
|
||||||
|
use project::{DocumentSymbol, Symbol};
|
||||||
|
use text::OffsetRangeExt;
|
||||||
|
use ui::{prelude::*, ListItem};
|
||||||
|
use util::ResultExt as _;
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||||
|
use crate::context_store::ContextStore;
|
||||||
|
|
||||||
|
pub struct SymbolContextPicker {
|
||||||
|
picker: Entity<Picker<SymbolContextPickerDelegate>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymbolContextPicker {
|
||||||
|
pub fn new(
|
||||||
|
context_picker: WeakEntity<ContextPicker>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
context_store: WeakEntity<ContextStore>,
|
||||||
|
confirm_behavior: ConfirmBehavior,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let delegate = SymbolContextPickerDelegate::new(
|
||||||
|
context_picker,
|
||||||
|
workspace,
|
||||||
|
context_store,
|
||||||
|
confirm_behavior,
|
||||||
|
);
|
||||||
|
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||||
|
|
||||||
|
Self { picker }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Focusable for SymbolContextPicker {
|
||||||
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||||
|
self.picker.focus_handle(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for SymbolContextPicker {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
self.picker.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SymbolContextPickerDelegate {
|
||||||
|
context_picker: WeakEntity<ContextPicker>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
context_store: WeakEntity<ContextStore>,
|
||||||
|
confirm_behavior: ConfirmBehavior,
|
||||||
|
matches: Vec<SymbolEntry>,
|
||||||
|
selected_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymbolContextPickerDelegate {
|
||||||
|
pub fn new(
|
||||||
|
context_picker: WeakEntity<ContextPicker>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
context_store: WeakEntity<ContextStore>,
|
||||||
|
confirm_behavior: ConfirmBehavior,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
context_picker,
|
||||||
|
workspace,
|
||||||
|
context_store,
|
||||||
|
confirm_behavior,
|
||||||
|
matches: Vec::new(),
|
||||||
|
selected_index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickerDelegate for SymbolContextPickerDelegate {
|
||||||
|
type ListItem = ListItem;
|
||||||
|
|
||||||
|
fn match_count(&self) -> usize {
|
||||||
|
self.matches.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_index(&self) -> usize {
|
||||||
|
self.selected_index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(
|
||||||
|
&mut self,
|
||||||
|
ix: usize,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut Context<Picker<Self>>,
|
||||||
|
) {
|
||||||
|
self.selected_index = ix;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||||
|
"Search symbols…".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_matches(
|
||||||
|
&mut self,
|
||||||
|
query: String,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Picker<Self>>,
|
||||||
|
) -> Task<()> {
|
||||||
|
let Some(workspace) = self.workspace.upgrade() else {
|
||||||
|
return Task::ready(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let search_task = search_symbols(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
||||||
|
let context_store = self.context_store.clone();
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let symbols = search_task
|
||||||
|
.await
|
||||||
|
.context("Failed to load symbols")
|
||||||
|
.log_err()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let symbol_entries = context_store
|
||||||
|
.read_with(cx, |context_store, cx| {
|
||||||
|
compute_symbol_entries(symbols, context_store, cx)
|
||||||
|
})
|
||||||
|
.log_err()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
this.update(cx, |this, _cx| {
|
||||||
|
this.delegate.matches = symbol_entries;
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||||
|
let Some(mat) = self.matches.get(self.selected_index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(workspace) = self.workspace.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let confirm_behavior = self.confirm_behavior;
|
||||||
|
let add_symbol_task = add_symbol(
|
||||||
|
mat.symbol.clone(),
|
||||||
|
true,
|
||||||
|
workspace,
|
||||||
|
self.context_store.clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
let selected_index = self.selected_index;
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let included = add_symbol_task.await?;
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
if let Some(mat) = this.delegate.matches.get_mut(selected_index) {
|
||||||
|
mat.is_included = included;
|
||||||
|
}
|
||||||
|
match confirm_behavior {
|
||||||
|
ConfirmBehavior::KeepOpen => {}
|
||||||
|
ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||||
|
self.context_picker
|
||||||
|
.update(cx, |_, cx| {
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_match(
|
||||||
|
&self,
|
||||||
|
ix: usize,
|
||||||
|
selected: bool,
|
||||||
|
_window: &mut Window,
|
||||||
|
_: &mut Context<Picker<Self>>,
|
||||||
|
) -> Option<Self::ListItem> {
|
||||||
|
let mat = &self.matches[ix];
|
||||||
|
|
||||||
|
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
|
||||||
|
render_symbol_context_entry(
|
||||||
|
ElementId::NamedInteger("symbol-ctx-picker".into(), ix),
|
||||||
|
mat,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct SymbolEntry {
|
||||||
|
pub symbol: Symbol,
|
||||||
|
pub is_included: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn add_symbol(
|
||||||
|
symbol: Symbol,
|
||||||
|
remove_if_exists: bool,
|
||||||
|
workspace: Entity<Workspace>,
|
||||||
|
context_store: WeakEntity<ContextStore>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Task<Result<bool>> {
|
||||||
|
let project = workspace.read(cx).project().clone();
|
||||||
|
let open_buffer_task = project.update(cx, |project, cx| {
|
||||||
|
project.open_buffer(symbol.path.clone(), cx)
|
||||||
|
});
|
||||||
|
cx.spawn(async move |cx| {
|
||||||
|
let buffer = open_buffer_task.await?;
|
||||||
|
let document_symbols = project
|
||||||
|
.update(cx, |project, cx| project.document_symbols(&buffer, cx))?
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Try to find a matching document symbol. Document symbols include
|
||||||
|
// not only the symbol itself (e.g. function name), but they also
|
||||||
|
// include the context that they contain (e.g. function body).
|
||||||
|
let (name, range, enclosing_range) = if let Some(DocumentSymbol {
|
||||||
|
name,
|
||||||
|
range,
|
||||||
|
selection_range,
|
||||||
|
..
|
||||||
|
}) =
|
||||||
|
find_matching_symbol(&symbol, document_symbols.as_slice())
|
||||||
|
{
|
||||||
|
(name, selection_range, range)
|
||||||
|
} else {
|
||||||
|
// If we do not find a matching document symbol, fall back to
|
||||||
|
// just the symbol itself
|
||||||
|
(symbol.name, symbol.range.clone(), symbol.range)
|
||||||
|
};
|
||||||
|
|
||||||
|
let (range, enclosing_range) = buffer.read_with(cx, |buffer, _| {
|
||||||
|
(
|
||||||
|
buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
|
||||||
|
buffer.anchor_after(enclosing_range.start)
|
||||||
|
..buffer.anchor_before(enclosing_range.end),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
context_store
|
||||||
|
.update(cx, move |context_store, cx| {
|
||||||
|
context_store.add_symbol(
|
||||||
|
buffer,
|
||||||
|
name.into(),
|
||||||
|
range,
|
||||||
|
enclosing_range,
|
||||||
|
remove_if_exists,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_matching_symbol(symbol: &Symbol, candidates: &[DocumentSymbol]) -> Option<DocumentSymbol> {
|
||||||
|
let mut candidates = candidates.iter();
|
||||||
|
let mut candidate = candidates.next()?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if candidate.range.start > symbol.range.end {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if candidate.range.end < symbol.range.start {
|
||||||
|
candidate = candidates.next()?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if candidate.selection_range == symbol.range {
|
||||||
|
return Some(candidate.clone());
|
||||||
|
}
|
||||||
|
if candidate.range.start <= symbol.range.start && symbol.range.end <= candidate.range.end {
|
||||||
|
candidates = candidate.children.iter();
|
||||||
|
candidate = candidates.next()?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn search_symbols(
|
||||||
|
query: String,
|
||||||
|
cancellation_flag: Arc<AtomicBool>,
|
||||||
|
workspace: &Entity<Workspace>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Task<Result<Vec<(StringMatch, Symbol)>>> {
|
||||||
|
let symbols_task = workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace
|
||||||
|
.project()
|
||||||
|
.update(cx, |project, cx| project.symbols(&query, cx))
|
||||||
|
});
|
||||||
|
let project = workspace.read(cx).project().clone();
|
||||||
|
cx.spawn(async move |cx| {
|
||||||
|
let symbols = symbols_task.await?;
|
||||||
|
let (visible_match_candidates, external_match_candidates): (Vec<_>, Vec<_>) = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
symbols
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(id, symbol)| StringMatchCandidate::new(id, &symbol.label.filter_text()))
|
||||||
|
.partition(|candidate| {
|
||||||
|
project
|
||||||
|
.entry_for_path(&symbols[candidate.id].path, cx)
|
||||||
|
.map_or(false, |e| !e.is_ignored)
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
const MAX_MATCHES: usize = 100;
|
||||||
|
let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
|
||||||
|
&visible_match_candidates,
|
||||||
|
&query,
|
||||||
|
false,
|
||||||
|
MAX_MATCHES,
|
||||||
|
&cancellation_flag,
|
||||||
|
cx.background_executor().clone(),
|
||||||
|
));
|
||||||
|
let mut external_matches = cx.background_executor().block(fuzzy::match_strings(
|
||||||
|
&external_match_candidates,
|
||||||
|
&query,
|
||||||
|
false,
|
||||||
|
MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
|
||||||
|
&cancellation_flag,
|
||||||
|
cx.background_executor().clone(),
|
||||||
|
));
|
||||||
|
let sort_key_for_match = |mat: &StringMatch| {
|
||||||
|
let symbol = &symbols[mat.candidate_id];
|
||||||
|
(Reverse(OrderedFloat(mat.score)), symbol.label.filter_text())
|
||||||
|
};
|
||||||
|
|
||||||
|
visible_matches.sort_unstable_by_key(sort_key_for_match);
|
||||||
|
external_matches.sort_unstable_by_key(sort_key_for_match);
|
||||||
|
let mut matches = visible_matches;
|
||||||
|
matches.append(&mut external_matches);
|
||||||
|
|
||||||
|
Ok(matches
|
||||||
|
.into_iter()
|
||||||
|
.map(|mut mat| {
|
||||||
|
let symbol = symbols[mat.candidate_id].clone();
|
||||||
|
let filter_start = symbol.label.filter_range.start;
|
||||||
|
for position in &mut mat.positions {
|
||||||
|
*position += filter_start;
|
||||||
|
}
|
||||||
|
(mat, symbol)
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_symbol_entries(
|
||||||
|
symbols: Vec<(StringMatch, Symbol)>,
|
||||||
|
context_store: &ContextStore,
|
||||||
|
cx: &App,
|
||||||
|
) -> Vec<SymbolEntry> {
|
||||||
|
let mut symbol_entries = Vec::with_capacity(symbols.len());
|
||||||
|
for (_, symbol) in symbols {
|
||||||
|
let symbols_for_path = context_store.included_symbols_by_path().get(&symbol.path);
|
||||||
|
let is_included = if let Some(symbols_for_path) = symbols_for_path {
|
||||||
|
let mut is_included = false;
|
||||||
|
for included_symbol_id in symbols_for_path {
|
||||||
|
if included_symbol_id.name.as_ref() == symbol.name.as_str() {
|
||||||
|
if let Some(buffer) = context_store.buffer_for_symbol(included_symbol_id) {
|
||||||
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
|
let included_symbol_range =
|
||||||
|
included_symbol_id.range.to_point_utf16(&snapshot);
|
||||||
|
|
||||||
|
if included_symbol_range.start == symbol.range.start.0
|
||||||
|
&& included_symbol_range.end == symbol.range.end.0
|
||||||
|
{
|
||||||
|
is_included = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is_included
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
symbol_entries.push(SymbolEntry {
|
||||||
|
symbol,
|
||||||
|
is_included,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
symbol_entries
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_symbol_context_entry(id: ElementId, entry: &SymbolEntry) -> Stateful<Div> {
|
||||||
|
let path = entry
|
||||||
|
.symbol
|
||||||
|
.path
|
||||||
|
.path
|
||||||
|
.file_name()
|
||||||
|
.map(|s| s.to_string_lossy())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let symbol_location = format!("{} L{}", path, entry.symbol.range.start.0.row + 1);
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.id(id)
|
||||||
|
.gap_1p5()
|
||||||
|
.w_full()
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::Code)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(Label::new(&entry.symbol.name))
|
||||||
|
.child(
|
||||||
|
Label::new(symbol_location)
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.when(entry.is_included, |el| {
|
||||||
|
el.child(
|
||||||
|
h_flex()
|
||||||
|
.w_full()
|
||||||
|
.justify_end()
|
||||||
|
.gap_0p5()
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::Check)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Success),
|
||||||
|
)
|
||||||
|
.child(Label::new("Added").size(LabelSize::Small)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::ops::Range;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
@ -6,15 +7,15 @@ use collections::{BTreeMap, HashMap, HashSet};
|
||||||
use futures::{self, future, Future, FutureExt};
|
use futures::{self, future, Future, FutureExt};
|
||||||
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, SharedString, Task, WeakEntity};
|
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, SharedString, Task, WeakEntity};
|
||||||
use language::Buffer;
|
use language::Buffer;
|
||||||
use project::{ProjectPath, Worktree};
|
use project::{ProjectItem, ProjectPath, Worktree};
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
use text::BufferId;
|
use text::{Anchor, BufferId, OffsetRangeExt};
|
||||||
use util::maybe;
|
use util::maybe;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
use crate::context::{
|
use crate::context::{
|
||||||
AssistantContext, ContextBuffer, ContextId, ContextSnapshot, DirectoryContext,
|
AssistantContext, ContextBuffer, ContextId, ContextSnapshot, ContextSymbol, ContextSymbolId,
|
||||||
FetchedUrlContext, FileContext, ThreadContext,
|
DirectoryContext, FetchedUrlContext, FileContext, SymbolContext, ThreadContext,
|
||||||
};
|
};
|
||||||
use crate::context_strip::SuggestedContext;
|
use crate::context_strip::SuggestedContext;
|
||||||
use crate::thread::{Thread, ThreadId};
|
use crate::thread::{Thread, ThreadId};
|
||||||
|
@ -26,6 +27,9 @@ pub struct ContextStore {
|
||||||
next_context_id: ContextId,
|
next_context_id: ContextId,
|
||||||
files: BTreeMap<BufferId, ContextId>,
|
files: BTreeMap<BufferId, ContextId>,
|
||||||
directories: HashMap<PathBuf, ContextId>,
|
directories: HashMap<PathBuf, ContextId>,
|
||||||
|
symbols: HashMap<ContextSymbolId, ContextId>,
|
||||||
|
symbol_buffers: HashMap<ContextSymbolId, Entity<Buffer>>,
|
||||||
|
symbols_by_path: HashMap<ProjectPath, Vec<ContextSymbolId>>,
|
||||||
threads: HashMap<ThreadId, ContextId>,
|
threads: HashMap<ThreadId, ContextId>,
|
||||||
fetched_urls: HashMap<String, ContextId>,
|
fetched_urls: HashMap<String, ContextId>,
|
||||||
}
|
}
|
||||||
|
@ -38,6 +42,9 @@ impl ContextStore {
|
||||||
next_context_id: ContextId(0),
|
next_context_id: ContextId(0),
|
||||||
files: BTreeMap::default(),
|
files: BTreeMap::default(),
|
||||||
directories: HashMap::default(),
|
directories: HashMap::default(),
|
||||||
|
symbols: HashMap::default(),
|
||||||
|
symbol_buffers: HashMap::default(),
|
||||||
|
symbols_by_path: HashMap::default(),
|
||||||
threads: HashMap::default(),
|
threads: HashMap::default(),
|
||||||
fetched_urls: HashMap::default(),
|
fetched_urls: HashMap::default(),
|
||||||
}
|
}
|
||||||
|
@ -107,6 +114,7 @@ impl ContextStore {
|
||||||
project_path.path.clone(),
|
project_path.path.clone(),
|
||||||
buffer_entity,
|
buffer_entity,
|
||||||
buffer,
|
buffer,
|
||||||
|
None,
|
||||||
cx.to_async(),
|
cx.to_async(),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
@ -136,6 +144,7 @@ impl ContextStore {
|
||||||
file.path().clone(),
|
file.path().clone(),
|
||||||
buffer_entity,
|
buffer_entity,
|
||||||
buffer,
|
buffer,
|
||||||
|
None,
|
||||||
cx.to_async(),
|
cx.to_async(),
|
||||||
))
|
))
|
||||||
})??;
|
})??;
|
||||||
|
@ -222,6 +231,7 @@ impl ContextStore {
|
||||||
path,
|
path,
|
||||||
buffer_entity,
|
buffer_entity,
|
||||||
buffer,
|
buffer,
|
||||||
|
None,
|
||||||
cx.to_async(),
|
cx.to_async(),
|
||||||
);
|
);
|
||||||
buffer_infos.push(buffer_info);
|
buffer_infos.push(buffer_info);
|
||||||
|
@ -262,6 +272,84 @@ impl ContextStore {
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_symbol(
|
||||||
|
&mut self,
|
||||||
|
buffer: Entity<Buffer>,
|
||||||
|
symbol_name: SharedString,
|
||||||
|
symbol_range: Range<Anchor>,
|
||||||
|
symbol_enclosing_range: Range<Anchor>,
|
||||||
|
remove_if_exists: bool,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Task<Result<bool>> {
|
||||||
|
let buffer_ref = buffer.read(cx);
|
||||||
|
let Some(file) = buffer_ref.file() else {
|
||||||
|
return Task::ready(Err(anyhow!("Buffer has no path.")));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(project_path) = buffer_ref.project_path(cx) else {
|
||||||
|
return Task::ready(Err(anyhow!("Buffer has no project path.")));
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(symbols_for_path) = self.symbols_by_path.get(&project_path) {
|
||||||
|
let mut matching_symbol_id = None;
|
||||||
|
for symbol in symbols_for_path {
|
||||||
|
if &symbol.name == &symbol_name {
|
||||||
|
let snapshot = buffer_ref.snapshot();
|
||||||
|
if symbol.range.to_offset(&snapshot) == symbol_range.to_offset(&snapshot) {
|
||||||
|
matching_symbol_id = self.symbols.get(symbol).cloned();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(id) = matching_symbol_id {
|
||||||
|
if remove_if_exists {
|
||||||
|
self.remove_context(id);
|
||||||
|
}
|
||||||
|
return Task::ready(Ok(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (buffer_info, collect_content_task) = collect_buffer_info_and_text(
|
||||||
|
file.path().clone(),
|
||||||
|
buffer,
|
||||||
|
buffer_ref,
|
||||||
|
Some(symbol_enclosing_range.clone()),
|
||||||
|
cx.to_async(),
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
let content = collect_content_task.await;
|
||||||
|
|
||||||
|
this.update(cx, |this, _cx| {
|
||||||
|
this.insert_symbol(make_context_symbol(
|
||||||
|
buffer_info,
|
||||||
|
project_path,
|
||||||
|
symbol_name,
|
||||||
|
symbol_range,
|
||||||
|
symbol_enclosing_range,
|
||||||
|
content,
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
anyhow::Ok(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_symbol(&mut self, context_symbol: ContextSymbol) {
|
||||||
|
let id = self.next_context_id.post_inc();
|
||||||
|
self.symbols.insert(context_symbol.id.clone(), id);
|
||||||
|
self.symbols_by_path
|
||||||
|
.entry(context_symbol.id.path.clone())
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(context_symbol.id.clone());
|
||||||
|
self.symbol_buffers
|
||||||
|
.insert(context_symbol.id.clone(), context_symbol.buffer.clone());
|
||||||
|
self.context.push(AssistantContext::Symbol(SymbolContext {
|
||||||
|
id,
|
||||||
|
context_symbol,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_thread(
|
pub fn add_thread(
|
||||||
&mut self,
|
&mut self,
|
||||||
thread: Entity<Thread>,
|
thread: Entity<Thread>,
|
||||||
|
@ -340,6 +428,19 @@ impl ContextStore {
|
||||||
AssistantContext::Directory(_) => {
|
AssistantContext::Directory(_) => {
|
||||||
self.directories.retain(|_, context_id| *context_id != id);
|
self.directories.retain(|_, context_id| *context_id != id);
|
||||||
}
|
}
|
||||||
|
AssistantContext::Symbol(symbol) => {
|
||||||
|
if let Some(symbols_in_path) =
|
||||||
|
self.symbols_by_path.get_mut(&symbol.context_symbol.id.path)
|
||||||
|
{
|
||||||
|
symbols_in_path.retain(|s| {
|
||||||
|
self.symbols
|
||||||
|
.get(s)
|
||||||
|
.map_or(false, |context_id| *context_id != id)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.symbol_buffers.remove(&symbol.context_symbol.id);
|
||||||
|
self.symbols.retain(|_, context_id| *context_id != id);
|
||||||
|
}
|
||||||
AssistantContext::FetchedUrl(_) => {
|
AssistantContext::FetchedUrl(_) => {
|
||||||
self.fetched_urls.retain(|_, context_id| *context_id != id);
|
self.fetched_urls.retain(|_, context_id| *context_id != id);
|
||||||
}
|
}
|
||||||
|
@ -403,6 +504,18 @@ impl ContextStore {
|
||||||
self.directories.get(path).copied()
|
self.directories.get(path).copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn included_symbol(&self, symbol_id: &ContextSymbolId) -> Option<ContextId> {
|
||||||
|
self.symbols.get(symbol_id).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn included_symbols_by_path(&self) -> &HashMap<ProjectPath, Vec<ContextSymbolId>> {
|
||||||
|
&self.symbols_by_path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buffer_for_symbol(&self, symbol_id: &ContextSymbolId) -> Option<Entity<Buffer>> {
|
||||||
|
self.symbol_buffers.get(symbol_id).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn includes_thread(&self, thread_id: &ThreadId) -> Option<ContextId> {
|
pub fn includes_thread(&self, thread_id: &ThreadId) -> Option<ContextId> {
|
||||||
self.threads.get(thread_id).copied()
|
self.threads.get(thread_id).copied()
|
||||||
}
|
}
|
||||||
|
@ -431,6 +544,7 @@ impl ContextStore {
|
||||||
buffer_path_log_err(buffer).map(|p| p.to_path_buf())
|
buffer_path_log_err(buffer).map(|p| p.to_path_buf())
|
||||||
}
|
}
|
||||||
AssistantContext::Directory(_)
|
AssistantContext::Directory(_)
|
||||||
|
| AssistantContext::Symbol(_)
|
||||||
| AssistantContext::FetchedUrl(_)
|
| AssistantContext::FetchedUrl(_)
|
||||||
| AssistantContext::Thread(_) => None,
|
| AssistantContext::Thread(_) => None,
|
||||||
})
|
})
|
||||||
|
@ -463,10 +577,28 @@ fn make_context_buffer(info: BufferInfo, text: SharedString) -> ContextBuffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn make_context_symbol(
|
||||||
|
info: BufferInfo,
|
||||||
|
path: ProjectPath,
|
||||||
|
name: SharedString,
|
||||||
|
range: Range<Anchor>,
|
||||||
|
enclosing_range: Range<Anchor>,
|
||||||
|
text: SharedString,
|
||||||
|
) -> ContextSymbol {
|
||||||
|
ContextSymbol {
|
||||||
|
id: ContextSymbolId { name, range, path },
|
||||||
|
buffer_version: info.version,
|
||||||
|
enclosing_range,
|
||||||
|
buffer: info.buffer_entity,
|
||||||
|
text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn collect_buffer_info_and_text(
|
fn collect_buffer_info_and_text(
|
||||||
path: Arc<Path>,
|
path: Arc<Path>,
|
||||||
buffer_entity: Entity<Buffer>,
|
buffer_entity: Entity<Buffer>,
|
||||||
buffer: &Buffer,
|
buffer: &Buffer,
|
||||||
|
range: Option<Range<Anchor>>,
|
||||||
cx: AsyncApp,
|
cx: AsyncApp,
|
||||||
) -> (BufferInfo, Task<SharedString>) {
|
) -> (BufferInfo, Task<SharedString>) {
|
||||||
let buffer_info = BufferInfo {
|
let buffer_info = BufferInfo {
|
||||||
|
@ -475,7 +607,11 @@ fn collect_buffer_info_and_text(
|
||||||
version: buffer.version(),
|
version: buffer.version(),
|
||||||
};
|
};
|
||||||
// Important to collect version at the same time as content so that staleness logic is correct.
|
// Important to collect version at the same time as content so that staleness logic is correct.
|
||||||
let content = buffer.as_rope().clone();
|
let content = if let Some(range) = range {
|
||||||
|
buffer.text_for_range(range).collect::<Rope>()
|
||||||
|
} else {
|
||||||
|
buffer.as_rope().clone()
|
||||||
|
};
|
||||||
let text_task = cx.background_spawn(async move { to_fenced_codeblock(&path, content) });
|
let text_task = cx.background_spawn(async move { to_fenced_codeblock(&path, content) });
|
||||||
(buffer_info, text_task)
|
(buffer_info, text_task)
|
||||||
}
|
}
|
||||||
|
@ -577,6 +713,14 @@ pub fn refresh_context_store_text(
|
||||||
return refresh_directory_text(context_store, directory_context, cx);
|
return refresh_directory_text(context_store, directory_context, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AssistantContext::Symbol(symbol_context) => {
|
||||||
|
if changed_buffers.is_empty()
|
||||||
|
|| changed_buffers.contains(&symbol_context.context_symbol.buffer)
|
||||||
|
{
|
||||||
|
let context_store = context_store.clone();
|
||||||
|
return refresh_symbol_text(context_store, symbol_context, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
AssistantContext::Thread(thread_context) => {
|
AssistantContext::Thread(thread_context) => {
|
||||||
if changed_buffers.is_empty() {
|
if changed_buffers.is_empty() {
|
||||||
let context_store = context_store.clone();
|
let context_store = context_store.clone();
|
||||||
|
@ -660,6 +804,28 @@ fn refresh_directory_text(
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn refresh_symbol_text(
|
||||||
|
context_store: Entity<ContextStore>,
|
||||||
|
symbol_context: &SymbolContext,
|
||||||
|
cx: &App,
|
||||||
|
) -> Option<Task<()>> {
|
||||||
|
let id = symbol_context.id;
|
||||||
|
let task = refresh_context_symbol(&symbol_context.context_symbol, cx);
|
||||||
|
if let Some(task) = task {
|
||||||
|
Some(cx.spawn(async move |cx| {
|
||||||
|
let context_symbol = task.await;
|
||||||
|
context_store
|
||||||
|
.update(cx, |context_store, _| {
|
||||||
|
let new_symbol_context = SymbolContext { id, context_symbol };
|
||||||
|
context_store.replace_context(AssistantContext::Symbol(new_symbol_context));
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn refresh_thread_text(
|
fn refresh_thread_text(
|
||||||
context_store: Entity<ContextStore>,
|
context_store: Entity<ContextStore>,
|
||||||
thread_context: &ThreadContext,
|
thread_context: &ThreadContext,
|
||||||
|
@ -692,6 +858,7 @@ fn refresh_context_buffer(
|
||||||
path,
|
path,
|
||||||
context_buffer.buffer.clone(),
|
context_buffer.buffer.clone(),
|
||||||
buffer,
|
buffer,
|
||||||
|
None,
|
||||||
cx.to_async(),
|
cx.to_async(),
|
||||||
);
|
);
|
||||||
Some(text_task.map(move |text| make_context_buffer(buffer_info, text)))
|
Some(text_task.map(move |text| make_context_buffer(buffer_info, text)))
|
||||||
|
@ -699,3 +866,36 @@ fn refresh_context_buffer(
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn refresh_context_symbol(
|
||||||
|
context_symbol: &ContextSymbol,
|
||||||
|
cx: &App,
|
||||||
|
) -> Option<impl Future<Output = ContextSymbol>> {
|
||||||
|
let buffer = context_symbol.buffer.read(cx);
|
||||||
|
let path = buffer_path_log_err(buffer)?;
|
||||||
|
let project_path = buffer.project_path(cx)?;
|
||||||
|
if buffer.version.changed_since(&context_symbol.buffer_version) {
|
||||||
|
let (buffer_info, text_task) = collect_buffer_info_and_text(
|
||||||
|
path,
|
||||||
|
context_symbol.buffer.clone(),
|
||||||
|
buffer,
|
||||||
|
Some(context_symbol.enclosing_range.clone()),
|
||||||
|
cx.to_async(),
|
||||||
|
);
|
||||||
|
let name = context_symbol.id.name.clone();
|
||||||
|
let range = context_symbol.id.range.clone();
|
||||||
|
let enclosing_range = context_symbol.enclosing_range.clone();
|
||||||
|
Some(text_task.map(move |text| {
|
||||||
|
make_context_symbol(
|
||||||
|
buffer_info,
|
||||||
|
project_path,
|
||||||
|
name,
|
||||||
|
range,
|
||||||
|
enclosing_range,
|
||||||
|
text,
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -190,9 +190,10 @@ impl RenderOnce for ContextPill {
|
||||||
.child(
|
.child(
|
||||||
Label::new(match kind {
|
Label::new(match kind {
|
||||||
ContextKind::File => "Active Tab",
|
ContextKind::File => "Active Tab",
|
||||||
ContextKind::Thread | ContextKind::Directory | ContextKind::FetchedUrl => {
|
ContextKind::Thread
|
||||||
"Active"
|
| ContextKind::Directory
|
||||||
}
|
| ContextKind::FetchedUrl
|
||||||
|
| ContextKind::Symbol => "Active",
|
||||||
})
|
})
|
||||||
.size(LabelSize::XSmall)
|
.size(LabelSize::XSmall)
|
||||||
.color(Color::Muted),
|
.color(Color::Muted),
|
||||||
|
|
|
@ -15494,9 +15494,9 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn project_path(&self, cx: &mut Context<Self>) -> Option<ProjectPath> {
|
pub fn project_path(&self, cx: &App) -> Option<ProjectPath> {
|
||||||
if let Some(buffer) = self.buffer.read(cx).as_singleton() {
|
if let Some(buffer) = self.buffer.read(cx).as_singleton() {
|
||||||
buffer.read_with(cx, |buffer, cx| buffer.project_path(cx))
|
buffer.read(cx).project_path(cx)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue