Generated
+18
@@ -1719,6 +1719,23 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
"collections",
|
||||
"editor",
|
||||
"gpui",
|
||||
"postage",
|
||||
"regex",
|
||||
"smol",
|
||||
"theme",
|
||||
"unindent",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.2.0"
|
||||
@@ -5725,6 +5742,7 @@ dependencies = [
|
||||
"editor",
|
||||
"env_logger",
|
||||
"file_finder",
|
||||
"find",
|
||||
"fsevent",
|
||||
"futures",
|
||||
"fuzzy",
|
||||
|
||||
@@ -217,7 +217,7 @@ impl ChatPanel {
|
||||
let theme = &self.settings.borrow().theme;
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Container::new(ChildView::new(self.channel_select.id()).boxed())
|
||||
Container::new(ChildView::new(&self.channel_select).boxed())
|
||||
.with_style(theme.chat_panel.channel_select.container)
|
||||
.boxed(),
|
||||
)
|
||||
@@ -282,7 +282,7 @@ impl ChatPanel {
|
||||
|
||||
fn render_input_box(&self) -> ElementBox {
|
||||
let theme = &self.settings.borrow().theme;
|
||||
Container::new(ChildView::new(self.input_editor.id()).boxed())
|
||||
Container::new(ChildView::new(&self.input_editor).boxed())
|
||||
.with_style(theme.chat_panel.input_editor.container)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ impl View for ProjectDiagnosticsEditor {
|
||||
.with_style(theme.container)
|
||||
.boxed()
|
||||
} else {
|
||||
ChildView::new(self.editor.id()).boxed()
|
||||
ChildView::new(&self.editor).boxed()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+188
-9
@@ -9,12 +9,13 @@ mod test;
|
||||
|
||||
use aho_corasick::AhoCorasick;
|
||||
use clock::ReplicaId;
|
||||
use collections::{HashMap, HashSet};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
pub use display_map::DisplayPoint;
|
||||
use display_map::*;
|
||||
pub use element::*;
|
||||
use gpui::{
|
||||
action,
|
||||
color::Color,
|
||||
elements::*,
|
||||
fonts::TextStyle,
|
||||
geometry::vector::{vec2f, Vector2F},
|
||||
@@ -28,23 +29,25 @@ use language::{
|
||||
AnchorRangeExt as _, BracketPair, Buffer, Diagnostic, DiagnosticSeverity, Language, Point,
|
||||
Selection, SelectionGoal, TransactionId,
|
||||
};
|
||||
use multi_buffer::MultiBufferChunks;
|
||||
pub use multi_buffer::{
|
||||
Anchor, AnchorRangeExt, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, ToPoint,
|
||||
Anchor, AnchorRangeExt, ExcerptId, ExcerptProperties, MultiBuffer, MultiBufferSnapshot,
|
||||
ToOffset, ToPoint,
|
||||
};
|
||||
use multi_buffer::{MultiBufferChunks, MultiBufferSnapshot};
|
||||
use postage::watch;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
use smol::Timer;
|
||||
use std::{
|
||||
cmp,
|
||||
any::TypeId,
|
||||
cmp::{self, Ordering},
|
||||
iter::{self, FromIterator},
|
||||
mem,
|
||||
ops::{Deref, Range, RangeInclusive, Sub},
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use sum_tree::Bias;
|
||||
pub use sum_tree::Bias;
|
||||
use text::rope::TextDimension;
|
||||
use theme::{DiagnosticStyle, EditorStyle};
|
||||
use util::post_inc;
|
||||
@@ -382,6 +385,7 @@ pub struct Editor {
|
||||
vertical_scroll_margin: f32,
|
||||
placeholder_text: Option<Arc<str>>,
|
||||
highlighted_rows: Option<Range<u32>>,
|
||||
highlighted_ranges: BTreeMap<TypeId, (Color, Vec<Range<Anchor>>)>,
|
||||
nav_history: Option<ItemNavHistory>,
|
||||
}
|
||||
|
||||
@@ -435,6 +439,14 @@ pub struct NavigationData {
|
||||
offset: usize,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
|
||||
pub enum CharKind {
|
||||
Newline,
|
||||
Punctuation,
|
||||
Whitespace,
|
||||
Word,
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn single_line(build_settings: BuildSettings, cx: &mut ViewContext<Self>) -> Self {
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
|
||||
@@ -522,6 +534,7 @@ impl Editor {
|
||||
vertical_scroll_margin: 3.0,
|
||||
placeholder_text: None,
|
||||
highlighted_rows: None,
|
||||
highlighted_ranges: Default::default(),
|
||||
nav_history: None,
|
||||
};
|
||||
let selection = Selection {
|
||||
@@ -848,7 +861,7 @@ impl Editor {
|
||||
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let buffer = &display_map.buffer_snapshot;
|
||||
let newest_selection = self.newest_selection_internal().unwrap().clone();
|
||||
let newest_selection = self.newest_anchor_selection().unwrap().clone();
|
||||
|
||||
let start;
|
||||
let end;
|
||||
@@ -1078,6 +1091,11 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
if self.mode != EditorMode::Full {
|
||||
cx.propagate_action();
|
||||
return;
|
||||
}
|
||||
|
||||
if self.active_diagnostics.is_some() {
|
||||
self.dismiss_diagnostics(cx);
|
||||
} else if let Some(PendingSelection { selection, .. }) = self.pending_selection.take() {
|
||||
@@ -3345,10 +3363,10 @@ impl Editor {
|
||||
&self,
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
) -> Selection<D> {
|
||||
self.resolve_selection(self.newest_selection_internal().unwrap(), snapshot)
|
||||
self.resolve_selection(self.newest_anchor_selection().unwrap(), snapshot)
|
||||
}
|
||||
|
||||
pub fn newest_selection_internal(&self) -> Option<&Selection<Anchor>> {
|
||||
pub fn newest_anchor_selection(&self) -> Option<&Selection<Anchor>> {
|
||||
self.pending_selection
|
||||
.as_ref()
|
||||
.map(|s| &s.selection)
|
||||
@@ -3364,7 +3382,7 @@ impl Editor {
|
||||
T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug,
|
||||
{
|
||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||
let old_cursor_position = self.newest_selection_internal().map(|s| s.head());
|
||||
let old_cursor_position = self.newest_anchor_selection().map(|s| s.head());
|
||||
selections.sort_unstable_by_key(|s| s.start);
|
||||
|
||||
// Merge overlapping selections.
|
||||
@@ -3498,6 +3516,7 @@ impl Editor {
|
||||
buffer.set_active_selections(&self.selections, cx)
|
||||
});
|
||||
}
|
||||
cx.emit(Event::SelectionsChanged);
|
||||
}
|
||||
|
||||
pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext<Self>) {
|
||||
@@ -3721,6 +3740,76 @@ impl Editor {
|
||||
self.highlighted_rows.clone()
|
||||
}
|
||||
|
||||
pub fn highlight_ranges<T: 'static>(
|
||||
&mut self,
|
||||
ranges: Vec<Range<Anchor>>,
|
||||
color: Color,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.highlighted_ranges
|
||||
.insert(TypeId::of::<T>(), (color, ranges));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn clear_highlighted_ranges<T: 'static>(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.highlighted_ranges.remove(&TypeId::of::<T>());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub fn all_highlighted_ranges(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Vec<(Range<DisplayPoint>, Color)> {
|
||||
let snapshot = self.snapshot(cx);
|
||||
let buffer = &snapshot.buffer_snapshot;
|
||||
let start = buffer.anchor_before(0);
|
||||
let end = buffer.anchor_after(buffer.len());
|
||||
self.highlighted_ranges_in_range(start..end, &snapshot)
|
||||
}
|
||||
|
||||
pub fn highlighted_ranges_for_type<T: 'static>(&self) -> Option<(Color, &[Range<Anchor>])> {
|
||||
self.highlighted_ranges
|
||||
.get(&TypeId::of::<T>())
|
||||
.map(|(color, ranges)| (*color, ranges.as_slice()))
|
||||
}
|
||||
|
||||
pub fn highlighted_ranges_in_range(
|
||||
&self,
|
||||
search_range: Range<Anchor>,
|
||||
display_snapshot: &DisplaySnapshot,
|
||||
) -> Vec<(Range<DisplayPoint>, Color)> {
|
||||
let mut results = Vec::new();
|
||||
let buffer = &display_snapshot.buffer_snapshot;
|
||||
for (color, ranges) in self.highlighted_ranges.values() {
|
||||
let start_ix = match ranges.binary_search_by(|probe| {
|
||||
let cmp = probe.end.cmp(&search_range.start, &buffer).unwrap();
|
||||
if cmp.is_gt() {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
Ordering::Less
|
||||
}
|
||||
}) {
|
||||
Ok(i) | Err(i) => i,
|
||||
};
|
||||
for range in &ranges[start_ix..] {
|
||||
if range.start.cmp(&search_range.end, &buffer).unwrap().is_ge() {
|
||||
break;
|
||||
}
|
||||
let start = range
|
||||
.start
|
||||
.to_point(buffer)
|
||||
.to_display_point(display_snapshot);
|
||||
let end = range
|
||||
.end
|
||||
.to_point(buffer)
|
||||
.to_display_point(display_snapshot);
|
||||
results.push((start..end, *color))
|
||||
}
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
fn next_blink_epoch(&mut self) -> usize {
|
||||
self.blink_epoch += 1;
|
||||
self.blink_epoch
|
||||
@@ -3934,6 +4023,7 @@ pub enum Event {
|
||||
Dirtied,
|
||||
Saved,
|
||||
TitleChanged,
|
||||
SelectionsChanged,
|
||||
Closed,
|
||||
}
|
||||
|
||||
@@ -4159,6 +4249,18 @@ pub fn settings_builder(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn char_kind(c: char) -> CharKind {
|
||||
if c == '\n' {
|
||||
CharKind::Newline
|
||||
} else if c.is_whitespace() {
|
||||
CharKind::Whitespace
|
||||
} else if c.is_alphanumeric() || c == '_' {
|
||||
CharKind::Word
|
||||
} else {
|
||||
CharKind::Punctuation
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -6555,6 +6657,83 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) {
|
||||
let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
|
||||
let settings = EditorSettings::test(&cx);
|
||||
let (_, editor) = cx.add_window(Default::default(), |cx| {
|
||||
build_editor(buffer.clone(), settings, cx)
|
||||
});
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
struct Type1;
|
||||
struct Type2;
|
||||
|
||||
let buffer = buffer.read(cx).snapshot(cx);
|
||||
|
||||
let anchor_range = |range: Range<Point>| {
|
||||
buffer.anchor_after(range.start)..buffer.anchor_after(range.end)
|
||||
};
|
||||
|
||||
editor.highlight_ranges::<Type1>(
|
||||
vec![
|
||||
anchor_range(Point::new(2, 1)..Point::new(2, 3)),
|
||||
anchor_range(Point::new(4, 2)..Point::new(4, 4)),
|
||||
anchor_range(Point::new(6, 3)..Point::new(6, 5)),
|
||||
anchor_range(Point::new(8, 4)..Point::new(8, 6)),
|
||||
],
|
||||
Color::red(),
|
||||
cx,
|
||||
);
|
||||
editor.highlight_ranges::<Type2>(
|
||||
vec![
|
||||
anchor_range(Point::new(3, 2)..Point::new(3, 5)),
|
||||
anchor_range(Point::new(5, 3)..Point::new(5, 6)),
|
||||
anchor_range(Point::new(7, 4)..Point::new(7, 7)),
|
||||
anchor_range(Point::new(9, 5)..Point::new(9, 8)),
|
||||
],
|
||||
Color::green(),
|
||||
cx,
|
||||
);
|
||||
|
||||
let snapshot = editor.snapshot(cx);
|
||||
assert_eq!(
|
||||
editor.highlighted_ranges_in_range(
|
||||
anchor_range(Point::new(3, 4)..Point::new(7, 4)),
|
||||
&snapshot,
|
||||
),
|
||||
&[
|
||||
(
|
||||
DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4),
|
||||
Color::red(),
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
|
||||
Color::red(),
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5),
|
||||
Color::green(),
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6),
|
||||
Color::green(),
|
||||
),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
editor.highlighted_ranges_in_range(
|
||||
anchor_range(Point::new(5, 6)..Point::new(6, 4)),
|
||||
&snapshot,
|
||||
),
|
||||
&[(
|
||||
DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
|
||||
Color::red(),
|
||||
)]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||
let point = DisplayPoint::new(row as u32, column as u32);
|
||||
point..point
|
||||
|
||||
+141
-69
@@ -312,64 +312,47 @@ impl EditorElement {
|
||||
let end_row = ((scroll_top + bounds.height()) / layout.line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen
|
||||
let max_glyph_width = layout.em_width;
|
||||
let scroll_left = scroll_position.x() * max_glyph_width;
|
||||
let content_origin = bounds.origin() + layout.text_offset;
|
||||
|
||||
cx.scene.push_layer(Some(bounds));
|
||||
|
||||
// Draw selections
|
||||
let corner_radius = 2.5;
|
||||
for (range, color) in &layout.highlighted_ranges {
|
||||
self.paint_highlighted_range(
|
||||
range.clone(),
|
||||
start_row,
|
||||
end_row,
|
||||
*color,
|
||||
0.,
|
||||
0.15 * layout.line_height,
|
||||
layout,
|
||||
content_origin,
|
||||
scroll_top,
|
||||
scroll_left,
|
||||
bounds,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
let mut cursors = SmallVec::<[Cursor; 32]>::new();
|
||||
|
||||
let content_origin = bounds.origin() + layout.text_offset;
|
||||
|
||||
for (replica_id, selections) in &layout.selections {
|
||||
let style = style.replica_selection_style(*replica_id);
|
||||
let corner_radius = 0.15 * layout.line_height;
|
||||
|
||||
for selection in selections {
|
||||
if selection.start != selection.end {
|
||||
let row_range = if selection.end.column() == 0 {
|
||||
cmp::max(selection.start.row(), start_row)
|
||||
..cmp::min(selection.end.row(), end_row)
|
||||
} else {
|
||||
cmp::max(selection.start.row(), start_row)
|
||||
..cmp::min(selection.end.row() + 1, end_row)
|
||||
};
|
||||
|
||||
let selection = Selection {
|
||||
color: style.selection,
|
||||
line_height: layout.line_height,
|
||||
start_y: content_origin.y() + row_range.start as f32 * layout.line_height
|
||||
- scroll_top,
|
||||
lines: row_range
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
let line_layout = &layout.line_layouts[(row - start_row) as usize];
|
||||
SelectionLine {
|
||||
start_x: if row == selection.start.row() {
|
||||
content_origin.x()
|
||||
+ line_layout
|
||||
.x_for_index(selection.start.column() as usize)
|
||||
- scroll_left
|
||||
} else {
|
||||
content_origin.x() - scroll_left
|
||||
},
|
||||
end_x: if row == selection.end.row() {
|
||||
content_origin.x()
|
||||
+ line_layout
|
||||
.x_for_index(selection.end.column() as usize)
|
||||
- scroll_left
|
||||
} else {
|
||||
content_origin.x()
|
||||
+ line_layout.width()
|
||||
+ corner_radius * 2.0
|
||||
- scroll_left
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
selection.paint(bounds, cx.scene);
|
||||
}
|
||||
self.paint_highlighted_range(
|
||||
selection.start..selection.end,
|
||||
start_row,
|
||||
end_row,
|
||||
style.selection,
|
||||
corner_radius,
|
||||
corner_radius * 2.,
|
||||
layout,
|
||||
content_origin,
|
||||
scroll_top,
|
||||
scroll_left,
|
||||
bounds,
|
||||
cx,
|
||||
);
|
||||
|
||||
if view.show_local_cursors() || *replica_id != local_replica_id {
|
||||
let cursor_position = selection.head();
|
||||
@@ -412,6 +395,63 @@ impl EditorElement {
|
||||
cx.scene.pop_layer();
|
||||
}
|
||||
|
||||
fn paint_highlighted_range(
|
||||
&self,
|
||||
range: Range<DisplayPoint>,
|
||||
start_row: u32,
|
||||
end_row: u32,
|
||||
color: Color,
|
||||
corner_radius: f32,
|
||||
line_end_overshoot: f32,
|
||||
layout: &LayoutState,
|
||||
content_origin: Vector2F,
|
||||
scroll_top: f32,
|
||||
scroll_left: f32,
|
||||
bounds: RectF,
|
||||
cx: &mut PaintContext,
|
||||
) {
|
||||
if range.start != range.end {
|
||||
let row_range = if range.end.column() == 0 {
|
||||
cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
|
||||
} else {
|
||||
cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row)
|
||||
};
|
||||
|
||||
let highlighted_range = HighlightedRange {
|
||||
color,
|
||||
line_height: layout.line_height,
|
||||
corner_radius,
|
||||
start_y: content_origin.y() + row_range.start as f32 * layout.line_height
|
||||
- scroll_top,
|
||||
lines: row_range
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
let line_layout = &layout.line_layouts[(row - start_row) as usize];
|
||||
HighlightedRangeLine {
|
||||
start_x: if row == range.start.row() {
|
||||
content_origin.x()
|
||||
+ line_layout.x_for_index(range.start.column() as usize)
|
||||
- scroll_left
|
||||
} else {
|
||||
content_origin.x() - scroll_left
|
||||
},
|
||||
end_x: if row == range.end.row() {
|
||||
content_origin.x()
|
||||
+ line_layout.x_for_index(range.end.column() as usize)
|
||||
- scroll_left
|
||||
} else {
|
||||
content_origin.x() + line_layout.width() + line_end_overshoot
|
||||
- scroll_left
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
highlighted_range.paint(bounds, cx.scene);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_blocks(
|
||||
&mut self,
|
||||
bounds: RectF,
|
||||
@@ -715,10 +755,16 @@ impl Element for EditorElement {
|
||||
let mut selections = HashMap::default();
|
||||
let mut active_rows = BTreeMap::new();
|
||||
let mut highlighted_rows = None;
|
||||
let mut highlighted_ranges = Vec::new();
|
||||
self.update_view(cx.app, |view, cx| {
|
||||
highlighted_rows = view.highlighted_rows();
|
||||
let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
highlighted_rows = view.highlighted_rows();
|
||||
highlighted_ranges = view.highlighted_ranges_in_range(
|
||||
start_anchor.clone()..end_anchor.clone(),
|
||||
&display_map,
|
||||
);
|
||||
|
||||
let local_selections = view
|
||||
.local_selections_in_range(start_anchor.clone()..end_anchor.clone(), &display_map);
|
||||
for selection in &local_selections {
|
||||
@@ -837,6 +883,7 @@ impl Element for EditorElement {
|
||||
snapshot,
|
||||
active_rows,
|
||||
highlighted_rows,
|
||||
highlighted_ranges,
|
||||
line_layouts,
|
||||
line_number_layouts,
|
||||
blocks,
|
||||
@@ -950,6 +997,7 @@ pub struct LayoutState {
|
||||
line_height: f32,
|
||||
em_width: f32,
|
||||
em_advance: f32,
|
||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
||||
selections: HashMap<ReplicaId, Vec<text::Selection<DisplayPoint>>>,
|
||||
text_offset: Vector2F,
|
||||
}
|
||||
@@ -1036,20 +1084,21 @@ impl Cursor {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Selection {
|
||||
struct HighlightedRange {
|
||||
start_y: f32,
|
||||
line_height: f32,
|
||||
lines: Vec<SelectionLine>,
|
||||
lines: Vec<HighlightedRangeLine>,
|
||||
color: Color,
|
||||
corner_radius: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SelectionLine {
|
||||
struct HighlightedRangeLine {
|
||||
start_x: f32,
|
||||
end_x: f32,
|
||||
}
|
||||
|
||||
impl Selection {
|
||||
impl HighlightedRange {
|
||||
fn paint(&self, bounds: RectF, scene: &mut Scene) {
|
||||
if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x {
|
||||
self.paint_lines(self.start_y, &self.lines[0..1], bounds, scene);
|
||||
@@ -1064,26 +1113,31 @@ impl Selection {
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_lines(&self, start_y: f32, lines: &[SelectionLine], bounds: RectF, scene: &mut Scene) {
|
||||
fn paint_lines(
|
||||
&self,
|
||||
start_y: f32,
|
||||
lines: &[HighlightedRangeLine],
|
||||
bounds: RectF,
|
||||
scene: &mut Scene,
|
||||
) {
|
||||
if lines.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut path = PathBuilder::new();
|
||||
let corner_radius = 0.15 * self.line_height;
|
||||
let first_line = lines.first().unwrap();
|
||||
let last_line = lines.last().unwrap();
|
||||
|
||||
let first_top_left = vec2f(first_line.start_x, start_y);
|
||||
let first_top_right = vec2f(first_line.end_x, start_y);
|
||||
|
||||
let curve_height = vec2f(0., corner_radius);
|
||||
let curve_height = vec2f(0., self.corner_radius);
|
||||
let curve_width = |start_x: f32, end_x: f32| {
|
||||
let max = (end_x - start_x) / 2.;
|
||||
let width = if max < corner_radius {
|
||||
let width = if max < self.corner_radius {
|
||||
max
|
||||
} else {
|
||||
corner_radius
|
||||
self.corner_radius
|
||||
};
|
||||
|
||||
vec2f(width, 0.)
|
||||
@@ -1107,26 +1161,38 @@ impl Selection {
|
||||
Ordering::Less => {
|
||||
let curve_width = curve_width(next_top_right.x(), bottom_right.x());
|
||||
path.line_to(bottom_right - curve_height);
|
||||
path.curve_to(bottom_right - curve_width, bottom_right);
|
||||
if self.corner_radius > 0. {
|
||||
path.curve_to(bottom_right - curve_width, bottom_right);
|
||||
}
|
||||
path.line_to(next_top_right + curve_width);
|
||||
path.curve_to(next_top_right + curve_height, next_top_right);
|
||||
if self.corner_radius > 0. {
|
||||
path.curve_to(next_top_right + curve_height, next_top_right);
|
||||
}
|
||||
}
|
||||
Ordering::Greater => {
|
||||
let curve_width = curve_width(bottom_right.x(), next_top_right.x());
|
||||
path.line_to(bottom_right - curve_height);
|
||||
path.curve_to(bottom_right + curve_width, bottom_right);
|
||||
if self.corner_radius > 0. {
|
||||
path.curve_to(bottom_right + curve_width, bottom_right);
|
||||
}
|
||||
path.line_to(next_top_right - curve_width);
|
||||
path.curve_to(next_top_right + curve_height, next_top_right);
|
||||
if self.corner_radius > 0. {
|
||||
path.curve_to(next_top_right + curve_height, next_top_right);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let curve_width = curve_width(line.start_x, line.end_x);
|
||||
path.line_to(bottom_right - curve_height);
|
||||
path.curve_to(bottom_right - curve_width, bottom_right);
|
||||
if self.corner_radius > 0. {
|
||||
path.curve_to(bottom_right - curve_width, bottom_right);
|
||||
}
|
||||
|
||||
let bottom_left = vec2f(line.start_x, bottom_right.y());
|
||||
path.line_to(bottom_left + curve_width);
|
||||
path.curve_to(bottom_left - curve_height, bottom_left);
|
||||
if self.corner_radius > 0. {
|
||||
path.curve_to(bottom_left - curve_height, bottom_left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1134,14 +1200,20 @@ impl Selection {
|
||||
let curve_width = curve_width(last_line.start_x, first_line.start_x);
|
||||
let second_top_left = vec2f(last_line.start_x, start_y + self.line_height);
|
||||
path.line_to(second_top_left + curve_height);
|
||||
path.curve_to(second_top_left + curve_width, second_top_left);
|
||||
if self.corner_radius > 0. {
|
||||
path.curve_to(second_top_left + curve_width, second_top_left);
|
||||
}
|
||||
let first_bottom_left = vec2f(first_line.start_x, second_top_left.y());
|
||||
path.line_to(first_bottom_left - curve_width);
|
||||
path.curve_to(first_bottom_left - curve_height, first_bottom_left);
|
||||
if self.corner_radius > 0. {
|
||||
path.curve_to(first_bottom_left - curve_height, first_bottom_left);
|
||||
}
|
||||
}
|
||||
|
||||
path.line_to(first_top_left + curve_height);
|
||||
path.curve_to(first_top_left + top_curve_width, first_top_left);
|
||||
if self.corner_radius > 0. {
|
||||
path.curve_to(first_top_left + top_curve_width, first_top_left);
|
||||
}
|
||||
path.line_to(first_top_right - top_curve_width);
|
||||
|
||||
scene.push_path(path.build(self.color, Some(bounds)));
|
||||
|
||||
@@ -141,7 +141,7 @@ impl ItemView for Editor {
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(selection) = self.newest_selection_internal() {
|
||||
if let Some(selection) = self.newest_anchor_selection() {
|
||||
self.push_to_nav_history(selection.head(), None, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
|
||||
use crate::ToPoint;
|
||||
use crate::{char_kind, CharKind, ToPoint};
|
||||
use anyhow::Result;
|
||||
use std::{cmp, ops::Range};
|
||||
|
||||
@@ -215,26 +215,6 @@ pub fn surrounding_word(map: &DisplaySnapshot, point: DisplayPoint) -> Range<Dis
|
||||
..end.to_point(&map.buffer_snapshot).to_display_point(map)
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
|
||||
enum CharKind {
|
||||
Newline,
|
||||
Punctuation,
|
||||
Whitespace,
|
||||
Word,
|
||||
}
|
||||
|
||||
fn char_kind(c: char) -> CharKind {
|
||||
if c == '\n' {
|
||||
CharKind::Newline
|
||||
} else if c.is_whitespace() {
|
||||
CharKind::Whitespace
|
||||
} else if c.is_alphanumeric() || c == '_' {
|
||||
CharKind::Word
|
||||
} else {
|
||||
CharKind::Punctuation
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -76,6 +76,7 @@ struct BufferState {
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct MultiBufferSnapshot {
|
||||
singleton: bool,
|
||||
excerpts: SumTree<Excerpt>,
|
||||
parse_count: usize,
|
||||
diagnostics_update_count: usize,
|
||||
@@ -163,6 +164,7 @@ impl MultiBuffer {
|
||||
},
|
||||
cx,
|
||||
);
|
||||
this.snapshot.borrow_mut().singleton = true;
|
||||
this
|
||||
}
|
||||
|
||||
@@ -1079,11 +1081,9 @@ impl MultiBufferSnapshot {
|
||||
.eq(needle.bytes())
|
||||
}
|
||||
|
||||
fn as_singleton(&self) -> Option<&BufferSnapshot> {
|
||||
let mut excerpts = self.excerpts.iter();
|
||||
let buffer = excerpts.next().map(|excerpt| &excerpt.buffer);
|
||||
if excerpts.next().is_none() {
|
||||
buffer
|
||||
fn as_singleton(&self) -> Option<&Excerpt> {
|
||||
if self.singleton {
|
||||
self.excerpts.iter().next()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -1098,6 +1098,10 @@ impl MultiBufferSnapshot {
|
||||
}
|
||||
|
||||
pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize {
|
||||
if let Some(excerpt) = self.as_singleton() {
|
||||
return excerpt.buffer.clip_offset(offset, bias);
|
||||
}
|
||||
|
||||
let mut cursor = self.excerpts.cursor::<usize>();
|
||||
cursor.seek(&offset, Bias::Right, &());
|
||||
let overshoot = if let Some(excerpt) = cursor.item() {
|
||||
@@ -1113,6 +1117,10 @@ impl MultiBufferSnapshot {
|
||||
}
|
||||
|
||||
pub fn clip_point(&self, point: Point, bias: Bias) -> Point {
|
||||
if let Some(excerpt) = self.as_singleton() {
|
||||
return excerpt.buffer.clip_point(point, bias);
|
||||
}
|
||||
|
||||
let mut cursor = self.excerpts.cursor::<Point>();
|
||||
cursor.seek(&point, Bias::Right, &());
|
||||
let overshoot = if let Some(excerpt) = cursor.item() {
|
||||
@@ -1128,6 +1136,10 @@ impl MultiBufferSnapshot {
|
||||
}
|
||||
|
||||
pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 {
|
||||
if let Some(excerpt) = self.as_singleton() {
|
||||
return excerpt.buffer.clip_point_utf16(point, bias);
|
||||
}
|
||||
|
||||
let mut cursor = self.excerpts.cursor::<PointUtf16>();
|
||||
cursor.seek(&point, Bias::Right, &());
|
||||
let overshoot = if let Some(excerpt) = cursor.item() {
|
||||
@@ -1193,6 +1205,10 @@ impl MultiBufferSnapshot {
|
||||
}
|
||||
|
||||
pub fn offset_to_point(&self, offset: usize) -> Point {
|
||||
if let Some(excerpt) = self.as_singleton() {
|
||||
return excerpt.buffer.offset_to_point(offset);
|
||||
}
|
||||
|
||||
let mut cursor = self.excerpts.cursor::<(usize, Point)>();
|
||||
cursor.seek(&offset, Bias::Right, &());
|
||||
if let Some(excerpt) = cursor.item() {
|
||||
@@ -1210,6 +1226,10 @@ impl MultiBufferSnapshot {
|
||||
}
|
||||
|
||||
pub fn point_to_offset(&self, point: Point) -> usize {
|
||||
if let Some(excerpt) = self.as_singleton() {
|
||||
return excerpt.buffer.point_to_offset(point);
|
||||
}
|
||||
|
||||
let mut cursor = self.excerpts.cursor::<(Point, usize)>();
|
||||
cursor.seek(&point, Bias::Right, &());
|
||||
if let Some(excerpt) = cursor.item() {
|
||||
@@ -1227,6 +1247,10 @@ impl MultiBufferSnapshot {
|
||||
}
|
||||
|
||||
pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize {
|
||||
if let Some(excerpt) = self.as_singleton() {
|
||||
return excerpt.buffer.point_utf16_to_offset(point);
|
||||
}
|
||||
|
||||
let mut cursor = self.excerpts.cursor::<(PointUtf16, usize)>();
|
||||
cursor.seek(&point, Bias::Right, &());
|
||||
if let Some(excerpt) = cursor.item() {
|
||||
@@ -1520,6 +1544,14 @@ impl MultiBufferSnapshot {
|
||||
|
||||
pub fn anchor_at<T: ToOffset>(&self, position: T, mut bias: Bias) -> Anchor {
|
||||
let offset = position.to_offset(self);
|
||||
if let Some(excerpt) = self.as_singleton() {
|
||||
return Anchor {
|
||||
buffer_id: excerpt.buffer_id,
|
||||
excerpt_id: excerpt.id.clone(),
|
||||
text_anchor: excerpt.buffer.anchor_at(offset, bias),
|
||||
};
|
||||
}
|
||||
|
||||
let mut cursor = self.excerpts.cursor::<(usize, Option<&ExcerptId>)>();
|
||||
cursor.seek(&offset, Bias::Right, &());
|
||||
if cursor.item().is_none() && offset == cursor.start().0 && bias == Bias::Left {
|
||||
@@ -1675,7 +1707,7 @@ impl MultiBufferSnapshot {
|
||||
{
|
||||
self.as_singleton()
|
||||
.into_iter()
|
||||
.flat_map(move |buffer| buffer.diagnostic_group(group_id))
|
||||
.flat_map(move |excerpt| excerpt.buffer.diagnostic_group(group_id))
|
||||
}
|
||||
|
||||
pub fn diagnostics_in_range<'a, T, O>(
|
||||
@@ -1686,8 +1718,10 @@ impl MultiBufferSnapshot {
|
||||
T: 'a + ToOffset,
|
||||
O: 'a + text::FromAnchor,
|
||||
{
|
||||
self.as_singleton().into_iter().flat_map(move |buffer| {
|
||||
buffer.diagnostics_in_range(range.start.to_offset(self)..range.end.to_offset(self))
|
||||
self.as_singleton().into_iter().flat_map(move |excerpt| {
|
||||
excerpt
|
||||
.buffer
|
||||
.diagnostics_in_range(range.start.to_offset(self)..range.end.to_offset(self))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1730,17 +1764,16 @@ impl MultiBufferSnapshot {
|
||||
}
|
||||
|
||||
pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
|
||||
let buffer = self.as_singleton()?;
|
||||
let outline = buffer.outline(theme)?;
|
||||
let excerpt_id = &self.excerpts.iter().next().unwrap().id;
|
||||
let excerpt = self.as_singleton()?;
|
||||
let outline = excerpt.buffer.outline(theme)?;
|
||||
Some(Outline::new(
|
||||
outline
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|item| OutlineItem {
|
||||
depth: item.depth,
|
||||
range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start)
|
||||
..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end),
|
||||
range: self.anchor_in_excerpt(excerpt.id.clone(), item.range.start)
|
||||
..self.anchor_in_excerpt(excerpt.id.clone(), item.range.end),
|
||||
text: item.text,
|
||||
highlight_ranges: item.highlight_ranges,
|
||||
name_ranges: item.name_ranges,
|
||||
|
||||
@@ -76,7 +76,7 @@ impl View for FileFinder {
|
||||
Container::new(
|
||||
Flex::new(Axis::Vertical)
|
||||
.with_child(
|
||||
Container::new(ChildView::new(self.query_editor.id()).boxed())
|
||||
Container::new(ChildView::new(&self.query_editor).boxed())
|
||||
.with_style(settings.theme.selector.input_editor.container)
|
||||
.boxed(),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "find"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/find.rs"
|
||||
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
theme = { path = "../theme" }
|
||||
workspace = { path = "../workspace" }
|
||||
aho-corasick = "0.7"
|
||||
anyhow = "1.0"
|
||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||
regex = "1.5"
|
||||
smol = { version = "1.2" }
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
unindent = "0.1"
|
||||
@@ -0,0 +1,945 @@
|
||||
use aho_corasick::AhoCorasickBuilder;
|
||||
use anyhow::Result;
|
||||
use collections::HashSet;
|
||||
use editor::{
|
||||
char_kind, display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor, EditorSettings,
|
||||
MultiBufferSnapshot,
|
||||
};
|
||||
use gpui::{
|
||||
action, elements::*, keymap::Binding, platform::CursorStyle, Entity, MutableAppContext,
|
||||
RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use postage::watch;
|
||||
use regex::RegexBuilder;
|
||||
use smol::future::yield_now;
|
||||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
};
|
||||
use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace};
|
||||
|
||||
action!(Deploy, bool);
|
||||
action!(Dismiss);
|
||||
action!(FocusEditor);
|
||||
action!(ToggleMode, SearchMode);
|
||||
action!(GoToMatch, Direction);
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Direction {
|
||||
Prev,
|
||||
Next,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum SearchMode {
|
||||
WholeWord,
|
||||
CaseSensitive,
|
||||
Regex,
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_bindings([
|
||||
Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")),
|
||||
Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")),
|
||||
Binding::new("escape", Dismiss, Some("FindBar")),
|
||||
Binding::new("cmd-f", FocusEditor, Some("FindBar")),
|
||||
Binding::new("enter", GoToMatch(Direction::Next), Some("FindBar")),
|
||||
Binding::new("shift-enter", GoToMatch(Direction::Prev), Some("FindBar")),
|
||||
Binding::new("cmd-g", GoToMatch(Direction::Next), Some("Pane")),
|
||||
Binding::new("cmd-shift-G", GoToMatch(Direction::Prev), Some("Pane")),
|
||||
]);
|
||||
cx.add_action(FindBar::deploy);
|
||||
cx.add_action(FindBar::dismiss);
|
||||
cx.add_action(FindBar::focus_editor);
|
||||
cx.add_action(FindBar::toggle_mode);
|
||||
cx.add_action(FindBar::go_to_match);
|
||||
cx.add_action(FindBar::go_to_match_on_pane);
|
||||
}
|
||||
|
||||
struct FindBar {
|
||||
settings: watch::Receiver<Settings>,
|
||||
query_editor: ViewHandle<Editor>,
|
||||
active_editor: Option<ViewHandle<Editor>>,
|
||||
active_match_index: Option<usize>,
|
||||
active_editor_subscription: Option<Subscription>,
|
||||
highlighted_editors: HashSet<WeakViewHandle<Editor>>,
|
||||
pending_search: Option<Task<()>>,
|
||||
case_sensitive_mode: bool,
|
||||
whole_word_mode: bool,
|
||||
regex_mode: bool,
|
||||
query_contains_error: bool,
|
||||
}
|
||||
|
||||
impl Entity for FindBar {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for FindBar {
|
||||
fn ui_name() -> &'static str {
|
||||
"FindBar"
|
||||
}
|
||||
|
||||
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.focus(&self.query_editor);
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = &self.settings.borrow().theme;
|
||||
let editor_container = if self.query_contains_error {
|
||||
theme.find.invalid_editor
|
||||
} else {
|
||||
theme.find.editor.input.container
|
||||
};
|
||||
Flex::row()
|
||||
.with_child(
|
||||
ChildView::new(&self.query_editor)
|
||||
.contained()
|
||||
.with_style(editor_container)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_max_width(theme.find.editor.max_width)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(self.render_mode_button("Case", SearchMode::CaseSensitive, cx))
|
||||
.with_child(self.render_mode_button("Word", SearchMode::WholeWord, cx))
|
||||
.with_child(self.render_mode_button("Regex", SearchMode::Regex, cx))
|
||||
.contained()
|
||||
.with_style(theme.find.mode_button_group)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(self.render_nav_button("<", Direction::Prev, cx))
|
||||
.with_child(self.render_nav_button(">", Direction::Next, cx))
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(self.active_editor.as_ref().and_then(|editor| {
|
||||
let (_, highlighted_ranges) =
|
||||
editor.read(cx).highlighted_ranges_for_type::<Self>()?;
|
||||
let message = if let Some(match_ix) = self.active_match_index {
|
||||
format!("{}/{}", match_ix + 1, highlighted_ranges.len())
|
||||
} else {
|
||||
"No matches".to_string()
|
||||
};
|
||||
|
||||
Some(
|
||||
Label::new(message, theme.find.match_index.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.find.match_index.container)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
}))
|
||||
.contained()
|
||||
.with_style(theme.find.container)
|
||||
.constrained()
|
||||
.with_height(theme.workspace.toolbar.height)
|
||||
.named("find bar")
|
||||
}
|
||||
}
|
||||
|
||||
impl Toolbar for FindBar {
|
||||
fn active_item_changed(
|
||||
&mut self,
|
||||
item: Option<Box<dyn ItemViewHandle>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool {
|
||||
self.active_editor_subscription.take();
|
||||
self.active_editor.take();
|
||||
self.pending_search.take();
|
||||
|
||||
if let Some(editor) = item.and_then(|item| item.act_as::<Editor>(cx)) {
|
||||
self.active_editor_subscription =
|
||||
Some(cx.subscribe(&editor, Self::on_active_editor_event));
|
||||
self.active_editor = Some(editor);
|
||||
self.update_matches(cx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn on_dismiss(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.active_editor.take();
|
||||
self.active_editor_subscription.take();
|
||||
self.active_match_index.take();
|
||||
self.pending_search.take();
|
||||
self.clear_matches(cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl FindBar {
|
||||
fn new(settings: watch::Receiver<Settings>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let query_editor = cx.add_view(|cx| {
|
||||
Editor::auto_height(
|
||||
2,
|
||||
{
|
||||
let settings = settings.clone();
|
||||
Arc::new(move |_| {
|
||||
let settings = settings.borrow();
|
||||
EditorSettings {
|
||||
style: settings.theme.find.editor.input.as_editor(),
|
||||
tab_size: settings.tab_size,
|
||||
soft_wrap: editor::SoftWrap::None,
|
||||
}
|
||||
})
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.subscribe(&query_editor, Self::on_query_editor_event)
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
query_editor,
|
||||
active_editor: None,
|
||||
active_editor_subscription: None,
|
||||
active_match_index: None,
|
||||
highlighted_editors: Default::default(),
|
||||
case_sensitive_mode: false,
|
||||
whole_word_mode: false,
|
||||
regex_mode: false,
|
||||
settings,
|
||||
pending_search: None,
|
||||
query_contains_error: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
|
||||
self.query_editor.update(cx, |query_editor, cx| {
|
||||
query_editor.buffer().update(cx, |query_buffer, cx| {
|
||||
let len = query_buffer.read(cx).len();
|
||||
query_buffer.edit([0..len], query, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn render_mode_button(
|
||||
&self,
|
||||
icon: &str,
|
||||
mode: SearchMode,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
let theme = &self.settings.borrow().theme.find;
|
||||
let is_active = self.is_mode_enabled(mode);
|
||||
MouseEventHandler::new::<Self, _, _, _>((cx.view_id(), mode as usize), cx, |state, _| {
|
||||
let style = match (is_active, state.hovered) {
|
||||
(false, false) => &theme.mode_button,
|
||||
(false, true) => &theme.hovered_mode_button,
|
||||
(true, false) => &theme.active_mode_button,
|
||||
(true, true) => &theme.active_hovered_mode_button,
|
||||
};
|
||||
Label::new(icon.to_string(), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.on_click(move |cx| cx.dispatch_action(ToggleMode(mode)))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_nav_button(
|
||||
&self,
|
||||
icon: &str,
|
||||
direction: Direction,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
let theme = &self.settings.borrow().theme.find;
|
||||
MouseEventHandler::new::<Self, _, _, _>(
|
||||
(cx.view_id(), 10 + direction as usize),
|
||||
cx,
|
||||
|state, _| {
|
||||
let style = if state.hovered {
|
||||
&theme.hovered_mode_button
|
||||
} else {
|
||||
&theme.mode_button
|
||||
};
|
||||
Label::new(icon.to_string(), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
},
|
||||
)
|
||||
.on_click(move |cx| cx.dispatch_action(GoToMatch(direction)))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn deploy(workspace: &mut Workspace, Deploy(focus): &Deploy, cx: &mut ViewContext<Workspace>) {
|
||||
let settings = workspace.settings();
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
let findbar_was_visible = pane
|
||||
.active_toolbar()
|
||||
.map_or(false, |toolbar| toolbar.downcast::<Self>().is_some());
|
||||
|
||||
pane.show_toolbar(cx, |cx| FindBar::new(settings, cx));
|
||||
|
||||
if let Some(find_bar) = pane
|
||||
.active_toolbar()
|
||||
.and_then(|toolbar| toolbar.downcast::<Self>())
|
||||
{
|
||||
if !findbar_was_visible {
|
||||
let editor = pane.active_item().unwrap().act_as::<Editor>(cx).unwrap();
|
||||
let display_map = editor
|
||||
.update(cx, |editor, cx| editor.snapshot(cx))
|
||||
.display_snapshot;
|
||||
let selection = editor
|
||||
.read(cx)
|
||||
.newest_selection::<usize>(&display_map.buffer_snapshot);
|
||||
|
||||
let mut text: String;
|
||||
if selection.start == selection.end {
|
||||
let point = selection.start.to_display_point(&display_map);
|
||||
let range = editor::movement::surrounding_word(&display_map, point);
|
||||
let range = range.start.to_offset(&display_map, Bias::Left)
|
||||
..range.end.to_offset(&display_map, Bias::Right);
|
||||
text = display_map.buffer_snapshot.text_for_range(range).collect();
|
||||
if text.trim().is_empty() {
|
||||
text = String::new();
|
||||
}
|
||||
} else {
|
||||
text = display_map
|
||||
.buffer_snapshot
|
||||
.text_for_range(selection.start..selection.end)
|
||||
.collect();
|
||||
}
|
||||
|
||||
if !text.is_empty() {
|
||||
find_bar.update(cx, |find_bar, cx| find_bar.set_query(&text, cx));
|
||||
}
|
||||
}
|
||||
|
||||
if *focus {
|
||||
if !findbar_was_visible {
|
||||
let query_editor = find_bar.read(cx).query_editor.clone();
|
||||
query_editor.update(cx, |query_editor, cx| {
|
||||
query_editor.select_all(&editor::SelectAll, cx);
|
||||
});
|
||||
}
|
||||
cx.focus(&find_bar);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn dismiss(workspace: &mut Workspace, _: &Dismiss, cx: &mut ViewContext<Workspace>) {
|
||||
workspace
|
||||
.active_pane()
|
||||
.update(cx, |pane, cx| pane.dismiss_toolbar(cx));
|
||||
}
|
||||
|
||||
fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
|
||||
if let Some(active_editor) = self.active_editor.as_ref() {
|
||||
cx.focus(active_editor);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_mode_enabled(&self, mode: SearchMode) -> bool {
|
||||
match mode {
|
||||
SearchMode::WholeWord => self.whole_word_mode,
|
||||
SearchMode::CaseSensitive => self.case_sensitive_mode,
|
||||
SearchMode::Regex => self.regex_mode,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_mode(&mut self, ToggleMode(mode): &ToggleMode, cx: &mut ViewContext<Self>) {
|
||||
let value = match mode {
|
||||
SearchMode::WholeWord => &mut self.whole_word_mode,
|
||||
SearchMode::CaseSensitive => &mut self.case_sensitive_mode,
|
||||
SearchMode::Regex => &mut self.regex_mode,
|
||||
};
|
||||
*value = !*value;
|
||||
self.update_matches(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn go_to_match(&mut self, GoToMatch(direction): &GoToMatch, cx: &mut ViewContext<Self>) {
|
||||
if let Some(mut index) = self.active_match_index {
|
||||
if let Some(editor) = self.active_editor.as_ref() {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let newest_selection = editor.newest_anchor_selection().cloned();
|
||||
if let Some(((_, ranges), newest_selection)) = editor
|
||||
.highlighted_ranges_for_type::<Self>()
|
||||
.zip(newest_selection)
|
||||
{
|
||||
let position = newest_selection.head();
|
||||
let buffer = editor.buffer().read(cx).read(cx);
|
||||
if ranges[index].start.cmp(&position, &buffer).unwrap().is_gt() {
|
||||
if *direction == Direction::Prev {
|
||||
if index == 0 {
|
||||
index = ranges.len() - 1;
|
||||
} else {
|
||||
index -= 1;
|
||||
}
|
||||
}
|
||||
} else if ranges[index].end.cmp(&position, &buffer).unwrap().is_lt() {
|
||||
if *direction == Direction::Next {
|
||||
index = 0;
|
||||
}
|
||||
} else if *direction == Direction::Prev {
|
||||
if index == 0 {
|
||||
index = ranges.len() - 1;
|
||||
} else {
|
||||
index -= 1;
|
||||
}
|
||||
} else if *direction == Direction::Next {
|
||||
if index == ranges.len() - 1 {
|
||||
index = 0
|
||||
} else {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let range_to_select = ranges[index].clone();
|
||||
drop(buffer);
|
||||
editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn go_to_match_on_pane(pane: &mut Pane, action: &GoToMatch, cx: &mut ViewContext<Pane>) {
|
||||
if let Some(find_bar) = pane.toolbar::<FindBar>() {
|
||||
find_bar.update(cx, |find_bar, cx| find_bar.go_to_match(action, cx));
|
||||
}
|
||||
}
|
||||
|
||||
fn on_query_editor_event(
|
||||
&mut self,
|
||||
_: ViewHandle<Editor>,
|
||||
event: &editor::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
editor::Event::Edited => {
|
||||
self.query_contains_error = false;
|
||||
self.clear_matches(cx);
|
||||
self.update_matches(cx);
|
||||
cx.notify();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_active_editor_event(
|
||||
&mut self,
|
||||
_: ViewHandle<Editor>,
|
||||
event: &editor::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
editor::Event::Edited => self.update_matches(cx),
|
||||
editor::Event::SelectionsChanged => self.update_match_index(cx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
|
||||
for editor in self.highlighted_editors.drain() {
|
||||
if let Some(editor) = editor.upgrade(cx) {
|
||||
if Some(&editor) != self.active_editor.as_ref() {
|
||||
editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::<Self>(cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let query = self.query_editor.read(cx).text(cx);
|
||||
self.pending_search.take();
|
||||
if let Some(editor) = self.active_editor.as_ref() {
|
||||
if query.is_empty() {
|
||||
self.active_match_index.take();
|
||||
editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::<Self>(cx));
|
||||
} else {
|
||||
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
let case_sensitive = self.case_sensitive_mode;
|
||||
let whole_word = self.whole_word_mode;
|
||||
let ranges = if self.regex_mode {
|
||||
cx.background()
|
||||
.spawn(regex_search(buffer, query, case_sensitive, whole_word))
|
||||
} else {
|
||||
cx.background().spawn(async move {
|
||||
Ok(search(buffer, query, case_sensitive, whole_word).await)
|
||||
})
|
||||
};
|
||||
|
||||
let editor = editor.downgrade();
|
||||
self.pending_search = Some(cx.spawn(|this, mut cx| async move {
|
||||
match ranges.await {
|
||||
Ok(ranges) => {
|
||||
if let Some(editor) = cx.read(|cx| editor.upgrade(cx)) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.highlighted_editors.insert(editor.downgrade());
|
||||
editor.update(cx, |editor, cx| {
|
||||
let theme = &this.settings.borrow().theme.find;
|
||||
editor.highlight_ranges::<Self>(
|
||||
ranges,
|
||||
theme.match_background,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
this.update_match_index(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.query_contains_error = true;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.active_match_index = self.active_match_index(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn active_match_index(&mut self, cx: &mut ViewContext<Self>) -> Option<usize> {
|
||||
let editor = self.active_editor.as_ref()?;
|
||||
let editor = editor.read(cx);
|
||||
let position = editor.newest_anchor_selection()?.head();
|
||||
let ranges = editor.highlighted_ranges_for_type::<Self>()?.1;
|
||||
if ranges.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let buffer = editor.buffer().read(cx).read(cx);
|
||||
match ranges.binary_search_by(|probe| {
|
||||
if probe.end.cmp(&position, &*buffer).unwrap().is_lt() {
|
||||
Ordering::Less
|
||||
} else if probe.start.cmp(&position, &*buffer).unwrap().is_gt() {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
}) {
|
||||
Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const YIELD_INTERVAL: usize = 20000;
|
||||
|
||||
async fn search(
|
||||
buffer: MultiBufferSnapshot,
|
||||
query: String,
|
||||
case_sensitive: bool,
|
||||
whole_word: bool,
|
||||
) -> Vec<Range<Anchor>> {
|
||||
let mut ranges = Vec::new();
|
||||
|
||||
let search = AhoCorasickBuilder::new()
|
||||
.auto_configure(&[&query])
|
||||
.ascii_case_insensitive(!case_sensitive)
|
||||
.build(&[&query]);
|
||||
for (ix, mat) in search
|
||||
.stream_find_iter(buffer.bytes_in_range(0..buffer.len()))
|
||||
.enumerate()
|
||||
{
|
||||
if (ix + 1) % YIELD_INTERVAL == 0 {
|
||||
yield_now().await;
|
||||
}
|
||||
|
||||
let mat = mat.unwrap();
|
||||
|
||||
if whole_word {
|
||||
let prev_kind = buffer.reversed_chars_at(mat.start()).next().map(char_kind);
|
||||
let start_kind = char_kind(buffer.chars_at(mat.start()).next().unwrap());
|
||||
let end_kind = char_kind(buffer.reversed_chars_at(mat.end()).next().unwrap());
|
||||
let next_kind = buffer.chars_at(mat.end()).next().map(char_kind);
|
||||
if Some(start_kind) == prev_kind || Some(end_kind) == next_kind {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end()));
|
||||
}
|
||||
|
||||
ranges
|
||||
}
|
||||
|
||||
async fn regex_search(
|
||||
buffer: MultiBufferSnapshot,
|
||||
mut query: String,
|
||||
case_sensitive: bool,
|
||||
whole_word: bool,
|
||||
) -> Result<Vec<Range<Anchor>>> {
|
||||
if whole_word {
|
||||
let mut word_query = String::new();
|
||||
word_query.push_str("\\b");
|
||||
word_query.push_str(&query);
|
||||
word_query.push_str("\\b");
|
||||
query = word_query;
|
||||
}
|
||||
|
||||
let mut ranges = Vec::new();
|
||||
|
||||
if query.contains("\n") || query.contains("\\n") {
|
||||
let regex = RegexBuilder::new(&query)
|
||||
.case_insensitive(!case_sensitive)
|
||||
.multi_line(true)
|
||||
.build()?;
|
||||
for (ix, mat) in regex.find_iter(&buffer.text()).enumerate() {
|
||||
if (ix + 1) % YIELD_INTERVAL == 0 {
|
||||
yield_now().await;
|
||||
}
|
||||
|
||||
ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end()));
|
||||
}
|
||||
} else {
|
||||
let regex = RegexBuilder::new(&query)
|
||||
.case_insensitive(!case_sensitive)
|
||||
.build()?;
|
||||
|
||||
let mut line = String::new();
|
||||
let mut line_offset = 0;
|
||||
for (chunk_ix, chunk) in buffer
|
||||
.chunks(0..buffer.len(), None)
|
||||
.map(|c| c.text)
|
||||
.chain(["\n"])
|
||||
.enumerate()
|
||||
{
|
||||
if (chunk_ix + 1) % YIELD_INTERVAL == 0 {
|
||||
yield_now().await;
|
||||
}
|
||||
|
||||
for (newline_ix, text) in chunk.split('\n').enumerate() {
|
||||
if newline_ix > 0 {
|
||||
for mat in regex.find_iter(&line) {
|
||||
let start = line_offset + mat.start();
|
||||
let end = line_offset + mat.end();
|
||||
ranges.push(buffer.anchor_after(start)..buffer.anchor_before(end));
|
||||
}
|
||||
|
||||
line_offset += line.len() + 1;
|
||||
line.clear();
|
||||
}
|
||||
line.push_str(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ranges)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use editor::{DisplayPoint, Editor, EditorSettings, MultiBuffer};
|
||||
use gpui::{color::Color, TestAppContext};
|
||||
use std::sync::Arc;
|
||||
use unindent::Unindent as _;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_find_simple(mut cx: TestAppContext) {
|
||||
let fonts = cx.font_cache();
|
||||
let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
|
||||
theme.find.match_background = Color::red();
|
||||
let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap();
|
||||
|
||||
let buffer = cx.update(|cx| {
|
||||
MultiBuffer::build_simple(
|
||||
&r#"
|
||||
A regular expression (shortened as regex or regexp;[1] also referred to as
|
||||
rational expression[2][3]) is a sequence of characters that specifies a search
|
||||
pattern in text. Usually such patterns are used by string-searching algorithms
|
||||
for "find" or "find and replace" operations on strings, or for input validation.
|
||||
"#
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let editor = cx.add_view(Default::default(), |cx| {
|
||||
Editor::new(buffer.clone(), Arc::new(EditorSettings::test), cx)
|
||||
});
|
||||
|
||||
let find_bar = cx.add_view(Default::default(), |cx| {
|
||||
let mut find_bar = FindBar::new(watch::channel_with(settings).1, cx);
|
||||
find_bar.active_item_changed(Some(Box::new(editor.clone())), cx);
|
||||
find_bar
|
||||
});
|
||||
|
||||
// Search for a string that appears with different casing.
|
||||
// By default, search is case-insensitive.
|
||||
find_bar.update(&mut cx, |find_bar, cx| {
|
||||
find_bar.set_query("us", cx);
|
||||
});
|
||||
editor.next_notification(&cx).await;
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.all_highlighted_ranges(cx),
|
||||
&[
|
||||
(
|
||||
DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
|
||||
Color::red(),
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
|
||||
Color::red(),
|
||||
),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// Switch to a case sensitive search.
|
||||
find_bar.update(&mut cx, |find_bar, cx| {
|
||||
find_bar.toggle_mode(&ToggleMode(SearchMode::CaseSensitive), cx);
|
||||
});
|
||||
editor.next_notification(&cx).await;
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.all_highlighted_ranges(cx),
|
||||
&[(
|
||||
DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
|
||||
Color::red(),
|
||||
)]
|
||||
);
|
||||
});
|
||||
|
||||
// Search for a string that appears both as a whole word and
|
||||
// within other words. By default, all results are found.
|
||||
find_bar.update(&mut cx, |find_bar, cx| {
|
||||
find_bar.set_query("or", cx);
|
||||
});
|
||||
editor.next_notification(&cx).await;
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.all_highlighted_ranges(cx),
|
||||
&[
|
||||
(
|
||||
DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
|
||||
Color::red(),
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
|
||||
Color::red(),
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
|
||||
Color::red(),
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
|
||||
Color::red(),
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
|
||||
Color::red(),
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
|
||||
Color::red(),
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
|
||||
Color::red(),
|
||||
),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// Switch to a whole word search.
|
||||
find_bar.update(&mut cx, |find_bar, cx| {
|
||||
find_bar.toggle_mode(&ToggleMode(SearchMode::WholeWord), cx);
|
||||
});
|
||||
editor.next_notification(&cx).await;
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.all_highlighted_ranges(cx),
|
||||
&[
|
||||
(
|
||||
DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
|
||||
Color::red(),
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
|
||||
Color::red(),
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
|
||||
Color::red(),
|
||||
),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx);
|
||||
});
|
||||
find_bar.update(&mut cx, |find_bar, cx| {
|
||||
assert_eq!(find_bar.active_match_index, Some(0));
|
||||
find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
|
||||
assert_eq!(
|
||||
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
|
||||
[DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
|
||||
);
|
||||
});
|
||||
find_bar.read_with(&cx, |find_bar, _| {
|
||||
assert_eq!(find_bar.active_match_index, Some(0));
|
||||
});
|
||||
|
||||
find_bar.update(&mut cx, |find_bar, cx| {
|
||||
find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
|
||||
assert_eq!(
|
||||
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
|
||||
[DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
|
||||
);
|
||||
});
|
||||
find_bar.read_with(&cx, |find_bar, _| {
|
||||
assert_eq!(find_bar.active_match_index, Some(1));
|
||||
});
|
||||
|
||||
find_bar.update(&mut cx, |find_bar, cx| {
|
||||
find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
|
||||
assert_eq!(
|
||||
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
|
||||
[DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
|
||||
);
|
||||
});
|
||||
find_bar.read_with(&cx, |find_bar, _| {
|
||||
assert_eq!(find_bar.active_match_index, Some(2));
|
||||
});
|
||||
|
||||
find_bar.update(&mut cx, |find_bar, cx| {
|
||||
find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
|
||||
assert_eq!(
|
||||
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
|
||||
[DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
|
||||
);
|
||||
});
|
||||
find_bar.read_with(&cx, |find_bar, _| {
|
||||
assert_eq!(find_bar.active_match_index, Some(0));
|
||||
});
|
||||
|
||||
find_bar.update(&mut cx, |find_bar, cx| {
|
||||
find_bar.go_to_match(&GoToMatch(Direction::Prev), cx);
|
||||
assert_eq!(
|
||||
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
|
||||
[DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
|
||||
);
|
||||
});
|
||||
find_bar.read_with(&cx, |find_bar, _| {
|
||||
assert_eq!(find_bar.active_match_index, Some(2));
|
||||
});
|
||||
|
||||
find_bar.update(&mut cx, |find_bar, cx| {
|
||||
find_bar.go_to_match(&GoToMatch(Direction::Prev), cx);
|
||||
assert_eq!(
|
||||
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
|
||||
[DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
|
||||
);
|
||||
});
|
||||
find_bar.read_with(&cx, |find_bar, _| {
|
||||
assert_eq!(find_bar.active_match_index, Some(1));
|
||||
});
|
||||
|
||||
find_bar.update(&mut cx, |find_bar, cx| {
|
||||
find_bar.go_to_match(&GoToMatch(Direction::Prev), cx);
|
||||
assert_eq!(
|
||||
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
|
||||
[DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
|
||||
);
|
||||
});
|
||||
find_bar.read_with(&cx, |find_bar, _| {
|
||||
assert_eq!(find_bar.active_match_index, Some(0));
|
||||
});
|
||||
|
||||
// Park the cursor in between matches and ensure that going to the previous match selects
|
||||
// the closest match to the left.
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx);
|
||||
});
|
||||
find_bar.update(&mut cx, |find_bar, cx| {
|
||||
assert_eq!(find_bar.active_match_index, Some(1));
|
||||
find_bar.go_to_match(&GoToMatch(Direction::Prev), cx);
|
||||
assert_eq!(
|
||||
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
|
||||
[DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
|
||||
);
|
||||
});
|
||||
find_bar.read_with(&cx, |find_bar, _| {
|
||||
assert_eq!(find_bar.active_match_index, Some(0));
|
||||
});
|
||||
|
||||
// Park the cursor in between matches and ensure that going to the next match selects the
|
||||
// closest match to the right.
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx);
|
||||
});
|
||||
find_bar.update(&mut cx, |find_bar, cx| {
|
||||
assert_eq!(find_bar.active_match_index, Some(1));
|
||||
find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
|
||||
assert_eq!(
|
||||
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
|
||||
[DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
|
||||
);
|
||||
});
|
||||
find_bar.read_with(&cx, |find_bar, _| {
|
||||
assert_eq!(find_bar.active_match_index, Some(1));
|
||||
});
|
||||
|
||||
// Park the cursor after the last match and ensure that going to the previous match selects
|
||||
// the last match.
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx);
|
||||
});
|
||||
find_bar.update(&mut cx, |find_bar, cx| {
|
||||
assert_eq!(find_bar.active_match_index, Some(2));
|
||||
find_bar.go_to_match(&GoToMatch(Direction::Prev), cx);
|
||||
assert_eq!(
|
||||
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
|
||||
[DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
|
||||
);
|
||||
});
|
||||
find_bar.read_with(&cx, |find_bar, _| {
|
||||
assert_eq!(find_bar.active_match_index, Some(2));
|
||||
});
|
||||
|
||||
// Park the cursor after the last match and ensure that going to the next match selects the
|
||||
// first match.
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx);
|
||||
});
|
||||
find_bar.update(&mut cx, |find_bar, cx| {
|
||||
assert_eq!(find_bar.active_match_index, Some(2));
|
||||
find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
|
||||
assert_eq!(
|
||||
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
|
||||
[DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
|
||||
);
|
||||
});
|
||||
find_bar.read_with(&cx, |find_bar, _| {
|
||||
assert_eq!(find_bar.active_match_index, Some(0));
|
||||
});
|
||||
|
||||
// Park the cursor before the first match and ensure that going to the previous match
|
||||
// selects the last match.
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx);
|
||||
});
|
||||
find_bar.update(&mut cx, |find_bar, cx| {
|
||||
assert_eq!(find_bar.active_match_index, Some(0));
|
||||
find_bar.go_to_match(&GoToMatch(Direction::Prev), cx);
|
||||
assert_eq!(
|
||||
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
|
||||
[DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
|
||||
);
|
||||
});
|
||||
find_bar.read_with(&cx, |find_bar, _| {
|
||||
assert_eq!(find_bar.active_match_index, Some(2));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -181,7 +181,7 @@ impl View for GoToLine {
|
||||
Container::new(
|
||||
Flex::new(Axis::Vertical)
|
||||
.with_child(
|
||||
Container::new(ChildView::new(self.line_editor.id()).boxed())
|
||||
Container::new(ChildView::new(&self.line_editor).boxed())
|
||||
.with_style(theme.input_editor.container)
|
||||
.boxed(),
|
||||
)
|
||||
|
||||
@@ -3246,6 +3246,7 @@ impl Drop for AnyModelHandle {
|
||||
self.ref_counts.lock().dec_model(self.model_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WeakViewHandle<T> {
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
@@ -3288,6 +3289,21 @@ impl<T> Clone for WeakViewHandle<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq for WeakViewHandle<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.window_id == other.window_id && self.view_id == other.view_id
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Eq for WeakViewHandle<T> {}
|
||||
|
||||
impl<T> Hash for WeakViewHandle<T> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.window_id.hash(state);
|
||||
self.view_id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ElementStateId(usize, usize);
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ use crate::{
|
||||
json::{self, ToJson},
|
||||
platform::Event,
|
||||
text_layout::TextLayoutCache,
|
||||
Action, AnyAction, AssetCache, ElementBox, Entity, FontSystem, ModelHandle, ReadModel,
|
||||
ReadView, Scene, View, ViewHandle,
|
||||
Action, AnyAction, AnyViewHandle, AssetCache, ElementBox, Entity, FontSystem, ModelHandle,
|
||||
ReadModel, ReadView, Scene, View, ViewHandle,
|
||||
};
|
||||
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
||||
use serde_json::json;
|
||||
@@ -462,8 +462,10 @@ pub struct ChildView {
|
||||
}
|
||||
|
||||
impl ChildView {
|
||||
pub fn new(view_id: usize) -> Self {
|
||||
Self { view_id }
|
||||
pub fn new(view: impl Into<AnyViewHandle>) -> Self {
|
||||
Self {
|
||||
view_id: view.into().id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ impl View for OutlineView {
|
||||
|
||||
Flex::new(Axis::Vertical)
|
||||
.with_child(
|
||||
Container::new(ChildView::new(self.query_editor.id()).boxed())
|
||||
Container::new(ChildView::new(&self.query_editor).boxed())
|
||||
.with_style(settings.theme.selector.input_editor.container)
|
||||
.boxed(),
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ pub struct Theme {
|
||||
pub project_panel: ProjectPanel,
|
||||
pub selector: Selector,
|
||||
pub editor: EditorStyle,
|
||||
pub find: Find,
|
||||
pub project_diagnostics: ProjectDiagnostics,
|
||||
}
|
||||
|
||||
@@ -37,6 +38,7 @@ pub struct Workspace {
|
||||
pub left_sidebar: Sidebar,
|
||||
pub right_sidebar: Sidebar,
|
||||
pub status_bar: StatusBar,
|
||||
pub toolbar: Toolbar,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
@@ -87,6 +89,33 @@ pub struct Tab {
|
||||
pub icon_conflict: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct Toolbar {
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct Find {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub editor: FindEditor,
|
||||
pub invalid_editor: ContainerStyle,
|
||||
pub mode_button_group: ContainerStyle,
|
||||
pub mode_button: ContainedText,
|
||||
pub active_mode_button: ContainedText,
|
||||
pub hovered_mode_button: ContainedText,
|
||||
pub active_hovered_mode_button: ContainedText,
|
||||
pub match_background: Color,
|
||||
pub match_index: ContainedText,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct FindEditor {
|
||||
#[serde(flatten)]
|
||||
pub input: InputEditorStyle,
|
||||
pub max_width: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct Sidebar {
|
||||
#[serde(flatten)]
|
||||
|
||||
@@ -299,7 +299,7 @@ impl View for ThemeSelector {
|
||||
ConstrainedBox::new(
|
||||
Container::new(
|
||||
Flex::new(Axis::Vertical)
|
||||
.with_child(ChildView::new(self.query_editor.id()).boxed())
|
||||
.with_child(ChildView::new(&self.query_editor).boxed())
|
||||
.with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
|
||||
.boxed(),
|
||||
)
|
||||
|
||||
+130
-11
@@ -7,11 +7,17 @@ use gpui::{
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
keymap::Binding,
|
||||
platform::CursorStyle,
|
||||
Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext, ViewHandle,
|
||||
AnyViewHandle, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext,
|
||||
ViewHandle,
|
||||
};
|
||||
use postage::watch;
|
||||
use project::ProjectPath;
|
||||
use std::{any::Any, cell::RefCell, cmp, mem, rc::Rc};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::RefCell,
|
||||
cmp, mem,
|
||||
rc::Rc,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
action!(Split, SplitDirection);
|
||||
@@ -75,12 +81,29 @@ pub struct Pane {
|
||||
active_item_index: usize,
|
||||
settings: watch::Receiver<Settings>,
|
||||
nav_history: Rc<RefCell<NavHistory>>,
|
||||
toolbars: HashMap<TypeId, Box<dyn ToolbarHandle>>,
|
||||
active_toolbar_type: Option<TypeId>,
|
||||
active_toolbar_visible: bool,
|
||||
}
|
||||
|
||||
// #[derive(Debug, Eq, PartialEq)]
|
||||
// pub struct State {
|
||||
// pub tabs: Vec<TabState>,
|
||||
// }
|
||||
pub trait Toolbar: View {
|
||||
fn active_item_changed(
|
||||
&mut self,
|
||||
item: Option<Box<dyn ItemViewHandle>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool;
|
||||
fn on_dismiss(&mut self, cx: &mut ViewContext<Self>);
|
||||
}
|
||||
|
||||
trait ToolbarHandle {
|
||||
fn active_item_changed(
|
||||
&self,
|
||||
item: Option<Box<dyn ItemViewHandle>>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> bool;
|
||||
fn on_dismiss(&self, cx: &mut MutableAppContext);
|
||||
fn to_any(&self) -> AnyViewHandle;
|
||||
}
|
||||
|
||||
pub struct ItemNavHistory {
|
||||
history: Rc<RefCell<NavHistory>>,
|
||||
@@ -120,6 +143,9 @@ impl Pane {
|
||||
active_item_index: 0,
|
||||
settings,
|
||||
nav_history: Default::default(),
|
||||
toolbars: Default::default(),
|
||||
active_toolbar_type: Default::default(),
|
||||
active_toolbar_visible: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,9 +314,12 @@ impl Pane {
|
||||
pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext<Self>) {
|
||||
if index < self.item_views.len() {
|
||||
let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
|
||||
if prev_active_item_ix != self.active_item_index {
|
||||
if prev_active_item_ix != self.active_item_index
|
||||
&& prev_active_item_ix < self.item_views.len()
|
||||
{
|
||||
self.item_views[prev_active_item_ix].1.deactivated(cx);
|
||||
}
|
||||
self.update_active_toolbar(cx);
|
||||
self.focus_active_item(cx);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -344,14 +373,19 @@ impl Pane {
|
||||
true
|
||||
}
|
||||
});
|
||||
self.active_item_index = cmp::min(
|
||||
self.active_item_index,
|
||||
self.item_views.len().saturating_sub(1),
|
||||
self.activate_item(
|
||||
cmp::min(
|
||||
self.active_item_index,
|
||||
self.item_views.len().saturating_sub(1),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
|
||||
if self.item_views.is_empty() {
|
||||
self.update_active_toolbar(cx);
|
||||
cx.emit(Event::Remove);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -365,6 +399,68 @@ impl Pane {
|
||||
cx.emit(Event::Split(direction));
|
||||
}
|
||||
|
||||
pub fn show_toolbar<F, V>(&mut self, cx: &mut ViewContext<Self>, build_toolbar: F)
|
||||
where
|
||||
F: FnOnce(&mut ViewContext<V>) -> V,
|
||||
V: Toolbar,
|
||||
{
|
||||
let type_id = TypeId::of::<V>();
|
||||
if self.active_toolbar_type != Some(type_id) {
|
||||
self.dismiss_toolbar(cx);
|
||||
|
||||
let active_item = self.active_item();
|
||||
self.toolbars
|
||||
.entry(type_id)
|
||||
.or_insert_with(|| Box::new(cx.add_view(build_toolbar)));
|
||||
|
||||
self.active_toolbar_type = Some(type_id);
|
||||
self.active_toolbar_visible =
|
||||
self.toolbars[&type_id].active_item_changed(active_item, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dismiss_toolbar(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(active_toolbar_type) = self.active_toolbar_type.take() {
|
||||
self.toolbars
|
||||
.get_mut(&active_toolbar_type)
|
||||
.unwrap()
|
||||
.on_dismiss(cx);
|
||||
self.active_toolbar_visible = false;
|
||||
self.focus_active_item(cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toolbar<T: Toolbar>(&self) -> Option<ViewHandle<T>> {
|
||||
self.toolbars
|
||||
.get(&TypeId::of::<T>())
|
||||
.and_then(|toolbar| toolbar.to_any().downcast())
|
||||
}
|
||||
|
||||
pub fn active_toolbar(&self) -> Option<AnyViewHandle> {
|
||||
let type_id = self.active_toolbar_type?;
|
||||
let toolbar = self.toolbars.get(&type_id)?;
|
||||
if self.active_toolbar_visible {
|
||||
Some(toolbar.to_any())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn update_active_toolbar(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(type_id) = self.active_toolbar_type {
|
||||
if let Some(toolbar) = self.toolbars.get(&type_id) {
|
||||
self.active_toolbar_visible = toolbar.active_item_changed(
|
||||
self.item_views
|
||||
.get(self.active_item_index)
|
||||
.map(|i| i.1.clone()),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let settings = self.settings.borrow();
|
||||
let theme = &settings.theme;
|
||||
@@ -516,7 +612,12 @@ impl View for Pane {
|
||||
if let Some(active_item) = self.active_item() {
|
||||
Flex::column()
|
||||
.with_child(self.render_tabs(cx))
|
||||
.with_child(ChildView::new(active_item.id()).flexible(1., true).boxed())
|
||||
.with_children(
|
||||
self.active_toolbar()
|
||||
.as_ref()
|
||||
.map(|view| ChildView::new(view).boxed()),
|
||||
)
|
||||
.with_child(ChildView::new(active_item).flexible(1., true).boxed())
|
||||
.named("pane")
|
||||
} else {
|
||||
Empty::new().named("pane")
|
||||
@@ -528,6 +629,24 @@ impl View for Pane {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Toolbar> ToolbarHandle for ViewHandle<T> {
|
||||
fn active_item_changed(
|
||||
&self,
|
||||
item: Option<Box<dyn ItemViewHandle>>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> bool {
|
||||
self.update(cx, |this, cx| this.active_item_changed(item, cx))
|
||||
}
|
||||
|
||||
fn on_dismiss(&self, cx: &mut MutableAppContext) {
|
||||
self.update(cx, |this, cx| this.on_dismiss(cx));
|
||||
}
|
||||
|
||||
fn to_any(&self) -> AnyViewHandle {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemNavHistory {
|
||||
pub fn new<T: ItemView>(history: Rc<RefCell<NavHistory>>, item_view: &ViewHandle<T>) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -1,43 +1,45 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{elements::*, Axis};
|
||||
use gpui::{elements::*, Axis, ViewHandle};
|
||||
use theme::Theme;
|
||||
|
||||
use crate::Pane;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PaneGroup {
|
||||
root: Member,
|
||||
}
|
||||
|
||||
impl PaneGroup {
|
||||
pub fn new(pane_id: usize) -> Self {
|
||||
pub fn new(pane: ViewHandle<Pane>) -> Self {
|
||||
Self {
|
||||
root: Member::Pane(pane_id),
|
||||
root: Member::Pane(pane),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn split(
|
||||
&mut self,
|
||||
old_pane_id: usize,
|
||||
new_pane_id: usize,
|
||||
old_pane: &ViewHandle<Pane>,
|
||||
new_pane: &ViewHandle<Pane>,
|
||||
direction: SplitDirection,
|
||||
) -> Result<()> {
|
||||
match &mut self.root {
|
||||
Member::Pane(pane_id) => {
|
||||
if *pane_id == old_pane_id {
|
||||
self.root = Member::new_axis(old_pane_id, new_pane_id, direction);
|
||||
Member::Pane(pane) => {
|
||||
if pane == old_pane {
|
||||
self.root = Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("Pane not found"))
|
||||
}
|
||||
}
|
||||
Member::Axis(axis) => axis.split(old_pane_id, new_pane_id, direction),
|
||||
Member::Axis(axis) => axis.split(old_pane, new_pane, direction),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, pane_id: usize) -> Result<bool> {
|
||||
pub fn remove(&mut self, pane: &ViewHandle<Pane>) -> Result<bool> {
|
||||
match &mut self.root {
|
||||
Member::Pane(_) => Ok(false),
|
||||
Member::Axis(axis) => {
|
||||
if let Some(last_pane) = axis.remove(pane_id)? {
|
||||
if let Some(last_pane) = axis.remove(pane)? {
|
||||
self.root = last_pane;
|
||||
}
|
||||
Ok(true)
|
||||
@@ -53,11 +55,15 @@ impl PaneGroup {
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
enum Member {
|
||||
Axis(PaneAxis),
|
||||
Pane(usize),
|
||||
Pane(ViewHandle<Pane>),
|
||||
}
|
||||
|
||||
impl Member {
|
||||
fn new_axis(old_pane_id: usize, new_pane_id: usize, direction: SplitDirection) -> Self {
|
||||
fn new_axis(
|
||||
old_pane: ViewHandle<Pane>,
|
||||
new_pane: ViewHandle<Pane>,
|
||||
direction: SplitDirection,
|
||||
) -> Self {
|
||||
use Axis::*;
|
||||
use SplitDirection::*;
|
||||
|
||||
@@ -67,16 +73,16 @@ impl Member {
|
||||
};
|
||||
|
||||
let members = match direction {
|
||||
Up | Left => vec![Member::Pane(new_pane_id), Member::Pane(old_pane_id)],
|
||||
Down | Right => vec![Member::Pane(old_pane_id), Member::Pane(new_pane_id)],
|
||||
Up | Left => vec![Member::Pane(new_pane), Member::Pane(old_pane)],
|
||||
Down | Right => vec![Member::Pane(old_pane), Member::Pane(new_pane)],
|
||||
};
|
||||
|
||||
Member::Axis(PaneAxis { axis, members })
|
||||
}
|
||||
|
||||
pub fn render<'a>(&self, theme: &Theme) -> ElementBox {
|
||||
pub fn render(&self, theme: &Theme) -> ElementBox {
|
||||
match self {
|
||||
Member::Pane(view_id) => ChildView::new(*view_id).boxed(),
|
||||
Member::Pane(pane) => ChildView::new(pane).boxed(),
|
||||
Member::Axis(axis) => axis.render(theme),
|
||||
}
|
||||
}
|
||||
@@ -91,8 +97,8 @@ struct PaneAxis {
|
||||
impl PaneAxis {
|
||||
fn split(
|
||||
&mut self,
|
||||
old_pane_id: usize,
|
||||
new_pane_id: usize,
|
||||
old_pane: &ViewHandle<Pane>,
|
||||
new_pane: &ViewHandle<Pane>,
|
||||
direction: SplitDirection,
|
||||
) -> Result<()> {
|
||||
use SplitDirection::*;
|
||||
@@ -100,23 +106,24 @@ impl PaneAxis {
|
||||
for (idx, member) in self.members.iter_mut().enumerate() {
|
||||
match member {
|
||||
Member::Axis(axis) => {
|
||||
if axis.split(old_pane_id, new_pane_id, direction).is_ok() {
|
||||
if axis.split(old_pane, new_pane, direction).is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Member::Pane(pane_id) => {
|
||||
if *pane_id == old_pane_id {
|
||||
Member::Pane(pane) => {
|
||||
if pane == old_pane {
|
||||
if direction.matches_axis(self.axis) {
|
||||
match direction {
|
||||
Up | Left => {
|
||||
self.members.insert(idx, Member::Pane(new_pane_id));
|
||||
self.members.insert(idx, Member::Pane(new_pane.clone()));
|
||||
}
|
||||
Down | Right => {
|
||||
self.members.insert(idx + 1, Member::Pane(new_pane_id));
|
||||
self.members.insert(idx + 1, Member::Pane(new_pane.clone()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
*member = Member::new_axis(old_pane_id, new_pane_id, direction);
|
||||
*member =
|
||||
Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
@@ -126,13 +133,13 @@ impl PaneAxis {
|
||||
Err(anyhow!("Pane not found"))
|
||||
}
|
||||
|
||||
fn remove(&mut self, pane_id_to_remove: usize) -> Result<Option<Member>> {
|
||||
fn remove(&mut self, pane_to_remove: &ViewHandle<Pane>) -> Result<Option<Member>> {
|
||||
let mut found_pane = false;
|
||||
let mut remove_member = None;
|
||||
for (idx, member) in self.members.iter_mut().enumerate() {
|
||||
match member {
|
||||
Member::Axis(axis) => {
|
||||
if let Ok(last_pane) = axis.remove(pane_id_to_remove) {
|
||||
if let Ok(last_pane) = axis.remove(pane_to_remove) {
|
||||
if let Some(last_pane) = last_pane {
|
||||
*member = last_pane;
|
||||
}
|
||||
@@ -140,8 +147,8 @@ impl PaneAxis {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Member::Pane(pane_id) => {
|
||||
if *pane_id == pane_id_to_remove {
|
||||
Member::Pane(pane) => {
|
||||
if pane == pane_to_remove {
|
||||
found_pane = true;
|
||||
remove_member = Some(idx);
|
||||
break;
|
||||
|
||||
@@ -136,7 +136,7 @@ impl Sidebar {
|
||||
|
||||
container.add_child(
|
||||
Hook::new(
|
||||
ConstrainedBox::new(ChildView::new(active_item.id()).boxed())
|
||||
ConstrainedBox::new(ChildView::new(active_item).boxed())
|
||||
.with_max_width(*self.width.borrow())
|
||||
.boxed(),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{ItemViewHandle, Pane, Settings};
|
||||
use gpui::{
|
||||
elements::*, ElementBox, Entity, MutableAppContext, RenderContext, Subscription, View,
|
||||
ViewContext, ViewHandle,
|
||||
elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, Subscription,
|
||||
View, ViewContext, ViewHandle,
|
||||
};
|
||||
use postage::watch;
|
||||
|
||||
@@ -14,7 +14,7 @@ pub trait StatusItemView: View {
|
||||
}
|
||||
|
||||
trait StatusItemViewHandle {
|
||||
fn id(&self) -> usize;
|
||||
fn to_any(&self) -> AnyViewHandle;
|
||||
fn set_active_pane_item(
|
||||
&self,
|
||||
active_pane_item: Option<&dyn ItemViewHandle>,
|
||||
@@ -45,13 +45,13 @@ impl View for StatusBar {
|
||||
.with_children(
|
||||
self.left_items
|
||||
.iter()
|
||||
.map(|i| ChildView::new(i.id()).aligned().boxed()),
|
||||
.map(|i| ChildView::new(i.as_ref()).aligned().boxed()),
|
||||
)
|
||||
.with_child(Empty::new().flexible(1., true).boxed())
|
||||
.with_children(
|
||||
self.right_items
|
||||
.iter()
|
||||
.map(|i| ChildView::new(i.id()).aligned().boxed()),
|
||||
.map(|i| ChildView::new(i.as_ref()).aligned().boxed()),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.container)
|
||||
@@ -111,8 +111,8 @@ impl StatusBar {
|
||||
}
|
||||
|
||||
impl<T: StatusItemView> StatusItemViewHandle for ViewHandle<T> {
|
||||
fn id(&self) -> usize {
|
||||
self.id()
|
||||
fn to_any(&self) -> AnyViewHandle {
|
||||
self.into()
|
||||
}
|
||||
|
||||
fn set_active_pane_item(
|
||||
@@ -125,3 +125,9 @@ impl<T: StatusItemView> StatusItemViewHandle for ViewHandle<T> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<AnyViewHandle> for &dyn StatusItemViewHandle {
|
||||
fn into(self) -> AnyViewHandle {
|
||||
self.to_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,7 +599,7 @@ impl Workspace {
|
||||
Workspace {
|
||||
modal: None,
|
||||
weak_self: cx.weak_handle(),
|
||||
center: PaneGroup::new(pane.id()),
|
||||
center: PaneGroup::new(pane.clone()),
|
||||
panes: vec![pane.clone()],
|
||||
active_pane: pane.clone(),
|
||||
status_bar,
|
||||
@@ -1048,15 +1048,13 @@ impl Workspace {
|
||||
new_pane.update(cx, |new_pane, cx| new_pane.add_item_view(clone, cx));
|
||||
}
|
||||
}
|
||||
self.center
|
||||
.split(pane.id(), new_pane.id(), direction)
|
||||
.unwrap();
|
||||
self.center.split(&pane, &new_pane, direction).unwrap();
|
||||
cx.notify();
|
||||
new_pane
|
||||
}
|
||||
|
||||
fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
|
||||
if self.center.remove(pane.id()).unwrap() {
|
||||
if self.center.remove(&pane).unwrap() {
|
||||
self.panes.retain(|p| p != &pane);
|
||||
self.activate_pane(self.panes.last().unwrap().clone(), cx);
|
||||
}
|
||||
@@ -1287,7 +1285,7 @@ impl View for Workspace {
|
||||
Flexible::new(1., true, self.center.render(&settings.theme))
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(ChildView::new(self.status_bar.id()).boxed())
|
||||
.with_child(ChildView::new(&self.status_bar).boxed())
|
||||
.flexible(1., true)
|
||||
.boxed(),
|
||||
);
|
||||
@@ -1298,7 +1296,7 @@ impl View for Workspace {
|
||||
content.add_child(self.right_sidebar.render(&settings, cx));
|
||||
content.boxed()
|
||||
})
|
||||
.with_children(self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()))
|
||||
.with_children(self.modal.as_ref().map(|m| ChildView::new(m).boxed()))
|
||||
.flexible(1.0, true)
|
||||
.boxed(),
|
||||
)
|
||||
|
||||
@@ -36,6 +36,7 @@ contacts_panel = { path = "../contacts_panel" }
|
||||
diagnostics = { path = "../diagnostics" }
|
||||
editor = { path = "../editor" }
|
||||
file_finder = { path = "../file_finder" }
|
||||
find = { path = "../find" }
|
||||
fsevent = { path = "../fsevent" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
go_to_line = { path = "../go_to_line" }
|
||||
|
||||
@@ -81,6 +81,9 @@ item_spacing = 24
|
||||
cursor_position = "$text.2"
|
||||
diagnostic_message = "$text.2"
|
||||
|
||||
[workspace.toolbar]
|
||||
height = 44
|
||||
|
||||
[panel]
|
||||
padding = { top = 12, left = 12, bottom = 12, right = 12 }
|
||||
|
||||
@@ -318,3 +321,50 @@ status_bar_item = { extends = "$text.2", margin.right = 10 }
|
||||
tab_icon_width = 13
|
||||
tab_icon_spacing = 4
|
||||
tab_summary_spacing = 10
|
||||
|
||||
[find]
|
||||
match_background = "$state.highlighted_line"
|
||||
background = "$surface.1"
|
||||
|
||||
[find.mode_button]
|
||||
extends = "$text.1"
|
||||
padding = { left = 6, right = 6, top = 1, bottom = 1 }
|
||||
corner_radius = 6
|
||||
background = "$surface.1"
|
||||
border = { width = 1, color = "$border.0" }
|
||||
margin.left = 1
|
||||
margin.right = 1
|
||||
|
||||
[find.mode_button_group]
|
||||
padding = { left = 2, right = 2 }
|
||||
|
||||
[find.active_mode_button]
|
||||
extends = "$find.mode_button"
|
||||
background = "$surface.2"
|
||||
|
||||
[find.hovered_mode_button]
|
||||
extends = "$find.mode_button"
|
||||
background = "$surface.2"
|
||||
|
||||
[find.active_hovered_mode_button]
|
||||
extends = "$find.mode_button"
|
||||
background = "$surface.2"
|
||||
|
||||
[find.match_index]
|
||||
extends = "$text.1"
|
||||
padding = 6
|
||||
|
||||
[find.editor]
|
||||
max_width = 400
|
||||
background = "$surface.0"
|
||||
corner_radius = 6
|
||||
padding = { left = 13, right = 13, top = 3, bottom = 3 }
|
||||
margin = { top = 5, bottom = 5, left = 5, right = 5 }
|
||||
text = "$text.0"
|
||||
placeholder_text = "$text.2"
|
||||
selection = "$selection.host"
|
||||
border = { width = 1, color = "$border.0" }
|
||||
|
||||
[find.invalid_editor]
|
||||
extends = "$find.editor"
|
||||
border = { width = 1, color = "$status.bad" }
|
||||
|
||||
@@ -62,6 +62,7 @@ fn main() {
|
||||
outline::init(cx);
|
||||
project_panel::init(cx);
|
||||
diagnostics::init(cx);
|
||||
find::init(cx);
|
||||
cx.spawn({
|
||||
let client = client.clone();
|
||||
|cx| async move {
|
||||
|
||||
Reference in New Issue
Block a user