clean up input component

This commit is contained in:
2026-07-27 08:38:25 +07:00
parent 9addbd49f4
commit 2f15615d7b
17 changed files with 94 additions and 3189 deletions

View File

@@ -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 std::ops::Range;
use gpui::{App, Font, Pixels}; use gpui::{App, Font, Pixels};
use ropey::Rope; use ropey::Rope;
use super::fold_map::FoldMap;
use super::folding::FoldRange;
use super::text_wrapper::{LineItem, WrapDisplayPoint}; use super::text_wrapper::{LineItem, WrapDisplayPoint};
use super::wrap_map::WrapMap; use super::wrap_map::WrapMap;
use super::{BufferPoint, DisplayPoint};
use crate::input::Point as TreeSitterPoint; 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. /// DisplayMap is the main interface for 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.
pub struct DisplayMap { pub struct DisplayMap {
wrap_map: WrapMap, wrap_map: WrapMap,
fold_map: FoldMap,
} }
impl DisplayMap { impl DisplayMap {
pub fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self { pub fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
Self { Self {
wrap_map: WrapMap::new(font, font_size, wrap_width), wrap_map: WrapMap::new(font, font_size, wrap_width),
fold_map: FoldMap::new(),
} }
} }
// ==================== Core Coordinate Mapping ==================== /// Get total number of display rows (same as wrap rows without folding)
/// 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
#[inline] #[inline]
pub fn display_row_count(&self) -> usize { 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 /// Get the buffer line for a given display row
pub fn display_row_to_buffer_line(&self, display_row: usize) -> usize { pub fn display_row_to_buffer_line(&self, display_row: usize) -> usize {
// Display → Wrap self.wrap_map.wrap_row_to_buffer_line(display_row)
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)
} }
/// Get the display row range for a buffer line: [start, end) /// 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<Range<usize>> { pub fn buffer_line_to_display_row_range(&self, line: usize) -> Option<Range<usize>> {
// Buffer line → Wrap row range let range = self.wrap_map.buffer_line_to_wrap_row_range(line);
let wrap_row_range = self.wrap_map.buffer_line_to_wrap_row_range(line); if range.is_empty() { None } else { Some(range) }
// 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
}
} }
/// Check if a buffer line is completely hidden /// Check if a buffer line is completely hidden (never true without folding)
#[inline] #[inline]
pub fn is_buffer_line_hidden(&self, line: usize) -> bool { pub fn is_buffer_line_hidden(&self, _line: usize) -> bool {
self.buffer_line_to_display_row_range(line).is_none() false
} }
/// Set fold candidates (from tree-sitter/LSP) /// All wrap rows are visible since there's no folding.
pub fn set_fold_candidates(&mut self, candidates: Vec<FoldRange>) {
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
#[inline] #[inline]
pub fn is_folded_at(&self, start_line: usize) -> bool { pub fn folded_ranges(&self) -> &[()] {
self.fold_map.is_folded_at(start_line) &[]
} }
/// Check if a line is a fold candidate /// Adjust folds for edit (no-op without folding)
#[inline] pub fn adjust_folds_for_edit(
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<usize>, 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(
&mut self, &mut self,
tree: &super::folding::Tree, _old_text: &Rope,
edit_byte_range: Range<usize>, _range: &Range<usize>,
new_text: &Rope, _new_text: &str,
) { ) {
let new_start_line = new_text.offset_to_point(edit_byte_range.start).row; // No-op: no folding
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);
} }
/// Update text (incremental or full) /// Update text (incremental or full)
@@ -209,52 +68,28 @@ impl DisplayMap {
) { ) {
self.wrap_map self.wrap_map
.on_text_changed(changed_text, range, new_text, cx); .on_text_changed(changed_text, range, new_text, cx);
self.rebuild_fold_projection();
} }
/// Update layout parameters (wrap width or font) /// Update layout parameters (wrap width or font)
pub fn on_layout_changed(&mut self, wrap_width: Option<Pixels>, cx: &mut App) { pub fn on_layout_changed(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
self.wrap_map.on_layout_changed(wrap_width, cx); self.wrap_map.on_layout_changed(wrap_width, cx);
self.rebuild_fold_projection();
} }
/// Set font parameters /// Set font parameters
pub fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) { pub fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
self.wrap_map.set_font(font, font_size, cx); self.wrap_map.set_font(font, font_size, cx);
self.rebuild_fold_projection();
} }
/// Ensure text is prepared (initializes wrapper if needed) /// Ensure text is prepared (initializes wrapper if needed)
pub fn ensure_text_prepared(&mut self, text: &Rope, cx: &mut App) { pub fn ensure_text_prepared(&mut self, text: &Rope, cx: &mut App) {
let did_initialize = self.wrap_map.ensure_text_prepared(text, cx); self.wrap_map.ensure_text_prepared(text, cx);
if did_initialize {
self.rebuild_fold_projection();
}
} }
/// Initialize with text /// Initialize with text
pub fn set_text(&mut self, text: &Rope, cx: &mut App) { pub fn set_text(&mut self, text: &Rope, cx: &mut App) {
self.wrap_map.set_text(text, cx); 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). /// Convert byte offset to wrap display point (with soft wrap info).
#[inline] #[inline]
pub(crate) fn offset_to_wrap_display_point(&self, offset: usize) -> WrapDisplayPoint { 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) self.wrap_map.wrapper().display_point_to_point(point)
} }
/// Convert a wrap row to a display row (skipping folded rows). /// Since there's no folding, wrap row == display row.
/// Returns None if the wrap row is folded.
#[inline] #[inline]
pub fn wrap_row_to_display_row(&self, wrap_row: usize) -> Option<usize> { pub fn wrap_row_to_display_row(&self, wrap_row: usize) -> Option<usize> {
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] #[inline]
pub fn nearest_visible_display_row(&self, wrap_row: usize) -> usize { 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] #[inline]
pub fn display_row_to_wrap_row(&self, display_row: usize) -> Option<usize> { pub fn display_row_to_wrap_row(&self, display_row: usize) -> Option<usize> {
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). /// Get the longest row index (by byte length).
@@ -298,8 +140,6 @@ impl DisplayMap {
self.wrap_map.wrapper().longest_row.row self.wrap_map.wrapper().longest_row.row
} }
// ==================== Access Methods ====================
/// Get access to line items (for rendering) /// Get access to line items (for rendering)
#[inline] #[inline]
pub(crate) fn lines(&self) -> &[LineItem] { pub(crate) fn lines(&self) -> &[LineItem] {
@@ -312,14 +152,13 @@ impl DisplayMap {
self.wrap_map.text() 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] #[inline]
pub fn visible_wrap_row_count_for_buffer_line(&self, line: usize) -> usize { pub fn visible_wrap_row_count_for_buffer_line(&self, line: usize) -> usize {
self.wrap_map self.wrap_map.visible_wrap_row_count_for_buffer_line(line)
.visible_wrap_row_count_for_line(line, &self.fold_map)
} }
/// Get the wrap row count (before folding) /// Get the wrap row count
#[inline] #[inline]
pub fn wrap_row_count(&self) -> usize { pub fn wrap_row_count(&self) -> usize {
self.wrap_map.wrap_row_count() self.wrap_map.wrap_row_count()

View File

@@ -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<usize>,
/// Reverse mapping: wrap_row → display_row
/// index = wrap_row, value = Some(display_row) if visible, None if folded
wrap_row_to_display_row: Vec<Option<usize>>,
/// Candidate fold ranges (from tree-sitter/LSP)
/// Sorted by start_line, unique start_line
candidates: Vec<FoldRange>,
/// Currently folded ranges
/// Subset of candidates, sorted by start_line
folded: Vec<FoldRange>,
/// 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<usize> {
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<usize> {
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<FoldRange>) {
// 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<FoldRange>,
) {
// 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;
}
}

View File

@@ -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<usize>) -> Vec<FoldRange> {
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<usize>,
ranges: &mut Vec<FoldRange>,
) {
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);
}
}

View File

@@ -1,61 +1,7 @@
#[allow(clippy::module_inception)] #[allow(clippy::module_inception)]
mod display_map; mod display_map;
mod fold_map;
#[cfg(not(target_family = "wasm"))]
mod folding;
#[cfg(target_family = "wasm")]
pub mod folding;
mod text_wrapper; mod text_wrapper;
mod wrap_map; 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 use self::display_map::DisplayMap;
pub(crate) use self::text_wrapper::LineLayout; 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 }
}
}

