Merge branch 'main' into divs
This commit is contained in:
commit
d375f7992d
277 changed files with 19044 additions and 8896 deletions
|
@ -353,19 +353,26 @@ impl DisplaySnapshot {
|
|||
}
|
||||
}
|
||||
|
||||
// used by line_mode selections and tries to match vim behaviour
|
||||
pub fn expand_to_line(&self, range: Range<Point>) -> Range<Point> {
|
||||
let mut new_start = self.prev_line_boundary(range.start).0;
|
||||
let mut new_end = self.next_line_boundary(range.end).0;
|
||||
let new_start = if range.start.row == 0 {
|
||||
Point::new(0, 0)
|
||||
} else if range.start.row == self.max_buffer_row()
|
||||
|| (range.end.column > 0 && range.end.row == self.max_buffer_row())
|
||||
{
|
||||
Point::new(range.start.row - 1, self.line_len(range.start.row - 1))
|
||||
} else {
|
||||
self.prev_line_boundary(range.start).0
|
||||
};
|
||||
|
||||
if new_start.row == range.start.row && new_end.row == range.end.row {
|
||||
if new_end.row < self.buffer_snapshot.max_point().row {
|
||||
new_end.row += 1;
|
||||
new_end.column = 0;
|
||||
} else if new_start.row > 0 {
|
||||
new_start.row -= 1;
|
||||
new_start.column = self.buffer_snapshot.line_len(new_start.row);
|
||||
}
|
||||
}
|
||||
let new_end = if range.end.column == 0 {
|
||||
range.end
|
||||
} else if range.end.row < self.max_buffer_row() {
|
||||
self.buffer_snapshot
|
||||
.clip_point(Point::new(range.end.row + 1, 0), Bias::Left)
|
||||
} else {
|
||||
self.buffer_snapshot.max_point()
|
||||
};
|
||||
|
||||
new_start..new_end
|
||||
}
|
||||
|
|
|
@ -302,10 +302,11 @@ actions!(
|
|||
Hover,
|
||||
Format,
|
||||
ToggleSoftWrap,
|
||||
ToggleInlayHints,
|
||||
RevealInFinder,
|
||||
CopyPath,
|
||||
CopyRelativePath,
|
||||
CopyHighlightJson
|
||||
CopyHighlightJson,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -446,6 +447,7 @@ pub fn init(cx: &mut AppContext) {
|
|||
cx.add_action(Editor::toggle_code_actions);
|
||||
cx.add_action(Editor::open_excerpts);
|
||||
cx.add_action(Editor::toggle_soft_wrap);
|
||||
cx.add_action(Editor::toggle_inlay_hints);
|
||||
cx.add_action(Editor::reveal_in_finder);
|
||||
cx.add_action(Editor::copy_path);
|
||||
cx.add_action(Editor::copy_relative_path);
|
||||
|
@ -575,6 +577,7 @@ pub struct Editor {
|
|||
searchable: bool,
|
||||
cursor_shape: CursorShape,
|
||||
collapse_matches: bool,
|
||||
autoindent_mode: Option<AutoindentMode>,
|
||||
workspace: Option<(WeakViewHandle<Workspace>, i64)>,
|
||||
keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
|
||||
input_enabled: bool,
|
||||
|
@ -867,7 +870,7 @@ impl CompletionsMenu {
|
|||
let completion = &completions[mat.candidate_id];
|
||||
let item_ix = start_ix + ix;
|
||||
items.push(
|
||||
MouseEventHandler::<CompletionTag, _>::new(
|
||||
MouseEventHandler::new::<CompletionTag, _>(
|
||||
mat.candidate_id,
|
||||
cx,
|
||||
|state, _| {
|
||||
|
@ -1044,7 +1047,7 @@ impl CodeActionsMenu {
|
|||
for (ix, action) in actions[range].iter().enumerate() {
|
||||
let item_ix = start_ix + ix;
|
||||
items.push(
|
||||
MouseEventHandler::<ActionTag, _>::new(item_ix, cx, |state, _| {
|
||||
MouseEventHandler::new::<ActionTag, _>(item_ix, cx, |state, _| {
|
||||
let item_style = if item_ix == selected_item {
|
||||
style.autocomplete.selected_item
|
||||
} else if state.hovered() {
|
||||
|
@ -1237,7 +1240,8 @@ enum GotoDefinitionKind {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum InlayRefreshReason {
|
||||
enum InlayHintRefreshReason {
|
||||
Toggle(bool),
|
||||
SettingsChange(InlayHintSettings),
|
||||
NewLinesShown,
|
||||
BufferEdited(HashSet<Arc<Language>>),
|
||||
|
@ -1354,8 +1358,8 @@ impl Editor {
|
|||
}));
|
||||
}
|
||||
project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| {
|
||||
if let project::Event::RefreshInlays = event {
|
||||
editor.refresh_inlays(InlayRefreshReason::RefreshRequested, cx);
|
||||
if let project::Event::RefreshInlayHints = event {
|
||||
editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
@ -1409,6 +1413,7 @@ impl Editor {
|
|||
searchable: true,
|
||||
override_text_style: None,
|
||||
cursor_shape: Default::default(),
|
||||
autoindent_mode: Some(AutoindentMode::EachLine),
|
||||
collapse_matches: false,
|
||||
workspace: None,
|
||||
keymap_context_layers: Default::default(),
|
||||
|
@ -1587,6 +1592,14 @@ impl Editor {
|
|||
self.input_enabled = input_enabled;
|
||||
}
|
||||
|
||||
pub fn set_autoindent(&mut self, autoindent: bool) {
|
||||
if autoindent {
|
||||
self.autoindent_mode = Some(AutoindentMode::EachLine);
|
||||
} else {
|
||||
self.autoindent_mode = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_read_only(&mut self, read_only: bool) {
|
||||
self.read_only = read_only;
|
||||
}
|
||||
|
@ -1719,7 +1732,7 @@ impl Editor {
|
|||
}
|
||||
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(edits, Some(AutoindentMode::EachLine), cx)
|
||||
buffer.edit(edits, self.autoindent_mode.clone(), cx)
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2090,12 +2103,12 @@ impl Editor {
|
|||
for (selection, autoclose_region) in
|
||||
self.selections_with_autoclose_regions(selections, &snapshot)
|
||||
{
|
||||
if let Some(language) = snapshot.language_scope_at(selection.head()) {
|
||||
if let Some(scope) = snapshot.language_scope_at(selection.head()) {
|
||||
// Determine if the inserted text matches the opening or closing
|
||||
// bracket of any of this language's bracket pairs.
|
||||
let mut bracket_pair = None;
|
||||
let mut is_bracket_pair_start = false;
|
||||
for (pair, enabled) in language.brackets() {
|
||||
for (pair, enabled) in scope.brackets() {
|
||||
if enabled && pair.close && pair.start.ends_with(text.as_ref()) {
|
||||
bracket_pair = Some(pair.clone());
|
||||
is_bracket_pair_start = true;
|
||||
|
@ -2117,7 +2130,7 @@ impl Editor {
|
|||
let following_text_allows_autoclose = snapshot
|
||||
.chars_at(selection.start)
|
||||
.next()
|
||||
.map_or(true, |c| language.should_autoclose_before(c));
|
||||
.map_or(true, |c| scope.should_autoclose_before(c));
|
||||
let preceding_text_matches_prefix = prefix_len == 0
|
||||
|| (selection.start.column >= (prefix_len as u32)
|
||||
&& snapshot.contains_str_at(
|
||||
|
@ -2194,7 +2207,7 @@ impl Editor {
|
|||
drop(snapshot);
|
||||
self.transact(cx, |this, cx| {
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
|
||||
buffer.edit(edits, this.autoindent_mode.clone(), cx);
|
||||
});
|
||||
|
||||
let new_anchor_selections = new_selections.iter().map(|e| &e.0);
|
||||
|
@ -2654,7 +2667,6 @@ impl Editor {
|
|||
false
|
||||
});
|
||||
}
|
||||
|
||||
fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option<String> {
|
||||
let offset = position.to_offset(buffer);
|
||||
let (word_range, kind) = buffer.surrounding_word(offset);
|
||||
|
@ -2669,13 +2681,41 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
|
||||
fn refresh_inlays(&mut self, reason: InlayRefreshReason, cx: &mut ViewContext<Self>) {
|
||||
pub fn toggle_inlay_hints(&mut self, _: &ToggleInlayHints, cx: &mut ViewContext<Self>) {
|
||||
self.refresh_inlay_hints(
|
||||
InlayHintRefreshReason::Toggle(!self.inlay_hint_cache.enabled),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn inlay_hints_enabled(&self) -> bool {
|
||||
self.inlay_hint_cache.enabled
|
||||
}
|
||||
|
||||
fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut ViewContext<Self>) {
|
||||
if self.project.is_none() || self.mode != EditorMode::Full {
|
||||
return;
|
||||
}
|
||||
|
||||
let (invalidate_cache, required_languages) = match reason {
|
||||
InlayRefreshReason::SettingsChange(new_settings) => {
|
||||
InlayHintRefreshReason::Toggle(enabled) => {
|
||||
self.inlay_hint_cache.enabled = enabled;
|
||||
if enabled {
|
||||
(InvalidationStrategy::RefreshRequested, None)
|
||||
} else {
|
||||
self.inlay_hint_cache.clear();
|
||||
self.splice_inlay_hints(
|
||||
self.visible_inlay_hints(cx)
|
||||
.iter()
|
||||
.map(|inlay| inlay.id)
|
||||
.collect(),
|
||||
Vec::new(),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
InlayHintRefreshReason::SettingsChange(new_settings) => {
|
||||
match self.inlay_hint_cache.update_settings(
|
||||
&self.buffer,
|
||||
new_settings,
|
||||
|
@ -2693,11 +2733,13 @@ impl Editor {
|
|||
ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None),
|
||||
}
|
||||
}
|
||||
InlayRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
|
||||
InlayRefreshReason::BufferEdited(buffer_languages) => {
|
||||
InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
|
||||
InlayHintRefreshReason::BufferEdited(buffer_languages) => {
|
||||
(InvalidationStrategy::BufferEdited, Some(buffer_languages))
|
||||
}
|
||||
InlayRefreshReason::RefreshRequested => (InvalidationStrategy::RefreshRequested, None),
|
||||
InlayHintRefreshReason::RefreshRequested => {
|
||||
(InvalidationStrategy::RefreshRequested, None)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(InlaySplice {
|
||||
|
@ -2723,7 +2765,7 @@ impl Editor {
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn excerpt_visible_offsets(
|
||||
pub fn excerpt_visible_offsets(
|
||||
&self,
|
||||
restrict_to_languages: Option<&HashSet<Arc<Language>>>,
|
||||
cx: &mut ViewContext<'_, '_, Editor>,
|
||||
|
@ -2774,6 +2816,7 @@ impl Editor {
|
|||
self.display_map.update(cx, |display_map, cx| {
|
||||
display_map.splice_inlays(to_remove, to_insert, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn trigger_on_type_formatting(
|
||||
|
@ -3003,7 +3046,7 @@ impl Editor {
|
|||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
ranges.iter().map(|range| (range.clone(), text)),
|
||||
Some(AutoindentMode::EachLine),
|
||||
this.autoindent_mode.clone(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
@ -3547,7 +3590,7 @@ impl Editor {
|
|||
if self.available_code_actions.is_some() {
|
||||
enum CodeActions {}
|
||||
Some(
|
||||
MouseEventHandler::<CodeActions, _>::new(0, cx, |state, _| {
|
||||
MouseEventHandler::new::<CodeActions, _>(0, cx, |state, _| {
|
||||
Svg::new("icons/bolt_8.svg").with_color(
|
||||
style
|
||||
.code_actions
|
||||
|
@ -3594,7 +3637,7 @@ impl Editor {
|
|||
fold_data
|
||||
.map(|(fold_status, buffer_row, active)| {
|
||||
(active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
|
||||
MouseEventHandler::<FoldIndicators, _>::new(
|
||||
MouseEventHandler::new::<FoldIndicators, _>(
|
||||
ix as usize,
|
||||
cx,
|
||||
|mouse_state, _| {
|
||||
|
@ -7696,8 +7739,8 @@ impl Editor {
|
|||
.cloned()
|
||||
.collect::<HashSet<_>>();
|
||||
if !languages_affected.is_empty() {
|
||||
self.refresh_inlays(
|
||||
InlayRefreshReason::BufferEdited(languages_affected),
|
||||
self.refresh_inlay_hints(
|
||||
InlayHintRefreshReason::BufferEdited(languages_affected),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
@ -7735,8 +7778,8 @@ impl Editor {
|
|||
|
||||
fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.refresh_copilot_suggestions(true, cx);
|
||||
self.refresh_inlays(
|
||||
InlayRefreshReason::SettingsChange(inlay_hint_settings(
|
||||
self.refresh_inlay_hints(
|
||||
InlayHintRefreshReason::SettingsChange(inlay_hint_settings(
|
||||
self.selections.newest_anchor().head(),
|
||||
&self.buffer.read(cx).snapshot(cx),
|
||||
cx,
|
||||
|
@ -8664,7 +8707,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
|
|||
let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
|
||||
let anchor_x = cx.anchor_x;
|
||||
enum BlockContextToolip {}
|
||||
MouseEventHandler::<BlockContext, _>::new(cx.block_id, cx, |_, _| {
|
||||
MouseEventHandler::new::<BlockContext, _>(cx.block_id, cx, |_, _| {
|
||||
Flex::column()
|
||||
.with_children(highlighted_lines.iter().map(|(line, highlights)| {
|
||||
Label::new(
|
||||
|
|
|
@ -5237,6 +5237,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
|||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
resolve_provider: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
|
@ -7528,6 +7529,7 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
|
|||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string()]),
|
||||
resolve_provider: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
|
|
|
@ -63,6 +63,7 @@ struct SelectionLayout {
|
|||
cursor_shape: CursorShape,
|
||||
is_newest: bool,
|
||||
range: Range<DisplayPoint>,
|
||||
active_rows: Range<u32>,
|
||||
}
|
||||
|
||||
impl SelectionLayout {
|
||||
|
@ -73,25 +74,44 @@ impl SelectionLayout {
|
|||
map: &DisplaySnapshot,
|
||||
is_newest: bool,
|
||||
) -> Self {
|
||||
let point_selection = selection.map(|p| p.to_point(&map.buffer_snapshot));
|
||||
let display_selection = point_selection.map(|p| p.to_display_point(map));
|
||||
let mut range = display_selection.range();
|
||||
let mut head = display_selection.head();
|
||||
let mut active_rows = map.prev_line_boundary(point_selection.start).1.row()
|
||||
..map.next_line_boundary(point_selection.end).1.row();
|
||||
|
||||
// vim visual line mode
|
||||
if line_mode {
|
||||
let selection = selection.map(|p| p.to_point(&map.buffer_snapshot));
|
||||
let point_range = map.expand_to_line(selection.range());
|
||||
Self {
|
||||
head: selection.head().to_display_point(map),
|
||||
cursor_shape,
|
||||
is_newest,
|
||||
range: point_range.start.to_display_point(map)
|
||||
..point_range.end.to_display_point(map),
|
||||
}
|
||||
} else {
|
||||
let selection = selection.map(|p| p.to_display_point(map));
|
||||
Self {
|
||||
head: selection.head(),
|
||||
cursor_shape,
|
||||
is_newest,
|
||||
range: selection.range(),
|
||||
let point_range = map.expand_to_line(point_selection.range());
|
||||
range = point_range.start.to_display_point(map)..point_range.end.to_display_point(map);
|
||||
}
|
||||
|
||||
// any vim visual mode (including line mode)
|
||||
if cursor_shape == CursorShape::Block && !range.is_empty() && !selection.reversed {
|
||||
if head.column() > 0 {
|
||||
head = map.clip_point(DisplayPoint::new(head.row(), head.column() - 1), Bias::Left)
|
||||
} else if head.row() > 0 && head != map.max_point() {
|
||||
head = map.clip_point(
|
||||
DisplayPoint::new(head.row() - 1, map.line_len(head.row() - 1)),
|
||||
Bias::Left,
|
||||
);
|
||||
// updating range.end is a no-op unless you're cursor is
|
||||
// on the newline containing a multi-buffer divider
|
||||
// in which case the clip_point may have moved the head up
|
||||
// an additional row.
|
||||
range.end = DisplayPoint::new(head.row() + 1, 0);
|
||||
active_rows.end = head.row();
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
head,
|
||||
cursor_shape,
|
||||
is_newest,
|
||||
range,
|
||||
active_rows,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1637,7 +1657,7 @@ impl EditorElement {
|
|||
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
|
||||
|
||||
enum JumpIcon {}
|
||||
MouseEventHandler::<JumpIcon, _>::new((*id).into(), cx, |state, _| {
|
||||
MouseEventHandler::new::<JumpIcon, _>((*id).into(), cx, |state, _| {
|
||||
let style = style.jump_icon.style_for(state);
|
||||
Svg::new("icons/arrow_up_right_8.svg")
|
||||
.with_color(style.color)
|
||||
|
@ -2152,22 +2172,37 @@ impl Element<Editor> for EditorElement {
|
|||
}
|
||||
selections.extend(remote_selections);
|
||||
|
||||
let mut newest_selection_head = None;
|
||||
|
||||
if editor.show_local_selections {
|
||||
let mut local_selections = editor
|
||||
let mut local_selections: Vec<Selection<Point>> = editor
|
||||
.selections
|
||||
.disjoint_in_range(start_anchor..end_anchor, cx);
|
||||
local_selections.extend(editor.selections.pending(cx));
|
||||
let mut layouts = Vec::new();
|
||||
let newest = editor.selections.newest(cx);
|
||||
for selection in &local_selections {
|
||||
for selection in local_selections.drain(..) {
|
||||
let is_empty = selection.start == selection.end;
|
||||
let selection_start = snapshot.prev_line_boundary(selection.start).1;
|
||||
let selection_end = snapshot.next_line_boundary(selection.end).1;
|
||||
for row in cmp::max(selection_start.row(), start_row)
|
||||
..=cmp::min(selection_end.row(), end_row)
|
||||
let is_newest = selection == newest;
|
||||
|
||||
let layout = SelectionLayout::new(
|
||||
selection,
|
||||
editor.selections.line_mode,
|
||||
editor.cursor_shape,
|
||||
&snapshot.display_snapshot,
|
||||
is_newest,
|
||||
);
|
||||
if is_newest {
|
||||
newest_selection_head = Some(layout.head);
|
||||
}
|
||||
|
||||
for row in cmp::max(layout.active_rows.start, start_row)
|
||||
..=cmp::min(layout.active_rows.end, end_row)
|
||||
{
|
||||
let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty);
|
||||
*contains_non_empty_selection |= !is_empty;
|
||||
}
|
||||
layouts.push(layout);
|
||||
}
|
||||
|
||||
// Render the local selections in the leader's color when following.
|
||||
|
@ -2175,22 +2210,7 @@ impl Element<Editor> for EditorElement {
|
|||
.leader_replica_id
|
||||
.unwrap_or_else(|| editor.replica_id(cx));
|
||||
|
||||
selections.push((
|
||||
local_replica_id,
|
||||
local_selections
|
||||
.into_iter()
|
||||
.map(|selection| {
|
||||
let is_newest = selection == newest;
|
||||
SelectionLayout::new(
|
||||
selection,
|
||||
editor.selections.line_mode,
|
||||
editor.cursor_shape,
|
||||
&snapshot.display_snapshot,
|
||||
is_newest,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
));
|
||||
selections.push((local_replica_id, layouts));
|
||||
}
|
||||
|
||||
let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
|
||||
|
@ -2295,28 +2315,26 @@ impl Element<Editor> for EditorElement {
|
|||
snapshot = editor.snapshot(cx);
|
||||
}
|
||||
|
||||
let newest_selection_head = editor
|
||||
.selections
|
||||
.newest::<usize>(cx)
|
||||
.head()
|
||||
.to_display_point(&snapshot);
|
||||
let style = editor.style(cx);
|
||||
|
||||
let mut context_menu = None;
|
||||
let mut code_actions_indicator = None;
|
||||
if (start_row..end_row).contains(&newest_selection_head.row()) {
|
||||
if editor.context_menu_visible() {
|
||||
context_menu = editor.render_context_menu(newest_selection_head, style.clone(), cx);
|
||||
if let Some(newest_selection_head) = newest_selection_head {
|
||||
if (start_row..end_row).contains(&newest_selection_head.row()) {
|
||||
if editor.context_menu_visible() {
|
||||
context_menu =
|
||||
editor.render_context_menu(newest_selection_head, style.clone(), cx);
|
||||
}
|
||||
|
||||
let active = matches!(
|
||||
editor.context_menu,
|
||||
Some(crate::ContextMenu::CodeActions(_))
|
||||
);
|
||||
|
||||
code_actions_indicator = editor
|
||||
.render_code_actions_indicator(&style, active, cx)
|
||||
.map(|indicator| (newest_selection_head.row(), indicator));
|
||||
}
|
||||
|
||||
let active = matches!(
|
||||
editor.context_menu,
|
||||
Some(crate::ContextMenu::CodeActions(_))
|
||||
);
|
||||
|
||||
code_actions_indicator = editor
|
||||
.render_code_actions_indicator(&style, active, cx)
|
||||
.map(|indicator| (newest_selection_head.row(), indicator));
|
||||
}
|
||||
|
||||
let visible_rows = start_row..start_row + line_layouts.len() as u32;
|
||||
|
@ -2995,6 +3013,154 @@ mod tests {
|
|||
assert_eq!(layouts.len(), 6);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_vim_visual_selections(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let editor = cx
|
||||
.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, None, cx)
|
||||
})
|
||||
.root(cx);
|
||||
let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
|
||||
let (_, state) = editor.update(cx, |editor, cx| {
|
||||
editor.cursor_shape = CursorShape::Block;
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([
|
||||
Point::new(0, 0)..Point::new(1, 0),
|
||||
Point::new(3, 2)..Point::new(3, 3),
|
||||
Point::new(5, 6)..Point::new(6, 0),
|
||||
]);
|
||||
});
|
||||
let mut new_parents = Default::default();
|
||||
let mut notify_views_if_parents_change = Default::default();
|
||||
let mut layout_cx = LayoutContext::new(
|
||||
cx,
|
||||
&mut new_parents,
|
||||
&mut notify_views_if_parents_change,
|
||||
false,
|
||||
);
|
||||
element.layout(
|
||||
SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),
|
||||
editor,
|
||||
&mut layout_cx,
|
||||
)
|
||||
});
|
||||
assert_eq!(state.selections.len(), 1);
|
||||
let local_selections = &state.selections[0].1;
|
||||
assert_eq!(local_selections.len(), 3);
|
||||
// moves cursor back one line
|
||||
assert_eq!(local_selections[0].head, DisplayPoint::new(0, 6));
|
||||
assert_eq!(
|
||||
local_selections[0].range,
|
||||
DisplayPoint::new(0, 0)..DisplayPoint::new(1, 0)
|
||||
);
|
||||
|
||||
// moves cursor back one column
|
||||
assert_eq!(
|
||||
local_selections[1].range,
|
||||
DisplayPoint::new(3, 2)..DisplayPoint::new(3, 3)
|
||||
);
|
||||
assert_eq!(local_selections[1].head, DisplayPoint::new(3, 2));
|
||||
|
||||
// leaves cursor on the max point
|
||||
assert_eq!(
|
||||
local_selections[2].range,
|
||||
DisplayPoint::new(5, 6)..DisplayPoint::new(6, 0)
|
||||
);
|
||||
assert_eq!(local_selections[2].head, DisplayPoint::new(6, 0));
|
||||
|
||||
// active lines does not include 1 (even though the range of the selection does)
|
||||
assert_eq!(
|
||||
state.active_rows.keys().cloned().collect::<Vec<u32>>(),
|
||||
vec![0, 3, 5, 6]
|
||||
);
|
||||
|
||||
// multi-buffer support
|
||||
// in DisplayPoint co-ordinates, this is what we're dealing with:
|
||||
// 0: [[file
|
||||
// 1: header]]
|
||||
// 2: aaaaaa
|
||||
// 3: bbbbbb
|
||||
// 4: cccccc
|
||||
// 5:
|
||||
// 6: ...
|
||||
// 7: ffffff
|
||||
// 8: gggggg
|
||||
// 9: hhhhhh
|
||||
// 10:
|
||||
// 11: [[file
|
||||
// 12: header]]
|
||||
// 13: bbbbbb
|
||||
// 14: cccccc
|
||||
// 15: dddddd
|
||||
let editor = cx
|
||||
.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_multi(
|
||||
[
|
||||
(
|
||||
&(sample_text(8, 6, 'a') + "\n"),
|
||||
vec![
|
||||
Point::new(0, 0)..Point::new(3, 0),
|
||||
Point::new(4, 0)..Point::new(7, 0),
|
||||
],
|
||||
),
|
||||
(
|
||||
&(sample_text(8, 6, 'a') + "\n"),
|
||||
vec![Point::new(1, 0)..Point::new(3, 0)],
|
||||
),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
Editor::new(EditorMode::Full, buffer, None, None, cx)
|
||||
})
|
||||
.root(cx);
|
||||
let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
|
||||
let (_, state) = editor.update(cx, |editor, cx| {
|
||||
editor.cursor_shape = CursorShape::Block;
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_display_ranges([
|
||||
DisplayPoint::new(4, 0)..DisplayPoint::new(7, 0),
|
||||
DisplayPoint::new(10, 0)..DisplayPoint::new(13, 0),
|
||||
]);
|
||||
});
|
||||
let mut new_parents = Default::default();
|
||||
let mut notify_views_if_parents_change = Default::default();
|
||||
let mut layout_cx = LayoutContext::new(
|
||||
cx,
|
||||
&mut new_parents,
|
||||
&mut notify_views_if_parents_change,
|
||||
false,
|
||||
);
|
||||
element.layout(
|
||||
SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),
|
||||
editor,
|
||||
&mut layout_cx,
|
||||
)
|
||||
});
|
||||
|
||||
assert_eq!(state.selections.len(), 1);
|
||||
let local_selections = &state.selections[0].1;
|
||||
assert_eq!(local_selections.len(), 2);
|
||||
|
||||
// moves cursor on excerpt boundary back a line
|
||||
// and doesn't allow selection to bleed through
|
||||
assert_eq!(
|
||||
local_selections[0].range,
|
||||
DisplayPoint::new(4, 0)..DisplayPoint::new(6, 0)
|
||||
);
|
||||
assert_eq!(local_selections[0].head, DisplayPoint::new(5, 0));
|
||||
|
||||
// moves cursor on buffer boundary back two lines
|
||||
// and doesn't allow selection to bleed through
|
||||
assert_eq!(
|
||||
local_selections[1].range,
|
||||
DisplayPoint::new(10, 0)..DisplayPoint::new(11, 0)
|
||||
);
|
||||
assert_eq!(local_selections[1].head, DisplayPoint::new(10, 0));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
|
|
@ -565,7 +565,7 @@ impl InfoPopover {
|
|||
)
|
||||
});
|
||||
|
||||
MouseEventHandler::<InfoPopover, _>::new(0, cx, |_, cx| {
|
||||
MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
|
||||
let mut region_id = 0;
|
||||
let view_id = cx.view_id();
|
||||
|
||||
|
@ -654,7 +654,7 @@ impl DiagnosticPopover {
|
|||
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
|
||||
MouseEventHandler::<DiagnosticPopover, _>::new(0, cx, |_, _| {
|
||||
MouseEventHandler::new::<DiagnosticPopover, _>(0, cx, |_, _| {
|
||||
text.with_soft_wrap(true)
|
||||
.contained()
|
||||
.with_style(container_style)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1028,7 +1028,7 @@ impl SearchableItem for Editor {
|
|||
if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
|
||||
ranges.extend(
|
||||
query
|
||||
.search(excerpt_buffer.as_rope())
|
||||
.search(excerpt_buffer, None)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
|
@ -1038,17 +1038,22 @@ impl SearchableItem for Editor {
|
|||
} else {
|
||||
for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
|
||||
let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer);
|
||||
let rope = excerpt.buffer.as_rope().slice(excerpt_range.clone());
|
||||
ranges.extend(query.search(&rope).await.into_iter().map(|range| {
|
||||
let start = excerpt
|
||||
.buffer
|
||||
.anchor_after(excerpt_range.start + range.start);
|
||||
let end = excerpt
|
||||
.buffer
|
||||
.anchor_before(excerpt_range.start + range.end);
|
||||
buffer.anchor_in_excerpt(excerpt.id.clone(), start)
|
||||
..buffer.anchor_in_excerpt(excerpt.id.clone(), end)
|
||||
}));
|
||||
ranges.extend(
|
||||
query
|
||||
.search(&excerpt.buffer, Some(excerpt_range.clone()))
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
let start = excerpt
|
||||
.buffer
|
||||
.anchor_after(excerpt_range.start + range.start);
|
||||
let end = excerpt
|
||||
.buffer
|
||||
.anchor_before(excerpt_range.start + range.end);
|
||||
buffer.anchor_in_excerpt(excerpt.id.clone(), start)
|
||||
..buffer.anchor_in_excerpt(excerpt.id.clone(), end)
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
ranges
|
||||
|
|
|
@ -13,6 +13,13 @@ pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
|||
map.clip_point(point, Bias::Left)
|
||||
}
|
||||
|
||||
pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||
if point.column() > 0 {
|
||||
*point.column_mut() -= 1;
|
||||
}
|
||||
map.clip_point(point, Bias::Left)
|
||||
}
|
||||
|
||||
pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||
let max_column = map.line_len(point.row());
|
||||
if point.column() < max_column {
|
||||
|
@ -24,6 +31,11 @@ pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
|||
map.clip_point(point, Bias::Right)
|
||||
}
|
||||
|
||||
pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||
*point.column_mut() += 1;
|
||||
map.clip_point(point, Bias::Right)
|
||||
}
|
||||
|
||||
pub fn up(
|
||||
map: &DisplaySnapshot,
|
||||
start: DisplayPoint,
|
||||
|
@ -49,10 +61,10 @@ pub fn up_by_rows(
|
|||
goal: SelectionGoal,
|
||||
preserve_column_at_start: bool,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let mut goal_column = if let SelectionGoal::Column(column) = goal {
|
||||
column
|
||||
} else {
|
||||
map.column_to_chars(start.row(), start.column())
|
||||
let mut goal_column = match goal {
|
||||
SelectionGoal::Column(column) => column,
|
||||
SelectionGoal::ColumnRange { end, .. } => end,
|
||||
_ => map.column_to_chars(start.row(), start.column()),
|
||||
};
|
||||
|
||||
let prev_row = start.row().saturating_sub(row_count);
|
||||
|
@ -83,10 +95,10 @@ pub fn down_by_rows(
|
|||
goal: SelectionGoal,
|
||||
preserve_column_at_end: bool,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let mut goal_column = if let SelectionGoal::Column(column) = goal {
|
||||
column
|
||||
} else {
|
||||
map.column_to_chars(start.row(), start.column())
|
||||
let mut goal_column = match goal {
|
||||
SelectionGoal::Column(column) => column,
|
||||
SelectionGoal::ColumnRange { end, .. } => end,
|
||||
_ => map.column_to_chars(start.row(), start.column()),
|
||||
};
|
||||
|
||||
let new_row = start.row() + row_count;
|
||||
|
@ -164,14 +176,21 @@ pub fn line_end(
|
|||
}
|
||||
|
||||
pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||
let raw_point = point.to_point(map);
|
||||
let language = map.buffer_snapshot.language_at(raw_point);
|
||||
|
||||
find_preceding_boundary(map, point, |left, right| {
|
||||
(char_kind(left) != char_kind(right) && !right.is_whitespace()) || left == '\n'
|
||||
(char_kind(language, left) != char_kind(language, right) && !right.is_whitespace())
|
||||
|| left == '\n'
|
||||
})
|
||||
}
|
||||
|
||||
pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||
let raw_point = point.to_point(map);
|
||||
let language = map.buffer_snapshot.language_at(raw_point);
|
||||
find_preceding_boundary(map, point, |left, right| {
|
||||
let is_word_start = char_kind(left) != char_kind(right) && !right.is_whitespace();
|
||||
let is_word_start =
|
||||
char_kind(language, left) != char_kind(language, right) && !right.is_whitespace();
|
||||
let is_subword_start =
|
||||
left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
|
||||
is_word_start || is_subword_start || left == '\n'
|
||||
|
@ -179,14 +198,20 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis
|
|||
}
|
||||
|
||||
pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||
let raw_point = point.to_point(map);
|
||||
let language = map.buffer_snapshot.language_at(raw_point);
|
||||
find_boundary(map, point, |left, right| {
|
||||
(char_kind(left) != char_kind(right) && !left.is_whitespace()) || right == '\n'
|
||||
(char_kind(language, left) != char_kind(language, right) && !left.is_whitespace())
|
||||
|| right == '\n'
|
||||
})
|
||||
}
|
||||
|
||||
pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||
let raw_point = point.to_point(map);
|
||||
let language = map.buffer_snapshot.language_at(raw_point);
|
||||
find_boundary(map, point, |left, right| {
|
||||
let is_word_end = (char_kind(left) != char_kind(right)) && !left.is_whitespace();
|
||||
let is_word_end =
|
||||
(char_kind(language, left) != char_kind(language, right)) && !left.is_whitespace();
|
||||
let is_subword_end =
|
||||
left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
|
||||
is_word_end || is_subword_end || right == '\n'
|
||||
|
@ -373,10 +398,15 @@ pub fn find_boundary_in_line(
|
|||
}
|
||||
|
||||
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
|
||||
let raw_point = point.to_point(map);
|
||||
let language = map.buffer_snapshot.language_at(raw_point);
|
||||
let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
|
||||
let text = &map.buffer_snapshot;
|
||||
let next_char_kind = text.chars_at(ix).next().map(char_kind);
|
||||
let prev_char_kind = text.reversed_chars_at(ix).next().map(char_kind);
|
||||
let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(language, c));
|
||||
let prev_char_kind = text
|
||||
.reversed_chars_at(ix)
|
||||
.next()
|
||||
.map(|c| char_kind(language, c));
|
||||
prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
|
||||
}
|
||||
|
||||
|
|
|
@ -1565,6 +1565,25 @@ impl MultiBuffer {
|
|||
cx.add_model(|cx| Self::singleton(buffer, cx))
|
||||
}
|
||||
|
||||
pub fn build_multi<const COUNT: usize>(
|
||||
excerpts: [(&str, Vec<Range<Point>>); COUNT],
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> ModelHandle<Self> {
|
||||
let multi = cx.add_model(|_| Self::new(0));
|
||||
for (text, ranges) in excerpts {
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
|
||||
let excerpt_ranges = ranges.into_iter().map(|range| ExcerptRange {
|
||||
context: range,
|
||||
primary: None,
|
||||
});
|
||||
multi.update(cx, |multi, cx| {
|
||||
multi.push_excerpts(buffer, excerpt_ranges, cx)
|
||||
});
|
||||
}
|
||||
|
||||
multi
|
||||
}
|
||||
|
||||
pub fn build_from_buffer(
|
||||
buffer: ModelHandle<Buffer>,
|
||||
cx: &mut gpui::AppContext,
|
||||
|
@ -1846,13 +1865,16 @@ impl MultiBufferSnapshot {
|
|||
let mut end = start;
|
||||
let mut next_chars = self.chars_at(start).peekable();
|
||||
let mut prev_chars = self.reversed_chars_at(start).peekable();
|
||||
|
||||
let language = self.language_at(start);
|
||||
let kind = |c| char_kind(language, c);
|
||||
let word_kind = cmp::max(
|
||||
prev_chars.peek().copied().map(char_kind),
|
||||
next_chars.peek().copied().map(char_kind),
|
||||
prev_chars.peek().copied().map(kind),
|
||||
next_chars.peek().copied().map(kind),
|
||||
);
|
||||
|
||||
for ch in prev_chars {
|
||||
if Some(char_kind(ch)) == word_kind && ch != '\n' {
|
||||
if Some(kind(ch)) == word_kind && ch != '\n' {
|
||||
start -= ch.len_utf8();
|
||||
} else {
|
||||
break;
|
||||
|
@ -1860,7 +1882,7 @@ impl MultiBufferSnapshot {
|
|||
}
|
||||
|
||||
for ch in next_chars {
|
||||
if Some(char_kind(ch)) == word_kind && ch != '\n' {
|
||||
if Some(kind(ch)) == word_kind && ch != '\n' {
|
||||
end += ch.len_utf8();
|
||||
} else {
|
||||
break;
|
||||
|
|
|
@ -13,13 +13,13 @@ use gpui::{
|
|||
};
|
||||
use language::{Bias, Point};
|
||||
use util::ResultExt;
|
||||
use workspace::{item::Item, WorkspaceId};
|
||||
use workspace::WorkspaceId;
|
||||
|
||||
use crate::{
|
||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||
hover_popover::hide_hover,
|
||||
persistence::DB,
|
||||
Anchor, DisplayPoint, Editor, EditorMode, Event, InlayRefreshReason, MultiBufferSnapshot,
|
||||
Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot,
|
||||
ToPoint,
|
||||
};
|
||||
|
||||
|
@ -29,6 +29,7 @@ use self::{
|
|||
};
|
||||
|
||||
pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
|
||||
pub const VERTICAL_SCROLL_MARGIN: f32 = 3.;
|
||||
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -136,7 +137,7 @@ pub struct ScrollManager {
|
|||
impl ScrollManager {
|
||||
pub fn new() -> Self {
|
||||
ScrollManager {
|
||||
vertical_scroll_margin: 3.0,
|
||||
vertical_scroll_margin: VERTICAL_SCROLL_MARGIN,
|
||||
anchor: ScrollAnchor::new(),
|
||||
ongoing: OngoingScroll::new(),
|
||||
autoscroll_request: None,
|
||||
|
@ -301,7 +302,7 @@ impl Editor {
|
|||
cx.spawn(|editor, mut cx| async move {
|
||||
editor
|
||||
.update(&mut cx, |editor, cx| {
|
||||
editor.refresh_inlays(InlayRefreshReason::NewLinesShown, cx)
|
||||
editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
|
@ -333,9 +334,7 @@ impl Editor {
|
|||
cx,
|
||||
);
|
||||
|
||||
if !self.is_singleton(cx) {
|
||||
self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx);
|
||||
}
|
||||
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
|
||||
}
|
||||
|
||||
pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::{
|
||||
cell::Ref,
|
||||
cmp, iter, mem,
|
||||
ops::{Deref, Range, Sub},
|
||||
ops::{Deref, DerefMut, Range, Sub},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
|
@ -53,7 +53,7 @@ impl SelectionsCollection {
|
|||
}
|
||||
}
|
||||
|
||||
fn display_map(&self, cx: &mut AppContext) -> DisplaySnapshot {
|
||||
pub fn display_map(&self, cx: &mut AppContext) -> DisplaySnapshot {
|
||||
self.display_map.update(cx, |map, cx| map.snapshot(cx))
|
||||
}
|
||||
|
||||
|
@ -250,6 +250,10 @@ impl SelectionsCollection {
|
|||
resolve(self.oldest_anchor(), &self.buffer(cx))
|
||||
}
|
||||
|
||||
pub fn first_anchor(&self) -> Selection<Anchor> {
|
||||
self.disjoint[0].clone()
|
||||
}
|
||||
|
||||
pub fn first<D: TextDimension + Ord + Sub<D, Output = D>>(
|
||||
&self,
|
||||
cx: &AppContext,
|
||||
|
@ -352,7 +356,7 @@ pub struct MutableSelectionsCollection<'a> {
|
|||
}
|
||||
|
||||
impl<'a> MutableSelectionsCollection<'a> {
|
||||
fn display_map(&mut self) -> DisplaySnapshot {
|
||||
pub fn display_map(&mut self) -> DisplaySnapshot {
|
||||
self.collection.display_map(self.cx)
|
||||
}
|
||||
|
||||
|
@ -607,6 +611,10 @@ impl<'a> MutableSelectionsCollection<'a> {
|
|||
self.select_anchors(selections)
|
||||
}
|
||||
|
||||
pub fn new_selection_id(&mut self) -> usize {
|
||||
post_inc(&mut self.next_selection_id)
|
||||
}
|
||||
|
||||
pub fn select_display_ranges<T>(&mut self, ranges: T)
|
||||
where
|
||||
T: IntoIterator<Item = Range<DisplayPoint>>,
|
||||
|
@ -831,6 +839,12 @@ impl<'a> Deref for MutableSelectionsCollection<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> DerefMut for MutableSelectionsCollection<'a> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.collection
|
||||
}
|
||||
}
|
||||
|
||||
// Panics if passed selections are not in order
|
||||
pub fn resolve_multiple<'a, D, I>(
|
||||
selections: I,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue