Allow using the inline assistant in prompt library (#12680)
Release Notes: - N/A
This commit is contained in:
parent
0289c312c9
commit
a96782cc6b
6 changed files with 1589 additions and 1470 deletions
|
@ -1,7 +1,7 @@
|
||||||
pub mod assistant_panel;
|
pub mod assistant_panel;
|
||||||
pub mod assistant_settings;
|
pub mod assistant_settings;
|
||||||
mod codegen;
|
|
||||||
mod completion_provider;
|
mod completion_provider;
|
||||||
|
mod inline_assistant;
|
||||||
mod model_selector;
|
mod model_selector;
|
||||||
mod prompt_library;
|
mod prompt_library;
|
||||||
mod prompts;
|
mod prompts;
|
||||||
|
@ -18,6 +18,7 @@ use client::{proto, Client};
|
||||||
use command_palette_hooks::CommandPaletteFilter;
|
use command_palette_hooks::CommandPaletteFilter;
|
||||||
pub(crate) use completion_provider::*;
|
pub(crate) use completion_provider::*;
|
||||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||||
|
pub(crate) use inline_assistant::*;
|
||||||
pub(crate) use model_selector::*;
|
pub(crate) use model_selector::*;
|
||||||
pub(crate) use saved_conversation::*;
|
pub(crate) use saved_conversation::*;
|
||||||
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
|
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
|
||||||
|
@ -31,6 +32,7 @@ use std::{
|
||||||
fmt::{self, Display},
|
fmt::{self, Display},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
pub(crate) use streaming_diff::*;
|
||||||
use util::paths::EMBEDDINGS_DIR;
|
use util::paths::EMBEDDINGS_DIR;
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
|
@ -273,10 +275,11 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
prompt_library::init(cx);
|
prompt_library::init(cx);
|
||||||
completion_provider::init(client, cx);
|
completion_provider::init(client.clone(), cx);
|
||||||
assistant_slash_command::init(cx);
|
assistant_slash_command::init(cx);
|
||||||
register_slash_commands(cx);
|
register_slash_commands(cx);
|
||||||
assistant_panel::init(cx);
|
assistant_panel::init(cx);
|
||||||
|
inline_assistant::init(client.telemetry().clone(), cx);
|
||||||
|
|
||||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||||
filter.hide_namespace(Assistant::NAMESPACE);
|
filter.hide_namespace(Assistant::NAMESPACE);
|
||||||
|
|
|
@ -1,31 +1,27 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
assistant_settings::{AssistantDockPosition, AssistantSettings},
|
assistant_settings::{AssistantDockPosition, AssistantSettings},
|
||||||
codegen::{self, Codegen, CodegenKind},
|
|
||||||
prompt_library::open_prompt_library,
|
prompt_library::open_prompt_library,
|
||||||
prompts::generate_content_prompt,
|
|
||||||
search::*,
|
search::*,
|
||||||
slash_command::{
|
slash_command::{
|
||||||
default_command::DefaultSlashCommand, SlashCommandCompletionProvider, SlashCommandLine,
|
default_command::DefaultSlashCommand, SlashCommandCompletionProvider, SlashCommandLine,
|
||||||
SlashCommandRegistry,
|
SlashCommandRegistry,
|
||||||
},
|
},
|
||||||
ApplyEdit, Assist, CompletionProvider, ConfirmCommand, CycleMessageRole, InlineAssist,
|
ApplyEdit, Assist, CompletionProvider, ConfirmCommand, CycleMessageRole, InlineAssist,
|
||||||
LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata, MessageStatus,
|
InlineAssistant, LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata,
|
||||||
ModelSelector, QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata,
|
MessageStatus, ModelSelector, QuoteSelection, ResetKey, Role, SavedConversation,
|
||||||
SavedMessage, Split, ToggleFocus, ToggleHistory, ToggleModelSelector,
|
SavedConversationMetadata, SavedMessage, Split, ToggleFocus, ToggleHistory,
|
||||||
|
ToggleModelSelector,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
||||||
use client::telemetry::Telemetry;
|
use client::telemetry::Telemetry;
|
||||||
use collections::{hash_map, BTreeSet, HashMap, HashSet, VecDeque};
|
use collections::{BTreeSet, HashMap, HashSet};
|
||||||
use editor::{actions::ShowCompletions, GutterDimensions};
|
use editor::actions::ShowCompletions;
|
||||||
use editor::{
|
use editor::{
|
||||||
actions::{FoldAt, MoveDown, MoveToEndOfLine, MoveUp, Newline, UnfoldAt},
|
actions::{FoldAt, MoveToEndOfLine, Newline, UnfoldAt},
|
||||||
display_map::{
|
display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, Flap, ToDisplayPoint},
|
||||||
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, Flap, ToDisplayPoint,
|
|
||||||
},
|
|
||||||
scroll::{Autoscroll, AutoscrollStrategy},
|
scroll::{Autoscroll, AutoscrollStrategy},
|
||||||
Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MultiBufferSnapshot, RowExt,
|
Anchor, Editor, EditorEvent, RowExt, ToOffset as _, ToPoint,
|
||||||
ToOffset as _, ToPoint,
|
|
||||||
};
|
};
|
||||||
use editor::{display_map::FlapId, FoldPlaceholder};
|
use editor::{display_map::FlapId, FoldPlaceholder};
|
||||||
use file_icons::FileIcons;
|
use file_icons::FileIcons;
|
||||||
|
@ -33,19 +29,17 @@ use fs::Fs;
|
||||||
use futures::future::Shared;
|
use futures::future::Shared;
|
||||||
use futures::{FutureExt, StreamExt};
|
use futures::{FutureExt, StreamExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, point, relative, rems, uniform_list, Action, AnyElement, AnyView, AppContext,
|
div, point, rems, uniform_list, Action, AnyElement, AnyView, AppContext, AsyncAppContext,
|
||||||
AsyncAppContext, AsyncWindowContext, ClipboardItem, Context, Empty, EventEmitter, FocusHandle,
|
AsyncWindowContext, ClipboardItem, Context, Empty, EventEmitter, FocusHandle, FocusableView,
|
||||||
FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement, IntoElement, Model,
|
InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels, Render,
|
||||||
ModelContext, ParentElement, Pixels, Render, SharedString, StatefulInteractiveElement, Styled,
|
SharedString, StatefulInteractiveElement, Styled, Subscription, Task, UniformListScrollHandle,
|
||||||
Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext,
|
UpdateGlobal, View, ViewContext, VisualContext, WeakView, WindowContext,
|
||||||
WeakModel, WeakView, WhiteSpace, WindowContext,
|
|
||||||
};
|
};
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::SoftWrap, AnchorRangeExt, AutoindentMode, Buffer, LanguageRegistry,
|
language_settings::SoftWrap, AnchorRangeExt, AutoindentMode, Buffer, LanguageRegistry,
|
||||||
LspAdapterDelegate, OffsetRangeExt as _, Point, ToOffset as _,
|
LspAdapterDelegate, OffsetRangeExt as _, Point, ToOffset as _,
|
||||||
};
|
};
|
||||||
use multi_buffer::MultiBufferRow;
|
use multi_buffer::MultiBufferRow;
|
||||||
use parking_lot::Mutex;
|
|
||||||
use project::{Project, ProjectLspAdapterDelegate, ProjectTransaction};
|
use project::{Project, ProjectLspAdapterDelegate, ProjectTransaction};
|
||||||
use search::{buffer_search::DivRegistrar, BufferSearchBar};
|
use search::{buffer_search::DivRegistrar, BufferSearchBar};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
@ -59,19 +53,18 @@ use std::{
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use telemetry_events::AssistantKind;
|
use telemetry_events::AssistantKind;
|
||||||
use theme::ThemeSettings;
|
|
||||||
use ui::{
|
use ui::{
|
||||||
popover_menu, prelude::*, ButtonLike, ContextMenu, ElevationIndex, KeyBinding,
|
popover_menu, prelude::*, ButtonLike, ContextMenu, ElevationIndex, KeyBinding,
|
||||||
PopoverMenuHandle, Tab, TabBar, Tooltip,
|
PopoverMenuHandle, Tab, TabBar, Tooltip,
|
||||||
};
|
};
|
||||||
use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
|
use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use workspace::NewFile;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
searchable::Direction,
|
searchable::Direction,
|
||||||
Save, Toast, ToggleZoom, Toolbar, Workspace,
|
Save, ToggleZoom, Toolbar, Workspace,
|
||||||
};
|
};
|
||||||
use workspace::{notifications::NotificationId, NewFile};
|
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
cx.observe_new_views(
|
cx.observe_new_views(
|
||||||
|
@ -110,10 +103,6 @@ pub struct AssistantPanel {
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
telemetry: Arc<Telemetry>,
|
telemetry: Arc<Telemetry>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
next_inline_assist_id: usize,
|
|
||||||
pending_inline_assists: HashMap<usize, PendingInlineAssist>,
|
|
||||||
pending_inline_assist_ids_by_editor: HashMap<WeakView<Editor>, Vec<usize>>,
|
|
||||||
inline_prompt_history: VecDeque<String>,
|
|
||||||
_watch_saved_conversations: Task<Result<()>>,
|
_watch_saved_conversations: Task<Result<()>>,
|
||||||
authentication_prompt: Option<AnyView>,
|
authentication_prompt: Option<AnyView>,
|
||||||
model_menu_handle: PopoverMenuHandle<ContextMenu>,
|
model_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||||
|
@ -125,8 +114,6 @@ struct ActiveConversationEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AssistantPanel {
|
impl AssistantPanel {
|
||||||
const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20;
|
|
||||||
|
|
||||||
pub fn load(
|
pub fn load(
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
cx: AsyncWindowContext,
|
cx: AsyncWindowContext,
|
||||||
|
@ -205,10 +192,6 @@ impl AssistantPanel {
|
||||||
width: None,
|
width: None,
|
||||||
height: None,
|
height: None,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
next_inline_assist_id: 0,
|
|
||||||
pending_inline_assists: Default::default(),
|
|
||||||
pending_inline_assist_ids_by_editor: Default::default(),
|
|
||||||
inline_prompt_history: Default::default(),
|
|
||||||
_watch_saved_conversations,
|
_watch_saved_conversations,
|
||||||
authentication_prompt: None,
|
authentication_prompt: None,
|
||||||
model_menu_handle: PopoverMenuHandle::default(),
|
model_menu_handle: PopoverMenuHandle::default(),
|
||||||
|
@ -295,26 +278,30 @@ impl AssistantPanel {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let show_include_conversation;
|
let include_conversation;
|
||||||
let active_editor;
|
let active_editor;
|
||||||
if let Some(conversation_editor) = conversation_editor {
|
if let Some(conversation_editor) = conversation_editor {
|
||||||
active_editor = conversation_editor;
|
active_editor = conversation_editor;
|
||||||
show_include_conversation = false;
|
include_conversation = false;
|
||||||
} else if let Some(workspace_editor) = workspace
|
} else if let Some(workspace_editor) = workspace
|
||||||
.active_item(cx)
|
.active_item(cx)
|
||||||
.and_then(|item| item.act_as::<Editor>(cx))
|
.and_then(|item| item.act_as::<Editor>(cx))
|
||||||
{
|
{
|
||||||
active_editor = workspace_editor;
|
active_editor = workspace_editor;
|
||||||
show_include_conversation = true;
|
include_conversation = true;
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let project = workspace.project().clone();
|
|
||||||
|
|
||||||
if assistant.update(cx, |assistant, cx| assistant.is_authenticated(cx)) {
|
if assistant.update(cx, |assistant, cx| assistant.is_authenticated(cx)) {
|
||||||
assistant.update(cx, |assistant, cx| {
|
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||||
assistant.new_inline_assist(&active_editor, &project, show_include_conversation, cx)
|
assistant.assist(
|
||||||
});
|
&active_editor,
|
||||||
|
Some(cx.view().downgrade()),
|
||||||
|
include_conversation,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
let assistant = assistant.downgrade();
|
let assistant = assistant.downgrade();
|
||||||
cx.spawn(|workspace, mut cx| async move {
|
cx.spawn(|workspace, mut cx| async move {
|
||||||
|
@ -322,14 +309,16 @@ impl AssistantPanel {
|
||||||
.update(&mut cx, |assistant, cx| assistant.authenticate(cx))?
|
.update(&mut cx, |assistant, cx| assistant.authenticate(cx))?
|
||||||
.await?;
|
.await?;
|
||||||
if assistant.update(&mut cx, |assistant, cx| assistant.is_authenticated(cx))? {
|
if assistant.update(&mut cx, |assistant, cx| assistant.is_authenticated(cx))? {
|
||||||
assistant.update(&mut cx, |assistant, cx| {
|
cx.update(|cx| {
|
||||||
assistant.new_inline_assist(
|
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||||
&active_editor,
|
assistant.assist(
|
||||||
&project,
|
&active_editor,
|
||||||
show_include_conversation,
|
Some(workspace),
|
||||||
cx,
|
include_conversation,
|
||||||
)
|
cx,
|
||||||
})?;
|
)
|
||||||
|
})
|
||||||
|
})?
|
||||||
} else {
|
} else {
|
||||||
workspace.update(&mut cx, |workspace, cx| {
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
workspace.focus_panel::<AssistantPanel>(cx)
|
workspace.focus_panel::<AssistantPanel>(cx)
|
||||||
|
@ -342,439 +331,17 @@ impl AssistantPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_inline_assist(
|
|
||||||
&mut self,
|
|
||||||
editor: &View<Editor>,
|
|
||||||
project: &Model<Project>,
|
|
||||||
include_conversation: bool,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) {
|
|
||||||
let selection = editor.read(cx).selections.newest_anchor().clone();
|
|
||||||
if selection.start.excerpt_id != selection.end.excerpt_id {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
|
|
||||||
|
|
||||||
// Extend the selection to the start and the end of the line.
|
|
||||||
let mut point_selection = selection.map(|selection| selection.to_point(&snapshot));
|
|
||||||
if point_selection.end > point_selection.start {
|
|
||||||
point_selection.start.column = 0;
|
|
||||||
// If the selection ends at the start of the line, we don't want to include it.
|
|
||||||
if point_selection.end.column == 0 {
|
|
||||||
point_selection.end.row -= 1;
|
|
||||||
}
|
|
||||||
point_selection.end.column = snapshot.line_len(MultiBufferRow(point_selection.end.row));
|
|
||||||
}
|
|
||||||
|
|
||||||
let codegen_kind = if point_selection.start == point_selection.end {
|
|
||||||
CodegenKind::Generate {
|
|
||||||
position: snapshot.anchor_after(point_selection.start),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
CodegenKind::Transform {
|
|
||||||
range: snapshot.anchor_before(point_selection.start)
|
|
||||||
..snapshot.anchor_after(point_selection.end),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
|
|
||||||
let telemetry = self.telemetry.clone();
|
|
||||||
|
|
||||||
let codegen = cx.new_model(|cx| {
|
|
||||||
Codegen::new(
|
|
||||||
editor.read(cx).buffer().clone(),
|
|
||||||
codegen_kind,
|
|
||||||
Some(telemetry),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let measurements = Arc::new(Mutex::new(GutterDimensions::default()));
|
|
||||||
let inline_assistant = cx.new_view(|cx| {
|
|
||||||
InlineAssistant::new(
|
|
||||||
inline_assist_id,
|
|
||||||
measurements.clone(),
|
|
||||||
include_conversation,
|
|
||||||
self.inline_prompt_history.clone(),
|
|
||||||
codegen.clone(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let block_id = editor.update(cx, |editor, cx| {
|
|
||||||
editor.change_selections(None, cx, |selections| {
|
|
||||||
selections.select_anchor_ranges([selection.head()..selection.head()])
|
|
||||||
});
|
|
||||||
editor.insert_blocks(
|
|
||||||
[BlockProperties {
|
|
||||||
style: BlockStyle::Flex,
|
|
||||||
position: snapshot.anchor_before(Point::new(point_selection.head().row, 0)),
|
|
||||||
height: 2,
|
|
||||||
render: Box::new({
|
|
||||||
let inline_assistant = inline_assistant.clone();
|
|
||||||
move |cx: &mut BlockContext| {
|
|
||||||
*measurements.lock() = *cx.gutter_dimensions;
|
|
||||||
inline_assistant.clone().into_any_element()
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
disposition: if selection.reversed {
|
|
||||||
BlockDisposition::Above
|
|
||||||
} else {
|
|
||||||
BlockDisposition::Below
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
Some(Autoscroll::Strategy(AutoscrollStrategy::Newest)),
|
|
||||||
cx,
|
|
||||||
)[0]
|
|
||||||
});
|
|
||||||
|
|
||||||
self.pending_inline_assists.insert(
|
|
||||||
inline_assist_id,
|
|
||||||
PendingInlineAssist {
|
|
||||||
editor: editor.downgrade(),
|
|
||||||
inline_assistant: Some((block_id, inline_assistant.clone())),
|
|
||||||
codegen: codegen.clone(),
|
|
||||||
project: project.downgrade(),
|
|
||||||
_subscriptions: vec![
|
|
||||||
cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event),
|
|
||||||
cx.subscribe(editor, {
|
|
||||||
let inline_assistant = inline_assistant.downgrade();
|
|
||||||
move |_, editor, event, cx| {
|
|
||||||
if let Some(inline_assistant) = inline_assistant.upgrade() {
|
|
||||||
if let EditorEvent::SelectionsChanged { local } = event {
|
|
||||||
if *local
|
|
||||||
&& inline_assistant.focus_handle(cx).contains_focused(cx)
|
|
||||||
{
|
|
||||||
cx.focus_view(&editor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
cx.observe(&codegen, {
|
|
||||||
let editor = editor.downgrade();
|
|
||||||
move |this, _, cx| {
|
|
||||||
if let Some(editor) = editor.upgrade() {
|
|
||||||
this.update_highlights_for_editor(&editor, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
cx.subscribe(&codegen, move |this, codegen, event, cx| match event {
|
|
||||||
codegen::Event::Undone => {
|
|
||||||
this.finish_inline_assist(inline_assist_id, false, cx)
|
|
||||||
}
|
|
||||||
codegen::Event::Finished => {
|
|
||||||
let pending_assist = if let Some(pending_assist) =
|
|
||||||
this.pending_inline_assists.get(&inline_assist_id)
|
|
||||||
{
|
|
||||||
pending_assist
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let error = codegen
|
|
||||||
.read(cx)
|
|
||||||
.error()
|
|
||||||
.map(|error| format!("Inline assistant error: {}", error));
|
|
||||||
if let Some(error) = error {
|
|
||||||
if pending_assist.inline_assistant.is_none() {
|
|
||||||
if let Some(workspace) = this.workspace.upgrade() {
|
|
||||||
workspace.update(cx, |workspace, cx| {
|
|
||||||
struct InlineAssistantError;
|
|
||||||
|
|
||||||
let id =
|
|
||||||
NotificationId::identified::<InlineAssistantError>(
|
|
||||||
inline_assist_id,
|
|
||||||
);
|
|
||||||
|
|
||||||
workspace.show_toast(Toast::new(id, error), cx);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.finish_inline_assist(inline_assist_id, false, cx);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.finish_inline_assist(inline_assist_id, false, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
self.pending_inline_assist_ids_by_editor
|
|
||||||
.entry(editor.downgrade())
|
|
||||||
.or_default()
|
|
||||||
.push(inline_assist_id);
|
|
||||||
self.update_highlights_for_editor(editor, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_inline_assistant_event(
|
|
||||||
&mut self,
|
|
||||||
inline_assistant: View<InlineAssistant>,
|
|
||||||
event: &InlineAssistantEvent,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) {
|
|
||||||
let assist_id = inline_assistant.read(cx).id;
|
|
||||||
match event {
|
|
||||||
InlineAssistantEvent::Confirmed {
|
|
||||||
prompt,
|
|
||||||
include_conversation,
|
|
||||||
} => {
|
|
||||||
self.confirm_inline_assist(assist_id, prompt, *include_conversation, cx);
|
|
||||||
}
|
|
||||||
InlineAssistantEvent::Canceled => {
|
|
||||||
self.finish_inline_assist(assist_id, true, cx);
|
|
||||||
}
|
|
||||||
InlineAssistantEvent::Dismissed => {
|
|
||||||
self.hide_inline_assist(assist_id, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cancel_last_inline_assist(
|
fn cancel_last_inline_assist(
|
||||||
workspace: &mut Workspace,
|
_workspace: &mut Workspace,
|
||||||
_: &editor::actions::Cancel,
|
_: &editor::actions::Cancel,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) {
|
) {
|
||||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
let canceled = InlineAssistant::update_global(cx, |assistant, cx| {
|
||||||
if let Some(editor) = workspace
|
assistant.cancel_last_inline_assist(cx)
|
||||||
.active_item(cx)
|
|
||||||
.and_then(|item| item.downcast::<Editor>())
|
|
||||||
{
|
|
||||||
let handled = panel.update(cx, |panel, cx| {
|
|
||||||
if let Some(assist_id) = panel
|
|
||||||
.pending_inline_assist_ids_by_editor
|
|
||||||
.get(&editor.downgrade())
|
|
||||||
.and_then(|assist_ids| assist_ids.last().copied())
|
|
||||||
{
|
|
||||||
panel.finish_inline_assist(assist_id, true, cx);
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if handled {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.propagate();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finish_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext<Self>) {
|
|
||||||
self.hide_inline_assist(assist_id, cx);
|
|
||||||
|
|
||||||
if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) {
|
|
||||||
if let hash_map::Entry::Occupied(mut entry) = self
|
|
||||||
.pending_inline_assist_ids_by_editor
|
|
||||||
.entry(pending_assist.editor.clone())
|
|
||||||
{
|
|
||||||
entry.get_mut().retain(|id| *id != assist_id);
|
|
||||||
if entry.get().is_empty() {
|
|
||||||
entry.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(editor) = pending_assist.editor.upgrade() {
|
|
||||||
self.update_highlights_for_editor(&editor, cx);
|
|
||||||
|
|
||||||
if undo {
|
|
||||||
pending_assist
|
|
||||||
.codegen
|
|
||||||
.update(cx, |codegen, cx| codegen.undo(cx));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hide_inline_assist(&mut self, assist_id: usize, cx: &mut ViewContext<Self>) {
|
|
||||||
if let Some(pending_assist) = self.pending_inline_assists.get_mut(&assist_id) {
|
|
||||||
if let Some(editor) = pending_assist.editor.upgrade() {
|
|
||||||
if let Some((block_id, inline_assistant)) = pending_assist.inline_assistant.take() {
|
|
||||||
editor.update(cx, |editor, cx| {
|
|
||||||
editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
|
|
||||||
if inline_assistant.focus_handle(cx).contains_focused(cx) {
|
|
||||||
editor.focus(cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn confirm_inline_assist(
|
|
||||||
&mut self,
|
|
||||||
inline_assist_id: usize,
|
|
||||||
user_prompt: &str,
|
|
||||||
include_conversation: bool,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) {
|
|
||||||
let conversation = if include_conversation {
|
|
||||||
self.active_conversation_editor()
|
|
||||||
.map(|editor| editor.read(cx).conversation.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let pending_assist =
|
|
||||||
if let Some(pending_assist) = self.pending_inline_assists.get_mut(&inline_assist_id) {
|
|
||||||
pending_assist
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let editor = if let Some(editor) = pending_assist.editor.upgrade() {
|
|
||||||
editor
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let project = pending_assist.project.clone();
|
|
||||||
|
|
||||||
let project_name = project.upgrade().map(|project| {
|
|
||||||
project
|
|
||||||
.read(cx)
|
|
||||||
.worktree_root_names(cx)
|
|
||||||
.collect::<Vec<&str>>()
|
|
||||||
.join("/")
|
|
||||||
});
|
});
|
||||||
|
if !canceled {
|
||||||
self.inline_prompt_history
|
cx.propagate();
|
||||||
.retain(|prompt| prompt != user_prompt);
|
|
||||||
self.inline_prompt_history.push_back(user_prompt.into());
|
|
||||||
if self.inline_prompt_history.len() > Self::INLINE_PROMPT_HISTORY_MAX_LEN {
|
|
||||||
self.inline_prompt_history.pop_front();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let codegen = pending_assist.codegen.clone();
|
|
||||||
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
|
|
||||||
let range = codegen.read(cx).range();
|
|
||||||
let start = snapshot.point_to_buffer_offset(range.start);
|
|
||||||
let end = snapshot.point_to_buffer_offset(range.end);
|
|
||||||
let (buffer, range) = if let Some((start, end)) = start.zip(end) {
|
|
||||||
let (start_buffer, start_buffer_offset) = start;
|
|
||||||
let (end_buffer, end_buffer_offset) = end;
|
|
||||||
if start_buffer.remote_id() == end_buffer.remote_id() {
|
|
||||||
(start_buffer.clone(), start_buffer_offset..end_buffer_offset)
|
|
||||||
} else {
|
|
||||||
self.finish_inline_assist(inline_assist_id, false, cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.finish_inline_assist(inline_assist_id, false, cx);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let language = buffer.language_at(range.start);
|
|
||||||
let language_name = if let Some(language) = language.as_ref() {
|
|
||||||
if Arc::ptr_eq(language, &language::PLAIN_TEXT) {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(language.name())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Higher Temperature increases the randomness of model outputs.
|
|
||||||
// If Markdown or No Language is Known, increase the randomness for more creative output
|
|
||||||
// If Code, decrease temperature to get more deterministic outputs
|
|
||||||
let temperature = if let Some(language) = language_name.clone() {
|
|
||||||
if language.as_ref() == "Markdown" {
|
|
||||||
1.0
|
|
||||||
} else {
|
|
||||||
0.5
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
1.0
|
|
||||||
};
|
|
||||||
|
|
||||||
let user_prompt = user_prompt.to_string();
|
|
||||||
|
|
||||||
let prompt = cx.background_executor().spawn(async move {
|
|
||||||
let language_name = language_name.as_deref();
|
|
||||||
generate_content_prompt(user_prompt, language_name, buffer, range, project_name)
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut messages = Vec::new();
|
|
||||||
if let Some(conversation) = conversation {
|
|
||||||
let conversation = conversation.read(cx);
|
|
||||||
let buffer = conversation.buffer.read(cx);
|
|
||||||
messages.extend(
|
|
||||||
conversation
|
|
||||||
.messages(cx)
|
|
||||||
.map(|message| message.to_request_message(buffer)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let model = CompletionProvider::global(cx).model();
|
|
||||||
|
|
||||||
cx.spawn(|_, mut cx| async move {
|
|
||||||
// I Don't know if we want to return a ? here.
|
|
||||||
let prompt = prompt.await?;
|
|
||||||
|
|
||||||
messages.push(LanguageModelRequestMessage {
|
|
||||||
role: Role::User,
|
|
||||||
content: prompt,
|
|
||||||
});
|
|
||||||
|
|
||||||
let request = LanguageModelRequest {
|
|
||||||
model,
|
|
||||||
messages,
|
|
||||||
stop: vec!["|END|>".to_string()],
|
|
||||||
temperature,
|
|
||||||
};
|
|
||||||
|
|
||||||
codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx))?;
|
|
||||||
anyhow::Ok(())
|
|
||||||
})
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_highlights_for_editor(&self, editor: &View<Editor>, cx: &mut ViewContext<Self>) {
|
|
||||||
let mut background_ranges = Vec::new();
|
|
||||||
let mut foreground_ranges = Vec::new();
|
|
||||||
let empty_inline_assist_ids = Vec::new();
|
|
||||||
let inline_assist_ids = self
|
|
||||||
.pending_inline_assist_ids_by_editor
|
|
||||||
.get(&editor.downgrade())
|
|
||||||
.unwrap_or(&empty_inline_assist_ids);
|
|
||||||
|
|
||||||
for inline_assist_id in inline_assist_ids {
|
|
||||||
if let Some(pending_assist) = self.pending_inline_assists.get(inline_assist_id) {
|
|
||||||
let codegen = pending_assist.codegen.read(cx);
|
|
||||||
background_ranges.push(codegen.range());
|
|
||||||
foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
|
|
||||||
merge_ranges(&mut background_ranges, &snapshot);
|
|
||||||
merge_ranges(&mut foreground_ranges, &snapshot);
|
|
||||||
editor.update(cx, |editor, cx| {
|
|
||||||
if background_ranges.is_empty() {
|
|
||||||
editor.clear_background_highlights::<PendingInlineAssist>(cx);
|
|
||||||
} else {
|
|
||||||
editor.highlight_background::<PendingInlineAssist>(
|
|
||||||
&background_ranges,
|
|
||||||
|theme| theme.editor_active_line_background, // TODO use the appropriate color
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if foreground_ranges.is_empty() {
|
|
||||||
editor.clear_highlights::<PendingInlineAssist>(cx);
|
|
||||||
} else {
|
|
||||||
editor.highlight_text::<PendingInlineAssist>(
|
|
||||||
foreground_ranges,
|
|
||||||
HighlightStyle {
|
|
||||||
fade_out: Some(0.6),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> Option<View<ConversationEditor>> {
|
fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> Option<View<ConversationEditor>> {
|
||||||
|
@ -919,6 +486,15 @@ impl AssistantPanel {
|
||||||
Some(&self.active_conversation_editor.as_ref()?.editor)
|
Some(&self.active_conversation_editor.as_ref()?.editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn active_conversation(&self, cx: &AppContext) -> Option<Model<Conversation>> {
|
||||||
|
Some(
|
||||||
|
self.active_conversation_editor()?
|
||||||
|
.read(cx)
|
||||||
|
.conversation
|
||||||
|
.clone(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn render_popover_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render_popover_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let assistant = cx.view().clone();
|
let assistant = cx.view().clone();
|
||||||
let zoomed = self.zoomed;
|
let zoomed = self.zoomed;
|
||||||
|
@ -2045,7 +1621,7 @@ impl Conversation {
|
||||||
user_messages
|
user_messages
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_completion_request(&self, cx: &mut ModelContext<Conversation>) -> LanguageModelRequest {
|
pub fn to_completion_request(&self, cx: &AppContext) -> LanguageModelRequest {
|
||||||
let messages = self
|
let messages = self
|
||||||
.messages(cx)
|
.messages(cx)
|
||||||
.filter(|message| matches!(message.status, MessageStatus::Done))
|
.filter(|message| matches!(message.status, MessageStatus::Done))
|
||||||
|
@ -2919,16 +2495,8 @@ impl ConversationEditor {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let render_trailer = {
|
let render_trailer =
|
||||||
let confirm_command = confirm_command.clone();
|
|_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
|
||||||
move |row, _, cx: &mut WindowContext| {
|
|
||||||
render_pending_slash_command_trailer(
|
|
||||||
row,
|
|
||||||
confirm_command.clone(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let start = buffer
|
let start = buffer
|
||||||
.anchor_in_excerpt(excerpt_id, command.source_range.start)
|
.anchor_in_excerpt(excerpt_id, command.source_range.start)
|
||||||
|
@ -3517,239 +3085,6 @@ impl Message {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum InlineAssistantEvent {
|
|
||||||
Confirmed {
|
|
||||||
prompt: String,
|
|
||||||
include_conversation: bool,
|
|
||||||
},
|
|
||||||
Canceled,
|
|
||||||
Dismissed,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct InlineAssistant {
|
|
||||||
id: usize,
|
|
||||||
prompt_editor: View<Editor>,
|
|
||||||
confirmed: bool,
|
|
||||||
include_conversation: bool,
|
|
||||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
|
||||||
prompt_history: VecDeque<String>,
|
|
||||||
prompt_history_ix: Option<usize>,
|
|
||||||
pending_prompt: String,
|
|
||||||
codegen: Model<Codegen>,
|
|
||||||
_subscriptions: Vec<Subscription>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<InlineAssistantEvent> for InlineAssistant {}
|
|
||||||
|
|
||||||
impl Render for InlineAssistant {
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
|
||||||
let gutter_dimensions = *self.gutter_dimensions.lock();
|
|
||||||
let icon_size = IconSize::default();
|
|
||||||
h_flex()
|
|
||||||
.w_full()
|
|
||||||
.py_2()
|
|
||||||
.border_y_1()
|
|
||||||
.border_color(cx.theme().colors().border)
|
|
||||||
.bg(cx.theme().colors().editor_background)
|
|
||||||
.on_action(cx.listener(Self::confirm))
|
|
||||||
.on_action(cx.listener(Self::cancel))
|
|
||||||
.on_action(cx.listener(Self::move_up))
|
|
||||||
.on_action(cx.listener(Self::move_down))
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
|
|
||||||
.pr(gutter_dimensions.fold_area_width())
|
|
||||||
.justify_end()
|
|
||||||
.children(if let Some(error) = self.codegen.read(cx).error() {
|
|
||||||
let error_message = SharedString::from(error.to_string());
|
|
||||||
Some(
|
|
||||||
div()
|
|
||||||
.id("error")
|
|
||||||
.tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::XCircle)
|
|
||||||
.size(icon_size)
|
|
||||||
.color(Color::Error),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(h_flex().flex_1().child(self.render_prompt_editor(cx)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FocusableView for InlineAssistant {
|
|
||||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
|
||||||
self.prompt_editor.focus_handle(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InlineAssistant {
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn new(
|
|
||||||
id: usize,
|
|
||||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
|
||||||
include_conversation: bool,
|
|
||||||
prompt_history: VecDeque<String>,
|
|
||||||
codegen: Model<Codegen>,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let prompt_editor = cx.new_view(|cx| {
|
|
||||||
let mut editor = Editor::single_line(cx);
|
|
||||||
let placeholder = match codegen.read(cx).kind() {
|
|
||||||
CodegenKind::Transform { .. } => "Enter transformation prompt…",
|
|
||||||
CodegenKind::Generate { .. } => "Enter generation prompt…",
|
|
||||||
};
|
|
||||||
editor.set_placeholder_text(placeholder, cx);
|
|
||||||
editor
|
|
||||||
});
|
|
||||||
cx.focus_view(&prompt_editor);
|
|
||||||
|
|
||||||
let subscriptions = vec![
|
|
||||||
cx.observe(&codegen, Self::handle_codegen_changed),
|
|
||||||
cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events),
|
|
||||||
];
|
|
||||||
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
prompt_editor,
|
|
||||||
confirmed: false,
|
|
||||||
include_conversation,
|
|
||||||
gutter_dimensions,
|
|
||||||
prompt_history,
|
|
||||||
prompt_history_ix: None,
|
|
||||||
pending_prompt: String::new(),
|
|
||||||
codegen,
|
|
||||||
_subscriptions: subscriptions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_prompt_editor_events(
|
|
||||||
&mut self,
|
|
||||||
_: View<Editor>,
|
|
||||||
event: &EditorEvent,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) {
|
|
||||||
if let EditorEvent::Edited = event {
|
|
||||||
self.pending_prompt = self.prompt_editor.read(cx).text(cx);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
|
|
||||||
let is_read_only = !self.codegen.read(cx).idle();
|
|
||||||
self.prompt_editor.update(cx, |editor, cx| {
|
|
||||||
let was_read_only = editor.read_only(cx);
|
|
||||||
if was_read_only != is_read_only {
|
|
||||||
if is_read_only {
|
|
||||||
editor.set_read_only(true);
|
|
||||||
} else {
|
|
||||||
self.confirmed = false;
|
|
||||||
editor.set_read_only(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
|
|
||||||
cx.emit(InlineAssistantEvent::Canceled);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
|
||||||
if self.confirmed {
|
|
||||||
cx.emit(InlineAssistantEvent::Dismissed);
|
|
||||||
} else {
|
|
||||||
let prompt = self.prompt_editor.read(cx).text(cx);
|
|
||||||
self.prompt_editor
|
|
||||||
.update(cx, |editor, _cx| editor.set_read_only(true));
|
|
||||||
cx.emit(InlineAssistantEvent::Confirmed {
|
|
||||||
prompt,
|
|
||||||
include_conversation: self.include_conversation,
|
|
||||||
});
|
|
||||||
self.confirmed = true;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
|
|
||||||
if let Some(ix) = self.prompt_history_ix {
|
|
||||||
if ix > 0 {
|
|
||||||
self.prompt_history_ix = Some(ix - 1);
|
|
||||||
let prompt = self.prompt_history[ix - 1].clone();
|
|
||||||
self.set_prompt(&prompt, cx);
|
|
||||||
}
|
|
||||||
} else if !self.prompt_history.is_empty() {
|
|
||||||
self.prompt_history_ix = Some(self.prompt_history.len() - 1);
|
|
||||||
let prompt = self.prompt_history[self.prompt_history.len() - 1].clone();
|
|
||||||
self.set_prompt(&prompt, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
|
|
||||||
if let Some(ix) = self.prompt_history_ix {
|
|
||||||
if ix < self.prompt_history.len() - 1 {
|
|
||||||
self.prompt_history_ix = Some(ix + 1);
|
|
||||||
let prompt = self.prompt_history[ix + 1].clone();
|
|
||||||
self.set_prompt(&prompt, cx);
|
|
||||||
} else {
|
|
||||||
self.prompt_history_ix = None;
|
|
||||||
let pending_prompt = self.pending_prompt.clone();
|
|
||||||
self.set_prompt(&pending_prompt, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_prompt(&mut self, prompt: &str, cx: &mut ViewContext<Self>) {
|
|
||||||
self.prompt_editor.update(cx, |editor, cx| {
|
|
||||||
editor.buffer().update(cx, |buffer, cx| {
|
|
||||||
let len = buffer.len(cx);
|
|
||||||
buffer.edit([(0..len, prompt)], None, cx);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
|
||||||
let settings = ThemeSettings::get_global(cx);
|
|
||||||
let text_style = TextStyle {
|
|
||||||
color: if self.prompt_editor.read(cx).read_only(cx) {
|
|
||||||
cx.theme().colors().text_disabled
|
|
||||||
} else {
|
|
||||||
cx.theme().colors().text
|
|
||||||
},
|
|
||||||
font_family: settings.ui_font.family.clone(),
|
|
||||||
font_features: settings.ui_font.features.clone(),
|
|
||||||
font_size: rems(0.875).into(),
|
|
||||||
font_weight: FontWeight::NORMAL,
|
|
||||||
font_style: FontStyle::Normal,
|
|
||||||
line_height: relative(1.3),
|
|
||||||
background_color: None,
|
|
||||||
underline: None,
|
|
||||||
strikethrough: None,
|
|
||||||
white_space: WhiteSpace::Normal,
|
|
||||||
};
|
|
||||||
EditorElement::new(
|
|
||||||
&self.prompt_editor,
|
|
||||||
EditorStyle {
|
|
||||||
background: cx.theme().colors().editor_background,
|
|
||||||
local_player: cx.theme().players().local(),
|
|
||||||
text: text_style,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PendingInlineAssist {
|
|
||||||
editor: WeakView<Editor>,
|
|
||||||
inline_assistant: Option<(BlockId, View<InlineAssistant>)>,
|
|
||||||
codegen: Model<Codegen>,
|
|
||||||
_subscriptions: Vec<Subscription>,
|
|
||||||
project: WeakModel<Project>,
|
|
||||||
}
|
|
||||||
|
|
||||||
type ToggleFold = Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>;
|
type ToggleFold = Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>;
|
||||||
|
|
||||||
fn render_slash_command_output_toggle(
|
fn render_slash_command_output_toggle(
|
||||||
|
@ -3801,47 +3136,6 @@ fn render_pending_slash_command_gutter_decoration(
|
||||||
icon.into_any_element()
|
icon.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_pending_slash_command_trailer(
|
|
||||||
_row: MultiBufferRow,
|
|
||||||
_confirm_command: Arc<dyn Fn(&mut WindowContext)>,
|
|
||||||
_cx: &mut WindowContext,
|
|
||||||
) -> AnyElement {
|
|
||||||
Empty.into_any()
|
|
||||||
// ButtonLike::new(("run_button", row.0))
|
|
||||||
// .style(ButtonStyle::Filled)
|
|
||||||
// .size(ButtonSize::Compact)
|
|
||||||
// .layer(ElevationIndex::ModalSurface)
|
|
||||||
// .children(
|
|
||||||
// KeyBinding::for_action(&Confirm, cx)
|
|
||||||
// .map(|binding| binding.icon_size(IconSize::XSmall).into_any_element()),
|
|
||||||
// )
|
|
||||||
// .child(Label::new("Run").size(LabelSize::XSmall))
|
|
||||||
// .on_click(move |_, cx| confirm_command(cx))
|
|
||||||
// .into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
|
|
||||||
ranges.sort_unstable_by(|a, b| {
|
|
||||||
a.start
|
|
||||||
.cmp(&b.start, buffer)
|
|
||||||
.then_with(|| b.end.cmp(&a.end, buffer))
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut ix = 0;
|
|
||||||
while ix + 1 < ranges.len() {
|
|
||||||
let b = ranges[ix + 1].clone();
|
|
||||||
let a = &mut ranges[ix];
|
|
||||||
if a.end.cmp(&b.start, buffer).is_gt() {
|
|
||||||
if a.end.cmp(&b.end, buffer).is_lt() {
|
|
||||||
a.end = b.end;
|
|
||||||
}
|
|
||||||
ranges.remove(ix + 1);
|
|
||||||
} else {
|
|
||||||
ix += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_lsp_adapter_delegate(
|
fn make_lsp_adapter_delegate(
|
||||||
project: &Model<Project>,
|
project: &Model<Project>,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
|
|
|
@ -1,704 +0,0 @@
|
||||||
use crate::{
|
|
||||||
streaming_diff::{Hunk, StreamingDiff},
|
|
||||||
CompletionProvider, LanguageModelRequest,
|
|
||||||
};
|
|
||||||
use anyhow::Result;
|
|
||||||
use client::telemetry::Telemetry;
|
|
||||||
use editor::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
|
|
||||||
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
|
|
||||||
use gpui::{EventEmitter, Model, ModelContext, Task};
|
|
||||||
use language::{Rope, TransactionId};
|
|
||||||
use multi_buffer::MultiBufferRow;
|
|
||||||
use std::{cmp, future, ops::Range, sync::Arc, time::Instant};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Event {
|
|
||||||
Finished,
|
|
||||||
Undone,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub enum CodegenKind {
|
|
||||||
Transform { range: Range<Anchor> },
|
|
||||||
Generate { position: Anchor },
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Codegen {
|
|
||||||
buffer: Model<MultiBuffer>,
|
|
||||||
snapshot: MultiBufferSnapshot,
|
|
||||||
kind: CodegenKind,
|
|
||||||
last_equal_ranges: Vec<Range<Anchor>>,
|
|
||||||
transaction_id: Option<TransactionId>,
|
|
||||||
error: Option<anyhow::Error>,
|
|
||||||
generation: Task<()>,
|
|
||||||
idle: bool,
|
|
||||||
telemetry: Option<Arc<Telemetry>>,
|
|
||||||
_subscription: gpui::Subscription,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<Event> for Codegen {}
|
|
||||||
|
|
||||||
impl Codegen {
|
|
||||||
pub fn new(
|
|
||||||
buffer: Model<MultiBuffer>,
|
|
||||||
kind: CodegenKind,
|
|
||||||
telemetry: Option<Arc<Telemetry>>,
|
|
||||||
cx: &mut ModelContext<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let snapshot = buffer.read(cx).snapshot(cx);
|
|
||||||
Self {
|
|
||||||
buffer: buffer.clone(),
|
|
||||||
snapshot,
|
|
||||||
kind,
|
|
||||||
last_equal_ranges: Default::default(),
|
|
||||||
transaction_id: Default::default(),
|
|
||||||
error: Default::default(),
|
|
||||||
idle: true,
|
|
||||||
generation: Task::ready(()),
|
|
||||||
telemetry,
|
|
||||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_buffer_event(
|
|
||||||
&mut self,
|
|
||||||
_buffer: Model<MultiBuffer>,
|
|
||||||
event: &multi_buffer::Event,
|
|
||||||
cx: &mut ModelContext<Self>,
|
|
||||||
) {
|
|
||||||
if let multi_buffer::Event::TransactionUndone { transaction_id } = event {
|
|
||||||
if self.transaction_id == Some(*transaction_id) {
|
|
||||||
self.transaction_id = None;
|
|
||||||
self.generation = Task::ready(());
|
|
||||||
cx.emit(Event::Undone);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn range(&self) -> Range<Anchor> {
|
|
||||||
match &self.kind {
|
|
||||||
CodegenKind::Transform { range } => range.clone(),
|
|
||||||
CodegenKind::Generate { position } => position.bias_left(&self.snapshot)..*position,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn kind(&self) -> &CodegenKind {
|
|
||||||
&self.kind
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn last_equal_ranges(&self) -> &[Range<Anchor>] {
|
|
||||||
&self.last_equal_ranges
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn idle(&self) -> bool {
|
|
||||||
self.idle
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn error(&self) -> Option<&anyhow::Error> {
|
|
||||||
self.error.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
|
|
||||||
let range = self.range();
|
|
||||||
let snapshot = self.snapshot.clone();
|
|
||||||
let selected_text = snapshot
|
|
||||||
.text_for_range(range.start..range.end)
|
|
||||||
.collect::<Rope>();
|
|
||||||
|
|
||||||
let selection_start = range.start.to_point(&snapshot);
|
|
||||||
let suggested_line_indent = snapshot
|
|
||||||
.suggested_indents(selection_start.row..selection_start.row + 1, cx)
|
|
||||||
.into_values()
|
|
||||||
.next()
|
|
||||||
.unwrap_or_else(|| snapshot.indent_size_for_line(MultiBufferRow(selection_start.row)));
|
|
||||||
|
|
||||||
let model_telemetry_id = prompt.model.telemetry_id();
|
|
||||||
let response = CompletionProvider::global(cx).complete(prompt);
|
|
||||||
let telemetry = self.telemetry.clone();
|
|
||||||
self.generation = cx.spawn(|this, mut cx| {
|
|
||||||
async move {
|
|
||||||
let generate = async {
|
|
||||||
let mut edit_start = range.start.to_offset(&snapshot);
|
|
||||||
|
|
||||||
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
|
|
||||||
let diff: Task<anyhow::Result<()>> =
|
|
||||||
cx.background_executor().spawn(async move {
|
|
||||||
let mut response_latency = None;
|
|
||||||
let request_start = Instant::now();
|
|
||||||
let diff = async {
|
|
||||||
let chunks = strip_invalid_spans_from_codeblock(response.await?);
|
|
||||||
futures::pin_mut!(chunks);
|
|
||||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
|
||||||
|
|
||||||
let mut new_text = String::new();
|
|
||||||
let mut base_indent = None;
|
|
||||||
let mut line_indent = None;
|
|
||||||
let mut first_line = true;
|
|
||||||
|
|
||||||
while let Some(chunk) = chunks.next().await {
|
|
||||||
if response_latency.is_none() {
|
|
||||||
response_latency = Some(request_start.elapsed());
|
|
||||||
}
|
|
||||||
let chunk = chunk?;
|
|
||||||
|
|
||||||
let mut lines = chunk.split('\n').peekable();
|
|
||||||
while let Some(line) = lines.next() {
|
|
||||||
new_text.push_str(line);
|
|
||||||
if line_indent.is_none() {
|
|
||||||
if let Some(non_whitespace_ch_ix) =
|
|
||||||
new_text.find(|ch: char| !ch.is_whitespace())
|
|
||||||
{
|
|
||||||
line_indent = Some(non_whitespace_ch_ix);
|
|
||||||
base_indent = base_indent.or(line_indent);
|
|
||||||
|
|
||||||
let line_indent = line_indent.unwrap();
|
|
||||||
let base_indent = base_indent.unwrap();
|
|
||||||
let indent_delta =
|
|
||||||
line_indent as i32 - base_indent as i32;
|
|
||||||
let mut corrected_indent_len = cmp::max(
|
|
||||||
0,
|
|
||||||
suggested_line_indent.len as i32 + indent_delta,
|
|
||||||
)
|
|
||||||
as usize;
|
|
||||||
if first_line {
|
|
||||||
corrected_indent_len = corrected_indent_len
|
|
||||||
.saturating_sub(
|
|
||||||
selection_start.column as usize,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let indent_char = suggested_line_indent.char();
|
|
||||||
let mut indent_buffer = [0; 4];
|
|
||||||
let indent_str =
|
|
||||||
indent_char.encode_utf8(&mut indent_buffer);
|
|
||||||
new_text.replace_range(
|
|
||||||
..line_indent,
|
|
||||||
&indent_str.repeat(corrected_indent_len),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if line_indent.is_some() {
|
|
||||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
|
||||||
new_text.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if lines.peek().is_some() {
|
|
||||||
hunks_tx.send(diff.push_new("\n")).await?;
|
|
||||||
line_indent = None;
|
|
||||||
first_line = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
|
||||||
hunks_tx.send(diff.finish()).await?;
|
|
||||||
|
|
||||||
anyhow::Ok(())
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = diff.await;
|
|
||||||
|
|
||||||
let error_message =
|
|
||||||
result.as_ref().err().map(|error| error.to_string());
|
|
||||||
if let Some(telemetry) = telemetry {
|
|
||||||
telemetry.report_assistant_event(
|
|
||||||
None,
|
|
||||||
telemetry_events::AssistantKind::Inline,
|
|
||||||
model_telemetry_id,
|
|
||||||
response_latency,
|
|
||||||
error_message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
result?;
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
|
|
||||||
while let Some(hunks) = hunks_rx.next().await {
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
this.last_equal_ranges.clear();
|
|
||||||
|
|
||||||
let transaction = this.buffer.update(cx, |buffer, cx| {
|
|
||||||
// Avoid grouping assistant edits with user edits.
|
|
||||||
buffer.finalize_last_transaction(cx);
|
|
||||||
|
|
||||||
buffer.start_transaction(cx);
|
|
||||||
buffer.edit(
|
|
||||||
hunks.into_iter().filter_map(|hunk| match hunk {
|
|
||||||
Hunk::Insert { text } => {
|
|
||||||
let edit_start = snapshot.anchor_after(edit_start);
|
|
||||||
Some((edit_start..edit_start, text))
|
|
||||||
}
|
|
||||||
Hunk::Remove { len } => {
|
|
||||||
let edit_end = edit_start + len;
|
|
||||||
let edit_range = snapshot.anchor_after(edit_start)
|
|
||||||
..snapshot.anchor_before(edit_end);
|
|
||||||
edit_start = edit_end;
|
|
||||||
Some((edit_range, String::new()))
|
|
||||||
}
|
|
||||||
Hunk::Keep { len } => {
|
|
||||||
let edit_end = edit_start + len;
|
|
||||||
let edit_range = snapshot.anchor_after(edit_start)
|
|
||||||
..snapshot.anchor_before(edit_end);
|
|
||||||
edit_start = edit_end;
|
|
||||||
this.last_equal_ranges.push(edit_range);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
buffer.end_transaction(cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(transaction) = transaction {
|
|
||||||
if let Some(first_transaction) = this.transaction_id {
|
|
||||||
// Group all assistant edits into the first transaction.
|
|
||||||
this.buffer.update(cx, |buffer, cx| {
|
|
||||||
buffer.merge_transactions(
|
|
||||||
transaction,
|
|
||||||
first_transaction,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.transaction_id = Some(transaction);
|
|
||||||
this.buffer.update(cx, |buffer, cx| {
|
|
||||||
buffer.finalize_last_transaction(cx)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
diff.await?;
|
|
||||||
|
|
||||||
anyhow::Ok(())
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = generate.await;
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
this.last_equal_ranges.clear();
|
|
||||||
this.idle = true;
|
|
||||||
if let Err(error) = result {
|
|
||||||
this.error = Some(error);
|
|
||||||
}
|
|
||||||
cx.emit(Event::Finished);
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
self.error.take();
|
|
||||||
self.idle = false;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
|
|
||||||
if let Some(transaction_id) = self.transaction_id {
|
|
||||||
self.buffer
|
|
||||||
.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn strip_invalid_spans_from_codeblock(
|
|
||||||
stream: impl Stream<Item = Result<String>>,
|
|
||||||
) -> impl Stream<Item = Result<String>> {
|
|
||||||
let mut first_line = true;
|
|
||||||
let mut buffer = String::new();
|
|
||||||
let mut starts_with_markdown_codeblock = false;
|
|
||||||
let mut includes_start_or_end_span = false;
|
|
||||||
stream.filter_map(move |chunk| {
|
|
||||||
let chunk = match chunk {
|
|
||||||
Ok(chunk) => chunk,
|
|
||||||
Err(err) => return future::ready(Some(Err(err))),
|
|
||||||
};
|
|
||||||
buffer.push_str(&chunk);
|
|
||||||
|
|
||||||
if buffer.len() > "<|S|".len() && buffer.starts_with("<|S|") {
|
|
||||||
includes_start_or_end_span = true;
|
|
||||||
|
|
||||||
buffer = buffer
|
|
||||||
.strip_prefix("<|S|>")
|
|
||||||
.or_else(|| buffer.strip_prefix("<|S|"))
|
|
||||||
.unwrap_or(&buffer)
|
|
||||||
.to_string();
|
|
||||||
} else if buffer.ends_with("|E|>") {
|
|
||||||
includes_start_or_end_span = true;
|
|
||||||
} else if buffer.starts_with("<|")
|
|
||||||
|| buffer.starts_with("<|S")
|
|
||||||
|| buffer.starts_with("<|S|")
|
|
||||||
|| buffer.ends_with('|')
|
|
||||||
|| buffer.ends_with("|E")
|
|
||||||
|| buffer.ends_with("|E|")
|
|
||||||
{
|
|
||||||
return future::ready(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
if first_line {
|
|
||||||
if buffer.is_empty() || buffer == "`" || buffer == "``" {
|
|
||||||
return future::ready(None);
|
|
||||||
} else if buffer.starts_with("```") {
|
|
||||||
starts_with_markdown_codeblock = true;
|
|
||||||
if let Some(newline_ix) = buffer.find('\n') {
|
|
||||||
buffer.replace_range(..newline_ix + 1, "");
|
|
||||||
first_line = false;
|
|
||||||
} else {
|
|
||||||
return future::ready(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut text = buffer.to_string();
|
|
||||||
if starts_with_markdown_codeblock {
|
|
||||||
text = text
|
|
||||||
.strip_suffix("\n```\n")
|
|
||||||
.or_else(|| text.strip_suffix("\n```"))
|
|
||||||
.or_else(|| text.strip_suffix("\n``"))
|
|
||||||
.or_else(|| text.strip_suffix("\n`"))
|
|
||||||
.or_else(|| text.strip_suffix('\n'))
|
|
||||||
.unwrap_or(&text)
|
|
||||||
.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
if includes_start_or_end_span {
|
|
||||||
text = text
|
|
||||||
.strip_suffix("|E|>")
|
|
||||||
.or_else(|| text.strip_suffix("E|>"))
|
|
||||||
.or_else(|| text.strip_prefix("|>"))
|
|
||||||
.or_else(|| text.strip_prefix('>'))
|
|
||||||
.unwrap_or(&text)
|
|
||||||
.to_string();
|
|
||||||
};
|
|
||||||
|
|
||||||
if text.contains('\n') {
|
|
||||||
first_line = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let remainder = buffer.split_off(text.len());
|
|
||||||
let result = if buffer.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(Ok(buffer.clone()))
|
|
||||||
};
|
|
||||||
|
|
||||||
buffer = remainder;
|
|
||||||
future::ready(result)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use crate::FakeCompletionProvider;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use futures::stream::{self};
|
|
||||||
use gpui::{Context, TestAppContext};
|
|
||||||
use indoc::indoc;
|
|
||||||
use language::{
|
|
||||||
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
|
|
||||||
Point,
|
|
||||||
};
|
|
||||||
use rand::prelude::*;
|
|
||||||
use serde::Serialize;
|
|
||||||
use settings::SettingsStore;
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct DummyCompletionRequest {
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
|
||||||
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
|
|
||||||
let provider = FakeCompletionProvider::default();
|
|
||||||
cx.set_global(cx.update(SettingsStore::test));
|
|
||||||
cx.set_global(CompletionProvider::Fake(provider.clone()));
|
|
||||||
cx.update(language_settings::init);
|
|
||||||
|
|
||||||
let text = indoc! {"
|
|
||||||
fn main() {
|
|
||||||
let x = 0;
|
|
||||||
for _ in 0..10 {
|
|
||||||
x += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"};
|
|
||||||
let buffer =
|
|
||||||
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
|
|
||||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
|
||||||
let range = buffer.read_with(cx, |buffer, cx| {
|
|
||||||
let snapshot = buffer.snapshot(cx);
|
|
||||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
|
|
||||||
});
|
|
||||||
let codegen = cx.new_model(|cx| {
|
|
||||||
Codegen::new(buffer.clone(), CodegenKind::Transform { range }, None, cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
let request = LanguageModelRequest::default();
|
|
||||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
|
||||||
|
|
||||||
let mut new_text = concat!(
|
|
||||||
" let mut x = 0;\n",
|
|
||||||
" while x < 10 {\n",
|
|
||||||
" x += 1;\n",
|
|
||||||
" }",
|
|
||||||
);
|
|
||||||
while !new_text.is_empty() {
|
|
||||||
let max_len = cmp::min(new_text.len(), 10);
|
|
||||||
let len = rng.gen_range(1..=max_len);
|
|
||||||
let (chunk, suffix) = new_text.split_at(len);
|
|
||||||
provider.send_completion(chunk.into());
|
|
||||||
new_text = suffix;
|
|
||||||
cx.background_executor.run_until_parked();
|
|
||||||
}
|
|
||||||
provider.finish_completion();
|
|
||||||
cx.background_executor.run_until_parked();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
|
||||||
indoc! {"
|
|
||||||
fn main() {
|
|
||||||
let mut x = 0;
|
|
||||||
while x < 10 {
|
|
||||||
x += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
|
||||||
async fn test_autoindent_when_generating_past_indentation(
|
|
||||||
cx: &mut TestAppContext,
|
|
||||||
mut rng: StdRng,
|
|
||||||
) {
|
|
||||||
let provider = FakeCompletionProvider::default();
|
|
||||||
cx.set_global(CompletionProvider::Fake(provider.clone()));
|
|
||||||
cx.set_global(cx.update(SettingsStore::test));
|
|
||||||
cx.update(language_settings::init);
|
|
||||||
|
|
||||||
let text = indoc! {"
|
|
||||||
fn main() {
|
|
||||||
le
|
|
||||||
}
|
|
||||||
"};
|
|
||||||
let buffer =
|
|
||||||
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
|
|
||||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
|
||||||
let position = buffer.read_with(cx, |buffer, cx| {
|
|
||||||
let snapshot = buffer.snapshot(cx);
|
|
||||||
snapshot.anchor_before(Point::new(1, 6))
|
|
||||||
});
|
|
||||||
let codegen = cx.new_model(|cx| {
|
|
||||||
Codegen::new(buffer.clone(), CodegenKind::Generate { position }, None, cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
let request = LanguageModelRequest::default();
|
|
||||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
|
||||||
|
|
||||||
let mut new_text = concat!(
|
|
||||||
"t mut x = 0;\n",
|
|
||||||
"while x < 10 {\n",
|
|
||||||
" x += 1;\n",
|
|
||||||
"}", //
|
|
||||||
);
|
|
||||||
while !new_text.is_empty() {
|
|
||||||
let max_len = cmp::min(new_text.len(), 10);
|
|
||||||
let len = rng.gen_range(1..=max_len);
|
|
||||||
let (chunk, suffix) = new_text.split_at(len);
|
|
||||||
provider.send_completion(chunk.into());
|
|
||||||
new_text = suffix;
|
|
||||||
cx.background_executor.run_until_parked();
|
|
||||||
}
|
|
||||||
provider.finish_completion();
|
|
||||||
cx.background_executor.run_until_parked();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
|
||||||
indoc! {"
|
|
||||||
fn main() {
|
|
||||||
let mut x = 0;
|
|
||||||
while x < 10 {
|
|
||||||
x += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
|
||||||
async fn test_autoindent_when_generating_before_indentation(
|
|
||||||
cx: &mut TestAppContext,
|
|
||||||
mut rng: StdRng,
|
|
||||||
) {
|
|
||||||
let provider = FakeCompletionProvider::default();
|
|
||||||
cx.set_global(CompletionProvider::Fake(provider.clone()));
|
|
||||||
cx.set_global(cx.update(SettingsStore::test));
|
|
||||||
cx.update(language_settings::init);
|
|
||||||
|
|
||||||
let text = concat!(
|
|
||||||
"fn main() {\n",
|
|
||||||
" \n",
|
|
||||||
"}\n" //
|
|
||||||
);
|
|
||||||
let buffer =
|
|
||||||
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
|
|
||||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
|
||||||
let position = buffer.read_with(cx, |buffer, cx| {
|
|
||||||
let snapshot = buffer.snapshot(cx);
|
|
||||||
snapshot.anchor_before(Point::new(1, 2))
|
|
||||||
});
|
|
||||||
let codegen = cx.new_model(|cx| {
|
|
||||||
Codegen::new(buffer.clone(), CodegenKind::Generate { position }, None, cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
let request = LanguageModelRequest::default();
|
|
||||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
|
||||||
|
|
||||||
let mut new_text = concat!(
|
|
||||||
"let mut x = 0;\n",
|
|
||||||
"while x < 10 {\n",
|
|
||||||
" x += 1;\n",
|
|
||||||
"}", //
|
|
||||||
);
|
|
||||||
while !new_text.is_empty() {
|
|
||||||
let max_len = cmp::min(new_text.len(), 10);
|
|
||||||
let len = rng.gen_range(1..=max_len);
|
|
||||||
let (chunk, suffix) = new_text.split_at(len);
|
|
||||||
provider.send_completion(chunk.into());
|
|
||||||
new_text = suffix;
|
|
||||||
cx.background_executor.run_until_parked();
|
|
||||||
}
|
|
||||||
provider.finish_completion();
|
|
||||||
cx.background_executor.run_until_parked();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
|
||||||
indoc! {"
|
|
||||||
fn main() {
|
|
||||||
let mut x = 0;
|
|
||||||
while x < 10 {
|
|
||||||
x += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_strip_invalid_spans_from_codeblock() {
|
|
||||||
assert_eq!(
|
|
||||||
strip_invalid_spans_from_codeblock(chunks("Lorem ipsum dolor", 2))
|
|
||||||
.map(|chunk| chunk.unwrap())
|
|
||||||
.collect::<String>()
|
|
||||||
.await,
|
|
||||||
"Lorem ipsum dolor"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor", 2))
|
|
||||||
.map(|chunk| chunk.unwrap())
|
|
||||||
.collect::<String>()
|
|
||||||
.await,
|
|
||||||
"Lorem ipsum dolor"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```", 2))
|
|
||||||
.map(|chunk| chunk.unwrap())
|
|
||||||
.collect::<String>()
|
|
||||||
.await,
|
|
||||||
"Lorem ipsum dolor"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2))
|
|
||||||
.map(|chunk| chunk.unwrap())
|
|
||||||
.collect::<String>()
|
|
||||||
.await,
|
|
||||||
"Lorem ipsum dolor"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
strip_invalid_spans_from_codeblock(chunks(
|
|
||||||
"```html\n```js\nLorem ipsum dolor\n```\n```",
|
|
||||||
2
|
|
||||||
))
|
|
||||||
.map(|chunk| chunk.unwrap())
|
|
||||||
.collect::<String>()
|
|
||||||
.await,
|
|
||||||
"```js\nLorem ipsum dolor\n```"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
strip_invalid_spans_from_codeblock(chunks("``\nLorem ipsum dolor\n```", 2))
|
|
||||||
.map(|chunk| chunk.unwrap())
|
|
||||||
.collect::<String>()
|
|
||||||
.await,
|
|
||||||
"``\nLorem ipsum dolor\n```"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
strip_invalid_spans_from_codeblock(chunks("<|S|Lorem ipsum|E|>", 2))
|
|
||||||
.map(|chunk| chunk.unwrap())
|
|
||||||
.collect::<String>()
|
|
||||||
.await,
|
|
||||||
"Lorem ipsum"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
strip_invalid_spans_from_codeblock(chunks("<|S|>Lorem ipsum", 2))
|
|
||||||
.map(|chunk| chunk.unwrap())
|
|
||||||
.collect::<String>()
|
|
||||||
.await,
|
|
||||||
"Lorem ipsum"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
strip_invalid_spans_from_codeblock(chunks("```\n<|S|>Lorem ipsum\n```", 2))
|
|
||||||
.map(|chunk| chunk.unwrap())
|
|
||||||
.collect::<String>()
|
|
||||||
.await,
|
|
||||||
"Lorem ipsum"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
strip_invalid_spans_from_codeblock(chunks("```\n<|S|Lorem ipsum|E|>\n```", 2))
|
|
||||||
.map(|chunk| chunk.unwrap())
|
|
||||||
.collect::<String>()
|
|
||||||
.await,
|
|
||||||
"Lorem ipsum"
|
|
||||||
);
|
|
||||||
fn chunks(text: &str, size: usize) -> impl Stream<Item = Result<String>> {
|
|
||||||
stream::iter(
|
|
||||||
text.chars()
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.chunks(size)
|
|
||||||
.map(|chunk| Ok(chunk.iter().collect::<String>()))
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rust_lang() -> Language {
|
|
||||||
Language::new(
|
|
||||||
LanguageConfig {
|
|
||||||
name: "Rust".into(),
|
|
||||||
matcher: LanguageMatcher {
|
|
||||||
path_suffixes: vec!["rs".to_string()],
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
Some(tree_sitter_rust::language()),
|
|
||||||
)
|
|
||||||
.with_indents_query(
|
|
||||||
r#"
|
|
||||||
(call_expression) @indent
|
|
||||||
(field_expression) @indent
|
|
||||||
(_ "(" ")" @end) @indent
|
|
||||||
(_ "{" "}" @end) @indent
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
1449
crates/assistant/src/inline_assistant.rs
Normal file
1449
crates/assistant/src/inline_assistant.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
slash_command::SlashCommandCompletionProvider, CompletionProvider, LanguageModelRequest,
|
slash_command::SlashCommandCompletionProvider, AssistantPanel, CompletionProvider,
|
||||||
LanguageModelRequestMessage, Role,
|
InlineAssist, InlineAssistant, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use assistant_slash_command::SlashCommandRegistry;
|
use assistant_slash_command::SlashCommandRegistry;
|
||||||
|
@ -14,8 +14,8 @@ use futures::{
|
||||||
use fuzzy::StringMatchCandidate;
|
use fuzzy::StringMatchCandidate;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, point, size, AnyElement, AppContext, BackgroundExecutor, Bounds, DevicePixels,
|
actions, point, size, AnyElement, AppContext, BackgroundExecutor, Bounds, DevicePixels,
|
||||||
EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TitlebarOptions, View,
|
EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TitlebarOptions,
|
||||||
WindowBounds, WindowHandle, WindowOptions,
|
UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
|
||||||
};
|
};
|
||||||
use heed::{types::SerdeBincode, Database, RoTxn};
|
use heed::{types::SerdeBincode, Database, RoTxn};
|
||||||
use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
|
use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
|
||||||
|
@ -35,6 +35,7 @@ use ui::{
|
||||||
};
|
};
|
||||||
use util::{paths::PROMPTS_DIR, ResultExt, TryFutureExt};
|
use util::{paths::PROMPTS_DIR, ResultExt, TryFutureExt};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
prompt_library,
|
prompt_library,
|
||||||
|
@ -603,6 +604,49 @@ impl PromptLibrary {
|
||||||
self.picker.update(cx, |picker, cx| picker.focus(cx));
|
self.picker.update(cx, |picker, cx| picker.focus(cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn inline_assist(&mut self, _: &InlineAssist, cx: &mut ViewContext<Self>) {
|
||||||
|
let Some(active_prompt_id) = self.active_prompt_id else {
|
||||||
|
cx.propagate();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let prompt_editor = &self.prompt_editors[&active_prompt_id].editor;
|
||||||
|
let provider = CompletionProvider::global(cx);
|
||||||
|
if provider.is_authenticated() {
|
||||||
|
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||||
|
assistant.assist(&prompt_editor, None, false, cx)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
for window in cx.windows() {
|
||||||
|
if let Some(workspace) = window.downcast::<Workspace>() {
|
||||||
|
let panel = workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
cx.activate_window();
|
||||||
|
workspace.focus_panel::<AssistantPanel>(cx)
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
.flatten();
|
||||||
|
if panel.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancel_last_inline_assist(
|
||||||
|
&mut self,
|
||||||
|
_: &editor::actions::Cancel,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
let canceled = InlineAssistant::update_global(cx, |assistant, cx| {
|
||||||
|
assistant.cancel_last_inline_assist(cx)
|
||||||
|
});
|
||||||
|
if !canceled {
|
||||||
|
cx.propagate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_prompt_editor_event(
|
fn handle_prompt_editor_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
prompt_id: PromptId,
|
prompt_id: PromptId,
|
||||||
|
@ -732,6 +776,8 @@ impl PromptLibrary {
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.on_action(cx.listener(Self::focus_picker))
|
.on_action(cx.listener(Self::focus_picker))
|
||||||
|
.on_action(cx.listener(Self::inline_assist))
|
||||||
|
.on_action(cx.listener(Self::cancel_last_inline_assist))
|
||||||
.flex_grow()
|
.flex_grow()
|
||||||
.h_full()
|
.h_full()
|
||||||
.pt(Spacing::Large.rems(cx))
|
.pt(Spacing::Large.rems(cx))
|
||||||
|
|
|
@ -1036,6 +1036,37 @@ impl<'a> WindowContext<'a> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Subscribe to events emitted by a model or view.
|
||||||
|
/// The entity to which you're subscribing must implement the [`EventEmitter`] trait.
|
||||||
|
/// The callback will be invoked a handle to the emitting entity (either a [`View`] or [`Model`]), the event, and a window context for the current window.
|
||||||
|
pub fn observe<E, T>(
|
||||||
|
&mut self,
|
||||||
|
entity: &E,
|
||||||
|
mut on_notify: impl FnMut(E, &mut WindowContext<'_>) + 'static,
|
||||||
|
) -> Subscription
|
||||||
|
where
|
||||||
|
E: Entity<T>,
|
||||||
|
{
|
||||||
|
let entity_id = entity.entity_id();
|
||||||
|
let entity = entity.downgrade();
|
||||||
|
let window_handle = self.window.handle;
|
||||||
|
self.app.new_observer(
|
||||||
|
entity_id,
|
||||||
|
Box::new(move |cx| {
|
||||||
|
window_handle
|
||||||
|
.update(cx, |_, cx| {
|
||||||
|
if let Some(handle) = E::upgrade_from(&entity) {
|
||||||
|
on_notify(handle, cx);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Subscribe to events emitted by a model or view.
|
/// Subscribe to events emitted by a model or view.
|
||||||
/// The entity to which you're subscribing must implement the [`EventEmitter`] trait.
|
/// The entity to which you're subscribing must implement the [`EventEmitter`] trait.
|
||||||
/// The callback will be invoked a handle to the emitting entity (either a [`View`] or [`Model`]), the event, and a window context for the current window.
|
/// The callback will be invoked a handle to the emitting entity (either a [`View`] or [`Model`]), the event, and a window context for the current window.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue