From bf39968105f8d3be1b71ba6bd3e1f8823b3ab1c0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 24 Nov 2023 13:00:06 +0100 Subject: [PATCH 01/11] Return `TextRun`s in `combine_syntax_and_fuzzy_match_highlights` --- crates/editor2/src/editor.rs | 38 ++++++++++++++------ crates/editor2/src/editor_tests.rs | 57 +++++++++++++----------------- crates/gpui2/src/style.rs | 12 ++++++- 3 files changed, 62 insertions(+), 45 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 43d1fecc585f185b24cd5d5f7de70d5965a230a3..05660df430257b7bd216754b7173302941234235 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -43,7 +43,7 @@ use gpui::{ AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, KeyContext, Model, MouseButton, ParentElement, Pixels, Render, RenderOnce, - SharedString, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, + SharedString, Styled, Subscription, Task, TextRun, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; @@ -10080,11 +10080,25 @@ pub fn diagnostic_style( pub fn combine_syntax_and_fuzzy_match_highlights( text: &str, - default_style: HighlightStyle, + default_style: TextStyle, syntax_ranges: impl Iterator, HighlightStyle)>, match_indices: &[usize], -) -> Vec<(Range, HighlightStyle)> { - let mut result = Vec::new(); +) -> Vec { + let mut current_index = 0; + let mut runs = Vec::new(); + let mut push_run = |range: Range, highlight_style: HighlightStyle| { + if current_index < range.start { + runs.push(default_style.clone().to_run(range.start - current_index)); + } + runs.push( + default_style + .clone() + .highlight(highlight_style) + .to_run(range.len()), + ); + current_index = range.end; + }; + let mut match_indices = match_indices.iter().copied().peekable(); for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())]) @@ -10099,9 +10113,7 @@ pub fn combine_syntax_and_fuzzy_match_highlights( } match_indices.next(); let end_index = char_ix_after(match_index, text); - let mut match_style = default_style; - match_style.font_weight = Some(FontWeight::BOLD); - result.push((match_index..end_index, match_style)); + push_run(match_index..end_index, FontWeight::BOLD.into()); } if range.start == usize::MAX { @@ -10118,7 +10130,7 @@ pub fn combine_syntax_and_fuzzy_match_highlights( match_indices.next(); if match_index > offset { - result.push((offset..match_index, syntax_highlight)); + push_run(offset..match_index, syntax_highlight); } let mut end_index = char_ix_after(match_index, text); @@ -10133,20 +10145,24 @@ pub fn combine_syntax_and_fuzzy_match_highlights( let mut match_style = syntax_highlight; match_style.font_weight = Some(FontWeight::BOLD); - result.push((match_index..end_index, match_style)); + push_run(match_index..end_index, match_style); offset = end_index; } if offset < range.end { - result.push((offset..range.end, syntax_highlight)); + push_run(offset..range.end, syntax_highlight); } } + if current_index < text.len() { + runs.push(default_style.to_run(text.len() - current_index)); + } + fn char_ix_after(ix: usize, text: &str) -> usize { ix + text[ix..].chars().next().unwrap().len_utf8() } - result + runs } pub fn styled_runs_for_code_label<'a>( diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index f0609fc9a8507a4adf5db179c90ff1d519378dd8..42a1bf3c62db6a97cd17504e636c87f9f8194bfb 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -6758,6 +6758,13 @@ fn test_combine_syntax_and_fuzzy_match_highlights() { ..Default::default() }, ), + ( + 12..13, + HighlightStyle { + color: Some(Hsla::blue()), + ..Default::default() + }, + ), ]; let match_indices = [4, 6, 7, 8]; assert_eq!( @@ -6768,43 +6775,27 @@ fn test_combine_syntax_and_fuzzy_match_highlights() { &match_indices, ), &[ - ( - 0..3, - HighlightStyle { - color: Some(Hsla::red()), - ..Default::default() - }, - ), - ( - 4..5, - HighlightStyle { - color: Some(Hsla::green()), - font_weight: Some(gpui::FontWeight::BOLD), - ..Default::default() - }, - ), - ( - 5..6, - HighlightStyle { + TextStyle::default().highlight(Hsla::red()).to_run(3), + TextStyle::default().to_run(1), + TextStyle::default() + .highlight(HighlightStyle { color: Some(Hsla::green()), + font_weight: Some(FontWeight::BOLD), ..Default::default() - }, - ), - ( - 6..8, - HighlightStyle { + }) + .to_run(1), + TextStyle::default().highlight(Hsla::green()).to_run(1), + TextStyle::default() + .highlight(HighlightStyle { color: Some(Hsla::green()), - font_weight: Some(gpui::FontWeight::BOLD), + font_weight: Some(FontWeight::BOLD), ..Default::default() - }, - ), - ( - 8..9, - HighlightStyle { - font_weight: Some(gpui::FontWeight::BOLD), - ..Default::default() - }, - ), + }) + .to_run(2), + TextStyle::default().highlight(FontWeight::BOLD).to_run(1), + TextStyle::default().to_run(3), + TextStyle::default().highlight(Hsla::blue()).to_run(1), + TextStyle::default().to_run(3), ] ); } diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 77d732032b29baeb08f5ec7d081f6012ab8d3de6..472b81c0f83ab29a3dce82774036a757a6f40b0b 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -168,7 +168,8 @@ impl Default for TextStyle { } impl TextStyle { - pub fn highlight(mut self, style: HighlightStyle) -> Self { + pub fn highlight(mut self, style: impl Into) -> Self { + let style = style.into(); if let Some(weight) = style.font_weight { self.font_weight = weight; } @@ -502,6 +503,15 @@ impl From for HighlightStyle { } } +impl From for HighlightStyle { + fn from(font_weight: FontWeight) -> Self { + Self { + font_weight: Some(font_weight), + ..Default::default() + } + } +} + impl From for HighlightStyle { fn from(color: Rgba) -> Self { Self { From 54357d65535153996fd157362dd53ad819e2450f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 24 Nov 2023 13:10:56 +0100 Subject: [PATCH 02/11] Syntax highlight completions --- crates/editor2/src/editor.rs | 25 ++++++++++++++----------- crates/editor2/src/editor_tests.rs | 2 +- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 05660df430257b7bd216754b7173302941234235..8387e41e7feb15033687a8512bd2f9f864c641bc 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -43,8 +43,8 @@ use gpui::{ AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, KeyContext, Model, MouseButton, ParentElement, Pixels, Render, RenderOnce, - SharedString, Styled, Subscription, Task, TextRun, TextStyle, UniformListScrollHandle, View, - ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext, + SharedString, Styled, StyledText, Subscription, Task, TextRun, TextStyle, + UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -1251,6 +1251,7 @@ impl CompletionsMenu { let completions = self.completions.clone(); let matches = self.matches.clone(); let selected_item = self.selected_item; + let style = style.clone(); let list = uniform_list( cx.view().clone(), @@ -1274,13 +1275,12 @@ impl CompletionsMenu { &None }; - // todo!("highlights") - // let highlights = combine_syntax_and_fuzzy_match_highlights( - // &completion.label.text, - // style.text.color.into(), - // styled_runs_for_code_label(&completion.label, &style.syntax), - // &mat.positions, - // ) + let completion_runs = combine_syntax_and_fuzzy_match_highlights( + &completion.label.text, + &style.text, + styled_runs_for_code_label(&completion.label, &style.syntax), + &mat.positions, + ); // todo!("documentation") // MouseEventHandler::new::(mat.candidate_id, cx, |state, _| { @@ -1364,7 +1364,10 @@ impl CompletionsMenu { .bg(gpui::green()) .hover(|style| style.bg(gpui::blue())) .when(item_ix == selected_item, |div| div.bg(gpui::red())) - .child(SharedString::from(completion.label.text.clone())) + .child( + StyledText::new(completion.label.text.clone()) + .with_runs(completion_runs), + ) .min_w(px(300.)) .max_w(px(700.)) }) @@ -10080,7 +10083,7 @@ pub fn diagnostic_style( pub fn combine_syntax_and_fuzzy_match_highlights( text: &str, - default_style: TextStyle, + default_style: &TextStyle, syntax_ranges: impl Iterator, HighlightStyle)>, match_indices: &[usize], ) -> Vec { diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 42a1bf3c62db6a97cd17504e636c87f9f8194bfb..feee78fa3a55f0e4fc2958e3aa3680fbd6283e10 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -6770,7 +6770,7 @@ fn test_combine_syntax_and_fuzzy_match_highlights() { assert_eq!( combine_syntax_and_fuzzy_match_highlights( string, - Default::default(), + &TextStyle::default(), syntax_ranges.into_iter(), &match_indices, ), From 19bfed165b4704f48ae3d6048fe2314e46802f43 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 24 Nov 2023 13:22:25 +0100 Subject: [PATCH 03/11] Show single-line docs in autocomplete and apply completion on mousedown --- crates/editor2/src/editor.rs | 347 ++++++++++++++-------------------- crates/editor2/src/element.rs | 6 +- 2 files changed, 151 insertions(+), 202 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 8387e41e7feb15033687a8512bd2f9f864c641bc..653faa14aff4a3ae3c8f5915db1662f1d3feb86b 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -1281,95 +1281,40 @@ impl CompletionsMenu { styled_runs_for_code_label(&completion.label, &style.syntax), &mat.positions, ); + let completion_label = StyledText::new(completion.label.text.clone()) + .with_runs(completion_runs); + let documentation_label = + if let Some(Documentation::SingleLine(text)) = documentation { + Some(SharedString::from(text.clone())) + } else { + None + }; - // todo!("documentation") - // MouseEventHandler::new::(mat.candidate_id, cx, |state, _| { - // let completion_label = HighlightedLabel::new( - // completion.label.text.clone(), - // combine_syntax_and_fuzzy_match_highlights( - // &completion.label.text, - // style.text.color.into(), - // styled_runs_for_code_label(&completion.label, &style.syntax), - // &mat.positions, - // ), - // ); - // Text::new(completion.label.text.clone(), style.text.clone()) - // .with_soft_wrap(false) - // .with_highlights(); - - // if let Some(Documentation::SingleLine(text)) = documentation { - // h_stack() - // .child(completion_label) - // .with_children((|| { - // let text_style = TextStyle { - // color: style.autocomplete.inline_docs_color, - // font_size: style.text.font_size - // * style.autocomplete.inline_docs_size_percent, - // ..style.text.clone() - // }; - - // let label = Text::new(text.clone(), text_style) - // .aligned() - // .constrained() - // .dynamically(move |constraint, _, _| gpui::SizeConstraint { - // min: constraint.min, - // max: vec2f(constraint.max.x(), constraint.min.y()), - // }); - - // if Some(item_ix) == widest_completion_ix { - // Some( - // label - // .contained() - // .with_style(style.autocomplete.inline_docs_container) - // .into_any(), - // ) - // } else { - // Some(label.flex_float().into_any()) - // } - // })()) - // .into_any() - // } else { - // completion_label.into_any() - // } - // .contained() - // .with_style(item_style) - // .constrained() - // .dynamically(move |constraint, _, _| { - // if Some(item_ix) == widest_completion_ix { - // constraint - // } else { - // gpui::SizeConstraint { - // min: constraint.min, - // max: constraint.min, - // } - // } - // }) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_down(MouseButton::Left, move |_, this, cx| { - // this.confirm_completion( - // &ConfirmCompletion { - // item_ix: Some(item_ix), - // }, - // cx, - // ) - // .map(|task| task.detach()); - // }) - // .constrained() - // div() .id(mat.candidate_id) + .min_w(px(300.)) + .max_w(px(700.)) .whitespace_nowrap() .overflow_hidden() .bg(gpui::green()) .hover(|style| style.bg(gpui::blue())) .when(item_ix == selected_item, |div| div.bg(gpui::red())) - .child( - StyledText::new(completion.label.text.clone()) - .with_runs(completion_runs), + .on_mouse_down( + MouseButton::Left, + cx.listener(move |editor, event, cx| { + cx.stop_propagation(); + editor + .confirm_completion( + &ConfirmCompletion { + item_ix: Some(item_ix), + }, + cx, + ) + .map(|task| task.detach_and_log_err(cx)); + }), ) - .min_w(px(300.)) - .max_w(px(700.)) + .child(completion_label) + .children(documentation_label) }) .collect() }, @@ -3698,135 +3643,135 @@ impl Editor { self.completion_tasks.push((id, task)); } - // pub fn confirm_completion( - // &mut self, - // action: &ConfirmCompletion, - // cx: &mut ViewContext, - // ) -> Option>> { - // use language::ToOffset as _; + pub fn confirm_completion( + &mut self, + action: &ConfirmCompletion, + cx: &mut ViewContext, + ) -> Option>> { + use language::ToOffset as _; - // let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? { - // menu - // } else { - // return None; - // }; + let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? { + menu + } else { + return None; + }; - // let mat = completions_menu - // .matches - // .get(action.item_ix.unwrap_or(completions_menu.selected_item))?; - // let buffer_handle = completions_menu.buffer; - // let completions = completions_menu.completions.read(); - // let completion = completions.get(mat.candidate_id)?; - - // let snippet; - // let text; - // if completion.is_snippet() { - // snippet = Some(Snippet::parse(&completion.new_text).log_err()?); - // text = snippet.as_ref().unwrap().text.clone(); - // } else { - // snippet = None; - // text = completion.new_text.clone(); - // }; - // let selections = self.selections.all::(cx); - // let buffer = buffer_handle.read(cx); - // let old_range = completion.old_range.to_offset(buffer); - // let old_text = buffer.text_for_range(old_range.clone()).collect::(); - - // let newest_selection = self.selections.newest_anchor(); - // if newest_selection.start.buffer_id != Some(buffer_handle.read(cx).remote_id()) { - // return None; - // } + let mat = completions_menu + .matches + .get(action.item_ix.unwrap_or(completions_menu.selected_item))?; + let buffer_handle = completions_menu.buffer; + let completions = completions_menu.completions.read(); + let completion = completions.get(mat.candidate_id)?; + + let snippet; + let text; + if completion.is_snippet() { + snippet = Some(Snippet::parse(&completion.new_text).log_err()?); + text = snippet.as_ref().unwrap().text.clone(); + } else { + snippet = None; + text = completion.new_text.clone(); + }; + let selections = self.selections.all::(cx); + let buffer = buffer_handle.read(cx); + let old_range = completion.old_range.to_offset(buffer); + let old_text = buffer.text_for_range(old_range.clone()).collect::(); - // let lookbehind = newest_selection - // .start - // .text_anchor - // .to_offset(buffer) - // .saturating_sub(old_range.start); - // let lookahead = old_range - // .end - // .saturating_sub(newest_selection.end.text_anchor.to_offset(buffer)); - // let mut common_prefix_len = old_text - // .bytes() - // .zip(text.bytes()) - // .take_while(|(a, b)| a == b) - // .count(); - - // let snapshot = self.buffer.read(cx).snapshot(cx); - // let mut range_to_replace: Option> = None; - // let mut ranges = Vec::new(); - // for selection in &selections { - // if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) { - // let start = selection.start.saturating_sub(lookbehind); - // let end = selection.end + lookahead; - // if selection.id == newest_selection.id { - // range_to_replace = Some( - // ((start + common_prefix_len) as isize - selection.start as isize) - // ..(end as isize - selection.start as isize), - // ); - // } - // ranges.push(start + common_prefix_len..end); - // } else { - // common_prefix_len = 0; - // ranges.clear(); - // ranges.extend(selections.iter().map(|s| { - // if s.id == newest_selection.id { - // range_to_replace = Some( - // old_range.start.to_offset_utf16(&snapshot).0 as isize - // - selection.start as isize - // ..old_range.end.to_offset_utf16(&snapshot).0 as isize - // - selection.start as isize, - // ); - // old_range.clone() - // } else { - // s.start..s.end - // } - // })); - // break; - // } - // } - // let text = &text[common_prefix_len..]; + let newest_selection = self.selections.newest_anchor(); + if newest_selection.start.buffer_id != Some(buffer_handle.read(cx).remote_id()) { + return None; + } - // cx.emit(Event::InputHandled { - // utf16_range_to_replace: range_to_replace, - // text: text.into(), - // }); + let lookbehind = newest_selection + .start + .text_anchor + .to_offset(buffer) + .saturating_sub(old_range.start); + let lookahead = old_range + .end + .saturating_sub(newest_selection.end.text_anchor.to_offset(buffer)); + let mut common_prefix_len = old_text + .bytes() + .zip(text.bytes()) + .take_while(|(a, b)| a == b) + .count(); - // self.transact(cx, |this, cx| { - // if let Some(mut snippet) = snippet { - // snippet.text = text.to_string(); - // for tabstop in snippet.tabstops.iter_mut().flatten() { - // tabstop.start -= common_prefix_len as isize; - // tabstop.end -= common_prefix_len as isize; - // } + let snapshot = self.buffer.read(cx).snapshot(cx); + let mut range_to_replace: Option> = None; + let mut ranges = Vec::new(); + for selection in &selections { + if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) { + let start = selection.start.saturating_sub(lookbehind); + let end = selection.end + lookahead; + if selection.id == newest_selection.id { + range_to_replace = Some( + ((start + common_prefix_len) as isize - selection.start as isize) + ..(end as isize - selection.start as isize), + ); + } + ranges.push(start + common_prefix_len..end); + } else { + common_prefix_len = 0; + ranges.clear(); + ranges.extend(selections.iter().map(|s| { + if s.id == newest_selection.id { + range_to_replace = Some( + old_range.start.to_offset_utf16(&snapshot).0 as isize + - selection.start as isize + ..old_range.end.to_offset_utf16(&snapshot).0 as isize + - selection.start as isize, + ); + old_range.clone() + } else { + s.start..s.end + } + })); + break; + } + } + let text = &text[common_prefix_len..]; - // this.insert_snippet(&ranges, snippet, cx).log_err(); - // } else { - // this.buffer.update(cx, |buffer, cx| { - // buffer.edit( - // ranges.iter().map(|range| (range.clone(), text)), - // this.autoindent_mode.clone(), - // cx, - // ); - // }); - // } + cx.emit(EditorEvent::InputHandled { + utf16_range_to_replace: range_to_replace, + text: text.into(), + }); - // this.refresh_copilot_suggestions(true, cx); - // }); + self.transact(cx, |this, cx| { + if let Some(mut snippet) = snippet { + snippet.text = text.to_string(); + for tabstop in snippet.tabstops.iter_mut().flatten() { + tabstop.start -= common_prefix_len as isize; + tabstop.end -= common_prefix_len as isize; + } - // let project = self.project.clone()?; - // let apply_edits = project.update(cx, |project, cx| { - // project.apply_additional_edits_for_completion( - // buffer_handle, - // completion.clone(), - // true, - // cx, - // ) - // }); - // Some(cx.foreground().spawn(async move { - // apply_edits.await?; - // Ok(()) - // })) - // } + this.insert_snippet(&ranges, snippet, cx).log_err(); + } else { + this.buffer.update(cx, |buffer, cx| { + buffer.edit( + ranges.iter().map(|range| (range.clone(), text)), + this.autoindent_mode.clone(), + cx, + ); + }); + } + + this.refresh_copilot_suggestions(true, cx); + }); + + let project = self.project.clone()?; + let apply_edits = project.update(cx, |project, cx| { + project.apply_additional_edits_for_completion( + buffer_handle, + completion.clone(), + true, + cx, + ) + }); + Some(cx.foreground_executor().spawn(async move { + apply_edits.await?; + Ok(()) + })) + } pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext) { let mut context_menu = self.context_menu.write(); diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index ab0477a9c4e6caa0f301d1f219f4b86b7fb90d0d..c6035e8734325cd8a60b016462930b960f83476d 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -268,7 +268,11 @@ impl EditorElement { }); register_action(view, cx, Editor::restart_language_server); register_action(view, cx, Editor::show_character_palette); - // on_action(cx, Editor::confirm_completion); todo!() + register_action(view, cx, |editor, action, cx| { + editor + .confirm_completion(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); register_action(view, cx, |editor, action, cx| { editor .confirm_code_action(action, cx) From e5b6b0ee9e130f548b0025deeb8143974c1cacc5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 24 Nov 2023 15:04:25 +0100 Subject: [PATCH 04/11] WIP --- crates/editor2/src/editor.rs | 60 ++++++++++++++++++------------ crates/editor2/src/editor_tests.rs | 57 ++++++++++++++++------------ 2 files changed, 69 insertions(+), 48 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 653faa14aff4a3ae3c8f5915db1662f1d3feb86b..225ccd97e4eee7c4a2c70a54f2fc3f1746e4139c 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -42,8 +42,8 @@ use gpui::{ actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle, - Hsla, InputHandler, KeyContext, Model, MouseButton, ParentElement, Pixels, Render, RenderOnce, - SharedString, Styled, StyledText, Subscription, Task, TextRun, TextStyle, + Hsla, InputHandler, InteractiveText, KeyContext, Model, MouseButton, ParentElement, Pixels, + Render, RenderOnce, SharedString, Styled, StyledText, Subscription, Task, TextRun, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; @@ -116,11 +116,12 @@ pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); // pub fn render_parsed_markdown( +// element_id: impl Into, // parsed: &language::ParsedMarkdown, // editor_style: &EditorStyle, // workspace: Option>, // cx: &mut ViewContext, -// ) -> Text { +// ) -> InteractiveText { // enum RenderedMarkdown {} // let parsed = parsed.clone(); @@ -1275,14 +1276,18 @@ impl CompletionsMenu { &None }; - let completion_runs = combine_syntax_and_fuzzy_match_highlights( + let highlights = combine_syntax_and_fuzzy_match_highlights( &completion.label.text, &style.text, styled_runs_for_code_label(&completion.label, &style.syntax), &mat.positions, ); let completion_label = StyledText::new(completion.label.text.clone()) - .with_runs(completion_runs); + .with_runs(text_runs_for_highlights( + &completion.label.text, + &style.text, + highlights, + )); let documentation_label = if let Some(Documentation::SingleLine(text)) = documentation { Some(SharedString::from(text.clone())) @@ -10026,27 +10031,38 @@ pub fn diagnostic_style( } } -pub fn combine_syntax_and_fuzzy_match_highlights( +pub fn text_runs_for_highlights( text: &str, default_style: &TextStyle, - syntax_ranges: impl Iterator, HighlightStyle)>, - match_indices: &[usize], + highlights: impl IntoIterator, HighlightStyle)>, ) -> Vec { - let mut current_index = 0; let mut runs = Vec::new(); - let mut push_run = |range: Range, highlight_style: HighlightStyle| { - if current_index < range.start { - runs.push(default_style.clone().to_run(range.start - current_index)); + let mut ix = 0; + for (range, highlight) in highlights { + if ix < range.start { + runs.push(default_style.clone().to_run(range.start - ix)); } runs.push( default_style .clone() - .highlight(highlight_style) + .highlight(highlight) .to_run(range.len()), ); - current_index = range.end; - }; + ix = range.end; + } + if ix < text.len() { + runs.push(default_style.to_run(text.len() - ix)); + } + runs +} +pub fn combine_syntax_and_fuzzy_match_highlights( + text: &str, + default_style: &TextStyle, + syntax_ranges: impl Iterator, HighlightStyle)>, + match_indices: &[usize], +) -> Vec<(Range, HighlightStyle)> { + let mut highlights = Vec::new(); let mut match_indices = match_indices.iter().copied().peekable(); for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())]) @@ -10061,7 +10077,7 @@ pub fn combine_syntax_and_fuzzy_match_highlights( } match_indices.next(); let end_index = char_ix_after(match_index, text); - push_run(match_index..end_index, FontWeight::BOLD.into()); + highlights.push((match_index..end_index, FontWeight::BOLD.into())); } if range.start == usize::MAX { @@ -10078,7 +10094,7 @@ pub fn combine_syntax_and_fuzzy_match_highlights( match_indices.next(); if match_index > offset { - push_run(offset..match_index, syntax_highlight); + highlights.push((offset..match_index, syntax_highlight)); } let mut end_index = char_ix_after(match_index, text); @@ -10093,24 +10109,20 @@ pub fn combine_syntax_and_fuzzy_match_highlights( let mut match_style = syntax_highlight; match_style.font_weight = Some(FontWeight::BOLD); - push_run(match_index..end_index, match_style); + highlights.push((match_index..end_index, match_style)); offset = end_index; } if offset < range.end { - push_run(offset..range.end, syntax_highlight); + highlights.push((offset..range.end, syntax_highlight)); } } - if current_index < text.len() { - runs.push(default_style.to_run(text.len() - current_index)); - } - fn char_ix_after(ix: usize, text: &str) -> usize { ix + text[ix..].chars().next().unwrap().len_utf8() } - runs + highlights } pub fn styled_runs_for_code_label<'a>( diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index feee78fa3a55f0e4fc2958e3aa3680fbd6283e10..3a0de328e5bb4ddf6716739b291c9e88b792dc99 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -6758,13 +6758,6 @@ fn test_combine_syntax_and_fuzzy_match_highlights() { ..Default::default() }, ), - ( - 12..13, - HighlightStyle { - color: Some(Hsla::blue()), - ..Default::default() - }, - ), ]; let match_indices = [4, 6, 7, 8]; assert_eq!( @@ -6775,27 +6768,43 @@ fn test_combine_syntax_and_fuzzy_match_highlights() { &match_indices, ), &[ - TextStyle::default().highlight(Hsla::red()).to_run(3), - TextStyle::default().to_run(1), - TextStyle::default() - .highlight(HighlightStyle { + ( + 0..3, + HighlightStyle { + color: Some(Hsla::red()), + ..Default::default() + }, + ), + ( + 4..5, + HighlightStyle { color: Some(Hsla::green()), - font_weight: Some(FontWeight::BOLD), + font_weight: Some(gpui::FontWeight::BOLD), ..Default::default() - }) - .to_run(1), - TextStyle::default().highlight(Hsla::green()).to_run(1), - TextStyle::default() - .highlight(HighlightStyle { + }, + ), + ( + 5..6, + HighlightStyle { color: Some(Hsla::green()), - font_weight: Some(FontWeight::BOLD), ..Default::default() - }) - .to_run(2), - TextStyle::default().highlight(FontWeight::BOLD).to_run(1), - TextStyle::default().to_run(3), - TextStyle::default().highlight(Hsla::blue()).to_run(1), - TextStyle::default().to_run(3), + }, + ), + ( + 6..8, + HighlightStyle { + color: Some(Hsla::green()), + font_weight: Some(gpui::FontWeight::BOLD), + ..Default::default() + }, + ), + ( + 8..9, + HighlightStyle { + font_weight: Some(gpui::FontWeight::BOLD), + ..Default::default() + }, + ), ] ); } From d31b53b9122d572d690930991e70df7f2696a311 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 24 Nov 2023 16:31:38 +0100 Subject: [PATCH 05/11] Extract a `gpui::combine_highlights` function --- crates/editor2/src/editor.rs | 84 ++-------------- crates/editor2/src/editor_tests.rs | 69 ------------- crates/fuzzy2/src/strings.rs | 26 +++++ crates/gpui2/src/style.rs | 149 +++++++++++++++++++++++++++++ 4 files changed, 185 insertions(+), 143 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 225ccd97e4eee7c4a2c70a54f2fc3f1746e4139c..5754f8be2a710d40b15def4d971ca4dfa17d7c27 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -1276,11 +1276,16 @@ impl CompletionsMenu { &None }; - let highlights = combine_syntax_and_fuzzy_match_highlights( - &completion.label.text, - &style.text, - styled_runs_for_code_label(&completion.label, &style.syntax), - &mat.positions, + let highlights = gpui::combine_highlights( + mat.ranges().map(|range| (range, FontWeight::BOLD.into())), + styled_runs_for_code_label(&completion.label, &style.syntax).map( + |(range, mut highlight)| { + // Ignore font weight for syntax highlighting, as we'll use it + // for fuzzy matches. + highlight.font_weight = None; + (range, highlight) + }, + ), ); let completion_label = StyledText::new(completion.label.text.clone()) .with_runs(text_runs_for_highlights( @@ -10056,75 +10061,6 @@ pub fn text_runs_for_highlights( runs } -pub fn combine_syntax_and_fuzzy_match_highlights( - text: &str, - default_style: &TextStyle, - syntax_ranges: impl Iterator, HighlightStyle)>, - match_indices: &[usize], -) -> Vec<(Range, HighlightStyle)> { - let mut highlights = Vec::new(); - let mut match_indices = match_indices.iter().copied().peekable(); - - for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())]) - { - syntax_highlight.font_weight = None; - - // Add highlights for any fuzzy match characters before the next - // syntax highlight range. - while let Some(&match_index) = match_indices.peek() { - if match_index >= range.start { - break; - } - match_indices.next(); - let end_index = char_ix_after(match_index, text); - highlights.push((match_index..end_index, FontWeight::BOLD.into())); - } - - if range.start == usize::MAX { - break; - } - - // Add highlights for any fuzzy match characters within the - // syntax highlight range. - let mut offset = range.start; - while let Some(&match_index) = match_indices.peek() { - if match_index >= range.end { - break; - } - - match_indices.next(); - if match_index > offset { - highlights.push((offset..match_index, syntax_highlight)); - } - - let mut end_index = char_ix_after(match_index, text); - while let Some(&next_match_index) = match_indices.peek() { - if next_match_index == end_index && next_match_index < range.end { - end_index = char_ix_after(next_match_index, text); - match_indices.next(); - } else { - break; - } - } - - let mut match_style = syntax_highlight; - match_style.font_weight = Some(FontWeight::BOLD); - highlights.push((match_index..end_index, match_style)); - offset = end_index; - } - - if offset < range.end { - highlights.push((offset..range.end, syntax_highlight)); - } - } - - fn char_ix_after(ix: usize, text: &str) -> usize { - ix + text[ix..].chars().next().unwrap().len_utf8() - } - - highlights -} - pub fn styled_runs_for_code_label<'a>( label: &'a CodeLabel, syntax_theme: &'a theme::SyntaxTheme, diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 3a0de328e5bb4ddf6716739b291c9e88b792dc99..6865e81cfa5d830f9dbe76f1154cbc0fe180edad 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -6740,75 +6740,6 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { // ); // } -#[test] -fn test_combine_syntax_and_fuzzy_match_highlights() { - let string = "abcdefghijklmnop"; - let syntax_ranges = [ - ( - 0..3, - HighlightStyle { - color: Some(Hsla::red()), - ..Default::default() - }, - ), - ( - 4..8, - HighlightStyle { - color: Some(Hsla::green()), - ..Default::default() - }, - ), - ]; - let match_indices = [4, 6, 7, 8]; - assert_eq!( - combine_syntax_and_fuzzy_match_highlights( - string, - &TextStyle::default(), - syntax_ranges.into_iter(), - &match_indices, - ), - &[ - ( - 0..3, - HighlightStyle { - color: Some(Hsla::red()), - ..Default::default() - }, - ), - ( - 4..5, - HighlightStyle { - color: Some(Hsla::green()), - font_weight: Some(gpui::FontWeight::BOLD), - ..Default::default() - }, - ), - ( - 5..6, - HighlightStyle { - color: Some(Hsla::green()), - ..Default::default() - }, - ), - ( - 6..8, - HighlightStyle { - color: Some(Hsla::green()), - font_weight: Some(gpui::FontWeight::BOLD), - ..Default::default() - }, - ), - ( - 8..9, - HighlightStyle { - font_weight: Some(gpui::FontWeight::BOLD), - ..Default::default() - }, - ), - ] - ); -} - #[gpui::test] async fn go_to_prev_overlapping_diagnostic( executor: BackgroundExecutor, diff --git a/crates/fuzzy2/src/strings.rs b/crates/fuzzy2/src/strings.rs index 085362dd2c843be595c58d9a0eec4a997376c081..ab3de3fa445860b523b2421573ee2ad3fae0debc 100644 --- a/crates/fuzzy2/src/strings.rs +++ b/crates/fuzzy2/src/strings.rs @@ -6,6 +6,8 @@ use gpui::BackgroundExecutor; use std::{ borrow::Cow, cmp::{self, Ordering}, + iter, + ops::Range, sync::atomic::AtomicBool, }; @@ -54,6 +56,30 @@ pub struct StringMatch { pub string: String, } +impl StringMatch { + pub fn ranges<'a>(&'a self) -> impl 'a + Iterator> { + let mut positions = self.positions.iter().peekable(); + iter::from_fn(move || { + while let Some(start) = positions.next().copied() { + let mut end = start + self.char_len_at_index(start); + while let Some(next_start) = positions.peek() { + if end == **next_start { + end += self.char_len_at_index(end); + positions.next(); + } + } + + return Some(start..end); + } + None + }) + } + + fn char_len_at_index(&self, ix: usize) -> usize { + self.string[ix..].chars().next().unwrap().len_utf8() + } +} + impl PartialEq for StringMatch { fn eq(&self, other: &Self) -> bool { self.cmp(other).is_eq() diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 472b81c0f83ab29a3dce82774036a757a6f40b0b..640538fff0ed204d3af16e24ecd0006d98f8357e 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -1,9 +1,12 @@ +use std::{iter, mem, ops::Range}; + use crate::{ black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext, }; +use collections::HashSet; use refineable::{Cascade, Refineable}; use smallvec::SmallVec; pub use taffy::style::{ @@ -512,6 +515,15 @@ impl From for HighlightStyle { } } +impl From for HighlightStyle { + fn from(font_style: FontStyle) -> Self { + Self { + font_style: Some(font_style), + ..Default::default() + } + } +} + impl From for HighlightStyle { fn from(color: Rgba) -> Self { Self { @@ -520,3 +532,140 @@ impl From for HighlightStyle { } } } + +pub fn combine_highlights( + a: impl IntoIterator, HighlightStyle)>, + b: impl IntoIterator, HighlightStyle)>, +) -> impl Iterator, HighlightStyle)> { + let mut endpoints = Vec::new(); + let mut highlights = Vec::new(); + for (range, highlight) in a.into_iter().chain(b) { + if !range.is_empty() { + let highlight_id = highlights.len(); + endpoints.push((range.start, highlight_id, true)); + endpoints.push((range.end, highlight_id, false)); + highlights.push(highlight); + } + } + endpoints.sort_unstable_by_key(|(position, _, _)| *position); + let mut endpoints = endpoints.into_iter().peekable(); + + let mut active_styles = HashSet::default(); + let mut ix = 0; + iter::from_fn(move || { + while let Some((endpoint_ix, highlight_id, is_start)) = endpoints.peek() { + let prev_index = mem::replace(&mut ix, *endpoint_ix); + if ix > prev_index && !active_styles.is_empty() { + let mut current_style = HighlightStyle::default(); + for highlight_id in &active_styles { + current_style.highlight(highlights[*highlight_id]); + } + return Some((prev_index..ix, current_style)); + } + + if *is_start { + active_styles.insert(*highlight_id); + } else { + active_styles.remove(highlight_id); + } + endpoints.next(); + } + None + }) +} + +#[cfg(test)] +mod tests { + use crate::{blue, green, red, yellow}; + + use super::*; + + #[test] + fn test_combine_highlights() { + assert_eq!( + combine_highlights( + [ + (0..5, green().into()), + (4..10, FontWeight::BOLD.into()), + (15..20, yellow().into()), + ], + [ + (2..6, FontStyle::Italic.into()), + (1..3, blue().into()), + (21..23, red().into()), + ] + ) + .collect::>(), + [ + ( + 0..1, + HighlightStyle { + color: Some(green()), + ..Default::default() + } + ), + ( + 1..2, + HighlightStyle { + color: Some(blue()), + ..Default::default() + } + ), + ( + 2..3, + HighlightStyle { + color: Some(blue()), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 3..4, + HighlightStyle { + color: Some(green()), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 4..5, + HighlightStyle { + color: Some(green()), + font_weight: Some(FontWeight::BOLD), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 5..6, + HighlightStyle { + font_weight: Some(FontWeight::BOLD), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 6..10, + HighlightStyle { + font_weight: Some(FontWeight::BOLD), + ..Default::default() + } + ), + ( + 15..20, + HighlightStyle { + color: Some(yellow()), + ..Default::default() + } + ), + ( + 21..23, + HighlightStyle { + color: Some(red()), + ..Default::default() + } + ) + ] + ); + } +} From 0baa9a782bc90fec7e60f191bfaf55f2fa2c8577 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 24 Nov 2023 17:28:59 +0100 Subject: [PATCH 06/11] Start on wiring up render_parsed_markdown --- crates/editor2/src/editor.rs | 203 +++++++++++++++-------------------- crates/fuzzy2/src/strings.rs | 2 + 2 files changed, 90 insertions(+), 115 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 5754f8be2a710d40b15def4d971ca4dfa17d7c27..aa34f9ec4761eafe9b49694a95a41140d4f5afe7 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -40,7 +40,7 @@ use fuzzy::{StringMatch, StringMatchCandidate}; use git::diff_hunk_to_display; use gpui::{ actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement, - AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, + AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, ElementId, EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, InteractiveText, KeyContext, Model, MouseButton, ParentElement, Pixels, Render, RenderOnce, SharedString, Styled, StyledText, Subscription, Task, TextRun, TextStyle, @@ -54,9 +54,10 @@ use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, - point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, - CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, LanguageRegistry, - LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId, + markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, + Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, + LanguageRegistry, LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal, + TransactionId, }; use lazy_static::lazy_static; use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState}; @@ -97,7 +98,7 @@ use text::{OffsetUtf16, Rope}; use theme::{ ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings, }; -use ui::{h_stack, v_stack, HighlightedLabel, IconButton, StyledExt, Tooltip}; +use ui::{h_stack, v_stack, HighlightedLabel, IconButton, Popover, StyledExt, Tooltip}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ item::{ItemEvent, ItemHandle}, @@ -115,71 +116,70 @@ pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); -// pub fn render_parsed_markdown( -// element_id: impl Into, -// parsed: &language::ParsedMarkdown, -// editor_style: &EditorStyle, -// workspace: Option>, -// cx: &mut ViewContext, -// ) -> InteractiveText { -// enum RenderedMarkdown {} - -// let parsed = parsed.clone(); -// let view_id = cx.view_id(); -// let code_span_background_color = editor_style.document_highlight_read_background; - -// let mut region_id = 0; - -// todo!() -// // Text::new(parsed.text, editor_style.text.clone()) -// // .with_highlights( -// // parsed -// // .highlights -// // .iter() -// // .filter_map(|(range, highlight)| { -// // let highlight = highlight.to_highlight_style(&editor_style.syntax)?; -// // Some((range.clone(), highlight)) -// // }) -// // .collect::>(), -// // ) -// // .with_custom_runs(parsed.region_ranges, move |ix, bounds, cx| { -// // region_id += 1; -// // let region = parsed.regions[ix].clone(); - -// // if let Some(link) = region.link { -// // cx.scene().push_cursor_region(CursorRegion { -// // bounds, -// // style: CursorStyle::PointingHand, -// // }); -// // cx.scene().push_mouse_region( -// // MouseRegion::new::<(RenderedMarkdown, Tag)>(view_id, region_id, bounds) -// // .on_down::(MouseButton::Left, move |_, _, cx| match &link { -// // markdown::Link::Web { url } => cx.platform().open_url(url), -// // markdown::Link::Path { path } => { -// // if let Some(workspace) = &workspace { -// // _ = workspace.update(cx, |workspace, cx| { -// // workspace.open_abs_path(path.clone(), false, cx).detach(); -// // }); -// // } -// // } -// // }), -// // ); -// // } - -// // if region.code { -// // cx.draw_quad(Quad { -// // bounds, -// // background: Some(code_span_background_color), -// // corner_radii: (2.0).into(), -// // order: todo!(), -// // content_mask: todo!(), -// // border_color: todo!(), -// // border_widths: todo!(), -// // }); -// // } -// // }) -// // .with_soft_wrap(true) -// } +pub fn render_parsed_markdown( + element_id: impl Into, + parsed: &language::ParsedMarkdown, + editor_style: &EditorStyle, + workspace: Option>, + cx: &mut ViewContext, +) -> InteractiveText { + let code_span_background_color = cx + .theme() + .colors() + .editor_document_highlight_read_background; + + let highlights = gpui::combine_highlights( + parsed.highlights.iter().filter_map(|(range, highlight)| { + let highlight = highlight.to_highlight_style(&editor_style.syntax)?; + Some((range.clone(), highlight)) + }), + parsed + .regions + .iter() + .zip(&parsed.region_ranges) + .filter_map(|(region, range)| { + if region.code { + Some(( + range.clone(), + HighlightStyle { + background_color: Some(code_span_background_color), + ..Default::default() + }, + )) + } else { + None + } + }), + ); + let runs = text_runs_for_highlights(&parsed.text, &editor_style.text, highlights); + + // todo!("add the ability to change cursor style for link ranges") + let mut links = Vec::new(); + let mut link_ranges = Vec::new(); + for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) { + if let Some(link) = region.link.clone() { + links.push(link); + link_ranges.push(range.clone()); + } + } + + InteractiveText::new( + element_id, + StyledText::new(parsed.text.clone()).with_runs(runs), + ) + .on_click(link_ranges, move |clicked_range_ix, cx| { + match &links[clicked_range_ix] { + markdown::Link::Web { url } => cx.open_url(url), + markdown::Link::Path { path } => { + if let Some(workspace) = &workspace { + _ = workspace.update(cx, |workspace, cx| { + workspace.open_abs_path(path.clone(), false, cx).detach(); + }); + } + } + } + }) +} #[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct SelectNext { @@ -1254,6 +1254,18 @@ impl CompletionsMenu { let selected_item = self.selected_item; let style = style.clone(); + let multiline_docs = { + let mat = &self.matches[selected_item]; + match &self.completions.read()[mat.candidate_id].documentation { + Some(Documentation::MultiLinePlainText(text)) => { + Some(div().child(SharedString::from(text.clone()))) + } + Some(Documentation::MultiLineMarkdown(parsed)) => Some(div().child( + render_parsed_markdown("completions_markdown", parsed, &style, workspace, cx), + )), + _ => None, + } + }; let list = uniform_list( cx.view().clone(), "completions", @@ -1332,51 +1344,12 @@ impl CompletionsMenu { .track_scroll(self.scroll_handle.clone()) .with_width_from_item(widest_completion_ix); - list.into_any_element() - // todo!("multiline documentation") - // enum MultiLineDocumentation {} - - // Flex::row() - // .with_child(list.flex(1., false)) - // .with_children({ - // let mat = &self.matches[selected_item]; - // let completions = self.completions.read(); - // let completion = &completions[mat.candidate_id]; - // let documentation = &completion.documentation; - - // match documentation { - // Some(Documentation::MultiLinePlainText(text)) => Some( - // Flex::column() - // .scrollable::(0, None, cx) - // .with_child( - // Text::new(text.clone(), style.text.clone()).with_soft_wrap(true), - // ) - // .contained() - // .with_style(style.autocomplete.alongside_docs_container) - // .constrained() - // .with_max_width(style.autocomplete.alongside_docs_max_width) - // .flex(1., false), - // ), - - // Some(Documentation::MultiLineMarkdown(parsed)) => Some( - // Flex::column() - // .scrollable::(0, None, cx) - // .with_child(render_parsed_markdown::( - // parsed, &style, workspace, cx, - // )) - // .contained() - // .with_style(style.autocomplete.alongside_docs_container) - // .constrained() - // .with_max_width(style.autocomplete.alongside_docs_max_width) - // .flex(1., false), - // ), - - // _ => None, - // } - // }) - // .contained() - // .with_style(style.autocomplete.container) - // .into_any() + Popover::new() + .child(list) + .when_some(multiline_docs, |popover, multiline_docs| { + popover.aside(multiline_docs.id("multiline_docs").overflow_y_scroll()) + }) + .into_any_element() } pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) { diff --git a/crates/fuzzy2/src/strings.rs b/crates/fuzzy2/src/strings.rs index ab3de3fa445860b523b2421573ee2ad3fae0debc..5028a43fd7bb3459bc1f2772a4e521bb3dec3408 100644 --- a/crates/fuzzy2/src/strings.rs +++ b/crates/fuzzy2/src/strings.rs @@ -66,6 +66,8 @@ impl StringMatch { if end == **next_start { end += self.char_len_at_index(end); positions.next(); + } else { + break; } } From f227c3284de3792f39ad42fca559529403ea0783 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 24 Nov 2023 17:53:48 +0100 Subject: [PATCH 07/11] Consume newline from run if it spans it --- crates/gpui2/src/text_system.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/gpui2/src/text_system.rs b/crates/gpui2/src/text_system.rs index 76e21e5f7339cf2ac69e8caf3773e3456a56f3f8..440789dd472b35c02e1bbf3c2605e7b4c8ae3be3 100644 --- a/crates/gpui2/src/text_system.rs +++ b/crates/gpui2/src/text_system.rs @@ -290,7 +290,15 @@ impl TextSystem { text: SharedString::from(line_text), }); - line_start = line_end + 1; // Skip `\n` character. + // Skip `\n` character. + line_start = line_end + 1; + if let Some(run) = runs.peek_mut() { + run.len = run.len.saturating_sub(1); + if run.len == 0 { + runs.next(); + } + } + font_runs.clear(); } From 047cfe5108222a039c1189e76f74d0793c125655 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 24 Nov 2023 18:23:08 +0100 Subject: [PATCH 08/11] Fix painting when underlines and quads appeared after a wrap boundary --- crates/gpui2/src/text_system/line.rs | 36 ++++++++++++++++------------ 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/crates/gpui2/src/text_system/line.rs b/crates/gpui2/src/text_system/line.rs index 045a985ce7c1fbcb44d4afc6df88d9eee52ee75c..0d15647b88fdfb112b72b150b0500d20ffac8b37 100644 --- a/crates/gpui2/src/text_system/line.rs +++ b/crates/gpui2/src/text_system/line.rs @@ -40,7 +40,6 @@ impl ShapedLine { &self.layout, line_height, &self.decoration_runs, - None, &[], cx, )?; @@ -74,7 +73,6 @@ impl WrappedLine { &self.layout.unwrapped_layout, line_height, &self.decoration_runs, - self.wrap_width, &self.wrap_boundaries, cx, )?; @@ -88,7 +86,6 @@ fn paint_line( layout: &LineLayout, line_height: Pixels, decoration_runs: &[DecorationRun], - wrap_width: Option, wrap_boundaries: &[WrapBoundary], cx: &mut WindowContext<'_>, ) -> Result<()> { @@ -113,24 +110,28 @@ fn paint_line( if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { wraps.next(); - if let Some((background_origin, background_color)) = current_background.take() { + if let Some((background_origin, background_color)) = current_background.as_mut() { cx.paint_quad( Bounds { - origin: background_origin, + origin: *background_origin, size: size(glyph_origin.x - background_origin.x, line_height), }, Corners::default(), - background_color, + *background_color, Edges::default(), transparent_black(), ); + background_origin.x = origin.x; + background_origin.y += line_height; } - if let Some((underline_origin, underline_style)) = current_underline.take() { + if let Some((underline_origin, underline_style)) = current_underline.as_mut() { cx.paint_underline( - underline_origin, + *underline_origin, glyph_origin.x - underline_origin.x, - &underline_style, + underline_style, ); + underline_origin.x = origin.x; + underline_origin.y += line_height; } glyph_origin.x = origin.x; @@ -149,7 +150,7 @@ fn paint_line( } if let Some(run_background) = style_run.background_color { current_background - .get_or_insert((point(glyph_origin.x, origin.y), run_background)); + .get_or_insert((point(glyph_origin.x, glyph_origin.y), run_background)); } if let Some((_, underline_style)) = &mut current_underline { @@ -161,7 +162,7 @@ fn paint_line( current_underline.get_or_insert(( point( glyph_origin.x, - origin.y + baseline_offset.y + (layout.descent * 0.618), + glyph_origin.y + baseline_offset.y + (layout.descent * 0.618), ), UnderlineStyle { color: Some(run_underline.color.unwrap_or(style_run.color)), @@ -228,12 +229,18 @@ fn paint_line( } } + let mut last_line_end_x = origin.x + layout.width; + if let Some(boundary) = wrap_boundaries.last() { + let run = &layout.runs[boundary.run_ix]; + let glyph = &run.glyphs[boundary.glyph_ix]; + last_line_end_x -= glyph.position.x; + } + if let Some((background_origin, background_color)) = current_background.take() { - let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width); cx.paint_quad( Bounds { origin: background_origin, - size: size(line_end_x - background_origin.x, line_height), + size: size(last_line_end_x - background_origin.x, line_height), }, Corners::default(), background_color, @@ -243,10 +250,9 @@ fn paint_line( } if let Some((underline_start, underline_style)) = current_underline.take() { - let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width); cx.paint_underline( underline_start, - line_end_x - underline_start.x, + last_line_end_x - underline_start.x, &underline_style, ); } From 682712f132ff0187a4ff46548fec30f0f396e698 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 24 Nov 2023 18:32:48 +0100 Subject: [PATCH 09/11] Account for previous line lengths when returning index --- crates/gpui2/src/elements/text.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index c81d4ff0edafc32271e3ececc4873aead286274c..a0715b81a90b564f1fc3397e54e00068951ef2b1 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -244,13 +244,17 @@ impl TextState { let line_height = element_state.line_height; let mut line_origin = bounds.origin; + let mut line_start_ix = 0; for line in &element_state.lines { let line_bottom = line_origin.y + line.size(line_height).height; if position.y > line_bottom { line_origin.y = line_bottom; + line_start_ix += line.len() + 1; } else { let position_within_line = position - line_origin; - return line.index_for_position(position_within_line, line_height); + let index_within_line = + line.index_for_position(position_within_line, line_height)?; + return Some(line_start_ix + index_within_line); } } From eb647be685e7d37995dc42bdf2bdf7689d1aeb5e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 27 Nov 2023 14:56:46 +0100 Subject: [PATCH 10/11] Pass max height manually --- crates/editor2/src/editor.rs | 25 ++++++++++++++++++++----- crates/editor2/src/element.rs | 13 ++++--------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index aa34f9ec4761eafe9b49694a95a41140d4f5afe7..451a3c6535259d4750655f2fa3c39cf86201630e 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -906,12 +906,16 @@ impl ContextMenu { &self, cursor_position: DisplayPoint, style: &EditorStyle, + max_height: Pixels, workspace: Option>, cx: &mut ViewContext, ) -> (DisplayPoint, AnyElement) { match self { - ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)), - ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx), + ContextMenu::Completions(menu) => ( + cursor_position, + menu.render(style, max_height, workspace, cx), + ), + ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, max_height, cx), } } } @@ -1223,6 +1227,7 @@ impl CompletionsMenu { fn render( &self, style: &EditorStyle, + max_height: Pixels, workspace: Option>, cx: &mut ViewContext, ) -> AnyElement { @@ -1256,7 +1261,7 @@ impl CompletionsMenu { let multiline_docs = { let mat = &self.matches[selected_item]; - match &self.completions.read()[mat.candidate_id].documentation { + let multiline_docs = match &self.completions.read()[mat.candidate_id].documentation { Some(Documentation::MultiLinePlainText(text)) => { Some(div().child(SharedString::from(text.clone()))) } @@ -1264,7 +1269,12 @@ impl CompletionsMenu { render_parsed_markdown("completions_markdown", parsed, &style, workspace, cx), )), _ => None, - } + }; + multiline_docs.map(|div| { + div.id("multiline_docs") + .max_h(max_height) + .overflow_y_scroll() + }) }; let list = uniform_list( cx.view().clone(), @@ -1341,13 +1351,14 @@ impl CompletionsMenu { .collect() }, ) + .max_h(max_height) .track_scroll(self.scroll_handle.clone()) .with_width_from_item(widest_completion_ix); Popover::new() .child(list) .when_some(multiline_docs, |popover, multiline_docs| { - popover.aside(multiline_docs.id("multiline_docs").overflow_y_scroll()) + popover.aside(multiline_docs) }) .into_any_element() } @@ -1466,6 +1477,7 @@ impl CodeActionsMenu { &self, mut cursor_position: DisplayPoint, style: &EditorStyle, + max_height: Pixels, cx: &mut ViewContext, ) -> (DisplayPoint, AnyElement) { let actions = self.actions.clone(); @@ -1520,6 +1532,7 @@ impl CodeActionsMenu { .elevation_1(cx) .px_2() .py_1() + .max_h(max_height) .track_scroll(self.scroll_handle.clone()) .with_width_from_item( self.actions @@ -4377,12 +4390,14 @@ impl Editor { &self, cursor_position: DisplayPoint, style: &EditorStyle, + max_height: Pixels, cx: &mut ViewContext, ) -> Option<(DisplayPoint, AnyElement)> { self.context_menu.read().as_ref().map(|menu| { menu.render( cursor_position, style, + max_height, self.workspace.as_ref().map(|(w, _)| w.clone()), cx, ) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index c6035e8734325cd8a60b016462930b960f83476d..e591dd84cf55da1ed71292dd39ac102d38736554 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -1026,14 +1026,8 @@ impl EditorElement { if let Some((position, mut context_menu)) = layout.context_menu.take() { cx.with_z_index(1, |cx| { - let line_height = self.style.text.line_height_in_pixels(cx.rem_size()); - let available_space = size( - AvailableSpace::MinContent, - AvailableSpace::Definite( - (12. * line_height) - .min((text_bounds.size.height - line_height) / 2.), - ), - ); + let available_space = + size(AvailableSpace::MinContent, AvailableSpace::MinContent); let context_menu_size = context_menu.measure(available_space, cx); let cursor_row_layout = &layout.position_map.line_layouts @@ -1978,8 +1972,9 @@ impl EditorElement { if let Some(newest_selection_head) = newest_selection_head { if (start_row..end_row).contains(&newest_selection_head.row()) { if editor.context_menu_visible() { + let max_height = (12. * line_height).min((bounds.size.height - line_height) / 2.); context_menu = - editor.render_context_menu(newest_selection_head, &self.style, cx); + editor.render_context_menu(newest_selection_head, &self.style, max_height, cx); } let active = matches!( From 3ba5dbb9e88acf6b3459caba68244b7b988fd625 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 27 Nov 2023 15:15:53 +0100 Subject: [PATCH 11/11] Prevent mousedown on docs from being propagated to the editor --- crates/editor2/src/editor.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 451a3c6535259d4750655f2fa3c39cf86201630e..3a9a40328d2b6dc0a11cbc75004f2d9c36b865ee 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -1274,6 +1274,9 @@ impl CompletionsMenu { div.id("multiline_docs") .max_h(max_height) .overflow_y_scroll() + // Prevent a mouse down on documentation from being propagated to the editor, + // because that would move the cursor. + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) }) }; let list = uniform_list(