From 890443241de465827c7200491816ce0cadc01005 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 25 Jun 2024 11:43:30 -0400 Subject: [PATCH] Prompt Library Refinements (#13470) TODO: - [x] Moving the cursor out of the title editor should unselect any selected text Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra Co-authored-by: Richard --- assets/icons/book.svg | 1 + assets/icons/book_copy.svg | 1 + assets/icons/book_plus.svg | 1 + crates/assistant/src/prompt_library.rs | 469 +++++++++++------- crates/editor/src/editor.rs | 60 ++- crates/editor/src/element.rs | 101 +++- crates/editor/src/scroll.rs | 2 +- crates/editor/src/scroll/actions.rs | 2 +- crates/ui/src/clickable.rs | 4 +- crates/ui/src/components/button/button.rs | 5 + .../ui/src/components/button/button_like.rs | 9 +- .../ui/src/components/button/icon_button.rs | 5 + .../ui/src/components/button/toggle_button.rs | 5 + crates/ui/src/components/disclosure.rs | 9 +- crates/ui/src/components/icon.rs | 6 + 15 files changed, 454 insertions(+), 226 deletions(-) create mode 100644 assets/icons/book.svg create mode 100644 assets/icons/book_copy.svg create mode 100644 assets/icons/book_plus.svg diff --git a/assets/icons/book.svg b/assets/icons/book.svg new file mode 100644 index 0000000000..d30f81f32e --- /dev/null +++ b/assets/icons/book.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/book_copy.svg b/assets/icons/book_copy.svg new file mode 100644 index 0000000000..b055d47b5f --- /dev/null +++ b/assets/icons/book_copy.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/book_plus.svg b/assets/icons/book_plus.svg new file mode 100644 index 0000000000..2868f07cd0 --- /dev/null +++ b/assets/icons/book_plus.svg @@ -0,0 +1 @@ + diff --git a/crates/assistant/src/prompt_library.rs b/crates/assistant/src/prompt_library.rs index 6d87e383ce..6ae7767340 100644 --- a/crates/assistant/src/prompt_library.rs +++ b/crates/assistant/src/prompt_library.rs @@ -6,16 +6,16 @@ use anyhow::{anyhow, Result}; use assistant_slash_command::SlashCommandRegistry; use chrono::{DateTime, Utc}; use collections::HashMap; -use editor::{actions::Tab, CurrentLineHighlight, Editor, EditorEvent}; +use editor::{actions::Tab, CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle}; use futures::{ future::{self, BoxFuture, Shared}, FutureExt, }; use fuzzy::StringMatchCandidate; use gpui::{ - actions, percentage, point, size, Animation, AnimationExt, AppContext, BackgroundExecutor, - Bounds, EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TitlebarOptions, - Transformation, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions, + actions, point, size, transparent_black, AppContext, BackgroundExecutor, Bounds, EventEmitter, + Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle, + TitlebarOptions, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions, }; use heed::{types::SerdeBincode, Database, RoTxn}; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry}; @@ -109,12 +109,13 @@ pub struct PromptLibrary { } struct PromptEditor { - editor: View, + title_editor: View, + body_editor: View, token_count: Option, pending_token_count: Task>, - next_body_to_save: Option, + next_title_and_body_to_save: Option<(String, Rope)>, pending_save: Option>>, - _subscription: Subscription, + _subscriptions: Vec, } struct PromptPickerDelegate { @@ -345,7 +346,8 @@ impl PromptLibrary { let prompt_metadata = self.store.metadata(prompt_id).unwrap(); let prompt_editor = self.prompt_editors.get_mut(&prompt_id).unwrap(); - let body = prompt_editor.editor.update(cx, |editor, cx| { + let title = prompt_editor.title_editor.read(cx).text(cx); + let body = prompt_editor.body_editor.update(cx, |editor, cx| { editor .buffer() .read(cx) @@ -359,20 +361,24 @@ impl PromptLibrary { let store = self.store.clone(); let executor = cx.background_executor().clone(); - prompt_editor.next_body_to_save = Some(body); + prompt_editor.next_title_and_body_to_save = Some((title, body)); if prompt_editor.pending_save.is_none() { prompt_editor.pending_save = Some(cx.spawn(|this, mut cx| { async move { loop { - let next_body_to_save = this.update(&mut cx, |this, _| { + let title_and_body = this.update(&mut cx, |this, _| { this.prompt_editors .get_mut(&prompt_id)? - .next_body_to_save + .next_title_and_body_to_save .take() })?; - if let Some(body) = next_body_to_save { - let title = title_from_body(body.chars_at(0)); + if let Some((title, body)) = title_and_body { + let title = if title.trim().is_empty() { + None + } else { + Some(SharedString::from(title)) + }; store .save(prompt_id, title, prompt_metadata.default, body) .await @@ -425,11 +431,11 @@ impl PromptLibrary { if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) { if focus { prompt_editor - .editor + .body_editor .update(cx, |editor, cx| editor.focus(cx)); } self.set_active_prompt(Some(prompt_id), cx); - } else { + } else if let Some(prompt_metadata) = self.store.metadata(prompt_id) { let language_registry = self.language_registry.clone(); let commands = SlashCommandRegistry::global(cx); let prompt = self.store.load(prompt_id); @@ -438,13 +444,20 @@ impl PromptLibrary { let markdown = language_registry.language_for_name("Markdown").await; this.update(&mut cx, |this, cx| match prompt { Ok(prompt) => { - let buffer = cx.new_model(|cx| { - let mut buffer = Buffer::local(prompt, cx); - buffer.set_language(markdown.log_err(), cx); - buffer.set_language_registry(language_registry); - buffer + let title_editor = cx.new_view(|cx| { + let mut editor = Editor::auto_width(cx); + editor.set_placeholder_text("Untitled", cx); + editor.set_text(prompt_metadata.title.unwrap_or_default(), cx); + editor }); - let editor = cx.new_view(|cx| { + let body_editor = cx.new_view(|cx| { + let buffer = cx.new_model(|cx| { + let mut buffer = Buffer::local(prompt, cx); + buffer.set_language(markdown.log_err(), cx); + buffer.set_language_registry(language_registry); + buffer + }); + let mut editor = Editor::for_buffer(buffer, None, cx); editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); editor.set_show_gutter(false, cx); @@ -460,19 +473,24 @@ impl PromptLibrary { } editor }); - let _subscription = - cx.subscribe(&editor, move |this, _editor, event, cx| { - this.handle_prompt_editor_event(prompt_id, event, cx) - }); + let _subscriptions = vec![ + cx.subscribe(&title_editor, move |this, editor, event, cx| { + this.handle_prompt_title_editor_event(prompt_id, editor, event, cx) + }), + cx.subscribe(&body_editor, move |this, editor, event, cx| { + this.handle_prompt_body_editor_event(prompt_id, editor, event, cx) + }), + ]; this.prompt_editors.insert( prompt_id, PromptEditor { - editor, - next_body_to_save: None, + title_editor, + body_editor, + next_title_and_body_to_save: None, pending_save: None, token_count: None, pending_token_count: Task::ready(None), - _subscription, + _subscriptions, }, ); this.set_active_prompt(Some(prompt_id), cx); @@ -549,7 +567,7 @@ impl PromptLibrary { fn focus_active_prompt(&mut self, _: &Tab, cx: &mut ViewContext) { if let Some(active_prompt) = self.active_prompt_id { self.prompt_editors[&active_prompt] - .editor + .body_editor .update(cx, |editor, cx| editor.focus(cx)); cx.stop_propagation(); } @@ -565,7 +583,7 @@ impl PromptLibrary { return; }; - let prompt_editor = &self.prompt_editors[&active_prompt_id].editor; + let prompt_editor = &self.prompt_editors[&active_prompt_id].body_editor; let provider = CompletionProvider::global(cx); if provider.is_authenticated() { InlineAssistant::update_global(cx, |assistant, cx| { @@ -589,50 +607,73 @@ impl PromptLibrary { } } - fn handle_prompt_editor_event( + fn move_down_from_title(&mut self, _: &editor::actions::MoveDown, cx: &mut ViewContext) { + if let Some(prompt_id) = self.active_prompt_id { + if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) { + cx.focus_view(&prompt_editor.body_editor); + } + } + } + + fn move_up_from_body(&mut self, _: &editor::actions::MoveUp, cx: &mut ViewContext) { + if let Some(prompt_id) = self.active_prompt_id { + if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) { + cx.focus_view(&prompt_editor.title_editor); + } + } + } + + fn handle_prompt_title_editor_event( &mut self, prompt_id: PromptId, + title_editor: View, event: &EditorEvent, cx: &mut ViewContext, ) { - if let EditorEvent::BufferEdited = event { - let prompt_editor = self.prompt_editors.get(&prompt_id).unwrap(); - let buffer = prompt_editor - .editor - .read(cx) - .buffer() - .read(cx) - .as_singleton() - .unwrap(); + match event { + EditorEvent::BufferEdited => { + self.save_prompt(prompt_id, cx); + self.count_tokens(prompt_id, cx); + } + EditorEvent::Blurred => { + title_editor.update(cx, |title_editor, cx| { + title_editor.change_selections(None, cx, |selections| { + let cursor = selections.oldest_anchor().head(); + selections.select_anchor_ranges([cursor..cursor]); + }); + }); + } + _ => {} + } + } - buffer.update(cx, |buffer, cx| { - let mut chars = buffer.chars_at(0); - match chars.next() { - Some('#') => { - if chars.next() != Some(' ') { - drop(chars); - buffer.edit([(1..1, " ")], None, cx); - } - } - Some(' ') => { - drop(chars); - buffer.edit([(0..0, "#")], None, cx); - } - _ => { - drop(chars); - buffer.edit([(0..0, "# ")], None, cx); - } - } - }); - - self.save_prompt(prompt_id, cx); - self.count_tokens(prompt_id, cx); + fn handle_prompt_body_editor_event( + &mut self, + prompt_id: PromptId, + body_editor: View, + event: &EditorEvent, + cx: &mut ViewContext, + ) { + match event { + EditorEvent::BufferEdited => { + self.save_prompt(prompt_id, cx); + self.count_tokens(prompt_id, cx); + } + EditorEvent::Blurred => { + body_editor.update(cx, |body_editor, cx| { + body_editor.change_selections(None, cx, |selections| { + let cursor = selections.oldest_anchor().head(); + selections.select_anchor_ranges([cursor..cursor]); + }); + }); + } + _ => {} } } fn count_tokens(&mut self, prompt_id: PromptId, cx: &mut ViewContext) { if let Some(prompt) = self.prompt_editors.get_mut(&prompt_id) { - let editor = &prompt.editor.read(cx); + let editor = &prompt.body_editor.read(cx); let buffer = &editor.buffer().read(cx).as_singleton().unwrap().read(cx); let body = buffer.as_rope().clone(); prompt.pending_token_count = cx.spawn(|this, mut cx| { @@ -708,122 +749,209 @@ impl PromptLibrary { .flex_none() .min_w_64() .children(self.active_prompt_id.and_then(|prompt_id| { - let buffer_font = ThemeSettings::get_global(cx).buffer_font.family.clone(); let prompt_metadata = self.store.metadata(prompt_id)?; let prompt_editor = &self.prompt_editors[&prompt_id]; - let focus_handle = prompt_editor.editor.focus_handle(cx); + let focus_handle = prompt_editor.body_editor.focus_handle(cx); let current_model = CompletionProvider::global(cx).model(); - let token_count = prompt_editor.token_count.map(|count| count.to_string()); + let settings = ThemeSettings::get_global(cx); Some( - h_flex() + v_flex() .id("prompt-editor-inner") .size_full() - .items_start() + .relative() + .overflow_hidden() + .pl(Spacing::XXLarge.rems(cx)) + .pt(Spacing::Large.rems(cx)) .on_click(cx.listener(move |_, _, cx| { cx.focus(&focus_handle); })) .child( - div() - .on_action(cx.listener(Self::focus_picker)) - .on_action(cx.listener(Self::inline_assist)) - .flex_grow() - .h_full() - .pt(Spacing::XXLarge.rems(cx)) - .pl(Spacing::XXLarge.rems(cx)) - .child(prompt_editor.editor.clone()), - ) - .child( - v_flex() - .w_12() - .py(Spacing::Large.rems(cx)) - .justify_start() - .items_end() - .gap_1() - .child(h_flex().h_8().font_family(buffer_font).when_some_else( - token_count, - |tokens_ready, token_count| { - tokens_ready.pr_3().justify_end().child( - // This isn't actually a button, it just let's us easily add - // a tooltip to the token count. - Button::new("token_count", token_count.clone()) - .style(ButtonStyle::Transparent) - .color(Color::Muted) - .tooltip(move |cx| { - Tooltip::with_meta( - format!("{} tokens", token_count,), - None, - format!( - "Model: {}", - current_model.display_name() - ), - cx, - ) - }), - ) - }, - |tokens_loading| { - tokens_loading.w_12().justify_center().child( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .color(Color::Muted) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(4)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate( - percentage(delta), - )) - }, - ), - ) - }, - )) + h_flex() + .group("active-editor-header") + .pr(Spacing::XXLarge.rems(cx)) + .pt(Spacing::XSmall.rems(cx)) + .pb(Spacing::Large.rems(cx)) + .justify_between() .child( - h_flex().justify_center().w_12().h_8().child( - IconButton::new("toggle-default-prompt", IconName::Sparkle) - .style(ButtonStyle::Transparent) - .selected(prompt_metadata.default) - .selected_icon(IconName::SparkleFilled) - .icon_color(if prompt_metadata.default { - Color::Accent - } else { - Color::Muted - }) - .shape(IconButtonShape::Square) - .tooltip(move |cx| { - Tooltip::text( - if prompt_metadata.default { - "Remove from Default Prompt" - } else { - "Add to Default Prompt" - }, - cx, + h_flex().gap_1().child( + div() + .max_w_80() + .on_action(cx.listener(Self::move_down_from_title)) + .border_1() + .border_color(transparent_black()) + .rounded_md() + .group_hover("active-editor-header", |this| { + this.border_color( + cx.theme().colors().border_variant, ) }) - .on_click(|_, cx| { - cx.dispatch_action(Box::new(ToggleDefaultPrompt)); - }), + .child(EditorElement::new( + &prompt_editor.title_editor, + EditorStyle { + background: cx.theme().system().transparent, + local_player: cx.theme().players().local(), + text: TextStyle { + color: cx + .theme() + .colors() + .editor_foreground, + font_family: settings + .ui_font + .family + .clone(), + font_features: settings + .ui_font + .features + .clone(), + font_size: HeadlineSize::Large + .size() + .into(), + font_weight: settings.ui_font.weight, + line_height: relative( + settings.buffer_line_height.value(), + ), + ..Default::default() + }, + scrollbar_width: Pixels::ZERO, + syntax: cx.theme().syntax().clone(), + status: cx.theme().status().clone(), + inlay_hints_style: HighlightStyle { + color: Some(cx.theme().status().hint), + ..HighlightStyle::default() + }, + suggestions_style: HighlightStyle { + color: Some(cx.theme().status().predictive), + ..HighlightStyle::default() + }, + }, + )), ), ) .child( - h_flex().justify_center().w_12().h_8().child( - IconButton::new("delete-prompt", IconName::Trash) - .size(ButtonSize::Large) - .style(ButtonStyle::Transparent) - .shape(IconButtonShape::Square) - .tooltip(move |cx| { - Tooltip::for_action( - "Delete Prompt", - &DeletePrompt, - cx, + h_flex() + .h_full() + .child( + h_flex() + .h_full() + .gap(Spacing::XXLarge.rems(cx)) + .child(div()), + ) + .child( + h_flex() + .h_full() + .gap(Spacing::XXLarge.rems(cx)) + .child( + IconButton::new( + "delete-prompt", + IconName::Trash, + ) + .size(ButtonSize::Large) + .style(ButtonStyle::Transparent) + .shape(IconButtonShape::Square) + .size(ButtonSize::Large) + .tooltip(move |cx| { + Tooltip::for_action( + "Delete Prompt", + &DeletePrompt, + cx, + ) + }) + .on_click(|_, cx| { + cx.dispatch_action(Box::new(DeletePrompt)); + }), ) - }) - .on_click(|_, cx| { - cx.dispatch_action(Box::new(DeletePrompt)); - }), - ), + // .child( + // IconButton::new( + // "duplicate-prompt", + // IconName::BookCopy, + // ) + // .size(ButtonSize::Large) + // .style(ButtonStyle::Transparent) + // .shape(IconButtonShape::Square) + // .size(ButtonSize::Large) + // .tooltip(move |cx| { + // Tooltip::for_action( + // "Duplicate Prompt", + // &gpui::NoAction, + // cx, + // ) + // }) + // .disabled(true), + // ) + .child( + IconButton::new( + "toggle-default-prompt", + IconName::Sparkle, + ) + .style(ButtonStyle::Transparent) + .selected(prompt_metadata.default) + .selected_icon(IconName::SparkleFilled) + .icon_color(if prompt_metadata.default { + Color::Accent + } else { + Color::Muted + }) + .shape(IconButtonShape::Square) + .size(ButtonSize::Large) + .tooltip(move |cx| { + Tooltip::text( + if prompt_metadata.default { + "Remove from Default Prompt" + } else { + "Add to Default Prompt" + }, + cx, + ) + }) + .on_click(|_, cx| { + cx.dispatch_action(Box::new( + ToggleDefaultPrompt, + )); + }), + ), + ), ), + ) + .child( + div() + .on_action(cx.listener(Self::focus_picker)) + .on_action(cx.listener(Self::inline_assist)) + .on_action(cx.listener(Self::move_up_from_body)) + .flex_grow() + .h_full() + .child(prompt_editor.body_editor.clone()) + .children(prompt_editor.token_count.map(|token_count| { + let token_count: SharedString = token_count.to_string().into(); + let label_token_count: SharedString = + token_count.to_string().into(); + + h_flex() + .id("token_count") + .absolute() + .bottom_1() + .right_4() + .flex_initial() + .px_2() + .py_1() + .tooltip(move |cx| { + let token_count = token_count.clone(); + + Tooltip::with_meta( + format!("{} tokens", token_count.clone()), + None, + format!("Model: {}", current_model.display_name()), + cx, + ) + }) + .child( + Label::new(format!( + "{} tokens", + label_token_count.clone() + )) + .color(Color::Muted), + ) + })), ), ) })) @@ -1115,24 +1243,3 @@ pub struct GlobalPromptStore( ); impl Global for GlobalPromptStore {} - -fn title_from_body(body: impl IntoIterator) -> Option { - let mut chars = body.into_iter().take_while(|c| *c != '\n').peekable(); - - let mut level = 0; - while let Some('#') = chars.peek() { - level += 1; - chars.next(); - } - - if level > 0 { - let title = chars.collect::().trim().to_string(); - if title.is_empty() { - None - } else { - Some(title.into()) - } - } else { - None - } -} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ffda62eecd..7672299112 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -335,7 +335,7 @@ pub enum SelectMode { #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum EditorMode { - SingleLine, + SingleLine { auto_width: bool }, AutoHeight { max_lines: usize }, Full, } @@ -1580,7 +1580,13 @@ impl Editor { pub fn single_line(cx: &mut ViewContext) -> Self { let buffer = cx.new_model(|cx| Buffer::local("", cx)); let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new(EditorMode::SingleLine, buffer, None, false, cx) + Self::new( + EditorMode::SingleLine { auto_width: false }, + buffer, + None, + false, + cx, + ) } pub fn multi_line(cx: &mut ViewContext) -> Self { @@ -1589,6 +1595,18 @@ impl Editor { Self::new(EditorMode::Full, buffer, None, false, cx) } + pub fn auto_width(cx: &mut ViewContext) -> Self { + let buffer = cx.new_model(|cx| Buffer::local("", cx)); + let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new( + EditorMode::SingleLine { auto_width: true }, + buffer, + None, + false, + cx, + ) + } + pub fn auto_height(max_lines: usize, cx: &mut ViewContext) -> Self { let buffer = cx.new_model(|cx| Buffer::local("", cx)); let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); @@ -1701,8 +1719,8 @@ impl Editor { let blink_manager = cx.new_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); - let soft_wrap_mode_override = - (mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::PreferLine); + let soft_wrap_mode_override = matches!(mode, EditorMode::SingleLine { .. }) + .then(|| language_settings::SoftWrap::PreferLine); let mut project_subscriptions = Vec::new(); if mode == EditorMode::Full { @@ -1749,7 +1767,7 @@ impl Editor { .detach(); cx.on_blur(&focus_handle, Self::handle_blur).detach(); - let show_indent_guides = if mode == EditorMode::SingleLine { + let show_indent_guides = if matches!(mode, EditorMode::SingleLine { .. }) { Some(false) } else { None @@ -1905,7 +1923,7 @@ impl Editor { let mut key_context = KeyContext::new_with_defaults(); key_context.add("Editor"); let mode = match self.mode { - EditorMode::SingleLine => "single_line", + EditorMode::SingleLine { .. } => "single_line", EditorMode::AutoHeight { .. } => "auto_height", EditorMode::Full => "full", }; @@ -6660,7 +6678,7 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -6697,7 +6715,7 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -6728,7 +6746,7 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -6791,7 +6809,7 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -6839,7 +6857,7 @@ impl Editor { pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { self.take_rename(true, cx); - if self.mode == EditorMode::SingleLine { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -6900,7 +6918,7 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -7248,7 +7266,7 @@ impl Editor { _: &MoveToStartOfParagraph, cx: &mut ViewContext, ) { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -7268,7 +7286,7 @@ impl Editor { _: &MoveToEndOfParagraph, cx: &mut ViewContext, ) { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -7288,7 +7306,7 @@ impl Editor { _: &SelectToStartOfParagraph, cx: &mut ViewContext, ) { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -7308,7 +7326,7 @@ impl Editor { _: &SelectToEndOfParagraph, cx: &mut ViewContext, ) { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -7324,7 +7342,7 @@ impl Editor { } pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext) { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -7344,7 +7362,7 @@ impl Editor { } pub fn move_to_end(&mut self, _: &MoveToEnd, cx: &mut ViewContext) { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -8203,7 +8221,7 @@ impl Editor { let advance_downwards = action.advance_downwards && selections_on_single_row && !selections_selecting - && this.mode != EditorMode::SingleLine; + && !matches!(this.mode, EditorMode::SingleLine { .. }); if advance_downwards { let snapshot = this.buffer.read(cx).snapshot(cx); @@ -12079,7 +12097,7 @@ impl Render for Editor { let settings = ThemeSettings::get_global(cx); let text_style = match self.mode { - EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle { + EditorMode::SingleLine { .. } | EditorMode::AutoHeight { .. } => TextStyle { color: cx.theme().colors().editor_foreground, font_family: settings.ui_font.family.clone(), font_features: settings.ui_font.features.clone(), @@ -12108,7 +12126,7 @@ impl Render for Editor { }; let background = match self.mode { - EditorMode::SingleLine => cx.theme().system().transparent, + EditorMode::SingleLine { .. } => cx.theme().system().transparent, EditorMode::AutoHeight { max_lines: _ } => cx.theme().system().transparent, EditorMode::Full => cx.theme().colors().editor_background, }; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 17b0a4ae58..4b21088b24 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1831,10 +1831,10 @@ impl EditorElement { } fn layout_lines( - &self, rows: Range, line_number_layouts: &[Option], snapshot: &EditorSnapshot, + style: &EditorStyle, cx: &mut WindowContext, ) -> Vec { if rows.start >= rows.end { @@ -1843,7 +1843,7 @@ impl EditorElement { // Show the placeholder when the editor is empty if snapshot.is_empty() { - let font_size = self.style.text.font_size.to_pixels(cx.rem_size()); + let font_size = style.text.font_size.to_pixels(cx.rem_size()); let placeholder_color = cx.theme().colors().text_placeholder; let placeholder_text = snapshot.placeholder_text(); @@ -1858,7 +1858,7 @@ impl EditorElement { .filter_map(move |line| { let run = TextRun { len: line.len(), - font: self.style.text.font(), + font: style.text.font(), color: placeholder_color, background_color: None, underline: Default::default(), @@ -1877,10 +1877,10 @@ impl EditorElement { }) .collect() } else { - let chunks = snapshot.highlighted_chunks(rows.clone(), true, &self.style); + let chunks = snapshot.highlighted_chunks(rows.clone(), true, style); LineWithInvisibles::from_chunks( chunks, - &self.style.text, + &style.text, MAX_LINE_LEN, rows.len(), line_number_layouts, @@ -4475,7 +4475,7 @@ impl EditorElement { // We currently use single-line and auto-height editors in UI contexts, // so we don't want to scale everything with the buffer font size, as it // ends up looking off. - EditorMode::SingleLine | EditorMode::AutoHeight { .. } => None, + EditorMode::SingleLine { .. } | EditorMode::AutoHeight { .. } => None, } } } @@ -4499,12 +4499,43 @@ impl Element for EditorElement { editor.set_style(self.style.clone(), cx); let layout_id = match editor.mode { - EditorMode::SingleLine => { + EditorMode::SingleLine { auto_width } => { let rem_size = cx.rem_size(); - let mut style = Style::default(); - style.size.width = relative(1.).into(); - style.size.height = self.style.text.line_height_in_pixels(rem_size).into(); - cx.request_layout(style, None) + + let height = self.style.text.line_height_in_pixels(rem_size); + if auto_width { + let editor_handle = cx.view().clone(); + let style = self.style.clone(); + cx.request_measured_layout(Style::default(), move |_, _, cx| { + let editor_snapshot = + editor_handle.update(cx, |editor, cx| editor.snapshot(cx)); + let line = Self::layout_lines( + DisplayRow(0)..DisplayRow(1), + &[], + &editor_snapshot, + &style, + cx, + ) + .pop() + .unwrap(); + + let font_id = cx.text_system().resolve_font(&style.text.font()); + let font_size = style.text.font_size.to_pixels(cx.rem_size()); + let em_width = cx + .text_system() + .typographic_bounds(font_id, font_size, 'm') + .unwrap() + .size + .width; + + size(line.width + em_width, height) + }) + } else { + let mut style = Style::default(); + style.size.height = height.into(); + style.size.width = relative(1.).into(); + cx.request_layout(style, None) + } } EditorMode::AutoHeight { max_lines } => { let editor_handle = cx.view().clone(); @@ -4763,8 +4794,13 @@ impl Element for EditorElement { ); let mut max_visible_line_width = Pixels::ZERO; - let mut line_layouts = - self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx); + let mut line_layouts = Self::layout_lines( + start_row..end_row, + &line_numbers, + &snapshot, + &self.style, + cx, + ); for line_with_invisibles in &line_layouts { if line_with_invisibles.width > max_visible_line_width { max_visible_line_width = line_with_invisibles.width; @@ -4792,16 +4828,43 @@ impl Element for EditorElement { ) }); - let scroll_pixel_position = point( - scroll_position.x * em_width, - scroll_position.y * line_height, - ); - let start_buffer_row = MultiBufferRow(start_anchor.to_point(&snapshot.buffer_snapshot).row); let end_buffer_row = MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row); + let scroll_max = point( + ((scroll_width - text_hitbox.size.width) / em_width).max(0.0), + max_row.as_f32(), + ); + + self.editor.update(cx, |editor, cx| { + let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x); + + let autoscrolled = if autoscroll_horizontally { + editor.autoscroll_horizontally( + start_row, + text_hitbox.size.width, + scroll_width, + em_width, + &line_layouts, + cx, + ) + } else { + false + }; + + if clamped || autoscrolled { + snapshot = editor.snapshot(cx); + scroll_position = snapshot.scroll_position(); + } + }); + + let scroll_pixel_position = point( + scroll_position.x * em_width, + scroll_position.y * line_height, + ); + let indent_guides = self.layout_indent_guides( content_origin, text_hitbox.origin, @@ -6065,7 +6128,7 @@ mod tests { }); for editor_mode_without_invisibles in [ - EditorMode::SingleLine, + EditorMode::SingleLine { auto_width: false }, EditorMode::AutoHeight { max_lines: 100 }, ] { let invisibles = collect_invisibles_from_new_editor( diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 1ee1b18fd1..992ef11106 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -455,7 +455,7 @@ impl Editor { } pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext) { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } diff --git a/crates/editor/src/scroll/actions.rs b/crates/editor/src/scroll/actions.rs index c43191e57c..fabdf82d04 100644 --- a/crates/editor/src/scroll/actions.rs +++ b/crates/editor/src/scroll/actions.rs @@ -15,7 +15,7 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } diff --git a/crates/ui/src/clickable.rs b/crates/ui/src/clickable.rs index 462d2c6099..6ebd6fbf58 100644 --- a/crates/ui/src/clickable.rs +++ b/crates/ui/src/clickable.rs @@ -1,7 +1,9 @@ -use gpui::{ClickEvent, WindowContext}; +use gpui::{ClickEvent, CursorStyle, WindowContext}; /// A trait for elements that can be clicked. Enables the use of the `on_click` method. pub trait Clickable { /// Sets the click handler that will fire whenever the element is clicked. fn on_click(self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self; + /// Sets the cursor style when hovering over the element. + fn cursor_style(self, cursor_style: CursorStyle) -> Self; } diff --git a/crates/ui/src/components/button/button.rs b/crates/ui/src/components/button/button.rs index 58d3264a4f..906d808c0c 100644 --- a/crates/ui/src/components/button/button.rs +++ b/crates/ui/src/components/button/button.rs @@ -249,6 +249,11 @@ impl Clickable for Button { self.base = self.base.on_click(handler); self } + + fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self { + self.base = self.base.cursor_style(cursor_style); + self + } } impl FixedWidth for Button { diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index d9ffc29e1f..ee865fa364 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -1,4 +1,4 @@ -use gpui::{relative, DefiniteLength, MouseButton}; +use gpui::{relative, CursorStyle, DefiniteLength, MouseButton}; use gpui::{transparent_black, AnyElement, AnyView, ClickEvent, Hsla, Rems}; use smallvec::SmallVec; @@ -344,6 +344,7 @@ pub struct ButtonLike { size: ButtonSize, rounding: Option, tooltip: Option AnyView>>, + cursor_style: CursorStyle, on_click: Option>, children: SmallVec<[AnyElement; 2]>, } @@ -363,6 +364,7 @@ impl ButtonLike { rounding: Some(ButtonLikeRounding::All), tooltip: None, children: SmallVec::new(), + cursor_style: CursorStyle::PointingHand, on_click: None, layer: None, } @@ -405,6 +407,11 @@ impl Clickable for ButtonLike { self.on_click = Some(Box::new(handler)); self } + + fn cursor_style(mut self, cursor_style: CursorStyle) -> Self { + self.cursor_style = cursor_style; + self + } } impl FixedWidth for ButtonLike { diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index 9edc23ac2d..cb88793240 100644 --- a/crates/ui/src/components/button/icon_button.rs +++ b/crates/ui/src/components/button/icon_button.rs @@ -86,6 +86,11 @@ impl Clickable for IconButton { self.base = self.base.on_click(handler); self } + + fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self { + self.base = self.base.cursor_style(cursor_style); + self + } } impl FixedWidth for IconButton { diff --git a/crates/ui/src/components/button/toggle_button.rs b/crates/ui/src/components/button/toggle_button.rs index c74ffd6655..3ad47ed166 100644 --- a/crates/ui/src/components/button/toggle_button.rs +++ b/crates/ui/src/components/button/toggle_button.rs @@ -82,6 +82,11 @@ impl Clickable for ToggleButton { self.base = self.base.on_click(handler); self } + + fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self { + self.base = self.base.cursor_style(cursor_style); + self + } } impl ButtonCommon for ToggleButton { diff --git a/crates/ui/src/components/disclosure.rs b/crates/ui/src/components/disclosure.rs index 09f817f021..41ff0a4c3a 100644 --- a/crates/ui/src/components/disclosure.rs +++ b/crates/ui/src/components/disclosure.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use gpui::ClickEvent; +use gpui::{ClickEvent, CursorStyle}; use crate::{prelude::*, Color, IconButton, IconButtonShape, IconName, IconSize}; @@ -10,6 +10,7 @@ pub struct Disclosure { is_open: bool, selected: bool, on_toggle: Option>, + cursor_style: CursorStyle, } impl Disclosure { @@ -19,6 +20,7 @@ impl Disclosure { is_open, selected: false, on_toggle: None, + cursor_style: CursorStyle::PointingHand, } } @@ -43,6 +45,11 @@ impl Clickable for Disclosure { self.on_toggle = Some(Arc::new(handler)); self } + + fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self { + self.cursor_style = cursor_style; + self + } } impl RenderOnce for Disclosure { diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index b752da75db..72c7a85bbe 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -97,6 +97,9 @@ pub enum IconName { BellOff, BellRing, Bolt, + Book, + BookCopy, + BookPlus, CaseSensitive, Check, ChevronDown, @@ -231,6 +234,9 @@ impl IconName { IconName::BellOff => "icons/bell_off.svg", IconName::BellRing => "icons/bell_ring.svg", IconName::Bolt => "icons/bolt.svg", + IconName::Book => "icons/book.svg", + IconName::BookCopy => "icons/book_copy.svg", + IconName::BookPlus => "icons/book_plus.svg", IconName::CaseSensitive => "icons/case_insensitive.svg", IconName::Check => "icons/check.svg", IconName::ChevronDown => "icons/chevron_down.svg",