@@ -40,11 +40,11 @@ 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, KeyContext, Model, MouseButton, ParentElement, Pixels, Render, RenderOnce,
- SharedString, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View,
- ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
+ 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;
use hover_popover::{hide_hover, HoverState};
@@ -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,70 +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<Tag: 'static>(
-// parsed: &language::ParsedMarkdown,
-// editor_style: &EditorStyle,
-// workspace: Option<WeakView<Workspace>>,
-// cx: &mut ViewContext<Editor>,
-// ) -> Text {
-// 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::<Vec<_>>(),
-// // )
-// // .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::<Editor, _>(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<ElementId>,
+ parsed: &language::ParsedMarkdown,
+ editor_style: &EditorStyle,
+ workspace: Option<WeakView<Workspace>>,
+ cx: &mut ViewContext<Editor>,
+) -> 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 {
@@ -905,12 +906,16 @@ impl ContextMenu {
&self,
cursor_position: DisplayPoint,
style: &EditorStyle,
+ max_height: Pixels,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> (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),
}
}
}
@@ -1222,6 +1227,7 @@ impl CompletionsMenu {
fn render(
&self,
style: &EditorStyle,
+ max_height: Pixels,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> AnyElement {
@@ -1251,7 +1257,28 @@ impl CompletionsMenu {
let completions = self.completions.clone();
let matches = self.matches.clone();
let selected_item = self.selected_item;
+ let style = style.clone();
+ let multiline_docs = {
+ let mat = &self.matches[selected_item];
+ let multiline_docs = 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,
+ };
+ multiline_docs.map(|div| {
+ 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(
cx.view().clone(),
"completions",
@@ -1274,151 +1301,69 @@ 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,
- // )
-
- // todo!("documentation")
- // MouseEventHandler::new::<CompletionTag, _>(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()
- //
+ 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(
+ &completion.label.text,
+ &style.text,
+ highlights,
+ ));
+ let documentation_label =
+ if let Some(Documentation::SingleLine(text)) = documentation {
+ Some(SharedString::from(text.clone()))
+ } else {
+ None
+ };
+
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(SharedString::from(completion.label.text.clone()))
- .min_w(px(300.))
- .max_w(px(700.))
+ .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));
+ }),
+ )
+ .child(completion_label)
+ .children(documentation_label)
})
.collect()
},
)
+ .max_h(max_height)
.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::<MultiLineDocumentation>(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::<MultiLineDocumentation>(0, None, cx)
- // .with_child(render_parsed_markdown::<MultiLineDocumentation>(
- // 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)
+ })
+ .into_any_element()
}
pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
@@ -1535,6 +1480,7 @@ impl CodeActionsMenu {
&self,
mut cursor_position: DisplayPoint,
style: &EditorStyle,
+ max_height: Pixels,
cx: &mut ViewContext<Editor>,
) -> (DisplayPoint, AnyElement) {
let actions = self.actions.clone();
@@ -1589,6 +1535,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
@@ -3695,135 +3642,135 @@ impl Editor {
self.completion_tasks.push((id, task));
}
- // pub fn confirm_completion(
- // &mut self,
- // action: &ConfirmCompletion,
- // cx: &mut ViewContext<Self>,
- // ) -> Option<Task<Result<()>>> {
- // use language::ToOffset as _;
+ pub fn confirm_completion(
+ &mut self,
+ action: &ConfirmCompletion,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Task<Result<()>>> {
+ 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::<usize>(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::<String>();
-
- // 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::<usize>(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::<String>();
- // 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<Range<isize>> = 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<Range<isize>> = 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<Self>) {
let mut context_menu = self.context_menu.write();
@@ -4446,12 +4393,14 @@ impl Editor {
&self,
cursor_position: DisplayPoint,
style: &EditorStyle,
+ max_height: Pixels,
cx: &mut ViewContext<Editor>,
) -> 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,
)
@@ -10078,75 +10027,29 @@ pub fn diagnostic_style(
}
}
-pub fn combine_syntax_and_fuzzy_match_highlights(
+pub fn text_runs_for_highlights(
text: &str,
- default_style: HighlightStyle,
- syntax_ranges: impl Iterator<Item = (Range<usize>, HighlightStyle)>,
- match_indices: &[usize],
-) -> Vec<(Range<usize>, HighlightStyle)> {
- let mut result = 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);
- let mut match_style = default_style;
- match_style.font_weight = Some(FontWeight::BOLD);
- result.push((match_index..end_index, match_style));
- }
-
- 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 {
- result.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);
- result.push((match_index..end_index, match_style));
- offset = end_index;
- }
-
- if offset < range.end {
- result.push((offset..range.end, syntax_highlight));
- }
+ default_style: &TextStyle,
+ highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
+) -> Vec<TextRun> {
+ let mut runs = Vec::new();
+ 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)
+ .to_run(range.len()),
+ );
+ ix = range.end;
}
-
- fn char_ix_after(ix: usize, text: &str) -> usize {
- ix + text[ix..].chars().next().unwrap().len_utf8()
+ if ix < text.len() {
+ runs.push(default_style.to_run(text.len() - ix));
}
-
- result
+ runs
}
pub fn styled_runs_for_code_label<'a>(
@@ -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::{
@@ -168,7 +171,8 @@ impl Default for TextStyle {
}
impl TextStyle {
- pub fn highlight(mut self, style: HighlightStyle) -> Self {
+ pub fn highlight(mut self, style: impl Into<HighlightStyle>) -> Self {
+ let style = style.into();
if let Some(weight) = style.font_weight {
self.font_weight = weight;
}
@@ -502,6 +506,24 @@ impl From<Hsla> for HighlightStyle {
}
}
+impl From<FontWeight> for HighlightStyle {
+ fn from(font_weight: FontWeight) -> Self {
+ Self {
+ font_weight: Some(font_weight),
+ ..Default::default()
+ }
+ }
+}
+
+impl From<FontStyle> for HighlightStyle {
+ fn from(font_style: FontStyle) -> Self {
+ Self {
+ font_style: Some(font_style),
+ ..Default::default()
+ }
+ }
+}
+
impl From<Rgba> for HighlightStyle {
fn from(color: Rgba) -> Self {
Self {
@@ -510,3 +532,140 @@ impl From<Rgba> for HighlightStyle {
}
}
}
+
+pub fn combine_highlights(
+ a: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
+ b: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
+) -> impl Iterator<Item = (Range<usize>, 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::<Vec<_>>(),
+ [
+ (
+ 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()
+ }
+ )
+ ]
+ );
+ }
+}