From 2f15615d7bfcea0727a1327aaa438c7e4e193bf2 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Mon, 27 Jul 2026 08:38:25 +0700 Subject: [PATCH] clean up input component --- .../ui/src/input/display_map/display_map.rs | 233 +----- crates/ui/src/input/display_map/fold_map.rs | 343 --------- crates/ui/src/input/display_map/folding.rs | 96 --- crates/ui/src/input/display_map/mod.rs | 54 -- crates/ui/src/input/display_map/wrap_map.rs | 60 +- crates/ui/src/input/element.rs | 708 +----------------- crates/ui/src/input/indent.rs | 167 +---- crates/ui/src/input/input.rs | 6 - crates/ui/src/input/mod.rs | 4 +- crates/ui/src/input/mode.rs | 90 +-- .../ui/src/input/popovers/code_action_menu.rs | 337 --------- .../ui/src/input/popovers/completion_menu.rs | 446 ----------- crates/ui/src/input/popovers/context_menu.rs | 142 ---- .../src/input/popovers/diagnostic_popover.rs | 95 --- crates/ui/src/input/popovers/hover_popover.rs | 292 -------- crates/ui/src/input/popovers/mod.rs | 41 - crates/ui/src/input/state.rs | 169 +---- 17 files changed, 94 insertions(+), 3189 deletions(-) delete mode 100644 crates/ui/src/input/display_map/fold_map.rs delete mode 100644 crates/ui/src/input/display_map/folding.rs delete mode 100644 crates/ui/src/input/popovers/code_action_menu.rs delete mode 100644 crates/ui/src/input/popovers/completion_menu.rs delete mode 100644 crates/ui/src/input/popovers/context_menu.rs delete mode 100644 crates/ui/src/input/popovers/diagnostic_popover.rs delete mode 100644 crates/ui/src/input/popovers/hover_popover.rs delete mode 100644 crates/ui/src/input/popovers/mod.rs diff --git a/crates/ui/src/input/display_map/display_map.rs b/crates/ui/src/input/display_map/display_map.rs index bb19824..affdfa3 100644 --- a/crates/ui/src/input/display_map/display_map.rs +++ b/crates/ui/src/input/display_map/display_map.rs @@ -1,202 +1,61 @@ -/// DisplayMap: Public facade for Editor/Input display mapping. -/// -/// This combines WrapMap and FoldMap to provide a unified API: -/// - BufferPoint ↔ DisplayPoint conversion -/// - Fold management (candidates, toggle, query) -/// - Automatic projection updates on text/layout changes use std::ops::Range; use gpui::{App, Font, Pixels}; use ropey::Rope; -use super::fold_map::FoldMap; -use super::folding::FoldRange; use super::text_wrapper::{LineItem, WrapDisplayPoint}; use super::wrap_map::WrapMap; -use super::{BufferPoint, DisplayPoint}; use crate::input::Point as TreeSitterPoint; -use crate::input::display_map::WrapPoint; -use crate::input::rope_ext::RopeExt as _; -/// DisplayMap is the main interface for Editor/Input coordinate mapping. -/// -/// It manages the two-layer projection: -/// 1. Buffer → Wrap (soft-wrapping) -/// 2. Wrap → Display (folding) -/// -/// Editor/Input only needs to work with BufferPoint and DisplayPoint. +/// DisplayMap is the main interface for Input coordinate mapping. pub struct DisplayMap { wrap_map: WrapMap, - fold_map: FoldMap, } impl DisplayMap { pub fn new(font: Font, font_size: Pixels, wrap_width: Option) -> Self { Self { wrap_map: WrapMap::new(font, font_size, wrap_width), - fold_map: FoldMap::new(), } } - // ==================== Core Coordinate Mapping ==================== - - /// Convert buffer position to display position - pub fn buffer_pos_to_display_pos(&self, pos: BufferPoint) -> DisplayPoint { - // Buffer → Wrap - let wrap_pos = self.wrap_map.buffer_pos_to_wrap_pos(pos); - - // Wrap → Display - if let Some(display_row) = self.fold_map.wrap_row_to_display_row(wrap_pos.row) { - DisplayPoint::new(display_row, wrap_pos.col) - } else { - // Cursor is in a folded region, find nearest visible row - let display_row = self.fold_map.nearest_visible_display_row(wrap_pos.row); - DisplayPoint::new(display_row, 0) // Column 0 at fold boundary - } - } - - /// Convert display position to buffer position - pub fn display_pos_to_buffer_pos(&self, pos: DisplayPoint) -> BufferPoint { - // Display → Wrap - let wrap_row = self.fold_map.display_row_to_wrap_row(pos.row).unwrap_or(0); - - // Wrap → Buffer - let wrap_pos = WrapPoint::new(wrap_row, pos.col); - self.wrap_map.wrap_pos_to_buffer_pos(wrap_pos) - } - - /// Get total number of visible display rows + /// Get total number of display rows (same as wrap rows without folding) #[inline] pub fn display_row_count(&self) -> usize { - self.fold_map.display_row_count() + self.wrap_map.wrap_row_count() } /// Get the buffer line for a given display row pub fn display_row_to_buffer_line(&self, display_row: usize) -> usize { - // Display → Wrap - let wrap_row = self - .fold_map - .display_row_to_wrap_row(display_row) - .unwrap_or(0); - - // Wrap → Buffer line - self.wrap_map.wrap_row_to_buffer_line(wrap_row) + self.wrap_map.wrap_row_to_buffer_line(display_row) } /// Get the display row range for a buffer line: [start, end) - /// Returns None if the buffer line is completely hidden pub fn buffer_line_to_display_row_range(&self, line: usize) -> Option> { - // Buffer line → Wrap row range - let wrap_row_range = self.wrap_map.buffer_line_to_wrap_row_range(line); - - // Find first and last visible display rows in this range - let mut first_display_row = None; - let mut last_display_row = None; - - for wrap_row in wrap_row_range { - if let Some(display_row) = self.fold_map.wrap_row_to_display_row(wrap_row) { - if first_display_row.is_none() { - first_display_row = Some(display_row); - } - last_display_row = Some(display_row); - } - } - - if let (Some(start), Some(end)) = (first_display_row, last_display_row) { - Some(start..end + 1) - } else { - None // Completely folded - } + let range = self.wrap_map.buffer_line_to_wrap_row_range(line); + if range.is_empty() { None } else { Some(range) } } - /// Check if a buffer line is completely hidden + /// Check if a buffer line is completely hidden (never true without folding) #[inline] - pub fn is_buffer_line_hidden(&self, line: usize) -> bool { - self.buffer_line_to_display_row_range(line).is_none() + pub fn is_buffer_line_hidden(&self, _line: usize) -> bool { + false } - /// Set fold candidates (from tree-sitter/LSP) - pub fn set_fold_candidates(&mut self, candidates: Vec) { - self.fold_map.set_candidates(candidates); - self.rebuild_fold_projection(); - } - - /// Set a fold at the given start_line (must be in candidates) - pub fn set_folded(&mut self, start_line: usize, folded: bool) { - self.fold_map.set_folded(start_line, folded); - self.rebuild_fold_projection(); - } - - /// Toggle fold at the given start_line - pub fn toggle_fold(&mut self, start_line: usize) { - self.fold_map.toggle_fold(start_line); - self.rebuild_fold_projection(); - } - - /// Check if a line is currently folded + /// All wrap rows are visible since there's no folding. #[inline] - pub fn is_folded_at(&self, start_line: usize) -> bool { - self.fold_map.is_folded_at(start_line) + pub fn folded_ranges(&self) -> &[()] { + &[] } - /// Check if a line is a fold candidate - #[inline] - pub fn is_fold_candidate(&self, start_line: usize) -> bool { - self.fold_map.is_fold_candidate(start_line) - } - - /// Get all currently folded ranges - #[inline] - pub fn folded_ranges(&self) -> &[FoldRange] { - self.fold_map.folded_ranges() - } - - /// Clear all folds - pub fn clear_folds(&mut self) { - self.fold_map.clear_folds(); - self.rebuild_fold_projection(); - } - - // ==================== Text and Layout Updates ==================== - - /// Adjust folds and candidates for a text edit before updating the wrap map. - /// - /// Must be called with the OLD text (before replacement) and the edit range/new_text - /// so we can compute which old lines were affected. - pub fn adjust_folds_for_edit(&mut self, old_text: &Rope, range: &Range, new_text: &str) { - if self.fold_map.folded_ranges().is_empty() && self.fold_map.fold_candidates().is_empty() { - return; - } - - let edit_start_line = old_text.offset_to_point(range.start).row; - let edit_end_line = old_text.offset_to_point(range.end.min(old_text.len())).row; - - let old_lines_in_range = edit_end_line.saturating_sub(edit_start_line); - let new_lines_in_range = new_text.chars().filter(|c| *c == '\n').count(); - let line_delta = new_lines_in_range as isize - old_lines_in_range as isize; - - self.fold_map - .adjust_folds_for_edit(edit_start_line, edit_end_line, line_delta); - } - - /// Incrementally update fold candidates after a text edit. - /// - /// Extracts new fold candidates only within the edited byte range - /// and merges them with existing (already adjusted) candidates. - pub fn update_fold_candidates_for_edit( + /// Adjust folds for edit (no-op without folding) + pub fn adjust_folds_for_edit( &mut self, - tree: &super::folding::Tree, - edit_byte_range: Range, - new_text: &Rope, + _old_text: &Rope, + _range: &Range, + _new_text: &str, ) { - let new_start_line = new_text.offset_to_point(edit_byte_range.start).row; - let new_end_line = new_text - .offset_to_point(edit_byte_range.end.min(new_text.len())) - .row; - - let new_candidates = super::folding::extract_fold_ranges_in_range(tree, edit_byte_range); - self.fold_map - .merge_candidates_for_edit(new_start_line, new_end_line, new_candidates); + // No-op: no folding } /// Update text (incremental or full) @@ -209,52 +68,28 @@ impl DisplayMap { ) { self.wrap_map .on_text_changed(changed_text, range, new_text, cx); - self.rebuild_fold_projection(); } /// Update layout parameters (wrap width or font) pub fn on_layout_changed(&mut self, wrap_width: Option, cx: &mut App) { self.wrap_map.on_layout_changed(wrap_width, cx); - self.rebuild_fold_projection(); } /// Set font parameters pub fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) { self.wrap_map.set_font(font, font_size, cx); - self.rebuild_fold_projection(); } /// Ensure text is prepared (initializes wrapper if needed) pub fn ensure_text_prepared(&mut self, text: &Rope, cx: &mut App) { - let did_initialize = self.wrap_map.ensure_text_prepared(text, cx); - if did_initialize { - self.rebuild_fold_projection(); - } + self.wrap_map.ensure_text_prepared(text, cx); } /// Initialize with text pub fn set_text(&mut self, text: &Rope, cx: &mut App) { self.wrap_map.set_text(text, cx); - self.rebuild_fold_projection(); } - // ==================== Internal Helpers ==================== - - /// Rebuild fold projection after wrap_map or fold state changes - /// Only rebuilds if there are actually folded ranges - fn rebuild_fold_projection(&mut self) { - if !self.fold_map.folded_ranges().is_empty() { - self.fold_map.rebuild(&self.wrap_map); - } else { - // No active folds: identity mapping (wrap_row == display_row). - // Just update cached count so query methods work without Vec allocation. - self.fold_map - .mark_dirty_with_wrap_count(self.wrap_map.wrap_row_count()); - } - } - - // ==================== Wrap Display Point Operations ==================== - /// Convert byte offset to wrap display point (with soft wrap info). #[inline] pub(crate) fn offset_to_wrap_display_point(&self, offset: usize) -> WrapDisplayPoint { @@ -273,23 +108,30 @@ impl DisplayMap { self.wrap_map.wrapper().display_point_to_point(point) } - /// Convert a wrap row to a display row (skipping folded rows). - /// Returns None if the wrap row is folded. + /// Since there's no folding, wrap row == display row. #[inline] pub fn wrap_row_to_display_row(&self, wrap_row: usize) -> Option { - self.fold_map.wrap_row_to_display_row(wrap_row) + if wrap_row < self.wrap_row_count() { + Some(wrap_row) + } else { + None + } } - /// Find the nearest visible display row for a given wrap row. + /// Since there's no folding, nearest visible row is the row itself. #[inline] pub fn nearest_visible_display_row(&self, wrap_row: usize) -> usize { - self.fold_map.nearest_visible_display_row(wrap_row) + wrap_row.min(self.wrap_row_count().saturating_sub(1)) } - /// Convert a display row to a wrap row. + /// Since there's no folding, display row == wrap row. #[inline] pub fn display_row_to_wrap_row(&self, display_row: usize) -> Option { - self.fold_map.display_row_to_wrap_row(display_row) + if display_row < self.wrap_row_count() { + Some(display_row) + } else { + None + } } /// Get the longest row index (by byte length). @@ -298,8 +140,6 @@ impl DisplayMap { self.wrap_map.wrapper().longest_row.row } - // ==================== Access Methods ==================== - /// Get access to line items (for rendering) #[inline] pub(crate) fn lines(&self) -> &[LineItem] { @@ -312,14 +152,13 @@ impl DisplayMap { self.wrap_map.text() } - /// Calculate how many wrap rows of a buffer line are visible (not folded) + /// Calculate how many wrap rows of a buffer line are visible #[inline] pub fn visible_wrap_row_count_for_buffer_line(&self, line: usize) -> usize { - self.wrap_map - .visible_wrap_row_count_for_line(line, &self.fold_map) + self.wrap_map.visible_wrap_row_count_for_buffer_line(line) } - /// Get the wrap row count (before folding) + /// Get the wrap row count #[inline] pub fn wrap_row_count(&self) -> usize { self.wrap_map.wrap_row_count() diff --git a/crates/ui/src/input/display_map/fold_map.rs b/crates/ui/src/input/display_map/fold_map.rs deleted file mode 100644 index bcec679..0000000 --- a/crates/ui/src/input/display_map/fold_map.rs +++ /dev/null @@ -1,343 +0,0 @@ -/// FoldMap: Folding projection layer (Wrap rows → Display rows). -/// -/// This module manages code folding by: -/// - Filtering out wrap rows that belong to folded regions -/// - Maintaining bidirectional mapping: wrap_row ↔ display_row -/// - Handling fold state changes and rebuilding the projection -use super::folding::FoldRange; -use super::wrap_map::WrapMap; - -/// FoldMap projects wrap rows to display rows by hiding folded regions. -pub struct FoldMap { - /// Mapping: display_row → wrap_row - /// index = display_row, value = actual wrap_row - visible_wrap_rows: Vec, - - /// Reverse mapping: wrap_row → display_row - /// index = wrap_row, value = Some(display_row) if visible, None if folded - wrap_row_to_display_row: Vec>, - - /// Candidate fold ranges (from tree-sitter/LSP) - /// Sorted by start_line, unique start_line - candidates: Vec, - - /// Currently folded ranges - /// Subset of candidates, sorted by start_line - folded: Vec, - - /// Flag indicating if the fold projection needs rebuilding - /// Used for lazy evaluation to avoid expensive rebuilds on every text change - needs_rebuild: bool, - - /// Cached wrap_row_count from last rebuild - /// Used to detect if WrapMap changed and rebuild is needed - cached_wrap_row_count: usize, -} - -impl FoldMap { - pub fn new() -> Self { - Self { - visible_wrap_rows: Vec::new(), - wrap_row_to_display_row: Vec::new(), - candidates: Vec::new(), - folded: Vec::new(), - needs_rebuild: true, - cached_wrap_row_count: 0, - } - } - - /// Update cached wrap_row_count without full rebuild. - /// Used when no folds are active (identity mapping assumed). - pub(super) fn mark_dirty_with_wrap_count(&mut self, wrap_row_count: usize) { - self.needs_rebuild = true; - self.cached_wrap_row_count = wrap_row_count; - } - - /// Get total number of visible display rows - pub fn display_row_count(&self) -> usize { - if self.folded.is_empty() { - return self.cached_wrap_row_count; - } - self.visible_wrap_rows.len() - } - - /// Convert wrap_row to display_row - /// Returns None if the wrap_row is hidden by folding - pub fn wrap_row_to_display_row(&self, wrap_row: usize) -> Option { - if self.folded.is_empty() { - return if wrap_row < self.cached_wrap_row_count { - Some(wrap_row) - } else { - None - }; - } - self.wrap_row_to_display_row - .get(wrap_row) - .copied() - .flatten() - } - - /// Convert display_row to wrap_row - pub fn display_row_to_wrap_row(&self, display_row: usize) -> Option { - if self.folded.is_empty() { - return if display_row < self.cached_wrap_row_count { - Some(display_row) - } else { - None - }; - } - self.visible_wrap_rows.get(display_row).copied() - } - - /// Find the nearest visible display_row for a given wrap_row - pub fn nearest_visible_display_row(&self, wrap_row: usize) -> usize { - if self.folded.is_empty() { - return wrap_row.min(self.cached_wrap_row_count.saturating_sub(1)); - } - - if let Some(dr) = self.wrap_row_to_display_row(wrap_row) { - return dr; - } - - match self.visible_wrap_rows.binary_search(&wrap_row) { - Ok(idx) => idx, - Err(insert_pos) => insert_pos.saturating_sub(1), - } - } - - /// Set fold candidates (from tree-sitter/LSP), full replacement. - pub fn set_candidates(&mut self, mut candidates: Vec) { - // Sort and deduplicate by start_line - candidates.sort_by_key(|r| r.start_line); - candidates.dedup_by_key(|r| r.start_line); - self.candidates = candidates; - - // Remove any folded ranges that are no longer in candidates - self.folded.retain(|fold| { - self.candidates - .iter() - .any(|c| c.start_line == fold.start_line) - }); - } - - /// Merge new candidates extracted from an edited region into existing candidates. - /// - /// Replaces candidates within [edit_start_line, edit_end_line] with `new_candidates`, - /// keeping candidates outside the edit range intact. - pub fn merge_candidates_for_edit( - &mut self, - edit_start_line: usize, - edit_end_line: usize, - new_candidates: Vec, - ) { - // Remove old candidates within the edit range (already done by adjust_folds_for_edit) - // But do it again in case adjust wasn't called or range differs - self.candidates - .retain(|c| c.start_line < edit_start_line || c.start_line > edit_end_line); - - // Add new candidates - self.candidates.extend(new_candidates); - self.candidates.sort_by_key(|r| r.start_line); - self.candidates.dedup_by_key(|r| r.start_line); - } - - /// Set a fold at the given start_line (must be in candidates) - pub fn set_folded(&mut self, start_line: usize, folded: bool) { - if folded { - // Find the candidate range for this start_line - if let Some(candidate) = self.candidates.iter().find(|c| c.start_line == start_line) { - // Add to folded if not already present - if !self.folded.iter().any(|f| f.start_line == start_line) { - self.folded.push(*candidate); - self.folded.sort_by_key(|r| r.start_line); - self.needs_rebuild = true; - } - } - } else { - // Remove from folded - self.folded.retain(|f| f.start_line != start_line); - self.needs_rebuild = true; - } - } - - /// Toggle fold at the given start_line - pub fn toggle_fold(&mut self, start_line: usize) { - let is_folded = self.is_folded_at(start_line); - self.set_folded(start_line, !is_folded); - } - - /// Check if a line is currently folded - pub fn is_folded_at(&self, start_line: usize) -> bool { - self.folded.iter().any(|f| f.start_line == start_line) - } - - /// Check if a line is a fold candidate - pub fn is_fold_candidate(&self, start_line: usize) -> bool { - self.candidates.iter().any(|c| c.start_line == start_line) - } - - /// Get all fold candidates - #[inline] - pub fn fold_candidates(&self) -> &[FoldRange] { - &self.candidates - } - - /// Get all currently folded ranges - #[inline] - pub fn folded_ranges(&self) -> &[FoldRange] { - &self.folded - } - - /// Clear all folds - #[inline] - pub fn clear_folds(&mut self) { - self.folded.clear(); - } - - /// Adjust folds and candidates after a text edit. - /// - /// - Folds/candidates overlapping the edited line range are removed - /// - Folds/candidates after the edit are shifted by line_delta - /// - /// This avoids expensive full tree traversal on every keystroke. - pub fn adjust_folds_for_edit( - &mut self, - edit_start_line: usize, - edit_end_line: usize, - line_delta: isize, - ) { - // Adjust folded ranges - if !self.folded.is_empty() { - self.folded.retain(|fold| { - !(fold.start_line <= edit_end_line && fold.end_line >= edit_start_line) - }); - - if line_delta != 0 { - for fold in &mut self.folded { - if fold.start_line > edit_end_line { - fold.start_line = (fold.start_line as isize + line_delta).max(0) as usize; - fold.end_line = (fold.end_line as isize + line_delta).max(0) as usize; - } - } - } - } - - // Adjust candidates the same way - if !self.candidates.is_empty() { - self.candidates - .retain(|c| !(c.start_line <= edit_end_line && c.end_line >= edit_start_line)); - - if line_delta != 0 { - for c in &mut self.candidates { - if c.start_line > edit_end_line { - c.start_line = (c.start_line as isize + line_delta).max(0) as usize; - c.end_line = (c.end_line as isize + line_delta).max(0) as usize; - } - } - } - } - - self.needs_rebuild = true; - } - - /// Rebuild the fold mapping after wrap_map or fold state changes - /// - /// This is the core algorithm that projects wrap rows to display rows. - pub fn rebuild(&mut self, wrap_map: &WrapMap) { - let wrap_row_count = wrap_map.wrap_row_count(); - - // Performance optimization: skip rebuild if nothing changed - if !self.needs_rebuild && wrap_row_count == self.cached_wrap_row_count { - return; - } - - self.cached_wrap_row_count = wrap_row_count; - - self.visible_wrap_rows.clear(); - self.wrap_row_to_display_row = vec![None; wrap_row_count]; - - if self.folded.is_empty() { - // Fast path: no folds, all wrap rows are visible - self.visible_wrap_rows = (0..wrap_row_count).collect(); - for (display_row, &wrap_row) in self.visible_wrap_rows.iter().enumerate() { - self.wrap_row_to_display_row[wrap_row] = Some(display_row); - } - self.needs_rebuild = false; - return; - } - - // Build set of hidden wrap_row ranges from folded buffer lines - let mut hidden_ranges = Vec::new(); - for fold in &self.folded { - // Hide wrap rows from (start_line + 1) to (end_line - 1) (inclusive) - // Both the first line and last line of the fold remain visible - let hide_start_line = fold.start_line + 1; - let hide_end_line = fold.end_line.saturating_sub(1); - - if hide_start_line > hide_end_line { - continue; // No middle lines to hide (0 or 1 lines between start and end) - } - - // Get wrap_row ranges for the hidden buffer lines - let start_wrap_row = wrap_map.buffer_line_to_first_wrap_row(hide_start_line); - let end_wrap_row = if hide_end_line + 1 < wrap_map.buffer_line_count() { - wrap_map.buffer_line_to_first_wrap_row(hide_end_line + 1) - } else { - wrap_row_count - }; - - if start_wrap_row < end_wrap_row { - hidden_ranges.push(start_wrap_row..end_wrap_row); - } - } - - // Merge overlapping hidden ranges - hidden_ranges.sort_by_key(|r| r.start); - let mut merged_hidden = Vec::new(); - for range in hidden_ranges { - if let Some(last) = merged_hidden.last_mut() { - if range.start <= *last { - // Overlapping or adjacent, merge - *last = (*last).max(range.end); - } else { - merged_hidden.push(range.start); - merged_hidden.push(range.end); - } - } else { - merged_hidden.push(range.start); - merged_hidden.push(range.end); - } - } - - // Scan all wrap rows and filter out hidden ones - let mut display_row = 0; - let mut hidden_iter = merged_hidden.chunks_exact(2); - let mut current_hidden = hidden_iter.next(); - - for wrap_row in 0..wrap_row_count { - // Check if wrap_row is in current hidden range - let is_hidden = if let Some(&[start, end]) = current_hidden { - if wrap_row >= end { - current_hidden = hidden_iter.next(); - if let Some(&[new_start, new_end]) = current_hidden { - wrap_row >= new_start && wrap_row < new_end - } else { - false - } - } else { - wrap_row >= start && wrap_row < end - } - } else { - false - }; - - if !is_hidden { - self.visible_wrap_rows.push(wrap_row); - self.wrap_row_to_display_row[wrap_row] = Some(display_row); - display_row += 1; - } - } - - self.needs_rebuild = false; - } -} diff --git a/crates/ui/src/input/display_map/folding.rs b/crates/ui/src/input/display_map/folding.rs deleted file mode 100644 index 3c711fc..0000000 --- a/crates/ui/src/input/display_map/folding.rs +++ /dev/null @@ -1,96 +0,0 @@ -use std::ops::Range; - -#[cfg(not(target_family = "wasm"))] -use tree_sitter::Node; -#[cfg(not(target_family = "wasm"))] -pub use tree_sitter::Tree; - -#[cfg(target_family = "wasm")] -/// Stub type for tree-sitter Tree on WASM (tree-sitter not available). -pub struct Tree; - -#[cfg(not(target_family = "wasm"))] -/// Minimum line span for a node to be considered foldable. -const MIN_FOLD_LINES: usize = 2; - -/// A fold range representing a foldable code region. -/// -/// The fold range spans from start_line to end_line (inclusive). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct FoldRange { - /// Start line (inclusive) - pub start_line: usize, - /// End line (inclusive) - pub end_line: usize, -} - -impl FoldRange { - pub fn new(start_line: usize, end_line: usize) -> Self { - assert!( - start_line <= end_line, - "fold start_line must be <= end_line" - ); - Self { - start_line, - end_line, - } - } -} - -#[cfg(not(target_family = "wasm"))] -/// Check if a named node qualifies as a fold candidate. -/// -/// Uses a structural heuristic: any **named** node spanning ≥ MIN_FOLD_LINES -/// is foldable. tree-sitter already parses code into semantic units (functions, -/// classes, blocks, etc.), so named nodes naturally correspond to meaningful -/// foldable regions across all languages without a per-language node-type list. -fn is_foldable_node(node: &Node) -> bool { - let start = node.start_position().row; - let end = node.end_position().row; - end.saturating_sub(start) >= MIN_FOLD_LINES -} - -#[cfg(not(target_family = "wasm"))] -/// Extract fold ranges only within a byte range (for incremental updates after edits). -/// -/// Skips subtrees entirely outside the range, making it O(nodes in range) -/// instead of O(all nodes in tree). -pub fn extract_fold_ranges_in_range(tree: &Tree, byte_range: Range) -> Vec { - let mut ranges = Vec::new(); - let root = tree.root_node(); - let mut cursor = root.walk(); - // Skip the root, it's not foldable. Use named_children to skip literal tokens. - for child in root.named_children(&mut cursor) { - collect_foldable_nodes_in_range(child, &byte_range, &mut ranges); - } - - ranges.sort_by_key(|r| r.start_line); - ranges.dedup_by_key(|r| r.start_line); - ranges -} - -#[cfg(not(target_family = "wasm"))] -/// Recursively collect foldable nodes, skipping subtrees outside byte_range. -fn collect_foldable_nodes_in_range( - node: Node, - byte_range: &Range, - ranges: &mut Vec, -) { - if node.end_byte() <= byte_range.start || node.start_byte() >= byte_range.end { - return; - } - - if !is_foldable_node(&node) { - return; - } - - ranges.push(FoldRange { - start_line: node.start_position().row, - end_line: node.end_position().row, - }); - - let mut cursor = node.walk(); - for child in node.named_children(&mut cursor) { - collect_foldable_nodes_in_range(child, byte_range, ranges); - } -} diff --git a/crates/ui/src/input/display_map/mod.rs b/crates/ui/src/input/display_map/mod.rs index 11738a5..486b3e5 100644 --- a/crates/ui/src/input/display_map/mod.rs +++ b/crates/ui/src/input/display_map/mod.rs @@ -1,61 +1,7 @@ #[allow(clippy::module_inception)] mod display_map; -mod fold_map; -#[cfg(not(target_family = "wasm"))] -mod folding; -#[cfg(target_family = "wasm")] -pub mod folding; mod text_wrapper; mod wrap_map; -// Re-export public API -// Re-export FoldRange and extract_fold_ranges -pub use folding::FoldRange; - pub use self::display_map::DisplayMap; pub(crate) use self::text_wrapper::LineLayout; - -/// Position in the buffer (logical text). -/// -/// - `line`: 0-based logical line number (split by `\n`) -/// - `col`: 0-based column offset (byte offset) -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct BufferPoint { - pub line: usize, - pub col: usize, -} - -impl BufferPoint { - pub fn new(line: usize, col: usize) -> Self { - Self { line, col } - } -} - -/// Position after soft-wrapping but before folding (internal). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub(super) struct WrapPoint { - pub row: usize, - pub col: usize, -} - -impl WrapPoint { - pub fn new(row: usize, col: usize) -> Self { - Self { row, col } - } -} - -/// Final display position (after soft-wrapping and folding). -/// -/// - `row`: 0-based display row (final visible row) -/// - `col`: 0-based display column -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct DisplayPoint { - pub row: usize, - pub col: usize, -} - -impl DisplayPoint { - pub fn new(row: usize, col: usize) -> Self { - Self { row, col } - } -} diff --git a/crates/ui/src/input/display_map/wrap_map.rs b/crates/ui/src/input/display_map/wrap_map.rs index a93cf72..ac422fc 100644 --- a/crates/ui/src/input/display_map/wrap_map.rs +++ b/crates/ui/src/input/display_map/wrap_map.rs @@ -9,10 +9,7 @@ use std::ops::Range; use gpui::{App, Font, Pixels}; use ropey::Rope; -use super::fold_map::FoldMap; -use super::text_wrapper::{LineItem, TextWrapper, WrapDisplayPoint}; -use super::{BufferPoint, WrapPoint}; -use crate::input::rope_ext::RopeExt; +use super::text_wrapper::{LineItem, TextWrapper}; /// WrapMap manages soft-wrapping and provides buffer ↔ wrap coordinate mapping. pub struct WrapMap { @@ -55,49 +52,6 @@ impl WrapMap { self.wrapper.lines.len() } - /// Convert buffer position to wrap position - pub(super) fn buffer_pos_to_wrap_pos(&self, pos: BufferPoint) -> WrapPoint { - let BufferPoint { line, col } = pos; - - // Clamp to valid range - let line = line.min(self.buffer_line_count().saturating_sub(1)); - let line_item = self.wrapper.lines.get(line); - - let col = if let Some(line_item) = line_item { - col.min(line_item.len()) - } else { - 0 - }; - - // Calculate offset in rope - let line_start_offset = self.wrapper.text().line_start_offset(line); - let offset = line_start_offset + col; - - // Use TextWrapper's existing conversion - let display_point = self.wrapper.offset_to_display_point(offset); - - WrapPoint::new(display_point.row, display_point.column) - } - - /// Convert wrap position to buffer position - pub(super) fn wrap_pos_to_buffer_pos(&self, pos: WrapPoint) -> BufferPoint { - let WrapPoint { row, col } = pos; - - // Clamp wrap_row to valid range - let row = row.min(self.wrap_row_count().saturating_sub(1)); - - // Use TextWrapper's existing conversion - let display_point = WrapDisplayPoint::new(row, 0, col); - let offset = self.wrapper.display_point_to_offset(display_point); - - // Convert offset to buffer position - let point = self.wrapper.text().offset_to_point(offset); - let line_start = self.wrapper.text().line_start_offset(point.row); - let col = offset.saturating_sub(line_start); - - BufferPoint::new(point.row, col) - } - /// Get the buffer line for a given wrap row pub fn wrap_row_to_buffer_line(&self, wrap_row: usize) -> usize { if wrap_row >= self.wrap_row_count() { @@ -176,8 +130,6 @@ impl WrapMap { let wrap_row_count = self.wrapper.len(); // Skip if nothing changed: both buffer line count and total wrap row count must match. - // Checking wrap_row_count is essential because soft-wrap can change the number of - // wrap rows per line without changing the buffer line count. if line_count == self.cached_line_count && wrap_row_count == self.cached_wrap_row_count && !self.buffer_line_starts.is_empty() @@ -212,11 +164,9 @@ impl WrapMap { self.wrapper.text() } - /// Calculate how many wrap rows of a buffer line are visible (not folded) - pub fn visible_wrap_row_count_for_line(&self, line: usize, fold_map: &FoldMap) -> usize { - let wrap_range = self.buffer_line_to_wrap_row_range(line); - wrap_range - .filter(|&wr| fold_map.wrap_row_to_display_row(wr).is_some()) - .count() + /// Calculate how many wrap rows of a buffer line are visible. + /// Without folding, all wrap rows are visible. + pub fn visible_wrap_row_count_for_buffer_line(&self, line: usize) -> usize { + self.buffer_line_to_wrap_row_range(line).len() } } diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs index 8367e27..a19c27f 100644 --- a/crates/ui/src/input/element.rs +++ b/crates/ui/src/input/element.rs @@ -3,9 +3,8 @@ use std::rc::Rc; use gpui::{ AnyElement, App, Bounds, Corners, Edges, Element, ElementId, ElementInputHandler, Entity, - GlobalElementId, Half, HighlightStyle, Hitbox, HitboxBehavior, Hsla, InteractiveElement, - IntoElement, LayoutId, MouseButton, MouseMoveEvent, MouseUpEvent, Path, Pixels, Point, - Position, ShapedLine, SharedString, Size, Style, Styled as _, TextAlign, TextRun, TextStyle, + GlobalElementId, Half, Hsla, IntoElement, LayoutId, MouseButton, MouseMoveEvent, MouseUpEvent, + Path, Pixels, Point, Position, SharedString, Size, Style, TextAlign, TextRun, TextStyle, UnderlineStyle, Window, fill, point, px, relative, size, }; use ropey::Rope; @@ -14,19 +13,14 @@ use theme::ActiveTheme; use super::mode::InputMode; use super::{InputState, LastLayout, WhitespaceIndicators}; -use crate::button::{Button, ButtonVariants as _}; +use crate::Root; use crate::input::RopeExt as _; use crate::input::blink_cursor::CURSOR_WIDTH; use crate::input::display_map::LineLayout; use crate::scroll::Scrollbar; -use crate::{IconName, Root, Selectable, Sizable as _}; const BOTTOM_MARGIN_ROWS: usize = 3; pub(super) const RIGHT_MARGIN: Pixels = px(10.); -pub(super) const LINE_NUMBER_RIGHT_MARGIN: Pixels = px(10.); -const FOLD_ICON_WIDTH: Pixels = px(14.); -const FOLD_ICON_HITBOX_WIDTH: Pixels = px(18.); -const MAX_HIGHLIGHT_LINE_LENGTH: usize = 10_000; #[derive(Clone, Copy, Debug, PartialEq)] struct EditorScrollbarLayout { @@ -72,7 +66,7 @@ impl EditorScrollbarLayout { let left = if line_number_width == px(0.) { px(0.) } else { - paddings.left + line_number_width - LINE_NUMBER_RIGHT_MARGIN + paddings.left + line_number_width }; Self { @@ -216,14 +210,6 @@ fn masked_display_offset(text: &Rope, original_offset: usize) -> usize { text.offset_to_char_index(original_offset) * MASK_CHAR.len_utf8() } -/// Layout information for fold icons. -struct FoldIconLayout { - /// Hitbox for the line number area (used for hover detection) - line_number_hitbox: Hitbox, - /// List of (display_row, is_folded, icon_element) pairs for each fold candidate - icons: Vec<(usize, bool, gpui::AnyElement)>, -} - pub(super) struct TextElement { pub(crate) state: Entity, placeholder: SharedString, @@ -285,7 +271,7 @@ impl TextElement { scroll_size: Size, _: &mut Window, cx: &mut App, - ) -> (Option>, Point, Option) { + ) -> (Option>, Point) { let state = self.state.read(cx); let line_height = last_layout.line_height; @@ -307,7 +293,6 @@ impl TextElement { cursor = masked_display_offset(&state.text, cursor); } - let mut current_row = None; let mut scroll_offset = state.scroll_handle.offset(); let mut cursor_bounds = None; @@ -330,7 +315,6 @@ impl TextElement { let visible_buffer_lines = &last_layout.visible_buffer_lines; let mut vi = 0; // index into visible_buffer_lines / lines for (ix, wrap_line) in buffer_lines.iter().enumerate() { - let row = ix; let line_origin = point(px(0.), offset_y); // break loop if all cursor positions are found @@ -353,7 +337,6 @@ impl TextElement { if let Some(pos) = line.position_for_index(offset, last_layout, state.cursor_line_end_affinity) { - current_row = Some(row); cursor_pos = Some(line_origin + pos); } } @@ -377,7 +360,6 @@ impl TextElement { // Not visible (before visible range or hidden/folded). // Just increase the offset_y and prev_lines_offset for scroll tracking. if prev_lines_offset >= cursor && cursor_pos.is_none() { - current_row = Some(row); cursor_pos = Some(line_origin); } if prev_lines_offset >= selected_range.start && cursor_start.is_none() { @@ -494,7 +476,7 @@ impl TextElement { bounds.origin += scroll_offset; - (cursor_bounds, scroll_offset, current_row) + (cursor_bounds, scroll_offset) } /// Layout the match range to a Path. @@ -642,41 +624,6 @@ impl TextElement { builder.build().ok() } - fn layout_search_matches( - &self, - _last_layout: &LastLayout, - _bounds: &Bounds, - _cx: &mut App, - ) -> Vec<(Path, bool)> { - vec![] - } - - fn layout_hover_highlight( - &self, - _last_layout: &LastLayout, - _bounds: &Bounds, - _cx: &mut App, - ) -> Option> { - None - } - - fn layout_document_colors( - &self, - document_colors: &[(Range, Hsla)], - last_layout: &LastLayout, - bounds: &Bounds, - _cx: &mut App, - ) -> Vec<(Path, Hsla)> { - let mut paths = vec![]; - for (range, color) in document_colors.iter() { - if let Some(path) = Self::layout_match_range(range.clone(), last_layout, bounds) { - paths.push((path, *color)); - } - } - - paths - } - fn layout_selections( &self, last_layout: &LastLayout, @@ -784,267 +731,20 @@ impl TextElement { (visible_range, visible_buffer_lines, visible_top) } - /// Return (line_number_width, line_number_len) - fn layout_line_numbers( - state: &InputState, - text: &Rope, - font_size: Pixels, - style: &TextStyle, - window: &mut Window, - ) -> (Pixels, usize) { - let total_lines = text.lines_len(); - let line_number_len = match total_lines { - 0..=9999 => 5, - 10000..=99999 => 6, - 100000..=999999 => 7, - _ => 8, - }; - - let mut line_number_width = if state.mode.line_number() { - let empty_line_number = window.text_system().shape_line( - "+".repeat(line_number_len).into(), - font_size, - &[TextRun { - len: line_number_len, - font: style.font(), - color: gpui::black(), - background_color: None, - underline: None, - strikethrough: None, - }], - None, - ); - - empty_line_number.width + LINE_NUMBER_RIGHT_MARGIN - } else if state.mode.is_code_editor() && state.mode.is_multi_line() { - LINE_NUMBER_RIGHT_MARGIN - } else { - px(0.) - }; - - if state.mode.is_folding() { - // Add extra space for fold icons - line_number_width += FOLD_ICON_HITBOX_WIDTH - } - - (line_number_width, line_number_len) - } - /// Layout shaped lines for whitespace indicators (space and tab). /// /// Returns `WhitespaceIndicators` with shaped lines for space and tab characters. fn layout_whitespace_indicators( - state: &InputState, + _state: &InputState, text_size: Pixels, style: &TextStyle, window: &mut Window, cx: &App, ) -> Option { - if !state.show_whitespaces { - return None; - } - - let invisible_color = cx.theme().text_muted; - let space_font_size = text_size.half(); - let tab_font_size = text_size; - - let space_text = SharedString::new_static("•"); - let space = window.text_system().shape_line( - space_text.clone(), - space_font_size, - &[TextRun { - len: space_text.len(), - font: style.font(), - color: invisible_color, - background_color: None, - underline: None, - strikethrough: None, - }], - None, - ); - - let tab_text = SharedString::new_static("→"); - let tab = window.text_system().shape_line( - tab_text.clone(), - tab_font_size, - &[TextRun { - len: tab_text.len(), - font: style.font(), - color: invisible_color, - background_color: None, - underline: None, - strikethrough: None, - }], - None, - ); - - Some(WhitespaceIndicators { space, tab }) - } - - /// Compute inline completion ghost lines for rendering. - /// - /// Returns (first_line, ghost_lines) where: - /// - first_line: Shaped text for the first line (goes after cursor on same line) - /// - ghost_lines: Shaped lines for subsequent lines (shift content down) - fn layout_inline_completion( - _state: &InputState, - _visible_range: &Range, - _font_size: Pixels, - _window: &mut Window, - _cx: &App, - ) -> (Option, Vec) { - (None, vec![]) - } - - /// Return (line_number_width, line_number_len) - /// Layout fold icon hitboxes during prepaint phase. - /// - /// This creates hitboxes for the fold icon area, positioned to the right of line numbers. - /// Icons are created and prepainted here to avoid panics. - fn layout_fold_icons( - &self, - origin_x: Pixels, - bounds: &Bounds, - last_layout: &LastLayout, - window: &mut Window, - cx: &mut App, - ) -> FoldIconLayout { - // First pass: collect fold information from state - struct FoldInfo { - buffer_line: usize, - is_folded: bool, - display_row: usize, - offset_y: Pixels, - } - - let line_number_hitbox = window.insert_hitbox( - Bounds::new( - point(origin_x, bounds.origin.y + last_layout.visible_top), - size(last_layout.line_number_width, bounds.size.height), - ), - HitboxBehavior::Normal, - ); - - let mut icon_layout = FoldIconLayout { - line_number_hitbox, - icons: vec![], - }; - - let fold_infos: Vec = { - let state = self.state.read(cx); - if !state.mode.is_folding() { - return icon_layout; - } - - let mut infos = Vec::with_capacity(last_layout.visible_buffer_lines.len()); - let mut offset_y = last_layout.visible_top; - - for (line, &buffer_line) in last_layout - .lines - .iter() - .zip(last_layout.visible_buffer_lines.iter()) - { - if state.display_map.is_fold_candidate(buffer_line) { - let is_folded = state.display_map.is_folded_at(buffer_line); - infos.push(FoldInfo { - buffer_line, - is_folded, - display_row: buffer_line, - offset_y, - }); - } - - offset_y += line.wrapped_lines.len() * last_layout.line_height; - } - - infos - }; // state is dropped here - - // Second pass: create and prepaint icons - let line_height = last_layout.line_height; - let line_number_width = - last_layout.line_number_width - LINE_NUMBER_RIGHT_MARGIN - FOLD_ICON_HITBOX_WIDTH; - let icon_relative_pos = point( - (FOLD_ICON_HITBOX_WIDTH - FOLD_ICON_WIDTH).half(), - (line_height - FOLD_ICON_WIDTH).half(), - ); - - for (ix, info) in fold_infos.iter().enumerate() { - // Position fold icon to the right of line numbers. - // Use origin_x (unscrolled) so icons stay fixed in the gutter during horizontal scroll. - let fold_icon_bounds = Bounds::new( - point( - origin_x + icon_relative_pos.x + line_number_width, - bounds.origin.y + icon_relative_pos.y + info.offset_y, - ), - size(FOLD_ICON_HITBOX_WIDTH, line_height), - ); - - // Create and prepaint icon - let mut icon = Button::new(("fold", ix)) - .ghost() - .icon(if info.is_folded { - IconName::CaretRight - } else { - IconName::CaretDown - }) - .xsmall() - .rounded_xs() - .size(FOLD_ICON_WIDTH) - .selected(info.is_folded) - .on_mouse_down(MouseButton::Left, { - let state = self.state.clone(); - let buffer_line = info.buffer_line; - move |_, _: &mut Window, cx: &mut App| { - cx.stop_propagation(); - - state.update(cx, |state, cx| { - state.display_map.toggle_fold(buffer_line); - cx.notify(); - }); - } - }) - .into_any_element(); - - icon.prepaint_as_root( - fold_icon_bounds.origin, - fold_icon_bounds.size.into(), - window, - cx, - ); - - icon_layout - .icons - .push((info.display_row, info.is_folded, icon)); - } - - icon_layout - } - - /// Paint fold icons using prepaint hitboxes. - /// - /// This handles: - /// - Rendering fold icons (chevron-right for folded, chevron-down for expanded) - /// - Mouse click handling to toggle fold state - /// - Cursor style changes on hover - /// - Only show icon on hover or for current line - fn paint_fold_icons( - &mut self, - fold_icon_layout: &mut FoldIconLayout, - current_row: Option, - window: &mut Window, - cx: &mut App, - ) { - let is_hovered = fold_icon_layout.line_number_hitbox.is_hovered(window); - for (display_row, is_folded, icon) in fold_icon_layout.icons.iter_mut() { - let is_current_line = current_row == Some(*display_row); - - if !is_hovered && !is_current_line && !*is_folded { - continue; - } - - icon.paint(window, cx); - } + // Whitespace indicators are not currently enabled. + // When re-enabled, check `state.show_whitespaces` to conditionally enable. + let _ = (text_size, style, window, cx); + None } #[allow(clippy::too_many_arguments)] @@ -1098,10 +798,6 @@ impl TextElement { } let mut lines = Vec::with_capacity(last_layout.visible_buffer_lines.len()); - // run_offset tracks position in the runs vec coordinate space (only visible line bytes). - // This is separate from the visible_text offset because runs from highlight_lines - // only cover visible (non-folded) lines. - let mut run_offset = 0; for (vi, &buffer_line) in last_layout.visible_buffer_lines.iter().enumerate() { let line_text: String = display_text.slice_line(buffer_line).into(); @@ -1112,9 +808,10 @@ impl TextElement { debug_assert_eq!(line_item.len(), line_text.len()); let mut wrapped_lines = SmallVec::with_capacity(1); + let line_offset = display_text.line_start_offset(buffer_line); for range in &line_item.wrapped_lines { - let line_runs = runs_for_range(runs, run_offset, range); + let line_runs = runs_for_range(runs, line_offset, range); let line_runs = if bg_segments.is_empty() { line_runs } else { @@ -1137,107 +834,21 @@ impl TextElement { .lines(wrapped_lines) .with_whitespaces(whitespace_indicators.clone()); lines.push(line_layout); - - // +1 for the `\n` - run_offset += line_text.len() + 1; } lines } - - /// First usize is the offset of skipped. - fn highlight_lines( - &mut self, - visible_buffer_lines: &[usize], - _visible_top: Pixels, - _visible_byte_range: Range, - cx: &mut App, - ) -> Option, HighlightStyle)>> { - let state = self.state.read(cx); - let text = &state.text; - let is_multi_line = state.mode.is_multi_line(); - - let mut styles = Vec::with_capacity(visible_buffer_lines.len()); - - // Helper to flush a contiguous range of lines. These ranges are disjoint, - // so appending avoids repeatedly cloning and recombining prior styles. - let flush_range = |start_line: usize, end_line: usize, _skip: bool, styles: &mut Vec<_>| { - let byte_start = text.line_start_offset(start_line); - let byte_end = if is_multi_line { - // +1 for `\n` - text.line_start_offset(end_line + 1) - } else { - text.line_end_offset(end_line) - }; - let range_styles = vec![(byte_start..byte_end, HighlightStyle::default())]; - styles.extend(range_styles); - }; - - // Group contiguous visible lines into ranges and call styles() once per range - let mut visible_iter = visible_buffer_lines.iter().peekable(); - let mut range_start: Option = None; - - while let Some(&line) = visible_iter.next() { - // Check if this line is too long for highlighting - let line_len = text.slice_line(line).len(); - if line_len > MAX_HIGHLIGHT_LINE_LENGTH { - // Flush any accumulated range first - if let Some(start) = range_start.take() { - flush_range(start, line - 1, false, &mut styles); - } - - flush_range(line, line, true, &mut styles); - continue; - } - - range_start.get_or_insert(line); - - // Check if next line is contiguous, if so keep accumulating - if visible_iter - .peek() - .map(|&&next| next == line + 1) - .unwrap_or(false) - { - continue; - } - - // Flush the contiguous range - let start_line = range_start.take().unwrap(); - flush_range(start_line, line, false, &mut styles); - } - - Some(styles) - } } pub(super) struct PrepaintState { /// The lines of entire lines. last_layout: LastLayout, - /// The lines only contains the visible lines in the viewport, based on `visible_range`. - /// - /// The child is the soft lines. - line_numbers: Option>>, /// Size of the scrollable area by entire lines. scroll_size: Size, cursor_bounds: Option>, cursor_scroll_offset: Point, - /// row index (zero based), no wrap, same line as the cursor. - current_row: Option, selection_path: Option>, - hover_highlight_path: Option>, - search_match_paths: Vec<(Path, bool)>, - document_color_paths: Vec<(Path, Hsla)>, - hover_definition_hitbox: Option, - indent_guides_path: Option>, bounds: Bounds, - /// Fold icon layout data - fold_icon_layout: FoldIconLayout, - // Inline completion rendering data - /// Shaped ghost lines to paint after cursor row (completion lines 2+) - ghost_lines: Vec, - /// First line of inline completion (painted after cursor on same line) - ghost_first_line: Option, - ghost_lines_height: Pixels, } impl PrepaintState { @@ -1356,13 +967,6 @@ impl Element for TextElement { .text .line_end_offset(visible_range.end.saturating_sub(1)); - let highlight_styles = self.highlight_lines( - &visible_buffer_lines, - visible_top, - visible_start_offset..visible_end_offset, - cx, - ); - let state = self.state.read(cx); let multi_line = state.mode.is_multi_line(); let text = state.text.clone(); @@ -1382,9 +986,8 @@ impl Element for TextElement { (&text, fg) }; - // Calculate the width of the line numbers - let (line_number_width, line_number_len) = - Self::layout_line_numbers(state, &text, text_size, &text_style, window); + // Line numbers are not used (code editor mode removed) + let line_number_width = px(0.); let mut bounds = bounds; let wrap_width = if multi_line && state.soft_wrap { @@ -1452,28 +1055,7 @@ impl Element for TextElement { }; let runs = if !is_empty { - if let Some(highlight_styles) = highlight_styles { - let mut runs = Vec::with_capacity(highlight_styles.len()); - - runs.extend(highlight_styles.iter().map(|(range, style)| { - let mut run = text_style.clone().highlight(*style).to_run(range.len()); - - if let Some(ime_marked_range) = &state.ime_marked_range - && range.start >= ime_marked_range.start - && range.end <= ime_marked_range.end - { - run.color = marked_run.color; - run.strikethrough = marked_run.strikethrough; - run.underline = marked_run.underline; - } - - run - })); - - runs.into_iter().filter(|run| run.len > 0).collect() - } else { - vec![run] - } + vec![run] } else if let Some(ime_marked_range) = &state.ime_marked_range { // IME marked text vec![ @@ -1498,8 +1080,6 @@ impl Element for TextElement { vec![run] }; - let document_colors = []; - // Create shaped lines for whitespace indicators before layout let whitespace_indicators = Self::layout_whitespace_indicators(state, text_size, &text_style, window, cx); @@ -1510,7 +1090,7 @@ impl Element for TextElement { &last_layout, text_size, &runs, - &document_colors, + &[], whitespace_indicators, window, ); @@ -1540,26 +1120,8 @@ impl Element for TextElement { } last_layout.lines = Rc::new(lines); - let (ghost_first_line, ghost_lines) = Self::layout_inline_completion( - state, - &last_layout.visible_range, - text_size, - window, - cx, - ); - let ghost_line_count = ghost_lines.len(); - let ghost_lines_height = ghost_line_count as f32 * line_height; - let total_wrapped_lines = state.display_map.wrap_row_count(); - let empty_bottom_height = if state.mode.is_code_editor() { - bounds - .size - .height - .half() - .max(BOTTOM_MARGIN_ROWS * line_height) - } else { - px(0.) - }; + let empty_bottom_height = px(0.); let mut scroll_size = size( if longest_line_width + line_number_width + RIGHT_MARGIN > bounds.size.width { @@ -1567,7 +1129,7 @@ impl Element for TextElement { } else { longest_line_width }, - (total_wrapped_lines as f32 * line_height + empty_bottom_height + ghost_lines_height) + (total_wrapped_lines as f32 * line_height + empty_bottom_height) .max(bounds.size.height), ); @@ -1608,75 +1170,16 @@ impl Element for TextElement { // Calculate the scroll offset to keep the cursor in view - // Save the unscrolled x before layout_cursor modifies bounds.origin with scroll_offset. - // Fold icons and their hitboxes must use this value so they stay fixed in the gutter - // regardless of horizontal scroll position. + // Save the bounds before layout_cursor modifies bounds.origin with scroll_offset. let input_bounds = bounds; - let original_x = bounds.origin.x; - let (cursor_bounds, cursor_scroll_offset, current_row) = + let (cursor_bounds, cursor_scroll_offset) = self.layout_cursor(&last_layout, &mut bounds, scroll_size, window, cx); last_layout.cursor_bounds = cursor_bounds; - let search_match_paths = self.layout_search_matches(&last_layout, &bounds, cx); let selection_path = self.layout_selections(&last_layout, &mut bounds, window, cx); - let hover_highlight_path = self.layout_hover_highlight(&last_layout, &bounds, cx); - let document_color_paths = - self.layout_document_colors(&document_colors, &last_layout, &bounds, cx); let state = self.state.read(cx); - let line_numbers = if state.mode.line_number() { - let mut line_numbers = Vec::with_capacity(last_layout.visible_buffer_lines.len()); - let other_line_runs = vec![TextRun { - len: line_number_len, - font: style.font(), - color: cx.theme().text_muted, - background_color: None, - underline: None, - strikethrough: None, - }]; - let current_line_runs = vec![TextRun { - len: line_number_len, - font: style.font(), - color: cx.theme().text, - background_color: None, - underline: None, - strikethrough: None, - }]; - - // build line numbers - for (line, &buffer_line) in last_layout - .lines - .iter() - .zip(last_layout.visible_buffer_lines.iter()) - { - let line_no: SharedString = - format!("{:>width$}", buffer_line + 1, width = line_number_len).into(); - - let runs = if current_row == Some(buffer_line) { - ¤t_line_runs - } else { - &other_line_runs - }; - - let mut sub_lines: SmallVec<[ShapedLine; 1]> = SmallVec::new(); - sub_lines.push( - window - .text_system() - .shape_line(line_no, text_size, runs, None), - ); - for _ in 0..line.wrapped_lines.len().saturating_sub(1) { - sub_lines.push(ShapedLine::default()); - } - line_numbers.push(sub_lines); - } - Some(line_numbers) - } else { - None - }; - - let indent_guides_path = - self.layout_indent_guides(state, &bounds, &last_layout, &text_style, window); state .editor_scrollbar_snapshot @@ -1688,27 +1191,13 @@ impl Element for TextElement { state, ))); - let fold_icon_layout = - self.layout_fold_icons(original_x, &bounds, &last_layout, window, cx); - PrepaintState { bounds, last_layout, scroll_size, - line_numbers, cursor_bounds, cursor_scroll_offset, - current_row, selection_path, - search_match_paths, - hover_highlight_path, - hover_definition_hitbox: None, - document_color_paths, - indent_guides_path, - fold_icon_layout, - ghost_first_line, - ghost_lines, - ghost_lines_height, } } @@ -1765,56 +1254,15 @@ impl Element for TextElement { let invisible_top_padding = prepaint.last_layout.visible_top; - // Paint active line - let mut offset_y = px(0.); - if let Some(line_numbers) = prepaint.line_numbers.as_ref() { - offset_y += invisible_top_padding; - - // Each item is the normal lines. - for (lines, _) in line_numbers - .iter() - .zip(prepaint.last_layout.visible_buffer_lines.iter()) - { - let height = line_height * lines.len() as f32; - offset_y += height; - } - } - - // Paint indent guides - if let Some(path) = prepaint.indent_guides_path.take() { - window.paint_path(path, cx.theme().border.opacity(0.85)); - } - // Paint selections - if window.is_window_active() { - let secondary_selection = cx.theme().selection; - for (path, is_active) in prepaint.search_match_paths.iter() { - window.paint_path(path.clone(), secondary_selection); - - if *is_active { - window.paint_path(path.clone(), cx.theme().selection); - } - } - - if let Some(path) = prepaint.selection_path.take() { - window.paint_path(path, cx.theme().selection); - } - - // Paint hover highlight - if let Some(path) = prepaint.hover_highlight_path.take() { - window.paint_path(path, secondary_selection); - } + if window.is_window_active() + && let Some(path) = prepaint.selection_path.take() + { + window.paint_path(path, cx.theme().selection); } - // Paint document colors - for (path, color) in prepaint.document_color_paths.iter() { - window.paint_path(path.clone(), *color); - } - - // Paint text with inline completion ghost line support + // Paint text let mut offset_y = invisible_top_padding; - let ghost_lines = &prepaint.ghost_lines; - let has_ghost_lines = !ghost_lines.is_empty(); // Keep scrollbar offset always be positive,Start from the left position let scroll_offset = if text_align == TextAlign::Right { @@ -1827,16 +1275,12 @@ impl Element for TextElement { px(0.) }; - // Track the y-position of the cursor row for positioning the first line suffix - let mut cursor_row_y = None; - - for (line, &buffer_line) in prepaint + for (line, _buffer_line) in prepaint .last_layout .lines .iter() .zip(prepaint.last_layout.visible_buffer_lines.iter()) { - let row = buffer_line; let line_y = origin.y + offset_y; let p = point( origin.x + prepaint.last_layout.line_number_width + (scroll_offset), @@ -1853,40 +1297,6 @@ impl Element for TextElement { cx, ); offset_y += line.size(line_height).height; - - if Some(row) == prepaint.current_row { - cursor_row_y = Some(line_y); - } - - // After the cursor row, paint ghost lines (which shifts subsequent content down) - if has_ghost_lines && Some(row) == prepaint.current_row { - let ghost_x = origin.x + prepaint.last_layout.line_number_width; - - for ghost_line in ghost_lines { - let ghost_p = point(ghost_x, origin.y + offset_y); - - // Paint semi-transparent background for ghost line - let ghost_bounds = Bounds::new( - ghost_p, - size( - bounds.size.width - prepaint.last_layout.line_number_width, - line_height, - ), - ); - window.paint_quad(fill(ghost_bounds, cx.theme().surface_background)); - - // Paint ghost line text - _ = ghost_line.paint( - ghost_p, - line_height, - text_align, - Some(prepaint.last_layout.content_width), - window, - cx, - ); - offset_y += line_height; - } - } } // Paint blinking cursor @@ -1897,49 +1307,6 @@ impl Element for TextElement { window.paint_quad(fill(cursor_bounds, cx.theme().cursor)); } - // Paint line numbers - let mut offset_y = px(0.); - if let Some(line_numbers) = prepaint.line_numbers.as_ref() { - offset_y += invisible_top_padding; - - window.paint_quad(fill( - Bounds { - origin: input_bounds.origin, - size: size( - prepaint.last_layout.line_number_width - LINE_NUMBER_RIGHT_MARGIN, - input_bounds.size.height + prepaint.ghost_lines_height, - ), - }, - cx.theme().surface_background, - )); - - // Each item is the normal lines. - for (lines, &buffer_line) in line_numbers - .iter() - .zip(prepaint.last_layout.visible_buffer_lines.iter()) - { - let p = point(input_bounds.origin.x, origin.y + offset_y); - - for line in lines { - _ = line.paint(p, line_height, TextAlign::Left, None, window, cx); - offset_y += line_height; - } - - // Add ghost line height after cursor row for line numbers alignment - if !prepaint.ghost_lines.is_empty() && prepaint.current_row == Some(buffer_line) { - offset_y += prepaint.ghost_lines_height; - } - } - } - - // Paint fold icons (only visible on hover or for current line) - self.paint_fold_icons( - &mut prepaint.fold_icon_layout, - prepaint.current_row, - window, - cx, - ); - self.state.update(cx, |state, cx| { state.last_layout = Some(prepaint.last_layout.clone()); state.last_bounds = Some(bounds); @@ -1953,27 +1320,6 @@ impl Element for TextElement { cx.notify(); }); - if let Some(hitbox) = prepaint.hover_definition_hitbox.as_ref() { - window.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox); - } - - // Paint inline completion first line suffix (after cursor on same line) - if focused - && let Some(first_line) = &prepaint.ghost_first_line - && let (Some(cursor_bounds), Some(cursor_row_y)) = - (prepaint.cursor_bounds_with_scroll(), cursor_row_y) - { - let first_line_x = cursor_bounds.origin.x + cursor_bounds.size.width; - let p = point(first_line_x, cursor_row_y); - - // Paint background to cover any existing text - let bg_bounds = Bounds::new(p, size(first_line.width + px(4.), line_height)); - window.paint_quad(fill(bg_bounds, cx.theme().surface_background)); - - // Paint first line completion text - _ = first_line.paint(p, line_height, text_align, None, window, cx); - } - self.paint_mouse_listeners(window, cx); } } diff --git a/crates/ui/src/input/indent.rs b/crates/ui/src/input/indent.rs index 54dd386..2f38e49 100644 --- a/crates/ui/src/input/indent.rs +++ b/crates/ui/src/input/indent.rs @@ -1,12 +1,8 @@ -use gpui::{ - Bounds, Context, EntityInputHandler as _, Hsla, Path, PathBuilder, Pixels, SharedString, - TextRun, TextStyle, Window, point, px, -}; +use gpui::{Context, EntityInputHandler, SharedString, Window}; use ropey::RopeSlice; -use crate::input::element::TextElement; use crate::input::mode::InputMode; -use crate::input::{Indent, IndentInline, InputState, LastLayout, Outdent, OutdentInline, RopeExt}; +use crate::input::{Indent, IndentInline, InputState, Outdent, OutdentInline}; #[derive(Debug, Copy, Clone)] pub struct TabSize { @@ -49,165 +45,14 @@ impl TabSize { } } -impl InputMode { - #[inline] - pub(super) fn is_indentable(&self) -> bool { - match self { - InputMode::PlainText { multi_line, .. } | InputMode::CodeEditor { multi_line, .. } => { - *multi_line - } - _ => false, - } - } - - #[inline] - pub(super) fn has_indent_guides(&self) -> bool { - match self { - InputMode::CodeEditor { - indent_guides, - multi_line, - .. - } => *indent_guides && *multi_line, - _ => false, - } - } - - #[inline] - pub(super) fn tab_size(&self) -> TabSize { - match self { - InputMode::PlainText { tab, .. } => *tab, - InputMode::CodeEditor { tab, .. } => *tab, - _ => TabSize::default(), - } - } -} - -impl TextElement { - /// Measure the indent width in pixels for given column count. - fn measure_indent_width(&self, style: &TextStyle, column: usize, window: &Window) -> Pixels { - let font_size = style.font_size.to_pixels(window.rem_size()); - let layout = window.text_system().shape_line( - SharedString::from(" ".repeat(column)), - font_size, - &[TextRun { - len: column, - font: style.font(), - color: Hsla::default(), - background_color: None, - strikethrough: None, - underline: None, - }], - None, - ); - - layout.width - } - - pub(super) fn layout_indent_guides( - &self, - state: &InputState, - bounds: &Bounds, - last_layout: &LastLayout, - text_style: &TextStyle, - window: &mut Window, - ) -> Option> { - if !state.mode.has_indent_guides() { - return None; - } - - let indent_width = - self.measure_indent_width(text_style, state.mode.tab_size().tab_size, window); - - let tab_size = state.mode.tab_size(); - let line_height = last_layout.line_height; - let mut builder = PathBuilder::stroke(px(1.)); - let mut offset_y = last_layout.visible_top; - let mut last_indents = vec![]; - - for (&buffer_line, line_layout) in last_layout - .visible_buffer_lines - .iter() - .zip(last_layout.lines.iter()) - { - let line = state.text.slice_line(buffer_line); - let mut current_indents = vec![]; - if line.len() > 0 { - let indent_count = tab_size.indent_count(&line); - for offset in (0..indent_count).step_by(tab_size.tab_size) { - let x = if indent_count > 0 { - indent_width * offset as f32 / tab_size.tab_size as f32 - } else { - px(0.) - }; - - let pos = point(x + last_layout.line_number_width, offset_y); - - builder.move_to(pos); - builder.line_to(point(pos.x, pos.y + line_height)); - current_indents.push(pos.x); - } - } else if !last_indents.is_empty() { - for x in &last_indents { - let pos = point(*x, offset_y); - builder.move_to(pos); - builder.line_to(point(pos.x, pos.y + line_height)); - } - current_indents = last_indents.clone(); - } - - offset_y += line_layout.wrapped_lines.len() * line_height; - last_indents = current_indents; - } - - builder.translate(bounds.origin); - let path = builder.build().unwrap(); - Some(path) - } -} - impl InputState { - /// Set whether to show indent guides in code editor mode, default is true. - /// - /// Only for [`InputMode::CodeEditor`] mode. - pub fn indent_guides(mut self, indent_guides: bool) -> Self { - debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line()); - if let InputMode::CodeEditor { - indent_guides: l, .. - } = &mut self.mode - { - *l = indent_guides; - } - self - } - - /// Set indent guides in code editor mode. - /// - /// Only for [`InputMode::CodeEditor`] mode. - pub fn set_indent_guides( - &mut self, - indent_guides: bool, - _: &mut Window, - cx: &mut Context, - ) { - debug_assert!(self.mode.is_code_editor()); - if let InputMode::CodeEditor { - indent_guides: l, .. - } = &mut self.mode - { - *l = indent_guides; - } - cx.notify(); - } - /// Set the tab size for the input. /// - /// Only for [`InputMode::PlainText`] and [`InputMode::CodeEditor`] mode with multi_line. + /// Only for [`InputMode::PlainText`] mode with multi_line. pub fn tab_size(mut self, tab: TabSize) -> Self { - debug_assert!(self.mode.is_multi_line() || self.mode.is_code_editor()); - match &mut self.mode { - InputMode::PlainText { tab: t, .. } => *t = tab, - InputMode::CodeEditor { tab: t, .. } => *t = tab, - _ => {} + debug_assert!(self.mode.is_multi_line()); + if let InputMode::PlainText { tab: t, .. } = &mut self.mode { + *t = tab; } self } diff --git a/crates/ui/src/input/input.rs b/crates/ui/src/input/input.rs index 8187819..47d760a 100644 --- a/crates/ui/src/input/input.rs +++ b/crates/ui/src/input/input.rs @@ -234,12 +234,6 @@ impl RenderOnce for Input { let (bg, _) = input_style(state.disabled, cx); - let bg = if state.mode.is_code_editor() { - cx.theme().surface_background - } else { - bg - }; - let prefix = self.prefix; let suffix = self.suffix; let show_clear_button = self.cleanable diff --git a/crates/ui/src/input/mod.rs b/crates/ui/src/input/mod.rs index 63d5bce..de8e497 100644 --- a/crates/ui/src/input/mod.rs +++ b/crates/ui/src/input/mod.rs @@ -18,9 +18,7 @@ mod state; pub(crate) use clear_button::*; pub use cursor::*; -#[cfg(target_family = "wasm")] -pub use display_map::folding::Tree; -pub use display_map::{BufferPoint, DisplayMap, DisplayPoint, FoldRange}; +pub use display_map::DisplayMap; pub use indent::TabSize; pub use input::*; pub use mask_pattern::MaskPattern; diff --git a/crates/ui/src/input/mode.rs b/crates/ui/src/input/mode.rs index aea247a..30b1f43 100644 --- a/crates/ui/src/input/mode.rs +++ b/crates/ui/src/input/mode.rs @@ -1,26 +1,11 @@ -use std::cell::RefCell; -use std::rc::Rc; - -use gpui::{SharedString, Task}; -use ropey::Rope; - use super::display_map::DisplayMap; -use crate::input::TabSize; - -#[allow(dead_code)] -pub(super) struct PendingBackgroundParse { - pub parse_task: Rc>>>, - pub language: SharedString, - pub text: Rope, - pub is_folding: bool, -} #[derive(Clone)] pub(crate) enum InputMode { /// A plain text input mode. PlainText { multi_line: bool, - tab: TabSize, + tab: crate::input::indent::TabSize, rows: usize, }, /// An auto grow input mode. @@ -29,18 +14,6 @@ pub(crate) enum InputMode { min_rows: usize, max_rows: usize, }, - /// A code editor input mode. - CodeEditor { - multi_line: bool, - tab: TabSize, - rows: usize, - /// Show line number - line_number: bool, - language: SharedString, - indent_guides: bool, - folding: bool, - parse_task: Rc>>>, - }, } impl Default for InputMode { @@ -55,25 +28,11 @@ impl InputMode { pub(super) fn plain_text() -> Self { InputMode::PlainText { multi_line: false, - tab: TabSize::default(), + tab: crate::input::indent::TabSize::default(), rows: 1, } } - /// Create a code editor input mode with default settings. - pub(super) fn code_editor(language: impl Into) -> Self { - InputMode::CodeEditor { - rows: 2, - multi_line: true, - tab: TabSize::default(), - language: language.into(), - line_number: true, - indent_guides: true, - folding: true, - parse_task: Rc::new(RefCell::new(None)), - } - } - /// Create an auto grow input mode with given min and max rows. pub(super) fn auto_grow(min_rows: usize, max_rows: usize) -> Self { InputMode::AutoGrow { @@ -86,7 +45,6 @@ impl InputMode { pub(super) fn multi_line(mut self, multi_line: bool) -> Self { match &mut self { InputMode::PlainText { multi_line: ml, .. } => *ml = multi_line, - InputMode::CodeEditor { multi_line: ml, .. } => *ml = multi_line, InputMode::AutoGrow { .. } => {} } self @@ -97,28 +55,6 @@ impl InputMode { !self.is_multi_line() } - #[inline] - pub(super) fn is_code_editor(&self) -> bool { - matches!(self, InputMode::CodeEditor { .. }) - } - - /// Return true if the mode is code editor and `folding: true`, `multi_line: true`. - #[inline] - pub(crate) fn is_folding(&self) -> bool { - if cfg!(target_family = "wasm") { - return false; - } - - matches!( - self, - InputMode::CodeEditor { - folding: true, - multi_line: true, - .. - } - ) - } - #[inline] pub(super) fn is_auto_grow(&self) -> bool { matches!(self, InputMode::AutoGrow { .. }) @@ -128,7 +64,6 @@ impl InputMode { pub(super) fn is_multi_line(&self) -> bool { match self { InputMode::PlainText { multi_line, .. } => *multi_line, - InputMode::CodeEditor { multi_line, .. } => *multi_line, InputMode::AutoGrow { max_rows, .. } => *max_rows > 1, } } @@ -138,9 +73,6 @@ impl InputMode { InputMode::PlainText { rows, .. } => { *rows = new_rows; } - InputMode::CodeEditor { rows, .. } => { - *rows = new_rows; - } InputMode::AutoGrow { rows, min_rows, @@ -168,7 +100,6 @@ impl InputMode { match self { InputMode::PlainText { rows, .. } => *rows, - InputMode::CodeEditor { rows, .. } => *rows, InputMode::AutoGrow { rows, .. } => *rows, } .max(1) @@ -196,16 +127,19 @@ impl InputMode { } } - /// Return false if the mode is not [`InputMode::CodeEditor`]. #[inline] - pub(super) fn line_number(&self) -> bool { + pub(super) fn is_indentable(&self) -> bool { match self { - InputMode::CodeEditor { - line_number, - multi_line, - .. - } => *line_number && *multi_line, + InputMode::PlainText { multi_line, .. } => *multi_line, _ => false, } } + + #[inline] + pub(super) fn tab_size(&self) -> crate::input::indent::TabSize { + match self { + InputMode::PlainText { tab, .. } => *tab, + _ => crate::input::indent::TabSize::default(), + } + } } diff --git a/crates/ui/src/input/popovers/code_action_menu.rs b/crates/ui/src/input/popovers/code_action_menu.rs deleted file mode 100644 index 9e2ab5b..0000000 --- a/crates/ui/src/input/popovers/code_action_menu.rs +++ /dev/null @@ -1,337 +0,0 @@ -use std::rc::Rc; - -use gpui::prelude::FluentBuilder; -use gpui::{ - Action, AnyElement, App, AppContext, Context, DismissEvent, Empty, Entity, EventEmitter, - InteractiveElement as _, IntoElement, ParentElement, Pixels, Point, Render, RenderOnce, - SharedString, Styled, StyledText, Subscription, Window, deferred, div, px, relative, -}; -use lsp_types::CodeAction; -use theme::ActiveTheme; - -const MAX_MENU_WIDTH: Pixels = px(320.); -const MAX_MENU_HEIGHT: Pixels = px(480.); - -use crate::input::popovers::editor_popover; -use crate::input::{self, InputState}; -use crate::list::{List, ListDelegate, ListEvent, ListState}; -use crate::{IndexPath, Selectable, actions, h_flex}; - -#[derive(Debug, Clone)] -pub(crate) struct CodeActionItem { - /// The `id` of the `CodeActionProvider` that provided this item. - pub(crate) provider_id: SharedString, - pub(crate) action: CodeAction, -} - -struct MenuDelegate { - menu: Entity, - items: Vec>, - selected_ix: usize, -} - -impl MenuDelegate { - fn set_items(&mut self, items: Vec) { - self.items = items.into_iter().map(Rc::new).collect(); - self.selected_ix = 0; - } - - fn selected_item(&self) -> Option<&Rc> { - self.items.get(self.selected_ix) - } -} - -#[derive(IntoElement)] -struct MenuItem { - ix: usize, - item: Rc, - children: Vec, - selected: bool, -} - -impl MenuItem { - fn new(ix: usize, item: Rc) -> Self { - Self { - ix, - item, - children: vec![], - selected: false, - } - } -} -impl Selectable for MenuItem { - fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } - - fn is_selected(&self) -> bool { - self.selected - } -} - -impl ParentElement for MenuItem { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements); - } -} -impl RenderOnce for MenuItem { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let item = self.item; - - let highlights = vec![]; - - h_flex() - .id(self.ix) - .gap_2() - .p_1() - .text_xs() - .line_height(relative(1.)) - .rounded(cx.theme().radius) - .hover(|this| this.bg(cx.theme().secondary_hover)) - .when(self.selected, |this| { - this.bg(cx.theme().secondary_background) - .text_color(cx.theme().secondary_foreground) - }) - .child( - div().child(StyledText::new(item.action.title.clone()).with_highlights(highlights)), - ) - .children(self.children) - } -} - -impl EventEmitter for MenuDelegate {} - -impl ListDelegate for MenuDelegate { - type Item = MenuItem; - - fn items_count(&self, _: usize, _: &gpui::App) -> usize { - self.items.len() - } - - fn render_item( - &mut self, - ix: crate::IndexPath, - _: &mut Window, - _: &mut Context>, - ) -> Option { - let item = self.items.get(ix.row)?; - Some(MenuItem::new(ix.row, item.clone())) - } - - fn set_selected_index( - &mut self, - ix: Option, - _: &mut Window, - cx: &mut Context>, - ) { - self.selected_ix = ix.map(|i| i.row).unwrap_or(0); - cx.notify(); - } - - fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { - let Some(item) = self.selected_item() else { - return; - }; - - self.menu.update(cx, |this, cx| { - this.select_item(&item, window, cx); - }); - } -} - -/// A context menu for code completions and code actions. -pub struct CodeActionMenu { - offset: usize, - state: Entity, - list: Entity>, - open: bool, - - _subscriptions: Vec, -} - -impl CodeActionMenu { - /// Creates a new `CompletionMenu` with the given offset and completion items. - /// - /// NOTE: This element should not call from InputState::new, unless that will stack overflow. - pub(crate) fn new( - state: Entity, - window: &mut Window, - cx: &mut App, - ) -> Entity { - cx.new(|cx| { - let view = cx.entity(); - let menu = MenuDelegate { - menu: view, - items: vec![], - selected_ix: 0, - }; - - let list = cx.new(|cx| ListState::new(menu, window, cx)); - - let _subscriptions = - vec![ - cx.subscribe(&list, |this: &mut Self, _, ev: &ListEvent, cx| { - match ev { - ListEvent::Confirm(_) => { - this.hide(cx); - } - _ => {} - } - cx.notify(); - }), - ]; - - Self { - offset: 0, - state, - list, - open: false, - _subscriptions, - } - }) - } - - fn select_item(&mut self, item: &CodeActionItem, window: &mut Window, cx: &mut Context) { - let state = self.state.clone(); - let item = item.clone(); - - cx.spawn_in(window, { - async move |_, cx| { - state.update_in(cx, |state, window, cx| { - state.perform_code_action(&item, window, cx); - }) - } - }) - .detach(); - - self.hide(cx); - } - - pub(crate) fn handle_action( - &mut self, - action: Box, - window: &mut Window, - cx: &mut Context, - ) -> bool { - if !self.open { - return false; - } - - cx.propagate(); - if input::Enter::is_primary(&*action) { - self.on_action_enter(window, cx); - } else if action.partial_eq(&input::Escape) { - self.on_action_escape(window, cx); - } else if action.partial_eq(&input::MoveUp) { - self.on_action_up(window, cx); - } else if action.partial_eq(&input::MoveDown) { - self.on_action_down(window, cx); - } else { - return false; - } - - true - } - - fn on_action_enter(&mut self, window: &mut Window, cx: &mut Context) { - let Some(item) = self.list.read(cx).delegate().selected_item().cloned() else { - return; - }; - self.select_item(&item, window, cx); - } - - fn on_action_escape(&mut self, _: &mut Window, cx: &mut Context) { - self.hide(cx); - } - - fn on_action_up(&mut self, window: &mut Window, cx: &mut Context) { - self.list.update(cx, |this, cx| { - this.on_action_select_prev(&actions::SelectUp, window, cx) - }); - } - - fn on_action_down(&mut self, window: &mut Window, cx: &mut Context) { - self.list.update(cx, |this, cx| { - this.on_action_select_next(&actions::SelectDown, window, cx) - }); - } - - pub(crate) fn is_open(&self) -> bool { - self.open - } - - /// Hide the completion menu and reset the trigger start offset. - pub(crate) fn hide(&mut self, cx: &mut Context) { - self.open = false; - cx.notify(); - } - - pub(crate) fn show( - &mut self, - offset: usize, - items: impl Into>, - window: &mut Window, - cx: &mut Context, - ) { - let items = items.into(); - self.offset = offset; - self.open = true; - self.list.update(cx, |this, cx| { - this.delegate_mut().set_items(items); - this.set_selected_index(Some(IndexPath::new(0)), window, cx); - }); - - cx.notify(); - } - - fn origin(&self, cx: &App) -> Option> { - let state = self.state.read(cx); - let Some(last_layout) = state.last_layout.as_ref() else { - return None; - }; - let Some(cursor_origin) = last_layout.cursor_bounds.map(|b| b.origin) else { - return None; - }; - - let scroll_origin = self.state.read(cx).scroll_handle.offset(); - - Some( - scroll_origin + cursor_origin - state.input_bounds.origin - + Point::new(-px(4.), last_layout.line_height + px(4.)), - ) - } -} - -impl Render for CodeActionMenu { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - if !self.open { - return Empty.into_any_element(); - } - - if self.list.read(cx).delegate().items.is_empty() { - self.open = false; - return Empty.into_any_element(); - } - - let Some(pos) = self.origin(cx) else { - return Empty.into_any_element(); - }; - - let max_width = MAX_MENU_WIDTH.min(window.bounds().size.width - pos.x); - - deferred( - editor_popover("code-action-menu", cx) - .absolute() - .left(pos.x) - .top(pos.y) - .max_w(max_width) - .min_w(px(120.)) - .child(List::new(&self.list).max_h(MAX_MENU_HEIGHT)) - .on_mouse_down_out(cx.listener(|this, _, _, cx| { - this.hide(cx); - })), - ) - .into_any_element() - } -} diff --git a/crates/ui/src/input/popovers/completion_menu.rs b/crates/ui/src/input/popovers/completion_menu.rs deleted file mode 100644 index c9a0933..0000000 --- a/crates/ui/src/input/popovers/completion_menu.rs +++ /dev/null @@ -1,446 +0,0 @@ -use std::rc::Rc; - -use gpui::prelude::FluentBuilder; -use gpui::{ - Action, AnyElement, App, AppContext, Context, DismissEvent, Empty, Entity, EventEmitter, - Half as _, HighlightStyle, InteractiveElement as _, IntoElement, ParentElement, Pixels, Point, - Render, RenderOnce, SharedString, Styled, StyledText, Subscription, Window, deferred, div, px, - relative, -}; -use lsp_types::{CompletionItem, CompletionTextEdit}; -use theme::ActiveTheme; - -const MAX_MENU_WIDTH: Pixels = px(320.); -const MAX_MENU_HEIGHT: Pixels = px(240.); -const POPOVER_GAP: Pixels = px(4.); - -use crate::input::popovers::{editor_popover, render_markdown}; -use crate::input::{self, InputState, RopeExt}; -use crate::list::{List, ListDelegate, ListEvent, ListState}; -use crate::{IndexPath, Selectable, actions, h_flex}; - -struct ContextMenuDelegate { - query: SharedString, - menu: Entity, - items: Vec>, - selected_ix: usize, -} - -impl ContextMenuDelegate { - fn set_items(&mut self, items: Vec) { - self.items = items.into_iter().map(Rc::new).collect(); - self.selected_ix = 0; - } - - fn selected_item(&self) -> Option<&Rc> { - self.items.get(self.selected_ix) - } -} - -#[derive(IntoElement)] -struct CompletionMenuItem { - ix: usize, - item: Rc, - children: Vec, - selected: bool, - highlight_prefix: SharedString, -} - -impl CompletionMenuItem { - fn new(ix: usize, item: Rc) -> Self { - Self { - ix, - item, - children: vec![], - selected: false, - highlight_prefix: "".into(), - } - } - - fn highlight_prefix(mut self, s: impl Into) -> Self { - self.highlight_prefix = s.into(); - self - } -} -impl Selectable for CompletionMenuItem { - fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } - - fn is_selected(&self) -> bool { - self.selected - } -} - -impl ParentElement for CompletionMenuItem { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements); - } -} - -impl RenderOnce for CompletionMenuItem { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let item = self.item; - - let matched_len = item - .filter_text - .as_ref() - .map(|s| s.len()) - .unwrap_or(self.highlight_prefix.len()) - .min(item.label.len()); - - let highlights = vec![( - 0..matched_len, - HighlightStyle { - color: Some(cx.theme().selection), - ..Default::default() - }, - )]; - - h_flex() - .id(self.ix) - .gap_2() - .p_1() - .text_xs() - .line_height(relative(1.)) - .rounded(cx.theme().radius.half()) - .when(item.deprecated.unwrap_or(false), |this| this.line_through()) - .hover(|this| this.bg(cx.theme().secondary_hover)) - .when(self.selected, |this| { - this.bg(cx.theme().secondary_background) - .text_color(cx.theme().secondary_foreground) - }) - .child(div().child(StyledText::new(item.label.clone()).with_highlights(highlights))) - .children(self.children) - } -} - -impl EventEmitter for ContextMenuDelegate {} - -impl ListDelegate for ContextMenuDelegate { - type Item = CompletionMenuItem; - - fn items_count(&self, _: usize, _: &gpui::App) -> usize { - self.items.len() - } - - fn render_item( - &mut self, - ix: crate::IndexPath, - _: &mut Window, - _: &mut Context>, - ) -> Option { - let item = self.items.get(ix.row)?; - Some(CompletionMenuItem::new(ix.row, item.clone()).highlight_prefix(self.query.clone())) - } - - fn set_selected_index( - &mut self, - ix: Option, - _: &mut Window, - cx: &mut Context>, - ) { - self.selected_ix = ix.map(|i| i.row).unwrap_or(0); - cx.notify(); - } - - fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { - let Some(item) = self.selected_item() else { - return; - }; - - self.menu.update(cx, |this, cx| { - this.select_item(&item, window, cx); - }); - } -} - -/// A context menu for code completions and code actions. -pub struct CompletionMenu { - offset: usize, - editor: Entity, - list: Entity>, - open: bool, - - /// The offset of the first character that triggered the completion. - pub(crate) trigger_start_offset: Option, - query: SharedString, - _subscriptions: Vec, -} - -impl CompletionMenu { - /// Creates a new `CompletionMenu` with the given offset and completion items. - /// - /// NOTE: This element should not call from InputState::new, unless that will stack overflow. - pub(crate) fn new( - editor: Entity, - window: &mut Window, - cx: &mut App, - ) -> Entity { - cx.new(|cx| { - let view = cx.entity(); - let menu = ContextMenuDelegate { - query: SharedString::default(), - menu: view, - items: vec![], - selected_ix: 0, - }; - - let list = cx.new(|cx| ListState::new(menu, window, cx)); - - let _subscriptions = - vec![ - cx.subscribe(&list, |this: &mut Self, _, ev: &ListEvent, cx| { - match ev { - ListEvent::Confirm(_) => { - this.hide(cx); - } - _ => {} - } - cx.notify(); - }), - ]; - - Self { - offset: 0, - editor, - list, - open: false, - trigger_start_offset: None, - query: SharedString::default(), - _subscriptions, - } - }) - } - - fn select_item(&mut self, item: &CompletionItem, window: &mut Window, cx: &mut Context) { - let offset = self.offset; - let item = item.clone(); - let mut range = self.trigger_start_offset.unwrap_or(self.offset)..self.offset; - - let editor = self.editor.clone(); - - cx.spawn_in(window, async move |_, cx| { - editor.update_in(cx, |editor, window, cx| { - editor.completion_inserting = true; - - let mut new_text = item.label.clone(); - if let Some(text_edit) = item.text_edit.as_ref() { - match text_edit { - CompletionTextEdit::Edit(edit) => { - new_text = edit.new_text.clone(); - range.start = editor.text.position_to_offset(&edit.range.start); - range.end = editor.text.position_to_offset(&edit.range.end); - } - CompletionTextEdit::InsertAndReplace(edit) => { - new_text = edit.new_text.clone(); - range.start = editor.text.position_to_offset(&edit.replace.start); - range.end = editor.text.position_to_offset(&edit.replace.end); - } - } - } else if let Some(insert_text) = item.insert_text.clone() { - new_text = insert_text; - range = offset..offset; - } - - editor.replace_text_in_range_silent( - Some(editor.range_to_utf16(&range)), - &new_text, - window, - cx, - ); - editor.completion_inserting = false; - // FIXME: Input not get the focus - editor.focus(window, cx); - }) - }) - .detach(); - - self.hide(cx); - } - - pub(crate) fn handle_action( - &mut self, - action: Box, - window: &mut Window, - cx: &mut Context, - ) -> bool { - if !self.open { - return false; - } - - cx.propagate(); - if input::Enter::is_primary(&*action) { - self.on_action_enter(window, cx); - } else if action.partial_eq(&input::Escape) { - self.on_action_escape(window, cx); - } else if action.partial_eq(&input::MoveUp) { - self.on_action_up(window, cx); - } else if action.partial_eq(&input::MoveDown) { - self.on_action_down(window, cx); - } else { - return false; - } - - true - } - - fn on_action_enter(&mut self, window: &mut Window, cx: &mut Context) { - let Some(item) = self.list.read(cx).delegate().selected_item().cloned() else { - return; - }; - self.select_item(&item, window, cx); - } - - fn on_action_escape(&mut self, _: &mut Window, cx: &mut Context) { - self.hide(cx); - } - - fn on_action_up(&mut self, window: &mut Window, cx: &mut Context) { - self.list.update(cx, |this, cx| { - this.on_action_select_prev(&actions::SelectUp, window, cx) - }); - } - - fn on_action_down(&mut self, window: &mut Window, cx: &mut Context) { - self.list.update(cx, |this, cx| { - this.on_action_select_next(&actions::SelectDown, window, cx) - }); - } - - pub(crate) fn is_open(&self) -> bool { - self.open - } - - /// Hide the completion menu and reset the trigger start offset. - pub(crate) fn hide(&mut self, cx: &mut Context) { - self.open = false; - self.trigger_start_offset = None; - cx.notify(); - } - - /// Sets the trigger start offset if it is not already set. - pub(crate) fn update_query(&mut self, start_offset: usize, query: impl Into) { - if self.trigger_start_offset.is_none() { - self.trigger_start_offset = Some(start_offset); - } - self.query = query.into(); - } - - pub(crate) fn show( - &mut self, - offset: usize, - items: impl Into>, - window: &mut Window, - cx: &mut Context, - ) { - let items = items.into(); - self.offset = offset; - self.open = true; - self.list.update(cx, |this, cx| { - let longest_ix = items - .iter() - .enumerate() - .max_by_key(|(_, item)| { - item.label.len() + item.detail.as_ref().map(|d| d.len()).unwrap_or(0) - }) - .map(|(ix, _)| ix) - .unwrap_or(0); - - this.delegate_mut().query = self.query.clone(); - this.delegate_mut().set_items(items); - this.set_selected_index(Some(IndexPath::new(0)), window, cx); - this.set_item_to_measure_index(IndexPath::new(longest_ix), window, cx); - }); - - cx.notify(); - } - - fn origin(&self, cx: &App) -> Option> { - let editor = self.editor.read(cx); - let Some(last_layout) = editor.last_layout.as_ref() else { - return None; - }; - let Some(cursor_origin) = last_layout.cursor_bounds.map(|b| b.origin) else { - return None; - }; - - let scroll_origin = self.editor.read(cx).scroll_handle.offset(); - - Some( - scroll_origin + cursor_origin - editor.input_bounds.origin - + Point::new(-px(4.), last_layout.line_height + px(4.)), - ) - } -} - -impl Render for CompletionMenu { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - if !self.open { - return Empty.into_any_element(); - } - - if self.list.read(cx).delegate().items.is_empty() { - self.open = false; - return Empty.into_any_element(); - } - - let Some(pos) = self.origin(cx) else { - return Empty.into_any_element(); - }; - - let selected_documentation = self - .list - .read(cx) - .delegate() - .selected_item() - .and_then(|item| item.documentation.clone()); - - let max_width = MAX_MENU_WIDTH.min(window.bounds().size.width - pos.x); - let abs_pos = self.editor.read(cx).input_bounds.origin + pos; - let vertical_layout = - abs_pos.x + MAX_MENU_WIDTH + POPOVER_GAP + MAX_MENU_WIDTH + POPOVER_GAP - > window.bounds().size.width; - - deferred( - div() - .absolute() - .left(pos.x) - .top(pos.y) - .flex() - .flex_row() - .gap(POPOVER_GAP) - .items_start() - .when(vertical_layout, |this| this.flex_col()) - .child( - editor_popover("completion-menu", cx) - .max_w(max_width) - .min_w(px(120.)) - .child(List::new(&self.list).max_h(MAX_MENU_HEIGHT)), - ) - .when_some(selected_documentation, |this, documentation| { - let mut doc = match documentation { - lsp_types::Documentation::String(s) => s.clone(), - lsp_types::Documentation::MarkupContent(mc) => mc.value.clone(), - }; - if vertical_layout { - doc = doc.split("\n").next().unwrap_or_default().to_string(); - } - - this.child( - div().child( - editor_popover("completion-menu", cx) - .w(MAX_MENU_WIDTH) - .px_2() - .child(render_markdown("doc", doc, window, cx)), - ), - ) - }) - .on_mouse_down_out(cx.listener(|this, _, _, cx| { - this.hide(cx); - })), - ) - .into_any_element() - } -} diff --git a/crates/ui/src/input/popovers/context_menu.rs b/crates/ui/src/input/popovers/context_menu.rs deleted file mode 100644 index 980bf91..0000000 --- a/crates/ui/src/input/popovers/context_menu.rs +++ /dev/null @@ -1,142 +0,0 @@ -use gpui::prelude::FluentBuilder as _; -use gpui::{ - Anchor, App, AppContext as _, Context, DismissEvent, Entity, IntoElement, MouseDownEvent, - ParentElement as _, Pixels, Point, Render, Styled, Subscription, Window, anchored, deferred, - div, px, -}; - -use crate::input::popovers::ContextMenu; -use crate::input::{self, InputState}; -use crate::menu::PopupMenu; - -/// Context menu for mouse right clicks. -pub(crate) struct InputContextMenu { - editor: Entity, - menu: Entity, - mouse_position: Point, - open: bool, - - _subscriptions: Vec, -} - -impl InputState { - pub(crate) fn handle_right_click_menu( - &mut self, - event: &MouseDownEvent, - offset: usize, - window: &mut Window, - cx: &mut Context, - ) { - // Show Mouse context menu - if !self.selected_range.contains(offset) { - self.move_to(offset, None, cx); - } - - self.context_menu_content = Some(ContextMenu::RightClick(self.context_menu.clone())); - - let is_code_editor = self.mode.is_code_editor(); - if is_code_editor { - self.handle_hover_definition(offset, window, cx); - } - - let is_enable = !self.disabled; - let has_goto_definition = is_enable && self.lsp.definition_provider.is_some(); - let has_code_action = is_enable && !self.lsp.code_action_providers.is_empty(); - let is_selected = !self.selected_range.is_empty(); - let has_paste = is_enable && cx.read_from_clipboard().is_some(); - - let action_context = self.focus_handle.clone(); - self.context_menu.update(cx, |this, cx| { - this.mouse_position = event.position; - this.menu.update(cx, |menu, cx| { - let new_menu = if let Some(builder) = &self.context_menu_builder { - builder(PopupMenu::new(cx), window, cx) - } else { - PopupMenu::new(cx) - .when(is_code_editor, |m| { - m.menu_with_enable( - "Go to Definition", - Box::new(input::GoToDefinition), - has_goto_definition, - ) - .menu_with_enable( - "Show Code Actions", - Box::new(input::ToggleCodeActions), - has_code_action, - ) - .separator() - }) - .menu_with_enable("Cut", Box::new(input::Cut), is_enable && is_selected) - .menu_with_enable("Copy", Box::new(input::Copy), is_selected) - .menu_with_enable("Paste", Box::new(input::Paste), has_paste) - .separator() - .menu("Select All", Box::new(input::SelectAll)) - }; - - menu.menu_items = new_menu.menu_items; - menu.action_context = Some(action_context); - cx.notify(); - }); - cx.defer_in(window, |this, _, cx| { - this.open = true; - cx.notify(); - }); - }); - } -} - -impl InputContextMenu { - pub(crate) fn new( - editor: Entity, - window: &mut Window, - cx: &mut App, - ) -> Entity { - cx.new(|cx| { - let menu = cx.new(|cx| PopupMenu::new(cx).small()); - - let _subscriptions = vec![cx.subscribe_in(&menu, window, { - move |this: &mut Self, _, _: &DismissEvent, window, cx| { - this.close(window, cx); - } - })]; - - Self { - editor, - menu, - mouse_position: Point::default(), - open: false, - _subscriptions, - } - }) - } - - #[inline] - pub(crate) fn is_open(&self) -> bool { - self.open - } - - #[inline] - pub(crate) fn close(&mut self, window: &mut Window, cx: &mut Context) { - self.open = false; - self.editor.update(cx, |this, cx| { - this.focus(window, cx); - }); - } -} - -impl Render for InputContextMenu { - fn render(&mut self, _: &mut Window, _cx: &mut Context) -> impl IntoElement { - if !self.open { - return div().into_any_element(); - } - - deferred( - anchored() - .snap_to_window_with_margin(px(8.)) - .anchor(Anchor::TopLeft) - .position(self.mouse_position) - .child(div().cursor_default().child(self.menu.clone())), - ) - .into_any_element() - } -} diff --git a/crates/ui/src/input/popovers/diagnostic_popover.rs b/crates/ui/src/input/popovers/diagnostic_popover.rs deleted file mode 100644 index 84631e3..0000000 --- a/crates/ui/src/input/popovers/diagnostic_popover.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::rc::Rc; - -use gpui::{ - prelude::FluentBuilder as _, px, App, AppContext as _, Bounds, Context, Empty, Entity, - IntoElement, Pixels, Point, Render, Styled, Window, -}; - -use crate::{ - highlighter::DiagnosticEntry, - input::{ - popovers::{render_markdown, Popover}, - InputState, - }, -}; - -pub struct DiagnosticPopover { - state: Entity, - pub(crate) diagnostic: Rc, - bounds: Bounds, - open: bool, -} - -impl DiagnosticPopover { - pub fn new( - diagnostic: &DiagnosticEntry, - state: Entity, - cx: &mut App, - ) -> Entity { - let diagnostic = Rc::new(diagnostic.clone()); - - cx.new(|_| Self { - diagnostic, - state, - bounds: Bounds::default(), - open: true, - }) - } - - pub(crate) fn show(&mut self, cx: &mut Context) { - self.open = true; - cx.notify(); - } - - pub(crate) fn hide(&mut self, cx: &mut Context) { - self.open = false; - cx.notify(); - } - - pub(crate) fn check_to_hide(&mut self, mouse_position: Point, cx: &mut Context) { - if !self.open { - return; - } - - let padding = px(5.); - let bounds = Bounds { - origin: self.bounds.origin.map(|v| v - padding), - size: self.bounds.size.map(|v| v + padding * 2.), - }; - - if !bounds.contains(&mouse_position) { - self.hide(cx); - } - } -} - -impl Render for DiagnosticPopover { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - if !self.open { - return Empty.into_any_element(); - } - - let message = self.diagnostic.message.clone(); - - let (border, bg, fg) = ( - self.diagnostic.severity.border(cx), - self.diagnostic.severity.bg(cx), - self.diagnostic.severity.fg(cx), - ); - - Popover::new( - "diagnostic-popover", - self.state.clone(), - self.diagnostic.range.clone(), - move |window, cx| render_markdown("message", message.clone(), window, cx), - ) - .when(!self.open, |this| this.invisible()) - .px_1() - .py_0p5() - .bg(bg) - .text_color(fg) - .border_1() - .border_color(border) - .into_any_element() - } -} diff --git a/crates/ui/src/input/popovers/hover_popover.rs b/crates/ui/src/input/popovers/hover_popover.rs deleted file mode 100644 index 96d2c3b..0000000 --- a/crates/ui/src/input/popovers/hover_popover.rs +++ /dev/null @@ -1,292 +0,0 @@ -use std::{ops::Range, rc::Rc}; - -use gpui::{ - AnyElement, App, AppContext as _, AvailableSpace, Bounds, Element, ElementId, Entity, - InteractiveElement, IntoElement, MouseDownEvent, MouseMoveEvent, ParentElement as _, Pixels, - Render, StatefulInteractiveElement as _, StyleRefinement, Styled, Window, deferred, div, point, - px, -}; - -use crate::{ - StyledExt, - input::{InputState, popovers::render_markdown}, -}; - -pub struct HoverPopover { - editor: Entity, - /// The symbol range byte of the hover trigger. - pub(crate) symbol_range: Range, - pub(crate) hover: Rc, -} - -impl HoverPopover { - pub fn new( - editor: Entity, - symbol_range: Range, - hover: &lsp_types::Hover, - cx: &mut App, - ) -> Entity { - let hover = Rc::new(hover.clone()); - - cx.new(|_| Self { - editor, - symbol_range, - hover, - }) - } - - pub(crate) fn is_same(&self, offset: usize) -> bool { - self.symbol_range.contains(&offset) - } -} - -impl Render for HoverPopover { - fn render(&mut self, _: &mut Window, _: &mut gpui::Context) -> impl IntoElement { - let contents = match self.hover.contents.clone() { - lsp_types::HoverContents::Scalar(scalar) => match scalar { - lsp_types::MarkedString::String(s) => s, - lsp_types::MarkedString::LanguageString(ls) => ls.value, - }, - lsp_types::HoverContents::Array(arr) => arr - .into_iter() - .map(|item| match item { - lsp_types::MarkedString::String(s) => s, - lsp_types::MarkedString::LanguageString(ls) => ls.value, - }) - .collect::>() - .join("\n\n"), - lsp_types::HoverContents::Markup(markup) => markup.value, - }; - - Popover::new( - "hover-popover", - self.editor.clone(), - self.symbol_range.clone(), - move |window, cx| render_markdown("message", contents.clone(), window, cx), - ) - .into_any_element() - } -} - -pub(crate) struct Popover { - id: ElementId, - style: StyleRefinement, - editor: Entity, - range: Range, - width_limit: Range, - content_builder: Box AnyElement>, -} - -impl Styled for Popover { - fn style(&mut self) -> &mut StyleRefinement { - &mut self.style - } -} - -impl Popover { - pub fn new( - id: impl Into, - editor: Entity, - range: Range, - f: F, - ) -> Self - where - F: Fn(&mut Window, &mut App) -> E + 'static, - E: IntoElement, - { - Self { - id: id.into(), - editor, - range, - style: StyleRefinement::default(), - width_limit: px(200.)..px(500.), - content_builder: Box::new(move |window, cx| (f)(window, cx).into_any_element()), - } - } - - /// Get the bounds of the range in the editor, if it is visible. - fn trigger_bounds(&self, cx: &App) -> Option> { - let editor = self.editor.read(cx); - let Some(last_layout) = editor.last_layout.as_ref() else { - return None; - }; - - let Some(last_bounds) = editor.last_bounds else { - return None; - }; - - let (_, _, start_pos) = editor.line_and_position_for_offset(self.range.start); - let (_, _, end_pos) = editor.line_and_position_for_offset(self.range.end); - - let Some(start_pos) = start_pos else { - return None; - }; - let Some(end_pos) = end_pos else { - return None; - }; - - Some(Bounds::from_corners( - last_bounds.origin + start_pos, - last_bounds.origin + end_pos + point(px(0.), last_layout.line_height), - )) - } -} - -impl IntoElement for Popover { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -pub(crate) struct PopoverLayoutState { - bounds: Bounds, - element: Option, -} - -impl Element for Popover { - type RequestLayoutState = PopoverLayoutState; - type PrepaintState = (); - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _: Option<&gpui::GlobalElementId>, - _: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (gpui::LayoutId, Self::RequestLayoutState) { - let trigger_bounds = match self.trigger_bounds(cx) { - Some(bounds) => bounds, - None => { - return ( - div().into_any_element().request_layout(window, cx), - PopoverLayoutState { - bounds: Bounds::default(), - element: None, - }, - ); - } - }; - - let max_width = self - .width_limit - .end - .min(window.bounds().size.width - SNAP_TO_EDGE * 2) - .max(px(200.)); - let max_height = (window.bounds().size.height - SNAP_TO_EDGE * 2).min(px(320.)); - - let mut popover = deferred( - div() - .id("hover-popover-content") - .flex_none() - .occlude() - .p_1() - .text_xs() - .popover_style(cx) - .shadow_md() - .max_w(max_width) - .max_h(max_height) - .overflow_y_scroll() - .refine_style(&self.style) - .child((self.content_builder)(window, cx)), - ) - .into_any_element(); - - let popover_size = popover.layout_as_root(AvailableSpace::min_size(), window, cx); - const SNAP_TO_EDGE: Pixels = px(8.); - let top_space = trigger_bounds.top() - SNAP_TO_EDGE; - let right_space = window.bounds().size.width - trigger_bounds.left() - SNAP_TO_EDGE; - - let mut pos = point( - trigger_bounds.left(), - trigger_bounds.top() - popover_size.height, - ); - if popover_size.height > top_space { - pos.y = trigger_bounds.bottom(); - } - if popover_size.width > right_space { - pos.x = trigger_bounds.right() - popover_size.width; - } - - let mut empty = div().into_any_element(); - let layout_id = empty.request_layout(window, cx); - ( - layout_id, - PopoverLayoutState { - bounds: Bounds { - origin: pos, - size: popover_size, - }, - element: Some(popover), - }, - ) - } - - fn prepaint( - &mut self, - _: Option<&gpui::GlobalElementId>, - _: Option<&gpui::InspectorElementId>, - _: Bounds, - request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - let bounds = request_layout.bounds; - let Some(popover) = request_layout.element.as_mut() else { - return; - }; - - window.with_absolute_element_offset(bounds.origin, |window| { - popover.prepaint(window, cx); - }) - } - - fn paint( - &mut self, - _: Option<&gpui::GlobalElementId>, - _: Option<&gpui::InspectorElementId>, - _: Bounds, - request_layout: &mut Self::RequestLayoutState, - _: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - let bounds = request_layout.bounds; - let Some(popover) = request_layout.element.as_mut() else { - return; - }; - - popover.paint(window, cx); - - let editor = self.editor.clone(); - // Mouse down out to hide. - window.on_mouse_event(move |event: &MouseDownEvent, _, _, cx| { - if !bounds.contains(&event.position) { - let _ = editor.update(cx, |editor, cx| { - editor.clear_hover_state(cx); - }); - } - }); - - // Mouse out of trigger + popover bounds - let editor = self.editor.clone(); - let trigger_bounds = self.trigger_bounds(cx).unwrap_or(bounds); - let keep_open_region = trigger_bounds.union(&bounds); - window.on_mouse_event(move |event: &MouseMoveEvent, _, _, cx| { - if !keep_open_region.contains(&event.position) { - let _ = editor.update(cx, |editor, cx| { - editor.clear_hover_state(cx); - }); - } - }) - } -} diff --git a/crates/ui/src/input/popovers/mod.rs b/crates/ui/src/input/popovers/mod.rs deleted file mode 100644 index 597d812..0000000 --- a/crates/ui/src/input/popovers/mod.rs +++ /dev/null @@ -1,41 +0,0 @@ -mod code_action_menu; -mod completion_menu; -mod context_menu; -mod diagnostic_popover; -mod hover_popover; - -pub(crate) use code_action_menu::*; -pub(crate) use completion_menu::*; -pub(crate) use context_menu::*; -pub(crate) use diagnostic_popover::*; -use gpui::{ - App, Div, ElementId, Entity, InteractiveElement as _, IntoElement, SharedString, Stateful, - StyleRefinement, Styled as _, Window, div, px, rems, -}; -pub(crate) use hover_popover::*; - -use crate::StyledExt as _; - -pub(crate) enum ContextMenu { - Completion(Entity), - CodeAction(Entity), - RightClick(Entity), -} - -impl ContextMenu { - pub(crate) fn is_open(&self, cx: &App) -> bool { - match self { - ContextMenu::Completion(menu) => menu.read(cx).is_open(), - ContextMenu::CodeAction(menu) => menu.read(cx).is_open(), - ContextMenu::RightClick(menu) => menu.read(cx).is_open(), - } - } - - pub(crate) fn render(&self) -> impl IntoElement { - match self { - ContextMenu::Completion(menu) => menu.clone().into_any_element(), - ContextMenu::CodeAction(menu) => menu.clone().into_any_element(), - ContextMenu::RightClick(menu) => menu.clone().into_any_element(), - } - } -} diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index 6fdd4ff..5bd5fcf 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -99,9 +99,6 @@ actions!( MoveToPreviousWord, MoveToNextWord, Escape, - ToggleCodeActions, - Search, - GoToDefinition, ] ); @@ -250,14 +247,6 @@ pub(crate) fn init(cx: &mut App) { KeyBinding::new("ctrl-z", Undo, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-y", Redo, Some(CONTEXT)), - #[cfg(target_os = "macos")] - KeyBinding::new("cmd-.", ToggleCodeActions, Some(CONTEXT)), - #[cfg(not(target_os = "macos"))] - KeyBinding::new("ctrl-.", ToggleCodeActions, Some(CONTEXT)), - #[cfg(target_os = "macos")] - KeyBinding::new("cmd-f", Search, Some(CONTEXT)), - #[cfg(not(target_os = "macos"))] - KeyBinding::new("ctrl-f", Search, Some(CONTEXT)), ]); } @@ -355,7 +344,6 @@ pub struct InputState { pub(super) clean_on_escape: bool, pub(super) submit_on_enter: bool, pub(super) soft_wrap: bool, - pub(super) show_whitespaces: bool, /// This flag tells the renderer to prefer the end of the current visual line. pub(crate) cursor_line_end_affinity: bool, pub(super) pattern: Option, @@ -435,7 +423,6 @@ impl InputState { clean_on_escape: false, submit_on_enter: false, soft_wrap: true, - show_whitespaces: false, loading: false, pattern: None, validate: None, @@ -480,33 +467,6 @@ impl InputState { self } - /// Set Input to use [`InputMode::CodeEditor`] mode. - /// - /// Default options: - /// - /// - line_number: true - /// - tab_size: 2 - /// - hard_tabs: false - /// - height: 100% - /// - multi_line: true - /// - indent_guides: true - /// - /// If `highlighter` is None, will use the default highlighter. - /// - /// Code Editor aim for help used to simple code editing or display, not a full-featured code editor. - /// - /// ## Features - /// - /// - Syntax Highlighting - /// - Auto Indent - /// - Line Number - /// - Large Text support, up to 50K lines. - pub fn code_editor(mut self, language: impl Into) -> Self { - let language: SharedString = language.into(); - self.mode = InputMode::code_editor(language); - self - } - /// Set whether search UI allows replacement, default is true. pub fn replaceable(mut self, allow: bool) -> Self { self.replaceable = allow; @@ -519,49 +479,6 @@ impl InputState { self } - /// Set enable/disable code folding, only for [`InputMode::CodeEditor`] mode. - /// - /// Default: true - pub fn folding(mut self, folding: bool) -> Self { - debug_assert!(self.mode.is_code_editor()); - if let InputMode::CodeEditor { folding: f, .. } = &mut self.mode { - *f = folding; - } - self - } - - /// Set code folding at runtime, only for [`InputMode::CodeEditor`] mode. - /// - /// When disabling, all existing folds are cleared. - pub fn set_folding(&mut self, folding: bool, _: &mut Window, cx: &mut Context) { - debug_assert!(self.mode.is_code_editor()); - if let InputMode::CodeEditor { folding: f, .. } = &mut self.mode { - *f = folding; - } - if !folding { - self.display_map.clear_folds(); - } - cx.notify(); - } - - /// Set enable/disable line number, only for [`InputMode::CodeEditor`] mode. - pub fn line_number(mut self, line_number: bool) -> Self { - debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line()); - if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode { - *l = line_number; - } - self - } - - /// Set line number, only for [`InputMode::CodeEditor`] mode. - pub fn set_line_number(&mut self, line_number: bool, _: &mut Window, cx: &mut Context) { - debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line()); - if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode { - *l = line_number; - } - cx.notify(); - } - /// Set the number of rows for the multi-line Textarea. /// /// This is only used when `multi_line` is set to true. @@ -569,9 +486,7 @@ impl InputState { /// default: 2 pub fn rows(mut self, rows: usize) -> Self { match &mut self.mode { - InputMode::PlainText { rows: r, .. } | InputMode::CodeEditor { rows: r, .. } => { - *r = rows - } + InputMode::PlainText { rows: r, .. } => *r = rows, InputMode::AutoGrow { max_rows: max_r, rows: r, @@ -584,31 +499,6 @@ impl InputState { self } - /// Set highlighter language for for [`InputMode::CodeEditor`] mode. - pub fn set_highlighter( - &mut self, - new_language: impl Into, - cx: &mut Context, - ) { - if let InputMode::CodeEditor { - language, - parse_task, - .. - } = &mut self.mode - { - *language = new_language.into(); - parse_task.borrow_mut().take(); - } - cx.notify(); - } - - fn reset_highlighter(&mut self, cx: &mut Context) { - if let InputMode::CodeEditor { parse_task, .. } = &mut self.mode { - parse_task.borrow_mut().take(); - } - cx.notify(); - } - /// Set placeholder pub fn set_placeholder( &mut self, @@ -726,7 +616,6 @@ impl InputState { let text: SharedString = text.into(); let range = 0..self.text.chars().map(|c| c.len_utf16()).sum(); self.replace_text_in_range_silent(Some(range), &text, window, cx); - self.reset_highlighter(cx); self.disabled = was_disabled; } @@ -779,12 +668,6 @@ impl InputState { self } - /// Set whether to show whitespace characters. - pub fn show_whitespaces(mut self, show: bool) -> Self { - self.show_whitespaces = show; - self - } - /// Update the soft wrap mode for multi-line input, default is true. pub fn set_soft_wrap(&mut self, wrap: bool, _: &mut Window, cx: &mut Context) { debug_assert!(self.mode.is_multi_line()); @@ -808,12 +691,6 @@ impl InputState { cx.notify(); } - /// Update whether to show whitespace characters. - pub fn set_show_whitespaces(&mut self, show: bool, _: &mut Window, cx: &mut Context) { - self.show_whitespaces = show; - cx.notify(); - } - /// Set the regular expression pattern of the input field. /// /// Only for [`InputMode::SingleLine`] mode. @@ -1038,7 +915,7 @@ impl InputState { let row = self.text.offset_to_point(self.cursor()).row; let logical_start = self.text.line_start_offset(row); - if self.soft_wrap && self.mode.is_code_editor() { + if self.soft_wrap { let wrap_point = self.display_map.offset_to_wrap_display_point(self.cursor()); if let Some(line) = self.display_map.lines().get(row) && let Some(range) = line.wrapped_lines.get(wrap_point.local_row) @@ -1066,7 +943,7 @@ impl InputState { let logical_start = self.text.line_start_offset(row); let logical_end = self.text.line_end_offset(row); - if self.soft_wrap && self.mode.is_code_editor() { + if self.soft_wrap { let wrap_point = self.display_map.offset_to_wrap_display_point(self.cursor()); if let Some(line) = self.display_map.lines().get(row) && let Some(range) = line.wrapped_lines.get(wrap_point.local_row) @@ -1264,7 +1141,7 @@ impl InputState { if insert_newline { // Get current line indent - let indent = if self.mode.is_code_editor() { + let indent = if self.mode.is_indentable() { self.indent_of_next_line() } else { "".to_string() @@ -1465,11 +1342,7 @@ impl InputState { // Check if row_offset_y is out of the viewport // If row offset is not in the viewport, scroll to make it visible - let edge_height = if direction.is_some() && self.mode.is_code_editor() { - 3 * line_height - } else { - line_height - }; + let edge_height = line_height; if row_offset_y - edge_height + line_height < -scroll_offset.y { // Scroll up scroll_offset.y = -row_offset_y + edge_height - line_height; @@ -1749,34 +1622,6 @@ impl InputState { self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end) } - /// If offset falls on a hidden (folded) line, clamp backward to the end of - /// the fold header line (last visible position before the fold). - fn clamp_offset_to_visible_backward(&self, offset: usize) -> usize { - let line = self.text.offset_to_point(offset).row; - if self.display_map.is_buffer_line_hidden(line) { - for fold in self.display_map.folded_ranges() { - if line > fold.start_line && line <= fold.end_line { - return self.text.line_end_offset(fold.start_line); - } - } - } - offset - } - - /// If offset falls on a hidden (folded) line, clamp forward to the start of - /// the fold end line (first visible position after the fold). - fn clamp_offset_to_visible_forward(&self, offset: usize) -> usize { - let line = self.text.offset_to_point(offset).row; - if self.display_map.is_buffer_line_hidden(line) { - for fold in self.display_map.folded_ranges() { - if line > fold.start_line && line <= fold.end_line { - return self.text.line_start_offset(fold.end_line); - } - } - } - offset - } - pub(super) fn previous_boundary(&self, offset: usize) -> usize { let mut offset = self.text.clip_offset(offset.saturating_sub(1), Bias::Left); if let Some(ch) = self.text.char_at(offset) @@ -1785,7 +1630,7 @@ impl InputState { offset -= 1; } - self.clamp_offset_to_visible_backward(offset) + offset } pub(super) fn next_boundary(&self, offset: usize) -> usize { @@ -1796,7 +1641,7 @@ impl InputState { offset += 1; } - self.clamp_offset_to_visible_forward(offset) + offset } /// Returns the true to let InputElement to render cursor, when Input is focused and current BlinkCursor is visible.