diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 92f74907ab..72252edd71 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -11,22 +11,22 @@ "enter": "menu::Confirm", "escape": "menu::Cancel", "ctrl-c": "menu::Cancel", - "shift-cmd-{": "pane::ActivatePrevItem", - "shift-cmd-}": "pane::ActivateNextItem", + "cmd-{": "pane::ActivatePrevItem", + "cmd-}": "pane::ActivateNextItem", "alt-cmd-left": "pane::ActivatePrevItem", "alt-cmd-right": "pane::ActivateNextItem", "cmd-w": "pane::CloseActiveItem", - "cmd-shift-W": "workspace::CloseWindow", + "cmd-shift-w": "workspace::CloseWindow", "alt-cmd-t": "pane::CloseInactiveItems", "cmd-s": "workspace::Save", - "cmd-shift-S": "workspace::SaveAs", + "cmd-shift-s": "workspace::SaveAs", "cmd-=": "zed::IncreaseBufferFontSize", "cmd--": "zed::DecreaseBufferFontSize", "cmd-0": "zed::ResetBufferFontSize", "cmd-,": "zed::OpenSettings", "cmd-q": "zed::Quit", "cmd-n": "workspace::NewFile", - "cmd-shift-N": "workspace::NewWindow", + "cmd-shift-n": "workspace::NewWindow", "cmd-o": "workspace::Open" } }, @@ -53,7 +53,7 @@ "cmd-c": "editor::Copy", "cmd-v": "editor::Paste", "cmd-z": "editor::Undo", - "cmd-shift-Z": "editor::Redo", + "cmd-shift-z": "editor::Redo", "up": "editor::MoveUp", "down": "editor::MoveDown", "left": "editor::MoveLeft", @@ -73,17 +73,17 @@ "cmd-up": "editor::MoveToBeginning", "cmd-down": "editor::MoveToEnd", "shift-up": "editor::SelectUp", - "ctrl-shift-P": "editor::SelectUp", + "ctrl-shift-p": "editor::SelectUp", "shift-down": "editor::SelectDown", - "ctrl-shift-N": "editor::SelectDown", + "ctrl-shift-n": "editor::SelectDown", "shift-left": "editor::SelectLeft", - "ctrl-shift-B": "editor::SelectLeft", + "ctrl-shift-b": "editor::SelectLeft", "shift-right": "editor::SelectRight", - "ctrl-shift-F": "editor::SelectRight", + "ctrl-shift-f": "editor::SelectRight", "alt-shift-left": "editor::SelectToPreviousWordStart", - "alt-shift-B": "editor::SelectToPreviousWordStart", + "alt-shift-b": "editor::SelectToPreviousWordStart", "alt-shift-right": "editor::SelectToNextWordEnd", - "alt-shift-F": "editor::SelectToNextWordEnd", + "alt-shift-f": "editor::SelectToNextWordEnd", "cmd-shift-up": "editor::SelectToBeginning", "cmd-shift-down": "editor::SelectToEnd", "cmd-a": "editor::SelectAll", @@ -94,7 +94,7 @@ "stop_at_soft_wraps": true } ], - "ctrl-shift-A": [ + "ctrl-shift-a": [ "editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true @@ -106,14 +106,15 @@ "stop_at_soft_wraps": true } ], - "ctrl-shift-E": [ + "ctrl-shift-e": [ "editor::SelectToEndOfLine", { "stop_at_soft_wraps": true } ], "pageup": "editor::PageUp", - "pagedown": "editor::PageDown" + "pagedown": "editor::PageDown", + "ctrl-cmd-space": "editor::ShowCharacterPalette" } }, { @@ -137,10 +138,7 @@ { "context": "Editor && mode == auto_height", "bindings": { - "alt-enter": [ - "editor::Input", - "\n" - ] + "alt-enter": "editor::Newline" } }, { @@ -157,7 +155,7 @@ "bindings": { "cmd-f": "project_search::ToggleFocus", "cmd-g": "search::SelectNextMatch", - "cmd-shift-G": "search::SelectPrevMatch", + "cmd-shift-g": "search::SelectPrevMatch", "alt-cmd-c": "search::ToggleCaseSensitive", "alt-cmd-w": "search::ToggleWholeWord", "alt-cmd-r": "search::ToggleRegex" @@ -189,7 +187,7 @@ "alt-up": "editor::SelectLargerSyntaxNode", "alt-down": "editor::SelectSmallerSyntaxNode", "cmd-u": "editor::UndoSelection", - "cmd-shift-U": "editor::RedoSelection", + "cmd-shift-u": "editor::RedoSelection", "f8": "editor::GoToDiagnostic", "shift-f8": "editor::GoToPrevDiagnostic", "f2": "editor::Rename", @@ -205,7 +203,7 @@ { "context": "Editor && mode == full", "bindings": { - "cmd-shift-O": "outline::Toggle", + "cmd-shift-o": "outline::Toggle", "ctrl-g": "go_to_line::Toggle" } }, @@ -250,9 +248,9 @@ ], "ctrl-0": "pane::ActivateLastItem", "ctrl--": "pane::GoBack", - "shift-ctrl-_": "pane::GoForward", - "cmd-shift-T": "pane::ReopenClosedItem", - "cmd-shift-F": "project_search::ToggleFocus" + "ctrl-_": "pane::GoForward", + "cmd-shift-t": "pane::ReopenClosedItem", + "cmd-shift-f": "project_search::ToggleFocus" } }, { @@ -295,14 +293,14 @@ 8 ], "cmd-b": "workspace::ToggleLeftSidebar", - "cmd-shift-F": "project_search::Deploy", + "cmd-shift-f": "project_search::Deploy", "cmd-k cmd-t": "theme_selector::Toggle", "cmd-k cmd-s": "zed::OpenKeymap", "cmd-t": "project_symbols::Toggle", "cmd-p": "file_finder::Toggle", - "cmd-shift-P": "command_palette::Toggle", - "cmd-shift-M": "diagnostics::Deploy", - "cmd-shift-E": "project_panel::Toggle", + "cmd-shift-p": "command_palette::Toggle", + "cmd-shift-m": "diagnostics::Deploy", + "cmd-shift-e": "project_panel::Toggle", "cmd-alt-s": "workspace::SaveAll" } }, @@ -310,9 +308,9 @@ { "context": "Editor", "bindings": { - "ctrl-shift-K": "editor::DeleteLine", - "cmd-shift-D": "editor::DuplicateLine", - "cmd-shift-L": "editor::SplitSelectionIntoLines", + "ctrl-shift-k": "editor::DeleteLine", + "cmd-shift-d": "editor::DuplicateLine", + "cmd-shift-l": "editor::SplitSelectionIntoLines", "ctrl-cmd-up": "editor::MoveLineUp", "ctrl-cmd-down": "editor::MoveLineDown", "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart", @@ -324,9 +322,9 @@ "ctrl-alt-right": "editor::MoveToNextSubwordEnd", "ctrl-alt-f": "editor::MoveToNextSubwordEnd", "ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart", - "ctrl-alt-shift-B": "editor::SelectToPreviousSubwordStart", + "ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart", "ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd", - "ctrl-alt-shift-F": "editor::SelectToNextSubwordEnd" + "ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd" } }, { @@ -387,8 +385,8 @@ { "context": "Workspace", "bindings": { - "cmd-shift-C": "contacts_panel::Toggle", - "cmd-shift-B": "workspace::ToggleRightSidebar" + "cmd-shift-c": "contacts_panel::Toggle", + "cmd-shift-b": "workspace::ToggleRightSidebar" } }, { diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 358f016366..a3dcd5fbce 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -11,8 +11,8 @@ use client::{ }; use collections::{BTreeMap, HashMap, HashSet}; use editor::{ - self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename, - ToOffset, ToggleCodeActions, Undo, + self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, ToOffset, + ToggleCodeActions, Undo, }; use futures::{channel::mpsc, Future, StreamExt as _}; use gpui::{ @@ -154,9 +154,7 @@ async fn test_share_project( // .await; // Edit the buffer as client B and see that edit as client A. - editor_b.update(cx_b, |editor, cx| { - editor.handle_input(&Input("ok, ".into()), cx) - }); + editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx)); buffer_a .condition(&cx_a, |buffer, _| buffer.text() == "ok, b-contents") .await; @@ -1751,7 +1749,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu // Type a completion trigger character as the guest. editor_b.update(cx_b, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - editor.handle_input(&Input(".".into()), cx); + editor.handle_input(".", cx); cx.focus(&editor_b); }); diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index e4c888af4d..6f50da6a7d 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -11,7 +11,7 @@ use gpui::{ fonts::{FontId, HighlightStyle}, Entity, ModelContext, ModelHandle, }; -use language::{Point, Subscription as BufferSubscription}; +use language::{OffsetUtf16, Point, Subscription as BufferSubscription}; use settings::Settings; use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc}; use sum_tree::{Bias, TreeMap}; @@ -195,6 +195,11 @@ impl DisplayMap { .insert(Some(type_id), Arc::new((style, ranges))); } + pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range])> { + let highlights = self.text_highlights.get(&Some(type_id))?; + Some((highlights.0, &highlights.1)) + } + pub fn clear_text_highlights( &mut self, type_id: TypeId, @@ -544,6 +549,12 @@ impl ToDisplayPoint for usize { } } +impl ToDisplayPoint for OffsetUtf16 { + fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint { + self.to_offset(&map.buffer_snapshot).to_display_point(map) + } +} + impl ToDisplayPoint for Point { fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint { map.point_to_display_point(*self, Bias::Left) diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 95ca958ad6..95c3abb252 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -63,14 +63,14 @@ impl FoldPoint { .cursor::<(FoldPoint, TransformSummary)>(); cursor.seek(self, Bias::Right, &()); let overshoot = self.0 - cursor.start().1.output.lines; - let mut offset = cursor.start().1.output.bytes; + let mut offset = cursor.start().1.output.len; if !overshoot.is_zero() { let transform = cursor.item().expect("display point out of range"); assert!(transform.output_text.is_none()); let end_buffer_offset = snapshot .buffer_snapshot .point_to_offset(cursor.start().1.input.lines + overshoot); - offset += end_buffer_offset - cursor.start().1.input.bytes; + offset += end_buffer_offset - cursor.start().1.input.len; } FoldOffset(offset) } @@ -249,7 +249,7 @@ impl FoldMap { fn check_invariants(&self) { if cfg!(test) { assert_eq!( - self.transforms.lock().summary().input.bytes, + self.transforms.lock().summary().input.len, self.buffer.lock().len(), "transform tree does not match buffer's length" ); @@ -341,7 +341,7 @@ impl FoldMap { let mut fold = folds.next().unwrap(); let sum = new_transforms.summary(); - assert!(fold.start >= sum.input.bytes); + assert!(fold.start >= sum.input.len); while folds .peek() @@ -353,9 +353,9 @@ impl FoldMap { } } - if fold.start > sum.input.bytes { + if fold.start > sum.input.len { let text_summary = new_buffer - .text_summary_for_range::(sum.input.bytes..fold.start); + .text_summary_for_range::(sum.input.len..fold.start); new_transforms.push( Transform { summary: TransformSummary { @@ -384,9 +384,9 @@ impl FoldMap { } let sum = new_transforms.summary(); - if sum.input.bytes < edit.new.end { + if sum.input.len < edit.new.end { let text_summary = new_buffer - .text_summary_for_range::(sum.input.bytes..edit.new.end); + .text_summary_for_range::(sum.input.len..edit.new.end); new_transforms.push( Transform { summary: TransformSummary { @@ -558,7 +558,7 @@ impl FoldSnapshot { } pub fn len(&self) -> FoldOffset { - FoldOffset(self.transforms.summary().output.bytes) + FoldOffset(self.transforms.summary().output.len) } pub fn line_len(&self, row: u32) -> u32 { @@ -766,7 +766,7 @@ impl FoldSnapshot { ) } } else { - FoldOffset(self.transforms.summary().output.bytes) + FoldOffset(self.transforms.summary().output.len) } } @@ -1050,7 +1050,7 @@ impl<'a> Iterator for FoldChunks<'a> { // advance the transform and buffer cursors to the end of the fold. if let Some(output_text) = transform.output_text { self.buffer_chunk.take(); - self.buffer_offset += transform.summary.input.bytes; + self.buffer_offset += transform.summary.input.len; self.buffer_chunks.seek(self.buffer_offset); while self.buffer_offset >= self.transform_cursor.end(&()).1 @@ -1158,7 +1158,7 @@ impl FoldOffset { let overshoot = if cursor.item().map_or(true, |t| t.is_fold()) { Point::new(0, (self.0 - cursor.start().0 .0) as u32) } else { - let buffer_offset = cursor.start().1.input.bytes + self.0 - cursor.start().0 .0; + let buffer_offset = cursor.start().1.input.len + self.0 - cursor.start().0 .0; let buffer_point = snapshot.buffer_snapshot.offset_to_point(buffer_offset); buffer_point - cursor.start().1.input.lines }; @@ -1176,7 +1176,7 @@ impl Sub for FoldOffset { impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldOffset { fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { - self.0 += &summary.output.bytes; + self.0 += &summary.output.len; } } @@ -1188,7 +1188,7 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for Point { impl<'a> sum_tree::Dimension<'a, TransformSummary> for usize { fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { - *self += &summary.input.bytes; + *self += &summary.input.len; } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a37dfc734c..d92bd04251 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -39,15 +39,15 @@ pub use items::MAX_TAB_TITLE_LEN; pub use language::{char_kind, CharKind}; use language::{ BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticSeverity, - IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, + IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, }; use link_go_to_definition::LinkGoToDefinitionState; -use multi_buffer::MultiBufferChunks; pub use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; +use multi_buffer::{MultiBufferChunks, ToOffsetUtf16}; use ordered_float::OrderedFloat; use project::{LocationLink, Project, ProjectPath, ProjectTransaction}; use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection}; @@ -95,9 +95,6 @@ pub struct Jump { anchor: language::Anchor, } -#[derive(Clone, Deserialize, PartialEq)] -pub struct Input(pub String); - #[derive(Clone, Deserialize, PartialEq)] pub struct SelectToBeginningOfLine { #[serde(default)] @@ -186,6 +183,7 @@ actions!( Tab, TabPrev, ToggleComments, + ShowCharacterPalette, SelectLargerSyntaxNode, SelectSmallerSyntaxNode, GoToDefinition, @@ -210,7 +208,6 @@ actions!( impl_actions!( editor, [ - Input, SelectNext, SelectToBeginningOfLine, SelectToEndOfLine, @@ -224,6 +221,7 @@ impl_internal_actions!(editor, [Scroll, Select, Jump]); enum DocumentHighlightRead {} enum DocumentHighlightWrite {} +enum InputComposition {} #[derive(Copy, Clone, PartialEq, Eq)] pub enum Direction { @@ -236,7 +234,6 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx)); cx.add_action(Editor::select); cx.add_action(Editor::cancel); - cx.add_action(Editor::handle_input); cx.add_action(Editor::newline); cx.add_action(Editor::backspace); cx.add_action(Editor::delete); @@ -310,6 +307,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::open_excerpts); cx.add_action(Editor::jump); cx.add_action(Editor::restart_language_server); + cx.add_action(Editor::show_character_palette); cx.add_async_action(Editor::confirm_completion); cx.add_async_action(Editor::confirm_code_action); cx.add_async_action(Editor::rename); @@ -405,6 +403,7 @@ pub struct Editor { autoclose_stack: InvalidationStack, snippet_stack: InvalidationStack, select_larger_syntax_node_stack: Vec]>>, + ime_transaction: Option, active_diagnostics: Option, scroll_position: Vector2F, scroll_top_anchor: Anchor, @@ -992,6 +991,7 @@ impl Editor { autoclose_stack: Default::default(), snippet_stack: Default::default(), select_larger_syntax_node_stack: Vec::new(), + ime_transaction: Default::default(), active_diagnostics: None, soft_wrap_mode_override: None, get_field_editor_theme, @@ -1808,13 +1808,11 @@ impl Editor { cx.propagate_action(); } - pub fn handle_input(&mut self, action: &Input, cx: &mut ViewContext) { + pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext) { if !self.input_enabled { - cx.propagate_action(); return; } - let text = action.0.as_ref(); if !self.skip_autoclose_end(text, cx) { self.transact(cx, |this, cx| { if !this.surround_with_bracket_pair(text, cx) { @@ -2481,14 +2479,17 @@ impl Editor { }); if let Some((_, excerpted_buffer, excerpt_range)) = excerpt { if excerpted_buffer == *buffer { - let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); - let excerpt_range = excerpt_range.to_offset(&snapshot); - if snapshot - .edited_ranges_for_transaction(transaction) - .all(|range| { - excerpt_range.start <= range.start && excerpt_range.end >= range.end - }) - { + let all_edits_within_excerpt = buffer.read_with(&cx, |buffer, _| { + let excerpt_range = excerpt_range.to_offset(buffer); + buffer + .edited_ranges_for_transaction(transaction) + .all(|range| { + excerpt_range.start <= range.start + && excerpt_range.end >= range.end + }) + }); + + if all_edits_within_excerpt { return Ok(()); } } @@ -2501,12 +2502,12 @@ impl Editor { let mut ranges_to_highlight = Vec::new(); let excerpt_buffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(replica_id).with_title(title); - for (buffer, transaction) in &entries { - let snapshot = buffer.read(cx).snapshot(); + for (buffer_handle, transaction) in &entries { + let buffer = buffer_handle.read(cx); ranges_to_highlight.extend( multibuffer.push_excerpts_with_context_lines( - buffer.clone(), - snapshot + buffer_handle.clone(), + buffer .edited_ranges_for_transaction::(transaction) .collect(), 1, @@ -3614,6 +3615,7 @@ impl Editor { }); } self.request_autoscroll(Autoscroll::Fit, cx); + self.unmark_text(cx); cx.emit(Event::Edited); } } @@ -3627,6 +3629,7 @@ impl Editor { }); } self.request_autoscroll(Autoscroll::Fit, cx); + self.unmark_text(cx); cx.emit(Event::Edited); } } @@ -5026,6 +5029,10 @@ impl Editor { } } + fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext) { + cx.show_character_palette(); + } + fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext) { if let Some(active_diagnostics) = self.active_diagnostics.as_mut() { let buffer = self.buffer.read(cx).snapshot(cx); @@ -5151,10 +5158,10 @@ impl Editor { &mut self, cx: &mut ViewContext, update: impl FnOnce(&mut Self, &mut ViewContext), - ) { + ) -> Option { self.start_transaction_at(Instant::now(), cx); update(self, cx); - self.end_transaction_at(Instant::now(), cx); + self.end_transaction_at(Instant::now(), cx) } fn start_transaction_at(&mut self, now: Instant, cx: &mut ViewContext) { @@ -5168,7 +5175,11 @@ impl Editor { } } - fn end_transaction_at(&mut self, now: Instant, cx: &mut ViewContext) { + fn end_transaction_at( + &mut self, + now: Instant, + cx: &mut ViewContext, + ) -> Option { if let Some(tx_id) = self .buffer .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) @@ -5180,6 +5191,9 @@ impl Editor { } cx.emit(Event::Edited); + Some(tx_id) + } else { + None } } @@ -5528,6 +5542,13 @@ impl Editor { cx.notify(); } + pub fn text_highlights<'a, T: 'static>( + &'a self, + cx: &'a AppContext, + ) -> Option<(HighlightStyle, &'a [Range])> { + self.display_map.read(cx).text_highlights(TypeId::of::()) + } + pub fn clear_text_highlights( &mut self, cx: &mut ViewContext, @@ -5718,6 +5739,44 @@ impl Editor { }) .detach() } + + fn marked_text_ranges(&self, cx: &AppContext) -> Option>> { + let snapshot = self.buffer.read(cx).read(cx); + let (_, ranges) = self.text_highlights::(cx)?; + Some( + ranges + .into_iter() + .map(move |range| { + range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot) + }) + .collect(), + ) + } + + fn selection_replacement_ranges( + &self, + range: Range, + cx: &AppContext, + ) -> Vec> { + let selections = self.selections.all::(cx); + let newest_selection = selections + .iter() + .max_by_key(|selection| selection.id) + .unwrap(); + let start_delta = range.start.0 as isize - newest_selection.start.0 as isize; + let end_delta = range.end.0 as isize - newest_selection.end.0 as isize; + let snapshot = self.buffer.read(cx).read(cx); + selections + .into_iter() + .map(|mut selection| { + selection.start.0 = + (selection.start.0 as isize).saturating_add(start_delta) as usize; + selection.end.0 = (selection.end.0 as isize).saturating_add(end_delta) as usize; + snapshot.clip_offset_utf16(selection.start, Bias::Left) + ..snapshot.clip_offset_utf16(selection.end, Bias::Right) + }) + .collect() + } } impl EditorSnapshot { @@ -5773,6 +5832,7 @@ pub enum Event { SelectionsChanged { local: bool }, ScrollPositionChanged { local: bool }, Closed, + IgnoredInput, } pub struct EditorFocused(pub ViewHandle); @@ -5877,6 +5937,168 @@ impl View for Editor { context } + + fn text_for_range(&self, range_utf16: Range, cx: &AppContext) -> Option { + Some( + self.buffer + .read(cx) + .read(cx) + .text_for_range(OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end)) + .collect(), + ) + } + + fn selected_text_range(&self, cx: &AppContext) -> Option> { + // Prevent the IME menu from appearing when holding down an alphabetic key + // while input is disabled. + if !self.input_enabled { + return None; + } + + let range = self.selections.newest::(cx).range(); + Some(range.start.0..range.end.0) + } + + fn marked_text_range(&self, cx: &AppContext) -> Option> { + let snapshot = self.buffer.read(cx).read(cx); + let range = self.text_highlights::(cx)?.1.get(0)?; + Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0) + } + + fn unmark_text(&mut self, cx: &mut ViewContext) { + self.clear_text_highlights::(cx); + self.ime_transaction.take(); + } + + fn replace_text_in_range( + &mut self, + range_utf16: Option>, + text: &str, + cx: &mut ViewContext, + ) { + if !self.input_enabled { + cx.emit(Event::IgnoredInput); + return; + } + + self.transact(cx, |this, cx| { + let new_selected_ranges = if let Some(range_utf16) = range_utf16 { + let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end); + Some(this.selection_replacement_ranges(range_utf16, cx)) + } else if let Some(marked_ranges) = this.marked_text_ranges(cx) { + Some(marked_ranges) + } else { + None + }; + + if let Some(new_selected_ranges) = new_selected_ranges { + this.change_selections(None, cx, |selections| { + selections.select_ranges(new_selected_ranges) + }); + } + this.handle_input(text, cx); + }); + + if let Some(transaction) = self.ime_transaction { + self.buffer.update(cx, |buffer, cx| { + buffer.group_until_transaction(transaction, cx); + }); + } + + self.unmark_text(cx); + } + + fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + text: &str, + new_selected_range_utf16: Option>, + cx: &mut ViewContext, + ) { + if !self.input_enabled { + cx.emit(Event::IgnoredInput); + return; + } + + let transaction = self.transact(cx, |this, cx| { + let ranges_to_replace = if let Some(mut marked_ranges) = this.marked_text_ranges(cx) { + let snapshot = this.buffer.read(cx).read(cx); + if let Some(relative_range_utf16) = range_utf16.as_ref() { + for marked_range in &mut marked_ranges { + marked_range.end.0 = marked_range.start.0 + relative_range_utf16.end; + marked_range.start.0 += relative_range_utf16.start; + marked_range.start = + snapshot.clip_offset_utf16(marked_range.start, Bias::Left); + marked_range.end = + snapshot.clip_offset_utf16(marked_range.end, Bias::Right); + } + } + Some(marked_ranges) + } else if let Some(range_utf16) = range_utf16 { + let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end); + Some(this.selection_replacement_ranges(range_utf16, cx)) + } else { + None + }; + + if let Some(ranges) = ranges_to_replace { + this.change_selections(None, cx, |s| s.select_ranges(ranges)); + } + + let marked_ranges = { + let snapshot = this.buffer.read(cx).read(cx); + this.selections + .disjoint_anchors() + .into_iter() + .map(|selection| { + selection.start.bias_left(&*snapshot)..selection.end.bias_right(&*snapshot) + }) + .collect::>() + }; + + if text.is_empty() { + this.unmark_text(cx); + } else { + this.highlight_text::( + marked_ranges.clone(), + this.style(cx).composition_mark, + cx, + ); + } + + this.handle_input(text, cx); + + if let Some(new_selected_range) = new_selected_range_utf16 { + let snapshot = this.buffer.read(cx).read(cx); + let new_selected_ranges = marked_ranges + .into_iter() + .map(|marked_range| { + let insertion_start = marked_range.start.to_offset_utf16(&snapshot).0; + let new_start = OffsetUtf16(new_selected_range.start + insertion_start); + let new_end = OffsetUtf16(new_selected_range.end + insertion_start); + snapshot.clip_offset_utf16(new_start, Bias::Left) + ..snapshot.clip_offset_utf16(new_end, Bias::Right) + }) + .collect::>(); + + drop(snapshot); + this.change_selections(None, cx, |selections| { + selections.select_ranges(new_selected_ranges) + }); + } + }); + + self.ime_transaction = self.ime_transaction.or(transaction); + if let Some(transaction) = self.ime_transaction { + self.buffer.update(cx, |buffer, cx| { + buffer.group_until_transaction(transaction, cx); + }); + } + + if self.text_highlights::(cx).is_none() { + self.ime_transaction.take(); + } + } } fn build_style( @@ -6473,6 +6695,108 @@ mod tests { }); } + #[gpui::test] + fn test_ime_composition(cx: &mut MutableAppContext) { + cx.set_global(Settings::test(cx)); + let buffer = cx.add_model(|cx| { + let mut buffer = language::Buffer::new(0, "abcde", cx); + // Ensure automatic grouping doesn't occur. + buffer.set_group_interval(Duration::ZERO); + buffer + }); + + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + cx.add_window(Default::default(), |cx| { + let mut editor = build_editor(buffer.clone(), cx); + + // Start a new IME composition. + editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); + editor.replace_and_mark_text_in_range(Some(0..1), "á", None, cx); + editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, cx); + assert_eq!(editor.text(cx), "äbcde"); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![OffsetUtf16(0)..OffsetUtf16(1)]) + ); + + // Finalize IME composition. + editor.replace_text_in_range(None, "ā", cx); + assert_eq!(editor.text(cx), "ābcde"); + assert_eq!(editor.marked_text_ranges(cx), None); + + // IME composition edits are grouped and are undone/redone at once. + editor.undo(&Default::default(), cx); + assert_eq!(editor.text(cx), "abcde"); + assert_eq!(editor.marked_text_ranges(cx), None); + editor.redo(&Default::default(), cx); + assert_eq!(editor.text(cx), "ābcde"); + assert_eq!(editor.marked_text_ranges(cx), None); + + // Start a new IME composition. + editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![OffsetUtf16(0)..OffsetUtf16(1)]) + ); + + // Undoing during an IME composition cancels it. + editor.undo(&Default::default(), cx); + assert_eq!(editor.text(cx), "ābcde"); + assert_eq!(editor.marked_text_ranges(cx), None); + + // Start a new IME composition with an invalid marked range, ensuring it gets clipped. + editor.replace_and_mark_text_in_range(Some(4..999), "è", None, cx); + assert_eq!(editor.text(cx), "ābcdè"); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![OffsetUtf16(4)..OffsetUtf16(5)]) + ); + + // Finalize IME composition with an invalid replacement range, ensuring it gets clipped. + editor.replace_text_in_range(Some(4..999), "ę", cx); + assert_eq!(editor.text(cx), "ābcdę"); + assert_eq!(editor.marked_text_ranges(cx), None); + + // Start a new IME composition with multiple cursors. + editor.change_selections(None, cx, |s| { + s.select_ranges([ + OffsetUtf16(1)..OffsetUtf16(1), + OffsetUtf16(3)..OffsetUtf16(3), + OffsetUtf16(5)..OffsetUtf16(5), + ]) + }); + editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, cx); + assert_eq!(editor.text(cx), "XYZbXYZdXYZ"); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![ + OffsetUtf16(0)..OffsetUtf16(3), + OffsetUtf16(4)..OffsetUtf16(7), + OffsetUtf16(8)..OffsetUtf16(11) + ]) + ); + + // Ensure the newly-marked range gets treated as relative to the previously-marked ranges. + editor.replace_and_mark_text_in_range(Some(1..2), "1", None, cx); + assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z"); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![ + OffsetUtf16(1)..OffsetUtf16(2), + OffsetUtf16(5)..OffsetUtf16(6), + OffsetUtf16(9)..OffsetUtf16(10) + ]) + ); + + // Finalize IME composition with multiple cursors. + editor.replace_text_in_range(Some(9..10), "2", cx); + assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z"); + assert_eq!(editor.marked_text_ranges(cx), None); + + editor + }); + } + #[gpui::test] fn test_selection_with_mouse(cx: &mut gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); @@ -8247,9 +8571,9 @@ mod tests { // is pasted at each cursor. cx.set_state("|two one✅ four three six five |"); cx.update_editor(|e, cx| { - e.handle_input(&Input("( ".into()), cx); + e.handle_input("( ", cx); e.paste(&Paste, cx); - e.handle_input(&Input(") ".into()), cx); + e.handle_input(") ", cx); }); cx.assert_editor_state(indoc! {" ( one✅ @@ -8924,9 +9248,9 @@ mod tests { ]) }); - view.handle_input(&Input("{".to_string()), cx); - view.handle_input(&Input("{".to_string()), cx); - view.handle_input(&Input("{".to_string()), cx); + view.handle_input("{", cx); + view.handle_input("{", cx); + view.handle_input("{", cx); assert_eq!( view.text(cx), " @@ -8939,9 +9263,9 @@ mod tests { ); view.move_right(&MoveRight, cx); - view.handle_input(&Input("}".to_string()), cx); - view.handle_input(&Input("}".to_string()), cx); - view.handle_input(&Input("}".to_string()), cx); + view.handle_input("}", cx); + view.handle_input("}", cx); + view.handle_input("}", cx); assert_eq!( view.text(cx), " @@ -8954,8 +9278,8 @@ mod tests { ); view.undo(&Undo, cx); - view.handle_input(&Input("/".to_string()), cx); - view.handle_input(&Input("*".to_string()), cx); + view.handle_input("/", cx); + view.handle_input("*", cx); assert_eq!( view.text(cx), " @@ -8974,7 +9298,7 @@ mod tests { DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), ]) }); - view.handle_input(&Input("*".to_string()), cx); + view.handle_input("*", cx); assert_eq!( view.text(cx), " @@ -8992,7 +9316,7 @@ mod tests { view.change_selections(None, cx, |s| { s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]) }); - view.handle_input(&Input("{".to_string()), cx); + view.handle_input("{", cx); assert_eq!( view.text(cx), " @@ -9008,7 +9332,7 @@ mod tests { view.change_selections(None, cx, |s| { s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1)]) }); - view.handle_input(&Input("{".to_string()), cx); + view.handle_input("{", cx); assert_eq!( view.text(cx), " @@ -9025,7 +9349,7 @@ mod tests { ); view.undo(&Undo, cx); - view.handle_input(&Input("[".to_string()), cx); + view.handle_input("[", cx); assert_eq!( view.text(cx), " @@ -9045,7 +9369,7 @@ mod tests { view.change_selections(None, cx, |s| { s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)]) }); - view.handle_input(&Input("[".to_string()), cx); + view.handle_input("[", cx); assert_eq!( view.text(cx), " @@ -9101,9 +9425,9 @@ mod tests { ]) }); - view.handle_input(&Input("{".to_string()), cx); - view.handle_input(&Input("{".to_string()), cx); - view.handle_input(&Input("{".to_string()), cx); + view.handle_input("{", cx); + view.handle_input("{", cx); + view.handle_input("{", cx); assert_eq!( view.text(cx), " @@ -9183,9 +9507,9 @@ mod tests { ]) }); - editor.handle_input(&Input("{".to_string()), cx); - editor.handle_input(&Input("{".to_string()), cx); - editor.handle_input(&Input("_".to_string()), cx); + editor.handle_input("{", cx); + editor.handle_input("{", cx); + editor.handle_input("_", cx); assert_eq!( editor.text(cx), " @@ -9699,7 +10023,9 @@ mod tests { cx.set_state("editor|"); cx.simulate_keystroke("."); assert!(cx.editor(|e, _| e.context_menu.is_none())); - cx.simulate_keystrokes(["c", "l", "o"]); + cx.simulate_keystroke("c"); + cx.simulate_keystroke("l"); + cx.simulate_keystroke("o"); cx.assert_editor_state("editor.clo|"); assert!(cx.editor(|e, _| e.context_menu.is_none())); cx.update_editor(|editor, cx| { @@ -9911,7 +10237,7 @@ mod tests { ]) }); - view.handle_input(&Input("X".to_string()), cx); + view.handle_input("X", cx); assert_eq!(view.text(cx), "Xaaaa\nXbbbb"); assert_eq!( view.selections.ranges(cx), @@ -9951,7 +10277,7 @@ mod tests { assert_eq!(view.text(cx), expected_text); view.change_selections(None, cx, |s| s.select_ranges(selection_ranges)); - view.handle_input(&Input("X".to_string()), cx); + view.handle_input("X", cx); let (expected_text, expected_selections) = marked_text_ranges(indoc! {" aaaa diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c3ef8da399..b8bee49d8d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1,6 +1,6 @@ use super::{ display_map::{BlockContext, ToDisplayPoint}, - Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Input, Scroll, Select, SelectPhase, + Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Scroll, Select, SelectPhase, SoftWrap, ToPoint, MAX_LINE_LEN, }; use crate::{ @@ -24,13 +24,13 @@ use gpui::{ json::{self, ToJson}, platform::CursorStyle, text_layout::{self, Line, RunStyle, TextLayoutCache}, - AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext, KeyDownEvent, + AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext, LayoutContext, ModifiersChangedEvent, MouseButton, MouseButtonEvent, MouseMovedEvent, MutableAppContext, PaintContext, Quad, Scene, ScrollWheelEvent, SizeConstraint, ViewContext, WeakViewHandle, }; use json::json; -use language::{Bias, DiagnosticSeverity, Selection}; +use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection}; use project::ProjectPath; use settings::Settings; use smallvec::SmallVec; @@ -283,21 +283,6 @@ impl EditorElement { true } - fn key_down(&self, input: Option<&str>, cx: &mut EventContext) -> bool { - let view = self.view.upgrade(cx.app).unwrap(); - - if view.is_focused(cx.app) { - if let Some(input) = input { - cx.dispatch_action(Input(input.to_string())); - true - } else { - false - } - } else { - false - } - } - fn modifiers_changed(&self, cmd: bool, cx: &mut EventContext) -> bool { cx.dispatch_action(CmdChanged { cmd_down: cmd }); false @@ -1569,7 +1554,6 @@ impl Element for EditorElement { delta, precise, }) => self.scroll(*position, *delta, *precise, layout, paint, cx), - Event::KeyDown(KeyDownEvent { input, .. }) => self.key_down(input.as_deref(), cx), Event::ModifiersChanged(ModifiersChangedEvent { cmd, .. }) => { self.modifiers_changed(*cmd, cx) } @@ -1581,6 +1565,43 @@ impl Element for EditorElement { } } + fn rect_for_text_range( + &self, + range_utf16: Range, + bounds: RectF, + _: RectF, + layout: &Self::LayoutState, + _: &Self::PaintState, + _: &gpui::MeasurementContext, + ) -> Option { + let text_bounds = RectF::new( + bounds.origin() + vec2f(layout.gutter_size.x(), 0.0), + layout.text_size, + ); + let content_origin = text_bounds.origin() + vec2f(layout.gutter_margin, 0.); + let scroll_position = layout.snapshot.scroll_position(); + let start_row = scroll_position.y() as u32; + let scroll_top = scroll_position.y() * layout.line_height; + let scroll_left = scroll_position.x() * layout.em_width; + + let range_start = + OffsetUtf16(range_utf16.start).to_display_point(&layout.snapshot.display_snapshot); + if range_start.row() < start_row { + return None; + } + + let line = layout + .line_layouts + .get((range_start.row() - start_row) as usize)?; + let range_start_x = line.x_for_index(range_start.column() as usize); + let range_start_y = range_start.row() as f32 * layout.line_height; + Some(RectF::new( + content_origin + vec2f(range_start_x, range_start_y + layout.line_height) + - vec2f(scroll_left, scroll_top), + vec2f(layout.em_width, layout.line_height), + )) + } + fn debug( &self, bounds: RectF, @@ -1740,6 +1761,13 @@ impl Cursor { } } + pub fn bounding_rect(&self, origin: Vector2F) -> RectF { + RectF::new( + self.origin + origin, + vec2f(self.block_width, self.line_height), + ) + } + pub fn paint(&self, origin: Vector2F, cx: &mut PaintContext) { let bounds = match self.shape { CursorShape::Bar => RectF::new(self.origin + origin, vec2f(2.0, self.line_height)), diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 32dae4ecee..f0dc359b4b 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -419,7 +419,6 @@ mod tests { requests.next().await; cx.foreground().run_until_parked(); - println!("tag"); cx.assert_editor_text_highlights::(indoc! {" fn test() [do_work](); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index d5b85b0aee..00ef7b11a0 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -9,7 +9,7 @@ pub use language::Completion; use language::{ char_kind, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, Outline, OutlineItem, Selection, ToOffset as _, - ToPoint as _, ToPointUtf16 as _, TransactionId, + ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, }; use settings::Settings; use smallvec::SmallVec; @@ -29,7 +29,7 @@ use text::{ locator::Locator, rope::TextDimension, subscription::{Subscription, Topic}, - Edit, Point, PointUtf16, TextSummary, + Edit, OffsetUtf16, Point, PointUtf16, TextSummary, }; use theme::SyntaxTheme; use util::post_inc; @@ -72,6 +72,10 @@ pub trait ToOffset: 'static + fmt::Debug { fn to_offset(&self, snapshot: &MultiBufferSnapshot) -> usize; } +pub trait ToOffsetUtf16: 'static + fmt::Debug { + fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16; +} + pub trait ToPoint: 'static + fmt::Debug { fn to_point(&self, snapshot: &MultiBufferSnapshot) -> Point; } @@ -554,6 +558,20 @@ impl MultiBuffer { self.history.finalize_last_transaction(); } + pub fn group_until_transaction( + &mut self, + transaction_id: TransactionId, + cx: &mut ModelContext, + ) { + if let Some(buffer) = self.as_singleton() { + buffer.update(cx, |buffer, _| { + buffer.group_until_transaction(transaction_id) + }); + } else { + self.history.group_until(transaction_id); + } + } + pub fn set_active_selections( &mut self, selections: &[Selection], @@ -809,7 +827,7 @@ impl MultiBuffer { let mut cursor = snapshot.excerpts.cursor::>(); let mut new_excerpts = cursor.slice(&Some(prev_excerpt_id), Bias::Right, &()); - let edit_start = new_excerpts.summary().text.bytes; + let edit_start = new_excerpts.summary().text.len; new_excerpts.update_last( |excerpt| { excerpt.has_trailing_newline = true; @@ -862,7 +880,7 @@ impl MultiBuffer { &(), ); - let edit_end = new_excerpts.summary().text.bytes; + let edit_end = new_excerpts.summary().text.len; let suffix = cursor.suffix(&()); let changed_trailing_excerpt = suffix.is_empty(); @@ -1068,7 +1086,7 @@ impl MultiBuffer { // Push an edit for the removal of this run of excerpts. let old_end = cursor.start().1; - let new_start = new_excerpts.summary().text.bytes; + let new_start = new_excerpts.summary().text.len; edits.push(Edit { old: old_start..old_end, new: new_start..new_start, @@ -1297,7 +1315,7 @@ impl MultiBuffer { ) .map(|mut edit| { let excerpt_old_start = cursor.start().1; - let excerpt_new_start = new_excerpts.summary().text.bytes; + let excerpt_new_start = new_excerpts.summary().text.len; edit.old.start += excerpt_old_start; edit.old.end += excerpt_old_start; edit.new.start += excerpt_new_start; @@ -1527,7 +1545,7 @@ impl MultiBufferSnapshot { let mut cursor = self.excerpts.cursor::(); cursor.seek(&offset, Bias::Left, &()); let mut excerpt_chunks = cursor.item().map(|excerpt| { - let end_before_footer = cursor.start() + excerpt.text_summary.bytes; + let end_before_footer = cursor.start() + excerpt.text_summary.len; let start = excerpt.range.context.start.to_offset(&excerpt.buffer); let end = start + (cmp::min(offset, end_before_footer) - cursor.start()); excerpt.buffer.reversed_chunks_in_range(start..end) @@ -1629,7 +1647,7 @@ impl MultiBufferSnapshot { } pub fn len(&self) -> usize { - self.excerpts.summary().text.bytes + self.excerpts.summary().text.len } pub fn max_buffer_row(&self) -> u32 { @@ -1674,6 +1692,25 @@ impl MultiBufferSnapshot { *cursor.start() + overshoot } + pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 { + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.clip_offset_utf16(offset, bias); + } + + let mut cursor = self.excerpts.cursor::(); + cursor.seek(&offset, Bias::Right, &()); + let overshoot = if let Some(excerpt) = cursor.item() { + let excerpt_start = excerpt.range.context.start.to_offset_utf16(&excerpt.buffer); + let buffer_offset = excerpt + .buffer + .clip_offset_utf16(excerpt_start + (offset - cursor.start()), bias); + OffsetUtf16(buffer_offset.0.saturating_sub(excerpt_start.0)) + } else { + OffsetUtf16(0) + }; + *cursor.start() + overshoot + } + pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 { if let Some((_, _, buffer)) = self.as_singleton() { return buffer.clip_point_utf16(point, bias); @@ -1781,7 +1818,7 @@ impl MultiBufferSnapshot { .offset_to_point_utf16(excerpt_start_offset + overshoot); *start_point + (buffer_point - excerpt_start_point) } else { - self.excerpts.summary().text.lines_utf16 + self.excerpts.summary().text.lines_utf16() } } @@ -1803,7 +1840,7 @@ impl MultiBufferSnapshot { .point_to_point_utf16(excerpt_start_point + overshoot); *start_point + (buffer_point - excerpt_start_point_utf16) } else { - self.excerpts.summary().text.lines_utf16 + self.excerpts.summary().text.lines_utf16() } } @@ -1824,7 +1861,53 @@ impl MultiBufferSnapshot { .point_to_offset(excerpt_start_point + overshoot); *start_offset + buffer_offset - excerpt_start_offset } else { - self.excerpts.summary().text.bytes + self.excerpts.summary().text.len + } + } + + pub fn offset_utf16_to_offset(&self, offset_utf16: OffsetUtf16) -> usize { + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.offset_utf16_to_offset(offset_utf16); + } + + let mut cursor = self.excerpts.cursor::<(OffsetUtf16, usize)>(); + cursor.seek(&offset_utf16, Bias::Right, &()); + if let Some(excerpt) = cursor.item() { + let (start_offset_utf16, start_offset) = cursor.start(); + let overshoot = offset_utf16 - start_offset_utf16; + let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer); + let excerpt_start_offset_utf16 = + excerpt.buffer.offset_to_offset_utf16(excerpt_start_offset); + let buffer_offset = excerpt + .buffer + .offset_utf16_to_offset(excerpt_start_offset_utf16 + overshoot); + *start_offset + (buffer_offset - excerpt_start_offset) + } else { + self.excerpts.summary().text.len + } + } + + pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.offset_to_offset_utf16(offset); + } + + let mut cursor = self.excerpts.cursor::<(usize, OffsetUtf16)>(); + cursor.seek(&offset, Bias::Right, &()); + if let Some(excerpt) = cursor.item() { + let (start_offset, start_offset_utf16) = cursor.start(); + let overshoot = offset - start_offset; + let excerpt_start_offset_utf16 = + excerpt.range.context.start.to_offset_utf16(&excerpt.buffer); + let excerpt_start_offset = excerpt + .buffer + .offset_utf16_to_offset(excerpt_start_offset_utf16); + let buffer_offset_utf16 = excerpt + .buffer + .offset_to_offset_utf16(excerpt_start_offset + overshoot); + *start_offset_utf16 + (buffer_offset_utf16 - excerpt_start_offset_utf16) + } else { + self.excerpts.summary().text.len_utf16 } } @@ -1847,7 +1930,7 @@ impl MultiBufferSnapshot { .point_utf16_to_offset(excerpt_start_point + overshoot); *start_offset + (buffer_offset - excerpt_start_offset) } else { - self.excerpts.summary().text.bytes + self.excerpts.summary().text.len } } @@ -2311,7 +2394,7 @@ impl MultiBufferSnapshot { .context .start .to_offset(&start_excerpt.buffer); - let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.bytes; + let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.len; let start_in_buffer = excerpt_buffer_start + range.start.saturating_sub(*cursor.start()); @@ -2415,7 +2498,7 @@ impl MultiBufferSnapshot { .context .start .to_offset(&start_excerpt.buffer); - let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.bytes; + let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.len; let start_in_buffer = excerpt_buffer_start + range.start.saturating_sub(*cursor.start()); @@ -2651,9 +2734,8 @@ impl History { } fn group(&mut self) -> Option { - let mut new_len = self.undo_stack.len(); - let mut transactions = self.undo_stack.iter_mut(); - + let mut count = 0; + let mut transactions = self.undo_stack.iter(); if let Some(mut transaction) = transactions.next_back() { while let Some(prev_transaction) = transactions.next_back() { if !prev_transaction.suppress_grouping @@ -2661,13 +2743,31 @@ impl History { <= self.group_interval { transaction = prev_transaction; - new_len -= 1; + count += 1; } else { break; } } } + self.group_trailing(count) + } + fn group_until(&mut self, transaction_id: TransactionId) { + let mut count = 0; + for transaction in self.undo_stack.iter().rev() { + if transaction.id == transaction_id { + self.group_trailing(count); + break; + } else if transaction.suppress_grouping { + break; + } else { + count += 1; + } + } + } + + fn group_trailing(&mut self, n: usize) -> Option { + let new_len = self.undo_stack.len() - n; let (transactions_to_keep, transactions_to_merge) = self.undo_stack.split_at_mut(new_len); if let Some(last_transaction) = transactions_to_keep.last_mut() { if let Some(transaction) = transactions_to_merge.last() { @@ -2717,11 +2817,11 @@ impl Excerpt { ) -> ExcerptChunks<'a> { let content_start = self.range.context.start.to_offset(&self.buffer); let chunks_start = content_start + range.start; - let chunks_end = content_start + cmp::min(range.end, self.text_summary.bytes); + let chunks_end = content_start + cmp::min(range.end, self.text_summary.len); let footer_height = if self.has_trailing_newline - && range.start <= self.text_summary.bytes - && range.end > self.text_summary.bytes + && range.start <= self.text_summary.len + && range.end > self.text_summary.len { 1 } else { @@ -2739,10 +2839,10 @@ impl Excerpt { fn bytes_in_range(&self, range: Range) -> ExcerptBytes { let content_start = self.range.context.start.to_offset(&self.buffer); let bytes_start = content_start + range.start; - let bytes_end = content_start + cmp::min(range.end, self.text_summary.bytes); + let bytes_end = content_start + cmp::min(range.end, self.text_summary.len); let footer_height = if self.has_trailing_newline - && range.start <= self.text_summary.bytes - && range.end > self.text_summary.bytes + && range.start <= self.text_summary.len + && range.end > self.text_summary.len { 1 } else { @@ -2836,13 +2936,13 @@ impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for TextSummary { impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for usize { fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { - *self += summary.text.bytes; + *self += summary.text.len; } } impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for usize { fn cmp(&self, cursor_location: &ExcerptSummary, _: &()) -> cmp::Ordering { - Ord::cmp(self, &cursor_location.text.bytes) + Ord::cmp(self, &cursor_location.text.len) } } @@ -2852,6 +2952,12 @@ impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for Option<&'a } } +impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for OffsetUtf16 { + fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { + *self += summary.text.len_utf16; + } +} + impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Point { fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { *self += summary.text.lines; @@ -2860,7 +2966,7 @@ impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Point { impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for PointUtf16 { fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { - *self += summary.text.lines_utf16 + *self += summary.text.lines_utf16() } } @@ -3060,6 +3166,24 @@ impl ToOffset for usize { } } +impl ToOffset for OffsetUtf16 { + fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize { + snapshot.offset_utf16_to_offset(*self) + } +} + +impl ToOffsetUtf16 for OffsetUtf16 { + fn to_offset_utf16(&self, _snapshot: &MultiBufferSnapshot) -> OffsetUtf16 { + *self + } +} + +impl ToOffsetUtf16 for usize { + fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16 { + snapshot.offset_to_offset_utf16(*self) + } +} + impl ToPoint for usize { fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point { snapshot.offset_to_point(*self) @@ -3823,11 +3947,11 @@ mod tests { buffer.text_summary_for_range::(0..buffer_range.start); let excerpt_start = excerpt_starts.next().unwrap(); - let mut offset = excerpt_start.bytes; + let mut offset = excerpt_start.len; let mut buffer_offset = buffer_range.start; let mut point = excerpt_start.lines; let mut buffer_point = buffer_start_point; - let mut point_utf16 = excerpt_start.lines_utf16; + let mut point_utf16 = excerpt_start.lines_utf16(); let mut buffer_point_utf16 = buffer_start_point_utf16; for ch in buffer .snapshot() @@ -3841,7 +3965,7 @@ mod tests { let buffer_right_offset = buffer.clip_offset(buffer_offset, Bias::Right); assert_eq!( left_offset, - excerpt_start.bytes + (buffer_left_offset - buffer_range.start), + excerpt_start.len + (buffer_left_offset - buffer_range.start), "clip_offset({:?}, Left). buffer: {:?}, buffer offset: {:?}", offset, buffer_id, @@ -3849,7 +3973,7 @@ mod tests { ); assert_eq!( right_offset, - excerpt_start.bytes + (buffer_right_offset - buffer_range.start), + excerpt_start.len + (buffer_right_offset - buffer_range.start), "clip_offset({:?}, Right). buffer: {:?}, buffer offset: {:?}", offset, buffer_id, @@ -3910,7 +4034,7 @@ mod tests { buffer.clip_point_utf16(buffer_point_utf16, Bias::Right); assert_eq!( left_point_utf16, - excerpt_start.lines_utf16 + excerpt_start.lines_utf16() + (buffer_left_point_utf16 - buffer_start_point_utf16), "clip_point_utf16({:?}, Left). buffer: {:?}, buffer point_utf16: {:?}", point_utf16, @@ -3919,7 +4043,7 @@ mod tests { ); assert_eq!( right_point_utf16, - excerpt_start.lines_utf16 + excerpt_start.lines_utf16() + (buffer_right_point_utf16 - buffer_start_point_utf16), "clip_point_utf16({:?}, Right). buffer: {:?}, buffer point_utf16: {:?}", point_utf16, @@ -4069,7 +4193,7 @@ mod tests { let mut now = Instant::now(); multibuffer.update(cx, |multibuffer, cx| { - multibuffer.start_transaction_at(now, cx); + let transaction_1 = multibuffer.start_transaction_at(now, cx).unwrap(); multibuffer.edit( [ (Point::new(0, 0)..Point::new(0, 0), "A"), @@ -4152,6 +4276,16 @@ mod tests { assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678"); multibuffer.undo(cx); assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); + + // Transactions can be grouped manually. + multibuffer.redo(cx); + multibuffer.redo(cx); + assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); + multibuffer.group_until_transaction(transaction_1, cx); + multibuffer.undo(cx); + assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); + multibuffer.redo(cx); + assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); }); } } diff --git a/crates/editor/src/multi_buffer/anchor.rs b/crates/editor/src/multi_buffer/anchor.rs index df080f074c..1340ea814d 100644 --- a/crates/editor/src/multi_buffer/anchor.rs +++ b/crates/editor/src/multi_buffer/anchor.rs @@ -1,10 +1,10 @@ -use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToPoint}; +use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToOffsetUtf16, ToPoint}; use std::{ cmp::Ordering, ops::{Range, Sub}, }; use sum_tree::Bias; -use text::{rope::TextDimension, Point}; +use text::{rope::TextDimension, OffsetUtf16, Point}; #[derive(Clone, Eq, PartialEq, Debug, Hash)] pub struct Anchor { @@ -89,6 +89,12 @@ impl ToOffset for Anchor { } } +impl ToOffsetUtf16 for Anchor { + fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16 { + self.summary(snapshot) + } +} + impl ToPoint for Anchor { fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point { self.summary(snapshot) diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 9151f63e5b..18c13a4ba6 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -181,13 +181,7 @@ impl<'a> EditorTestContext<'a> { pub fn simulate_keystroke(&mut self, keystroke_text: &str) { let keystroke = Keystroke::parse(keystroke_text).unwrap(); - let input = if keystroke.modified() { - None - } else { - Some(keystroke.key.clone()) - }; - self.cx - .dispatch_keystroke(self.window_id, keystroke, input, false); + self.cx.dispatch_keystroke(self.window_id, keystroke, false); } pub fn simulate_keystrokes(&mut self, keystroke_texts: [&str; COUNT]) { diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index c8172e0de3..d69c95605d 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -279,7 +279,7 @@ impl PickerDelegate for FileFinder { #[cfg(test)] mod tests { use super::*; - use editor::{Editor, Input}; + use editor::Editor; use menu::{Confirm, SelectNext}; use serde_json::json; use workspace::{AppState, Workspace}; @@ -318,12 +318,14 @@ mod tests { cx.dispatch_action(window_id, Toggle); let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - cx.dispatch_action(window_id, Input("b".into())); - cx.dispatch_action(window_id, Input("n".into())); - cx.dispatch_action(window_id, Input("a".into())); finder - .condition(&cx, |finder, _| finder.matches.len() == 2) + .update(cx, |finder, cx| { + finder.update_matches("bna".to_string(), cx) + }) .await; + finder.read_with(cx, |finder, _| { + assert_eq!(finder.matches.len(), 2); + }); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); cx.dispatch_action(window_id, SelectNext); diff --git a/crates/gpui/examples/text.rs b/crates/gpui/examples/text.rs index 74e2c2c2ea..8e0153fe38 100644 --- a/crates/gpui/examples/text.rs +++ b/crates/gpui/examples/text.rs @@ -2,11 +2,12 @@ use gpui::{ color::Color, fonts::{Properties, Weight}, text_layout::RunStyle, - DebugContext, Element as _, Quad, + DebugContext, Element as _, MeasurementContext, Quad, }; use log::LevelFilter; use pathfinder_geometry::rect::RectF; use simplelog::SimpleLogger; +use std::ops::Range; fn main() { SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); @@ -112,6 +113,18 @@ impl gpui::Element for TextElement { false } + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &MeasurementContext, + ) -> Option { + None + } + fn debug( &self, _: RectF, diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 2cd6687bcb..b06bb642a2 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3,12 +3,13 @@ pub mod action; use crate::{ elements::ElementBox, executor::{self, Task}, + geometry::rect::RectF, keymap::{self, Binding, Keystroke}, platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions}, presenter::Presenter, util::post_inc, - AssetCache, AssetSource, ClipboardItem, FontCache, MouseRegionId, PathPromptOptions, - TextLayoutCache, + AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseRegionId, + PathPromptOptions, TextLayoutCache, }; pub use action::*; use anyhow::{anyhow, Context, Result}; @@ -28,7 +29,7 @@ use std::{ hash::{Hash, Hasher}, marker::PhantomData, mem, - ops::{Deref, DerefMut}, + ops::{Deref, DerefMut, Range}, path::{Path, PathBuf}, pin::Pin, rc::{self, Rc}, @@ -64,6 +65,32 @@ pub trait View: Entity + Sized { fn debug_json(&self, _: &AppContext) -> serde_json::Value { serde_json::Value::Null } + + fn text_for_range(&self, _: Range, _: &AppContext) -> Option { + None + } + fn selected_text_range(&self, _: &AppContext) -> Option> { + None + } + fn marked_text_range(&self, _: &AppContext) -> Option> { + None + } + fn unmark_text(&mut self, _: &mut ViewContext) {} + fn replace_text_in_range( + &mut self, + _: Option>, + _: &str, + _: &mut ViewContext, + ) { + } + fn replace_and_mark_text_in_range( + &mut self, + _: Option>, + _: &str, + _: Option>, + _: &mut ViewContext, + ) { + } } pub trait ReadModel { @@ -154,6 +181,11 @@ pub struct TestAppContext { condition_duration: Option, } +pub struct WindowInputHandler { + app: Rc>, + window_id: usize, +} + impl App { pub fn new(asset_source: impl AssetSource) -> Result { let platform = platform::current::platform(); @@ -310,6 +342,87 @@ impl App { } } +impl WindowInputHandler { + fn read_focused_view(&self, f: F) -> Option + where + F: FnOnce(&dyn AnyView, &AppContext) -> T, + { + let app = self.app.borrow(); + let view_id = app.focused_view_id(self.window_id)?; + let view = app.cx.views.get(&(self.window_id, view_id))?; + let result = f(view.as_ref(), &app); + Some(result) + } + + fn update_focused_view(&mut self, f: F) -> Option + where + F: FnOnce(usize, usize, &mut dyn AnyView, &mut MutableAppContext) -> T, + { + let mut app = self.app.borrow_mut(); + app.update(|app| { + let view_id = app.focused_view_id(self.window_id)?; + let mut view = app.cx.views.remove(&(self.window_id, view_id))?; + let result = f(self.window_id, view_id, view.as_mut(), &mut *app); + app.cx.views.insert((self.window_id, view_id), view); + Some(result) + }) + } +} + +impl InputHandler for WindowInputHandler { + fn text_for_range(&self, range: Range) -> Option { + self.read_focused_view(|view, cx| view.text_for_range(range.clone(), cx)) + .flatten() + } + + fn selected_text_range(&self) -> Option> { + self.read_focused_view(|view, cx| view.selected_text_range(cx)) + .flatten() + } + + fn replace_text_in_range(&mut self, range: Option>, text: &str) { + self.update_focused_view(|window_id, view_id, view, cx| { + view.replace_text_in_range(range, text, cx, window_id, view_id); + }); + } + + fn marked_text_range(&self) -> Option> { + self.read_focused_view(|view, cx| view.marked_text_range(cx)) + .flatten() + } + + fn unmark_text(&mut self) { + self.update_focused_view(|window_id, view_id, view, cx| { + view.unmark_text(cx, window_id, view_id); + }); + } + + fn replace_and_mark_text_in_range( + &mut self, + range: Option>, + new_text: &str, + new_selected_range: Option>, + ) { + self.update_focused_view(|window_id, view_id, view, cx| { + view.replace_and_mark_text_in_range( + range, + new_text, + new_selected_range, + cx, + window_id, + view_id, + ); + }); + } + + fn rect_for_range(&self, range_utf16: Range) -> Option { + let app = self.app.borrow(); + let (presenter, _) = app.presenters_and_platform_windows.get(&self.window_id)?; + let presenter = presenter.borrow(); + presenter.rect_for_text_range(range_utf16, &app) + } +} + #[cfg(any(test, feature = "test-support"))] impl TestAppContext { pub fn new( @@ -361,14 +474,8 @@ impl TestAppContext { self.cx.borrow_mut().dispatch_global_action(action); } - pub fn dispatch_keystroke( - &mut self, - window_id: usize, - keystroke: Keystroke, - input: Option, - is_held: bool, - ) { - self.cx.borrow_mut().update(|cx| { + pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) { + let handled = self.cx.borrow_mut().update(|cx| { let presenter = cx .presenters_and_platform_windows .get(&window_id) @@ -377,17 +484,29 @@ impl TestAppContext { .clone(); let dispatch_path = presenter.borrow().dispatch_path(cx.as_ref()); - if !cx.dispatch_keystroke(window_id, dispatch_path, &keystroke) { - presenter.borrow_mut().dispatch_event( - Event::KeyDown(KeyDownEvent { - keystroke, - input, - is_held, - }), - cx, - ); + if cx.dispatch_keystroke(window_id, dispatch_path, &keystroke) { + return true; } + if presenter.borrow_mut().dispatch_event( + Event::KeyDown(KeyDownEvent { + keystroke: keystroke.clone(), + is_held, + }), + cx, + ) { + return true; + } + + false }); + + if !handled && !keystroke.cmd && !keystroke.ctrl { + WindowInputHandler { + app: self.cx.clone(), + window_id, + } + .replace_text_in_range(None, &keystroke.key) + } } pub fn add_model(&mut self, build_model: F) -> ModelHandle @@ -1195,6 +1314,11 @@ impl MutableAppContext { .set_menus(menus, &self.keystroke_matcher); } + fn show_character_palette(&self, window_id: usize) { + let (_, window) = &self.presenters_and_platform_windows[&window_id]; + window.show_character_palette(); + } + fn prompt( &self, window_id: usize, @@ -1883,6 +2007,11 @@ impl MutableAppContext { })); } + window.set_input_handler(Box::new(WindowInputHandler { + app: self.upgrade().0, + window_id, + })); + let scene = presenter .borrow_mut() @@ -3174,6 +3303,28 @@ pub trait AnyView { fn on_blur(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize); fn keymap_context(&self, cx: &AppContext) -> keymap::Context; fn debug_json(&self, cx: &AppContext) -> serde_json::Value; + + fn text_for_range(&self, range: Range, cx: &AppContext) -> Option; + fn selected_text_range(&self, cx: &AppContext) -> Option>; + fn marked_text_range(&self, cx: &AppContext) -> Option>; + fn unmark_text(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize); + fn replace_text_in_range( + &mut self, + range: Option>, + text: &str, + cx: &mut MutableAppContext, + window_id: usize, + view_id: usize, + ); + fn replace_and_mark_text_in_range( + &mut self, + range: Option>, + new_text: &str, + new_selected_range: Option>, + cx: &mut MutableAppContext, + window_id: usize, + view_id: usize, + ); } impl AnyView for T @@ -3224,6 +3375,48 @@ where fn debug_json(&self, cx: &AppContext) -> serde_json::Value { View::debug_json(self, cx) } + + fn text_for_range(&self, range: Range, cx: &AppContext) -> Option { + View::text_for_range(self, range, cx) + } + + fn selected_text_range(&self, cx: &AppContext) -> Option> { + View::selected_text_range(self, cx) + } + + fn marked_text_range(&self, cx: &AppContext) -> Option> { + View::marked_text_range(self, cx) + } + + fn unmark_text(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize) { + let mut cx = ViewContext::new(cx, window_id, view_id); + View::unmark_text(self, &mut cx) + } + + fn replace_text_in_range( + &mut self, + range: Option>, + text: &str, + cx: &mut MutableAppContext, + window_id: usize, + view_id: usize, + ) { + let mut cx = ViewContext::new(cx, window_id, view_id); + View::replace_text_in_range(self, range, text, &mut cx) + } + + fn replace_and_mark_text_in_range( + &mut self, + range: Option>, + new_text: &str, + new_selected_range: Option>, + cx: &mut MutableAppContext, + window_id: usize, + view_id: usize, + ) { + let mut cx = ViewContext::new(cx, window_id, view_id); + View::replace_and_mark_text_in_range(self, range, new_text, new_selected_range, &mut cx) + } } pub struct ModelContext<'a, T: ?Sized> { @@ -3489,6 +3682,10 @@ impl<'a, T: View> ViewContext<'a, T> { self.app.platform() } + pub fn show_character_palette(&self) { + self.app.show_character_palette(self.window_id); + } + pub fn prompt( &self, level: PromptLevel, diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 35703b8a1f..7a0da8dad9 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -31,7 +31,9 @@ use crate::{ rect::RectF, vector::{vec2f, Vector2F}, }, - json, Action, DebugContext, Event, EventContext, LayoutContext, PaintContext, RenderContext, + json, + presenter::MeasurementContext, + Action, DebugContext, Event, EventContext, LayoutContext, PaintContext, RenderContext, SizeConstraint, View, }; use core::panic; @@ -41,7 +43,7 @@ use std::{ borrow::Cow, cell::RefCell, mem, - ops::{Deref, DerefMut}, + ops::{Deref, DerefMut, Range}, rc::Rc, }; @@ -49,6 +51,11 @@ trait AnyElement { fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F; fn paint(&mut self, origin: Vector2F, visible_bounds: RectF, cx: &mut PaintContext); fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool; + fn rect_for_text_range( + &self, + range_utf16: Range, + cx: &MeasurementContext, + ) -> Option; fn debug(&self, cx: &DebugContext) -> serde_json::Value; fn size(&self) -> Vector2F; @@ -83,6 +90,16 @@ pub trait Element { cx: &mut EventContext, ) -> bool; + fn rect_for_text_range( + &self, + range_utf16: Range, + bounds: RectF, + visible_bounds: RectF, + layout: &Self::LayoutState, + paint: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option; + fn metadata(&self) -> Option<&dyn Any> { None } @@ -287,6 +304,26 @@ impl AnyElement for Lifecycle { } } + fn rect_for_text_range( + &self, + range_utf16: Range, + cx: &MeasurementContext, + ) -> Option { + if let Lifecycle::PostPaint { + element, + bounds, + visible_bounds, + layout, + paint, + .. + } = self + { + element.rect_for_text_range(range_utf16, *bounds, *visible_bounds, layout, paint, cx) + } else { + None + } + } + fn size(&self) -> Vector2F { match self { Lifecycle::Empty | Lifecycle::Init { .. } => panic!("invalid element lifecycle state"), @@ -385,6 +422,14 @@ impl ElementRc { self.element.borrow_mut().dispatch_event(event, cx) } + pub fn rect_for_text_range( + &self, + range_utf16: Range, + cx: &MeasurementContext, + ) -> Option { + self.element.borrow().rect_for_text_range(range_utf16, cx) + } + pub fn size(&self) -> Vector2F { self.element.borrow().size() } diff --git a/crates/gpui/src/elements/align.rs b/crates/gpui/src/elements/align.rs index 5388f7647e..5158b7229e 100644 --- a/crates/gpui/src/elements/align.rs +++ b/crates/gpui/src/elements/align.rs @@ -1,6 +1,8 @@ use crate::{ geometry::{rect::RectF, vector::Vector2F}, - json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, + json, + presenter::MeasurementContext, + DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; use json::ToJson; @@ -94,6 +96,18 @@ impl Element for Align { self.child.dispatch_event(event, cx) } + fn rect_for_text_range( + &self, + range_utf16: std::ops::Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, cx) + } + fn debug( &self, bounds: pathfinder_geometry::rect::RectF, diff --git a/crates/gpui/src/elements/canvas.rs b/crates/gpui/src/elements/canvas.rs index 2e10c59049..6fec2dd1dc 100644 --- a/crates/gpui/src/elements/canvas.rs +++ b/crates/gpui/src/elements/canvas.rs @@ -1,6 +1,7 @@ use super::Element; use crate::{ json::{self, json}, + presenter::MeasurementContext, DebugContext, PaintContext, }; use json::ToJson; @@ -67,6 +68,18 @@ where false } + fn rect_for_text_range( + &self, + _: std::ops::Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &MeasurementContext, + ) -> Option { + None + } + fn debug( &self, bounds: RectF, diff --git a/crates/gpui/src/elements/constrained_box.rs b/crates/gpui/src/elements/constrained_box.rs index 5ab01df1e1..012bbe23dd 100644 --- a/crates/gpui/src/elements/constrained_box.rs +++ b/crates/gpui/src/elements/constrained_box.rs @@ -1,9 +1,13 @@ +use std::ops::Range; + use json::ToJson; use serde_json::json; use crate::{ geometry::{rect::RectF, vector::Vector2F}, - json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, + json, + presenter::MeasurementContext, + DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; @@ -165,6 +169,18 @@ impl Element for ConstrainedBox { self.child.dispatch_event(event, cx) } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, cx) + } + fn debug( &self, _: RectF, diff --git a/crates/gpui/src/elements/container.rs b/crates/gpui/src/elements/container.rs index b8eebad88c..a581e60b74 100644 --- a/crates/gpui/src/elements/container.rs +++ b/crates/gpui/src/elements/container.rs @@ -1,3 +1,5 @@ +use std::ops::Range; + use crate::{ color::Color, geometry::{ @@ -7,6 +9,7 @@ use crate::{ }, json::ToJson, platform::CursorStyle, + presenter::MeasurementContext, scene::{self, Border, CursorRegion, Quad}, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; @@ -271,6 +274,18 @@ impl Element for Container { self.child.dispatch_event(event, cx) } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, cx) + } + fn debug( &self, bounds: RectF, diff --git a/crates/gpui/src/elements/empty.rs b/crates/gpui/src/elements/empty.rs index afe24127b5..9a67115c23 100644 --- a/crates/gpui/src/elements/empty.rs +++ b/crates/gpui/src/elements/empty.rs @@ -1,9 +1,12 @@ +use std::ops::Range; + use crate::{ geometry::{ rect::RectF, vector::{vec2f, Vector2F}, }, json::{json, ToJson}, + presenter::MeasurementContext, DebugContext, }; use crate::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint}; @@ -67,6 +70,18 @@ impl Element for Empty { false } + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &MeasurementContext, + ) -> Option { + None + } + fn debug( &self, bounds: RectF, diff --git a/crates/gpui/src/elements/event_handler.rs b/crates/gpui/src/elements/event_handler.rs index 55e4928ad8..015e778314 100644 --- a/crates/gpui/src/elements/event_handler.rs +++ b/crates/gpui/src/elements/event_handler.rs @@ -1,11 +1,11 @@ use crate::{ - geometry::vector::Vector2F, CursorRegion, DebugContext, Element, ElementBox, Event, - EventContext, LayoutContext, MouseButton, MouseButtonEvent, MouseRegion, NavigationDirection, - PaintContext, SizeConstraint, + geometry::vector::Vector2F, presenter::MeasurementContext, CursorRegion, DebugContext, Element, + ElementBox, Event, EventContext, LayoutContext, MouseButton, MouseButtonEvent, MouseRegion, + NavigationDirection, PaintContext, SizeConstraint, }; use pathfinder_geometry::rect::RectF; use serde_json::json; -use std::any::TypeId; +use std::{any::TypeId, ops::Range}; pub struct EventHandler { child: ElementBox, @@ -150,6 +150,18 @@ impl Element for EventHandler { } } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, cx) + } + fn debug( &self, _: RectF, diff --git a/crates/gpui/src/elements/expanded.rs b/crates/gpui/src/elements/expanded.rs index 6f69d8a92a..e8803affab 100644 --- a/crates/gpui/src/elements/expanded.rs +++ b/crates/gpui/src/elements/expanded.rs @@ -1,6 +1,10 @@ +use std::ops::Range; + use crate::{ geometry::{rect::RectF, vector::Vector2F}, - json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, + json, + presenter::MeasurementContext, + DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; use serde_json::json; @@ -74,6 +78,18 @@ impl Element for Expanded { self.child.dispatch_event(event, cx) } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, cx) + } + fn debug( &self, _: RectF, diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index cb43c1db68..1d577344c6 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -1,7 +1,8 @@ -use std::{any::Any, f32::INFINITY}; +use std::{any::Any, f32::INFINITY, ops::Range}; use crate::{ json::{self, ToJson, Value}, + presenter::MeasurementContext, Axis, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext, LayoutContext, MouseMovedEvent, PaintContext, RenderContext, ScrollWheelEvent, SizeConstraint, Vector2FExt, View, @@ -334,6 +335,20 @@ impl Element for Flex { handled } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.children + .iter() + .find_map(|child| child.rect_for_text_range(range_utf16.clone(), cx)) + } + fn debug( &self, bounds: RectF, @@ -417,6 +432,18 @@ impl Element for FlexItem { self.child.dispatch_event(event, cx) } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, cx) + } + fn metadata(&self) -> Option<&dyn Any> { Some(&self.metadata) } diff --git a/crates/gpui/src/elements/hook.rs b/crates/gpui/src/elements/hook.rs index e947c3bac7..c620847372 100644 --- a/crates/gpui/src/elements/hook.rs +++ b/crates/gpui/src/elements/hook.rs @@ -1,6 +1,9 @@ +use std::ops::Range; + use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::json, + presenter::MeasurementContext, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; @@ -65,6 +68,18 @@ impl Element for Hook { self.child.dispatch_event(event, cx) } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, cx) + } + fn debug( &self, _: RectF, diff --git a/crates/gpui/src/elements/image.rs b/crates/gpui/src/elements/image.rs index 6b55b567b4..dff46804e7 100644 --- a/crates/gpui/src/elements/image.rs +++ b/crates/gpui/src/elements/image.rs @@ -5,11 +5,12 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::{json, ToJson}, + presenter::MeasurementContext, scene, Border, DebugContext, Element, Event, EventContext, ImageData, LayoutContext, PaintContext, SizeConstraint, }; use serde::Deserialize; -use std::sync::Arc; +use std::{ops::Range, sync::Arc}; pub struct Image { data: Arc, @@ -89,6 +90,18 @@ impl Element for Image { false } + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &MeasurementContext, + ) -> Option { + None + } + fn debug( &self, bounds: RectF, diff --git a/crates/gpui/src/elements/keystroke_label.rs b/crates/gpui/src/elements/keystroke_label.rs index 0112b54846..9b21a1b8a8 100644 --- a/crates/gpui/src/elements/keystroke_label.rs +++ b/crates/gpui/src/elements/keystroke_label.rs @@ -76,6 +76,18 @@ impl Element for KeystrokeLabel { element.dispatch_event(event, cx) } + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &MeasurementContext, + ) -> Option { + None + } + fn debug( &self, _: RectF, diff --git a/crates/gpui/src/elements/label.rs b/crates/gpui/src/elements/label.rs index e6ae9cbd51..1865480868 100644 --- a/crates/gpui/src/elements/label.rs +++ b/crates/gpui/src/elements/label.rs @@ -1,3 +1,5 @@ +use std::ops::Range; + use crate::{ fonts::TextStyle, geometry::{ @@ -5,6 +7,7 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::{ToJson, Value}, + presenter::MeasurementContext, text_layout::{Line, RunStyle}, DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; @@ -174,6 +177,18 @@ impl Element for Label { false } + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &MeasurementContext, + ) -> Option { + None + } + fn debug( &self, bounds: RectF, diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index e368b45288..2b44918e43 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -4,6 +4,7 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::json, + presenter::MeasurementContext, DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext, RenderContext, ScrollWheelEvent, SizeConstraint, View, ViewContext, }; @@ -328,6 +329,39 @@ impl Element for List { handled } + fn rect_for_text_range( + &self, + range_utf16: Range, + bounds: RectF, + _: RectF, + scroll_top: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + let state = self.state.0.borrow(); + let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item); + let mut cursor = state.items.cursor::(); + cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); + while let Some(item) = cursor.item() { + if item_origin.y() > bounds.max_y() { + break; + } + + if let ListItem::Rendered(element) = item { + if let Some(rect) = element.rect_for_text_range(range_utf16.clone(), cx) { + return Some(rect); + } + + item_origin.set_y(item_origin.y() + element.size().y()); + cursor.next(&()); + } else { + unreachable!(); + } + } + + None + } + fn debug( &self, bounds: RectF, @@ -939,6 +973,18 @@ mod tests { todo!() } + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &MeasurementContext, + ) -> Option { + todo!() + } + fn debug(&self, _: RectF, _: &(), _: &(), _: &DebugContext) -> serde_json::Value { self.id.into() } diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 14ebb20de9..0cdbe12304 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -1,5 +1,3 @@ -use std::any::TypeId; - use super::Padding; use crate::{ geometry::{ @@ -8,11 +6,12 @@ use crate::{ }, platform::CursorStyle, scene::{CursorRegion, HandlerSet}, - DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseButton, - MouseButtonEvent, MouseMovedEvent, MouseRegion, MouseState, PaintContext, RenderContext, - SizeConstraint, View, + DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MeasurementContext, + MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion, MouseState, PaintContext, + RenderContext, SizeConstraint, View, }; use serde_json::json; +use std::{any::TypeId, ops::Range}; pub struct MouseEventHandler { child: ElementBox, @@ -150,6 +149,18 @@ impl Element for MouseEventHandler { self.child.dispatch_event(event, cx) } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, cx) + } + fn debug( &self, _: RectF, diff --git a/crates/gpui/src/elements/overlay.rs b/crates/gpui/src/elements/overlay.rs index 4ed0ca7d22..bf2ef59579 100644 --- a/crates/gpui/src/elements/overlay.rs +++ b/crates/gpui/src/elements/overlay.rs @@ -1,6 +1,9 @@ +use std::ops::Range; + use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::ToJson, + presenter::MeasurementContext, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion, PaintContext, SizeConstraint, }; @@ -126,6 +129,18 @@ impl Element for Overlay { self.child.dispatch_event(event, cx) } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, cx) + } + fn debug( &self, _: RectF, diff --git a/crates/gpui/src/elements/stack.rs b/crates/gpui/src/elements/stack.rs index 4531734085..992a8922a7 100644 --- a/crates/gpui/src/elements/stack.rs +++ b/crates/gpui/src/elements/stack.rs @@ -1,6 +1,9 @@ +use std::ops::Range; + use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::{self, json, ToJson}, + presenter::MeasurementContext, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; @@ -64,6 +67,21 @@ impl Element for Stack { false } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.children + .iter() + .rev() + .find_map(|child| child.rect_for_text_range(range_utf16.clone(), cx)) + } + fn debug( &self, bounds: RectF, diff --git a/crates/gpui/src/elements/svg.rs b/crates/gpui/src/elements/svg.rs index d473e1f0fb..544abb3314 100644 --- a/crates/gpui/src/elements/svg.rs +++ b/crates/gpui/src/elements/svg.rs @@ -1,4 +1,4 @@ -use std::borrow::Cow; +use std::{borrow::Cow, ops::Range}; use serde_json::json; @@ -8,6 +8,7 @@ use crate::{ rect::RectF, vector::{vec2f, Vector2F}, }, + presenter::MeasurementContext, scene, DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; @@ -84,6 +85,18 @@ impl Element for Svg { false } + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &MeasurementContext, + ) -> Option { + None + } + fn debug( &self, bounds: RectF, diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index fb1e2ea331..580240e3fd 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -6,6 +6,7 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::{ToJson, Value}, + presenter::MeasurementContext, text_layout::{Line, RunStyle, ShapedBoundary}, DebugContext, Element, Event, EventContext, FontCache, LayoutContext, PaintContext, SizeConstraint, TextLayoutCache, @@ -63,7 +64,7 @@ impl Element for Text { cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { // Convert the string and highlight ranges into an iterator of highlighted chunks. - + let mut offset = 0; let mut highlight_ranges = self.highlights.iter().peekable(); let chunks = std::iter::from_fn(|| { @@ -81,7 +82,8 @@ impl Element for Text { "Highlight out of text range. Text len: {}, Highlight range: {}..{}", self.text.len(), range.start, - range.end); + range.end + ); result = None; } } else if offset < self.text.len() { @@ -188,6 +190,18 @@ impl Element for Text { false } + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &MeasurementContext, + ) -> Option { + None + } + fn debug( &self, bounds: RectF, diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 572d142521..68605fb6b3 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -6,12 +6,14 @@ use crate::{ fonts::TextStyle, geometry::{rect::RectF, vector::Vector2F}, json::json, + presenter::MeasurementContext, Action, Axis, ElementStateHandle, LayoutContext, MouseMovedEvent, PaintContext, RenderContext, SizeConstraint, Task, View, }; use serde::Deserialize; use std::{ cell::{Cell, RefCell}, + ops::Range, rc::Rc, time::Duration, }; @@ -196,6 +198,18 @@ impl Element for Tooltip { self.child.dispatch_event(event, cx) } + fn rect_for_text_range( + &self, + range: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + self.child.rect_for_text_range(range, cx) + } + fn debug( &self, _: RectF, diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 9b2d966a7d..2ab4e91538 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -5,6 +5,7 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::{self, json}, + presenter::MeasurementContext, ElementBox, RenderContext, ScrollWheelEvent, View, }; use json::ToJson; @@ -327,6 +328,21 @@ impl Element for UniformList { handled } + fn rect_for_text_range( + &self, + range: Range, + _: RectF, + _: RectF, + layout: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + layout + .items + .iter() + .find_map(|child| child.rect_for_text_range(range.clone(), cx)) + } + fn debug( &self, bounds: RectF, diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 723e25c55d..4cca93edc8 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -30,7 +30,8 @@ pub mod platform; pub use gpui_macros::test; pub use platform::*; pub use presenter::{ - Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt, + Axis, DebugContext, EventContext, LayoutContext, MeasurementContext, PaintContext, + SizeConstraint, Vector2FExt, }; pub use anyhow; diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index cf508a5634..f85b06438e 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -26,6 +26,7 @@ use serde::Deserialize; use std::{ any::Any, fmt::{self, Display}, + ops::Range, path::{Path, PathBuf}, rc::Rc, str::FromStr, @@ -88,6 +89,21 @@ pub trait Dispatcher: Send + Sync { fn run_on_main_thread(&self, task: Runnable); } +pub trait InputHandler { + fn selected_text_range(&self) -> Option>; + fn marked_text_range(&self) -> Option>; + fn text_for_range(&self, range_utf16: Range) -> Option; + fn replace_text_in_range(&mut self, replacement_range: Option>, text: &str); + fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + new_text: &str, + new_selected_range: Option>, + ); + fn unmark_text(&mut self); + fn rect_for_range(&self, range_utf16: Range) -> Option; +} + pub trait Window: WindowContext { fn as_any_mut(&mut self) -> &mut dyn Any; fn on_event(&mut self, callback: Box bool>); @@ -95,10 +111,12 @@ pub trait Window: WindowContext { fn on_resize(&mut self, callback: Box); fn on_should_close(&mut self, callback: Box bool>); fn on_close(&mut self, callback: Box); + fn set_input_handler(&mut self, input_handler: Box); fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver; fn activate(&self); fn set_title(&mut self, title: &str); fn set_edited(&mut self, edited: bool); + fn show_character_palette(&self); } pub trait WindowContext { diff --git a/crates/gpui/src/platform/event.rs b/crates/gpui/src/platform/event.rs index 57e945175b..e833bd8eb5 100644 --- a/crates/gpui/src/platform/event.rs +++ b/crates/gpui/src/platform/event.rs @@ -3,14 +3,12 @@ use crate::{geometry::vector::Vector2F, keymap::Keystroke}; #[derive(Clone, Debug)] pub struct KeyDownEvent { pub keystroke: Keystroke, - pub input: Option, pub is_held: bool, } #[derive(Clone, Debug)] pub struct KeyUpEvent { pub keystroke: Keystroke, - pub input: Option, } #[derive(Clone, Debug)] diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index 209d8de766..e0e178aa8c 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -10,12 +10,26 @@ use cocoa::{ base::{id, YES}, foundation::NSString as _, }; +use core_graphics::{ + event::{CGEvent, CGEventFlags, CGKeyCode}, + event_source::{CGEventSource, CGEventSourceStateID}, +}; +use objc::{class, msg_send, sel, sel_impl}; use std::{borrow::Cow, ffi::CStr, os::raw::c_char}; +const BACKSPACE_KEY: u16 = 0x7f; +const SPACE_KEY: u16 = b' ' as u16; +const ENTER_KEY: u16 = 0x0d; +const NUMPAD_ENTER_KEY: u16 = 0x03; +const ESCAPE_KEY: u16 = 0x1b; +const TAB_KEY: u16 = 0x09; +const SHIFT_TAB_KEY: u16 = 0x19; + pub fn key_to_native(key: &str) -> Cow { use cocoa::appkit::*; let code = match key { - "backspace" => 0x7F, + "space" => SPACE_KEY, + "backspace" => BACKSPACE_KEY, "up" => NSUpArrowFunctionKey, "down" => NSDownArrowFunctionKey, "left" => NSLeftArrowFunctionKey, @@ -68,49 +82,13 @@ impl Event { cmd, })) } - NSEventType::NSKeyDown => { - let modifiers = native_event.modifierFlags(); - let ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); - let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); - let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); - let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); - let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask); - - let (unmodified_chars, input) = get_key_text(native_event, cmd, ctrl, function)?; - - Some(Self::KeyDown(KeyDownEvent { - keystroke: Keystroke { - ctrl, - alt, - shift, - cmd, - key: unmodified_chars.into(), - }, - input, - is_held: native_event.isARepeat() == YES, - })) - } - NSEventType::NSKeyUp => { - let modifiers = native_event.modifierFlags(); - let ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); - let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); - let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); - let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); - let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask); - - let (unmodified_chars, input) = get_key_text(native_event, cmd, ctrl, function)?; - - Some(Self::KeyUp(KeyUpEvent { - keystroke: Keystroke { - ctrl, - alt, - shift, - cmd, - key: unmodified_chars.into(), - }, - input, - })) - } + NSEventType::NSKeyDown => Some(Self::KeyDown(KeyDownEvent { + keystroke: parse_keystroke(native_event), + is_held: native_event.isARepeat() == YES, + })), + NSEventType::NSKeyUp => Some(Self::KeyUp(KeyUpEvent { + keystroke: parse_keystroke(native_event), + })), NSEventType::NSLeftMouseDown | NSEventType::NSRightMouseDown | NSEventType::NSOtherMouseDown => { @@ -229,72 +207,109 @@ impl Event { } } -unsafe fn get_key_text( - native_event: id, - cmd: bool, - ctrl: bool, - function: bool, -) -> Option<(&'static str, Option)> { - let unmodified_chars = +unsafe fn parse_keystroke(native_event: id) -> Keystroke { + use cocoa::appkit::*; + + let modifiers = native_event.modifierFlags(); + let ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); + let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); + let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); + let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); + + let mut chars_ignoring_modifiers = CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char) .to_str() .unwrap(); - let mut input = None; - let first_char = unmodified_chars.chars().next()?; - use cocoa::appkit::*; - const BACKSPACE_KEY: u16 = 0x7f; - const ENTER_KEY: u16 = 0x0d; - const NUMPAD_ENTER_KEY: u16 = 0x03; - const ESCAPE_KEY: u16 = 0x1b; - const TAB_KEY: u16 = 0x09; - const SHIFT_TAB_KEY: u16 = 0x19; - const SPACE_KEY: u16 = b' ' as u16; - #[allow(non_upper_case_globals)] - let unmodified_chars = match first_char as u16 { - SPACE_KEY => { - input = Some(" ".to_string()); - "space" - } - BACKSPACE_KEY => "backspace", - ENTER_KEY | NUMPAD_ENTER_KEY => "enter", - ESCAPE_KEY => "escape", - TAB_KEY => "tab", - SHIFT_TAB_KEY => "tab", - - NSUpArrowFunctionKey => "up", - NSDownArrowFunctionKey => "down", - NSLeftArrowFunctionKey => "left", - NSRightArrowFunctionKey => "right", - NSPageUpFunctionKey => "pageup", - NSPageDownFunctionKey => "pagedown", - NSDeleteFunctionKey => "delete", - NSF1FunctionKey => "f1", - NSF2FunctionKey => "f2", - NSF3FunctionKey => "f3", - NSF4FunctionKey => "f4", - NSF5FunctionKey => "f5", - NSF6FunctionKey => "f6", - NSF7FunctionKey => "f7", - NSF8FunctionKey => "f8", - NSF9FunctionKey => "f9", - NSF10FunctionKey => "f10", - NSF11FunctionKey => "f11", - NSF12FunctionKey => "f12", - + let key = match chars_ignoring_modifiers.chars().next().map(|ch| ch as u16) { + Some(SPACE_KEY) => "space", + Some(BACKSPACE_KEY) => "backspace", + Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter", + Some(ESCAPE_KEY) => "escape", + Some(TAB_KEY) => "tab", + Some(SHIFT_TAB_KEY) => "tab", + Some(NSUpArrowFunctionKey) => "up", + Some(NSDownArrowFunctionKey) => "down", + Some(NSLeftArrowFunctionKey) => "left", + Some(NSRightArrowFunctionKey) => "right", + Some(NSPageUpFunctionKey) => "pageup", + Some(NSPageDownFunctionKey) => "pagedown", + Some(NSDeleteFunctionKey) => "delete", + Some(NSF1FunctionKey) => "f1", + Some(NSF2FunctionKey) => "f2", + Some(NSF3FunctionKey) => "f3", + Some(NSF4FunctionKey) => "f4", + Some(NSF5FunctionKey) => "f5", + Some(NSF6FunctionKey) => "f6", + Some(NSF7FunctionKey) => "f7", + Some(NSF8FunctionKey) => "f8", + Some(NSF9FunctionKey) => "f9", + Some(NSF10FunctionKey) => "f10", + Some(NSF11FunctionKey) => "f11", + Some(NSF12FunctionKey) => "f12", _ => { - if !cmd && !ctrl && !function { - input = Some( - CStr::from_ptr(native_event.characters().UTF8String() as *mut c_char) - .to_str() - .unwrap() - .into(), - ); + let mut chars_ignoring_modifiers_and_shift = + chars_for_modified_key(native_event.keyCode(), false, false); + + // Honor ⌘ when Dvorak-QWERTY is used. + let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), true, false); + if cmd && chars_ignoring_modifiers_and_shift != chars_with_cmd { + chars_ignoring_modifiers = + chars_for_modified_key(native_event.keyCode(), true, shift); + chars_ignoring_modifiers_and_shift = chars_with_cmd; + } + + if shift { + if chars_ignoring_modifiers_and_shift + == chars_ignoring_modifiers.to_ascii_lowercase() + { + chars_ignoring_modifiers_and_shift + } else if chars_ignoring_modifiers_and_shift != chars_ignoring_modifiers { + shift = false; + chars_ignoring_modifiers + } else { + chars_ignoring_modifiers + } + } else { + chars_ignoring_modifiers } - unmodified_chars } }; - Some((unmodified_chars, input)) + Keystroke { + ctrl, + alt, + shift, + cmd, + key: key.into(), + } +} + +fn chars_for_modified_key<'a>(code: CGKeyCode, cmd: bool, shift: bool) -> &'a str { + // Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that + // always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing + // an event with the given flags instead lets us access `characters`, which always + // returns a valid string. + let event = CGEvent::new_keyboard_event( + CGEventSource::new(CGEventSourceStateID::Private).unwrap(), + code, + true, + ) + .unwrap(); + let mut flags = CGEventFlags::empty(); + if cmd { + flags |= CGEventFlags::CGEventFlagCommand; + } + if shift { + flags |= CGEventFlags::CGEventFlagShift; + } + event.set_flags(flags); + + let event: id = unsafe { msg_send![class!(NSEvent), eventWithCGEvent: event] }; + unsafe { + CStr::from_ptr(event.characters().UTF8String()) + .to_str() + .unwrap() + } } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 577b6808c8..b34da93c45 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1,3 +1,4 @@ +use super::{geometry::RectFExt, renderer::Renderer}; use crate::{ executor, geometry::{ @@ -6,7 +7,8 @@ use crate::{ }, keymap::Keystroke, platform::{self, Event, WindowBounds, WindowContext}, - KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent, MouseMovedEvent, Scene, + InputHandler, KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent, + MouseMovedEvent, Scene, }; use block::ConcreteBlock; use cocoa::{ @@ -15,7 +17,9 @@ use cocoa::{ NSViewHeightSizable, NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowStyleMask, }, base::{id, nil}, - foundation::{NSAutoreleasePool, NSInteger, NSSize, NSString}, + foundation::{ + NSAutoreleasePool, NSInteger, NSNotFound, NSPoint, NSRect, NSSize, NSString, NSUInteger, + }, quartzcore::AutoresizingMask, }; use core_graphics::display::CGRect; @@ -34,20 +38,71 @@ use std::{ any::Any, cell::{Cell, RefCell}, convert::TryInto, - ffi::c_void, - mem, ptr, + ffi::{c_void, CStr}, + mem, + ops::Range, + os::raw::c_char, + ptr, rc::{Rc, Weak}, sync::Arc, time::Duration, }; -use super::{geometry::RectFExt, renderer::Renderer}; - const WINDOW_STATE_IVAR: &'static str = "windowState"; static mut WINDOW_CLASS: *const Class = ptr::null(); static mut VIEW_CLASS: *const Class = ptr::null(); +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct NSRange { + pub location: NSUInteger, + pub length: NSUInteger, +} + +impl NSRange { + fn invalid() -> Self { + Self { + location: NSNotFound as NSUInteger, + length: 0, + } + } + + fn is_valid(&self) -> bool { + self.location != NSNotFound as NSUInteger + } + + fn to_range(&self) -> Option> { + if self.is_valid() { + let start = self.location as usize; + let end = start + self.length as usize; + Some(start..end) + } else { + None + } + } +} + +impl From> for NSRange { + fn from(range: Range) -> Self { + NSRange { + location: range.start as NSUInteger, + length: range.len() as NSUInteger, + } + } +} + +unsafe impl objc::Encode for NSRange { + fn encode() -> objc::Encoding { + let encoding = format!( + "{{NSRange={}{}}}", + NSUInteger::encode().as_str(), + NSUInteger::encode().as_str() + ); + unsafe { objc::Encoding::from_str(&encoding) } + } +} + #[allow(non_upper_case_globals)] const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2; @@ -163,6 +218,48 @@ unsafe fn build_classes() { display_layer as extern "C" fn(&Object, Sel, id), ); + decl.add_protocol(Protocol::get("NSTextInputClient").unwrap()); + decl.add_method( + sel!(validAttributesForMarkedText), + valid_attributes_for_marked_text as extern "C" fn(&Object, Sel) -> id, + ); + decl.add_method( + sel!(hasMarkedText), + has_marked_text as extern "C" fn(&Object, Sel) -> BOOL, + ); + decl.add_method( + sel!(markedRange), + marked_range as extern "C" fn(&Object, Sel) -> NSRange, + ); + decl.add_method( + sel!(selectedRange), + selected_range as extern "C" fn(&Object, Sel) -> NSRange, + ); + decl.add_method( + sel!(firstRectForCharacterRange:actualRange:), + first_rect_for_character_range as extern "C" fn(&Object, Sel, NSRange, id) -> NSRect, + ); + decl.add_method( + sel!(insertText:replacementRange:), + insert_text as extern "C" fn(&Object, Sel, id, NSRange), + ); + decl.add_method( + sel!(setMarkedText:selectedRange:replacementRange:), + set_marked_text as extern "C" fn(&Object, Sel, id, NSRange, NSRange), + ); + decl.add_method(sel!(unmarkText), unmark_text as extern "C" fn(&Object, Sel)); + decl.add_method( + sel!(attributedSubstringForProposedRange:actualRange:), + attributed_substring_for_proposed_range + as extern "C" fn(&Object, Sel, NSRange, *mut c_void) -> id, + ); + + // Suppress beep on keystrokes with modifier keys. + decl.add_method( + sel!(doCommandBySelector:), + do_command_by_selector as extern "C" fn(&Object, Sel, Sel), + ); + decl.register() }; } @@ -177,12 +274,14 @@ struct WindowState { resize_callback: Option>, should_close_callback: Option bool>>, close_callback: Option>, + input_handler: Option>, + pending_key_down_event: Option, synthetic_drag_counter: usize, executor: Rc, scene_to_render: Option, renderer: Renderer, command_queue: metal::CommandQueue, - last_fresh_keydown: Option<(Keystroke, Option)>, + last_fresh_keydown: Option, layer: id, traffic_light_position: Option, previous_modifiers_changed_event: Option, @@ -263,6 +362,8 @@ impl Window { should_close_callback: None, close_callback: None, activate_callback: None, + input_handler: None, + pending_key_down_event: None, synthetic_drag_counter: 0, executor, scene_to_render: Default::default(), @@ -371,6 +472,10 @@ impl platform::Window for Window { self.0.as_ref().borrow_mut().activate_callback = Some(callback); } + fn set_input_handler(&mut self, input_handler: Box) { + self.0.as_ref().borrow_mut().input_handler = Some(input_handler); + } + fn prompt( &self, level: platform::PromptLevel, @@ -448,6 +553,14 @@ impl platform::Window for Window { // so we have to move it again. self.0.borrow().move_traffic_light(); } + + fn show_character_palette(&self) { + unsafe { + let app = NSApplication::sharedApplication(nil); + let window = self.0.borrow().native_window; + let _: () = msg_send![app, orderFrontCharacterPalette: window]; + } + } } impl platform::WindowContext for Window { @@ -581,38 +694,53 @@ extern "C" fn dealloc_view(this: &Object, _: Sel) { extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> BOOL { let window_state = unsafe { get_window_state(this) }; + let mut window_state_borrow = window_state.as_ref().borrow_mut(); let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) }; if let Some(event) = event { - match &event { - Event::KeyDown(KeyDownEvent { - keystroke, - input, - is_held, - }) => { - let keydown = (keystroke.clone(), input.clone()); + window_state_borrow.pending_key_down_event = match event { + Event::KeyDown(event) => { + let keydown = event.keystroke.clone(); // Ignore events from held-down keys after some of the initially-pressed keys // were released. - if *is_held { + if event.is_held { if window_state_borrow.last_fresh_keydown.as_ref() != Some(&keydown) { return YES; } } else { window_state_borrow.last_fresh_keydown = Some(keydown); } + + Some(event) } _ => return NO, + }; + drop(window_state_borrow); + + unsafe { + let input_context: id = msg_send![this, inputContext]; + let _: BOOL = msg_send![input_context, handleEvent: native_event]; } - if let Some(mut callback) = window_state_borrow.event_callback.take() { - drop(window_state_borrow); - let handled = callback(event); - window_state.borrow_mut().event_callback = Some(callback); - handled as BOOL - } else { - NO + let mut window_state_borrow = window_state.borrow_mut(); + if let Some(event) = window_state_borrow.pending_key_down_event.take() { + if let Some(mut callback) = window_state_borrow.event_callback.take() { + drop(window_state_borrow); + + let is_composing = + with_input_handler(this, |input_handler| input_handler.marked_text_range()) + .flatten() + .is_some(); + if !is_composing { + callback(Event::KeyDown(event)); + } + + window_state.borrow_mut().event_callback = Some(callback); + } } + + YES } else { NO } @@ -624,7 +752,6 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { let mut window_state_borrow = window_state.as_ref().borrow_mut(); let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) }; - if let Some(event) = event { match &event { Event::MouseMoved( @@ -691,21 +818,19 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { let window_state = unsafe { get_window_state(this) }; let mut window_state_borrow = window_state.as_ref().borrow_mut(); - let chars = ".".to_string(); let keystroke = Keystroke { cmd: true, ctrl: false, alt: false, shift: false, - key: chars.clone(), + key: ".".into(), }; let event = Event::KeyDown(KeyDownEvent { keystroke: keystroke.clone(), - input: Some(chars.clone()), is_held: false, }); - window_state_borrow.last_fresh_keydown = Some((keystroke, Some(chars))); + window_state_borrow.last_fresh_keydown = Some(keystroke); if let Some(mut callback) = window_state_borrow.event_callback.take() { drop(window_state_borrow); callback(event); @@ -866,6 +991,164 @@ extern "C" fn display_layer(this: &Object, _: Sel, _: id) { } } +extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id { + unsafe { msg_send![class!(NSArray), array] } +} + +extern "C" fn has_marked_text(this: &Object, _: Sel) -> BOOL { + with_input_handler(this, |input_handler| input_handler.marked_text_range()) + .flatten() + .is_some() as BOOL +} + +extern "C" fn marked_range(this: &Object, _: Sel) -> NSRange { + with_input_handler(this, |input_handler| input_handler.marked_text_range()) + .flatten() + .map_or(NSRange::invalid(), |range| range.into()) +} + +extern "C" fn selected_range(this: &Object, _: Sel) -> NSRange { + with_input_handler(this, |input_handler| input_handler.selected_text_range()) + .flatten() + .map_or(NSRange::invalid(), |range| range.into()) +} + +extern "C" fn first_rect_for_character_range( + this: &Object, + _: Sel, + range: NSRange, + _: id, +) -> NSRect { + let frame = unsafe { + let window = get_window_state(this).borrow().native_window; + NSView::frame(window) + }; + + with_input_handler(this, |input_handler| { + input_handler.rect_for_range(range.to_range()?) + }) + .flatten() + .map_or( + NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)), + |rect| { + NSRect::new( + NSPoint::new( + frame.origin.x + rect.origin_x() as f64, + frame.origin.y + frame.size.height - rect.origin_y() as f64, + ), + NSSize::new(rect.width() as f64, rect.height() as f64), + ) + }, + ) +} + +extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NSRange) { + unsafe { + let window_state = get_window_state(this); + let mut window_state_borrow = window_state.borrow_mut(); + let pending_key_down_event = window_state_borrow.pending_key_down_event.take(); + drop(window_state_borrow); + + let is_attributed_string: BOOL = + msg_send![text, isKindOfClass: [class!(NSAttributedString)]]; + let text: id = if is_attributed_string == YES { + msg_send![text, string] + } else { + text + }; + let text = CStr::from_ptr(text.UTF8String() as *mut c_char) + .to_str() + .unwrap(); + let replacement_range = replacement_range.to_range(); + + let is_composing = + with_input_handler(this, |input_handler| input_handler.marked_text_range()) + .flatten() + .is_some(); + + if is_composing || text.chars().count() > 1 || pending_key_down_event.is_none() { + with_input_handler(this, |input_handler| { + input_handler.replace_text_in_range(replacement_range, text) + }); + } else { + let mut handled = false; + + let event_callback = window_state.borrow_mut().event_callback.take(); + if let Some(mut event_callback) = event_callback { + handled = event_callback(Event::KeyDown(pending_key_down_event.unwrap())); + window_state.borrow_mut().event_callback = Some(event_callback); + } + + if !handled { + with_input_handler(this, |input_handler| { + input_handler.replace_text_in_range(replacement_range, text) + }); + } + } + } +} + +extern "C" fn set_marked_text( + this: &Object, + _: Sel, + text: id, + selected_range: NSRange, + replacement_range: NSRange, +) { + unsafe { + get_window_state(this) + .borrow_mut() + .pending_key_down_event + .take(); + + let is_attributed_string: BOOL = + msg_send![text, isKindOfClass: [class!(NSAttributedString)]]; + let text: id = if is_attributed_string == YES { + msg_send![text, string] + } else { + text + }; + let selected_range = selected_range.to_range(); + let replacement_range = replacement_range.to_range(); + let text = CStr::from_ptr(text.UTF8String() as *mut c_char) + .to_str() + .unwrap(); + + with_input_handler(this, |input_handler| { + input_handler.replace_and_mark_text_in_range(replacement_range, text, selected_range); + }); + } +} + +extern "C" fn unmark_text(this: &Object, _: Sel) { + with_input_handler(this, |input_handler| input_handler.unmark_text()); +} + +extern "C" fn attributed_substring_for_proposed_range( + this: &Object, + _: Sel, + range: NSRange, + _actual_range: *mut c_void, +) -> id { + with_input_handler(this, |input_handler| { + let range = range.to_range()?; + if range.is_empty() { + return None; + } + + let selected_text = input_handler.text_for_range(range)?; + unsafe { + let string: id = msg_send![class!(NSAttributedString), alloc]; + let string: id = msg_send![string, initWithString: ns_string(&selected_text)]; + Some(string) + } + }) + .flatten() + .unwrap_or(nil) +} + +extern "C" fn do_command_by_selector(_: &Object, _: Sel, _: Sel) {} + async fn synthetic_drag( window_state: Weak>, drag_id: usize, @@ -891,3 +1174,19 @@ async fn synthetic_drag( unsafe fn ns_string(string: &str) -> id { NSString::alloc(nil).init_str(string).autorelease() } + +fn with_input_handler(window: &Object, f: F) -> Option +where + F: FnOnce(&mut dyn InputHandler) -> R, +{ + let window_state = unsafe { get_window_state(window) }; + let mut window_state_borrow = window_state.as_ref().borrow_mut(); + if let Some(mut input_handler) = window_state_borrow.input_handler.take() { + drop(window_state_borrow); + let result = f(input_handler.as_mut()); + window_state.borrow_mut().input_handler = Some(input_handler); + Some(result) + } else { + None + } +} diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index a58bd603f2..dfb92b77f0 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -255,6 +255,8 @@ impl super::Window for Window { self.close_handlers.push(callback); } + fn set_input_handler(&mut self, _: Box) {} + fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str]) -> oneshot::Receiver { let (done_tx, done_rx) = oneshot::channel(); self.pending_prompts.borrow_mut().push_back(done_tx); @@ -274,6 +276,8 @@ impl super::Window for Window { fn on_should_close(&mut self, callback: Box bool>) { self.should_close_handler = Some(callback); } + + fn show_character_palette(&self) {} } pub fn platform() -> Platform { diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index a2b3230190..d0af2473a6 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -19,7 +19,7 @@ use smallvec::SmallVec; use std::{ collections::{HashMap, HashSet}, marker::PhantomData, - ops::{Deref, DerefMut}, + ops::{Deref, DerefMut, Range}, sync::Arc, }; @@ -224,6 +224,17 @@ impl Presenter { } } + pub fn rect_for_text_range(&self, range_utf16: Range, cx: &AppContext) -> Option { + cx.focused_view_id(self.window_id).and_then(|view_id| { + let cx = MeasurementContext { + app: cx, + rendered_views: &self.rendered_views, + window_id: self.window_id, + }; + cx.rect_for_text_range(view_id, range_utf16) + }) + } + pub fn dispatch_event(&mut self, event: Event, cx: &mut MutableAppContext) -> bool { if let Some(root_view_id) = cx.root_view_id(self.window_id) { let mut invalidated_views = Vec::new(); @@ -717,6 +728,27 @@ impl<'a> DerefMut for EventContext<'a> { } } +pub struct MeasurementContext<'a> { + app: &'a AppContext, + rendered_views: &'a HashMap, + pub window_id: usize, +} + +impl<'a> Deref for MeasurementContext<'a> { + type Target = AppContext; + + fn deref(&self) -> &Self::Target { + self.app + } +} + +impl<'a> MeasurementContext<'a> { + fn rect_for_text_range(&self, view_id: usize, range_utf16: Range) -> Option { + let element = self.rendered_views.get(&view_id)?; + element.rect_for_text_range(range_utf16, self) + } +} + pub struct DebugContext<'a> { rendered_views: &'a HashMap, pub font_cache: &'a FontCache, @@ -876,6 +908,18 @@ impl Element for ChildView { cx.dispatch_event(self.view.id(), event) } + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &MeasurementContext, + ) -> Option { + cx.rect_for_text_range(self.view.id(), range_utf16) + } + fn debug( &self, bounds: RectF, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 50c12d751e..bff9438124 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1076,6 +1076,10 @@ impl Buffer { self.text.finalize_last_transaction() } + pub fn group_until_transaction(&mut self, transaction_id: TransactionId) { + self.text.group_until_transaction(transaction_id); + } + pub fn forget_transaction(&mut self, transaction_id: TransactionId) { self.text.forget_transaction(transaction_id); } diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 5cb3feb214..4dca2f855d 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -39,11 +39,6 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation { local_timestamp: undo.id.value, lamport_timestamp: lamport_timestamp.value, version: serialize_version(&undo.version), - transaction_ranges: undo - .transaction_ranges - .iter() - .map(serialize_range) - .collect(), transaction_version: serialize_version(&undo.transaction_version), counts: undo .counts @@ -204,11 +199,6 @@ pub fn deserialize_operation(message: proto::Operation) -> Result { ) }) .collect(), - transaction_ranges: undo - .transaction_ranges - .into_iter() - .map(deserialize_range) - .collect(), transaction_version: deserialize_version(undo.transaction_version), }, }), @@ -460,8 +450,6 @@ pub fn serialize_transaction(transaction: &Transaction) -> proto::Transaction { .map(serialize_local_timestamp) .collect(), start: serialize_version(&transaction.start), - end: serialize_version(&transaction.end), - ranges: transaction.ranges.iter().map(serialize_range).collect(), } } @@ -478,12 +466,6 @@ pub fn deserialize_transaction(transaction: proto::Transaction) -> Result Result<()> { - let buffer_size = text.summary().bytes.min(10 * 1024); + let buffer_size = text.summary().len.min(10 * 1024); let file = smol::fs::File::create(path).await?; let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file); for chunk in chunks(text, line_ending) { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 6610ad41a1..35f3049edb 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -534,8 +534,6 @@ message Transaction { LocalTimestamp id = 1; repeated LocalTimestamp edit_ids = 2; repeated VectorClockEntry start = 3; - repeated VectorClockEntry end = 4; - repeated Range ranges = 5; } message LocalTimestamp { @@ -890,7 +888,6 @@ message Operation { uint32 local_timestamp = 2; uint32 lamport_timestamp = 3; repeated VectorClockEntry version = 4; - repeated Range transaction_ranges = 5; repeated VectorClockEntry transaction_version = 6; repeated UndoCount counts = 7; } diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 5fff19bb06..015ac10707 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 27; +pub const PROTOCOL_VERSION: u32 = 28; diff --git a/crates/terminal/src/connected_el.rs b/crates/terminal/src/connected_el.rs index 506e846e93..865bbc7de7 100644 --- a/crates/terminal/src/connected_el.rs +++ b/crates/terminal/src/connected_el.rs @@ -796,6 +796,27 @@ impl Element for TerminalEl { "type": "TerminalElement", }) } + + fn rect_for_text_range( + &self, + _: Range, + bounds: RectF, + _: RectF, + layout: &Self::LayoutState, + _: &Self::PaintState, + _: &gpui::MeasurementContext, + ) -> Option { + // Use the same origin that's passed to `Cursor::paint` in the paint + // method bove. + let mut origin = bounds.origin() + vec2f(layout.size.cell_width, 0.); + + // TODO - Why is it necessary to move downward one line to get correct + // positioning? I would think that we'd want the same rect that is + // painted for the cursor. + origin += vec2f(0., layout.size.line_height); + + Some(layout.cursor.as_ref()?.bounding_rect(origin)) + } } mod test { diff --git a/crates/terminal/src/connected_view.rs b/crates/terminal/src/connected_view.rs index 633bd70b5a..e7b7131147 100644 --- a/crates/terminal/src/connected_view.rs +++ b/crates/terminal/src/connected_view.rs @@ -1,6 +1,6 @@ use gpui::{ - actions, keymap::Keystroke, ClipboardItem, Element, ElementBox, ModelHandle, MutableAppContext, - View, ViewContext, + actions, keymap::Keystroke, AppContext, ClipboardItem, Element, ElementBox, ModelHandle, + MutableAppContext, View, ViewContext, }; use crate::{ @@ -159,4 +159,18 @@ impl View for ConnectedView { fn on_focus(&mut self, _cx: &mut ViewContext) { self.has_new_content = false; } + + fn selected_text_range(&self, _: &AppContext) -> Option> { + Some(0..0) + } + + fn replace_text_in_range( + &mut self, + _: Option>, + text: &str, + cx: &mut ViewContext, + ) { + self.terminal + .update(cx, |terminal, _| terminal.write_to_pty(text.into())); + } } diff --git a/crates/terminal/src/mappings/keys.rs b/crates/terminal/src/mappings/keys.rs index 51d02d6bb2..f88bfa927a 100644 --- a/crates/terminal/src/mappings/keys.rs +++ b/crates/terminal/src/mappings/keys.rs @@ -231,53 +231,7 @@ pub fn to_esc_str(keystroke: &Keystroke, mode: &TermMode) -> Option { } } - //Fallback to sending the keystroke input directly - //Skin colors in utf8 are implemented as a seperate, invisible character - //that modifies the associated emoji. Some languages may have similarly - //implemented modifiers, e.g. certain diacritics that can be typed as a single character. - //This means that we need to assume some user input can result in multi-byte, - //multi-char strings. This is somewhat difficult, as GPUI normalizes all - //keys into a string representation. Hence, the check here to filter out GPUI - //keys that weren't captured above. - if !matches_gpui_key_str(&keystroke.key) { - return Some(keystroke.key.clone()); - } else { - None - } -} - -///Checks if the given string matches a GPUI key string. -///Table made from reading the source at gpui/src/platform/mac/event.rs -fn matches_gpui_key_str(str: &str) -> bool { - match str { - "backspace" => true, - "up" => true, - "down" => true, - "left" => true, - "right" => true, - "pageup" => true, - "pagedown" => true, - "home" => true, - "end" => true, - "delete" => true, - "enter" => true, - "escape" => true, - "tab" => true, - "f1" => true, - "f2" => true, - "f3" => true, - "f4" => true, - "f5" => true, - "f6" => true, - "f7" => true, - "f8" => true, - "f9" => true, - "f10" => true, - "f11" => true, - "f12" => true, - "space" => true, - _ => false, - } + None } /// Code Modifiers @@ -351,17 +305,15 @@ mod test { } #[test] - fn test_multi_char_fallthrough() { + fn test_plain_inputs() { let ks = Keystroke { ctrl: false, alt: false, shift: false, cmd: false, - key: "🖖🏻".to_string(), //2 char string }; - - assert_eq!(to_esc_str(&ks, &TermMode::NONE), Some("🖖🏻".to_string())); + assert_eq!(to_esc_str(&ks, &TermMode::NONE), None); } #[test] diff --git a/crates/text/src/offset_utf16.rs b/crates/text/src/offset_utf16.rs new file mode 100644 index 0000000000..9a52b3c3f9 --- /dev/null +++ b/crates/text/src/offset_utf16.rs @@ -0,0 +1,50 @@ +use std::ops::{Add, AddAssign, Sub}; + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] +pub struct OffsetUtf16(pub usize); + +impl<'a> Add<&'a Self> for OffsetUtf16 { + type Output = Self; + + fn add(self, other: &'a Self) -> Self::Output { + Self(self.0 + other.0) + } +} + +impl Add for OffsetUtf16 { + type Output = Self; + + fn add(self, other: Self) -> Self::Output { + Self(self.0 + other.0) + } +} + +impl<'a> Sub<&'a Self> for OffsetUtf16 { + type Output = Self; + + fn sub(self, other: &'a Self) -> Self::Output { + debug_assert!(*other <= self); + Self(self.0 - other.0) + } +} + +impl Sub for OffsetUtf16 { + type Output = OffsetUtf16; + + fn sub(self, other: Self) -> Self::Output { + debug_assert!(other <= self); + Self(self.0 - other.0) + } +} + +impl<'a> AddAssign<&'a Self> for OffsetUtf16 { + fn add_assign(&mut self, other: &'a Self) { + self.0 += other.0; + } +} + +impl AddAssign for OffsetUtf16 { + fn add_assign(&mut self, other: Self) { + self.0 += other.0; + } +} diff --git a/crates/text/src/rope.rs b/crates/text/src/rope.rs index e8aff3f52f..012b7fdd66 100644 --- a/crates/text/src/rope.rs +++ b/crates/text/src/rope.rs @@ -1,6 +1,5 @@ -use crate::PointUtf16; - use super::Point; +use crate::{OffsetUtf16, PointUtf16}; use arrayvec::ArrayString; use bromberg_sl2::{DigestString, HashMatrix}; use smallvec::SmallVec; @@ -165,8 +164,34 @@ impl Rope { Chunks::new(self, range, true) } + pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { + if offset >= self.summary().len { + return self.summary().len_utf16; + } + let mut cursor = self.chunks.cursor::<(usize, OffsetUtf16)>(); + cursor.seek(&offset, Bias::Left, &()); + let overshoot = offset - cursor.start().0; + cursor.start().1 + + cursor.item().map_or(Default::default(), |chunk| { + chunk.offset_to_offset_utf16(overshoot) + }) + } + + pub fn offset_utf16_to_offset(&self, offset: OffsetUtf16) -> usize { + if offset >= self.summary().len_utf16 { + return self.summary().len; + } + let mut cursor = self.chunks.cursor::<(OffsetUtf16, usize)>(); + cursor.seek(&offset, Bias::Left, &()); + let overshoot = offset - cursor.start().0; + cursor.start().1 + + cursor.item().map_or(Default::default(), |chunk| { + chunk.offset_utf16_to_offset(overshoot) + }) + } + pub fn offset_to_point(&self, offset: usize) -> Point { - if offset >= self.summary().bytes { + if offset >= self.summary().len { return self.summary().lines; } let mut cursor = self.chunks.cursor::<(usize, Point)>(); @@ -179,8 +204,8 @@ impl Rope { } pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 { - if offset >= self.summary().bytes { - return self.summary().lines_utf16; + if offset >= self.summary().len { + return self.summary().lines_utf16(); } let mut cursor = self.chunks.cursor::<(usize, PointUtf16)>(); cursor.seek(&offset, Bias::Left, &()); @@ -193,7 +218,7 @@ impl Rope { pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 { if point >= self.summary().lines { - return self.summary().lines_utf16; + return self.summary().lines_utf16(); } let mut cursor = self.chunks.cursor::<(Point, PointUtf16)>(); cursor.seek(&point, Bias::Left, &()); @@ -206,7 +231,7 @@ impl Rope { pub fn point_to_offset(&self, point: Point) -> usize { if point >= self.summary().lines { - return self.summary().bytes; + return self.summary().len; } let mut cursor = self.chunks.cursor::<(Point, usize)>(); cursor.seek(&point, Bias::Left, &()); @@ -218,8 +243,8 @@ impl Rope { } pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { - if point >= self.summary().lines_utf16 { - return self.summary().bytes; + if point >= self.summary().lines_utf16() { + return self.summary().len; } let mut cursor = self.chunks.cursor::<(PointUtf16, usize)>(); cursor.seek(&point, Bias::Left, &()); @@ -231,7 +256,7 @@ impl Rope { } pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point { - if point >= self.summary().lines_utf16 { + if point >= self.summary().lines_utf16() { return self.summary().lines; } let mut cursor = self.chunks.cursor::<(PointUtf16, Point)>(); @@ -262,7 +287,18 @@ impl Rope { } offset } else { - self.summary().bytes + self.summary().len + } + } + + pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 { + let mut cursor = self.chunks.cursor::(); + cursor.seek(&offset, Bias::Right, &()); + if let Some(chunk) = cursor.item() { + let overshoot = offset - cursor.start(); + *cursor.start() + chunk.clip_offset_utf16(overshoot, bias) + } else { + self.summary().len_utf16 } } @@ -284,7 +320,7 @@ impl Rope { let overshoot = point - cursor.start(); *cursor.start() + chunk.clip_point_utf16(overshoot, bias) } else { - self.summary().lines_utf16 + self.summary().lines_utf16() } } @@ -543,6 +579,34 @@ impl<'a> io::Read for Bytes<'a> { struct Chunk(ArrayString<{ 2 * CHUNK_BASE }>); impl Chunk { + fn offset_to_offset_utf16(&self, target: usize) -> OffsetUtf16 { + let mut offset = 0; + let mut offset_utf16 = OffsetUtf16(0); + for ch in self.0.chars() { + if offset >= target { + break; + } + + offset += ch.len_utf8(); + offset_utf16.0 += ch.len_utf16(); + } + offset_utf16 + } + + fn offset_utf16_to_offset(&self, target: OffsetUtf16) -> usize { + let mut offset_utf16 = OffsetUtf16(0); + let mut offset = 0; + for ch in self.0.chars() { + if offset_utf16 >= target { + break; + } + + offset += ch.len_utf8(); + offset_utf16.0 += ch.len_utf16(); + } + offset + } + fn offset_to_point(&self, target: usize) -> Point { let mut offset = 0; let mut point = Point::new(0, 0); @@ -712,6 +776,18 @@ impl Chunk { } unreachable!() } + + fn clip_offset_utf16(&self, target: OffsetUtf16, bias: Bias) -> OffsetUtf16 { + let mut code_units = self.0.encode_utf16(); + let mut offset = code_units.by_ref().take(target.0 as usize).count(); + if char::decode_utf16(code_units).next().transpose().is_err() { + match bias { + Bias::Left => offset -= 1, + Bias::Right => offset += 1, + } + } + OffsetUtf16(offset) + } } impl sum_tree::Item for Chunk { @@ -748,31 +824,44 @@ impl sum_tree::Summary for ChunkSummary { #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct TextSummary { - pub bytes: usize, + pub len: usize, + pub len_utf16: OffsetUtf16, pub lines: Point, - pub lines_utf16: PointUtf16, pub first_line_chars: u32, pub last_line_chars: u32, + pub last_line_len_utf16: u32, pub longest_row: u32, pub longest_row_chars: u32, } +impl TextSummary { + pub fn lines_utf16(&self) -> PointUtf16 { + PointUtf16 { + row: self.lines.row, + column: self.last_line_len_utf16, + } + } +} + impl<'a> From<&'a str> for TextSummary { fn from(text: &'a str) -> Self { + let mut len_utf16 = OffsetUtf16(0); let mut lines = Point::new(0, 0); - let mut lines_utf16 = PointUtf16::new(0, 0); let mut first_line_chars = 0; let mut last_line_chars = 0; + let mut last_line_len_utf16 = 0; let mut longest_row = 0; let mut longest_row_chars = 0; for c in text.chars() { + len_utf16.0 += c.len_utf16(); + if c == '\n' { lines += Point::new(1, 0); - lines_utf16 += PointUtf16::new(1, 0); + last_line_len_utf16 = 0; last_line_chars = 0; } else { lines.column += c.len_utf8() as u32; - lines_utf16.column += c.len_utf16() as u32; + last_line_len_utf16 += c.len_utf16() as u32; last_line_chars += 1; } @@ -787,11 +876,12 @@ impl<'a> From<&'a str> for TextSummary { } TextSummary { - bytes: text.len(), + len: text.len(), + len_utf16, lines, - lines_utf16, first_line_chars, last_line_chars, + last_line_len_utf16, longest_row, longest_row_chars, } @@ -833,13 +923,15 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary { if other.lines.row == 0 { self.last_line_chars += other.first_line_chars; + self.last_line_len_utf16 += other.last_line_len_utf16; } else { self.last_line_chars = other.last_line_chars; + self.last_line_len_utf16 = other.last_line_len_utf16; } - self.bytes += other.bytes; + self.len += other.len; + self.len_utf16 += other.len_utf16; self.lines += other.lines; - self.lines_utf16 += other.lines_utf16; } } @@ -886,13 +978,29 @@ impl TextDimension for TextSummary { impl<'a> sum_tree::Dimension<'a, ChunkSummary> for usize { fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) { - *self += summary.text.bytes; + *self += summary.text.len; } } impl TextDimension for usize { fn from_text_summary(summary: &TextSummary) -> Self { - summary.bytes + summary.len + } + + fn add_assign(&mut self, other: &Self) { + *self += other; + } +} + +impl<'a> sum_tree::Dimension<'a, ChunkSummary> for OffsetUtf16 { + fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) { + *self += summary.text.len_utf16; + } +} + +impl TextDimension for OffsetUtf16 { + fn from_text_summary(summary: &TextSummary) -> Self { + summary.len_utf16 } fn add_assign(&mut self, other: &Self) { @@ -918,13 +1026,13 @@ impl TextDimension for Point { impl<'a> sum_tree::Dimension<'a, ChunkSummary> for PointUtf16 { fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) { - *self += summary.text.lines_utf16; + *self += summary.text.lines_utf16(); } } impl TextDimension for PointUtf16 { fn from_text_summary(summary: &TextSummary) -> Self { - summary.lines_utf16 + summary.lines_utf16() } fn add_assign(&mut self, other: &Self) { @@ -1000,6 +1108,19 @@ mod tests { rope.clip_point_utf16(PointUtf16::new(0, 3), Bias::Right), PointUtf16::new(0, 2) ); + + assert_eq!( + rope.clip_offset_utf16(OffsetUtf16(1), Bias::Left), + OffsetUtf16(0) + ); + assert_eq!( + rope.clip_offset_utf16(OffsetUtf16(1), Bias::Right), + OffsetUtf16(2) + ); + assert_eq!( + rope.clip_offset_utf16(OffsetUtf16(3), Bias::Right), + OffsetUtf16(2) + ); } #[gpui::test(iterations = 100)] @@ -1054,6 +1175,7 @@ mod tests { ); } + let mut offset_utf16 = OffsetUtf16(0); let mut point = Point::new(0, 0); let mut point_utf16 = PointUtf16::new(0, 0); for (ix, ch) in expected.char_indices().chain(Some((expected.len(), '\0'))) { @@ -1076,6 +1198,18 @@ mod tests { "point_utf16_to_offset({:?})", point_utf16 ); + assert_eq!( + actual.offset_to_offset_utf16(ix), + offset_utf16, + "offset_to_offset_utf16({:?})", + ix + ); + assert_eq!( + actual.offset_utf16_to_offset(offset_utf16), + ix, + "offset_utf16_to_offset({:?})", + offset_utf16 + ); if ch == '\n' { point += Point::new(1, 0); point_utf16 += PointUtf16::new(1, 0); @@ -1083,10 +1217,19 @@ mod tests { point.column += ch.len_utf8() as u32; point_utf16.column += ch.len_utf16() as u32; } + offset_utf16.0 += ch.len_utf16(); } + let mut offset_utf16 = OffsetUtf16(0); let mut point_utf16 = PointUtf16::zero(); for unit in expected.encode_utf16() { + let left_offset = actual.clip_offset_utf16(offset_utf16, Bias::Left); + let right_offset = actual.clip_offset_utf16(offset_utf16, Bias::Right); + assert!(right_offset >= left_offset); + // Ensure translating UTF-16 offsets to UTF-8 offsets doesn't panic. + actual.offset_utf16_to_offset(left_offset); + actual.offset_utf16_to_offset(right_offset); + let left_point = actual.clip_point_utf16(point_utf16, Bias::Left); let right_point = actual.clip_point_utf16(point_utf16, Bias::Right); assert!(right_point >= left_point); @@ -1094,6 +1237,7 @@ mod tests { actual.point_utf16_to_offset(left_point); actual.point_utf16_to_offset(right_point); + offset_utf16.0 += 1; if unit == b'\n' as u16 { point_utf16 += PointUtf16::new(1, 0); } else { diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 4da9edd735..33d02acf5a 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -247,11 +247,12 @@ fn test_text_summary_for_range() { assert_eq!( buffer.text_summary_for_range::(1..3), TextSummary { - bytes: 2, + len: 2, + len_utf16: OffsetUtf16(2), lines: Point::new(1, 0), - lines_utf16: PointUtf16::new(1, 0), first_line_chars: 1, last_line_chars: 0, + last_line_len_utf16: 0, longest_row: 0, longest_row_chars: 1, } @@ -259,11 +260,12 @@ fn test_text_summary_for_range() { assert_eq!( buffer.text_summary_for_range::(1..12), TextSummary { - bytes: 11, + len: 11, + len_utf16: OffsetUtf16(11), lines: Point::new(3, 0), - lines_utf16: PointUtf16::new(3, 0), first_line_chars: 1, last_line_chars: 0, + last_line_len_utf16: 0, longest_row: 2, longest_row_chars: 4, } @@ -271,11 +273,12 @@ fn test_text_summary_for_range() { assert_eq!( buffer.text_summary_for_range::(0..20), TextSummary { - bytes: 20, + len: 20, + len_utf16: OffsetUtf16(20), lines: Point::new(4, 1), - lines_utf16: PointUtf16::new(4, 1), first_line_chars: 2, last_line_chars: 1, + last_line_len_utf16: 1, longest_row: 3, longest_row_chars: 6, } @@ -283,11 +286,12 @@ fn test_text_summary_for_range() { assert_eq!( buffer.text_summary_for_range::(0..22), TextSummary { - bytes: 22, + len: 22, + len_utf16: OffsetUtf16(22), lines: Point::new(4, 3), - lines_utf16: PointUtf16::new(4, 3), first_line_chars: 2, last_line_chars: 3, + last_line_len_utf16: 3, longest_row: 3, longest_row_chars: 6, } @@ -295,11 +299,12 @@ fn test_text_summary_for_range() { assert_eq!( buffer.text_summary_for_range::(7..22), TextSummary { - bytes: 15, + len: 15, + len_utf16: OffsetUtf16(15), lines: Point::new(2, 3), - lines_utf16: PointUtf16::new(2, 3), first_line_chars: 4, last_line_chars: 3, + last_line_len_utf16: 3, longest_row: 1, longest_row_chars: 6, } @@ -520,7 +525,7 @@ fn test_history() { let mut now = Instant::now(); let mut buffer = Buffer::new(0, 0, "123456".into()); - buffer.start_transaction_at(now); + let transaction_1 = buffer.start_transaction_at(now).unwrap(); buffer.edit([(2..4, "cd")]); buffer.end_transaction_at(now); assert_eq!(buffer.text(), "12cd56"); @@ -559,7 +564,9 @@ fn test_history() { assert_eq!(buffer.text(), "12cde6"); // Redo stack gets cleared after performing an edit. + buffer.start_transaction_at(now); buffer.edit([(0..0, "X")]); + buffer.end_transaction_at(now); assert_eq!(buffer.text(), "X12cde6"); buffer.redo(); assert_eq!(buffer.text(), "X12cde6"); @@ -567,6 +574,16 @@ fn test_history() { assert_eq!(buffer.text(), "12cde6"); buffer.undo(); assert_eq!(buffer.text(), "123456"); + + // Transactions can be grouped manually. + buffer.redo(); + buffer.redo(); + assert_eq!(buffer.text(), "X12cde6"); + buffer.group_until_transaction(transaction_1); + buffer.undo(); + assert_eq!(buffer.text(), "123456"); + buffer.redo(); + assert_eq!(buffer.text(), "X12cde6"); } #[test] diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 536b832942..7da77b22ff 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -2,6 +2,7 @@ mod anchor; pub mod locator; #[cfg(any(test, feature = "test-support"))] pub mod network; +mod offset_utf16; pub mod operation_queue; mod patch; mod point; @@ -20,6 +21,7 @@ use clock::ReplicaId; use collections::{HashMap, HashSet}; use lazy_static::lazy_static; use locator::Locator; +pub use offset_utf16::*; use operation_queue::OperationQueue; pub use patch::Patch; pub use point::*; @@ -33,7 +35,7 @@ pub use rope::{Chunks, Rope, TextSummary}; pub use selection::*; use std::{ borrow::Cow, - cmp::{self, Ordering}, + cmp::{self, Ordering, Reverse}, future::Future, iter::Iterator, ops::{self, Deref, Range, Sub}, @@ -90,8 +92,6 @@ pub struct Transaction { pub id: TransactionId, pub edit_ids: Vec, pub start: clock::Global, - pub end: clock::Global, - pub ranges: Vec>, } #[derive(Clone, Copy, Debug, PartialEq)] @@ -104,79 +104,31 @@ impl HistoryEntry { pub fn transaction_id(&self) -> TransactionId { self.transaction.id } - - fn push_edit(&mut self, edit_operation: &EditOperation) { - self.transaction - .edit_ids - .push(edit_operation.timestamp.local()); - self.transaction - .end - .observe(edit_operation.timestamp.local()); - - let mut edits = edit_operation - .ranges - .iter() - .zip(edit_operation.new_text.iter()) - .peekable(); - let mut new_ranges = Vec::new(); - let mut delta = 0; - - for mut self_range in self.transaction.ranges.iter().cloned() { - self_range.start += delta; - self_range.end += delta; - - while let Some((other_range, new_text)) = edits.peek() { - let insertion_len = new_text.len(); - let mut other_range = (*other_range).clone(); - other_range.start += delta; - other_range.end += delta; - - if other_range.start <= self_range.end { - edits.next().unwrap(); - delta += insertion_len; - - if other_range.end < self_range.start { - new_ranges.push(other_range.start..other_range.end + insertion_len); - self_range.start += insertion_len; - self_range.end += insertion_len; - } else { - self_range.start = cmp::min(self_range.start, other_range.start); - self_range.end = cmp::max(self_range.end, other_range.end) + insertion_len; - } - } else { - break; - } - } - - new_ranges.push(self_range); - } - - for (other_range, new_text) in edits { - let insertion_len = new_text.len(); - new_ranges.push(other_range.start + delta..other_range.end + delta + insertion_len); - delta += insertion_len; - } - - self.transaction.ranges = new_ranges; - } } -#[derive(Clone)] struct History { // TODO: Turn this into a String or Rope, maybe. base_text: Arc, operations: HashMap, + insertion_slices: HashMap>, undo_stack: Vec, redo_stack: Vec, transaction_depth: usize, group_interval: Duration, } +#[derive(Clone, Debug)] +struct InsertionSlice { + insertion_id: clock::Local, + range: Range, +} + impl History { pub fn new(base_text: Arc) -> Self { Self { base_text, operations: Default::default(), + insertion_slices: Default::default(), undo_stack: Vec::new(), redo_stack: Vec::new(), transaction_depth: 0, @@ -200,10 +152,8 @@ impl History { self.undo_stack.push(HistoryEntry { transaction: Transaction { id, - start: start.clone(), - end: start, + start, edit_ids: Default::default(), - ranges: Default::default(), }, first_edit_at: now, last_edit_at: now, @@ -224,7 +174,7 @@ impl History { .last() .unwrap() .transaction - .ranges + .edit_ids .is_empty() { self.undo_stack.pop(); @@ -241,34 +191,49 @@ impl History { } fn group(&mut self) -> Option { - let mut new_len = self.undo_stack.len(); - let mut entries = self.undo_stack.iter_mut(); - + let mut count = 0; + let mut entries = self.undo_stack.iter(); if let Some(mut entry) = entries.next_back() { while let Some(prev_entry) = entries.next_back() { if !prev_entry.suppress_grouping && entry.first_edit_at - prev_entry.last_edit_at <= self.group_interval - && entry.transaction.start == prev_entry.transaction.end { entry = prev_entry; - new_len -= 1; + count += 1; } else { break; } } } + self.group_trailing(count) + } + fn group_until(&mut self, transaction_id: TransactionId) { + let mut count = 0; + for entry in self.undo_stack.iter().rev() { + if entry.transaction_id() == transaction_id { + self.group_trailing(count); + break; + } else if entry.suppress_grouping { + break; + } else { + count += 1; + } + } + } + + fn group_trailing(&mut self, n: usize) -> Option { + let new_len = self.undo_stack.len() - n; let (entries_to_keep, entries_to_merge) = self.undo_stack.split_at_mut(new_len); if let Some(last_entry) = entries_to_keep.last_mut() { for entry in &*entries_to_merge { for edit_id in &entry.transaction.edit_ids { - last_entry.push_edit(self.operations[edit_id].as_edit().unwrap()); + last_entry.transaction.edit_ids.push(*edit_id); } } if let Some(entry) = entries_to_merge.last_mut() { last_entry.last_edit_at = entry.last_edit_at; - last_entry.transaction.end = entry.transaction.end.clone(); } } @@ -296,9 +261,9 @@ impl History { fn push_undo(&mut self, op_id: clock::Local) { assert_ne!(self.transaction_depth, 0); - if let Some(Operation::Edit(edit)) = self.operations.get(&op_id) { + if let Some(Operation::Edit(_)) = self.operations.get(&op_id) { let last_transaction = self.undo_stack.last_mut().unwrap(); - last_transaction.push_edit(&edit); + last_transaction.transaction.edit_ids.push(op_id); } } @@ -547,7 +512,6 @@ pub struct EditOperation { pub struct UndoOperation { pub id: clock::Local, pub counts: HashMap, - pub transaction_ranges: Vec>, pub transaction_version: clock::Global, pub version: clock::Global, } @@ -677,6 +641,7 @@ impl Buffer { }; let mut new_insertions = Vec::new(); let mut insertion_offset = 0; + let mut insertion_slices = Vec::new(); let mut edits = edits .map(|(range, new_text)| (range.to_offset(&*self), new_text)) @@ -735,10 +700,6 @@ impl Buffer { if !new_text.is_empty() { let new_start = new_fragments.summary().text.visible; - edits_patch.push(Edit { - old: fragment_start..fragment_start, - new: new_start..new_start + new_text.len(), - }); let fragment = Fragment { id: Locator::between( &new_fragments.summary().max_id, @@ -753,6 +714,11 @@ impl Buffer { max_undos: Default::default(), visible: true, }; + edits_patch.push(Edit { + old: fragment_start..fragment_start, + new: new_start..new_start + new_text.len(), + }); + insertion_slices.push(fragment.insertion_slice()); new_insertions.push(InsertionFragment::insert_new(&fragment)); new_ropes.push_str(new_text.as_ref()); new_fragments.push(fragment, &None); @@ -781,6 +747,7 @@ impl Buffer { old: fragment_start..intersection_end, new: new_start..new_start, }); + insertion_slices.push(intersection.insertion_slice()); } new_insertions.push(InsertionFragment::insert_new(&intersection)); new_ropes.push_fragment(&intersection, fragment.visible); @@ -823,6 +790,9 @@ impl Buffer { self.snapshot.visible_text = visible_text; self.snapshot.deleted_text = deleted_text; self.subscriptions.publish_mut(&edits_patch); + self.history + .insertion_slices + .insert(timestamp.local(), insertion_slices); edit_op } @@ -892,6 +862,7 @@ impl Buffer { let edits = ranges.into_iter().zip(new_text.into_iter()); let mut edits_patch = Patch::default(); + let mut insertion_slices = Vec::new(); let cx = Some(version.clone()); let mut new_insertions = Vec::new(); let mut insertion_offset = 0; @@ -982,10 +953,6 @@ impl Buffer { old_start += fragment_start.0 - old_fragments.start().0.full_offset().0; } let new_start = new_fragments.summary().text.visible; - edits_patch.push(Edit { - old: old_start..old_start, - new: new_start..new_start + new_text.len(), - }); let fragment = Fragment { id: Locator::between( &new_fragments.summary().max_id, @@ -1000,6 +967,11 @@ impl Buffer { max_undos: Default::default(), visible: true, }; + edits_patch.push(Edit { + old: old_start..old_start, + new: new_start..new_start + new_text.len(), + }); + insertion_slices.push(fragment.insertion_slice()); new_insertions.push(InsertionFragment::insert_new(&fragment)); new_ropes.push_str(new_text); new_fragments.push(fragment, &None); @@ -1021,6 +993,7 @@ impl Buffer { Locator::between(&new_fragments.summary().max_id, &intersection.id); intersection.deletions.insert(timestamp.local()); intersection.visible = false; + insertion_slices.push(intersection.insertion_slice()); } if intersection.len > 0 { if fragment.visible && !intersection.visible { @@ -1068,90 +1041,104 @@ impl Buffer { self.snapshot.visible_text = visible_text; self.snapshot.deleted_text = deleted_text; self.snapshot.insertions.edit(new_insertions, &()); + self.history + .insertion_slices + .insert(timestamp.local(), insertion_slices); self.subscriptions.publish_mut(&edits_patch) } - fn apply_undo(&mut self, undo: &UndoOperation) -> Result<()> { - let mut edits = Patch::default(); - self.snapshot.undo_map.insert(undo); - - let mut cx = undo.transaction_version.clone(); - for edit_id in undo.counts.keys().copied() { - cx.observe(edit_id); - } - let cx = Some(cx); - - let mut old_fragments = self.fragments.cursor::<(VersionedFullOffset, usize)>(); - let mut new_fragments = old_fragments.slice( - &VersionedFullOffset::Offset(undo.transaction_ranges[0].start), - Bias::Right, - &cx, - ); - let mut new_ropes = - RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); - new_ropes.push_tree(new_fragments.summary().text); - - for range in &undo.transaction_ranges { - let mut end_offset = old_fragments.end(&cx).0.full_offset(); - - if end_offset < range.start { - let preceding_fragments = old_fragments.slice( - &VersionedFullOffset::Offset(range.start), - Bias::Right, - &cx, - ); - new_ropes.push_tree(preceding_fragments.summary().text); - new_fragments.push_tree(preceding_fragments, &None); + fn fragment_ids_for_edits<'a>( + &'a self, + edit_ids: impl Iterator, + ) -> Vec<&'a Locator> { + // Get all of the insertion slices changed by the given edits. + let mut insertion_slices = Vec::new(); + for edit_id in edit_ids { + if let Some(slices) = self.history.insertion_slices.get(edit_id) { + insertion_slices.extend_from_slice(slices) } + } + insertion_slices + .sort_unstable_by_key(|s| (s.insertion_id, s.range.start, Reverse(s.range.end))); - while end_offset <= range.end { - if let Some(fragment) = old_fragments.item() { - let mut fragment = fragment.clone(); - let fragment_was_visible = fragment.visible; - - if fragment.was_visible(&undo.transaction_version, &self.undo_map) - || undo - .counts - .contains_key(&fragment.insertion_timestamp.local()) - { - fragment.visible = fragment.is_visible(&self.undo_map); - fragment.max_undos.observe(undo.id); - } - - let old_start = old_fragments.start().1; - let new_start = new_fragments.summary().text.visible; - if fragment_was_visible && !fragment.visible { - edits.push(Edit { - old: old_start..old_start + fragment.len, - new: new_start..new_start, - }); - } else if !fragment_was_visible && fragment.visible { - edits.push(Edit { - old: old_start..old_start, - new: new_start..new_start + fragment.len, - }); - } - new_ropes.push_fragment(&fragment, fragment_was_visible); - new_fragments.push(fragment, &None); - - old_fragments.next(&cx); - if end_offset == old_fragments.end(&cx).0.full_offset() { - let unseen_fragments = old_fragments.slice( - &VersionedFullOffset::Offset(end_offset), - Bias::Right, - &cx, - ); - new_ropes.push_tree(unseen_fragments.summary().text); - new_fragments.push_tree(unseen_fragments, &None); - } - end_offset = old_fragments.end(&cx).0.full_offset(); - } else { + // Get all of the fragments corresponding to these insertion slices. + let mut fragment_ids = Vec::new(); + let mut insertions_cursor = self.insertions.cursor::(); + for insertion_slice in &insertion_slices { + if insertion_slice.insertion_id != insertions_cursor.start().timestamp + || insertion_slice.range.start > insertions_cursor.start().split_offset + { + insertions_cursor.seek_forward( + &InsertionFragmentKey { + timestamp: insertion_slice.insertion_id, + split_offset: insertion_slice.range.start, + }, + Bias::Left, + &(), + ); + } + while let Some(item) = insertions_cursor.item() { + if item.timestamp != insertion_slice.insertion_id + || item.split_offset >= insertion_slice.range.end + { break; } + fragment_ids.push(&item.fragment_id); + insertions_cursor.next(&()); + } + } + fragment_ids.sort_unstable(); + fragment_ids + } + + fn apply_undo(&mut self, undo: &UndoOperation) -> Result<()> { + self.snapshot.undo_map.insert(undo); + + let mut edits = Patch::default(); + let mut old_fragments = self.fragments.cursor::<(Option<&Locator>, usize)>(); + let mut new_fragments = SumTree::new(); + let mut new_ropes = + RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); + + for fragment_id in self.fragment_ids_for_edits(undo.counts.keys()) { + let preceding_fragments = old_fragments.slice(&Some(fragment_id), Bias::Left, &None); + new_ropes.push_tree(preceding_fragments.summary().text); + new_fragments.push_tree(preceding_fragments, &None); + + if let Some(fragment) = old_fragments.item() { + let mut fragment = fragment.clone(); + let fragment_was_visible = fragment.visible; + + if fragment.was_visible(&undo.transaction_version, &self.undo_map) + || undo + .counts + .contains_key(&fragment.insertion_timestamp.local()) + { + fragment.visible = fragment.is_visible(&self.undo_map); + fragment.max_undos.observe(undo.id); + } + + let old_start = old_fragments.start().1; + let new_start = new_fragments.summary().text.visible; + if fragment_was_visible && !fragment.visible { + edits.push(Edit { + old: old_start..old_start + fragment.len, + new: new_start..new_start, + }); + } else if !fragment_was_visible && fragment.visible { + edits.push(Edit { + old: old_start..old_start, + new: new_start..new_start + fragment.len, + }); + } + new_ropes.push_fragment(&fragment, fragment_was_visible); + new_fragments.push(fragment, &None); + + old_fragments.next(&None); } } - let suffix = old_fragments.suffix(&cx); + let suffix = old_fragments.suffix(&None); new_ropes.push_tree(suffix.summary().text); new_fragments.push_tree(suffix, &None); @@ -1225,6 +1212,10 @@ impl Buffer { self.history.finalize_last_transaction() } + pub fn group_until_transaction(&mut self, transaction_id: TransactionId) { + self.history.group_until(transaction_id); + } + pub fn base_text(&self) -> &Arc { &self.history.base_text } @@ -1302,7 +1293,6 @@ impl Buffer { id: self.local_clock.tick(), version: self.version(), counts, - transaction_ranges: transaction.ranges, transaction_version: transaction.start.clone(), }; self.apply_undo(&undo)?; @@ -1320,6 +1310,55 @@ impl Buffer { self.history.finalize_last_transaction(); } + pub fn edited_ranges_for_transaction<'a, D>( + &'a self, + transaction: &'a Transaction, + ) -> impl 'a + Iterator> + where + D: TextDimension, + { + // get fragment ranges + let mut cursor = self.fragments.cursor::<(Option<&Locator>, usize)>(); + let offset_ranges = self + .fragment_ids_for_edits(transaction.edit_ids.iter()) + .into_iter() + .filter_map(move |fragment_id| { + cursor.seek_forward(&Some(fragment_id), Bias::Left, &None); + let fragment = cursor.item()?; + let start_offset = cursor.start().1; + let end_offset = start_offset + if fragment.visible { fragment.len } else { 0 }; + Some(start_offset..end_offset) + }); + + // combine adjacent ranges + let mut prev_range: Option> = None; + let disjoint_ranges = offset_ranges + .map(Some) + .chain([None]) + .filter_map(move |range| { + if let Some((range, prev_range)) = range.as_ref().zip(prev_range.as_mut()) { + if prev_range.end == range.start { + prev_range.end = range.end; + return None; + } + } + let result = prev_range.clone(); + prev_range = range; + result + }); + + // convert to the desired text dimension. + let mut position = D::default(); + let mut rope_cursor = self.visible_text.cursor(0); + disjoint_ranges.map(move |range| { + position.add_assign(&rope_cursor.summary(range.start)); + let start = position.clone(); + position.add_assign(&rope_cursor.summary(range.end)); + let end = position.clone(); + start..end + }) + } + pub fn subscribe(&mut self) -> Subscription { self.subscriptions.subscribe() } @@ -1621,6 +1660,14 @@ impl BufferSnapshot { self.visible_text.point_utf16_to_point(point) } + pub fn offset_utf16_to_offset(&self, offset: OffsetUtf16) -> usize { + self.visible_text.offset_utf16_to_offset(offset) + } + + pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { + self.visible_text.offset_to_offset_utf16(offset) + } + pub fn offset_to_point(&self, offset: usize) -> Point { self.visible_text.offset_to_point(offset) } @@ -1854,6 +1901,10 @@ impl BufferSnapshot { self.visible_text.clip_point(point, bias) } + pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 { + self.visible_text.clip_offset_utf16(offset, bias) + } + pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 { self.visible_text.clip_point_utf16(point, bias) } @@ -1868,42 +1919,6 @@ impl BufferSnapshot { self.edits_since_in_range(since, Anchor::MIN..Anchor::MAX) } - pub fn edited_ranges_for_transaction<'a, D>( - &'a self, - transaction: &'a Transaction, - ) -> impl 'a + Iterator> - where - D: TextDimension, - { - let mut cursor = self.fragments.cursor::<(VersionedFullOffset, usize)>(); - let mut rope_cursor = self.visible_text.cursor(0); - let cx = Some(transaction.end.clone()); - let mut position = D::default(); - transaction.ranges.iter().map(move |range| { - cursor.seek_forward(&VersionedFullOffset::Offset(range.start), Bias::Right, &cx); - let mut start_offset = cursor.start().1; - if cursor - .item() - .map_or(false, |fragment| fragment.is_visible(&self.undo_map)) - { - start_offset += range.start - cursor.start().0.full_offset() - } - position.add_assign(&rope_cursor.summary(start_offset)); - let start = position.clone(); - - cursor.seek_forward(&VersionedFullOffset::Offset(range.end), Bias::Left, &cx); - let mut end_offset = cursor.start().1; - if cursor - .item() - .map_or(false, |fragment| fragment.is_visible(&self.undo_map)) - { - end_offset += range.end - cursor.start().0.full_offset(); - } - position.add_assign(&rope_cursor.summary(end_offset)); - start..position.clone() - }) - } - pub fn edits_since_in_range<'a, D>( &'a self, since: &'a clock::Global, @@ -2090,6 +2105,13 @@ impl<'a, D: TextDimension + Ord, F: FnMut(&FragmentSummary) -> bool> Iterator fo } impl Fragment { + fn insertion_slice(&self) -> InsertionSlice { + InsertionSlice { + insertion_id: self.insertion_timestamp.local(), + range: self.insertion_offset..self.insertion_offset + self.len, + } + } + fn is_visible(&self, undos: &UndoMap) -> bool { !undos.is_undone(self.insertion_timestamp.local()) && self.deletions.iter().all(|d| undos.is_undone(*d)) @@ -2423,6 +2445,12 @@ impl ToOffset for usize { } } +impl ToOffset for OffsetUtf16 { + fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize { + snapshot.offset_utf16_to_offset(*self) + } +} + impl ToOffset for Anchor { fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize { snapshot.summary_for_anchor(self) @@ -2491,6 +2519,28 @@ impl ToPointUtf16 for Point { } } +pub trait ToOffsetUtf16 { + fn to_offset_utf16<'a>(&self, snapshot: &BufferSnapshot) -> OffsetUtf16; +} + +impl ToOffsetUtf16 for Anchor { + fn to_offset_utf16<'a>(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 { + snapshot.summary_for_anchor(self) + } +} + +impl ToOffsetUtf16 for usize { + fn to_offset_utf16<'a>(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 { + snapshot.offset_to_offset_utf16(*self) + } +} + +impl ToOffsetUtf16 for OffsetUtf16 { + fn to_offset_utf16<'a>(&self, _snapshot: &BufferSnapshot) -> OffsetUtf16 { + *self + } +} + pub trait Clip { fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self; } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index d1c72b46a3..f7c470bb96 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -452,6 +452,7 @@ pub struct Editor { pub unnecessary_code_fade: f32, pub hover_popover: HoverPopover, pub link_definition: HighlightStyle, + pub composition_mark: HighlightStyle, pub jump_icon: Interactive, } diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index 7b777a50ed..68f36e8fc6 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -22,9 +22,20 @@ fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppCont vim.active_editor = Some(editor.downgrade()); vim.selection_subscription = Some(cx.subscribe(editor, |editor, event, cx| { if editor.read(cx).leader_replica_id().is_none() { - if let editor::Event::SelectionsChanged { local: true } = event { - let newest_empty = editor.read(cx).selections.newest::(cx).is_empty(); - editor_local_selections_changed(newest_empty, cx); + match event { + editor::Event::SelectionsChanged { local: true } => { + let newest_empty = + editor.read(cx).selections.newest::(cx).is_empty(); + editor_local_selections_changed(newest_empty, cx); + } + editor::Event::IgnoredInput => { + Vim::update(cx, |vim, cx| { + if vim.active_operator().is_some() { + vim.clear_operator(cx); + } + }); + } + _ => (), } } })); diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 56e2e599d8..f5764ba09b 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -11,7 +11,7 @@ mod visual; use collections::HashMap; use command_palette::CommandPaletteFilter; -use editor::{Bias, Cancel, CursorShape, Editor, Input}; +use editor::{Bias, Cancel, CursorShape, Editor}; use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle}; use serde::Deserialize; @@ -45,16 +45,6 @@ pub fn init(cx: &mut MutableAppContext) { ); // Editor Actions - cx.add_action(|_: &mut Editor, _: &Input, cx| { - // If we have an unbound input with an active operator, cancel that operator. Otherwise forward - // the input to the editor - if Vim::read(cx).active_operator().is_some() { - // Defer without updating editor - MutableAppContext::defer(cx, |cx| Vim::update(cx, |vim, cx| vim.clear_operator(cx))) - } else { - cx.propagate_action() - } - }); cx.add_action(|_: &mut Editor, _: &Cancel, cx| { // If we are in a non normal mode or have an active operator, swap to normal mode // Otherwise forward cancel on to the editor diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 9b19ec2a10..0321e770cb 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -43,6 +43,7 @@ use std::{ fmt, future::Future, mem, + ops::Range, path::{Path, PathBuf}, rc::Rc, sync::{ @@ -2546,6 +2547,18 @@ impl Element for AvatarRibbon { false } + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &gpui::MeasurementContext, + ) -> Option { + None + } + fn debug( &self, bounds: gpui::geometry::rect::RectF, diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index 5b95c2b7eb..e8297a1727 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -131,6 +131,10 @@ pub fn menus() -> Vec> { name: "Toggle Line Comment", action: Box::new(editor::ToggleComments), }, + MenuItem::Action { + name: "Emoji & Symbols", + action: Box::new(editor::ShowCharacterPalette), + }, ], }, Menu { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 91dab1f368..867913fc7b 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -920,11 +920,7 @@ mod tests { item.downcast::().unwrap() }); - cx.update(|cx| { - editor.update(cx, |editor, cx| { - editor.handle_input(&editor::Input("x".into()), cx) - }) - }); + cx.update(|cx| editor.update(cx, |editor, cx| editor.handle_input("x", cx))); app_state .fs .as_fake() @@ -971,7 +967,7 @@ mod tests { editor.language_at(0, cx).unwrap(), &languages::PLAIN_TEXT )); - editor.handle_input(&editor::Input("hi".into()), cx); + editor.handle_input("hi", cx); assert!(editor.is_dirty(cx)); }); @@ -997,7 +993,7 @@ mod tests { // Edit the file and save it again. This time, there is no filename prompt. editor.update(cx, |editor, cx| { - editor.handle_input(&editor::Input(" there".into()), cx); + editor.handle_input(" there", cx); assert_eq!(editor.is_dirty(cx.as_ref()), true); }); let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx)); @@ -1057,7 +1053,7 @@ mod tests { editor.language_at(0, cx).unwrap(), &languages::PLAIN_TEXT )); - editor.handle_input(&editor::Input("hi".into()), cx); + editor.handle_input("hi", cx); assert!(editor.is_dirty(cx.as_ref())); }); diff --git a/styles/package-lock.json b/styles/package-lock.json index 5499f1852c..582f1c8496 100644 --- a/styles/package-lock.json +++ b/styles/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "styles", "version": "1.0.0", "license": "ISC", "dependencies": { diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 7e2c512a7c..507eb74d8d 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -2,6 +2,7 @@ import Theme from "../themes/common/theme"; import { backgroundColor, border, + borderColor, iconColor, player, popoverShadow, @@ -138,8 +139,8 @@ export default function editor(theme: Theme) { invalidHintDiagnostic: diagnostic(theme, "muted"), invalidInformationDiagnostic: diagnostic(theme, "muted"), invalidWarningDiagnostic: diagnostic(theme, "muted"), - hover_popover: hoverPopover(theme), - link_definition: { + hoverPopover: hoverPopover(theme), + linkDefinition: { color: theme.syntax.linkUri.color, underline: theme.syntax.linkUri.underline, }, @@ -159,6 +160,12 @@ export default function editor(theme: Theme) { background: backgroundColor(theme, "on500", "base"), }, }, + compositionMark: { + underline: { + thickness: 1.0, + color: borderColor(theme, "active") + }, + }, syntax, }; }