From 342a00b89e4ee37a0937fb3c1887eac84ba3acf2 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 26 Sep 2023 10:49:55 -0400 Subject: [PATCH 01/14] Remove `dbg!` from `styleable_helpers!` (#3035) This PR removes a leftover `dbg!` from `styleable_helpers!`. We already removed this in the `gpui2-ui` branch, but getting this on `main` since @KCaverly pointed it out. Release Notes: - N/A --- crates/gpui2_macros/src/styleable_helpers.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/gpui2_macros/src/styleable_helpers.rs b/crates/gpui2_macros/src/styleable_helpers.rs index 238e64ed14..7283870858 100644 --- a/crates/gpui2_macros/src/styleable_helpers.rs +++ b/crates/gpui2_macros/src/styleable_helpers.rs @@ -135,10 +135,6 @@ fn generate_predefined_setter( } }; - if negate { - dbg!(method.to_string()); - } - method } From 36f022bb587bac5b31a627a816acfaceb14dc9a9 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 26 Sep 2023 18:40:41 +0200 Subject: [PATCH 02/14] project_replace: Fix up key bindings (#3034) Release Notes: - N/A --- assets/keymaps/default.json | 9 ++++++- crates/search/src/project_search.rs | 37 ++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 9c129cb1ac..fa75d0ce1b 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -262,6 +262,13 @@ "down": "search::NextHistoryQuery" } }, + { + "context": "ProjectSearchBar && in_replace", + "bindings": { + "enter": "search::ReplaceNext", + "cmd-enter": "search::ReplaceAll" + } + }, { "context": "ProjectSearchView", "bindings": { @@ -563,7 +570,7 @@ } }, { - "context": "ProjectSearchBar", + "context": "ProjectSearchBar && !in_replace", "bindings": { "cmd-enter": "project_search::SearchInNew" } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 863eb72fb3..9df62261a0 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -60,7 +60,7 @@ pub fn init(cx: &mut AppContext) { cx.set_global(ActiveSettings::default()); cx.add_action(ProjectSearchView::deploy); cx.add_action(ProjectSearchView::move_focus_to_results); - cx.add_action(ProjectSearchBar::search); + cx.add_action(ProjectSearchBar::confirm); cx.add_action(ProjectSearchBar::search_in_new); cx.add_action(ProjectSearchBar::select_next_match); cx.add_action(ProjectSearchBar::select_prev_match); @@ -1371,9 +1371,18 @@ impl ProjectSearchBar { }) } } - fn search(&mut self, _: &Confirm, cx: &mut ViewContext) { + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { + let mut should_propagate = true; if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| search_view.search(cx)); + search_view.update(cx, |search_view, cx| { + if !search_view.replacement_editor.is_focused(cx) { + should_propagate = false; + search_view.search(cx); + } + }); + } + if should_propagate { + cx.propagate_action(); } } @@ -1678,6 +1687,28 @@ impl View for ProjectSearchBar { "ProjectSearchBar" } + fn update_keymap_context( + &self, + keymap: &mut gpui::keymap_matcher::KeymapContext, + cx: &AppContext, + ) { + Self::reset_to_default_keymap_context(keymap); + let in_replace = self + .active_project_search + .as_ref() + .map(|search| { + search + .read(cx) + .replacement_editor + .read_with(cx, |_, cx| cx.is_self_focused()) + }) + .flatten() + .unwrap_or(false); + if in_replace { + keymap.add_identifier("in_replace"); + } + } + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { if let Some(_search) = self.active_project_search.as_ref() { let search = _search.read(cx); From 8c47f117dbb9485413939c2ba57d76b484a9051b Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 26 Sep 2023 19:21:15 +0200 Subject: [PATCH 03/14] editor: Start transaction in replace impl (#3036) This fixes the undo with replace in project /cc @maxbrunsfeld Release Notes: - N/A --- crates/editor/src/items.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 568ea223cc..504f12c574 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -996,7 +996,9 @@ impl SearchableItem for Editor { }; if let Some(replacement) = query.replacement_for(&text) { - self.edit([(identifier.clone(), Arc::from(&*replacement))], cx); + self.transact(cx, |this, cx| { + this.edit([(identifier.clone(), Arc::from(&*replacement))], cx); + }); } } fn match_index_for_direction( From e263805847b96d9c8f36b9adb9ddcbee43704eec Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 26 Sep 2023 19:35:10 +0200 Subject: [PATCH 04/14] workspace: change save prompt for unnamed buffers (#3037) Release Notes: - N/A --- crates/workspace/src/pane.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index e271008637..0f717a0edf 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2213,7 +2213,7 @@ fn dirty_message_for(buffer_path: Option) -> String { let path = buffer_path .as_ref() .and_then(|p| p.path.to_str()) - .unwrap_or(&"Untitled buffer"); + .unwrap_or(&"This buffer"); let path = truncate_and_remove_front(path, 80); format!("{path} contains unsaved edits. Do you want to save it?") } From 0897ed561f9acf83cda69d3790c83d58b01d6be2 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 26 Sep 2023 14:18:32 -0400 Subject: [PATCH 05/14] Rework call events api There were time when events with bad data were being emitted. What we found was that places where certain collaboration-related code could fail, like sending an, would still send events, and those events be in a bad state, as certain elements weren't constructed as expected, thus missing in the event. The new API guarantees that we have data in the correct configuration. In the future, we will add events for certain types of failures within Zed. Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com> --- crates/call/src/call.rs | 82 ++++++++++++++++------------ crates/collab_ui/src/channel_view.rs | 11 +--- crates/collab_ui/src/collab_ui.rs | 32 ++++------- 3 files changed, 62 insertions(+), 63 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 4db298fe98..6eb50d37e5 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -206,9 +206,14 @@ impl ActiveCall { cx.spawn(|this, mut cx| async move { let result = invite.await; + if result.is_ok() { + this.update(&mut cx, |this, cx| this.report_call_event("invite", cx)); + } else { + // TODO: Resport collaboration error + } + this.update(&mut cx, |this, cx| { this.pending_invites.remove(&called_user_id); - this.report_call_event("invite", cx); cx.notify(); }); result @@ -273,13 +278,7 @@ impl ActiveCall { .borrow_mut() .take() .ok_or_else(|| anyhow!("no incoming call"))?; - Self::report_call_event_for_room( - "decline incoming", - Some(call.room_id), - None, - &self.client, - cx, - ); + report_call_event_for_room("decline incoming", call.room_id, None, &self.client, cx); self.client.send(proto::DeclineCall { room_id: call.room_id, })?; @@ -409,31 +408,46 @@ impl ActiveCall { &self.pending_invites } - fn report_call_event(&self, operation: &'static str, cx: &AppContext) { - let (room_id, channel_id) = match self.room() { - Some(room) => { - let room = room.read(cx); - (Some(room.id()), room.channel_id()) - } - None => (None, None), - }; - Self::report_call_event_for_room(operation, room_id, channel_id, &self.client, cx) - } - - pub fn report_call_event_for_room( - operation: &'static str, - room_id: Option, - channel_id: Option, - client: &Arc, - cx: &AppContext, - ) { - let telemetry = client.telemetry(); - let telemetry_settings = *settings::get::(cx); - let event = ClickhouseEvent::Call { - operation, - room_id, - channel_id, - }; - telemetry.report_clickhouse_event(event, telemetry_settings); + pub fn report_call_event(&self, operation: &'static str, cx: &AppContext) { + if let Some(room) = self.room() { + let room = room.read(cx); + report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client, cx); + } } } + +pub fn report_call_event_for_room( + operation: &'static str, + room_id: u64, + channel_id: Option, + client: &Arc, + cx: &AppContext, +) { + let telemetry = client.telemetry(); + let telemetry_settings = *settings::get::(cx); + let event = ClickhouseEvent::Call { + operation, + room_id: Some(room_id), + channel_id, + }; + telemetry.report_clickhouse_event(event, telemetry_settings); +} + +pub fn report_call_event_for_channel( + operation: &'static str, + channel_id: u64, + client: &Arc, + cx: &AppContext, +) { + let room = ActiveCall::global(cx).read(cx).room(); + + let telemetry = client.telemetry(); + let telemetry_settings = *settings::get::(cx); + + let event = ClickhouseEvent::Call { + operation, + room_id: room.map(|r| r.read(cx).id()), + channel_id: Some(channel_id), + }; + telemetry.report_clickhouse_event(event, telemetry_settings); +} diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index c6f32cecd2..e46d31ac19 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, Result}; -use call::ActiveCall; +use call::report_call_event_for_channel; use channel::{ChannelBuffer, ChannelBufferEvent, ChannelId}; use client::proto; use clock::ReplicaId; @@ -42,14 +42,9 @@ impl ChannelView { cx.spawn(|mut cx| async move { let channel_view = channel_view.await?; pane.update(&mut cx, |pane, cx| { - let room_id = ActiveCall::global(cx) - .read(cx) - .room() - .map(|room| room.read(cx).id()); - ActiveCall::report_call_event_for_room( + report_call_event_for_channel( "open channel notes", - room_id, - Some(channel_id), + channel_id, &workspace.read(cx).app_state().client, cx, ); diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 3dca2ec76d..84a9b3b6b6 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -10,7 +10,7 @@ mod panel_settings; mod project_shared_notification; mod sharing_status_indicator; -use call::{ActiveCall, Room}; +use call::{report_call_event_for_room, ActiveCall, Room}; use gpui::{ actions, geometry::{ @@ -55,18 +55,18 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { let client = call.client(); let toggle_screen_sharing = room.update(cx, |room, cx| { if room.is_screen_sharing() { - ActiveCall::report_call_event_for_room( + report_call_event_for_room( "disable screen share", - Some(room.id()), + room.id(), room.channel_id(), &client, cx, ); Task::ready(room.unshare_screen(cx)) } else { - ActiveCall::report_call_event_for_room( + report_call_event_for_room( "enable screen share", - Some(room.id()), + room.id(), room.channel_id(), &client, cx, @@ -83,23 +83,13 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) { if let Some(room) = call.room().cloned() { let client = call.client(); room.update(cx, |room, cx| { - if room.is_muted(cx) { - ActiveCall::report_call_event_for_room( - "enable microphone", - Some(room.id()), - room.channel_id(), - &client, - cx, - ); + let operation = if room.is_muted(cx) { + "enable microphone" } else { - ActiveCall::report_call_event_for_room( - "disable microphone", - Some(room.id()), - room.channel_id(), - &client, - cx, - ); - } + "disable microphone" + }; + report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx); + room.toggle_mute(cx) }) .map(|task| task.detach_and_log_err(cx)) From 54c63063e483969ee937417ac3c4eb3805f5cdf8 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 26 Sep 2023 16:23:48 -0400 Subject: [PATCH 06/14] changed inline assist generate prompt to leverage outline as opposed to entire prior file Co-Authored-by: Antonio --- crates/assistant/src/assistant.rs | 1 + crates/assistant/src/assistant_panel.rs | 125 +++----- crates/assistant/src/prompts.rs | 378 ++++++++++++++++++++++++ 3 files changed, 413 insertions(+), 91 deletions(-) create mode 100644 crates/assistant/src/prompts.rs diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 258684db47..6c9b14333e 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -1,6 +1,7 @@ pub mod assistant_panel; mod assistant_settings; mod codegen; +mod prompts; mod streaming_diff; use ai::completion::Role; diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 42e5fb7897..55a1dfe0f6 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1,6 +1,7 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, codegen::{self, Codegen, CodegenKind}, + prompts::generate_content_prompt, MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata, SavedMessage, }; @@ -541,11 +542,31 @@ impl AssistantPanel { self.inline_prompt_history.pop_front(); } - let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); - let range = pending_assist.codegen.read(cx).range(); - let selected_text = snapshot.text_for_range(range.clone()).collect::(); + let multi_buffer = editor.read(cx).buffer().read(cx); + let multi_buffer_snapshot = multi_buffer.snapshot(cx); + let snapshot = if multi_buffer.all_buffers().len() > 1 { + return; + } else { + multi_buffer + .all_buffers() + .iter() + .next() + .unwrap() + .read(cx) + .snapshot() + }; - let language = snapshot.language_at(range.start); + let range = pending_assist.codegen.read(cx).range(); + let language_range = snapshot.anchor_at( + range.start.to_offset(&multi_buffer_snapshot), + language::Bias::Left, + ) + ..snapshot.anchor_at( + range.end.to_offset(&multi_buffer_snapshot), + language::Bias::Right, + ); + + let language = snapshot.language_at(language_range.start); let language_name = if let Some(language) = language.as_ref() { if Arc::ptr_eq(language, &language::PLAIN_TEXT) { None @@ -557,93 +578,15 @@ impl AssistantPanel { }; let language_name = language_name.as_deref(); - let mut prompt = String::new(); - if let Some(language_name) = language_name { - writeln!(prompt, "You're an expert {language_name} engineer.").unwrap(); - } - match pending_assist.codegen.read(cx).kind() { - CodegenKind::Transform { .. } => { - writeln!( - prompt, - "You're currently working inside an editor on this file:" - ) - .unwrap(); - if let Some(language_name) = language_name { - writeln!(prompt, "```{language_name}").unwrap(); - } else { - writeln!(prompt, "```").unwrap(); - } - for chunk in snapshot.text_for_range(Anchor::min()..Anchor::max()) { - write!(prompt, "{chunk}").unwrap(); - } - writeln!(prompt, "```").unwrap(); - - writeln!( - prompt, - "In particular, the user has selected the following text:" - ) - .unwrap(); - if let Some(language_name) = language_name { - writeln!(prompt, "```{language_name}").unwrap(); - } else { - writeln!(prompt, "```").unwrap(); - } - writeln!(prompt, "{selected_text}").unwrap(); - writeln!(prompt, "```").unwrap(); - writeln!(prompt).unwrap(); - writeln!( - prompt, - "Modify the selected text given the user prompt: {user_prompt}" - ) - .unwrap(); - writeln!( - prompt, - "You MUST reply only with the edited selected text, not the entire file." - ) - .unwrap(); - } - CodegenKind::Generate { .. } => { - writeln!( - prompt, - "You're currently working inside an editor on this file:" - ) - .unwrap(); - if let Some(language_name) = language_name { - writeln!(prompt, "```{language_name}").unwrap(); - } else { - writeln!(prompt, "```").unwrap(); - } - for chunk in snapshot.text_for_range(Anchor::min()..range.start) { - write!(prompt, "{chunk}").unwrap(); - } - write!(prompt, "<|>").unwrap(); - for chunk in snapshot.text_for_range(range.start..Anchor::max()) { - write!(prompt, "{chunk}").unwrap(); - } - writeln!(prompt).unwrap(); - writeln!(prompt, "```").unwrap(); - writeln!( - prompt, - "Assume the cursor is located where the `<|>` marker is." - ) - .unwrap(); - writeln!( - prompt, - "Text can't be replaced, so assume your answer will be inserted at the cursor." - ) - .unwrap(); - writeln!( - prompt, - "Complete the text given the user prompt: {user_prompt}" - ) - .unwrap(); - } - } - if let Some(language_name) = language_name { - writeln!(prompt, "Your answer MUST always be valid {language_name}.").unwrap(); - } - writeln!(prompt, "Always wrap your response in a Markdown codeblock.").unwrap(); - writeln!(prompt, "Never make remarks about the output.").unwrap(); + let codegen_kind = pending_assist.codegen.read(cx).kind().clone(); + let prompt = generate_content_prompt( + user_prompt.to_string(), + language_name, + &snapshot, + language_range, + cx, + codegen_kind, + ); let mut messages = Vec::new(); let mut model = settings::get::(cx) diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs new file mode 100644 index 0000000000..272ae9eac6 --- /dev/null +++ b/crates/assistant/src/prompts.rs @@ -0,0 +1,378 @@ +use gpui::AppContext; +use language::{BufferSnapshot, OffsetRangeExt, ToOffset}; +use std::cmp; +use std::ops::Range; +use std::{fmt::Write, iter}; + +use crate::codegen::CodegenKind; + +fn outline_for_prompt( + buffer: &BufferSnapshot, + range: Range, + cx: &AppContext, +) -> Option { + let indent = buffer + .language_indent_size_at(0, cx) + .chars() + .collect::(); + let outline = buffer.outline(None)?; + let range = range.to_offset(buffer); + + let mut text = String::new(); + let mut items = outline.items.into_iter().peekable(); + + let mut intersected = false; + let mut intersection_indent = 0; + let mut extended_range = range.clone(); + + while let Some(item) = items.next() { + let item_range = item.range.to_offset(buffer); + if item_range.end < range.start || item_range.start > range.end { + text.extend(iter::repeat(indent.as_str()).take(item.depth)); + text.push_str(&item.text); + text.push('\n'); + } else { + intersected = true; + let is_terminal = items + .peek() + .map_or(true, |next_item| next_item.depth <= item.depth); + if is_terminal { + if item_range.start <= extended_range.start { + extended_range.start = item_range.start; + intersection_indent = item.depth; + } + extended_range.end = cmp::max(extended_range.end, item_range.end); + } else { + let name_start = item_range.start + item.name_ranges.first().unwrap().start; + let name_end = item_range.start + item.name_ranges.last().unwrap().end; + + if range.start > name_end { + text.extend(iter::repeat(indent.as_str()).take(item.depth)); + text.push_str(&item.text); + text.push('\n'); + } else { + if name_start <= extended_range.start { + extended_range.start = item_range.start; + intersection_indent = item.depth; + } + extended_range.end = cmp::max(extended_range.end, name_end); + } + } + } + + if intersected + && items.peek().map_or(true, |next_item| { + next_item.range.start.to_offset(buffer) > range.end + }) + { + intersected = false; + text.extend(iter::repeat(indent.as_str()).take(intersection_indent)); + text.extend(buffer.text_for_range(extended_range.start..range.start)); + text.push_str("<|"); + text.extend(buffer.text_for_range(range.clone())); + text.push_str("|>"); + text.extend(buffer.text_for_range(range.end..extended_range.end)); + text.push('\n'); + } + } + + Some(text) +} + +pub fn generate_content_prompt( + user_prompt: String, + language_name: Option<&str>, + buffer: &BufferSnapshot, + range: Range, + cx: &AppContext, + kind: CodegenKind, +) -> String { + let mut prompt = String::new(); + + // General Preamble + if let Some(language_name) = language_name { + writeln!(prompt, "You're an expert {language_name} engineer.\n").unwrap(); + } else { + writeln!(prompt, "You're an expert engineer.\n").unwrap(); + } + + let outline = outline_for_prompt(buffer, range.clone(), cx); + if let Some(outline) = outline { + writeln!( + prompt, + "The file you are currently working on has the following outline:" + ) + .unwrap(); + if let Some(language_name) = language_name { + let language_name = language_name.to_lowercase(); + writeln!(prompt, "```{language_name}\n{outline}\n```").unwrap(); + } else { + writeln!(prompt, "```\n{outline}\n```").unwrap(); + } + } + + // Assume for now that we are just generating + if range.clone().start == range.end { + writeln!(prompt, "In particular, the user's cursor is current on the '<||>' span in the above outline, with no text selected.").unwrap(); + } else { + writeln!(prompt, "In particular, the user has selected a section of the text between the '<|' and '|>' spans.").unwrap(); + } + + match kind { + CodegenKind::Generate { position } => { + writeln!( + prompt, + "Assume the cursor is located where the `<|` marker is." + ) + .unwrap(); + writeln!( + prompt, + "Text can't be replaced, so assume your answer will be inserted at the cursor." + ) + .unwrap(); + writeln!( + prompt, + "Generate text based on the users prompt: {user_prompt}" + ) + .unwrap(); + } + CodegenKind::Transform { range } => { + writeln!( + prompt, + "Modify the users code selected text based upon the users prompt: {user_prompt}" + ) + .unwrap(); + writeln!( + prompt, + "You MUST reply with only the adjusted code, not the entire file." + ) + .unwrap(); + } + } + + if let Some(language_name) = language_name { + writeln!(prompt, "Your answer MUST always be valid {language_name}").unwrap(); + } + writeln!(prompt, "Always wrap your response in a Markdown codeblock").unwrap(); + writeln!(prompt, "Never make remarks about the output.").unwrap(); + + prompt +} + +#[cfg(test)] +pub(crate) mod tests { + + use super::*; + use std::sync::Arc; + + use gpui::AppContext; + use indoc::indoc; + use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; + use settings::SettingsStore; + + pub(crate) fn rust_lang() -> Language { + Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_indents_query( + r#" + (call_expression) @indent + (field_expression) @indent + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap() + .with_outline_query( + r#" + (struct_item + "struct" @context + name: (_) @name) @item + (enum_item + "enum" @context + name: (_) @name) @item + (enum_variant + name: (_) @name) @item + (field_declaration + name: (_) @name) @item + (impl_item + "impl" @context + trait: (_)? @name + "for"? @context + type: (_) @name) @item + (function_item + "fn" @context + name: (_) @name) @item + (mod_item + "mod" @context + name: (_) @name) @item + "#, + ) + .unwrap() + } + + #[gpui::test] + fn test_outline_for_prompt(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + language_settings::init(cx); + let text = indoc! {" + struct X { + a: usize, + b: usize, + } + + impl X { + + fn new() -> Self { + let a = 1; + let b = 2; + Self { a, b } + } + + pub fn a(&self, param: bool) -> usize { + self.a + } + + pub fn b(&self) -> usize { + self.b + } + } + "}; + let buffer = + cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let snapshot = buffer.read(cx).snapshot(); + + let outline = outline_for_prompt( + &snapshot, + snapshot.anchor_before(Point::new(1, 4))..snapshot.anchor_before(Point::new(1, 4)), + cx, + ); + assert_eq!( + outline.as_deref(), + Some(indoc! {" + struct X + <||>a: usize + b + impl X + fn new + fn a + fn b + "}) + ); + + let outline = outline_for_prompt( + &snapshot, + snapshot.anchor_before(Point::new(8, 12))..snapshot.anchor_before(Point::new(8, 14)), + cx, + ); + assert_eq!( + outline.as_deref(), + Some(indoc! {" + struct X + a + b + impl X + fn new() -> Self { + let <|a |>= 1; + let b = 2; + Self { a, b } + } + fn a + fn b + "}) + ); + + let outline = outline_for_prompt( + &snapshot, + snapshot.anchor_before(Point::new(6, 0))..snapshot.anchor_before(Point::new(6, 0)), + cx, + ); + assert_eq!( + outline.as_deref(), + Some(indoc! {" + struct X + a + b + impl X + <||> + fn new + fn a + fn b + "}) + ); + + let outline = outline_for_prompt( + &snapshot, + snapshot.anchor_before(Point::new(8, 12))..snapshot.anchor_before(Point::new(13, 9)), + cx, + ); + assert_eq!( + outline.as_deref(), + Some(indoc! {" + struct X + a + b + impl X + fn new() -> Self { + let <|a = 1; + let b = 2; + Self { a, b } + } + + pub f|>n a(&self, param: bool) -> usize { + self.a + } + fn b + "}) + ); + + let outline = outline_for_prompt( + &snapshot, + snapshot.anchor_before(Point::new(5, 6))..snapshot.anchor_before(Point::new(12, 0)), + cx, + ); + assert_eq!( + outline.as_deref(), + Some(indoc! {" + struct X + a + b + impl X<| { + + fn new() -> Self { + let a = 1; + let b = 2; + Self { a, b } + } + |> + fn a + fn b + "}) + ); + + let outline = outline_for_prompt( + &snapshot, + snapshot.anchor_before(Point::new(18, 8))..snapshot.anchor_before(Point::new(18, 8)), + cx, + ); + assert_eq!( + outline.as_deref(), + Some(indoc! {" + struct X + a + b + impl X + fn new + fn a + pub fn b(&self) -> usize { + <||>self.b + } + "}) + ); + } +} From e8dd412ac12372f2e1f47ae1499c54595ba7f34d Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 26 Sep 2023 17:10:31 -0400 Subject: [PATCH 07/14] update inline generate prompt to leverage more explicit <|START| and |END|> spans --- crates/assistant/src/assistant_panel.rs | 14 +++++--------- crates/assistant/src/prompts.rs | 16 ++++++++++------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 55a1dfe0f6..37d0d729fe 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -544,16 +544,10 @@ impl AssistantPanel { let multi_buffer = editor.read(cx).buffer().read(cx); let multi_buffer_snapshot = multi_buffer.snapshot(cx); - let snapshot = if multi_buffer.all_buffers().len() > 1 { - return; + let snapshot = if multi_buffer.is_singleton() { + multi_buffer.as_singleton().unwrap().read(cx).snapshot() } else { - multi_buffer - .all_buffers() - .iter() - .next() - .unwrap() - .read(cx) - .snapshot() + return; }; let range = pending_assist.codegen.read(cx).range(); @@ -588,6 +582,8 @@ impl AssistantPanel { codegen_kind, ); + dbg!(&prompt); + let mut messages = Vec::new(); let mut model = settings::get::(cx) .default_open_ai_model diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index 272ae9eac6..00db4c1302 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -68,9 +68,13 @@ fn outline_for_prompt( intersected = false; text.extend(iter::repeat(indent.as_str()).take(intersection_indent)); text.extend(buffer.text_for_range(extended_range.start..range.start)); - text.push_str("<|"); + text.push_str("<|START|"); text.extend(buffer.text_for_range(range.clone())); - text.push_str("|>"); + if range.start != range.end { + text.push_str("|END|>"); + } else { + text.push_str(">"); + } text.extend(buffer.text_for_range(range.end..extended_range.end)); text.push('\n'); } @@ -113,16 +117,16 @@ pub fn generate_content_prompt( // Assume for now that we are just generating if range.clone().start == range.end { - writeln!(prompt, "In particular, the user's cursor is current on the '<||>' span in the above outline, with no text selected.").unwrap(); + writeln!(prompt, "In particular, the user's cursor is current on the '<|START|>' span in the above outline, with no text selected.").unwrap(); } else { - writeln!(prompt, "In particular, the user has selected a section of the text between the '<|' and '|>' spans.").unwrap(); + writeln!(prompt, "In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.").unwrap(); } match kind { CodegenKind::Generate { position } => { writeln!( prompt, - "Assume the cursor is located where the `<|` marker is." + "Assume the cursor is located where the `<|START|` marker is." ) .unwrap(); writeln!( @@ -144,7 +148,7 @@ pub fn generate_content_prompt( .unwrap(); writeln!( prompt, - "You MUST reply with only the adjusted code, not the entire file." + "You MUST reply with only the adjusted code (within the '<|START|' and '|END|>' spans), not the entire file." ) .unwrap(); } From 90f17d4a28c716f13b5e6325a2bec3786bf9bd3f Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 26 Sep 2023 17:11:20 -0400 Subject: [PATCH 08/14] updated codegen match to leverage unused values --- crates/assistant/src/prompts.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index 00db4c1302..04cf5d089b 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -123,7 +123,7 @@ pub fn generate_content_prompt( } match kind { - CodegenKind::Generate { position } => { + CodegenKind::Generate { position: _ } => { writeln!( prompt, "Assume the cursor is located where the `<|START|` marker is." @@ -140,7 +140,7 @@ pub fn generate_content_prompt( ) .unwrap(); } - CodegenKind::Transform { range } => { + CodegenKind::Transform { range: _ } => { writeln!( prompt, "Modify the users code selected text based upon the users prompt: {user_prompt}" From 7e2cef98a7cfc9c71962b540e607a200f9bdcb6a Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 26 Sep 2023 19:20:33 +0300 Subject: [PATCH 09/14] Hide inlay hints toggle if they are not supported by the current editor --- crates/editor/src/editor.rs | 23 ++++++++ .../quick_action_bar/src/quick_action_bar.rs | 52 +++++++++++-------- 2 files changed, 53 insertions(+), 22 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ae04e6d903..ff34fea917 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8588,6 +8588,29 @@ impl Editor { self.handle_input(text, cx); } + + pub fn supports_inlay_hints(&self, cx: &AppContext) -> bool { + let Some(project) = self.project.as_ref() else { + return false; + }; + let project = project.read(cx); + + let mut supports = false; + self.buffer().read(cx).for_each_buffer(|buffer| { + if !supports { + supports = project + .language_servers_for_buffer(buffer.read(cx), cx) + .any( + |(_, server)| match server.capabilities().inlay_hint_provider { + Some(lsp::OneOf::Left(enabled)) => enabled, + Some(lsp::OneOf::Right(_)) => true, + None => false, + }, + ) + } + }); + supports + } } fn inlay_hint_settings( diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 7e0be4d097..d648e83f8f 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -48,24 +48,26 @@ impl View for QuickActionBar { return Empty::new().into_any(); }; - let inlay_hints_enabled = editor.read(cx).inlay_hints_enabled(); - let mut bar = Flex::row().with_child(render_quick_action_bar_button( - 0, - "icons/inlay_hint.svg", - inlay_hints_enabled, - ( - "Toggle Inlay Hints".to_string(), - Some(Box::new(editor::ToggleInlayHints)), - ), - cx, - |this, cx| { - if let Some(editor) = this.active_editor() { - editor.update(cx, |editor, cx| { - editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx); - }); - } - }, - )); + let mut bar = Flex::row(); + if editor.read(cx).supports_inlay_hints(cx) { + bar = bar.with_child(render_quick_action_bar_button( + 0, + "icons/inlay_hint.svg", + editor.read(cx).inlay_hints_enabled(), + ( + "Toggle Inlay Hints".to_string(), + Some(Box::new(editor::ToggleInlayHints)), + ), + cx, + |this, cx| { + if let Some(editor) = this.active_editor() { + editor.update(cx, |editor, cx| { + editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx); + }); + } + }, + )); + } if editor.read(cx).buffer().read(cx).is_singleton() { let search_bar_shown = !self.buffer_search_bar.read(cx).is_dismissed(); @@ -163,12 +165,18 @@ impl ToolbarItemView for QuickActionBar { if let Some(editor) = active_item.downcast::() { let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled(); + let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx); self._inlay_hints_enabled_subscription = Some(cx.observe(&editor, move |_, editor, cx| { - let new_inlay_hints_enabled = editor.read(cx).inlay_hints_enabled(); - if inlay_hints_enabled != new_inlay_hints_enabled { - inlay_hints_enabled = new_inlay_hints_enabled; - cx.notify(); + let editor = editor.read(cx); + let new_inlay_hints_enabled = editor.inlay_hints_enabled(); + let new_supports_inlay_hints = editor.supports_inlay_hints(cx); + let should_notify = inlay_hints_enabled != new_inlay_hints_enabled + || supports_inlay_hints != new_supports_inlay_hints; + inlay_hints_enabled = new_inlay_hints_enabled; + supports_inlay_hints = new_supports_inlay_hints; + if should_notify { + cx.notify() } })); ToolbarItemLocation::PrimaryRight { flex: None } From 568fec0f5431ad47d65fb426cf5cda18555c29dd Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 26 Sep 2023 18:15:41 -0400 Subject: [PATCH 10/14] Add `Sized` bound to `StyleHelpers` (#3042) This PR adds a `Sized` bound to the `StyleHelpers` trait. All of the individual methods on this trait already had a `Self: Sized` bound, so moving it up to the trait level will make it so we don't have to repeat ourselves so much. There's an open question of whether we can hoist the `Sized` bound to `Styleable`, but it's possible there are cases where we'd want to have a `Styleable` trait object. Release Notes: - N/A --- crates/gpui2/src/style.rs | 147 ++++++++------------------------------ 1 file changed, 30 insertions(+), 117 deletions(-) diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 3e4acd57e7..ecb9d79a6c 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -320,174 +320,114 @@ use crate as gpui2; // // Example: // // Sets the padding to 0.5rem, just like class="p-2" in Tailwind. -// fn p_2(mut self) -> Self where Self: Sized; -pub trait StyleHelpers: Styleable