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 238e64ed14f7778ce5f0c1ffa1544c0c5ea26b41..7283870858cfdd54b5f55eda644e0f2aff02c8b5 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 9c129cb1ac7373f1b8c17847767b7f2b8eeb9251..fa75d0ce1ba88b3144385d63bf7d8890239a5f90 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 863eb72fb319119e524b75ed25431aca1ceb44fc..9df62261a0f21fb25609a5b0ca15791771f9684e 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 568ea223cc10e7037414e5ca4b18d15f18779830..504f12c57482b34267b202d6a0017b34ea3bf83a 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 e27100863732b68607e6e6dce0c14ea008bb3298..0f717a0edfbfdd0296cbb1ed839cbfc0f19f4267 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 | 80 ++++++++++++++++------------ crates/collab_ui/src/channel_view.rs | 11 ++-- crates/collab_ui/src/collab_ui.rs | 32 ++++------- 3 files changed, 61 insertions(+), 62 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 4db298fe98eb5718f1d641dd6539d2005f90ff4b..6eb50d37e51fc4df3c22aba76ee68af8a7bb8e24 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(&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: 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_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 c6f32cecd271fea3e61101c4cc0d8104ad2329d3..e46d31ac199d70e12bd644b8923183dadfbf53b0 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 3dca2ec76da0b4ebccdfc415d4a821b01ea21110..84a9b3b6b6427a9b0383e59657e265d0963afd10 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 | 123 +++----- crates/assistant/src/prompts.rs | 378 ++++++++++++++++++++++++ 3 files changed, 412 insertions(+), 90 deletions(-) create mode 100644 crates/assistant/src/prompts.rs diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 258684db47096bac2d3df33d0289462dbc841214..6c9b14333e34cbf5fd49d8299ba7bd891b607526 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 42e5fb78979a6b8136c5c60d29e38e064df3435d..55a1dfe0f6f28e6d745a7d077869a6ae8e9ce8bf 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 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 range = pending_assist.codegen.read(cx).range(); - let selected_text = snapshot.text_for_range(range.clone()).collect::(); + 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(range.start); + 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 0000000000000000000000000000000000000000..272ae9eac686914faf6b2c0c007f59ac9ff9f77d --- /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 55a1dfe0f6f28e6d745a7d077869a6ae8e9ce8bf..37d0d729fe64e0def057402fb9a24b796ebdf317 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 272ae9eac686914faf6b2c0c007f59ac9ff9f77d..00db4c1302e61e776d94adbd27f622b38dff3711 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 00db4c1302e61e776d94adbd27f622b38dff3711..04cf5d089be4d145f9d0e45481e392fb8b415840 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 ae04e6d9033cf280395493604818d7b4fc097e13..ff34fea917fbd1bef401104c9c09c11cdcc48ba4 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 7e0be4d097d3d41cb87930f73d387ca6a86cb7bd..d648e83f8f1acbfe013ab411be89ca1bcd5380fe 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 3e4acd57e7cb747be66e9bbb9009ec6ae5c96d43..ecb9d79a6c47f4011550049282616ca68c6193d3 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