This commit is contained in:
Antonio Scandurra 2023-12-05 19:27:15 +01:00
parent e534c5fdcd
commit d86da04584
5 changed files with 307 additions and 327 deletions

View File

@ -104,7 +104,7 @@ pub struct OpenAIResponseStreamEvent {
pub async fn stream_completion(
credential: ProviderCredential,
executor: Arc<BackgroundExecutor>,
executor: BackgroundExecutor,
request: Box<dyn CompletionRequest>,
) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
let api_key = match credential {
@ -197,11 +197,11 @@ pub async fn stream_completion(
pub struct OpenAICompletionProvider {
model: OpenAILanguageModel,
credential: Arc<RwLock<ProviderCredential>>,
executor: Arc<BackgroundExecutor>,
executor: BackgroundExecutor,
}
impl OpenAICompletionProvider {
pub fn new(model_name: &str, executor: Arc<BackgroundExecutor>) -> Self {
pub fn new(model_name: &str, executor: BackgroundExecutor) -> Self {
let model = OpenAILanguageModel::load(model_name);
let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials));
Self {

View File

@ -27,8 +27,8 @@ use editor::{
use fs::Fs;
use futures::StreamExt;
use gpui::{
actions, div, point, uniform_list, Action, AnyElement, AppContext, AsyncAppContext,
ClipboardItem, Div, Element, Entity, EventEmitter, FocusHandle, Focusable, FocusableView,
actions, div, point, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext,
ClipboardItem, Context, Div, EventEmitter, FocusHandle, Focusable, FocusableView,
HighlightStyle, InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels,
PromptLevel, Render, StatefulInteractiveElement, Styled, Subscription, Task,
UniformListScrollHandle, View, ViewContext, VisualContext, WeakModel, WeakView, WindowContext,
@ -51,7 +51,7 @@ use std::{
};
use ui::{
h_stack, v_stack, Button, ButtonCommon, ButtonLike, Clickable, Color, Icon, IconButton,
IconElement, Label, Selectable, StyledExt, Tooltip,
IconElement, Label, Selectable, Tooltip,
};
use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
use uuid::Uuid;
@ -76,49 +76,18 @@ actions!(
pub fn init(cx: &mut AppContext) {
AssistantSettings::register(cx);
cx.add_action(
|this: &mut AssistantPanel,
_: &workspace::NewFile,
cx: &mut ViewContext<AssistantPanel>| {
this.new_conversation(cx);
cx.observe_new_views(
|workspace: &mut Workspace, cx: &mut ViewContext<Workspace>| {
workspace
.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<AssistantPanel>(cx);
})
.register_action(AssistantPanel::inline_assist)
.register_action(AssistantPanel::cancel_last_inline_assist)
.register_action(ConversationEditor::quote_selection);
},
);
cx.add_action(ConversationEditor::assist);
cx.capture_action(ConversationEditor::cancel_last_assist);
cx.capture_action(ConversationEditor::save);
cx.add_action(ConversationEditor::quote_selection);
cx.capture_action(ConversationEditor::copy);
cx.add_action(ConversationEditor::split);
cx.capture_action(ConversationEditor::cycle_message_role);
cx.add_action(AssistantPanel::save_credentials);
cx.add_action(AssistantPanel::reset_credentials);
cx.add_action(AssistantPanel::toggle_zoom);
cx.add_action(AssistantPanel::deploy);
cx.add_action(AssistantPanel::select_next_match);
cx.add_action(AssistantPanel::select_prev_match);
cx.add_action(AssistantPanel::handle_editor_cancel);
cx.add_action(
|workspace: &mut Workspace, _: &ToggleFocus, cx: &mut ViewContext<Workspace>| {
workspace.toggle_panel_focus::<AssistantPanel>(cx);
},
);
cx.add_action(AssistantPanel::inline_assist);
cx.add_action(AssistantPanel::cancel_last_inline_assist);
cx.add_action(InlineAssistant::confirm);
cx.add_action(InlineAssistant::cancel);
cx.add_action(InlineAssistant::toggle_include_conversation);
cx.add_action(InlineAssistant::toggle_retrieve_context);
cx.add_action(InlineAssistant::move_up);
cx.add_action(InlineAssistant::move_down);
}
#[derive(Debug)]
pub enum AssistantPanelEvent {
ZoomIn,
ZoomOut,
Focus,
Close,
DockPositionChanged,
)
.detach();
}
pub struct AssistantPanel {
@ -131,7 +100,6 @@ pub struct AssistantPanel {
saved_conversations: Vec<SavedConversationMetadata>,
saved_conversations_scroll_handle: UniformListScrollHandle,
zoomed: bool,
// todo!("remove has_focus field")
focus_handle: FocusHandle,
toolbar: View<Toolbar>,
completion_provider: Arc<dyn CompletionProvider>,
@ -152,9 +120,12 @@ pub struct AssistantPanel {
impl AssistantPanel {
const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20;
pub fn load(workspace: WeakView<Workspace>, cx: AsyncAppContext) -> Task<Result<View<Self>>> {
pub fn load(
workspace: WeakView<Workspace>,
mut cx: AsyncWindowContext,
) -> Task<Result<View<Self>>> {
cx.spawn(|mut cx| async move {
let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
let saved_conversations = SavedConversationMetadata::list(fs.clone())
.await
.log_err()
@ -163,7 +134,7 @@ impl AssistantPanel {
// TODO: deserialize state.
let workspace_handle = workspace.clone();
workspace.update(&mut cx, |workspace, cx| {
cx.add_view::<Self, _>(|cx| {
cx.build_view::<Self>(|cx| {
const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100);
let _watch_saved_conversations = cx.spawn(move |this, mut cx| async move {
let mut events = fs
@ -184,10 +155,10 @@ impl AssistantPanel {
anyhow::Ok(())
});
let toolbar = cx.add_view(|cx| {
let toolbar = cx.build_view(|cx| {
let mut toolbar = Toolbar::new();
toolbar.set_can_navigate(false, cx);
toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx);
toolbar.add_item(cx.build_view(|cx| BufferSearchBar::new(cx)), cx);
toolbar
});
@ -199,8 +170,8 @@ impl AssistantPanel {
));
let focus_handle = cx.focus_handle();
cx.on_focus_in(Self::focus_in).detach();
cx.on_focus_out(Self::focus_out).detach();
cx.on_focus_in(&focus_handle, Self::focus_in).detach();
cx.on_focus_out(&focus_handle, Self::focus_out).detach();
let mut this = Self {
workspace: workspace_handle,
@ -231,11 +202,11 @@ impl AssistantPanel {
let mut old_dock_position = this.position(cx);
this.subscriptions =
vec![cx.observe_global::<SettingsStore, _>(move |this, cx| {
vec![cx.observe_global::<SettingsStore>(move |this, cx| {
let new_dock_position = this.position(cx);
if new_dock_position != old_dock_position {
old_dock_position = new_dock_position;
cx.emit(AssistantPanelEvent::DockPositionChanged);
cx.emit(PanelEvent::ChangePosition);
}
cx.notify();
})];
@ -343,7 +314,7 @@ impl AssistantPanel {
// Retrieve Credentials Authenticates the Provider
provider.retrieve_credentials(cx);
let codegen = cx.add_model(|cx| {
let codegen = cx.build_model(|cx| {
Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
});
@ -353,14 +324,14 @@ impl AssistantPanel {
let previously_indexed = semantic_index
.update(&mut cx, |index, cx| {
index.project_previously_indexed(&project, cx)
})
})?
.await
.unwrap_or(false);
if previously_indexed {
let _ = semantic_index
.update(&mut cx, |index, cx| {
index.index_project(project.clone(), cx)
})
})?
.await;
}
anyhow::Ok(())
@ -369,7 +340,7 @@ impl AssistantPanel {
}
let measurements = Rc::new(Cell::new(BlockMeasurements::default()));
let inline_assistant = cx.add_view(|cx| {
let inline_assistant = cx.build_view(|cx| {
let assistant = InlineAssistant::new(
inline_assist_id,
measurements.clone(),
@ -382,7 +353,7 @@ impl AssistantPanel {
self.semantic_index.clone(),
project.clone(),
);
cx.focus_self();
assistant.focus_handle.focus(cx);
assistant
});
let block_id = editor.update(cx, |editor, cx| {
@ -429,8 +400,13 @@ impl AssistantPanel {
move |_, editor, event, cx| {
if let Some(inline_assistant) = inline_assistant.upgrade() {
if let EditorEvent::SelectionsChanged { local } = event {
if *local && inline_assistant.read(cx).has_focus {
cx.focus(&editor);
if *local
&& inline_assistant
.read(cx)
.focus_handle
.contains_focused(cx)
{
cx.focus_view(&editor);
}
}
}
@ -555,7 +531,7 @@ impl AssistantPanel {
}
}
cx.propagate_action();
cx.propagate();
}
fn finish_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext<Self>) {
@ -709,13 +685,17 @@ impl AssistantPanel {
let snippets = cx.spawn(|_, mut cx| async move {
let mut snippets = Vec::new();
for result in search_results.await {
snippets.push(PromptCodeSnippet::new(result.buffer, result.range, &mut cx));
snippets.push(PromptCodeSnippet::new(
result.buffer,
result.range,
&mut cx,
)?);
}
snippets
anyhow::Ok(snippets)
});
snippets
} else {
Task::ready(Vec::new())
Task::ready(Ok(Vec::new()))
};
let mut model = AssistantSettings::get_global(cx)
@ -724,7 +704,7 @@ impl AssistantPanel {
let model_name = model.full_name();
let prompt = cx.background_executor().spawn(async move {
let snippets = snippets.await;
let snippets = snippets.await?;
let language_name = language_name.as_deref();
generate_content_prompt(
@ -799,7 +779,7 @@ impl AssistantPanel {
} else {
editor.highlight_background::<PendingInlineAssist>(
background_ranges,
|theme| theme.assistant.inline.pending_edit_background,
|theme| gpui::red(), // todo!("use the appropriate color")
cx,
);
}
@ -820,7 +800,7 @@ impl AssistantPanel {
}
fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> View<ConversationEditor> {
let editor = cx.add_view(|cx| {
let editor = cx.build_view(|cx| {
ConversationEditor::new(
self.completion_provider.clone(),
self.languages.clone(),
@ -854,8 +834,8 @@ impl AssistantPanel {
self.toolbar.update(cx, |toolbar, cx| {
toolbar.set_active_item(Some(&editor), cx);
});
if self.has_focus(cx) {
cx.focus(&editor);
if self.focus_handle.contains_focused(cx) {
cx.focus_view(&editor);
}
} else {
self.toolbar.update(cx, |toolbar, cx| {
@ -891,31 +871,31 @@ impl AssistantPanel {
self.completion_provider.save_credentials(cx, credential);
self.api_key_editor.take();
cx.focus_self();
self.focus_handle.focus(cx);
cx.notify();
}
} else {
cx.propagate_action();
cx.propagate();
}
}
fn reset_credentials(&mut self, _: &ResetKey, cx: &mut ViewContext<Self>) {
self.completion_provider.delete_credentials(cx);
self.api_key_editor = Some(build_api_key_editor(cx));
cx.focus_self();
self.focus_handle.focus(cx);
cx.notify();
}
fn toggle_zoom(&mut self, _: &workspace::ToggleZoom, cx: &mut ViewContext<Self>) {
if self.zoomed {
cx.emit(AssistantPanelEvent::ZoomOut)
cx.emit(PanelEvent::ZoomOut)
} else {
cx.emit(AssistantPanelEvent::ZoomIn)
cx.emit(PanelEvent::ZoomIn)
}
}
fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext<Self>) {
let mut propagate_action = true;
let mut propagate = true;
if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |search_bar, cx| {
if search_bar.show(cx) {
@ -924,12 +904,12 @@ impl AssistantPanel {
search_bar.select_query(cx);
cx.focus_self();
}
propagate_action = false
propagate = false
}
});
}
if propagate_action {
cx.propagate_action();
if propagate {
cx.propagate();
}
}
@ -942,7 +922,7 @@ impl AssistantPanel {
return;
}
}
cx.propagate_action();
cx.propagate();
}
fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext<Self>) {
@ -976,9 +956,9 @@ impl AssistantPanel {
fn render_editor_tools(&self, cx: &mut ViewContext<Self>) -> Vec<AnyElement> {
if self.active_editor().is_some() {
vec![
Self::render_split_button(cx).into_any(),
Self::render_quote_button(cx).into_any(),
Self::render_assist_button(cx).into_any(),
Self::render_split_button(cx).into_any_element(),
Self::render_quote_button(cx).into_any_element(),
Self::render_assist_button(cx).into_any_element(),
]
} else {
Default::default()
@ -1028,16 +1008,13 @@ impl AssistantPanel {
}
fn render_zoom_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let zoomed = self.zoomed;
IconButton::new("zoom_button", Icon::Menu)
.on_click(cx.listener(|this, _event, cx| {
this.toggle_zoom(&ToggleZoom, cx);
}))
.tooltip(|cx| {
Tooltip::for_action(
if self.zoomed { "Zoom Out" } else { "Zoom In" },
&ToggleZoom,
cx,
)
.tooltip(move |cx| {
Tooltip::for_action(if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx)
})
}
@ -1072,16 +1049,16 @@ impl AssistantPanel {
cx.spawn(|this, mut cx| async move {
let saved_conversation = fs.load(&path).await?;
let saved_conversation = serde_json::from_str(&saved_conversation)?;
let conversation = cx.add_model(|cx| {
let conversation = cx.build_model(|cx| {
Conversation::deserialize(saved_conversation, path.clone(), languages, cx)
});
})?;
this.update(&mut cx, |this, cx| {
// If, by the time we've loaded the conversation, the user has already opened
// the same conversation, we don't want to open it again.
if let Some(ix) = this.editor_index_for_path(&path, cx) {
this.set_active_editor_index(Some(ix), cx);
} else {
let editor = cx.add_view(|cx| {
let editor = cx.build_view(|cx| {
ConversationEditor::for_conversation(conversation, fs, workspace, cx)
});
this.add_conversation(editor, cx);
@ -1120,6 +1097,7 @@ impl Render for AssistantPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
if let Some(api_key_editor) = self.api_key_editor.clone() {
v_stack()
.on_action(cx.listener(AssistantPanel::save_credentials))
.track_focus(&self.focus_handle)
.child(Label::new(
"To use the assistant panel or inline assistant, you need to add your OpenAI api key.",
@ -1159,6 +1137,15 @@ impl Render for AssistantPanel {
}
v_stack()
.on_action(cx.listener(|this, _: &workspace::NewFile, cx| {
this.new_conversation(cx);
}))
.on_action(cx.listener(AssistantPanel::reset_credentials))
.on_action(cx.listener(AssistantPanel::toggle_zoom))
.on_action(cx.listener(AssistantPanel::deploy))
.on_action(cx.listener(AssistantPanel::select_next_match))
.on_action(cx.listener(AssistantPanel::select_prev_match))
.on_action(cx.listener(AssistantPanel::handle_editor_cancel))
.track_focus(&self.focus_handle)
.child(header)
.children(if self.toolbar.read(cx).hidden() {
@ -1175,7 +1162,7 @@ impl Render for AssistantPanel {
self.saved_conversations.len(),
|this, range, cx| {
range
.map(|ix| this.render_saved_conversation(ix, cx).into_any())
.map(|ix| this.render_saved_conversation(ix, cx))
.collect()
},
)
@ -1311,17 +1298,14 @@ impl Conversation {
completion_provider: Arc<dyn CompletionProvider>,
) -> Self {
let markdown = language_registry.language_for_name("Markdown");
let buffer = cx.add_model(|cx| {
let mut buffer = Buffer::new(0, cx.model_id() as u64, "");
let buffer = cx.build_model(|cx| {
let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), "");
buffer.set_language_registry(language_registry);
cx.spawn_weak(|buffer, mut cx| async move {
cx.spawn(|buffer, mut cx| async move {
let markdown = markdown.await?;
let buffer = buffer
.upgrade(&cx)
.ok_or_else(|| anyhow!("buffer was dropped"))?;
buffer.update(&mut cx, |buffer: &mut Buffer, cx| {
buffer.set_language(Some(markdown), cx)
});
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
@ -1409,8 +1393,8 @@ impl Conversation {
let markdown = language_registry.language_for_name("Markdown");
let mut message_anchors = Vec::new();
let mut next_message_id = MessageId(0);
let buffer = cx.add_model(|cx| {
let mut buffer = Buffer::new(0, cx.model_id() as u64, saved_conversation.text);
let buffer = cx.build_model(|cx| {
let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), saved_conversation.text);
for message in saved_conversation.messages {
message_anchors.push(MessageAnchor {
id: message.id,
@ -1419,14 +1403,11 @@ impl Conversation {
next_message_id = cmp::max(next_message_id, MessageId(message.id.0 + 1));
}
buffer.set_language_registry(language_registry);
cx.spawn_weak(|buffer, mut cx| async move {
cx.spawn(|buffer, mut cx| async move {
let markdown = markdown.await?;
let buffer = buffer
.upgrade(&cx)
.ok_or_else(|| anyhow!("buffer was dropped"))?;
buffer.update(&mut cx, |buffer: &mut Buffer, cx| {
buffer.set_language(Some(markdown), cx)
});
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
@ -1497,26 +1478,24 @@ impl Conversation {
})
.collect::<Vec<_>>();
let model = self.model.clone();
self.pending_token_count = cx.spawn_weak(|this, mut cx| {
self.pending_token_count = cx.spawn(|this, mut cx| {
async move {
cx.background_executor()
.timer(Duration::from_millis(200))
.await;
let token_count = cx
.background()
.background_executor()
.spawn(async move {
tiktoken_rs::num_tokens_from_messages(&model.full_name(), &messages)
})
.await?;
this.upgrade(&cx)
.ok_or_else(|| anyhow!("conversation was dropped"))?
.update(&mut cx, |this, cx| {
this.max_token_count =
tiktoken_rs::model::get_context_size(&this.model.full_name());
this.token_count = Some(token_count);
cx.notify()
});
this.update(&mut cx, |this, cx| {
this.max_token_count =
tiktoken_rs::model::get_context_size(&this.model.full_name());
this.token_count = Some(token_count);
cx.notify()
})?;
anyhow::Ok(())
}
.log_err()
@ -1603,7 +1582,7 @@ impl Conversation {
.unwrap();
user_messages.push(user_message);
let task = cx.spawn_weak({
let task = cx.spawn({
|this, mut cx| async move {
let assistant_message_id = assistant_message.id;
let stream_completion = async {
@ -1612,59 +1591,55 @@ impl Conversation {
while let Some(message) = messages.next().await {
let text = message?;
this.upgrade(&cx)
.ok_or_else(|| anyhow!("conversation was dropped"))?
.update(&mut cx, |this, cx| {
let message_ix = this
.message_anchors
this.update(&mut cx, |this, cx| {
let message_ix = this
.message_anchors
.iter()
.position(|message| message.id == assistant_message_id)?;
this.buffer.update(cx, |buffer, cx| {
let offset = this.message_anchors[message_ix + 1..]
.iter()
.position(|message| message.id == assistant_message_id)?;
this.buffer.update(cx, |buffer, cx| {
let offset = this.message_anchors[message_ix + 1..]
.iter()
.find(|message| message.start.is_valid(buffer))
.map_or(buffer.len(), |message| {
message.start.to_offset(buffer).saturating_sub(1)
});
buffer.edit([(offset..offset, text)], None, cx);
});
cx.emit(ConversationEvent::StreamedCompletion);
Some(())
.find(|message| message.start.is_valid(buffer))
.map_or(buffer.len(), |message| {
message.start.to_offset(buffer).saturating_sub(1)
});
buffer.edit([(offset..offset, text)], None, cx);
});
cx.emit(ConversationEvent::StreamedCompletion);
Some(())
})?;
smol::future::yield_now().await;
}
this.upgrade(&cx)
.ok_or_else(|| anyhow!("conversation was dropped"))?
.update(&mut cx, |this, cx| {
this.pending_completions
.retain(|completion| completion.id != this.completion_count);
this.summarize(cx);
});
this.update(&mut cx, |this, cx| {
this.pending_completions
.retain(|completion| completion.id != this.completion_count);
this.summarize(cx);
})?;
anyhow::Ok(())
};
let result = stream_completion.await;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
if let Some(metadata) =
this.messages_metadata.get_mut(&assistant_message.id)
{
match result {
Ok(_) => {
metadata.status = MessageStatus::Done;
}
Err(error) => {
metadata.status =
MessageStatus::Error(error.to_string().trim().into());
}
this.update(&mut cx, |this, cx| {
if let Some(metadata) =
this.messages_metadata.get_mut(&assistant_message.id)
{
match result {
Ok(_) => {
metadata.status = MessageStatus::Done;
}
Err(error) => {
metadata.status =
MessageStatus::Error(error.to_string().trim().into());
}
cx.notify();
}
});
}
cx.notify();
}
})
.ok();
}
});
@ -1999,10 +1974,10 @@ impl Conversation {
None
};
(path, summary)
});
})?;
if let Some(summary) = summary {
let conversation = this.read_with(&cx, |this, cx| this.serialize(cx));
let conversation = this.read_with(&cx, |this, cx| this.serialize(cx))?;
let path = if let Some(old_path) = old_path {
old_path
} else {
@ -2026,7 +2001,7 @@ impl Conversation {
fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?;
fs.atomic_write(path.clone(), serde_json::to_string(&conversation).unwrap())
.await?;
this.update(&mut cx, |this, _| this.path = Some(path));
this.update(&mut cx, |this, _| this.path = Some(path))?;
}
Ok(())
@ -2069,7 +2044,7 @@ impl ConversationEditor {
cx: &mut ViewContext<Self>,
) -> Self {
let conversation =
cx.add_model(|cx| Conversation::new(language_registry, cx, completion_provider));
cx.build_model(|cx| Conversation::new(language_registry, cx, completion_provider));
Self::for_conversation(conversation, fs, workspace, cx)
}
@ -2079,7 +2054,7 @@ impl ConversationEditor {
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let editor = cx.add_view(|cx| {
let editor = cx.build_view(|cx| {
let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_show_gutter(false, cx);
@ -2093,7 +2068,7 @@ impl ConversationEditor {
cx.observe(&conversation, |_, _, cx| cx.notify()),
cx.subscribe(&conversation, Self::handle_conversation_event),
cx.subscribe(&editor, Self::handle_editor_event),
cx.on_focus(&focus_handle, |this, _, cx| cx.focus(&this.editor)),
cx.on_focus(&focus_handle, |this, cx| cx.focus_view(&this.editor)),
];
let mut this = Self {
@ -2155,7 +2130,7 @@ impl ConversationEditor {
.conversation
.update(cx, |conversation, _| conversation.cancel_last_assist())
{
cx.propagate_action();
cx.propagate();
}
}
@ -2247,8 +2222,8 @@ impl ConversationEditor {
.anchor()
.scroll_position(&snapshot.display_snapshot);
let scroll_bottom = scroll_position.y() + editor.visible_line_count().unwrap_or(0.);
if (scroll_position.y()..scroll_bottom).contains(&cursor_row) {
let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
Some(ScrollPosition {
cursor,
offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
@ -2286,7 +2261,7 @@ impl ConversationEditor {
})
.on_click({
let conversation = conversation.clone();
move |_, _, cx| {
move |_, cx| {
conversation.update(cx, |conversation, cx| {
conversation.cycle_message_roles(
HashSet::from_iter(Some(message_id)),
@ -2302,18 +2277,16 @@ impl ConversationEditor {
.border_color(gpui::red())
.child(sender)
.child(Label::new(message.sent_at.format("%I:%M%P").to_string()))
.with_children(
if let MessageStatus::Error(error) = &message.status {
Some(
div()
.id("error")
.tooltip(|cx| Tooltip::text(error, cx))
.child(IconElement::new(Icon::XCircle)),
)
} else {
None
},
)
.children(if let MessageStatus::Error(error) = &message.status {
Some(
div()
.id("error")
.tooltip(|cx| Tooltip::text(error, cx))
.child(IconElement::new(Icon::XCircle)),
)
} else {
None
})
.into_any_element()
}
}),
@ -2342,36 +2315,35 @@ impl ConversationEditor {
return;
};
let text = editor.read_with(cx, |editor, cx| {
let range = editor.selections.newest::<usize>(cx).range();
let buffer = editor.buffer().read(cx).snapshot(cx);
let start_language = buffer.language_at(range.start);
let end_language = buffer.language_at(range.end);
let language_name = if start_language == end_language {
start_language.map(|language| language.name())
} else {
None
};
let language_name = language_name.as_deref().unwrap_or("").to_lowercase();
let editor = editor.read(cx);
let range = editor.selections.newest::<usize>(cx).range();
let buffer = editor.buffer().read(cx).snapshot(cx);
let start_language = buffer.language_at(range.start);
let end_language = buffer.language_at(range.end);
let language_name = if start_language == end_language {
start_language.map(|language| language.name())
} else {
None
};
let language_name = language_name.as_deref().unwrap_or("").to_lowercase();
let selected_text = buffer.text_for_range(range).collect::<String>();
if selected_text.is_empty() {
None
let selected_text = buffer.text_for_range(range).collect::<String>();
let text = if selected_text.is_empty() {
None
} else {
Some(if language_name == "markdown" {
selected_text
.lines()
.map(|line| format!("> {}", line))
.collect::<Vec<_>>()
.join("\n")
} else {
Some(if language_name == "markdown" {
selected_text
.lines()
.map(|line| format!("> {}", line))
.collect::<Vec<_>>()
.join("\n")
} else {
format!("```{language_name}\n{selected_text}\n```")
})
}
});
format!("```{language_name}\n{selected_text}\n```")
})
};
// Activate the panel
if !panel.read(cx).has_focus(cx) {
if !panel.focus_handle(cx).contains_focused(cx) {
workspace.toggle_panel_focus::<AssistantPanel>(cx);
}
@ -2415,13 +2387,12 @@ impl ConversationEditor {
}
if spanned_messages > 1 {
cx.platform()
.write_to_clipboard(ClipboardItem::new(copied_text));
cx.write_to_clipboard(ClipboardItem::new(copied_text));
return;
}
}
cx.propagate_action();
cx.propagate();
}
fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
@ -2492,15 +2463,30 @@ impl Render for ConversationEditor {
type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div().relative().child(self.editor.clone()).child(
h_stack()
.absolute()
.gap_1()
.top_3()
.right_5()
.child(self.render_current_model(cx))
.children(self.render_remaining_tokens(cx)),
)
div()
.relative()
.capture_action(cx.listener(ConversationEditor::cancel_last_assist))
.capture_action(cx.listener(ConversationEditor::save))
.capture_action(cx.listener(ConversationEditor::copy))
.capture_action(cx.listener(ConversationEditor::cycle_message_role))
.on_action(cx.listener(ConversationEditor::assist))
.on_action(cx.listener(ConversationEditor::split))
.child(self.editor.clone())
.child(
h_stack()
.absolute()
.gap_1()
.top_3()
.right_5()
.child(self.render_current_model(cx))
.children(self.render_remaining_tokens(cx)),
)
}
}
impl FocusableView for ConversationEditor {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
@ -2577,30 +2563,40 @@ impl Render for InlineAssistant {
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let measurements = self.measurements.get();
h_stack()
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::toggle_include_conversation))
.on_action(cx.listener(Self::toggle_retrieve_context))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
.child(
h_stack()
.justify_center()
.w(measurements.gutter_width)
.child(
IconButton::new("include_conversation", Icon::Ai)
.action(ToggleIncludeConversation)
.action(Box::new(ToggleIncludeConversation))
.selected(self.include_conversation)
.tooltip(Tooltip::for_action(
"Include Conversation",
&ToggleIncludeConversation,
cx,
)),
.tooltip(|cx| {
Tooltip::for_action(
"Include Conversation",
&ToggleIncludeConversation,
cx,
)
}),
)
.children(if SemanticIndex::enabled(cx) {
Some(
IconButton::new("retrieve_context", Icon::MagnifyingGlass)
.action(ToggleRetrieveContext)
.action(Box::new(ToggleRetrieveContext))
.selected(self.retrieve_context)
.tooltip(Tooltip::for_action(
"Retrieve Context",
&ToggleRetrieveContext,
cx,
)),
.tooltip(|cx| {
Tooltip::for_action(
"Retrieve Context",
&ToggleRetrieveContext,
cx,
)
}),
)
} else {
None
@ -2629,6 +2625,12 @@ impl Render for InlineAssistant {
}
}
impl FocusableView for InlineAssistant {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl InlineAssistant {
fn new(
id: usize,
@ -2656,10 +2658,7 @@ impl InlineAssistant {
let mut subscriptions = vec![
cx.observe(&codegen, Self::handle_codegen_changed),
cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events),
cx.on_focus(
&focus_handle,
cx.listener(|this, _, cx| cx.focus(&this.prompt_editor)),
),
cx.on_focus(&focus_handle, |this, cx| cx.focus_view(&this.prompt_editor)),
];
if let Some(semantic_index) = semantic_index.clone() {
@ -2939,42 +2938,17 @@ impl InlineAssistant {
div()
.id("update")
.tooltip(|cx| Tooltip::text(status_text, cx))
.child(IconElement::new(Icon::Update).color(color))
.child(IconElement::new(Icon::Update).color(Color::Info))
.into_any_element()
Svg::new("icons/update.svg")
.with_color(theme.assistant.inline.context_status.in_progress_icon.color)
.constrained()
.with_width(theme.assistant.inline.context_status.in_progress_icon.width)
.contained()
.with_style(theme.assistant.inline.context_status.in_progress_icon.container)
.with_tooltip::<ContextStatusIcon>(
self.id,
status_text,
None,
theme.tooltip.clone(),
cx,
)
.aligned()
.into_any(),
)
}
SemanticIndexStatus::Indexed {} => Some(
Svg::new("icons/check.svg")
.with_color(theme.assistant.inline.context_status.complete_icon.color)
.constrained()
.with_width(theme.assistant.inline.context_status.complete_icon.width)
.contained()
.with_style(theme.assistant.inline.context_status.complete_icon.container)
.with_tooltip::<ContextStatusIcon>(
self.id,
"Index up to date",
None,
theme.tooltip.clone(),
cx,
)
.aligned()
.into_any(),
div()
.id("check")
.tooltip(|cx| Tooltip::text("Index up to date", cx))
.child(IconElement::new(Icon::Check).color(Color::Success))
.into_any_element()
),
}
} else {
@ -3083,7 +3057,8 @@ mod tests {
let registry = Arc::new(LanguageRegistry::test());
let completion_provider = Arc::new(FakeCompletionProvider::new());
let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider));
let conversation =
cx.build_model(|cx| Conversation::new(registry, cx, completion_provider));
let buffer = conversation.read(cx).buffer.clone();
let message_1 = conversation.read(cx).message_anchors[0].clone();
@ -3213,7 +3188,8 @@ mod tests {
let registry = Arc::new(LanguageRegistry::test());
let completion_provider = Arc::new(FakeCompletionProvider::new());
let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider));
let conversation =
cx.build_model(|cx| Conversation::new(registry, cx, completion_provider));
let buffer = conversation.read(cx).buffer.clone();
let message_1 = conversation.read(cx).message_anchors[0].clone();
@ -3310,7 +3286,8 @@ mod tests {
init(cx);
let registry = Arc::new(LanguageRegistry::test());
let completion_provider = Arc::new(FakeCompletionProvider::new());
let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider));
let conversation =
cx.build_model(|cx| Conversation::new(registry, cx, completion_provider));
let buffer = conversation.read(cx).buffer.clone();
let message_1 = conversation.read(cx).message_anchors[0].clone();
@ -3394,7 +3371,7 @@ mod tests {
let registry = Arc::new(LanguageRegistry::test());
let completion_provider = Arc::new(FakeCompletionProvider::new());
let conversation =
cx.add_model(|cx| Conversation::new(registry.clone(), cx, completion_provider));
cx.build_model(|cx| Conversation::new(registry.clone(), cx, completion_provider));
let buffer = conversation.read(cx).buffer.clone();
let message_0 = conversation.read(cx).message_anchors[0].id;
let message_1 = conversation.update(cx, |conversation, cx| {
@ -3427,7 +3404,7 @@ mod tests {
]
);
let deserialized_conversation = cx.add_model(|cx| {
let deserialized_conversation = cx.build_model(|cx| {
Conversation::deserialize(
conversation.read(cx).serialize(cx),
Default::default(),

View File

@ -1961,14 +1961,14 @@ impl Editor {
cx.notify();
}
// pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext<Self>) {
// self.cursor_shape = cursor_shape;
// cx.notify();
// }
pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext<Self>) {
self.cursor_shape = cursor_shape;
cx.notify();
}
// pub fn set_collapse_matches(&mut self, collapse_matches: bool) {
// self.collapse_matches = collapse_matches;
// }
pub fn set_collapse_matches(&mut self, collapse_matches: bool) {
self.collapse_matches = collapse_matches;
}
pub fn range_for_match<T: std::marker::Copy>(&self, range: &Range<T>) -> Range<T> {
if self.collapse_matches {
@ -1977,56 +1977,47 @@ impl Editor {
range.clone()
}
// pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext<Self>) {
// if self.display_map.read(cx).clip_at_line_ends != clip {
// self.display_map
// .update(cx, |map, _| map.clip_at_line_ends = clip);
// }
// }
pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext<Self>) {
if self.display_map.read(cx).clip_at_line_ends != clip {
self.display_map
.update(cx, |map, _| map.clip_at_line_ends = clip);
}
}
// pub fn set_keymap_context_layer<Tag: 'static>(
// &mut self,
// context: KeymapContext,
// cx: &mut ViewContext<Self>,
// ) {
// self.keymap_context_layers
// .insert(TypeId::of::<Tag>(), context);
// cx.notify();
// }
pub fn set_keymap_context_layer<Tag: 'static>(
&mut self,
context: KeyContext,
cx: &mut ViewContext<Self>,
) {
self.keymap_context_layers
.insert(TypeId::of::<Tag>(), context);
cx.notify();
}
// pub fn remove_keymap_context_layer<Tag: 'static>(&mut self, cx: &mut ViewContext<Self>) {
// self.keymap_context_layers.remove(&TypeId::of::<Tag>());
// cx.notify();
// }
pub fn remove_keymap_context_layer<Tag: 'static>(&mut self, cx: &mut ViewContext<Self>) {
self.keymap_context_layers.remove(&TypeId::of::<Tag>());
cx.notify();
}
// pub fn set_input_enabled(&mut self, input_enabled: bool) {
// self.input_enabled = input_enabled;
// }
pub fn set_input_enabled(&mut self, input_enabled: bool) {
self.input_enabled = input_enabled;
}
// pub fn set_autoindent(&mut self, autoindent: bool) {
// if autoindent {
// self.autoindent_mode = Some(AutoindentMode::EachLine);
// } else {
// self.autoindent_mode = None;
// }
// }
pub fn set_autoindent(&mut self, autoindent: bool) {
if autoindent {
self.autoindent_mode = Some(AutoindentMode::EachLine);
} else {
self.autoindent_mode = None;
}
}
// pub fn read_only(&self) -> bool {
// self.read_only
// }
pub fn read_only(&self) -> bool {
self.read_only
}
// pub fn set_read_only(&mut self, read_only: bool) {
// self.read_only = read_only;
// }
// pub fn set_field_editor_style(
// &mut self,
// style: Option<Arc<GetFieldEditorTheme>>,
// cx: &mut ViewContext<Self>,
// ) {
// self.get_field_editor_theme = style;
// cx.notify();
// }
pub fn set_read_only(&mut self, read_only: bool) {
self.read_only = read_only;
}
fn selections_did_change(
&mut self,

View File

@ -2816,3 +2816,9 @@ impl From<(&'static str, EntityId)> for ElementId {
ElementId::NamedInteger(name.into(), id.as_u64() as usize)
}
}
impl From<(&'static str, usize)> for ElementId {
fn from((name, id): (&'static str, usize)) -> Self {
ElementId::NamedInteger(name.into(), id)
}
}

View File

@ -44,12 +44,18 @@ impl<'a, T: ?Sized> From<&'a T> for ArcCow<'a, T> {
}
}
impl<T> From<Arc<T>> for ArcCow<'_, T> {
impl<T: ?Sized> From<Arc<T>> for ArcCow<'_, T> {
fn from(s: Arc<T>) -> Self {
Self::Owned(s)
}
}
impl<T: ?Sized> From<&'_ Arc<T>> for ArcCow<'_, T> {
fn from(s: &'_ Arc<T>) -> Self {
Self::Owned(s.clone())
}
}
impl From<String> for ArcCow<'_, str> {
fn from(value: String) -> Self {
Self::Owned(value.into())