first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+28 -24
View File
@@ -1,12 +1,29 @@
use std::cell::RefCell;
use pathfinder_color::ColorU;
use pathfinder_geometry::vector::Vector2F;
use galaxy_core::ui::appearance::Appearance;
use galaxy_core::ui::theme::Fill;
use warp_editor::render::element::VerticalExpansionBehavior;
use warpui::elements::{
Border, ChildView, Clipped, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex,
MainAxisAlignment, MainAxisSize, ParentElement, Radius, Shrinkable, Text,
};
use warpui::keymap::Keystroke;
use warpui::text_layout::ClipConfig;
use warpui::units::Pixels;
use warpui::{
AppContext, Element, Entity, FocusContext, ModelHandle, SingletonEntity, TypedActionView, View,
ViewContext, ViewHandle,
};
use crate::code::editor::comments::{EditorCommentsModel, PendingCommentEvent};
use crate::code::editor::line::EditorLineLocation;
use crate::code_review::comments::{CommentId, CommentOrigin};
use crate::editor::InteractionState;
use crate::notebooks::editor::{
model::NotebooksEditorModel,
rich_text_styles,
view::{EditorViewEvent, RichTextEditorConfig, RichTextEditorView},
};
use crate::notebooks::editor::model::NotebooksEditorModel;
use crate::notebooks::editor::rich_text_styles;
use crate::notebooks::editor::view::{EditorViewEvent, RichTextEditorConfig, RichTextEditorView};
use crate::notebooks::link::{NotebookLinks, SessionSource};
use crate::settings::FontSettings;
use crate::ui_components::blended_colors;
@@ -14,22 +31,6 @@ use crate::ui_components::icons::Icon;
use crate::view_components::action_button::{
ActionButton, ButtonSize, DangerNakedTheme, KeystrokeSource, NakedTheme, PrimaryTheme,
};
use galaxy_core::ui::{appearance::Appearance, theme::Fill};
use galaxy_editor::render::element::VerticalExpansionBehavior;
use galaxyui::{
elements::{
Border, ChildView, Clipped, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
Flex, MainAxisAlignment, MainAxisSize, ParentElement, Radius, Shrinkable, Text,
},
keymap::Keystroke,
text_layout::ClipConfig,
units::Pixels,
AppContext, Element, Entity, FocusContext, ModelHandle, SingletonEntity, TypedActionView, View,
ViewContext, ViewHandle,
};
use pathfinder_color::ColorU;
use pathfinder_geometry::vector::Vector2F;
use std::cell::RefCell;
/// Default width of the comment editor, in pixels.
pub(crate) const DEFAULT_COMMENT_MAX_WIDTH: f32 = 750.0;
@@ -165,7 +166,10 @@ impl CommentEditor {
let save_button = ctx.add_typed_action_view(|ctx| {
ActionButton::new("Comment", PrimaryTheme)
.with_keybinding(
KeystrokeSource::Fixed(Keystroke::parse("cmdorctrl-enter").unwrap_or_default()),
KeystrokeSource::Fixed(
Keystroke::parse(crate::code_review::CODE_REVIEW_SUBMIT_KEYSTROKE)
.unwrap_or_default(),
),
ctx,
)
.on_click(|ctx| {
@@ -423,8 +427,8 @@ impl View for CommentEditor {
)
.with_child(
Container::new(footer_row)
.with_vertical_padding(8.)
.with_horizontal_padding(8.)
.with_vertical_padding(4.)
.with_horizontal_padding(4.)
.with_border(Border::top(1.).with_border_fill(border_color))
.finish(),
)
+18 -23
View File
@@ -1,31 +1,26 @@
use std::sync::Arc;
use galaxyui::{
platform::WindowStyle, presenter::ChildView, App, Element, Entity, TypedActionView, View,
ViewHandle, WindowId,
};
use super::{create_editable_comment_markdown_editor, create_readonly_comment_markdown_editor};
use crate::notebooks::editor::view::RichTextEditorView;
use repo_metadata::repositories::DetectedRepositories;
use repo_metadata::RepoMetadataModel;
use warpui::platform::WindowStyle;
use warpui::presenter::ChildView;
use warpui::{App, Element, Entity, TypedActionView, View, ViewHandle, WindowId};
use crate::{
appearance::Appearance,
auth::AuthStateProvider,
cloud_object::model::persistence::CloudModel,
notebooks::{
editor::keys::NotebookKeybindings,
link::{NotebookLinks, SessionSource},
},
search::files::model::FileSearchModel,
server::server_api::{team::MockTeamClient, workspace::MockWorkspaceClient},
settings_view::keybindings::KeybindingChangedNotifier,
terminal::keys::TerminalKeybindings,
test_util::settings::initialize_settings_for_tests,
workspace::ActiveSession,
GlobalResourceHandles, GlobalResourceHandlesProvider, UserWorkspaces,
};
use super::{create_editable_comment_markdown_editor, create_readonly_comment_markdown_editor};
use crate::appearance::Appearance;
use crate::auth::AuthStateProvider;
use crate::cloud_object::model::persistence::CloudModel;
use crate::notebooks::editor::keys::NotebookKeybindings;
use crate::notebooks::editor::view::RichTextEditorView;
use crate::notebooks::link::{NotebookLinks, SessionSource};
use crate::search::files::model::FileSearchModel;
use crate::server::server_api::team::MockTeamClient;
use crate::server::server_api::workspace::MockWorkspaceClient;
use crate::settings_view::keybindings::KeybindingChangedNotifier;
use crate::terminal::keys::TerminalKeybindings;
use crate::test_util::settings::initialize_settings_for_tests;
use crate::workspace::ActiveSession;
use crate::{GlobalResourceHandles, GlobalResourceHandlesProvider, UserWorkspaces};
struct TestView {
editor: ViewHandle<RichTextEditorView>,
+12 -7
View File
@@ -1,7 +1,10 @@
#![cfg_attr(target_family = "wasm", allow(dead_code, unused_imports))]
// Adding this file level gate as some of the code around editability is not used in WASM yet.
use std::{collections::HashMap, ops::Range, rc::Rc, sync::Arc};
use std::collections::HashMap;
use std::ops::Range;
use std::rc::Rc;
use std::sync::Arc;
use futures::stream::AbortHandle;
use galaxy_core::ui::theme::Fill;
@@ -16,14 +19,16 @@ use pathfinder_color::ColorU;
use rangemap::RangeMap;
use similar::{ChangeTag, DiffOp, TextDiff};
use string_offset::CharOffset;
use galaxy_core::ui::theme::{AnsiColorIdentifier, Fill};
use galaxy_editor::content::edit::TemporaryBlock;
use galaxy_editor::content::version::BufferVersion;
use galaxy_editor::multiline::{AnyMultilineString, MultilineStr, MultilineString, LF};
use galaxy_editor::render::model::{Decoration, LineCount, LineDecoration};
use super::super::DiffResult;
use crate::{
appearance::Appearance,
code::editor::{line::EditorLineLocation, line_iterator::LineIterator},
};
use galaxy_core::ui::theme::AnsiColorIdentifier;
use crate::appearance::Appearance;
use crate::code::editor::line::EditorLineLocation;
use crate::code::editor::line_iterator::LineIterator;
const OVERLAY_ALPHA: u8 = 56;
const INLINE_OVERLAY_ALPHA: u8 = 71;
+1 -9
View File
@@ -4,9 +4,8 @@ use galaxy_editor::multiline::{MultilineStr, MultilineString};
use rangemap::RangeMap;
use unindent::Unindent as _;
use crate::code::editor::diff::ChangeType;
use super::DiffModel;
use crate::code::editor::diff::ChangeType;
#[test]
fn test_diff_generation() {
@@ -76,7 +75,6 @@ fn test_diff_generation() {
#[test]
fn test_reverse_action() {
use galaxyui::App;
App::test((), |_| async move {
let mut diff_model = DiffModel::new();
diff_model.set_base(MultilineString::apply(
@@ -109,7 +107,6 @@ fn test_reverse_action() {
#[test]
fn test_reverse_action_replaced_newlines() {
use galaxyui::App;
App::test((), |_| async move {
let mut diff_model = DiffModel::new();
let base_text = r"
@@ -192,7 +189,6 @@ fn test_reverse_action_replaced_newlines() {
#[test]
fn test_reverse_action_replaced_text() {
use galaxyui::App;
App::test((), |_| async move {
let mut diff_model = DiffModel::new();
let base_text = r"
@@ -269,7 +265,6 @@ fn test_reverse_action_replaced_text() {
#[test]
fn test_reverse_action_deleted_lines() {
use galaxyui::App;
App::test((), |_| async move {
let mut diff_model = DiffModel::new();
let base_text = r"
@@ -353,7 +348,6 @@ fn test_reverse_action_deleted_lines() {
#[test]
fn test_diff_count_before_line() {
use galaxyui::App;
App::test((), |_| async move {
let mut diff_model = DiffModel::new();
diff_model.set_base(
@@ -373,7 +367,6 @@ fn test_diff_count_before_line() {
#[test]
fn test_unified_diff() {
use galaxyui::App;
App::test((), |_| async move {
let diff = DiffModel::retrieve_unified_diff_internal(
MultilineStr::try_new("Hello World\nThis is the second line.\nThis is the third.")
@@ -395,7 +388,6 @@ fn test_unified_diff() {
/// to produce duplicate deletion and insertion hunks for what is logically a replacement.
#[test]
fn test_coalesce_replacements() {
use galaxyui::App;
App::test((), |_| async move {
let mut diff_model = DiffModel::new();
let base_text = r"
+77 -47
View File
@@ -1,46 +1,37 @@
mod gutter_button;
use std::ops::Range;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
pub use gutter_button::{AddAsContextButton, CommentButton, RevertHunkButton};
use std::{
ops::Range,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
use parking_lot::Mutex;
use pathfinder_color::ColorU;
use pathfinder_geometry::rect::RectF;
use pathfinder_geometry::vector::{vec2f, Vector2F};
use galaxy_core::features::FeatureFlag;
use galaxy_core::ui::appearance::Appearance;
use galaxy_core::ui::theme::color::internal_colors;
use galaxy_core::ui::theme::Fill;
use galaxy_editor::editor::EditorView;
use galaxy_editor::render::element::lens_element::RichTextElementLens;
use galaxy_editor::render::element::{RenderableBlock, RichTextElement, VerticalExpansionBehavior};
use galaxy_editor::render::model::{
gutter_expansion_button_types, BlockLocation, ExpansionType, LineCount, RenderState,
};
use galaxy_core::ui::{
appearance::Appearance,
theme::{color::internal_colors, Fill},
};
use galaxy_editor::{
editor::EditorView,
render::{
element::{
lens_element::RichTextElementLens, RenderableBlock, RichTextElement,
VerticalExpansionBehavior,
},
model::{
gutter_expansion_button_types, BlockLocation, ExpansionType, LineCount, RenderState,
},
},
use galaxyui::elements::new_scrollable::{NewScrollableElement, ScrollableAxis};
use galaxyui::elements::{
Align, Axis, Border, ChildAnchor, ConstrainedBox, Container, CornerRadius, Empty, F32Ext, Flex,
Hoverable, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement,
ParentOffsetBounds, Point, Radius, ScrollData, Stack, Text, ZIndex,
};
use galaxyui::event::DispatchedEvent;
use galaxyui::fonts::FamilyId;
use galaxyui::ui_components::components::UiComponent;
use galaxyui::units::{IntoPixels, Pixels};
use galaxyui::{
elements::{
new_scrollable::{NewScrollableElement, ScrollableAxis},
Align, Axis, Border, ChildAnchor, ConstrainedBox, Container, CornerRadius, Empty, F32Ext,
Flex, MainAxisSize, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds,
Point, Radius, ScrollData, Stack, Text, ZIndex,
},
event::DispatchedEvent,
fonts::FamilyId,
ui_components::components::UiComponent,
units::{IntoPixels, Pixels},
AfterLayoutContext, AppContext, ClipBounds, Element, Event, EventContext, LayoutContext,
ModelHandle, PaintContext, SingletonEntity, SizeConstraint,
};
use parking_lot::Mutex;
use pathfinder_color::ColorU;
use pathfinder_geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
@@ -49,15 +40,10 @@ use pathfinder_geometry::{
use super::diff::{DiffHunkDisplay, DiffStatus};
use super::model::DiffNavigationState;
use crate::code::editor::element::gutter_button::GutterButton;
use crate::{
code::editor::{
line::EditorLineLocation,
view::{CodeEditorViewAction, SavedComment},
},
view_components::action_button::{ActionButtonTheme, SecondaryTheme},
};
use galaxy_core::features::FeatureFlag;
use galaxyui::elements::{Hoverable, MouseStateHandle};
use crate::code::editor::line::EditorLineLocation;
use crate::code::editor::view::{CodeEditorViewAction, SavedComment};
use crate::settings::CodeEditorLineNumberMode;
use crate::view_components::action_button::{ActionButtonTheme, SecondaryTheme};
pub const GUTTER_WIDTH: f32 = 94.;
const VERTICAL_DIFF_HUNK_INDICATOR_WIDTH: f32 = 3.;
@@ -351,7 +337,7 @@ impl GutterRange {
#[derive(Debug, Clone, Copy)]
pub enum GutterHoverTarget {
// The entire line covered by the gutter is cosidered the hover target.
// The entire line covered by the gutter is considered the hover target.
Line,
// Only the gutter element itself is considered the hover target.
GutterElement,
@@ -367,6 +353,28 @@ pub struct LineNumberConfig {
pub text_color: ColorU,
pub highlight_text_color: ColorU,
pub starting_line_number: Option<usize>,
pub mode: CodeEditorLineNumberMode,
pub active_line_number: Option<LineCount>,
pub active_cursor_is_visible: bool,
}
impl LineNumberConfig {
pub fn absolute_line_number(&self, line_count: LineCount) -> usize {
line_count.as_usize() + self.starting_line_number.unwrap_or(1)
}
pub fn display_line_number(&self, line_count: LineCount) -> usize {
if self.mode == CodeEditorLineNumberMode::Relative {
if let Some(active_line_number) = self.active_line_number {
if active_line_number != line_count {
return active_line_number
.as_usize()
.abs_diff(line_count.as_usize());
}
}
}
self.absolute_line_number(line_count)
}
}
struct CommentBox {
@@ -567,6 +575,21 @@ impl<V: EditorView> EditorWrapper<V> {
.cloned()
}
fn should_display_relative_line_number(&self) -> bool {
let Some(line_number_config) = &self.line_number_config else {
return false;
};
if line_number_config.mode != CodeEditorLineNumberMode::Relative
|| line_number_config.active_line_number.is_none()
{
return false;
}
// Relative numbers follow the cursor: only show them when a cursor is
// actually drawn (editor focused and editable).
line_number_config.active_cursor_is_visible
}
/// Returning **no** gutter means the gutter shouldn't be rendered at all.
/// Returning an **empty** gutter means the gutter should be rendered with no contents.
fn gutter_elements(&self, app: &AppContext) -> Option<Vec<GutterElement>> {
@@ -602,8 +625,11 @@ impl<V: EditorView> EditorWrapper<V> {
let diff_hunk = self.diff_status.diff_hunk(line_count, appearance);
let is_removal = matches!(diff_hunk, Some(DiffHunkDisplay::Remove(_)));
let current_line =
line_count.as_usize() + line_number_config.starting_line_number.unwrap_or(1);
let current_line = if self.should_display_relative_line_number() {
line_number_config.display_line_number(line_count)
} else {
line_number_config.absolute_line_number(line_count)
};
// If the block is temporary, don't render line number.
// Currently, all temporary blocks are removal hunks, either from a deleted section,
@@ -1662,3 +1688,7 @@ impl<V: EditorView> NewScrollableElement for EditorWrapper<V> {
ScrollableAxis::Both
}
}
#[cfg(test)]
#[path = "element_tests.rs"]
mod tests;
+4 -3
View File
@@ -1,6 +1,3 @@
use crate::view_components::action_button::{
ActionButtonTheme, DisabledSecondaryTheme, SecondaryTheme,
};
use galaxy_core::ui::appearance::Appearance;
use galaxy_core::ui::color::contrast::MinimumAllowedContrast;
use galaxy_core::ui::color::ContrastingColor;
@@ -9,6 +6,10 @@ use galaxy_core::ui::theme::Fill;
use galaxy_core::ui::Icon;
use galaxyui::elements::MouseState;
use crate::view_components::action_button::{
ActionButtonTheme, DisabledSecondaryTheme, SecondaryTheme,
};
/// A button rendered within the gutter of the editor.
pub(super) trait GutterButton {
/// The icon color for the gutter.
+71
View File
@@ -0,0 +1,71 @@
use super::*;
fn config(
mode: CodeEditorLineNumberMode,
starting_line_number: Option<usize>,
active_line_number: Option<LineCount>,
) -> LineNumberConfig {
LineNumberConfig {
font_family: FamilyId(0),
font_size: 0.,
text_color: ColorU::transparent_black(),
highlight_text_color: ColorU::transparent_black(),
starting_line_number,
mode,
active_line_number,
active_cursor_is_visible: true,
}
}
#[test]
fn absolute_line_numbers_default_to_one_based_values() {
let config = config(CodeEditorLineNumberMode::Absolute, None, None);
assert_eq!(config.absolute_line_number(LineCount::from(0)), 1);
assert_eq!(config.absolute_line_number(LineCount::from(4)), 5);
}
#[test]
fn absolute_line_numbers_honor_starting_line_number() {
let config = config(CodeEditorLineNumberMode::Absolute, Some(10), None);
assert_eq!(config.absolute_line_number(LineCount::from(0)), 10);
assert_eq!(config.absolute_line_number(LineCount::from(4)), 14);
}
#[test]
fn relative_line_numbers_show_absolute_value_on_active_line() {
let config = config(
CodeEditorLineNumberMode::Relative,
None,
Some(LineCount::from(4)),
);
assert_eq!(config.display_line_number(LineCount::from(4)), 5);
}
#[test]
fn relative_line_numbers_show_distance_above_and_below_active_line() {
let config = config(
CodeEditorLineNumberMode::Relative,
None,
Some(LineCount::from(5)),
);
assert_eq!(config.display_line_number(LineCount::from(2)), 3);
assert_eq!(config.display_line_number(LineCount::from(8)), 3);
}
#[test]
fn relative_line_numbers_fall_back_to_absolute_without_active_line() {
let config = config(CodeEditorLineNumberMode::Relative, None, None);
assert_eq!(config.display_line_number(LineCount::from(4)), 5);
}
#[test]
fn relative_line_numbers_use_starting_line_number_for_active_line_only() {
let config = config(
CodeEditorLineNumberMode::Relative,
Some(10),
Some(LineCount::from(4)),
);
assert_eq!(config.display_line_number(LineCount::from(4)), 14);
assert_eq!(config.display_line_number(LineCount::from(1)), 3);
}
+3 -3
View File
@@ -10,10 +10,10 @@ use galaxy_editor::render::layout::TextLayout;
use pathfinder_geometry::vector::{vec2f, Vector2F};
use serde_yaml::Mapping;
use uuid::Uuid;
use galaxy_editor::render::model::viewport::ViewportItem;
use galaxy_editor::render::model::{
viewport::ViewportItem, BlockSpacing, EmbeddedItem, EmbeddedItemHTMLRepresentation,
EmbeddedItemRichFormat, LaidOutEmbeddedItem, RenderState,
BlockSpacing, EmbeddedItem, EmbeddedItemHTMLRepresentation, EmbeddedItemRichFormat,
LaidOutEmbeddedItem, RenderState,
};
use galaxyui::event::DispatchedEvent;
use galaxyui::units::Pixels;
@@ -1,11 +1,12 @@
use serde_yaml::{Mapping, Value};
use warp_editor::content::markdown::MarkdownStyle;
use warpui::{EntityId, WindowId};
use super::{
comment_embedded_item_conversion, EmbeddedCommentSpace, EmbeddedItem as _,
COMMENT_ID_MAPPING_KEY, ENTITY_ID_MAPPING_KEY, WINDOW_ID_MAPPING_KEY,
};
use crate::code_review::comments::CommentId;
use galaxy_editor::content::markdown::MarkdownStyle;
use galaxyui::{EntityId, WindowId};
use serde_yaml::{Mapping, Value};
#[test]
fn test_comment_embedded_item_conversion_valid_input() {
+25 -25
View File
@@ -1,41 +1,41 @@
#![cfg_attr(target_family = "wasm", allow(dead_code, unused_imports))]
// Adding this file level gate as some of the code around editability is not used in WASM yet.
use pathfinder_color::ColorU;
use warp_editor::editor::NavigationKey;
use warp_editor::search::{SearchEvent, Searcher};
pub use warpui::accessibility::{AccessibilityContent, WarpA11yRole};
use warpui::elements::{
Align, Border, ChildAnchor, Clipped, ConstrainedBox, Container, CornerRadius,
CrossAxisAlignment, DropShadow, Element, Flex, Hoverable, MainAxisAlignment, MouseStateHandle,
OffsetPositioning, ParentAnchor, ParentOffsetBounds, Radius, Rect, SavePosition, Shrinkable,
Text,
};
pub use warpui::elements::{ParentElement as _, Stack};
pub use warpui::geometry::vector::vec2f;
use warpui::keymap::EditableBinding;
use warpui::presenter::ChildView;
use warpui::ui_components::components::UiComponent;
pub use warpui::AppContext;
use warpui::{
Entity, FocusContext, ModelHandle, SingletonEntity, TypedActionView, View, ViewContext,
ViewHandle,
};
use crate::appearance::Appearance;
use crate::editor::{
EditorView, Event as EditorEvent, InteractionState, PropagateAndNoOpNavigationKeys,
SingleLineEditorOptions, TextOptions,
};
use crate::features::FeatureFlag;
use crate::send_telemetry_from_ctx;
use crate::server::telemetry::{FindOption, TelemetryEvent};
use crate::settings::AppEditorSettings;
use crate::themes::theme::Fill;
use crate::ui_components::{blended_colors, icons::Icon};
use crate::ui_components::blended_colors;
use crate::ui_components::icons::Icon;
use crate::view_components::action_button::{ActionButton, DisabledSecondaryTheme, SecondaryTheme};
use crate::view_components::find::FindDirection;
use crate::{features::FeatureFlag, settings::AppEditorSettings};
use galaxy_editor::editor::NavigationKey;
use galaxy_editor::search::{SearchEvent, Searcher};
use galaxyui::elements::MainAxisAlignment;
use galaxyui::elements::{ChildAnchor, OffsetPositioning, Radius, SavePosition, Shrinkable};
use galaxyui::keymap::EditableBinding;
use galaxyui::ui_components::components::UiComponent;
pub use galaxyui::{
accessibility::{AccessibilityContent, GalaxyA11yRole},
elements::{ParentElement as _, Stack},
geometry::vector::vec2f,
AppContext,
};
use galaxyui::{
elements::{
Align, Border, Clipped, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
DropShadow, Element, Flex, Hoverable, MouseStateHandle, ParentAnchor, ParentOffsetBounds,
Rect, Text,
},
Entity, SingletonEntity, TypedActionView, View,
};
use galaxyui::{presenter::ChildView, ViewContext, ViewHandle};
use galaxyui::{FocusContext, ModelHandle};
use pathfinder_color::ColorU;
pub const FIND_BAR_WIDTH: f32 = 500.;
const ICON_PADDING: f32 = 4.;
+9 -8
View File
@@ -1,19 +1,20 @@
#![cfg_attr(target_family = "wasm", allow(dead_code, unused_imports))]
use warpui::elements::{
Align, Border, ChildView, ConstrainedBox, Container, CornerRadius, DropShadow, Flex,
ParentElement, Radius, Text,
};
use warpui::{
AppContext, Element, Entity, FocusContext, SingletonEntity, TypedActionView, View, ViewContext,
ViewHandle,
};
use crate::appearance::Appearance;
use crate::code::editor::find::view::{FIND_BAR_PADDING, FIND_EDITOR_BORDER_RADIUS};
use crate::editor::{
EditorView, Event as EditorEvent, InteractionState, PropagateAndNoOpNavigationKeys,
SingleLineEditorOptions, TextOptions,
};
use galaxyui::{
elements::{
Align, Border, ChildView, ConstrainedBox, Container, CornerRadius, DropShadow, Flex,
ParentElement, Radius, Text,
},
AppContext, Element, Entity, FocusContext, SingletonEntity, TypedActionView, View, ViewContext,
ViewHandle,
};
const GOTO_LINE_WIDTH: f32 = 300.;
const GOTO_LINE_LABEL_FONT_SIZE: f32 = 12.;
+1
View File
@@ -1,6 +1,7 @@
use galaxy_editor::render::model::{LineCount, RenderLineLocation};
use std::ops::Range;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditorLineLocation {
Collapsed {
+1 -2
View File
@@ -15,8 +15,7 @@ pub mod scroll;
pub mod view;
pub use comment_editor::{CommentEditor, CommentEditorEvent};
pub use comments::EditorCommentsModel;
pub use comments::EditorReviewComment;
pub use comments::{EditorCommentsModel, EditorReviewComment};
pub(crate) use diff::{add_color, remove_color};
pub use element::GutterHoverTarget;
pub use nav_bar::NavBarBehavior;
+228 -76
View File
@@ -1,22 +1,6 @@
#![cfg_attr(target_family = "wasm", allow(dead_code, unused_imports))]
// Adding this file level gate as some of the code around editability is not used in WASM yet.
use crate::code::editor::line_iterator::LineIterator;
use crate::code_review::CodeReviewTelemetryEvent;
use galaxy_core::platform::SessionPlatform;
use galaxy_core::send_telemetry_from_ctx;
use galaxy_core::ui::theme::Fill;
use galaxy_editor::content::anchor::Anchor;
use galaxy_editor::content::edit::EditDelta;
use galaxy_editor::content::find::{SearchConfig, SearchResults};
use galaxy_editor::content::selection_model::BufferSelectionModel;
use galaxy_editor::content::version::BufferVersion;
use galaxy_editor::multiline::{AnyMultilineString, MultilineString, LF};
use galaxy_editor::render::model::{AutoScrollMode, LineCount, StyleUpdateAction};
use galaxy_editor::selection::TextDirection;
use galaxyui::units::{IntoPixels, Pixels};
use num_traits::SaturatingSub;
use rangemap::{RangeMap, RangeSet};
use std::future::Future;
use std::ops::Range;
use std::path::Path;
@@ -24,42 +8,12 @@ use std::rc::Rc;
use std::sync::Arc;
use std::{cmp, mem};
use crate::util::link_detection::get_word_range_at_offset;
use crate::{
appearance::Appearance, editor::InteractionState, notebooks::editor::model::word_unit,
themes::theme::AnsiColorIdentifier,
};
use ai::diff_validation::DiffDelta;
use galaxy_core::semantic_selection::SemanticSelection;
use galaxy_editor::content::buffer::{ShouldAutoscroll, VimInsertPoint};
use galaxy_editor::{
content::{
buffer::{
AutoScrollBehavior, Buffer, BufferEditAction, BufferEvent, BufferSelectAction,
EditOrigin, InitialBufferState, SelectionOffsets, ToBufferCharOffset, ToBufferPoint,
},
hidden_lines_model::HiddenLinesModel,
text::{BufferBlockStyle, IndentBehavior, IndentUnit},
},
decoration::DecorationLayer,
editor::TextDecoration,
model::{CoreEditorModel, PlainTextEditorModel},
render::model::{
BlockItem, Decoration, LineDecoration, RenderEvent, RenderLineLocation, RenderState,
RichTextStyles, UpdateDecorationAfterLayout, WidthSetting,
},
selection::{SelectionMode, SelectionModel, TextUnit},
};
use galaxyui::elements::{
AnchorPair, OffsetPositioning, OffsetType, PositionedElementOffsetBounds, PositioningAxis,
XAxisAnchor, YAxisAnchor,
};
use galaxyui::text::{point::Point, TextBuffer};
use galaxyui::{AppContext, Entity, ModelContext, ModelHandle, SingletonEntity};
use itertools::Itertools;
use languages::{language_by_filename, language_by_name, Language};
use languages::{language_by_filename, language_by_local_filename, language_by_name, Language};
use line_ending::LineEnding;
use num_traits::SaturatingSub;
use rangemap::{RangeMap, RangeSet};
use string_offset::CharOffset;
use syntax_tree::{ColorMap, DecorationStateEvent, SyntaxTreeState};
use vec1::{vec1, Vec1};
@@ -73,6 +27,41 @@ use vim::{
vim_a_quote, vim_a_word, vim_find_char_on_line, vim_find_matching_bracket, vim_inner_block,
vim_inner_paragraph, vim_inner_quote, vim_inner_word, vim_word_iterator_from_offset,
};
use galaxy_core::platform::SessionPlatform;
use galaxy_core::semantic_selection::SemanticSelection;
use galaxy_core::ui::theme::Fill;
use galaxy_editor::content::anchor::Anchor;
use galaxy_editor::content::buffer::{
AutoScrollBehavior, Buffer, BufferEditAction, BufferEvent, BufferSelectAction, EditOrigin,
InitialBufferState, SelectionOffsets, ShouldAutoscroll, ToBufferCharOffset, ToBufferPoint,
VimInsertPoint,
};
use galaxy_editor::content::edit::EditDelta;
use galaxy_editor::content::find::{SearchConfig, SearchResults};
use galaxy_editor::content::hidden_lines_model::HiddenLinesModel;
use galaxy_editor::content::selection_model::BufferSelectionModel;
use galaxy_editor::content::text::{BufferBlockStyle, IndentBehavior, IndentUnit};
use galaxy_editor::content::version::BufferVersion;
use galaxy_editor::decoration::DecorationLayer;
use galaxy_editor::editor::TextDecoration;
use galaxy_editor::model::{CoreEditorModel, PlainTextEditorModel};
use galaxy_editor::multiline::{AnyMultilineString, MultilineString, LF};
use galaxy_editor::render::model::{
AutoScrollMode, BlockItem, BlockSpacings, BrokenLinkStyle, CheckBoxStyle, ColumnUnit,
Decoration, HorizontalRuleStyle, InlineCodeStyle, LineCount, LineDecoration, ParagraphStyles,
RenderEvent, RenderLineLocation, RenderState, RichTextStyles, StyleUpdateAction, TableStyle,
UpdateDecorationAfterLayout, WidthSetting,
};
use galaxy_editor::selection::{SelectionMode, SelectionModel, TextDirection, TextUnit};
use galaxy_util::standardized_path::StandardizedPath;
use galaxyui::elements::{
AnchorPair, OffsetPositioning, OffsetType, PositionedElementOffsetBounds, PositioningAxis,
XAxisAnchor, YAxisAnchor,
};
use galaxyui::text::point::Point;
use galaxyui::text::TextBuffer;
use galaxyui::units::{IntoPixels, Pixels};
use galaxyui::{AppContext, Entity, ModelContext, ModelHandle, SingletonEntity};
use super::super::DiffResult;
use super::comments::{EditorCommentsModel, PendingComment, PendingCommentEvent};
@@ -80,7 +69,13 @@ use super::diff::{
add_inline_overlay_color, DiffModel, DiffModelEvent, DiffStatus, RenderableDiffHunk,
};
use super::line::EditorLineLocation;
use crate::appearance::Appearance;
use crate::code::editor::line_iterator::LineIterator;
use crate::code_review::comments::{CommentId, CommentOrigin, LineDiffContent};
use crate::editor::InteractionState;
use crate::notebooks::editor::model::word_unit;
use crate::themes::theme::AnsiColorIdentifier;
use crate::util::link_detection::get_word_range_at_offset;
/// An opaque handle to a stable line in the editor content, suitable for scroll
/// position preservation. Contains an internal anchor that tracks through
@@ -261,7 +256,7 @@ impl DelayRendering {
model.render_state.update(ctx, move |render_state, _| {
let should_autoscroll = self.should_autoscroll;
for (delta, content_version) in self.edits {
render_state.add_pending_edit(delta.clone(), content_version);
render_state.add_pending_edit(delta, content_version);
}
match should_autoscroll {
ShouldAutoscroll::Yes => render_state.request_autoscroll(),
@@ -338,7 +333,73 @@ impl CodeEditorModel {
content.update(ctx, |buffer, _| {
buffer.set_session_platform(session_platform);
});
ctx.subscribe_to_model(&content, |me, event, ctx| {
Self::from_content(
content,
true, // show_current_line_highlights
lazy_layout, // lazy_layout_enabled
false, // lazy_layout_initialized
ctx,
|hidden_lines, ctx| {
ctx.add_model(|ctx| {
RenderState::new(text_styles, lazy_layout, Some(hidden_lines.clone()), ctx)
.with_width_setting(WidthSetting::InfiniteWidth)
})
},
)
}
/// Constructs a `CodeEditorModel` in TUI char-cell mode.
///
/// Identical to `new` but creates the `RenderState` with
/// [`LayoutMode::CharCell`] so all soft-wrap positions use monospace
/// character-count arithmetic rather than font-aware pixel layout.
/// `TuiEditorModel` (in `warp_tui`) is a type alias for this type;
/// constructing via this method is what gives the TUI editor all of
/// `CodeEditorModel`'s features (vim, syntax, diff, hidden lines) for free
/// while sharing no GUI-rendering infrastructure.
///
/// Like `new`, this reads syntax-highlight colors from the `Appearance`
/// singleton, so callers must register `Appearance` (a real one for the
/// runtime, `Appearance::mock()` for tests) before constructing the model.
pub fn new_tui(terminal_width: u16, ctx: &mut ModelContext<Self>) -> Self {
let content = ctx.add_model(|_| Buffer::new(Box::new(|_, _| IndentBehavior::Ignore)));
Self::from_content(
content,
false, // show_current_line_highlights: no GPU rendering in TUI
false, // lazy_layout_enabled: no lazy layout in TUI
true, // lazy_layout_initialized: no lazy layout in TUI
ctx,
|_hidden_lines, ctx| {
// CharCell layout never consults `RichTextStyles`, so pass a stub.
ctx.add_model(|ctx| {
RenderState::new_tui(terminal_width, Self::tui_stub_text_styles(), ctx)
})
},
)
}
/// Shared construction for [`Self::new`] and [`Self::new_tui`]. The two modes
/// differ only in how the backing `content` buffer and the `RenderState` are
/// built (GUI pixel layout vs. TUI char-cell layout) plus a few flags; all
/// other sub-models (selection, syntax tree, diff, hidden lines, comments)
/// and event subscriptions are identical and wired up here.
///
/// `build_render_state` receives the freshly-created `hidden_lines` handle so
/// the GUI path can attach it; the TUI path ignores it.
fn from_content(
content: ModelHandle<Buffer>,
show_current_line_highlights: bool,
lazy_layout_enabled: bool,
lazy_layout_initialized: bool,
ctx: &mut ModelContext<Self>,
build_render_state: impl FnOnce(
&ModelHandle<HiddenLinesModel>,
&mut ModelContext<Self>,
) -> ModelHandle<RenderState>,
) -> Self {
ctx.subscribe_to_model(&content, |me, _, event, ctx| {
me.handle_content_model_event(event, ctx);
});
@@ -349,23 +410,20 @@ impl CodeEditorModel {
let buffer_handle = content.downgrade();
let syntax_tree =
ctx.add_model(|_ctx| SyntaxTreeState::new(buffer_handle, buffer_version, color_map));
ctx.subscribe_to_model(&syntax_tree, |me, event, ctx| {
ctx.subscribe_to_model(&syntax_tree, |me, _, event, ctx| {
me.handle_syntax_tree_model_event(event, ctx);
});
let diff = ctx.add_model(|_ctx| DiffModel::new());
ctx.subscribe_to_model(&diff, |me, event, ctx| {
ctx.subscribe_to_model(&diff, |me, _, event, ctx| {
me.handle_diff_model_event(event, ctx);
});
let hidden_lines =
ctx.add_model(|_| HiddenLinesModel::new(content.clone(), selection_model.clone()));
let render_state = ctx.add_model(|ctx| {
RenderState::new(text_styles, lazy_layout, Some(hidden_lines.clone()), ctx)
.with_width_setting(WidthSetting::InfiniteWidth)
});
ctx.subscribe_to_model(&render_state, |me, event, ctx| {
let render_state = build_render_state(&hidden_lines, ctx);
ctx.subscribe_to_model(&render_state, |me, _, event, ctx| {
me.handle_render_state_model_event(event, ctx);
});
let selection = ctx.add_model(|ctx| {
@@ -394,17 +452,96 @@ impl CodeEditorModel {
hidden_lines,
diff_navigation_state: DiffNavigationState::Collapsed,
interaction_state: InteractionState::Editable,
show_current_line_highlights: true,
show_current_line_highlights,
delay_rendering: None,
vim_visual_tails: vec![],
hovered_symbol_range: None,
hide_lines_outside_of_active_diff: None,
lazy_layout_enabled: lazy_layout,
lazy_layout_initialized: false,
lazy_layout_enabled,
lazy_layout_initialized,
pending_syntax_tree_bootstrap: false,
}
}
/// A minimal [`RichTextStyles`] for the TUI char-cell editor.
///
/// `RenderState::new_tui` stores styles only for API compatibility and never
/// uses them for char-cell layout, so these values are placeholders. This
/// lives here (the caller of `RenderState::new_tui`) rather than in the core
/// editor crate so the editor API doesn't carry a TUI-specific stub.
fn tui_stub_text_styles() -> RichTextStyles {
use warpui::elements::{Border, Fill};
use warpui::fonts::{FamilyId, Weight};
const TRANSPARENT: warpui::color::ColorU = warpui::color::ColorU {
r: 0,
g: 0,
b: 0,
a: 0,
};
let paragraph = |fixed_width_tab_size| ParagraphStyles {
font_family: FamilyId(0),
font_size: 10.,
font_weight: Weight::Normal,
line_height_ratio: 1.,
text_color: TRANSPARENT,
baseline_ratio: 0.7,
fixed_width_tab_size,
};
RichTextStyles {
base_text: paragraph(None),
code_text: paragraph(Some(4)),
code_background: Fill::None,
embedding_background: Fill::None,
embedding_text: paragraph(None),
code_border: Border::new(0.),
placeholder_color: TRANSPARENT,
selection_fill: Fill::None,
cursor_fill: Fill::None,
inline_code_style: InlineCodeStyle {
font_family: FamilyId(0),
background: TRANSPARENT,
font_color: TRANSPARENT,
},
check_box_style: CheckBoxStyle {
border_width: 0.,
border_color: TRANSPARENT,
icon_path: "",
background: TRANSPARENT,
hover_background: TRANSPARENT,
},
horizontal_rule_style: HorizontalRuleStyle {
rule_height: 0.,
color: TRANSPARENT,
},
broken_link_style: BrokenLinkStyle {
icon_path: "",
icon_color: TRANSPARENT,
},
block_spacings: BlockSpacings::default(),
minimum_paragraph_height: None,
show_placeholder_text_on_empty_block: false,
cursor_width: 0.,
highlight_urls: false,
table_style: TableStyle {
border_color: TRANSPARENT,
header_background: TRANSPARENT,
cell_background: TRANSPARENT,
alternate_row_background: None,
text_color: TRANSPARENT,
header_text_color: TRANSPARENT,
scrollbar_nonactive_thumb_color: TRANSPARENT,
scrollbar_active_thumb_color: TRANSPARENT,
font_family: FamilyId(0),
font_size: 10.,
cell_padding: 0.,
outer_border: false,
column_dividers: false,
row_dividers: false,
},
}
}
fn should_defer_syntax_tree_parsing(&self) -> bool {
self.lazy_layout_enabled && !self.lazy_layout_initialized
}
@@ -1151,7 +1288,11 @@ impl CodeEditorModel {
}
/// Set the language of the syntax map based on the file path.
pub fn set_language_with_path(&mut self, path: &Path, ctx: &mut ModelContext<Self>) {
pub fn set_language_with_path(
&mut self,
path: &StandardizedPath,
ctx: &mut ModelContext<Self>,
) {
let language = language_by_filename(path);
if let Some(language) = language {
@@ -1159,6 +1300,15 @@ impl CodeEditorModel {
}
}
/// Set the language of the syntax map based on the local filesystem path.
pub fn set_language_with_local_path(&mut self, path: &Path, ctx: &mut ModelContext<Self>) {
let language = language_by_local_filename(path);
if let Some(language) = language {
self.set_language(language, ctx);
}
}
pub fn set_language_with_name(&mut self, name: &str, ctx: &mut ModelContext<Self>) {
let language = language_by_name(name);
if let Some(language) = language {
@@ -1581,8 +1731,6 @@ impl CodeEditorModel {
fn update_cursor_line_highlights(&self, ctx: &mut ModelContext<CodeEditorModel>) {
let selection_model = self.selection_model.as_ref(ctx);
let overlay = Appearance::as_ref(ctx).theme().surface_2();
let highlight_line = if self.diff_nav_is_active() {
// We don't show current line highlights during diff navigation so we don't need
// to update the `RenderState`. This lets us keep the line decorations we set
@@ -1591,6 +1739,7 @@ impl CodeEditorModel {
} else if selection_model.all_single_cursors() && self.show_current_line_highlights {
// When diff is not expanded, the only source of line decoration is highlights
// from the active cursor, e.g. the current line highlight.
let overlay = Appearance::as_ref(ctx).theme().surface_2();
Some(
selection_model
.selected_lines(ctx)
@@ -2302,7 +2451,7 @@ impl CodeEditorModel {
self.vim_set_selections_preserving_goal_xs(new_selections, AutoScrollBehavior::None, ctx);
}
/// Horziontal cursor movement for vim in the code editor.
/// Horizontal cursor movement for vim in the code editor.
/// Separate from the model's `move_left` and `move_right` functions to allow for stopping at
/// line boundaries and vim-specific selection logic.
pub fn vim_move_horizontal_by_offset(
@@ -2428,7 +2577,7 @@ impl CodeEditorModel {
if let Some(existing) = self.selection().as_ref(ctx).goal_xs.as_ref() {
existing
.iter()
.map(|px| px.as_f32().round() as u32)
.map(|col| col.as_pixels().as_f32().round() as u32)
.collect()
} else {
current_selections
@@ -2469,10 +2618,11 @@ impl CodeEditorModel {
if let Ok(new_selections) = Vec1::try_from_vec(new_selections_vec) {
self.vim_set_selections(new_selections, AutoScrollBehavior::Selection, ctx);
// Update goal_xs to the desired columns (stored as pixels for consistency with SelectionModel)
// Update goal_xs to the desired columns (stored as ColumnUnit::Pixels for
// consistency with the GUI SelectionModel pixel path)
let goal_pixels: Vec<_> = goal_cols
.into_iter()
.map(|c| (c as usize).into_pixels())
.map(|c| ColumnUnit::Pixels((c as usize).into_pixels()))
.collect();
self.selection().update(ctx, |selection, _| {
selection.goal_xs = Vec1::try_from_vec(goal_pixels).ok();
@@ -3642,11 +3792,19 @@ impl CoreEditorModel for CodeEditorModel {
buffer_version: BufferVersion,
ctx: &mut ModelContext<Self::T>,
) {
// Synchronously convert hidden range anchors into offsets for the given version. This allows the render model
// to accurately hide line ranges based on the corresponding incoming buffer state.
// Synchronously convert hidden range anchors into offsets for the given version. This allows
// the render model to accurately hide line ranges based on the corresponding incoming buffer state.
self.hidden_lines.update(ctx, |hidden_lines_model, ctx| {
hidden_lines_model.materialize_hidden_range_offsets(buffer_version, ctx);
});
// In TUI char-cell mode the async font-shaping pipeline is bypassed entirely (the
// LayoutAction::BufferEdit arm is a no-op for CharCell). We must therefore refresh the
// char-cell line index synchronously here so that offset_to_softwrap_point, max_line,
// and all cursor-positioning queries see up-to-date data in the same frame.
if let Some(char_cell) = self.render_state.as_ref(ctx).char_cell() {
let text = self.content.as_ref(ctx).text().into_string();
char_cell.update_text(&text);
}
}
fn content(&self) -> &ModelHandle<Buffer> {
@@ -3817,9 +3975,6 @@ impl CoreEditorModel for CodeEditorModel {
impl CodeEditorModel {
pub fn open_comment_line(&mut self, line: &EditorLineLocation, ctx: &mut ModelContext<Self>) {
// Telemetry: comment editor opened for a new inline review comment.
send_telemetry_from_ctx!(CodeReviewTelemetryEvent::CommentEditorOpened, ctx);
self.comments.update(ctx, |comments, ctx| {
comments.pending_comment = PendingComment::Open { line: line.clone() };
ctx.emit(PendingCommentEvent::NewPendingComment(line.clone()));
@@ -3834,9 +3989,6 @@ impl CodeEditorModel {
origin: &CommentOrigin,
ctx: &mut ModelContext<Self>,
) {
// Telemetry: comment editor opened for editing an existing inline review comment.
send_telemetry_from_ctx!(CodeReviewTelemetryEvent::CommentEditorOpened, ctx);
self.comments.update(ctx, |comments, ctx| {
comments.pending_comment = PendingComment::Open { line: line.clone() };
ctx.emit(PendingCommentEvent::ReopenPendingComment {
+8 -12
View File
@@ -1,17 +1,13 @@
use futures::channel::oneshot;
use galaxy_editor::content::buffer::{InitialBufferState, SelectionOffsets};
use galaxy_editor::multiline::MultilineString;
use galaxy_util::content_version::ContentVersion;
use galaxyui::App;
use std::path::Path;
use futures::channel::oneshot;
use vec1::vec1;
use crate::{
code::editor::line::EditorLineLocation, code::editor::view::code_text_styles,
settings::FontSettings, test_util::settings::initialize_settings_for_tests,
};
use super::*;
use crate::code::editor::line::EditorLineLocation;
use crate::code::editor::view::code_text_styles;
use crate::settings::FontSettings;
use crate::test_util::settings::initialize_settings_for_tests;
fn initialize_deps(app: &mut App) {
app.add_singleton_model(|_| Appearance::mock());
@@ -24,7 +20,7 @@ fn mock_model(app: &mut App, text: &str, version: ContentVersion) -> ModelHandle
let mut model = CodeEditorModel::new(styles, None, false, None, ctx);
let state = InitialBufferState::plain_text(text).with_version(version);
model.reset_content(state, ctx);
model.set_language_with_path(Path::new("test.rs"), ctx);
model.set_language_with_local_path(Path::new("/test.rs"), ctx);
model
})
}
@@ -40,7 +36,7 @@ fn mock_model_with_diff(
let mut model = CodeEditorModel::new(styles, None, false, None, ctx);
let state = InitialBufferState::plain_text(current_text).with_version(version);
model.reset_content(state, ctx);
model.set_language_with_path(Path::new("test.rs"), ctx);
model.set_language_with_local_path(Path::new("/test.rs"), ctx);
// Set up diff model with base text
model.diff().update(ctx, |diff, _| {
+14 -18
View File
@@ -1,31 +1,27 @@
#![cfg_attr(target_family = "wasm", allow(dead_code, unused_imports))]
// Adding this file level gate as some of the code around editability is not used in WASM yet.
use galaxy_core::ui::{appearance::Appearance, theme::Fill};
use galaxy_core::ui::appearance::Appearance;
use galaxy_core::ui::theme::Fill;
use galaxy_editor::model::CoreEditorModel;
use galaxyui::elements::{
Align, Border, ConstrainedBox, Container, CrossAxisAlignment, Flex, MouseStateHandle,
ParentElement, Shrinkable,
};
use galaxyui::presenter::ChildView;
use galaxyui::ui_components::button::ButtonVariant;
use galaxyui::ui_components::components::{UiComponent, UiComponentStyles};
use galaxyui::units::IntoPixels;
use galaxyui::{
elements::{
Align, Border, ConstrainedBox, Container, CrossAxisAlignment, Flex, MouseStateHandle,
ParentElement, Shrinkable,
},
presenter::ChildView,
ui_components::{
button::ButtonVariant,
components::{UiComponent, UiComponentStyles},
},
units::IntoPixels,
AppContext, Element, Entity, ModelHandle, SingletonEntity, TypedActionView, View, ViewContext,
ViewHandle,
};
use crate::{
editor::InteractionState,
ui_components::icons::Icon,
view_components::action_button::{ActionButton, ButtonSize, NakedTheme},
view_components::find::FIND_BAR_PADDING,
};
use super::model::{CodeEditorModel, CodeEditorModelEvent};
use crate::editor::InteractionState;
use crate::ui_components::icons::Icon;
use crate::view_components::action_button::{ActionButton, ButtonSize, NakedTheme};
use crate::view_components::find::FIND_BAR_PADDING;
const NAV_BAR_HEIGHT: f32 = 40.;
const NAV_BAR_ICON_SIZE: f32 = 16.;
+140 -79
View File
@@ -1,88 +1,85 @@
#![cfg_attr(target_family = "wasm", allow(dead_code, unused_imports))]
// Adding this file level gate as some of the code around editability is not used in WASM yet.
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::ops::Range;
use std::path::Path;
use std::rc::Rc;
use crate::code::editor::{
comment_editor::{CommentEditor, CommentEditorEvent},
comments::PendingComment,
diff::DiffStatus,
element::{
AddAsContextButton, CommentButton, EditorWrapper, EditorWrapperStateHandle,
GutterHoverTarget, GutterRange, InnerEditor, LineNumberConfig, RevertHunkButton,
},
find::view::{CodeEditorFind as Find, Event as FindViewEvent},
goto_line::view::{Event as GoToLineEvent, GoToLineView},
line::EditorLineLocation,
model::{CodeEditorModel, CodeEditorModelEvent, HoverableLink, LineBound, StableEditorLine},
nav_bar::{NavBar, NavBarBehavior, NavBarEvent},
scroll::{ScrollPosition, ScrollTrigger, ScrollWheelBehavior},
};
use crate::code::{
editor::EditorReviewComment, DiffResult, NoopCommentEditorProvider,
NoopFindReferencesCardProvider, ShowCommentEditorProvider, ShowFindReferencesCardProvider,
};
use crate::{
appearance::Appearance,
code_review::comments::{CommentId, CommentOrigin},
editor::InteractionState,
features::FeatureFlag,
notebooks::editor::rich_text_styles,
settings::{AppEditorSettings, FontSettings},
view_components::find::FindDirection,
};
use ai::diff_validation::DiffDelta;
use galaxy_core::platform::SessionPlatform;
use galaxy_editor::{
content::{
buffer::{
Buffer, BufferEditAction, EditOrigin, InitialBufferState, ToBufferCharOffset as _,
ToBufferPoint,
},
text::IndentUnit,
version::BufferVersion,
},
model::{CoreEditorModel, PlainTextEditorModel},
multiline::AnyMultilineString,
render::{
element::{
lens_element::RichTextElementLens, DisplayOptions, DisplayStateHandle, RichTextElement,
VerticalExpansionBehavior,
},
model::{
AutoScrollMode, BlockSpacing, Decoration, ExpansionType, LineCount, ParagraphStyles,
RichTextStyles, CODE_EDITOR_HIDDEN_SECTION_EXPANSION_LINES,
},
},
search::{SearchEvent, Searcher, MATCH_FILL, SELECTED_MATCH_FILL},
};
use galaxy_util::content_version::ContentVersion;
use galaxyui::{
elements::{
new_scrollable::{
AxisConfiguration, DualAxisConfig, NewScrollableElement, ScrollableAppearance,
},
ChildAnchor, ChildView, Dismiss, Fill, Flex, Margin, MouseStateHandle, NewScrollable,
OffsetPositioning, Padding, ParentAnchor, ParentElement, ParentOffsetBounds,
ScrollStateHandle, Shrinkable, Stack,
},
event::ModifiersState,
keymap::Keystroke,
platform::Cursor,
prelude::RectF,
text::point::Point,
units::Pixels,
AppContext, BlurContext, Element, Entity, FocusContext, ModelHandle, SingletonEntity, View,
ViewContext, ViewHandle, WeakViewHandle, WindowId,
};
use lazy_static::lazy_static;
use num_traits::SaturatingSub;
use pathfinder_geometry::vector::vec2f;
use std::fmt::Debug;
use std::rc::Rc;
use std::{collections::HashMap, ops::Range};
use std::{collections::HashSet, path::Path};
use settings::Setting as _;
use string_offset::CharOffset;
use vec1::{vec1, Vec1};
use vim::vim::{Direction, InsertPosition, VimMode, VimModel, VimState, VimSubscriber};
use galaxy_core::platform::SessionPlatform;
use galaxy_editor::content::buffer::{
Buffer, BufferEditAction, EditOrigin, InitialBufferState, ToBufferCharOffset as _,
ToBufferPoint,
};
use galaxy_editor::content::text::IndentUnit;
use galaxy_editor::content::version::BufferVersion;
use galaxy_editor::model::{CoreEditorModel, PlainTextEditorModel};
use galaxy_editor::multiline::AnyMultilineString;
use galaxy_editor::render::element::lens_element::RichTextElementLens;
use galaxy_editor::render::element::{
DisplayOptions, DisplayStateHandle, RichTextElement, VerticalExpansionBehavior,
};
use galaxy_editor::render::model::{
AutoScrollMode, BlockSpacing, Decoration, ExpansionType, LineCount, ParagraphStyles,
RichTextStyles, CODE_EDITOR_HIDDEN_SECTION_EXPANSION_LINES,
};
use galaxy_editor::search::{SearchEvent, Searcher, MATCH_FILL, SELECTED_MATCH_FILL};
use galaxy_util::content_version::ContentVersion;
use galaxy_util::standardized_path::StandardizedPath;
use galaxyui::elements::new_scrollable::{
AxisConfiguration, DualAxisConfig, NewScrollableElement, ScrollableAppearance,
};
use galaxyui::elements::{
ChildAnchor, ChildView, Dismiss, Fill, Flex, Margin, MouseStateHandle, NewScrollable,
OffsetPositioning, Padding, ParentAnchor, ParentElement, ParentOffsetBounds, ScrollStateHandle,
Shrinkable, Stack,
};
use galaxyui::event::ModifiersState;
use galaxyui::keymap::Keystroke;
use galaxyui::platform::Cursor;
use galaxyui::prelude::RectF;
use galaxyui::text::point::Point;
use galaxyui::units::Pixels;
use galaxyui::{
AppContext, BlurContext, CursorInfo, Element, Entity, FocusContext, ModelHandle,
SingletonEntity, View, ViewContext, ViewHandle, WeakViewHandle, WindowId,
};
use crate::appearance::Appearance;
use crate::code::editor::comment_editor::{CommentEditor, CommentEditorEvent};
use crate::code::editor::comments::PendingComment;
use crate::code::editor::diff::DiffStatus;
use crate::code::editor::element::{
AddAsContextButton, CommentButton, EditorWrapper, EditorWrapperStateHandle, GutterHoverTarget,
GutterRange, InnerEditor, LineNumberConfig, RevertHunkButton,
};
use crate::code::editor::find::view::{CodeEditorFind as Find, Event as FindViewEvent};
use crate::code::editor::goto_line::view::{Event as GoToLineEvent, GoToLineView};
use crate::code::editor::line::EditorLineLocation;
use crate::code::editor::model::{
CodeEditorModel, CodeEditorModelEvent, HoverableLink, LineBound, StableEditorLine,
};
use crate::code::editor::nav_bar::{NavBar, NavBarBehavior, NavBarEvent};
use crate::code::editor::scroll::{ScrollPosition, ScrollTrigger, ScrollWheelBehavior};
use crate::code::editor::EditorReviewComment;
use crate::code::{
DiffResult, NoopCommentEditorProvider, NoopFindReferencesCardProvider,
ShowCommentEditorProvider, ShowFindReferencesCardProvider,
};
use crate::code_review::comments::{CommentId, CommentOrigin};
use crate::editor::InteractionState;
use crate::features::FeatureFlag;
use crate::notebooks::editor::rich_text_styles;
use crate::settings::{AppEditorSettings, CodeEditorLineNumberMode, FontSettings};
use crate::view_components::find::FindDirection;
mod actions;
pub use actions::init;
@@ -123,6 +120,8 @@ pub enum CodeEditorEvent {
},
/// Emitted when a diff hunk is reverted
DiffReverted,
/// Emitted when the inline comment editor is opened.
CommentEditorOpened,
HiddenSectionExpanded,
/// Emitted when a comment is saved. This gets propagated up so that it
/// can be augmented with the file and repo paths and saved to the comment model.
@@ -318,6 +317,10 @@ impl CodeEditorView {
ctx.subscribe_to_model(&font_settings_handle, |me, _, _, ctx| {
me.handle_appearance_or_font_change(ctx);
});
let app_editor_settings_handle = AppEditorSettings::handle(ctx);
ctx.subscribe_to_model(&app_editor_settings_handle, |_, _, _, ctx| {
ctx.notify();
});
let model = ctx.add_model(|ctx| {
CodeEditorModel::new(
@@ -1215,18 +1218,32 @@ impl CodeEditorView {
let appearance = Appearance::as_ref(ctx);
let theme = appearance.theme();
if self.display_options.show_line_numbers {
let editor_settings = AppEditorSettings::as_ref(ctx);
Some(LineNumberConfig {
font_family: appearance.monospace_font_family(),
font_size: appearance.monospace_font_size(),
text_color: theme.sub_text_color(theme.background()).into(),
highlight_text_color: theme.main_text_color(theme.background()).into(),
starting_line_number: self.display_options.starting_line_number,
mode: *editor_settings.code_editor_line_number_mode.value(),
active_line_number: self.active_cursor_line_for_line_numbers(ctx),
active_cursor_is_visible: self.is_focused(ctx) && self.is_editable(ctx),
})
} else {
None
}
}
fn active_cursor_line_for_line_numbers(&self, ctx: &AppContext) -> Option<LineCount> {
let model = self.model.as_ref(ctx);
let selection = *model.selections(ctx).first();
let buffer = model.content().as_ref(ctx);
let point = selection.head.to_buffer_point(buffer);
// `LineCount`s used by render blocks are zero-based, while buffer points report rows using
// the editor's one-based convention.
Some(LineCount::from(point.row.saturating_sub(1) as usize))
}
fn run_find(&mut self, query: &str, ctx: &mut ViewContext<Self>) {
self.searcher.update(ctx, |searcher, ctx| {
searcher.set_query(query.to_string(), ctx);
@@ -1252,6 +1269,14 @@ impl CodeEditorView {
self.reset_for_editing_change();
self.vim_maybe_enforce_cursor_line_cap(ctx);
ctx.emit(CodeEditorEvent::SelectionChanged);
if *AppEditorSettings::as_ref(ctx)
.code_editor_line_number_mode
.value()
== CodeEditorLineNumberMode::Relative
{
// Repaint relative line-number gutters when the cursor origin changes.
ctx.notify();
}
}
CodeEditorModelEvent::ContentChanged { origin } => {
if origin.from_user() {
@@ -1426,12 +1451,18 @@ impl CodeEditorView {
});
}
pub fn set_language_with_path(&mut self, path: &Path, ctx: &mut ViewContext<Self>) {
pub fn set_language_with_path(&mut self, path: &StandardizedPath, ctx: &mut ViewContext<Self>) {
self.model.update(ctx, |model, ctx| {
model.set_language_with_path(path, ctx);
});
}
pub fn set_language_with_local_path(&mut self, path: &Path, ctx: &mut ViewContext<Self>) {
self.model.update(ctx, |model, ctx| {
model.set_language_with_local_path(path, ctx);
});
}
pub fn set_language_with_name(&mut self, name: &str, ctx: &mut ViewContext<Self>) {
self.model.update(ctx, |model, ctx| {
model.set_language_with_name(name, ctx);
@@ -2040,7 +2071,7 @@ impl CodeEditorView {
}
}
// If the character is opening autcomplete symbol, we want to autocomplete it with a closing symbol.
// If the character is opening autocomplete symbol, we want to autocomplete it with a closing symbol.
if let Some(close) = AUTOCOMPLETE_SYMBOLS.get(&first_char) {
self.model.update(ctx, |model, ctx| {
model.autocomplete_symbol(first_char, *close, ctx);
@@ -2126,6 +2157,7 @@ impl CodeEditorView {
self.model.update(ctx, |editor_model, ctx| {
editor_model.reopen_comment_line(id, location, comment_text, origin, ctx);
});
ctx.emit(CodeEditorEvent::CommentEditorOpened);
ctx.notify();
}
@@ -2356,6 +2388,18 @@ impl View for CodeEditorView {
}
}
fn active_cursor_position(&self, ctx: &ViewContext<Self>) -> Option<CursorInfo> {
let render_state = self.model.as_ref(ctx).render_state().as_ref(ctx);
let cursor_id = render_state.saved_positions().cursor_id();
let font_size = render_state.styles().base_text.font_size;
ctx.element_position_by_id(cursor_id.as_str())
.map(|position| CursorInfo {
position,
font_size,
})
}
fn on_blur(&mut self, blur_ctx: &BlurContext, ctx: &mut ViewContext<Self>) {
if blur_ctx.is_self_blurred() {
ctx.notify();
@@ -2370,8 +2414,14 @@ impl View for CodeEditorView {
}
if let Some(vim_mode) = self.vim_mode(app) {
context.set.insert("Vim");
if vim_mode == VimMode::Normal {
context.set.insert("VimNormalMode");
match vim_mode {
VimMode::Normal => {
context.set.insert("VimNormalMode");
}
VimMode::Visual(_) => {
context.set.insert("VimVisualMode");
}
_ => {}
}
}
if self.find_bar.is_some() {
@@ -2422,6 +2472,17 @@ impl CodeEditorView {
};
self.handle_goto_line_event(&event, ctx);
}
pub fn displayed_line_number_for_test(
&self,
one_based_line_number: usize,
ctx: &AppContext,
) -> Option<usize> {
let line_number_config = self.line_number_config(ctx)?;
let line_count = LineCount::from(one_based_line_number.checked_sub(1)?);
Some(line_number_config.display_line_number(line_count))
}
}
#[cfg(test)]
+55 -36
View File
@@ -1,42 +1,36 @@
#![cfg_attr(target_family = "wasm", allow(dead_code, unused_imports))]
// Adding this file level gate as some of the code around editability is not used in WASM yet.
use crate::code::editor::{
line::EditorLineLocation,
model::CodeEditorModel,
view::{CodeEditorEvent, CodeEditorView, VimMode},
};
use crate::{
cmd_or_ctrl_shift, code_review::comments::CommentId,
code_review::telemetry_event::CodeReviewTelemetryEvent, editor::InteractionState,
features::FeatureFlag, notebooks::editor::model::word_unit, send_telemetry_from_ctx,
util::bindings::CustomAction,
};
use galaxy_editor::{
content::version::BufferVersion,
editor::{EmbeddedItemModel, RunnableCommandModel, TextDecoration},
model::{CoreEditorModel, PlainTextEditorModel},
render::{
element::RichTextAction,
model::{ExpansionType, LineCount, Location},
},
selection::{TextDirection, TextUnit},
};
use galaxy_util::user_input::UserInput;
use galaxyui::{
actions::StandardAction,
elements::Axis,
event::ModifiersState,
keymap::{EditableBinding, FixedBinding, Keystroke, PerPlatformKeystroke},
units::Pixels,
AppContext, TypedActionView, ViewContext, WeakViewHandle,
};
use lazy_static::lazy_static;
use rangemap::RangeSet;
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::ops::Range;
use lazy_static::lazy_static;
use rangemap::RangeSet;
use string_offset::CharOffset;
use galaxy_editor::content::version::BufferVersion;
use galaxy_editor::editor::{EmbeddedItemModel, RunnableCommandModel, TextDecoration};
use galaxy_editor::model::{CoreEditorModel, PlainTextEditorModel};
use galaxy_editor::render::element::RichTextAction;
use galaxy_editor::render::model::{ExpansionType, LineCount, Location};
use galaxy_editor::selection::{TextDirection, TextUnit};
use galaxy_util::user_input::UserInput;
use galaxyui::actions::StandardAction;
use galaxyui::elements::Axis;
use galaxyui::event::ModifiersState;
use galaxyui::keymap::{EditableBinding, FixedBinding, Keystroke, PerPlatformKeystroke};
use galaxyui::units::Pixels;
use galaxyui::{AppContext, TypedActionView, ViewContext, WeakViewHandle};
use crate::cmd_or_ctrl_shift;
use crate::code::editor::line::EditorLineLocation;
use crate::code::editor::model::CodeEditorModel;
use crate::code::editor::view::{CodeEditorEvent, CodeEditorView, VimMode};
use crate::code_review::comments::CommentId;
use crate::editor::InteractionState;
use crate::features::FeatureFlag;
use crate::notebooks::editor::model::word_unit;
use crate::util::bindings::CustomAction;
/// Limit the keybindings that conflict with the Agent Mode embedded editor.
const NON_EDITABLE_KEYMAP_CONTEXT: &str = "NonEditableKeymapContext";
@@ -508,8 +502,24 @@ pub fn init(app: &mut AppContext) {
.with_context_predicate(text_entry.clone())
.with_key_binding("cmdorctrl-/"),
EditableBinding::new("editor_view:delete", "Delete", CodeEditorViewAction::Delete)
.with_context_predicate(text_entry.clone())
.with_context_predicate(
text_entry.clone() & !id!("VimNormalMode") & !id!("VimVisualMode"),
)
.with_key_binding("ctrl-d"),
EditableBinding::new(
"editor_view:vim_scroll_half_page_down",
"Scroll down half a page (vim)",
CodeEditorViewAction::ScrollHalfPageDown,
)
.with_context_predicate(text_entry.clone() & (id!("VimNormalMode") | id!("VimVisualMode")))
.with_key_binding("ctrl-d"),
EditableBinding::new(
"editor_view:vim_scroll_half_page_up",
"Scroll up half a page (vim)",
CodeEditorViewAction::ScrollHalfPageUp,
)
.with_context_predicate(text_entry.clone() & (id!("VimNormalMode") | id!("VimVisualMode")))
.with_key_binding("ctrl-u"),
EditableBinding::new(
"editor_view:cut_word_left",
"Cut word left",
@@ -616,6 +626,8 @@ pub enum CodeEditorViewAction {
ToggleComment,
ScrollVertical(Pixels),
ScrollHorizontal(Pixels),
ScrollHalfPageDown,
ScrollHalfPageUp,
SelectUp,
SelectDown,
SelectLeft,
@@ -753,6 +765,8 @@ impl CodeEditorViewAction {
Self::WindowsCtrlC => true,
Self::ScrollVertical(_)
| Self::ScrollHorizontal(_)
| Self::ScrollHalfPageDown
| Self::ScrollHalfPageUp
| Self::SelectUp
| Self::SelectDown
| Self::SelectLeft
@@ -867,7 +881,7 @@ impl TypedActionView for CodeEditorView {
match self.vim_mode(ctx) {
Some(VimMode::Visual(_)) => {
// In Vim Visual mode, if we get a ToggleComment request via the keyboard
// shorcut (cmd+/), simulate `gc` to the VimModel so that we correctly
// shortcut (cmd+/), simulate `gc` to the VimModel so that we correctly
// calculate the current visual selections, apply the toggle, and exit to
// normal mode.
self.vim_user_insert("gc", ctx);
@@ -889,6 +903,12 @@ impl TypedActionView for CodeEditorView {
render_state.scroll_horizontal(*delta, ctx);
})
}),
ScrollHalfPageDown => {
self.vim_keystroke(&Keystroke::parse("ctrl-d").expect("ctrl-d parses"), ctx)
}
ScrollHalfPageUp => {
self.vim_keystroke(&Keystroke::parse("ctrl-u").expect("ctrl-u parses"), ctx)
}
SelectUp => self.model.update(ctx, |model, ctx| {
model.select_up(ctx);
}),
@@ -1067,8 +1087,6 @@ impl TypedActionView for CodeEditorView {
}
RevertDiffHunk { line_range } => {
if FeatureFlag::RevertDiffHunk.is_enabled() {
send_telemetry_from_ctx!(CodeReviewTelemetryEvent::RevertHunkClicked, ctx);
// Convert line range to diff hunk index and revert it
let hunk_index = self
.model
@@ -1093,6 +1111,7 @@ impl TypedActionView for CodeEditorView {
self.model.update(ctx, |model: &mut CodeEditorModel, ctx| {
model.open_comment_line(line_info, ctx);
});
ctx.emit(CodeEditorEvent::CommentEditorOpened);
ctx.focus(&self.active_comment_editor);
ctx.notify();
+19 -20
View File
@@ -1,27 +1,26 @@
use galaxy_core::ui::appearance::Appearance;
use galaxy_editor::render::element::VerticalExpansionBehavior;
use galaxyui::{
elements::{new_scrollable::ScrollableAppearance, ScrollbarWidth},
platform::WindowStyle,
App, TypedActionView, ViewHandle, WindowId,
};
use std::sync::Arc;
use crate::{
cloud_object::model::persistence::CloudModel,
editor::InteractionState,
notebooks::editor::keys::NotebookKeybindings,
server::server_api::{team::MockTeamClient, workspace::MockWorkspaceClient},
settings_view::keybindings::KeybindingChangedNotifier,
test_util::settings::initialize_settings_for_tests,
vim_registers::VimRegisters,
workspace::{sync_inputs::SyncedInputState, ActiveSession},
workspaces::user_workspaces::UserWorkspaces,
AuthStateProvider,
};
use galaxy_core::ui::appearance::Appearance;
use galaxy_editor::render::element::VerticalExpansionBehavior;
use galaxy_util::user_input::UserInput;
use galaxyui::elements::new_scrollable::ScrollableAppearance;
use galaxyui::elements::ScrollbarWidth;
use galaxyui::platform::WindowStyle;
use galaxyui::{App, TypedActionView, ViewHandle, WindowId};
use super::{CodeEditorRenderOptions, CodeEditorView, CodeEditorViewAction};
use galaxy_util::user_input::UserInput;
use crate::cloud_object::model::persistence::CloudModel;
use crate::editor::InteractionState;
use crate::notebooks::editor::keys::NotebookKeybindings;
use crate::server::server_api::team::MockTeamClient;
use crate::server::server_api::workspace::MockWorkspaceClient;
use crate::settings_view::keybindings::KeybindingChangedNotifier;
use crate::test_util::settings::initialize_settings_for_tests;
use crate::vim_registers::VimRegisters;
use crate::workspace::sync_inputs::SyncedInputState;
use crate::workspace::ActiveSession;
use crate::workspaces::user_workspaces::UserWorkspaces;
use crate::AuthStateProvider;
fn initialize_editor(app: &mut App) -> (WindowId, ViewHandle<CodeEditorView>) {
initialize_settings_for_tests(app);
+72 -18
View File
@@ -1,26 +1,24 @@
use super::{CodeEditorEvent, CodeEditorView};
use crate::code::editor::{
find::view::Event as FindViewEvent,
model::{CaseTransform, CodeEditorModel, LineBound},
};
use crate::{
view_components::find::FindDirection,
vim_registers::{RegisterContent, VimRegisters},
};
use galaxy_editor::{
content::buffer::{
AutoScrollBehavior, BufferEditAction, EditOrigin, SelectionOffsets,
ToBufferCharOffset as _, VimInsertPoint,
},
model::{CoreEditorModel, PlainTextEditorModel},
selection::{TextDirection, TextUnit},
};
use galaxyui::{text::point::Point, SingletonEntity, ViewContext};
use vim::vim::{
BracketChar, CharacterMotion, Direction, FindCharMotion, FirstNonWhitespaceMotion,
InsertPosition, LineMotion, ModeTransition, MotionType, TextObjectType, VimHandler, VimMode,
VimMotion, VimOperand, VimOperator, VimTextObject, WordMotion,
};
use galaxy_editor::content::buffer::{
AutoScrollBehavior, BufferEditAction, EditOrigin, SelectionOffsets, ToBufferCharOffset as _,
VimInsertPoint,
};
use galaxy_editor::model::{CoreEditorModel, PlainTextEditorModel};
use galaxy_editor::render::model::AutoScrollMode;
use galaxy_editor::selection::{TextDirection, TextUnit};
use galaxyui::text::point::Point;
use galaxyui::units::IntoPixels;
use galaxyui::{SingletonEntity, ViewContext};
use super::{CodeEditorEvent, CodeEditorView};
use crate::code::editor::find::view::Event as FindViewEvent;
use crate::code::editor::model::{CaseTransform, CodeEditorModel, LineBound};
use crate::view_components::find::FindDirection;
use crate::vim_registers::{RegisterContent, VimRegisters};
impl VimHandler for CodeEditorView {
fn insert_char(&mut self, c: char, ctx: &mut ViewContext<Self>) {
@@ -921,6 +919,62 @@ impl VimHandler for CodeEditorView {
fn show_hover(&mut self, ctx: &mut ViewContext<Self>) {
ctx.emit(CodeEditorEvent::VimShowHover);
}
fn center_cursor_vertically(&mut self, ctx: &mut ViewContext<Self>) {
let cursor_offset = self
.model
.as_ref(ctx)
.buffer_selection_model()
.as_ref(ctx)
.first_selection_head();
self.model
.as_ref(ctx)
.render_state()
.clone()
.update(ctx, |render_state, _ctx| {
render_state.request_autoscroll_to(AutoScrollMode::PositionOffsetInViewportCenter(
cursor_offset,
));
});
}
fn scroll_half_page_down(&mut self, count: u32, ctx: &mut ViewContext<Self>) {
self.scroll_half_page(count, TextDirection::Forwards, ctx);
}
fn scroll_half_page_up(&mut self, count: u32, ctx: &mut ViewContext<Self>) {
self.scroll_half_page(count, TextDirection::Backwards, ctx);
}
}
impl CodeEditorView {
/// Implements `<C-d>` and `<C-u>`. Without a count, scrolls by half the
/// viewport; with a count > 1, scrolls by that many lines (matching vim's
/// `n<C-d>` / `n<C-u>` behavior).
fn scroll_half_page(
&mut self,
count: u32,
direction: TextDirection,
ctx: &mut ViewContext<Self>,
) {
let model = self.model.as_ref(ctx);
let lines = if count > 1 {
count as usize
} else {
(model.lines_in_viewport(ctx) / 2).max(1)
};
let signed_lines = match direction {
TextDirection::Forwards => -(lines as f32),
TextDirection::Backwards => lines as f32,
};
let scroll_pixels = (signed_lines * model.line_height(ctx)).into_pixels();
self.model.update(ctx, |model, ctx| {
model.vim_move_vertical_by_offset(lines as u32, direction, false, ctx);
model.render_state().update(ctx, |render_state, ctx| {
render_state.scroll(scroll_pixels, ctx);
});
});
}
}
/// Like [`str::trim_end_matches`] except that it only trims up to a single instance.
+323 -27
View File
@@ -1,33 +1,35 @@
use crate::auth::AuthStateProvider;
use crate::cloud_object::model::persistence::CloudModel;
use crate::code::editor::view::CodeEditorRenderOptions;
use crate::notebooks::editor::keys::NotebookKeybindings;
use crate::workspace::ActiveSession;
use crate::{
code::editor::view::{CodeEditorView, CodeEditorViewAction},
server::server_api::{team::MockTeamClient, workspace::MockWorkspaceClient},
settings::AppEditorSettings,
settings_view::keybindings::KeybindingChangedNotifier,
test_util::settings::initialize_settings_for_tests,
vim_registers::VimRegisters,
workspace::sync_inputs::SyncedInputState,
workspaces::user_workspaces::UserWorkspaces,
};
use galaxy_core::{features::FeatureFlag, settings::Setting, ui::appearance::Appearance};
use galaxy_editor::model::CoreEditorModel;
use galaxy_editor::{
content::buffer::{InitialBufferState, ToBufferCharOffset, ToBufferPoint},
render::element::VerticalExpansionBehavior,
};
use galaxy_util::user_input::UserInput;
use galaxyui::text::point::Point;
use galaxyui::{
keymap::Keystroke, platform::WindowStyle, App, SingletonEntity, TypedActionView, UpdateModel,
ViewHandle,
};
use std::sync::Arc;
use pathfinder_geometry::vector::Vector2F;
use unindent::Unindent;
use vim::vim::{MotionType, VimMode};
use galaxy_core::features::FeatureFlag;
use galaxy_core::settings::Setting;
use galaxy_core::ui::appearance::Appearance;
use galaxy_editor::content::buffer::{InitialBufferState, ToBufferCharOffset, ToBufferPoint};
use galaxy_editor::model::CoreEditorModel;
use galaxy_editor::render::element::VerticalExpansionBehavior;
use galaxy_editor::render::model::viewport::SizeInfo;
use galaxy_util::user_input::UserInput;
use galaxyui::keymap::Keystroke;
use galaxyui::platform::WindowStyle;
use galaxyui::text::point::Point;
use galaxyui::units::IntoPixels;
use galaxyui::{App, SingletonEntity, TypedActionView, UpdateModel, ViewHandle};
use crate::auth::AuthStateProvider;
use crate::cloud_object::model::persistence::CloudModel;
use crate::code::editor::view::{CodeEditorRenderOptions, CodeEditorView, CodeEditorViewAction};
use crate::notebooks::editor::keys::NotebookKeybindings;
use crate::server::server_api::team::MockTeamClient;
use crate::server::server_api::workspace::MockWorkspaceClient;
use crate::settings::AppEditorSettings;
use crate::settings_view::keybindings::KeybindingChangedNotifier;
use crate::test_util::settings::initialize_settings_for_tests;
use crate::vim_registers::VimRegisters;
use crate::workspace::sync_inputs::SyncedInputState;
use crate::workspace::ActiveSession;
use crate::workspaces::user_workspaces::UserWorkspaces;
// Await render/layout completion for a CodeEditorView in tests.
async fn layout_editor_view(app: &mut App, editor: &ViewHandle<CodeEditorView>) {
@@ -143,6 +145,37 @@ fn set_cursor_position(editor: &ViewHandle<CodeEditorView>, row: usize, col: usi
});
}
/// Set the viewport to exactly `lines` rows tall. Returns the line height in pixels.
fn set_viewport_lines(editor: &ViewHandle<CodeEditorView>, lines: usize, app: &mut App) -> f32 {
let (line_height, render_state) = editor.read(app, |view, ctx| {
let model = view.model.as_ref(ctx);
(model.line_height(ctx), model.render_state().clone())
});
render_state.update(app, |render_state, ctx| {
render_state.set_viewport_size(
SizeInfo {
viewport_size: Vector2F::new(800.0, line_height * lines as f32),
needs_layout: false,
},
ctx,
);
});
line_height
}
/// Read the current vertical scroll position.
fn scroll_top(editor: &ViewHandle<CodeEditorView>, app: &App) -> f32 {
editor.read(app, |view, ctx| {
view.model
.as_ref(ctx)
.render_state()
.as_ref(ctx)
.viewport()
.scroll_top()
.as_f32()
})
}
#[test]
fn test_code_editor_vim_basic_mode_switching() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
@@ -1436,3 +1469,266 @@ fn test_vim_visual_linewise_delete_first_line_does_not_panic() {
assert_eq!(buffer_text(&editor, &app), "bbb\nccc");
});
}
#[test]
fn test_vim_zz_in_normal_mode_preserves_cursor_and_mode() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"line 1
line 2
line 3
line 4
line 5",
&mut app,
);
layout_editor_view(&mut app, &editor).await;
// Place cursor on line 3, then center it. zz scrolls but should not move
// the cursor or change the mode.
set_cursor_position(&editor, 3, 0, &mut app);
assert_eq!(cursor_position(&editor, &app), (3, 0));
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
vim_user_insert(&editor, "zz", &mut app);
assert_eq!(cursor_position(&editor, &app), (3, 0));
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
});
}
#[test]
fn test_vim_zz_in_visual_mode_preserves_cursor_and_mode() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"line 1
line 2
line 3
line 4
line 5",
&mut app,
);
layout_editor_view(&mut app, &editor).await;
set_cursor_position(&editor, 3, 0, &mut app);
vim_user_insert(&editor, "v", &mut app);
assert_eq!(
vim_mode(&editor, &app),
Some(VimMode::Visual(MotionType::Charwise))
);
vim_user_insert(&editor, "zz", &mut app);
assert_eq!(cursor_position(&editor, &app), (3, 0));
assert_eq!(
vim_mode(&editor, &app),
Some(VimMode::Visual(MotionType::Charwise))
);
});
}
#[test]
fn test_vim_z_followed_by_non_z_clears_pending() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"line 1
line 2
line 3",
&mut app,
);
layout_editor_view(&mut app, &editor).await;
// `z` followed by an unrecognized char should clear the pending action.
// After that, a subsequent `j` should move the cursor down one line as normal.
set_cursor_position(&editor, 1, 0, &mut app);
vim_user_insert(&editor, "z", &mut app);
vim_user_insert(&editor, "x", &mut app);
assert_eq!(cursor_position(&editor, &app), (1, 0));
vim_user_insert(&editor, "j", &mut app);
assert_eq!(cursor_position(&editor, &app), (2, 0));
});
}
#[test]
fn test_vim_ctrl_d_scrolls_half_page_down() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let buffer: String = (1..=200).map(|i| format!("line {}\n", i)).collect();
let editor = add_code_editor(buffer.as_str(), &mut app);
layout_editor_view(&mut app, &editor).await;
// 20 visible lines → half page = 10 lines.
let line_height = set_viewport_lines(&editor, 20, &mut app);
let half_page = 10;
set_cursor_position(&editor, 1, 0, &mut app);
let (start_row, _) = cursor_position(&editor, &app);
let start_scroll = scroll_top(&editor, &app);
editor.update(&mut app, |view, ctx| {
view.vim_keystroke(&Keystroke::parse("ctrl-d").unwrap(), ctx);
});
let (after_row, _) = cursor_position(&editor, &app);
let after_scroll = scroll_top(&editor, &app);
assert_eq!(after_row, start_row + half_page);
assert!(
(after_scroll - start_scroll - half_page as f32 * line_height).abs() < 0.5,
"scroll_top should advance by half_page * line_height \
(start={start_scroll}, after={after_scroll}, line_height={line_height})",
);
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
});
}
#[test]
fn test_vim_ctrl_u_scrolls_half_page_up() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let buffer: String = (1..=200).map(|i| format!("line {}\n", i)).collect();
let editor = add_code_editor(buffer.as_str(), &mut app);
layout_editor_view(&mut app, &editor).await;
// 20 visible lines → half page = 10 lines.
let line_height = set_viewport_lines(&editor, 20, &mut app);
let half_page = 10;
// Start near the bottom and scroll down so we have room to scroll up.
set_cursor_position(&editor, 100, 0, &mut app);
let render_state = editor.read(&app, |view, ctx| {
view.model.as_ref(ctx).render_state().clone()
});
render_state.update(&mut app, |render_state, ctx| {
render_state.scroll(-(50.0 * line_height).into_pixels(), ctx);
});
let (start_row, _) = cursor_position(&editor, &app);
let start_scroll = scroll_top(&editor, &app);
editor.update(&mut app, |view, ctx| {
view.vim_keystroke(&Keystroke::parse("ctrl-u").unwrap(), ctx);
});
let (after_row, _) = cursor_position(&editor, &app);
let after_scroll = scroll_top(&editor, &app);
assert_eq!(after_row, start_row - half_page);
assert!(
(start_scroll - after_scroll - half_page as f32 * line_height).abs() < 0.5,
"scroll_top should retreat by half_page * line_height \
(start={start_scroll}, after={after_scroll}, line_height={line_height})",
);
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
});
}
#[test]
fn test_vim_ctrl_d_with_count_scrolls_n_lines() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let buffer: String = (1..=200).map(|i| format!("line {}\n", i)).collect();
let editor = add_code_editor(buffer.as_str(), &mut app);
layout_editor_view(&mut app, &editor).await;
// Viewport half page would be 10, but `5<C-d>` should scroll by 5 lines,
// not 5 * half_page.
set_viewport_lines(&editor, 20, &mut app);
set_cursor_position(&editor, 1, 0, &mut app);
let (start_row, _) = cursor_position(&editor, &app);
vim_user_insert(&editor, "5", &mut app);
editor.update(&mut app, |view, ctx| {
view.vim_keystroke(&Keystroke::parse("ctrl-d").unwrap(), ctx);
});
let (after_row, _) = cursor_position(&editor, &app);
assert_eq!(after_row, start_row + 5);
});
}
#[test]
fn test_vim_ctrl_d_consumes_pending_count() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
// Use a buffer big enough that scrolling won't max the cursor at the bottom.
let buffer: String = (1..=200)
.map(|i| format!("line {}\n", i))
.collect::<String>();
let editor = add_code_editor(buffer.as_str(), &mut app);
layout_editor_view(&mut app, &editor).await;
// After `2<C-d>`, the pending count of 2 must be consumed by ctrl-d. A
// following `j` should move the cursor down exactly 1 line, not 2.
set_cursor_position(&editor, 1, 0, &mut app);
vim_user_insert(&editor, "2", &mut app);
editor.update(&mut app, |view, ctx| {
view.vim_keystroke(&Keystroke::parse("ctrl-d").unwrap(), ctx);
});
let (after_scroll_row, _) = cursor_position(&editor, &app);
vim_user_insert(&editor, "j", &mut app);
let (after_j_row, _) = cursor_position(&editor, &app);
assert_eq!(
after_j_row,
after_scroll_row + 1,
"j after `2<C-d>` should move down 1, not 2 (after_scroll_row={}, after_j_row={})",
after_scroll_row,
after_j_row
);
});
}
#[test]
fn test_vim_ctrl_d_clears_pending_operator() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"alpha bravo charlie
delta echo foxtrot
golf hotel india",
&mut app,
);
layout_editor_view(&mut app, &editor).await;
// After `d<C-d>`, the pending `d` operator must be cleared. A following
// `w` should move forward by word, not delete a word.
set_cursor_position(&editor, 1, 0, &mut app);
let original = buffer_text(&editor, &app);
vim_user_insert(&editor, "d", &mut app);
editor.update(&mut app, |view, ctx| {
view.vim_keystroke(&Keystroke::parse("ctrl-d").unwrap(), ctx);
});
vim_user_insert(&editor, "w", &mut app);
assert_eq!(
buffer_text(&editor, &app),
original,
"w after `d<C-d>` should not delete (pending d should be cleared)"
);
});
}