Detailed changes
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-book"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-book-copy"><path d="M2 16V4a2 2 0 0 1 2-2h11"/><path d="M5 14H4a2 2 0 1 0 0 4h1"/><path d="M22 18H11a2 2 0 1 0 0 4h11V6H11a2 2 0 0 0-2 2v12"/></svg>
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-book-plus"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/><path d="M9 10h6"/><path d="M12 7v6"/></svg>
@@ -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<Editor>,
+ title_editor: View<Editor>,
+ body_editor: View<Editor>,
token_count: Option<usize>,
pending_token_count: Task<Option<()>>,
- next_body_to_save: Option<Rope>,
+ next_title_and_body_to_save: Option<(String, Rope)>,
pending_save: Option<Task<Option<()>>>,
- _subscription: Subscription,
+ _subscriptions: Vec<Subscription>,
}
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<Self>) {
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<Self>) {
+ 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<Self>) {
+ 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<Editor>,
event: &EditorEvent,
cx: &mut ViewContext<Self>,
) {
- 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();
-
- 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);
- }
- }
- });
+ 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]);
+ });
+ });
+ }
+ _ => {}
+ }
+ }
- 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<Editor>,
+ event: &EditorEvent,
+ cx: &mut ViewContext<Self>,
+ ) {
+ 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<Self>) {
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<Item = char>) -> Option<SharedString> {
- 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::<String>().trim().to_string();
- if title.is_empty() {
- None
- } else {
- Some(title.into())
- }
- } else {
- None
- }
-}
@@ -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>) -> 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>) -> Self {
@@ -1589,6 +1595,18 @@ impl Editor {
Self::new(EditorMode::Full, buffer, None, false, cx)
}
+ pub fn auto_width(cx: &mut ViewContext<Self>) -> 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>) -> 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>) {
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<Self>,
) {
- 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<Self>,
) {
- 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<Self>,
) {
- 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<Self>,
) {
- 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<Self>) {
- 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<Self>) {
- 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,
};
@@ -1831,10 +1831,10 @@ impl EditorElement {
}
fn layout_lines(
- &self,
rows: Range<DisplayRow>,
line_number_layouts: &[Option<ShapedLine>],
snapshot: &EditorSnapshot,
+ style: &EditorStyle,
cx: &mut WindowContext,
) -> Vec<LineWithInvisibles> {
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(
@@ -455,7 +455,7 @@ impl Editor {
}
pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {
- if matches!(self.mode, EditorMode::SingleLine) {
+ if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}
@@ -15,7 +15,7 @@ impl Editor {
return;
}
- if matches!(self.mode, EditorMode::SingleLine) {
+ if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}
@@ -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;
}
@@ -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 {
@@ -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<ButtonLikeRounding>,
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
+ cursor_style: CursorStyle,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
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 {
@@ -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 {
@@ -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 {
@@ -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<Arc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
+ 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 {
@@ -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",