View File

@@ -9,10 +9,7 @@ use std::ops::Range;
use gpui::{App, Font, Pixels}; use gpui::{App, Font, Pixels};
use ropey::Rope; use ropey::Rope;
use super::fold_map::FoldMap; use super::text_wrapper::{LineItem, TextWrapper};
use super::text_wrapper::{LineItem, TextWrapper, WrapDisplayPoint};
use super::{BufferPoint, WrapPoint};
use crate::input::rope_ext::RopeExt;
/// WrapMap manages soft-wrapping and provides buffer ↔ wrap coordinate mapping. /// WrapMap manages soft-wrapping and provides buffer ↔ wrap coordinate mapping.
pub struct WrapMap { pub struct WrapMap {
@@ -55,49 +52,6 @@ impl WrapMap {
self.wrapper.lines.len() 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 /// Get the buffer line for a given wrap row
pub fn wrap_row_to_buffer_line(&self, wrap_row: usize) -> usize { pub fn wrap_row_to_buffer_line(&self, wrap_row: usize) -> usize {
if wrap_row >= self.wrap_row_count() { if wrap_row >= self.wrap_row_count() {
@@ -176,8 +130,6 @@ impl WrapMap {
let wrap_row_count = self.wrapper.len(); let wrap_row_count = self.wrapper.len();
// Skip if nothing changed: both buffer line count and total wrap row count must match. // 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 if line_count == self.cached_line_count
&& wrap_row_count == self.cached_wrap_row_count && wrap_row_count == self.cached_wrap_row_count
&& !self.buffer_line_starts.is_empty() && !self.buffer_line_starts.is_empty()
@@ -212,11 +164,9 @@ impl WrapMap {
self.wrapper.text() self.wrapper.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.
pub fn visible_wrap_row_count_for_line(&self, line: usize, fold_map: &FoldMap) -> usize { /// Without folding, all wrap rows are visible.
let wrap_range = self.buffer_line_to_wrap_row_range(line); pub fn visible_wrap_row_count_for_buffer_line(&self, line: usize) -> usize {
wrap_range self.buffer_line_to_wrap_row_range(line).len()
.filter(|&wr| fold_map.wrap_row_to_display_row(wr).is_some())
.count()
} }
} }

View File

@@ -3,9 +3,8 @@ use std::rc::Rc;
use gpui::{ use gpui::{
AnyElement, App, Bounds, Corners, Edges, Element, ElementId, ElementInputHandler, Entity, AnyElement, App, Bounds, Corners, Edges, Element, ElementId, ElementInputHandler, Entity,
GlobalElementId, Half, HighlightStyle, Hitbox, HitboxBehavior, Hsla, InteractiveElement, GlobalElementId, Half, Hsla, IntoElement, LayoutId, MouseButton, MouseMoveEvent, MouseUpEvent,
IntoElement, LayoutId, MouseButton, MouseMoveEvent, MouseUpEvent, Path, Pixels, Point, Path, Pixels, Point, Position, SharedString, Size, Style, TextAlign, TextRun, TextStyle,
Position, ShapedLine, SharedString, Size, Style, Styled as _, TextAlign, TextRun, TextStyle,
UnderlineStyle, Window, fill, point, px, relative, size, UnderlineStyle, Window, fill, point, px, relative, size,
}; };
use ropey::Rope; use ropey::Rope;
@@ -14,19 +13,14 @@ use theme::ActiveTheme;
use super::mode::InputMode; use super::mode::InputMode;
use super::{InputState, LastLayout, WhitespaceIndicators}; use super::{InputState, LastLayout, WhitespaceIndicators};
use crate::button::{Button, ButtonVariants as _}; use crate::Root;
use crate::input::RopeExt as _; use crate::input::RopeExt as _;
use crate::input::blink_cursor::CURSOR_WIDTH; use crate::input::blink_cursor::CURSOR_WIDTH;
use crate::input::display_map::LineLayout; use crate::input::display_map::LineLayout;
use crate::scroll::Scrollbar; use crate::scroll::Scrollbar;
use crate::{IconName, Root, Selectable, Sizable as _};
const BOTTOM_MARGIN_ROWS: usize = 3; const BOTTOM_MARGIN_ROWS: usize = 3;
pub(super) const RIGHT_MARGIN: Pixels = px(10.); 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)] #[derive(Clone, Copy, Debug, PartialEq)]
struct EditorScrollbarLayout { struct EditorScrollbarLayout {
@@ -72,7 +66,7 @@ impl EditorScrollbarLayout {
let left = if line_number_width == px(0.) { let left = if line_number_width == px(0.) {
px(0.) px(0.)
} else { } else {
paddings.left + line_number_width - LINE_NUMBER_RIGHT_MARGIN paddings.left + line_number_width
}; };
Self { 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() 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(super) struct TextElement {
pub(crate) state: Entity<InputState>, pub(crate) state: Entity<InputState>,
placeholder: SharedString, placeholder: SharedString,
@@ -285,7 +271,7 @@ impl TextElement {
scroll_size: Size<Pixels>, scroll_size: Size<Pixels>,
_: &mut Window, _: &mut Window,
cx: &mut App, cx: &mut App,
) -> (Option<Bounds<Pixels>>, Point<Pixels>, Option<usize>) { ) -> (Option<Bounds<Pixels>>, Point<Pixels>) {
let state = self.state.read(cx); let state = self.state.read(cx);
let line_height = last_layout.line_height; let line_height = last_layout.line_height;
@@ -307,7 +293,6 @@ impl TextElement {
cursor = masked_display_offset(&state.text, cursor); cursor = masked_display_offset(&state.text, cursor);
} }
let mut current_row = None;
let mut scroll_offset = state.scroll_handle.offset(); let mut scroll_offset = state.scroll_handle.offset();
let mut cursor_bounds = None; let mut cursor_bounds = None;
@@ -330,7 +315,6 @@ impl TextElement {
let visible_buffer_lines = &last_layout.visible_buffer_lines; let visible_buffer_lines = &last_layout.visible_buffer_lines;
let mut vi = 0; // index into visible_buffer_lines / lines let mut vi = 0; // index into visible_buffer_lines / lines
for (ix, wrap_line) in buffer_lines.iter().enumerate() { for (ix, wrap_line) in buffer_lines.iter().enumerate() {
let row = ix;
let line_origin = point(px(0.), offset_y); let line_origin = point(px(0.), offset_y);
// break loop if all cursor positions are found // break loop if all cursor positions are found
@@ -353,7 +337,6 @@ impl TextElement {
if let Some(pos) = if let Some(pos) =
line.position_for_index(offset, last_layout, state.cursor_line_end_affinity) line.position_for_index(offset, last_layout, state.cursor_line_end_affinity)
{ {
current_row = Some(row);
cursor_pos = Some(line_origin + pos); cursor_pos = Some(line_origin + pos);
} }
} }
@@ -377,7 +360,6 @@ impl TextElement {
// Not visible (before visible range or hidden/folded). // Not visible (before visible range or hidden/folded).
// Just increase the offset_y and prev_lines_offset for scroll tracking. // Just increase the offset_y and prev_lines_offset for scroll tracking.
if prev_lines_offset >= cursor && cursor_pos.is_none() { if prev_lines_offset >= cursor && cursor_pos.is_none() {
current_row = Some(row);
cursor_pos = Some(line_origin); cursor_pos = Some(line_origin);
} }
if prev_lines_offset >= selected_range.start && cursor_start.is_none() { if prev_lines_offset >= selected_range.start && cursor_start.is_none() {
@@ -494,7 +476,7 @@ impl TextElement {
bounds.origin += scroll_offset; bounds.origin += scroll_offset;
(cursor_bounds, scroll_offset, current_row) (cursor_bounds, scroll_offset)
} }
/// Layout the match range to a Path. /// Layout the match range to a Path.
@@ -642,41 +624,6 @@ impl TextElement {
builder.build().ok() builder.build().ok()
} }
fn layout_search_matches(
&self,
_last_layout: &LastLayout,
_bounds: &Bounds<Pixels>,
_cx: &mut App,
) -> Vec<(Path<Pixels>, bool)> {
vec![]
}
fn layout_hover_highlight(
&self,
_last_layout: &LastLayout,
_bounds: &Bounds<Pixels>,
_cx: &mut App,
) -> Option<Path<Pixels>> {
None
}
fn layout_document_colors(
&self,
document_colors: &[(Range<usize>, Hsla)],
last_layout: &LastLayout,
bounds: &Bounds<Pixels>,
_cx: &mut App,
) -> Vec<(Path<Pixels>, 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( fn layout_selections(
&self, &self,
last_layout: &LastLayout, last_layout: &LastLayout,
@@ -784,267 +731,20 @@ impl TextElement {
(visible_range, visible_buffer_lines, visible_top) (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). /// Layout shaped lines for whitespace indicators (space and tab).
/// ///
/// Returns `WhitespaceIndicators` with shaped lines for space and tab characters. /// Returns `WhitespaceIndicators` with shaped lines for space and tab characters.
fn layout_whitespace_indicators( fn layout_whitespace_indicators(
state: &InputState, _state: &InputState,
text_size: Pixels, text_size: Pixels,
style: &TextStyle, style: &TextStyle,
window: &mut Window, window: &mut Window,
cx: &App, cx: &App,
) -> Option<WhitespaceIndicators> { ) -> Option<WhitespaceIndicators> {
if !state.show_whitespaces { // Whitespace indicators are not currently enabled.
return None; // When re-enabled, check `state.show_whitespaces` to conditionally enable.
} let _ = (text_size, style, window, cx);
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<usize>,
_font_size: Pixels,
_window: &mut Window,
_cx: &App,
) -> (Option<ShapedLine>, Vec<ShapedLine>) {
(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<Pixels>,
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<FoldInfo> = {
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<usize>,
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);
}
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@@ -1098,10 +798,6 @@ impl TextElement {
} }
let mut lines = Vec::with_capacity(last_layout.visible_buffer_lines.len()); 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() { for (vi, &buffer_line) in last_layout.visible_buffer_lines.iter().enumerate() {
let line_text: String = display_text.slice_line(buffer_line).into(); 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()); debug_assert_eq!(line_item.len(), line_text.len());
let mut wrapped_lines = SmallVec::with_capacity(1); 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 { 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() { let line_runs = if bg_segments.is_empty() {
line_runs line_runs
} else { } else {
@@ -1137,107 +834,21 @@ impl TextElement {
.lines(wrapped_lines) .lines(wrapped_lines)
.with_whitespaces(whitespace_indicators.clone()); .with_whitespaces(whitespace_indicators.clone());
lines.push(line_layout); lines.push(line_layout);
// +1 for the `\n`
run_offset += line_text.len() + 1;
} }
lines lines
} }
/// First usize is the offset of skipped.
fn highlight_lines(
&mut self,
visible_buffer_lines: &[usize],
_visible_top: Pixels,
_visible_byte_range: Range<usize>,
cx: &mut App,
) -> Option<Vec<(Range<usize>, 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<usize> = 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 { pub(super) struct PrepaintState {
/// The lines of entire lines. /// The lines of entire lines.
last_layout: LastLayout, 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<Vec<SmallVec<[ShapedLine; 1]>>>,
/// Size of the scrollable area by entire lines. /// Size of the scrollable area by entire lines.
scroll_size: Size<Pixels>, scroll_size: Size<Pixels>,
cursor_bounds: Option<Bounds<Pixels>>, cursor_bounds: Option<Bounds<Pixels>>,
cursor_scroll_offset: Point<Pixels>, cursor_scroll_offset: Point<Pixels>,
/// row index (zero based), no wrap, same line as the cursor.
current_row: Option<usize>,
selection_path: Option<Path<Pixels>>, selection_path: Option<Path<Pixels>>,
hover_highlight_path: Option<Path<Pixels>>,
search_match_paths: Vec<(Path<Pixels>, bool)>,
document_color_paths: Vec<(Path<Pixels>, Hsla)>,
hover_definition_hitbox: Option<Hitbox>,
indent_guides_path: Option<Path<Pixels>>,
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
/// 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<ShapedLine>,
/// First line of inline completion (painted after cursor on same line)
ghost_first_line: Option<ShapedLine>,
ghost_lines_height: Pixels,
} }
impl PrepaintState { impl PrepaintState {
@@ -1356,13 +967,6 @@ impl Element for TextElement {
.text .text
.line_end_offset(visible_range.end.saturating_sub(1)); .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 state = self.state.read(cx);
let multi_line = state.mode.is_multi_line(); let multi_line = state.mode.is_multi_line();
let text = state.text.clone(); let text = state.text.clone();
@@ -1382,9 +986,8 @@ impl Element for TextElement {
(&text, fg) (&text, fg)
}; };
// Calculate the width of the line numbers // Line numbers are not used (code editor mode removed)
let (line_number_width, line_number_len) = let line_number_width = px(0.);
Self::layout_line_numbers(state, &text, text_size, &text_style, window);
let mut bounds = bounds; let mut bounds = bounds;
let wrap_width = if multi_line && state.soft_wrap { let wrap_width = if multi_line && state.soft_wrap {
@@ -1452,28 +1055,7 @@ impl Element for TextElement {
}; };
let runs = if !is_empty { let runs = if !is_empty {
if let Some(highlight_styles) = highlight_styles { vec![run]
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]
}
} else if let Some(ime_marked_range) = &state.ime_marked_range { } else if let Some(ime_marked_range) = &state.ime_marked_range {
// IME marked text // IME marked text
vec![ vec![
@@ -1498,8 +1080,6 @@ impl Element for TextElement {
vec![run] vec![run]
}; };
let document_colors = [];
// Create shaped lines for whitespace indicators before layout // Create shaped lines for whitespace indicators before layout
let whitespace_indicators = let whitespace_indicators =
Self::layout_whitespace_indicators(state, text_size, &text_style, window, cx); Self::layout_whitespace_indicators(state, text_size, &text_style, window, cx);
@@ -1510,7 +1090,7 @@ impl Element for TextElement {
&last_layout, &last_layout,
text_size, text_size,
&runs, &runs,
&document_colors, &[],
whitespace_indicators, whitespace_indicators,
window, window,
); );
@@ -1540,26 +1120,8 @@ impl Element for TextElement {
} }
last_layout.lines = Rc::new(lines); 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 total_wrapped_lines = state.display_map.wrap_row_count();
let empty_bottom_height = if state.mode.is_code_editor() { let empty_bottom_height = px(0.);
bounds
.size
.height
.half()
.max(BOTTOM_MARGIN_ROWS * line_height)
} else {
px(0.)
};
let mut scroll_size = size( let mut scroll_size = size(
if longest_line_width + line_number_width + RIGHT_MARGIN > bounds.size.width { if longest_line_width + line_number_width + RIGHT_MARGIN > bounds.size.width {
@@ -1567,7 +1129,7 @@ impl Element for TextElement {
} else { } else {
longest_line_width 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), .max(bounds.size.height),
); );
@@ -1608,75 +1170,16 @@ impl Element for TextElement {
// Calculate the scroll offset to keep the cursor in view // Calculate the scroll offset to keep the cursor in view
// Save the unscrolled x before layout_cursor modifies bounds.origin with scroll_offset. // Save the bounds 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.
let input_bounds = bounds; 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); self.layout_cursor(&last_layout, &mut bounds, scroll_size, window, cx);
last_layout.cursor_bounds = cursor_bounds; 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 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 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) {
&current_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 state
.editor_scrollbar_snapshot .editor_scrollbar_snapshot
@@ -1688,27 +1191,13 @@ impl Element for TextElement {
state, state,
))); )));
let fold_icon_layout =
self.layout_fold_icons(original_x, &bounds, &last_layout, window, cx);
PrepaintState { PrepaintState {
bounds, bounds,
last_layout, last_layout,
scroll_size, scroll_size,
line_numbers,
cursor_bounds, cursor_bounds,
cursor_scroll_offset, cursor_scroll_offset,
current_row,
selection_path, 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; 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 // Paint selections
if window.is_window_active() { if window.is_window_active()
let secondary_selection = cx.theme().selection; && let Some(path) = prepaint.selection_path.take()
for (path, is_active) in prepaint.search_match_paths.iter() { {
window.paint_path(path.clone(), secondary_selection); window.paint_path(path, cx.theme().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);
}
} }
// Paint document colors // Paint text
for (path, color) in prepaint.document_color_paths.iter() {
window.paint_path(path.clone(), *color);
}
// Paint text with inline completion ghost line support
let mut offset_y = invisible_top_padding; 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 positiveStart from the left position // Keep scrollbar offset always be positiveStart from the left position
let scroll_offset = if text_align == TextAlign::Right { let scroll_offset = if text_align == TextAlign::Right {
@@ -1827,16 +1275,12 @@ impl Element for TextElement {
px(0.) px(0.)
}; };
// Track the y-position of the cursor row for positioning the first line suffix for (line, _buffer_line) in prepaint
let mut cursor_row_y = None;
for (line, &buffer_line) in prepaint
.last_layout .last_layout
.lines .lines
.iter() .iter()
.zip(prepaint.last_layout.visible_buffer_lines.iter()) .zip(prepaint.last_layout.visible_buffer_lines.iter())
{ {
let row = buffer_line;
let line_y = origin.y + offset_y; let line_y = origin.y + offset_y;
let p = point( let p = point(
origin.x + prepaint.last_layout.line_number_width + (scroll_offset), origin.x + prepaint.last_layout.line_number_width + (scroll_offset),
@@ -1853,40 +1297,6 @@ impl Element for TextElement {
cx, cx,
); );
offset_y += line.size(line_height).height; 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 // Paint blinking cursor
@@ -1897,49 +1307,6 @@ impl Element for TextElement {
window.paint_quad(fill(cursor_bounds, cx.theme().cursor)); 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| { self.state.update(cx, |state, cx| {
state.last_layout = Some(prepaint.last_layout.clone()); state.last_layout = Some(prepaint.last_layout.clone());
state.last_bounds = Some(bounds); state.last_bounds = Some(bounds);
@@ -1953,27 +1320,6 @@ impl Element for TextElement {
cx.notify(); 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); self.paint_mouse_listeners(window, cx);
} }
} }

View File

@@ -1,12 +1,8 @@
use gpui::{ use gpui::{Context, EntityInputHandler, SharedString, Window};
Bounds, Context, EntityInputHandler as _, Hsla, Path, PathBuilder, Pixels, SharedString,
TextRun, TextStyle, Window, point, px,
};
use ropey::RopeSlice; use ropey::RopeSlice;
use crate::input::element::TextElement;
use crate::input::mode::InputMode; 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)] #[derive(Debug, Copy, Clone)]
pub struct TabSize { 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<Pixels>,
last_layout: &LastLayout,
text_style: &TextStyle,
window: &mut Window,
) -> Option<Path<Pixels>> {
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 { 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<Self>,
) {
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. /// 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 { pub fn tab_size(mut self, tab: TabSize) -> Self {
debug_assert!(self.mode.is_multi_line() || self.mode.is_code_editor()); debug_assert!(self.mode.is_multi_line());
match &mut self.mode { if let InputMode::PlainText { tab: t, .. } = &mut self.mode {
InputMode::PlainText { tab: t, .. } => *t = tab, *t = tab;
InputMode::CodeEditor { tab: t, .. } => *t = tab,
_ => {}
} }
self self
} }

View File

@@ -234,12 +234,6 @@ impl RenderOnce for Input {
let (bg, _) = input_style(state.disabled, cx); 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 prefix = self.prefix;
let suffix = self.suffix; let suffix = self.suffix;
let show_clear_button = self.cleanable let show_clear_button = self.cleanable

View File

@@ -18,9 +18,7 @@ mod state;
pub(crate) use clear_button::*; pub(crate) use clear_button::*;
pub use cursor::*; pub use cursor::*;
#[cfg(target_family = "wasm")] pub use display_map::DisplayMap;
pub use display_map::folding::Tree;
pub use display_map::{BufferPoint, DisplayMap, DisplayPoint, FoldRange};
pub use indent::TabSize; pub use indent::TabSize;
pub use input::*; pub use input::*;
pub use mask_pattern::MaskPattern; pub use mask_pattern::MaskPattern;

View File

@@ -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 super::display_map::DisplayMap;
use crate::input::TabSize;
#[allow(dead_code)]
pub(super) struct PendingBackgroundParse {
pub parse_task: Rc<RefCell<Option<Task<()>>>>,
pub language: SharedString,
pub text: Rope,
pub is_folding: bool,
}
#[derive(Clone)] #[derive(Clone)]
pub(crate) enum InputMode { pub(crate) enum InputMode {
/// A plain text input mode. /// A plain text input mode.
PlainText { PlainText {
multi_line: bool, multi_line: bool,
tab: TabSize, tab: crate::input::indent::TabSize,
rows: usize, rows: usize,
}, },
/// An auto grow input mode. /// An auto grow input mode.
@@ -29,18 +14,6 @@ pub(crate) enum InputMode {
min_rows: usize, min_rows: usize,
max_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<RefCell<Option<Task<()>>>>,
},
} }
impl Default for InputMode { impl Default for InputMode {
@@ -55,25 +28,11 @@ impl InputMode {
pub(super) fn plain_text() -> Self { pub(super) fn plain_text() -> Self {
InputMode::PlainText { InputMode::PlainText {
multi_line: false, multi_line: false,
tab: TabSize::default(), tab: crate::input::indent::TabSize::default(),
rows: 1, rows: 1,
} }
} }
/// Create a code editor input mode with default settings.
pub(super) fn code_editor(language: impl Into<SharedString>) -> 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. /// Create an auto grow input mode with given min and max rows.
pub(super) fn auto_grow(min_rows: usize, max_rows: usize) -> Self { pub(super) fn auto_grow(min_rows: usize, max_rows: usize) -> Self {
InputMode::AutoGrow { InputMode::AutoGrow {
@@ -86,7 +45,6 @@ impl InputMode {
pub(super) fn multi_line(mut self, multi_line: bool) -> Self { pub(super) fn multi_line(mut self, multi_line: bool) -> Self {
match &mut self { match &mut self {
InputMode::PlainText { multi_line: ml, .. } => *ml = multi_line, InputMode::PlainText { multi_line: ml, .. } => *ml = multi_line,
InputMode::CodeEditor { multi_line: ml, .. } => *ml = multi_line,
InputMode::AutoGrow { .. } => {} InputMode::AutoGrow { .. } => {}
} }
self self
@@ -97,28 +55,6 @@ impl InputMode {
!self.is_multi_line() !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] #[inline]
pub(super) fn is_auto_grow(&self) -> bool { pub(super) fn is_auto_grow(&self) -> bool {
matches!(self, InputMode::AutoGrow { .. }) matches!(self, InputMode::AutoGrow { .. })
@@ -128,7 +64,6 @@ impl InputMode {
pub(super) fn is_multi_line(&self) -> bool { pub(super) fn is_multi_line(&self) -> bool {
match self { match self {
InputMode::PlainText { multi_line, .. } => *multi_line, InputMode::PlainText { multi_line, .. } => *multi_line,
InputMode::CodeEditor { multi_line, .. } => *multi_line,
InputMode::AutoGrow { max_rows, .. } => *max_rows > 1, InputMode::AutoGrow { max_rows, .. } => *max_rows > 1,
} }
} }
@@ -138,9 +73,6 @@ impl InputMode {
InputMode::PlainText { rows, .. } => { InputMode::PlainText { rows, .. } => {
*rows = new_rows; *rows = new_rows;
} }
InputMode::CodeEditor { rows, .. } => {
*rows = new_rows;
}
InputMode::AutoGrow { InputMode::AutoGrow {
rows, rows,
min_rows, min_rows,
@@ -168,7 +100,6 @@ impl InputMode {
match self { match self {
InputMode::PlainText { rows, .. } => *rows, InputMode::PlainText { rows, .. } => *rows,
InputMode::CodeEditor { rows, .. } => *rows,
InputMode::AutoGrow { rows, .. } => *rows, InputMode::AutoGrow { rows, .. } => *rows,
} }
.max(1) .max(1)
@@ -196,16 +127,19 @@ impl InputMode {
} }
} }
/// Return false if the mode is not [`InputMode::CodeEditor`].
#[inline] #[inline]
pub(super) fn line_number(&self) -> bool { pub(super) fn is_indentable(&self) -> bool {
match self { match self {
InputMode::CodeEditor { InputMode::PlainText { multi_line, .. } => *multi_line,
line_number,
multi_line,
..
} => *line_number && *multi_line,
_ => false, _ => false,
} }
} }
#[inline]
pub(super) fn tab_size(&self) -> crate::input::indent::TabSize {
match self {
InputMode::PlainText { tab, .. } => *tab,
_ => crate::input::indent::TabSize::default(),
}
}
} }

View File

@@ -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<CodeActionMenu>,
items: Vec<Rc<CodeActionItem>>,
selected_ix: usize,
}
impl MenuDelegate {
fn set_items(&mut self, items: Vec<CodeActionItem>) {
self.items = items.into_iter().map(Rc::new).collect();
self.selected_ix = 0;
}
fn selected_item(&self) -> Option<&Rc<CodeActionItem>> {
self.items.get(self.selected_ix)
}
}
#[derive(IntoElement)]
struct MenuItem {
ix: usize,
item: Rc<CodeActionItem>,
children: Vec<AnyElement>,
selected: bool,
}
impl MenuItem {
fn new(ix: usize, item: Rc<CodeActionItem>) -> 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<Item = AnyElement>) {
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<DismissEvent> 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<ListState<Self>>,
) -> Option<Self::Item> {
let item = self.items.get(ix.row)?;
Some(MenuItem::new(ix.row, item.clone()))
}
fn set_selected_index(
&mut self,
ix: Option<crate::IndexPath>,
_: &mut Window,
cx: &mut Context<ListState<Self>>,
) {
self.selected_ix = ix.map(|i| i.row).unwrap_or(0);
cx.notify();
}
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
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<InputState>,
list: Entity<ListState<MenuDelegate>>,
open: bool,
_subscriptions: Vec<Subscription>,
}
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<InputState>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
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<Self>) {
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<dyn Action>,
window: &mut Window,
cx: &mut Context<Self>,
) -> 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<Self>) {
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>) {
self.hide(cx);
}
fn on_action_up(&mut self, window: &mut Window, cx: &mut Context<Self>) {
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>) {
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>) {
self.open = false;
cx.notify();
}
pub(crate) fn show(
&mut self,
offset: usize,
items: impl Into<Vec<CodeActionItem>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
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<Point<Pixels>> {
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<Self>) -> 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()
}
}

View File

@@ -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<CompletionMenu>,
items: Vec<Rc<CompletionItem>>,
selected_ix: usize,
}
impl ContextMenuDelegate {
fn set_items(&mut self, items: Vec<CompletionItem>) {
self.items = items.into_iter().map(Rc::new).collect();
self.selected_ix = 0;
}
fn selected_item(&self) -> Option<&Rc<CompletionItem>> {
self.items.get(self.selected_ix)
}
}
#[derive(IntoElement)]
struct CompletionMenuItem {
ix: usize,
item: Rc<CompletionItem>,
children: Vec<AnyElement>,
selected: bool,
highlight_prefix: SharedString,
}
impl CompletionMenuItem {
fn new(ix: usize, item: Rc<CompletionItem>) -> Self {
Self {
ix,
item,
children: vec![],
selected: false,
highlight_prefix: "".into(),
}
}
fn highlight_prefix(mut self, s: impl Into<SharedString>) -> 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<Item = AnyElement>) {
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<DismissEvent> 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<ListState<Self>>,
) -> Option<Self::Item> {
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<crate::IndexPath>,
_: &mut Window,
cx: &mut Context<ListState<Self>>,
) {
self.selected_ix = ix.map(|i| i.row).unwrap_or(0);
cx.notify();
}
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
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<InputState>,
list: Entity<ListState<ContextMenuDelegate>>,
open: bool,
/// The offset of the first character that triggered the completion.
pub(crate) trigger_start_offset: Option<usize>,
query: SharedString,
_subscriptions: Vec<Subscription>,
}
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<InputState>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
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<Self>) {
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<dyn Action>,
window: &mut Window,
cx: &mut Context<Self>,
) -> 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<Self>) {
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>) {
self.hide(cx);
}
fn on_action_up(&mut self, window: &mut Window, cx: &mut Context<Self>) {
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>) {
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>) {
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<SharedString>) {
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<Vec<CompletionItem>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
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<Point<Pixels>> {
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<Self>) -> 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()
}
}

View File

@@ -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<InputState>,
menu: Entity<PopupMenu>,
mouse_position: Point<Pixels>,
open: bool,
_subscriptions: Vec<Subscription>,
}
impl InputState {
pub(crate) fn handle_right_click_menu(
&mut self,
event: &MouseDownEvent,
offset: usize,
window: &mut Window,
cx: &mut Context<Self>,
) {
// 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<InputState>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
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>) {
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<Self>) -> 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()
}
}

View File

@@ -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<InputState>,
pub(crate) diagnostic: Rc<DiagnosticEntry>,
bounds: Bounds<Pixels>,
open: bool,
}
impl DiagnosticPopover {
pub fn new(
diagnostic: &DiagnosticEntry,
state: Entity<InputState>,
cx: &mut App,
) -> Entity<Self> {
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>) {
self.open = true;
cx.notify();
}
pub(crate) fn hide(&mut self, cx: &mut Context<Self>) {
self.open = false;
cx.notify();
}
pub(crate) fn check_to_hide(&mut self, mouse_position: Point<Pixels>, cx: &mut Context<Self>) {
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<Self>) -> 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()
}
}

View File

@@ -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<InputState>,
/// The symbol range byte of the hover trigger.
pub(crate) symbol_range: Range<usize>,
pub(crate) hover: Rc<lsp_types::Hover>,
}
impl HoverPopover {
pub fn new(
editor: Entity<InputState>,
symbol_range: Range<usize>,
hover: &lsp_types::Hover,
cx: &mut App,
) -> Entity<Self> {
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<Self>) -> 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::<Vec<_>>()
.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<InputState>,
range: Range<usize>,
width_limit: Range<Pixels>,
content_builder: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
}
impl Styled for Popover {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl Popover {
pub fn new<F, E>(
id: impl Into<ElementId>,
editor: Entity<InputState>,
range: Range<usize>,
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<Bounds<Pixels>> {
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<Pixels>,
element: Option<AnyElement>,
}
impl Element for Popover {
type RequestLayoutState = PopoverLayoutState;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
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<Pixels>,
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<Pixels>,
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);
});
}
})
}
}

View File

@@ -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<CompletionMenu>),
CodeAction(Entity<CodeActionMenu>),
RightClick(Entity<InputContextMenu>),
}
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(),
}
}
}

View File

@@ -99,9 +99,6 @@ actions!(
MoveToPreviousWord, MoveToPreviousWord,
MoveToNextWord, MoveToNextWord,
Escape, Escape,
ToggleCodeActions,
Search,
GoToDefinition,
] ]
); );
@@ -250,14 +247,6 @@ pub(crate) fn init(cx: &mut App) {
KeyBinding::new("ctrl-z", Undo, Some(CONTEXT)), KeyBinding::new("ctrl-z", Undo, Some(CONTEXT)),
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-y", Redo, Some(CONTEXT)), 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) clean_on_escape: bool,
pub(super) submit_on_enter: bool, pub(super) submit_on_enter: bool,
pub(super) soft_wrap: 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. /// This flag tells the renderer to prefer the end of the current visual line.
pub(crate) cursor_line_end_affinity: bool, pub(crate) cursor_line_end_affinity: bool,
pub(super) pattern: Option<regex::Regex>, pub(super) pattern: Option<regex::Regex>,
@@ -435,7 +423,6 @@ impl InputState {
clean_on_escape: false, clean_on_escape: false,
submit_on_enter: false, submit_on_enter: false,
soft_wrap: true, soft_wrap: true,
show_whitespaces: false,
loading: false, loading: false,
pattern: None, pattern: None,
validate: None, validate: None,
@@ -480,33 +467,6 @@ impl InputState {
self 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<SharedString>) -> Self {
let language: SharedString = language.into();
self.mode = InputMode::code_editor(language);
self
}
/// Set whether search UI allows replacement, default is true. /// Set whether search UI allows replacement, default is true.
pub fn replaceable(mut self, allow: bool) -> Self { pub fn replaceable(mut self, allow: bool) -> Self {
self.replaceable = allow; self.replaceable = allow;
@@ -519,49 +479,6 @@ impl InputState {
self 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<Self>) {
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<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;
}
cx.notify();
}
/// Set the number of rows for the multi-line Textarea. /// Set the number of rows for the multi-line Textarea.
/// ///
/// This is only used when `multi_line` is set to true. /// This is only used when `multi_line` is set to true.
@@ -569,9 +486,7 @@ impl InputState {
/// default: 2 /// default: 2
pub fn rows(mut self, rows: usize) -> Self { pub fn rows(mut self, rows: usize) -> Self {
match &mut self.mode { match &mut self.mode {
InputMode::PlainText { rows: r, .. } | InputMode::CodeEditor { rows: r, .. } => { InputMode::PlainText { rows: r, .. } => *r = rows,
*r = rows
}
InputMode::AutoGrow { InputMode::AutoGrow {
max_rows: max_r, max_rows: max_r,
rows: r, rows: r,
@@ -584,31 +499,6 @@ impl InputState {
self self
} }
/// Set highlighter language for for [`InputMode::CodeEditor`] mode.
pub fn set_highlighter(
&mut self,
new_language: impl Into<SharedString>,
cx: &mut Context<Self>,
) {
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<Self>) {
if let InputMode::CodeEditor { parse_task, .. } = &mut self.mode {
parse_task.borrow_mut().take();
}
cx.notify();
}
/// Set placeholder /// Set placeholder
pub fn set_placeholder( pub fn set_placeholder(
&mut self, &mut self,
@@ -726,7 +616,6 @@ impl InputState {
let text: SharedString = text.into(); let text: SharedString = text.into();
let range = 0..self.text.chars().map(|c| c.len_utf16()).sum(); let range = 0..self.text.chars().map(|c| c.len_utf16()).sum();
self.replace_text_in_range_silent(Some(range), &text, window, cx); self.replace_text_in_range_silent(Some(range), &text, window, cx);
self.reset_highlighter(cx);
self.disabled = was_disabled; self.disabled = was_disabled;
} }
@@ -779,12 +668,6 @@ impl InputState {
self 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. /// 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<Self>) { pub fn set_soft_wrap(&mut self, wrap: bool, _: &mut Window, cx: &mut Context<Self>) {
debug_assert!(self.mode.is_multi_line()); debug_assert!(self.mode.is_multi_line());
@@ -808,12 +691,6 @@ impl InputState {
cx.notify(); cx.notify();
} }
/// Update whether to show whitespace characters.
pub fn set_show_whitespaces(&mut self, show: bool, _: &mut Window, cx: &mut Context<Self>) {
self.show_whitespaces = show;
cx.notify();
}
/// Set the regular expression pattern of the input field. /// Set the regular expression pattern of the input field.
/// ///
/// Only for [`InputMode::SingleLine`] mode. /// Only for [`InputMode::SingleLine`] mode.
@@ -1038,7 +915,7 @@ impl InputState {
let row = self.text.offset_to_point(self.cursor()).row; let row = self.text.offset_to_point(self.cursor()).row;
let logical_start = self.text.line_start_offset(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()); let wrap_point = self.display_map.offset_to_wrap_display_point(self.cursor());
if let Some(line) = self.display_map.lines().get(row) if let Some(line) = self.display_map.lines().get(row)
&& let Some(range) = line.wrapped_lines.get(wrap_point.local_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_start = self.text.line_start_offset(row);
let logical_end = self.text.line_end_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()); let wrap_point = self.display_map.offset_to_wrap_display_point(self.cursor());
if let Some(line) = self.display_map.lines().get(row) if let Some(line) = self.display_map.lines().get(row)
&& let Some(range) = line.wrapped_lines.get(wrap_point.local_row) && let Some(range) = line.wrapped_lines.get(wrap_point.local_row)
@@ -1264,7 +1141,7 @@ impl InputState {
if insert_newline { if insert_newline {
// Get current line indent // Get current line indent
let indent = if self.mode.is_code_editor() { let indent = if self.mode.is_indentable() {
self.indent_of_next_line() self.indent_of_next_line()
} else { } else {
"".to_string() "".to_string()
@@ -1465,11 +1342,7 @@ impl InputState {
// Check if row_offset_y is out of the viewport // Check if row_offset_y is out of the viewport
// If row offset is not in the viewport, scroll to make it visible // 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() { let edge_height = line_height;
3 * line_height
} else {
line_height
};
if row_offset_y - edge_height + line_height < -scroll_offset.y { if row_offset_y - edge_height + line_height < -scroll_offset.y {
// Scroll up // Scroll up
scroll_offset.y = -row_offset_y + edge_height - line_height; 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) 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 { pub(super) fn previous_boundary(&self, offset: usize) -> usize {
let mut offset = self.text.clip_offset(offset.saturating_sub(1), Bias::Left); let mut offset = self.text.clip_offset(offset.saturating_sub(1), Bias::Left);
if let Some(ch) = self.text.char_at(offset) if let Some(ch) = self.text.char_at(offset)
@@ -1785,7 +1630,7 @@ impl InputState {
offset -= 1; offset -= 1;
} }
self.clamp_offset_to_visible_backward(offset) offset
} }
pub(super) fn next_boundary(&self, offset: usize) -> usize { pub(super) fn next_boundary(&self, offset: usize) -> usize {
@@ -1796,7 +1641,7 @@ impl InputState {
offset += 1; 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. /// Returns the true to let InputElement to render cursor, when Input is focused and current BlinkCursor is visible.