Finish autocomplete (#3404)

![image](https://github.com/zed-industries/zed/assets/482957/f1f40bec-4e8b-457b-8801-ce10ecb6fc80)


TODO:

- [x] Respect max height imposed by editor element
- [x] Ensure mouse down event handler in `EditorElement` doesn't prevent
links from being clicked

Release Notes:

- N/A
This commit is contained in:
Antonio Scandurra
2023-11-27 15:24:59 +01:00
committed by GitHub
8 changed files with 519 additions and 481 deletions
+287 -384
View File
@@ -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 {}
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 parsed = parsed.clone();
// let view_id = cx.view_id();
// let code_span_background_color = editor_style.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);
// let mut region_id = 0;
// 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());
}
}
// 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)
// }
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,
// )
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
};
// 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()
//
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 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 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 newest_selection = self.selections.newest_anchor();
if newest_selection.start.buffer_id != Some(buffer_handle.read(cx).remote_id()) {
return None;
}
// 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 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 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..];
// cx.emit(Event::InputHandled {
// utf16_range_to_replace: range_to_replace,
// text: text.into(),
// });
cx.emit(EditorEvent::InputHandled {
utf16_range_to_replace: range_to_replace,
text: text.into(),
});
// 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;
// }
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;
}
// 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.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);
// });
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().spawn(async move {
// apply_edits.await?;
// Ok(())
// }))
// }
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>(
-69
View File
@@ -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,
Default::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,
+9 -10
View File
@@ -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)
@@ -1022,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
@@ -1974,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!(
+28
View File
@@ -6,6 +6,8 @@ use gpui::BackgroundExecutor;
use std::{
borrow::Cow,
cmp::{self, Ordering},
iter,
ops::Range,
sync::atomic::AtomicBool,
};
@@ -54,6 +56,32 @@ pub struct StringMatch {
pub string: String,
}
impl StringMatch {
pub fn ranges<'a>(&'a self) -> impl 'a + Iterator<Item = Range<usize>> {
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();
} else {
break;
}
}
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()
+5 -1
View File
@@ -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);
}
}
+160 -1
View File
@@ -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()
}
)
]
);
}
}
+9 -1
View File
@@ -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();
}
+21 -15
View File
@@ -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<Pixels>,
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,
);
}