@@ -6,12 +6,9 @@ use anyhow::{anyhow, Result};
use chrono::{DateTime, Local};
use collections::{HashMap, HashSet};
use editor::{
- display_map::ToDisplayPoint,
- scroll::{
- autoscroll::{Autoscroll, AutoscrollStrategy},
- ScrollAnchor,
- },
- Anchor, DisplayPoint, Editor, ExcerptId, ExcerptRange, MultiBuffer,
+ display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint},
+ scroll::autoscroll::{Autoscroll, AutoscrollStrategy},
+ Anchor, Editor, ToOffset as _,
};
use fs::Fs;
use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
@@ -19,17 +16,20 @@ use gpui::{
actions,
elements::*,
executor::Background,
- geometry::vector::vec2f,
+ geometry::vector::{vec2f, Vector2F},
platform::{CursorStyle, MouseButton},
Action, AppContext, AsyncAppContext, ClipboardItem, Entity, ModelContext, ModelHandle,
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
};
use isahc::{http::StatusCode, Request, RequestExt};
-use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
+use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _};
use serde::Deserialize;
use settings::SettingsStore;
-use std::{borrow::Cow, cell::RefCell, cmp, fmt::Write, io, rc::Rc, sync::Arc, time::Duration};
-use util::{post_inc, truncate_and_trailoff, ResultExt, TryFutureExt};
+use std::{
+ borrow::Cow, cell::RefCell, cmp, fmt::Write, io, iter, ops::Range, rc::Rc, sync::Arc,
+ time::Duration,
+};
+use util::{channel::ReleaseChannel, post_inc, truncate_and_trailoff, ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel},
item::Item,
@@ -44,6 +44,12 @@ actions!(
);
pub fn init(cx: &mut AppContext) {
+ if *util::channel::RELEASE_CHANNEL == ReleaseChannel::Stable {
+ cx.update_default_global::<collections::CommandPaletteFilter, _, _>(move |filter, _cx| {
+ filter.filtered_namespaces.insert("assistant");
+ });
+ }
+
settings::register::<AssistantSettings>(cx);
cx.add_action(
|workspace: &mut Workspace, _: &NewContext, cx: &mut ViewContext<Workspace>| {
@@ -60,6 +66,11 @@ pub fn init(cx: &mut AppContext) {
cx.capture_action(AssistantEditor::copy);
cx.add_action(AssistantPanel::save_api_key);
cx.add_action(AssistantPanel::reset_api_key);
+ cx.add_action(
+ |workspace: &mut Workspace, _: &ToggleFocus, cx: &mut ViewContext<Workspace>| {
+ workspace.toggle_panel_focus::<AssistantPanel>(cx);
+ },
+ );
}
pub enum AssistantPanelEvent {
@@ -387,7 +398,7 @@ impl Panel for AssistantPanel {
}
fn icon_path(&self) -> &'static str {
- "icons/speech_bubble_12.svg"
+ "icons/robot_14.svg"
}
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
@@ -420,20 +431,20 @@ impl Panel for AssistantPanel {
}
enum AssistantEvent {
- MessagesEdited { ids: Vec<ExcerptId> },
+ MessagesEdited,
SummaryChanged,
StreamedCompletion,
}
struct Assistant {
- buffer: ModelHandle<MultiBuffer>,
+ buffer: ModelHandle<Buffer>,
messages: Vec<Message>,
- messages_metadata: HashMap<ExcerptId, MessageMetadata>,
+ messages_metadata: HashMap<MessageId, MessageMetadata>,
+ next_message_id: MessageId,
summary: Option<String>,
pending_summary: Task<Option<()>>,
completion_count: usize,
pending_completions: Vec<PendingCompletion>,
- languages: Arc<LanguageRegistry>,
model: String,
token_count: Option<usize>,
max_token_count: usize,
@@ -453,15 +464,32 @@ impl Assistant {
cx: &mut ModelContext<Self>,
) -> Self {
let model = "gpt-3.5-turbo";
- let buffer = cx.add_model(|_| MultiBuffer::new(0));
+ let markdown = language_registry.language_for_name("Markdown");
+ let buffer = cx.add_model(|cx| {
+ let mut buffer = Buffer::new(0, "", cx);
+ buffer.set_language_registry(language_registry);
+ cx.spawn_weak(|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, cx| {
+ buffer.set_language(Some(markdown), cx)
+ });
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ buffer
+ });
+
let mut this = Self {
messages: Default::default(),
messages_metadata: Default::default(),
+ next_message_id: Default::default(),
summary: None,
pending_summary: Task::ready(None),
completion_count: Default::default(),
pending_completions: Default::default(),
- languages: language_registry,
token_count: None,
max_token_count: tiktoken_rs::model::get_context_size(model),
pending_token_count: Task::ready(None),
@@ -470,23 +498,34 @@ impl Assistant {
api_key,
buffer,
};
- this.insert_message_after(ExcerptId::max(), Role::User, cx);
+ let message = Message {
+ id: MessageId(post_inc(&mut this.next_message_id.0)),
+ start: language::Anchor::MIN,
+ };
+ this.messages.push(message.clone());
+ this.messages_metadata.insert(
+ message.id,
+ MessageMetadata {
+ role: Role::User,
+ sent_at: Local::now(),
+ error: None,
+ },
+ );
+
this.count_remaining_tokens(cx);
this
}
fn handle_buffer_event(
&mut self,
- _: ModelHandle<MultiBuffer>,
- event: &editor::multi_buffer::Event,
+ _: ModelHandle<Buffer>,
+ event: &language::Event,
cx: &mut ModelContext<Self>,
) {
match event {
- editor::multi_buffer::Event::ExcerptsAdded { .. }
- | editor::multi_buffer::Event::ExcerptsRemoved { .. }
- | editor::multi_buffer::Event::Edited => self.count_remaining_tokens(cx),
- editor::multi_buffer::Event::ExcerptsEdited { ids } => {
- cx.emit(AssistantEvent::MessagesEdited { ids: ids.clone() });
+ language::Event::Edited => {
+ self.count_remaining_tokens(cx);
+ cx.emit(AssistantEvent::MessagesEdited);
}
_ => {}
}
@@ -494,16 +533,16 @@ impl Assistant {
fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
let messages = self
- .messages
- .iter()
+ .open_ai_request_messages(cx)
+ .into_iter()
.filter_map(|message| {
Some(tiktoken_rs::ChatCompletionRequestMessage {
- role: match self.messages_metadata.get(&message.excerpt_id)?.role {
+ role: match message.role {
Role::User => "user".into(),
Role::Assistant => "assistant".into(),
Role::System => "system".into(),
},
- content: message.content.read(cx).text(),
+ content: message.content,
name: None,
})
})
@@ -541,45 +580,48 @@ impl Assistant {
}
fn assist(&mut self, cx: &mut ModelContext<Self>) -> Option<(Message, Message)> {
- let messages = self
- .messages
- .iter()
- .filter_map(|message| {
- Some(RequestMessage {
- role: self.messages_metadata.get(&message.excerpt_id)?.role,
- content: message.content.read(cx).text(),
- })
- })
- .collect();
let request = OpenAIRequest {
model: self.model.clone(),
- messages,
+ messages: self.open_ai_request_messages(cx),
stream: true,
};
let api_key = self.api_key.borrow().clone()?;
let stream = stream_completion(api_key, cx.background().clone(), request);
- let assistant_message = self.insert_message_after(ExcerptId::max(), Role::Assistant, cx);
- let user_message = self.insert_message_after(ExcerptId::max(), Role::User, cx);
+ let assistant_message =
+ self.insert_message_after(self.messages.last()?.id, Role::Assistant, cx)?;
+ let user_message = self.insert_message_after(assistant_message.id, Role::User, cx)?;
let task = cx.spawn_weak({
- let assistant_message = assistant_message.clone();
|this, mut cx| async move {
- let assistant_message = assistant_message;
+ let assistant_message_id = assistant_message.id;
let stream_completion = async {
let mut messages = stream.await?;
while let Some(message) = messages.next().await {
let mut message = message?;
if let Some(choice) = message.choices.pop() {
- assistant_message.content.update(&mut cx, |content, cx| {
- let text: Arc<str> = choice.delta.content?.into();
- content.edit([(content.len()..content.len(), text)], None, cx);
- Some(())
- });
this.upgrade(&cx)
.ok_or_else(|| anyhow!("assistant was dropped"))?
- .update(&mut cx, |_, cx| {
+ .update(&mut cx, |this, cx| {
+ let text: Arc<str> = choice.delta.content?.into();
+ let message_ix = this
+ .messages
+ .iter()
+ .position(|message| message.id == assistant_message_id)?;
+ this.buffer.update(cx, |buffer, cx| {
+ let offset = if message_ix + 1 == this.messages.len() {
+ buffer.len()
+ } else {
+ this.messages[message_ix + 1]
+ .start
+ .to_offset(buffer)
+ .saturating_sub(1)
+ };
+ buffer.edit([(offset..offset, text)], None, cx);
+ });
cx.emit(AssistantEvent::StreamedCompletion);
+
+ Some(())
});
}
}
@@ -599,9 +641,8 @@ impl Assistant {
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
if let Err(error) = result {
- if let Some(metadata) = this
- .messages_metadata
- .get_mut(&assistant_message.excerpt_id)
+ if let Some(metadata) =
+ this.messages_metadata.get_mut(&assistant_message.id)
{
metadata.error = Some(error.to_string().trim().into());
cx.notify();
@@ -623,124 +664,64 @@ impl Assistant {
self.pending_completions.pop().is_some()
}
- fn remove_empty_messages<'a>(
- &mut self,
- excerpts: HashSet<ExcerptId>,
- protected_offsets: HashSet<usize>,
- cx: &mut ModelContext<Self>,
- ) {
- let mut offset = 0;
- let mut excerpts_to_remove = Vec::new();
- self.messages.retain(|message| {
- let range = offset..offset + message.content.read(cx).len();
- offset = range.end + 1;
- if range.is_empty()
- && !protected_offsets.contains(&range.start)
- && excerpts.contains(&message.excerpt_id)
- {
- excerpts_to_remove.push(message.excerpt_id);
- self.messages_metadata.remove(&message.excerpt_id);
- false
- } else {
- true
- }
- });
-
- if !excerpts_to_remove.is_empty() {
- self.buffer.update(cx, |buffer, cx| {
- buffer.remove_excerpts(excerpts_to_remove, cx)
- });
- cx.notify();
- }
- }
-
- fn cycle_message_role(&mut self, excerpt_id: ExcerptId, cx: &mut ModelContext<Self>) {
- if let Some(metadata) = self.messages_metadata.get_mut(&excerpt_id) {
+ fn cycle_message_role(&mut self, id: MessageId, cx: &mut ModelContext<Self>) {
+ if let Some(metadata) = self.messages_metadata.get_mut(&id) {
metadata.role.cycle();
+ cx.emit(AssistantEvent::MessagesEdited);
cx.notify();
}
}
fn insert_message_after(
&mut self,
- excerpt_id: ExcerptId,
+ message_id: MessageId,
role: Role,
cx: &mut ModelContext<Self>,
- ) -> Message {
- let content = cx.add_model(|cx| {
- let mut buffer = Buffer::new(0, "", cx);
- let markdown = self.languages.language_for_name("Markdown");
- cx.spawn_weak(|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, cx| {
- buffer.set_language(Some(markdown), cx)
- });
- anyhow::Ok(())
- })
- .detach_and_log_err(cx);
- buffer.set_language_registry(self.languages.clone());
- buffer
- });
- let new_excerpt_id = self.buffer.update(cx, |buffer, cx| {
- buffer
- .insert_excerpts_after(
- excerpt_id,
- content.clone(),
- vec![ExcerptRange {
- context: 0..0,
- primary: None,
- }],
- cx,
- )
- .pop()
- .unwrap()
- });
-
- let ix = self
+ ) -> Option<Message> {
+ if let Some(prev_message_ix) = self
.messages
.iter()
- .position(|message| message.excerpt_id == excerpt_id)
- .map_or(self.messages.len(), |ix| ix + 1);
- let message = Message {
- excerpt_id: new_excerpt_id,
- content: content.clone(),
- };
- self.messages.insert(ix, message.clone());
- self.messages_metadata.insert(
- new_excerpt_id,
- MessageMetadata {
- role,
- sent_at: Local::now(),
- error: None,
- },
- );
- message
+ .position(|message| message.id == message_id)
+ {
+ let start = self.buffer.update(cx, |buffer, cx| {
+ let offset = self.messages[prev_message_ix + 1..]
+ .iter()
+ .find(|message| message.start.is_valid(buffer))
+ .map_or(buffer.len(), |message| message.start.to_offset(buffer) - 1);
+ buffer.edit([(offset..offset, "\n")], None, cx);
+ buffer.anchor_before(offset + 1)
+ });
+ let message = Message {
+ id: MessageId(post_inc(&mut self.next_message_id.0)),
+ start,
+ };
+ self.messages.insert(prev_message_ix + 1, message.clone());
+ self.messages_metadata.insert(
+ message.id,
+ MessageMetadata {
+ role,
+ sent_at: Local::now(),
+ error: None,
+ },
+ );
+ cx.emit(AssistantEvent::MessagesEdited);
+ Some(message)
+ } else {
+ None
+ }
}
fn summarize(&mut self, cx: &mut ModelContext<Self>) {
if self.messages.len() >= 2 && self.summary.is_none() {
let api_key = self.api_key.borrow().clone();
if let Some(api_key) = api_key {
- let messages = self
- .messages
- .iter()
- .take(2)
- .filter_map(|message| {
- Some(RequestMessage {
- role: self.messages_metadata.get(&message.excerpt_id)?.role,
- content: message.content.read(cx).text(),
- })
- })
- .chain(Some(RequestMessage {
- role: Role::User,
- content:
- "Summarize the conversation into a short title without punctuation"
- .into(),
- }))
- .collect();
+ let mut messages = self.open_ai_request_messages(cx);
+ messages.truncate(2);
+ messages.push(RequestMessage {
+ role: Role::User,
+ content: "Summarize the conversation into a short title without punctuation"
+ .into(),
+ });
let request = OpenAIRequest {
model: self.model.clone(),
messages,
@@ -770,6 +751,54 @@ impl Assistant {
}
}
}
+
+ fn open_ai_request_messages(&self, cx: &AppContext) -> Vec<RequestMessage> {
+ let buffer = self.buffer.read(cx);
+ self.messages(cx)
+ .map(|(_message, metadata, range)| RequestMessage {
+ role: metadata.role,
+ content: buffer.text_for_range(range).collect(),
+ })
+ .collect()
+ }
+
+ fn message_id_for_offset(&self, offset: usize, cx: &AppContext) -> Option<MessageId> {
+ Some(
+ self.messages(cx)
+ .find(|(_, _, range)| range.contains(&offset))
+ .map(|(message, _, _)| message)
+ .or(self.messages.last())?
+ .id,
+ )
+ }
+
+ fn messages<'a>(
+ &'a self,
+ cx: &'a AppContext,
+ ) -> impl 'a + Iterator<Item = (&Message, &MessageMetadata, Range<usize>)> {
+ let buffer = self.buffer.read(cx);
+ let mut messages = self.messages.iter().peekable();
+ iter::from_fn(move || {
+ while let Some(message) = messages.next() {
+ let metadata = self.messages_metadata.get(&message.id)?;
+ let message_start = message.start.to_offset(buffer);
+ let mut message_end = None;
+ while let Some(next_message) = messages.peek() {
+ if next_message.start.is_valid(buffer) {
+ message_end = Some(next_message.start);
+ break;
+ } else {
+ messages.next();
+ }
+ }
+ let message_end = message_end
+ .unwrap_or(language::Anchor::MAX)
+ .to_offset(buffer);
+ return Some((message, metadata, message_start..message_end));
+ }
+ None
+ })
+ }
}
struct PendingCompletion {
@@ -781,10 +810,17 @@ enum AssistantEditorEvent {
TabContentChanged,
}
+#[derive(Copy, Clone, Debug, PartialEq)]
+struct ScrollPosition {
+ offset_before_cursor: Vector2F,
+ cursor: Anchor,
+}
+
struct AssistantEditor {
assistant: ModelHandle<Assistant>,
editor: ViewHandle<Editor>,
- scroll_bottom: ScrollAnchor,
+ blocks: HashSet<BlockId>,
+ scroll_position: Option<ScrollPosition>,
_subscriptions: Vec<Subscription>,
}
@@ -796,98 +832,9 @@ impl AssistantEditor {
) -> Self {
let assistant = cx.add_model(|cx| Assistant::new(api_key, language_registry, cx));
let editor = cx.add_view(|cx| {
- let mut editor = Editor::for_multibuffer(assistant.read(cx).buffer.clone(), None, cx);
+ let mut editor = Editor::for_buffer(assistant.read(cx).buffer.clone(), None, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_show_gutter(false, cx);
- editor.set_render_excerpt_header(
- {
- let assistant = assistant.clone();
- move |_editor, params: editor::RenderExcerptHeaderParams, cx| {
- enum Sender {}
- enum ErrorTooltip {}
-
- let theme = theme::current(cx);
- let style = &theme.assistant;
- let excerpt_id = params.id;
- if let Some(metadata) = assistant
- .read(cx)
- .messages_metadata
- .get(&excerpt_id)
- .cloned()
- {
- let sender = MouseEventHandler::<Sender, _>::new(
- params.id.into(),
- cx,
- |state, _| match metadata.role {
- Role::User => {
- let style = style.user_sender.style_for(state, false);
- Label::new("You", style.text.clone())
- .contained()
- .with_style(style.container)
- }
- Role::Assistant => {
- let style = style.assistant_sender.style_for(state, false);
- Label::new("Assistant", style.text.clone())
- .contained()
- .with_style(style.container)
- }
- Role::System => {
- let style = style.system_sender.style_for(state, false);
- Label::new("System", style.text.clone())
- .contained()
- .with_style(style.container)
- }
- },
- )
- .with_cursor_style(CursorStyle::PointingHand)
- .on_down(MouseButton::Left, {
- let assistant = assistant.clone();
- move |_, _, cx| {
- assistant.update(cx, |assistant, cx| {
- assistant.cycle_message_role(excerpt_id, cx)
- })
- }
- });
-
- Flex::row()
- .with_child(sender.aligned())
- .with_child(
- Label::new(
- metadata.sent_at.format("%I:%M%P").to_string(),
- style.sent_at.text.clone(),
- )
- .contained()
- .with_style(style.sent_at.container)
- .aligned(),
- )
- .with_children(metadata.error.map(|error| {
- Svg::new("icons/circle_x_mark_12.svg")
- .with_color(style.error_icon.color)
- .constrained()
- .with_width(style.error_icon.width)
- .contained()
- .with_style(style.error_icon.container)
- .with_tooltip::<ErrorTooltip>(
- params.id.into(),
- error,
- None,
- theme.tooltip.clone(),
- cx,
- )
- .aligned()
- }))
- .aligned()
- .left()
- .contained()
- .with_style(style.header)
- .into_any()
- } else {
- Empty::new().into_any()
- }
- }
- },
- cx,
- );
editor
});
@@ -897,60 +844,48 @@ impl AssistantEditor {
cx.subscribe(&editor, Self::handle_editor_event),
];
- Self {
+ let mut this = Self {
assistant,
editor,
- scroll_bottom: ScrollAnchor {
- offset: Default::default(),
- anchor: Anchor::max(),
- },
+ blocks: Default::default(),
+ scroll_position: None,
_subscriptions,
- }
+ };
+ this.update_message_headers(cx);
+ this
}
fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
let user_message = self.assistant.update(cx, |assistant, cx| {
let editor = self.editor.read(cx);
- let newest_selection = editor.selections.newest_anchor();
- let excerpt_id = if newest_selection.head() == Anchor::min() {
- assistant
- .messages
- .first()
- .map(|message| message.excerpt_id)?
- } else if newest_selection.head() == Anchor::max() {
- assistant
- .messages
- .last()
- .map(|message| message.excerpt_id)?
- } else {
- newest_selection.head().excerpt_id()
- };
-
- let metadata = assistant.messages_metadata.get(&excerpt_id)?;
+ let newest_selection = editor
+ .selections
+ .newest_anchor()
+ .head()
+ .to_offset(&editor.buffer().read(cx).snapshot(cx));
+ let message_id = assistant.message_id_for_offset(newest_selection, cx)?;
+ let metadata = assistant.messages_metadata.get(&message_id)?;
let user_message = if metadata.role == Role::User {
let (_, user_message) = assistant.assist(cx)?;
user_message
} else {
- let user_message = assistant.insert_message_after(excerpt_id, Role::User, cx);
+ let user_message = assistant.insert_message_after(message_id, Role::User, cx)?;
user_message
};
Some(user_message)
});
if let Some(user_message) = user_message {
+ let cursor = user_message
+ .start
+ .to_offset(&self.assistant.read(cx).buffer.read(cx));
self.editor.update(cx, |editor, cx| {
- let cursor = editor
- .buffer()
- .read(cx)
- .snapshot(cx)
- .anchor_in_excerpt(user_message.excerpt_id, language::Anchor::MIN);
editor.change_selections(
Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
cx,
- |selections| selections.select_anchor_ranges([cursor..cursor]),
+ |selections| selections.select_ranges([cursor..cursor]),
);
});
- self.update_scroll_bottom(cx);
}
}
@@ -970,34 +905,22 @@ impl AssistantEditor {
cx: &mut ViewContext<Self>,
) {
match event {
- AssistantEvent::MessagesEdited { ids } => {
- let selections = self.editor.read(cx).selections.all::<usize>(cx);
- let selection_heads = selections
- .iter()
- .map(|selection| selection.head())
- .collect::<HashSet<usize>>();
- let ids = ids.iter().copied().collect::<HashSet<_>>();
- self.assistant.update(cx, |assistant, cx| {
- assistant.remove_empty_messages(ids, selection_heads, cx)
- });
- }
+ AssistantEvent::MessagesEdited => self.update_message_headers(cx),
AssistantEvent::SummaryChanged => {
cx.emit(AssistantEditorEvent::TabContentChanged);
}
AssistantEvent::StreamedCompletion => {
self.editor.update(cx, |editor, cx| {
- let snapshot = editor.snapshot(cx);
- let scroll_bottom_row = self
- .scroll_bottom
- .anchor
- .to_display_point(&snapshot.display_snapshot)
- .row();
-
- let scroll_bottom = scroll_bottom_row as f32 + self.scroll_bottom.offset.y();
- let visible_line_count = editor.visible_line_count().unwrap_or(0.);
- let scroll_top = scroll_bottom - visible_line_count;
- editor
- .set_scroll_position(vec2f(self.scroll_bottom.offset.x(), scroll_top), cx);
+ if let Some(scroll_position) = self.scroll_position {
+ let snapshot = editor.snapshot(cx);
+ let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
+ let scroll_top =
+ cursor_point.row() as f32 - scroll_position.offset_before_cursor.y();
+ editor.set_scroll_position(
+ vec2f(scroll_position.offset_before_cursor.x(), scroll_top),
+ cx,
+ );
+ }
});
}
}
@@ -1010,34 +933,145 @@ impl AssistantEditor {
cx: &mut ViewContext<Self>,
) {
match event {
- editor::Event::ScrollPositionChanged { .. } => self.update_scroll_bottom(cx),
+ editor::Event::ScrollPositionChanged { autoscroll, .. } => {
+ let cursor_scroll_position = self.cursor_scroll_position(cx);
+ if *autoscroll {
+ self.scroll_position = cursor_scroll_position;
+ } else if self.scroll_position != cursor_scroll_position {
+ self.scroll_position = None;
+ }
+ }
+ editor::Event::SelectionsChanged { .. } => {
+ self.scroll_position = self.cursor_scroll_position(cx);
+ }
_ => {}
}
}
- fn update_scroll_bottom(&mut self, cx: &mut ViewContext<Self>) {
+ fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
self.editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
+ let cursor = editor.selections.newest_anchor().head();
+ let cursor_row = cursor.to_display_point(&snapshot.display_snapshot).row() as f32;
let scroll_position = editor
.scroll_manager
.anchor()
.scroll_position(&snapshot.display_snapshot);
+
let scroll_bottom = scroll_position.y() + editor.visible_line_count().unwrap_or(0.);
- let scroll_bottom_point = cmp::min(
- DisplayPoint::new(scroll_bottom.floor() as u32, 0),
- snapshot.display_snapshot.max_point(),
- );
- let scroll_bottom_anchor = snapshot
- .buffer_snapshot
- .anchor_after(scroll_bottom_point.to_point(&snapshot.display_snapshot));
- let scroll_bottom_offset = vec2f(
- scroll_position.x(),
- scroll_bottom - scroll_bottom_point.row() as f32,
- );
- self.scroll_bottom = ScrollAnchor {
- anchor: scroll_bottom_anchor,
- offset: scroll_bottom_offset,
- };
+ if (scroll_position.y()..scroll_bottom).contains(&cursor_row) {
+ Some(ScrollPosition {
+ cursor,
+ offset_before_cursor: vec2f(
+ scroll_position.x(),
+ cursor_row - scroll_position.y(),
+ ),
+ })
+ } else {
+ None
+ }
+ })
+ }
+
+ fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
+ self.editor.update(cx, |editor, cx| {
+ let buffer = editor.buffer().read(cx).snapshot(cx);
+ let excerpt_id = *buffer.as_singleton().unwrap().0;
+ let old_blocks = std::mem::take(&mut self.blocks);
+ let new_blocks = self
+ .assistant
+ .read(cx)
+ .messages(cx)
+ .map(|(message, metadata, _)| BlockProperties {
+ position: buffer.anchor_in_excerpt(excerpt_id, message.start),
+ height: 2,
+ style: BlockStyle::Sticky,
+ render: Arc::new({
+ let assistant = self.assistant.clone();
+ let metadata = metadata.clone();
+ let message = message.clone();
+ move |cx| {
+ enum Sender {}
+ enum ErrorTooltip {}
+
+ let theme = theme::current(cx);
+ let style = &theme.assistant;
+ let message_id = message.id;
+ let sender = MouseEventHandler::<Sender, _>::new(
+ message_id.0,
+ cx,
+ |state, _| match metadata.role {
+ Role::User => {
+ let style = style.user_sender.style_for(state, false);
+ Label::new("You", style.text.clone())
+ .contained()
+ .with_style(style.container)
+ }
+ Role::Assistant => {
+ let style = style.assistant_sender.style_for(state, false);
+ Label::new("Assistant", style.text.clone())
+ .contained()
+ .with_style(style.container)
+ }
+ Role::System => {
+ let style = style.system_sender.style_for(state, false);
+ Label::new("System", style.text.clone())
+ .contained()
+ .with_style(style.container)
+ }
+ },
+ )
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_down(MouseButton::Left, {
+ let assistant = assistant.clone();
+ move |_, _, cx| {
+ assistant.update(cx, |assistant, cx| {
+ assistant.cycle_message_role(message_id, cx)
+ })
+ }
+ });
+
+ Flex::row()
+ .with_child(sender.aligned())
+ .with_child(
+ Label::new(
+ metadata.sent_at.format("%I:%M%P").to_string(),
+ style.sent_at.text.clone(),
+ )
+ .contained()
+ .with_style(style.sent_at.container)
+ .aligned(),
+ )
+ .with_children(metadata.error.clone().map(|error| {
+ Svg::new("icons/circle_x_mark_12.svg")
+ .with_color(style.error_icon.color)
+ .constrained()
+ .with_width(style.error_icon.width)
+ .contained()
+ .with_style(style.error_icon.container)
+ .with_tooltip::<ErrorTooltip>(
+ message_id.0,
+ error,
+ None,
+ theme.tooltip.clone(),
+ cx,
+ )
+ .aligned()
+ }))
+ .aligned()
+ .left()
+ .contained()
+ .with_style(style.header)
+ .into_any()
+ }
+ }),
+ disposition: BlockDisposition::Above,
+ })
+ .collect::<Vec<_>>();
+
+ editor.remove_blocks(old_blocks, None, cx);
+ let ids = editor.insert_blocks(new_blocks, None, cx);
+ self.blocks = HashSet::from_iter(ids);
});
}
@@ -1111,33 +1145,23 @@ impl AssistantEditor {
let assistant = self.assistant.read(cx);
if editor.selections.count() == 1 {
let selection = editor.selections.newest::<usize>(cx);
- let mut offset = 0;
let mut copied_text = String::new();
let mut spanned_messages = 0;
- for message in &assistant.messages {
- let message_range = offset..offset + message.content.read(cx).len() + 1;
-
+ for (_message, metadata, message_range) in assistant.messages(cx) {
if message_range.start >= selection.range().end {
break;
} else if message_range.end >= selection.range().start {
let range = cmp::max(message_range.start, selection.range().start)
..cmp::min(message_range.end, selection.range().end);
if !range.is_empty() {
- if let Some(metadata) = assistant.messages_metadata.get(&message.excerpt_id)
- {
- spanned_messages += 1;
- write!(&mut copied_text, "## {}\n\n", metadata.role).unwrap();
- for chunk in
- assistant.buffer.read(cx).snapshot(cx).text_for_range(range)
- {
- copied_text.push_str(&chunk);
- }
- copied_text.push('\n');
+ spanned_messages += 1;
+ write!(&mut copied_text, "## {}\n\n", metadata.role).unwrap();
+ for chunk in assistant.buffer.read(cx).text_for_range(range) {
+ copied_text.push_str(&chunk);
}
+ copied_text.push('\n');
}
}
-
- offset = message_range.end;
}
if spanned_messages > 1 {
@@ -1255,10 +1279,13 @@ impl Item for AssistantEditor {
}
}
+#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
+struct MessageId(usize);
+
#[derive(Clone, Debug)]
struct Message {
- excerpt_id: ExcerptId,
- content: ModelHandle<Buffer>,
+ id: MessageId,
+ start: language::Anchor,
}
#[derive(Clone, Debug)]
@@ -1362,22 +1389,137 @@ mod tests {
#[gpui::test]
fn test_inserting_and_removing_messages(cx: &mut AppContext) {
let registry = Arc::new(LanguageRegistry::test());
+ let assistant = cx.add_model(|cx| Assistant::new(Default::default(), registry, cx));
+ let buffer = assistant.read(cx).buffer.clone();
- cx.add_model(|cx| {
- let mut assistant = Assistant::new(Default::default(), registry, cx);
- let message_1 = assistant.messages[0].clone();
- let message_2 = assistant.insert_message_after(ExcerptId::max(), Role::Assistant, cx);
- let message_3 = assistant.insert_message_after(message_2.excerpt_id, Role::User, cx);
- let message_4 = assistant.insert_message_after(message_2.excerpt_id, Role::User, cx);
- assistant.remove_empty_messages(
- HashSet::from_iter([message_3.excerpt_id, message_4.excerpt_id]),
- Default::default(),
- cx,
- );
- assert_eq!(assistant.messages.len(), 2);
- assert_eq!(assistant.messages[0].excerpt_id, message_1.excerpt_id);
- assert_eq!(assistant.messages[1].excerpt_id, message_2.excerpt_id);
+ let message_1 = assistant.read(cx).messages[0].clone();
+ assert_eq!(
+ messages(&assistant, cx),
+ vec![(message_1.id, Role::User, 0..0)]
+ );
+
+ let message_2 = assistant.update(cx, |assistant, cx| {
+ assistant
+ .insert_message_after(message_1.id, Role::Assistant, cx)
+ .unwrap()
+ });
+ assert_eq!(
+ messages(&assistant, cx),
+ vec![
+ (message_1.id, Role::User, 0..1),
+ (message_2.id, Role::Assistant, 1..1)
+ ]
+ );
+
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit([(0..0, "1"), (1..1, "2")], None, cx)
+ });
+ assert_eq!(
+ messages(&assistant, cx),
+ vec![
+ (message_1.id, Role::User, 0..2),
+ (message_2.id, Role::Assistant, 2..3)
+ ]
+ );
+
+ let message_3 = assistant.update(cx, |assistant, cx| {
+ assistant
+ .insert_message_after(message_2.id, Role::User, cx)
+ .unwrap()
+ });
+ assert_eq!(
+ messages(&assistant, cx),
+ vec![
+ (message_1.id, Role::User, 0..2),
+ (message_2.id, Role::Assistant, 2..4),
+ (message_3.id, Role::User, 4..4)
+ ]
+ );
+
+ let message_4 = assistant.update(cx, |assistant, cx| {
assistant
+ .insert_message_after(message_2.id, Role::User, cx)
+ .unwrap()
});
+ assert_eq!(
+ messages(&assistant, cx),
+ vec![
+ (message_1.id, Role::User, 0..2),
+ (message_2.id, Role::Assistant, 2..4),
+ (message_4.id, Role::User, 4..5),
+ (message_3.id, Role::User, 5..5),
+ ]
+ );
+
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit([(4..4, "C"), (5..5, "D")], None, cx)
+ });
+ assert_eq!(
+ messages(&assistant, cx),
+ vec![
+ (message_1.id, Role::User, 0..2),
+ (message_2.id, Role::Assistant, 2..4),
+ (message_4.id, Role::User, 4..6),
+ (message_3.id, Role::User, 6..7),
+ ]
+ );
+
+ // Deleting across message boundaries merges the messages.
+ buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx));
+ assert_eq!(
+ messages(&assistant, cx),
+ vec![
+ (message_1.id, Role::User, 0..3),
+ (message_3.id, Role::User, 3..4),
+ ]
+ );
+
+ // Undoing the deletion should also undo the merge.
+ buffer.update(cx, |buffer, cx| buffer.undo(cx));
+ assert_eq!(
+ messages(&assistant, cx),
+ vec![
+ (message_1.id, Role::User, 0..2),
+ (message_2.id, Role::Assistant, 2..4),
+ (message_4.id, Role::User, 4..6),
+ (message_3.id, Role::User, 6..7),
+ ]
+ );
+
+ // Redoing the deletion should also redo the merge.
+ buffer.update(cx, |buffer, cx| buffer.redo(cx));
+ assert_eq!(
+ messages(&assistant, cx),
+ vec![
+ (message_1.id, Role::User, 0..3),
+ (message_3.id, Role::User, 3..4),
+ ]
+ );
+
+ // Ensure we can still insert after a merged message.
+ let message_5 = assistant.update(cx, |assistant, cx| {
+ assistant
+ .insert_message_after(message_1.id, Role::System, cx)
+ .unwrap()
+ });
+ assert_eq!(
+ messages(&assistant, cx),
+ vec![
+ (message_1.id, Role::User, 0..3),
+ (message_5.id, Role::System, 3..4),
+ (message_3.id, Role::User, 4..5)
+ ]
+ );
+ }
+
+ fn messages(
+ assistant: &ModelHandle<Assistant>,
+ cx: &AppContext,
+ ) -> Vec<(MessageId, Role, Range<usize>)> {
+ assistant
+ .read(cx)
+ .messages(cx)
+ .map(|(message, metadata, range)| (message.id, metadata.role, range))
+ .collect()
}
}