Merge pull request #499 from zed-industries/project-find

Project-wide search
This commit is contained in:
Antonio Scandurra 2022-03-02 10:58:50 +01:00 committed by GitHub
commit b771667bf2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 2813 additions and 1214 deletions

75
Cargo.lock generated
View file

@ -867,7 +867,7 @@ dependencies = [
"gpui",
"postage",
"theme",
"time 0.3.2",
"time 0.3.7",
"util",
"workspace",
]
@ -988,7 +988,7 @@ dependencies = [
"sum_tree",
"surf",
"thiserror",
"time 0.3.2",
"time 0.3.7",
"tiny_http",
"util",
]
@ -1134,7 +1134,7 @@ dependencies = [
"percent-encoding",
"rand 0.8.3",
"sha2 0.9.5",
"time 0.2.25",
"time 0.2.27",
"version_check",
]
@ -1772,23 +1772,6 @@ 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"
@ -2232,7 +2215,7 @@ dependencies = [
"smallvec",
"smol",
"sum_tree",
"time 0.3.2",
"time 0.3.7",
"tiny-skia",
"tree-sitter",
"usvg",
@ -3089,6 +3072,15 @@ dependencies = [
"libc",
]
[[package]]
name = "num_threads"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15"
dependencies = [
"libc",
]
[[package]]
name = "oauth2"
version = "4.1.0"
@ -3554,6 +3546,7 @@ dependencies = [
name = "project"
version = "0.1.0"
dependencies = [
"aho-corasick",
"anyhow",
"async-trait",
"client",
@ -3572,6 +3565,7 @@ dependencies = [
"parking_lot",
"postage",
"rand 0.8.3",
"regex",
"rpc",
"serde",
"serde_json",
@ -4169,6 +4163,25 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "search"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"editor",
"gpui",
"language",
"log",
"postage",
"project",
"serde_json",
"theme",
"unindent",
"util",
"workspace",
]
[[package]]
name = "semver"
version = "0.9.0"
@ -4678,7 +4691,7 @@ dependencies = [
"sqlx-rt 0.5.5",
"stringprep",
"thiserror",
"time 0.2.25",
"time 0.2.27",
"url",
"uuid",
"webpki",
@ -5126,9 +5139,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.2.25"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1195b046942c221454c2539395f85413b33383a067449d78aab2b7b052a142f7"
checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242"
dependencies = [
"const_fn",
"libc",
@ -5141,11 +5154,12 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.2"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e0a10c9a9fb3a5dce8c2239ed670f1a2569fcf42da035f5face1b19860d52b0"
checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d"
dependencies = [
"libc",
"num_threads",
]
[[package]]
@ -5160,9 +5174,9 @@ dependencies = [
[[package]]
name = "time-macros-impl"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa"
checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f"
dependencies = [
"proc-macro-hack",
"proc-macro2",
@ -5848,7 +5862,6 @@ dependencies = [
"editor",
"env_logger",
"file_finder",
"find",
"fsevent",
"futures",
"fuzzy",
@ -5877,6 +5890,7 @@ dependencies = [
"rpc",
"rsa",
"rust-embed",
"search",
"serde",
"serde_json",
"serde_path_to_error",
@ -5890,7 +5904,6 @@ dependencies = [
"theme",
"theme_selector",
"thiserror",
"time 0.3.2",
"tiny_http",
"toml",
"tree-sitter",
@ -5942,7 +5955,7 @@ dependencies = [
"surf",
"tide",
"tide-compress",
"time 0.2.25",
"time 0.2.27",
"toml",
"util",
"zed",

View file

@ -1,13 +1,11 @@
pub mod items;
use anyhow::Result;
use collections::{BTreeSet, HashMap, HashSet};
use collections::{BTreeSet, HashSet};
use editor::{
diagnostic_block_renderer,
display_map::{BlockDisposition, BlockId, BlockProperties, RenderBlock},
highlight_diagnostic_message,
items::BufferItemHandle,
Autoscroll, Editor, ExcerptId, MultiBuffer, ToOffset,
highlight_diagnostic_message, Editor, ExcerptId, MultiBuffer, ToOffset,
};
use gpui::{
action, elements::*, fonts::TextStyle, keymap::Binding, AnyViewHandle, AppContext, Entity,
@ -28,24 +26,15 @@ use std::{
sync::Arc,
};
use util::TryFutureExt;
use workspace::{ItemNavHistory, ItemViewHandle as _, Workspace};
use workspace::{ItemHandle, ItemNavHistory, ItemViewHandle as _, Workspace};
action!(Deploy);
action!(OpenExcerpts);
const CONTEXT_LINE_COUNT: u32 = 1;
pub fn init(cx: &mut MutableAppContext) {
cx.add_bindings([
Binding::new("alt-shift-D", Deploy, Some("Workspace")),
Binding::new(
"alt-shift-D",
OpenExcerpts,
Some("ProjectDiagnosticsEditor"),
),
]);
cx.add_bindings([Binding::new("alt-shift-D", Deploy, Some("Workspace"))]);
cx.add_action(ProjectDiagnosticsEditor::deploy);
cx.add_action(ProjectDiagnosticsEditor::open_excerpts);
}
type Event = editor::Event;
@ -180,47 +169,6 @@ impl ProjectDiagnosticsEditor {
}
}
fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext<Self>) {
if let Some(workspace) = self.workspace.upgrade(cx) {
let editor = self.editor.read(cx);
let excerpts = self.excerpts.read(cx);
let mut new_selections_by_buffer = HashMap::default();
for selection in editor.local_selections::<usize>(cx) {
for (buffer, mut range) in
excerpts.range_to_buffer_ranges(selection.start..selection.end, cx)
{
if selection.reversed {
mem::swap(&mut range.start, &mut range.end);
}
new_selections_by_buffer
.entry(buffer)
.or_insert(Vec::new())
.push(range)
}
}
// We defer the pane interaction because we ourselves are a workspace item
// and activating a new item causes the pane to call a method on us reentrantly,
// which panics if we're on the stack.
workspace.defer(cx, |workspace, cx| {
for (buffer, ranges) in new_selections_by_buffer {
let buffer = BufferItemHandle(buffer);
if !workspace.activate_pane_for_item(&buffer, cx) {
workspace.activate_next_pane(cx);
}
let editor = workspace
.open_item(buffer, cx)
.downcast::<Editor>()
.unwrap();
editor.update(cx, |editor, cx| {
editor.select_ranges(ranges, Some(Autoscroll::Center), cx)
});
}
});
}
}
fn update_excerpts(&mut self, cx: &mut ViewContext<Self>) {
let paths = mem::take(&mut self.paths_to_update);
let project = self.model.read(cx).project.clone();
@ -536,8 +484,8 @@ impl workspace::Item for ProjectDiagnostics {
}
impl workspace::ItemView for ProjectDiagnosticsEditor {
fn item_id(&self, _: &AppContext) -> usize {
self.model.id()
fn item(&self, _: &AppContext) -> Box<dyn ItemHandle> {
Box::new(self.model.clone())
}
fn tab_content(&self, style: &theme::Tab, _: &AppContext) -> ElementBox {
@ -598,7 +546,11 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
}
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
fn clone_on_split(
&self,
nav_history: ItemNavHistory,
cx: &mut ViewContext<Self>,
) -> Option<Self>
where
Self: Sized,
{
@ -608,13 +560,8 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
self.settings.clone(),
cx,
);
diagnostics.editor.update(cx, |editor, cx| {
let nav_history = self
.editor
.read(cx)
.nav_history()
.map(|nav_history| ItemNavHistory::new(nav_history.history(), &cx.handle()));
editor.set_nav_history(nav_history);
diagnostics.editor.update(cx, |editor, _| {
editor.set_nav_history(Some(nav_history));
});
Some(diagnostics)
}

View file

@ -30,14 +30,14 @@ use gpui::{
};
use items::{BufferItemHandle, MultiBufferItemHandle};
use itertools::Itertools as _;
pub use language::{char_kind, CharKind};
use language::{
AnchorRangeExt as _, BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic,
DiagnosticSeverity, Language, Point, Selection, SelectionGoal, TransactionId,
};
use multi_buffer::MultiBufferChunks;
pub use multi_buffer::{
char_kind, Anchor, AnchorRangeExt, CharKind, ExcerptId, MultiBuffer, MultiBufferSnapshot,
ToOffset, ToPoint,
Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
};
use ordered_float::OrderedFloat;
use postage::watch;
@ -132,6 +132,7 @@ action!(ShowCompletions);
action!(ToggleCodeActions, bool);
action!(ConfirmCompletion, Option<usize>);
action!(ConfirmCodeAction, Option<usize>);
action!(OpenExcerpts);
pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpener>>) {
path_openers.push(Box::new(items::BufferOpener));
@ -259,6 +260,7 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
Binding::new("alt-cmd-f", FoldSelectedRanges, Some("Editor")),
Binding::new("ctrl-space", ShowCompletions, Some("Editor")),
Binding::new("cmd-.", ToggleCodeActions(false), Some("Editor")),
Binding::new("alt-enter", OpenExcerpts, Some("Editor")),
]);
cx.add_action(Editor::open_new);
@ -324,6 +326,7 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
cx.add_action(Editor::fold_selected_ranges);
cx.add_action(Editor::show_completions);
cx.add_action(Editor::toggle_code_actions);
cx.add_action(Editor::open_excerpts);
cx.add_async_action(Editor::confirm_completion);
cx.add_async_action(Editor::confirm_code_action);
cx.add_async_action(Editor::rename);
@ -446,6 +449,7 @@ pub struct Editor {
code_actions_task: Option<Task<()>>,
document_highlights_task: Option<Task<()>>,
pending_rename: Option<RenameState>,
searchable: bool,
}
pub struct EditorSnapshot {
@ -834,7 +838,7 @@ impl Editor {
Self::new(EditorMode::Full, buffer, project, settings, None, cx)
}
pub fn clone(&self, cx: &mut ViewContext<Self>) -> Self {
pub fn clone(&self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) -> Self {
let mut clone = Self::new(
self.mode,
self.buffer.clone(),
@ -845,10 +849,8 @@ impl Editor {
);
clone.scroll_position = self.scroll_position;
clone.scroll_top_anchor = self.scroll_top_anchor.clone();
clone.nav_history = self
.nav_history
.as_ref()
.map(|nav_history| ItemNavHistory::new(nav_history.history(), &cx.handle()));
clone.nav_history = Some(nav_history);
clone.searchable = self.searchable;
clone
}
@ -927,6 +929,7 @@ impl Editor {
code_actions_task: Default::default(),
document_highlights_task: Default::default(),
pending_rename: Default::default(),
searchable: true,
};
this.end_selection(cx);
this
@ -1058,7 +1061,8 @@ impl Editor {
first_cursor_top = highlighted_rows.start as f32;
last_cursor_bottom = first_cursor_top + 1.;
} else if autoscroll == Autoscroll::Newest {
let newest_selection = self.newest_selection::<Point>(&display_map.buffer_snapshot);
let newest_selection =
self.newest_selection_with_snapshot::<Point>(&display_map.buffer_snapshot);
first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32;
last_cursor_bottom = first_cursor_top + 1.;
} else {
@ -1205,7 +1209,7 @@ impl Editor {
) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let tail = self
.newest_selection::<usize>(&display_map.buffer_snapshot)
.newest_selection_with_snapshot::<usize>(&display_map.buffer_snapshot)
.tail();
self.begin_selection(position, false, click_count, cx);
@ -1325,7 +1329,7 @@ impl Editor {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let tail = self
.newest_selection::<Point>(&display_map.buffer_snapshot)
.newest_selection_with_snapshot::<Point>(&display_map.buffer_snapshot)
.tail();
self.columnar_selection_tail = Some(display_map.buffer_snapshot.anchor_before(tail));
@ -1511,8 +1515,7 @@ impl Editor {
self.set_selections(selections, None, cx);
self.request_autoscroll(Autoscroll::Fit, cx);
} else {
let buffer = self.buffer.read(cx).snapshot(cx);
let mut oldest_selection = self.oldest_selection::<usize>(&buffer);
let mut oldest_selection = self.oldest_selection::<usize>(&cx);
if self.selection_count() == 1 {
if oldest_selection.is_empty() {
cx.propagate_action();
@ -4083,7 +4086,7 @@ impl Editor {
pub fn show_next_diagnostic(&mut self, _: &ShowNextDiagnostic, cx: &mut ViewContext<Self>) {
let buffer = self.buffer.read(cx).snapshot(cx);
let selection = self.newest_selection::<usize>(&buffer);
let selection = self.newest_selection_with_snapshot::<usize>(&buffer);
let mut active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| {
active_diagnostics
.primary_range
@ -4155,8 +4158,7 @@ impl Editor {
};
let editor = editor_handle.read(cx);
let buffer = editor.buffer.read(cx);
let head = editor.newest_selection::<usize>(&buffer.read(cx)).head();
let head = editor.newest_selection::<usize>(cx).head();
let (buffer, head) =
if let Some(text_anchor) = editor.buffer.read(cx).text_anchor_for_position(head, cx) {
text_anchor
@ -4170,6 +4172,7 @@ impl Editor {
cx.spawn(|workspace, mut cx| async move {
let definitions = definitions.await?;
workspace.update(&mut cx, |workspace, cx| {
let nav_history = workspace.active_pane().read(cx).nav_history().clone();
for definition in definitions {
let range = definition.range.to_offset(definition.buffer.read(cx));
let target_editor_handle = workspace
@ -4180,15 +4183,11 @@ impl Editor {
target_editor_handle.update(cx, |target_editor, cx| {
// When selecting a definition in a different buffer, disable the nav history
// to avoid creating a history entry at the previous cursor location.
let disabled_history = if editor_handle == target_editor_handle {
None
} else {
target_editor.nav_history.take()
};
target_editor.select_ranges([range], Some(Autoscroll::Center), cx);
if disabled_history.is_some() {
target_editor.nav_history = disabled_history;
if editor_handle != target_editor_handle {
nav_history.borrow_mut().disable();
}
target_editor.select_ranges([range], Some(Autoscroll::Center), cx);
nav_history.borrow_mut().enable();
});
}
});
@ -4207,8 +4206,7 @@ impl Editor {
let editor_handle = active_item.act_as::<Self>(cx)?;
let editor = editor_handle.read(cx);
let buffer = editor.buffer.read(cx);
let head = editor.newest_selection::<usize>(&buffer.read(cx)).head();
let head = editor.newest_selection::<usize>(cx).head();
let (buffer, head) = editor.buffer.read(cx).text_anchor_for_position(head, cx)?;
let replica_id = editor.replica_id(cx);
@ -4432,12 +4430,11 @@ impl Editor {
self.clear_highlighted_ranges::<Rename>(cx);
let editor = rename.editor.read(cx);
let buffer = editor.buffer.read(cx).snapshot(cx);
let selection = editor.newest_selection::<usize>(&buffer);
let snapshot = self.buffer.read(cx).snapshot(cx);
let selection = editor.newest_selection_with_snapshot::<usize>(&snapshot);
// Update the selection to match the position of the selection inside
// the rename editor.
let snapshot = self.buffer.read(cx).snapshot(cx);
let rename_range = rename.range.to_offset(&snapshot);
let start = snapshot
.clip_offset(rename_range.start + selection.start, Bias::Left)
@ -4748,17 +4745,28 @@ impl Editor {
pub fn oldest_selection<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
snapshot: &MultiBufferSnapshot,
cx: &AppContext,
) -> Selection<D> {
let snapshot = self.buffer.read(cx).read(cx);
self.selections
.iter()
.min_by_key(|s| s.id)
.map(|selection| self.resolve_selection(selection, snapshot))
.or_else(|| self.pending_selection(snapshot))
.map(|selection| self.resolve_selection(selection, &snapshot))
.or_else(|| self.pending_selection(&snapshot))
.unwrap()
}
pub fn newest_selection<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
) -> Selection<D> {
self.resolve_selection(
self.newest_anchor_selection(),
&self.buffer.read(cx).read(cx),
)
}
pub fn newest_selection_with_snapshot<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
snapshot: &MultiBufferSnapshot,
) -> Selection<D> {
@ -5145,6 +5153,14 @@ impl Editor {
self.buffer.read(cx).read(cx).text()
}
pub fn set_text(&mut self, text: impl Into<String>, cx: &mut ViewContext<Self>) {
self.buffer
.read(cx)
.as_singleton()
.expect("you can only call set_text on editors for singleton buffers")
.update(cx, |buffer, cx| buffer.set_text(text, cx));
}
pub fn display_text(&self, cx: &mut MutableAppContext) -> String {
self.display_map
.update(cx, |map, cx| map.snapshot(cx))
@ -5344,6 +5360,78 @@ impl Editor {
fn on_display_map_changed(&mut self, _: ModelHandle<DisplayMap>, cx: &mut ViewContext<Self>) {
cx.notify();
}
pub fn set_searchable(&mut self, searchable: bool) {
self.searchable = searchable;
}
pub fn searchable(&self) -> bool {
self.searchable
}
fn open_excerpts(workspace: &mut Workspace, _: &OpenExcerpts, cx: &mut ViewContext<Workspace>) {
let active_item = workspace.active_item(cx);
let editor_handle = if let Some(editor) = active_item
.as_ref()
.and_then(|item| item.act_as::<Self>(cx))
{
editor
} else {
cx.propagate_action();
return;
};
let editor = editor_handle.read(cx);
let buffer = editor.buffer.read(cx);
if buffer.is_singleton() {
cx.propagate_action();
return;
}
let mut new_selections_by_buffer = HashMap::default();
for selection in editor.local_selections::<usize>(cx) {
for (buffer, mut range) in
buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
{
if selection.reversed {
mem::swap(&mut range.start, &mut range.end);
}
new_selections_by_buffer
.entry(buffer)
.or_insert(Vec::new())
.push(range)
}
}
editor_handle.update(cx, |editor, cx| {
editor.push_to_nav_history(editor.newest_anchor_selection().head(), None, cx);
});
let nav_history = workspace.active_pane().read(cx).nav_history().clone();
nav_history.borrow_mut().disable();
// We defer the pane interaction because we ourselves are a workspace item
// and activating a new item causes the pane to call a method on us reentrantly,
// which panics if we're on the stack.
cx.defer(move |workspace, cx| {
for (ix, (buffer, ranges)) in new_selections_by_buffer.into_iter().enumerate() {
let buffer = BufferItemHandle(buffer);
if ix == 0 && !workspace.activate_pane_for_item(&buffer, cx) {
workspace.activate_next_pane(cx);
}
let editor = workspace
.open_item(buffer, cx)
.downcast::<Editor>()
.unwrap();
editor.update(cx, |editor, cx| {
editor.select_ranges(ranges, Some(Autoscroll::Newest), cx);
});
}
nav_history.borrow_mut().enable();
});
}
}
impl EditorSnapshot {
@ -5463,9 +5551,14 @@ fn build_style(
get_field_editor_theme: Option<GetFieldEditorTheme>,
cx: &AppContext,
) -> EditorStyle {
let theme = settings.theme.editor.clone();
let mut theme = settings.theme.editor.clone();
if let Some(get_field_editor_theme) = get_field_editor_theme {
let field_editor_theme = get_field_editor_theme(&settings.theme);
if let Some(background) = field_editor_theme.container.background_color {
theme.background = background;
}
theme.text_color = field_editor_theme.text.color;
theme.selection = field_editor_theme.selection;
EditorStyle {
text: field_editor_theme.text,
placeholder_text: field_editor_theme.placeholder_text,

View file

@ -951,7 +951,7 @@ impl Element for EditorElement {
}
let newest_selection_head = view
.newest_selection::<usize>(&snapshot.buffer_snapshot)
.newest_selection_with_snapshot::<usize>(&snapshot.buffer_snapshot)
.head()
.to_display_point(&snapshot);

View file

@ -158,11 +158,11 @@ impl WeakItemHandle for WeakMultiBufferItemHandle {
}
impl ItemView for Editor {
fn item_id(&self, cx: &AppContext) -> usize {
fn item(&self, cx: &AppContext) -> Box<dyn ItemHandle> {
if let Some(buffer) = self.buffer.read(cx).as_singleton() {
buffer.id()
Box::new(BufferItemHandle(buffer))
} else {
self.buffer.id()
Box::new(MultiBufferItemHandle(self.buffer.clone()))
}
}
@ -194,11 +194,15 @@ impl ItemView for Editor {
})
}
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
fn clone_on_split(
&self,
nav_history: ItemNavHistory,
cx: &mut ViewContext<Self>,
) -> Option<Self>
where
Self: Sized,
{
Some(self.clone(cx))
Some(self.clone(nav_history, cx))
}
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
@ -378,7 +382,9 @@ impl DiagnosticMessage {
fn update(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
let editor = editor.read(cx);
let buffer = editor.buffer().read(cx);
let cursor_position = editor.newest_selection::<usize>(&buffer.read(cx)).head();
let cursor_position = editor
.newest_selection_with_snapshot::<usize>(&buffer.read(cx))
.head();
let new_diagnostic = buffer
.read(cx)
.diagnostics_in_range::<_, usize>(cursor_position..cursor_position)

View file

@ -7,8 +7,9 @@ use collections::{Bound, HashMap, HashSet};
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
pub use language::Completion;
use language::{
Buffer, BufferChunks, BufferSnapshot, Chunk, DiagnosticEntry, Event, File, Language, Outline,
OutlineItem, Selection, ToOffset as _, ToPoint as _, ToPointUtf16 as _, TransactionId,
char_kind, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, DiagnosticEntry, Event, File,
Language, Outline, OutlineItem, Selection, ToOffset as _, ToPoint as _, ToPointUtf16 as _,
TransactionId,
};
use std::{
cell::{Ref, RefCell},
@ -42,6 +43,7 @@ pub struct MultiBuffer {
title: Option<String>,
}
#[derive(Clone)]
struct History {
next_transaction_id: TransactionId,
undo_stack: Vec<Transaction>,
@ -50,14 +52,6 @@ struct History {
group_interval: Duration,
}
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)]
pub enum CharKind {
Newline,
Punctuation,
Whitespace,
Word,
}
#[derive(Clone)]
struct Transaction {
id: TransactionId,
@ -102,6 +96,7 @@ pub struct MultiBufferSnapshot {
}
pub struct ExcerptBoundary {
pub id: ExcerptId,
pub row: u32,
pub buffer: BufferSnapshot,
pub range: Range<text::Anchor>,
@ -174,6 +169,37 @@ impl MultiBuffer {
}
}
pub fn clone(&self, new_cx: &mut ModelContext<Self>) -> Self {
let mut buffers = HashMap::default();
for (buffer_id, buffer_state) in self.buffers.borrow().iter() {
buffers.insert(
*buffer_id,
BufferState {
buffer: buffer_state.buffer.clone(),
last_version: buffer_state.last_version.clone(),
last_parse_count: buffer_state.last_parse_count,
last_selections_update_count: buffer_state.last_selections_update_count,
last_diagnostics_update_count: buffer_state.last_diagnostics_update_count,
last_file_update_count: buffer_state.last_file_update_count,
excerpts: buffer_state.excerpts.clone(),
_subscriptions: [
new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()),
new_cx.subscribe(&buffer_state.buffer, Self::on_buffer_event),
],
},
);
}
Self {
snapshot: RefCell::new(self.snapshot.borrow().clone()),
buffers: RefCell::new(buffers),
subscriptions: Default::default(),
singleton: self.singleton,
replica_id: self.replica_id,
history: self.history.clone(),
title: self.title.clone(),
}
}
pub fn with_title(mut self, title: String) -> Self {
self.title = Some(title);
self
@ -693,6 +719,11 @@ impl MultiBuffer {
O: text::ToOffset,
{
assert_eq!(self.history.transaction_depth, 0);
let mut ranges = ranges.into_iter().peekable();
if ranges.peek().is_none() {
return Default::default();
}
self.sync(cx);
let buffer_id = buffer.id();
@ -733,7 +764,6 @@ impl MultiBuffer {
}
let mut ids = Vec::new();
let mut ranges = ranges.into_iter().peekable();
while let Some(range) = ranges.next() {
let id = ExcerptId::between(&prev_id, &next_id);
if let Err(ix) = buffer_state.excerpts.binary_search(&id) {
@ -773,6 +803,22 @@ impl MultiBuffer {
ids
}
pub fn clear(&mut self, cx: &mut ModelContext<Self>) {
self.sync(cx);
self.buffers.borrow_mut().clear();
let mut snapshot = self.snapshot.borrow_mut();
let prev_len = snapshot.len();
snapshot.excerpts = Default::default();
snapshot.trailing_excerpt_update_count += 1;
snapshot.is_dirty = false;
snapshot.has_conflict = false;
self.subscriptions.publish_mut([Edit {
old: 0..prev_len,
new: 0..0,
}]);
cx.notify();
}
pub fn excerpt_ids_for_buffer(&self, buffer: &ModelHandle<Buffer>) -> Vec<ExcerptId> {
self.buffers
.borrow()
@ -840,6 +886,7 @@ impl MultiBuffer {
excerpt_ids: impl IntoIterator<Item = &'a ExcerptId>,
cx: &mut ModelContext<Self>,
) {
self.sync(cx);
let mut buffers = self.buffers.borrow_mut();
let mut snapshot = self.snapshot.borrow_mut();
let mut new_excerpts = SumTree::new();
@ -1166,6 +1213,12 @@ impl MultiBuffer {
let mut buffers = Vec::new();
for _ in 0..mutation_count {
if rng.gen_bool(0.05) {
log::info!("Clearing multi-buffer");
self.clear(cx);
continue;
}
let excerpt_ids = self
.buffers
.borrow()
@ -1195,16 +1248,26 @@ impl MultiBuffer {
};
let buffer = buffer_handle.read(cx);
let end_ix = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Right);
let start_ix = buffer.clip_offset(rng.gen_range(0..=end_ix), Bias::Left);
let buffer_text = buffer.text();
let ranges = (0..rng.gen_range(0..5))
.map(|_| {
let end_ix =
buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Right);
let start_ix = buffer.clip_offset(rng.gen_range(0..=end_ix), Bias::Left);
start_ix..end_ix
})
.collect::<Vec<_>>();
log::info!(
"Inserting excerpt from buffer {} and range {:?}: {:?}",
"Inserting excerpts from buffer {} and ranges {:?}: {:?}",
buffer_handle.id(),
start_ix..end_ix,
&buffer.text()[start_ix..end_ix]
ranges,
ranges
.iter()
.map(|range| &buffer_text[range.clone()])
.collect::<Vec<_>>()
);
let excerpt_id = self.push_excerpts(buffer_handle.clone(), [start_ix..end_ix], cx);
let excerpt_id = self.push_excerpts(buffer_handle.clone(), ranges, cx);
log::info!("Inserted with id: {:?}", excerpt_id);
} else {
let remove_count = rng.gen_range(1..=excerpt_ids.len());
@ -1342,9 +1405,12 @@ impl MultiBufferSnapshot {
(start..end, word_kind)
}
fn as_singleton(&self) -> Option<&Excerpt> {
pub fn as_singleton(&self) -> Option<(&ExcerptId, usize, &BufferSnapshot)> {
if self.singleton {
self.excerpts.iter().next()
self.excerpts
.iter()
.next()
.map(|e| (&e.id, e.buffer_id, &e.buffer))
} else {
None
}
@ -1359,8 +1425,8 @@ 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);
if let Some((_, _, buffer)) = self.as_singleton() {
return buffer.clip_offset(offset, bias);
}
let mut cursor = self.excerpts.cursor::<usize>();
@ -1378,8 +1444,8 @@ 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);
if let Some((_, _, buffer)) = self.as_singleton() {
return buffer.clip_point(point, bias);
}
let mut cursor = self.excerpts.cursor::<Point>();
@ -1397,8 +1463,8 @@ 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);
if let Some((_, _, buffer)) = self.as_singleton() {
return buffer.clip_point_utf16(point, bias);
}
let mut cursor = self.excerpts.cursor::<PointUtf16>();
@ -1466,8 +1532,8 @@ 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);
if let Some((_, _, buffer)) = self.as_singleton() {
return buffer.offset_to_point(offset);
}
let mut cursor = self.excerpts.cursor::<(usize, Point)>();
@ -1487,8 +1553,8 @@ impl MultiBufferSnapshot {
}
pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 {
if let Some(excerpt) = self.as_singleton() {
return excerpt.buffer.offset_to_point_utf16(offset);
if let Some((_, _, buffer)) = self.as_singleton() {
return buffer.offset_to_point_utf16(offset);
}
let mut cursor = self.excerpts.cursor::<(usize, PointUtf16)>();
@ -1508,8 +1574,8 @@ impl MultiBufferSnapshot {
}
pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 {
if let Some(excerpt) = self.as_singleton() {
return excerpt.buffer.point_to_point_utf16(point);
if let Some((_, _, buffer)) = self.as_singleton() {
return buffer.point_to_point_utf16(point);
}
let mut cursor = self.excerpts.cursor::<(Point, PointUtf16)>();
@ -1529,8 +1595,8 @@ 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);
if let Some((_, _, buffer)) = self.as_singleton() {
return buffer.point_to_offset(point);
}
let mut cursor = self.excerpts.cursor::<(Point, usize)>();
@ -1550,8 +1616,8 @@ 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);
if let Some((_, _, buffer)) = self.as_singleton() {
return buffer.point_utf16_to_offset(point);
}
let mut cursor = self.excerpts.cursor::<(PointUtf16, usize)>();
@ -1711,9 +1777,8 @@ impl MultiBufferSnapshot {
D: TextDimension + Ord + Sub<D, Output = D>,
I: 'a + IntoIterator<Item = &'a Anchor>,
{
if let Some(excerpt) = self.as_singleton() {
return excerpt
.buffer
if let Some((_, _, buffer)) = self.as_singleton() {
return buffer
.summaries_for_anchors(anchors.into_iter().map(|a| &a.text_anchor))
.collect();
}
@ -1878,11 +1943,11 @@ 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() {
if let Some((excerpt_id, buffer_id, buffer)) = self.as_singleton() {
return Anchor {
buffer_id: Some(excerpt.buffer_id),
excerpt_id: excerpt.id.clone(),
text_anchor: excerpt.buffer.anchor_at(offset, bias),
buffer_id: Some(buffer_id),
excerpt_id: excerpt_id.clone(),
text_anchor: buffer.anchor_at(offset, bias),
};
}
@ -1989,6 +2054,7 @@ impl MultiBufferSnapshot {
let excerpt = cursor.item()?;
let starts_new_buffer = Some(excerpt.buffer_id) != prev_buffer_id;
let boundary = ExcerptBoundary {
id: excerpt.id.clone(),
row: cursor.start().1.row,
buffer: excerpt.buffer.clone(),
range: excerpt.range.clone(),
@ -2090,7 +2156,7 @@ impl MultiBufferSnapshot {
{
self.as_singleton()
.into_iter()
.flat_map(move |excerpt| excerpt.buffer.diagnostic_group(group_id))
.flat_map(move |(_, _, buffer)| buffer.diagnostic_group(group_id))
}
pub fn diagnostics_in_range<'a, T, O>(
@ -2101,11 +2167,11 @@ impl MultiBufferSnapshot {
T: 'a + ToOffset,
O: 'a + text::FromAnchor,
{
self.as_singleton().into_iter().flat_map(move |excerpt| {
excerpt
.buffer
.diagnostics_in_range(range.start.to_offset(self)..range.end.to_offset(self))
})
self.as_singleton()
.into_iter()
.flat_map(move |(_, _, buffer)| {
buffer.diagnostics_in_range(range.start.to_offset(self)..range.end.to_offset(self))
})
}
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
@ -2147,16 +2213,16 @@ impl MultiBufferSnapshot {
}
pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
let excerpt = self.as_singleton()?;
let outline = excerpt.buffer.outline(theme)?;
let (excerpt_id, _, buffer) = self.as_singleton()?;
let outline = 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,
@ -2764,18 +2830,6 @@ impl ToPointUtf16 for PointUtf16 {
}
}
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::*;

View file

@ -54,7 +54,7 @@ impl GoToLine {
let buffer = editor.buffer().read(cx).read(cx);
(
Some(scroll_position),
editor.newest_selection(&buffer).head(),
editor.newest_selection_with_snapshot(&buffer).head(),
buffer.max_point(),
)
});

View file

@ -85,6 +85,8 @@ pub trait UpgradeModelHandle {
handle: &WeakModelHandle<T>,
) -> Option<ModelHandle<T>>;
fn model_handle_is_upgradable<T: Entity>(&self, handle: &WeakModelHandle<T>) -> bool;
fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option<AnyModelHandle>;
}
@ -608,6 +610,10 @@ impl UpgradeModelHandle for AsyncAppContext {
self.0.borrow().upgrade_model_handle(handle)
}
fn model_handle_is_upgradable<T: Entity>(&self, handle: &WeakModelHandle<T>) -> bool {
self.0.borrow().model_handle_is_upgradable(handle)
}
fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option<AnyModelHandle> {
self.0.borrow().upgrade_any_model_handle(handle)
}
@ -710,7 +716,7 @@ impl ReadViewWith for TestAppContext {
}
type ActionCallback =
dyn FnMut(&mut dyn AnyView, &dyn AnyAction, &mut MutableAppContext, usize, usize) -> bool;
dyn FnMut(&mut dyn AnyView, &dyn AnyAction, &mut MutableAppContext, usize, usize);
type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext);
type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> bool>;
@ -722,6 +728,7 @@ pub struct MutableAppContext {
foreground_platform: Rc<dyn platform::ForegroundPlatform>,
assets: Arc<AssetCache>,
cx: AppContext,
capture_actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
global_actions: HashMap<TypeId, Box<GlobalActionCallback>>,
keystroke_matcher: keymap::Matcher,
@ -741,6 +748,7 @@ pub struct MutableAppContext {
pending_flushes: usize,
flushing_effects: bool,
next_cursor_style_handle_id: Arc<AtomicUsize>,
halt_action_dispatch: bool,
}
impl MutableAppContext {
@ -761,12 +769,14 @@ impl MutableAppContext {
models: Default::default(),
views: Default::default(),
windows: Default::default(),
app_states: Default::default(),
element_states: Default::default(),
ref_counts: Arc::new(Mutex::new(RefCounts::default())),
background,
font_cache,
platform,
},
capture_actions: HashMap::new(),
actions: HashMap::new(),
global_actions: HashMap::new(),
keystroke_matcher: keymap::Matcher::default(),
@ -785,6 +795,7 @@ impl MutableAppContext {
pending_flushes: 0,
flushing_effects: false,
next_cursor_style_handle_id: Default::default(),
halt_action_dispatch: false,
}
}
@ -855,7 +866,25 @@ impl MutableAppContext {
.map(|debug_elements| debug_elements(&self.cx))
}
pub fn add_action<A, V, F>(&mut self, mut handler: F)
pub fn add_action<A, V, F>(&mut self, handler: F)
where
A: Action,
V: View,
F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>),
{
self.add_action_internal(handler, false)
}
pub fn capture_action<A, V, F>(&mut self, handler: F)
where
A: Action,
V: View,
F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>),
{
self.add_action_internal(handler, true)
}
fn add_action_internal<A, V, F>(&mut self, mut handler: F, capture: bool)
where
A: Action,
V: View,
@ -876,11 +905,16 @@ impl MutableAppContext {
action,
&mut cx,
);
cx.halt_action_dispatch
},
);
self.actions
let actions = if capture {
&mut self.capture_actions
} else {
&mut self.actions
};
actions
.entry(TypeId::of::<V>())
.or_default()
.entry(TypeId::of::<A>())
@ -1167,45 +1201,59 @@ impl MutableAppContext {
action: &dyn AnyAction,
) -> bool {
self.update(|this| {
let mut halted_dispatch = false;
for view_id in path.iter().rev() {
if let Some(mut view) = this.cx.views.remove(&(window_id, *view_id)) {
this.halt_action_dispatch = false;
for (capture_phase, view_id) in path
.iter()
.map(|view_id| (true, *view_id))
.chain(path.iter().rev().map(|view_id| (false, *view_id)))
{
if let Some(mut view) = this.cx.views.remove(&(window_id, view_id)) {
let type_id = view.as_any().type_id();
if let Some((name, mut handlers)) = this
.actions
.actions_mut(capture_phase)
.get_mut(&type_id)
.and_then(|h| h.remove_entry(&action.id()))
{
for handler in handlers.iter_mut().rev() {
let halt_dispatch =
handler(view.as_mut(), action, this, window_id, *view_id);
if halt_dispatch {
halted_dispatch = true;
this.halt_action_dispatch = true;
handler(view.as_mut(), action, this, window_id, view_id);
if this.halt_action_dispatch {
break;
}
}
this.actions
this.actions_mut(capture_phase)
.get_mut(&type_id)
.unwrap()
.insert(name, handlers);
}
this.cx.views.insert((window_id, *view_id), view);
this.cx.views.insert((window_id, view_id), view);
if halted_dispatch {
if this.halt_action_dispatch {
break;
}
}
}
if !halted_dispatch {
halted_dispatch = this.dispatch_global_action_any(action);
if !this.halt_action_dispatch {
this.dispatch_global_action_any(action);
}
halted_dispatch
this.halt_action_dispatch
})
}
fn actions_mut(
&mut self,
capture_phase: bool,
) -> &mut HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>> {
if capture_phase {
&mut self.capture_actions
} else {
&mut self.actions
}
}
pub fn dispatch_global_action<A: Action>(&mut self, action: A) {
self.dispatch_global_action_any(&action);
}
@ -1265,6 +1313,27 @@ impl MutableAppContext {
Ok(pending)
}
pub fn add_app_state<T: 'static>(&mut self, state: T) {
self.cx
.app_states
.insert(TypeId::of::<T>(), Box::new(state));
}
pub fn update_app_state<T: 'static, F, U>(&mut self, update: F) -> U
where
F: FnOnce(&mut T, &mut MutableAppContext) -> U,
{
let type_id = TypeId::of::<T>();
let mut state = self
.cx
.app_states
.remove(&type_id)
.expect("no app state has been added for this type");
let result = update(state.downcast_mut().unwrap(), self);
self.cx.app_states.insert(type_id, state);
result
}
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
where
T: Entity,
@ -1787,6 +1856,10 @@ impl UpgradeModelHandle for MutableAppContext {
self.cx.upgrade_model_handle(handle)
}
fn model_handle_is_upgradable<T: Entity>(&self, handle: &WeakModelHandle<T>) -> bool {
self.cx.model_handle_is_upgradable(handle)
}
fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option<AnyModelHandle> {
self.cx.upgrade_any_model_handle(handle)
}
@ -1857,6 +1930,7 @@ pub struct AppContext {
models: HashMap<usize, Box<dyn AnyModel>>,
views: HashMap<(usize, usize), Box<dyn AnyView>>,
windows: HashMap<usize, Window>,
app_states: HashMap<TypeId, Box<dyn Any>>,
element_states: HashMap<ElementStateId, Box<dyn Any>>,
background: Arc<executor::Background>,
ref_counts: Arc<Mutex<RefCounts>>,
@ -1888,6 +1962,14 @@ impl AppContext {
pub fn platform(&self) -> &Arc<dyn Platform> {
&self.platform
}
pub fn app_state<T: 'static>(&self) -> &T {
self.app_states
.get(&TypeId::of::<T>())
.expect("no app state has been added for this type")
.downcast_ref()
.unwrap()
}
}
impl ReadModel for AppContext {
@ -1915,6 +1997,10 @@ impl UpgradeModelHandle for AppContext {
}
}
fn model_handle_is_upgradable<T: Entity>(&self, handle: &WeakModelHandle<T>) -> bool {
self.models.contains_key(&handle.model_id)
}
fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option<AnyModelHandle> {
if self.models.contains_key(&handle.model_id) {
self.ref_counts.lock().inc_model(handle.model_id);
@ -2320,6 +2406,10 @@ impl<M> UpgradeModelHandle for ModelContext<'_, M> {
self.cx.upgrade_model_handle(handle)
}
fn model_handle_is_upgradable<T: Entity>(&self, handle: &WeakModelHandle<T>) -> bool {
self.cx.model_handle_is_upgradable(handle)
}
fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option<AnyModelHandle> {
self.cx.upgrade_any_model_handle(handle)
}
@ -2344,7 +2434,6 @@ pub struct ViewContext<'a, T: ?Sized> {
window_id: usize,
view_id: usize,
view_type: PhantomData<T>,
halt_action_dispatch: bool,
}
impl<'a, T: View> ViewContext<'a, T> {
@ -2354,7 +2443,6 @@ impl<'a, T: View> ViewContext<'a, T> {
window_id,
view_id,
view_type: PhantomData,
halt_action_dispatch: true,
}
}
@ -2529,7 +2617,7 @@ impl<'a, T: View> ViewContext<'a, T> {
}
pub fn propagate_action(&mut self) {
self.halt_action_dispatch = false;
self.app.halt_action_dispatch = false;
}
pub fn spawn<F, Fut, S>(&self, f: F) -> Task<S>
@ -2660,6 +2748,10 @@ impl<V> UpgradeModelHandle for ViewContext<'_, V> {
self.cx.upgrade_model_handle(handle)
}
fn model_handle_is_upgradable<T: Entity>(&self, handle: &WeakModelHandle<T>) -> bool {
self.cx.model_handle_is_upgradable(handle)
}
fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option<AnyModelHandle> {
self.cx.upgrade_any_model_handle(handle)
}
@ -2902,6 +2994,12 @@ impl<T> PartialEq for ModelHandle<T> {
impl<T> Eq for ModelHandle<T> {}
impl<T> PartialEq<WeakModelHandle<T>> for ModelHandle<T> {
fn eq(&self, other: &WeakModelHandle<T>) -> bool {
self.model_id == other.model_id
}
}
impl<T> Hash for ModelHandle<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.model_id.hash(state);
@ -2974,6 +3072,10 @@ impl<T: Entity> WeakModelHandle<T> {
self.model_id
}
pub fn is_upgradable(&self, cx: &impl UpgradeModelHandle) -> bool {
cx.model_handle_is_upgradable(self)
}
pub fn upgrade(&self, cx: &impl UpgradeModelHandle) -> Option<ModelHandle<T>> {
cx.upgrade_model_handle(self)
}
@ -4322,37 +4424,58 @@ mod tests {
let actions = Rc::new(RefCell::new(Vec::new()));
let actions_clone = actions.clone();
cx.add_global_action(move |_: &Action, _: &mut MutableAppContext| {
actions_clone.borrow_mut().push("global".to_string());
});
{
let actions = actions.clone();
cx.add_global_action(move |_: &Action, _: &mut MutableAppContext| {
actions.borrow_mut().push("global".to_string());
});
}
let actions_clone = actions.clone();
cx.add_action(move |view: &mut ViewA, action: &Action, cx| {
assert_eq!(action.0, "bar");
cx.propagate_action();
actions_clone.borrow_mut().push(format!("{} a", view.id));
});
let actions_clone = actions.clone();
cx.add_action(move |view: &mut ViewA, _: &Action, cx| {
if view.id != 1 {
{
let actions = actions.clone();
cx.add_action(move |view: &mut ViewA, action: &Action, cx| {
assert_eq!(action.0, "bar");
cx.propagate_action();
}
actions_clone.borrow_mut().push(format!("{} b", view.id));
});
actions.borrow_mut().push(format!("{} a", view.id));
});
}
let actions_clone = actions.clone();
cx.add_action(move |view: &mut ViewB, _: &Action, cx| {
cx.propagate_action();
actions_clone.borrow_mut().push(format!("{} c", view.id));
});
{
let actions = actions.clone();
cx.add_action(move |view: &mut ViewA, _: &Action, cx| {
if view.id != 1 {
cx.add_view(|cx| {
cx.propagate_action(); // Still works on a nested ViewContext
ViewB { id: 5 }
});
}
actions.borrow_mut().push(format!("{} b", view.id));
});
}
let actions_clone = actions.clone();
cx.add_action(move |view: &mut ViewB, _: &Action, cx| {
cx.propagate_action();
actions_clone.borrow_mut().push(format!("{} d", view.id));
});
{
let actions = actions.clone();
cx.add_action(move |view: &mut ViewB, _: &Action, cx| {
cx.propagate_action();
actions.borrow_mut().push(format!("{} c", view.id));
});
}
{
let actions = actions.clone();
cx.add_action(move |view: &mut ViewB, _: &Action, cx| {
cx.propagate_action();
actions.borrow_mut().push(format!("{} d", view.id));
});
}
{
let actions = actions.clone();
cx.capture_action(move |view: &mut ViewA, _: &Action, cx| {
cx.propagate_action();
actions.borrow_mut().push(format!("{} capture", view.id));
});
}
let (window_id, view_1) = cx.add_window(Default::default(), |_| ViewA { id: 1 });
let view_2 = cx.add_view(window_id, |_| ViewB { id: 2 });
@ -4367,7 +4490,17 @@ mod tests {
assert_eq!(
*actions.borrow(),
vec!["4 d", "4 c", "3 b", "3 a", "2 d", "2 c", "1 b"]
vec![
"1 capture",
"3 capture",
"4 d",
"4 c",
"3 b",
"3 a",
"2 d",
"2 c",
"1 b"
]
);
// Remove view_1, which doesn't propagate the action
@ -4380,7 +4513,16 @@ mod tests {
assert_eq!(
*actions.borrow(),
vec!["4 d", "4 c", "3 b", "3 a", "2 d", "2 c", "global"]
vec![
"3 capture",
"4 d",
"4 c",
"3 b",
"3 a",
"2 d",
"2 c",
"global"
]
);
}

View file

@ -76,6 +76,19 @@ pub enum MatchResult {
Action(Box<dyn AnyAction>),
}
impl Debug for MatchResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MatchResult::None => f.debug_struct("MatchResult::None").finish(),
MatchResult::Pending => f.debug_struct("MatchResult::Pending").finish(),
MatchResult::Action(action) => f
.debug_tuple("MatchResult::Action")
.field(&action.name())
.finish(),
}
}
}
impl Matcher {
pub fn new(keymap: Keymap) -> Self {
Self {

View file

@ -281,6 +281,10 @@ impl<'a> UpgradeModelHandle for LayoutContext<'a> {
self.app.upgrade_model_handle(handle)
}
fn model_handle_is_upgradable<T: Entity>(&self, handle: &WeakModelHandle<T>) -> bool {
self.app.model_handle_is_upgradable(handle)
}
fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option<AnyModelHandle> {
self.app.upgrade_any_model_handle(handle)
}

View file

@ -365,6 +365,14 @@ pub(crate) struct DiagnosticEndpoint {
severity: DiagnosticSeverity,
}
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)]
pub enum CharKind {
Newline,
Punctuation,
Whitespace,
Word,
}
impl Buffer {
pub fn new<T: Into<Arc<str>>>(
replica_id: ReplicaId,
@ -1337,6 +1345,13 @@ impl Buffer {
let _ = language_server.latest_snapshot.blocking_send(snapshot);
}
pub fn set_text<T>(&mut self, text: T, cx: &mut ModelContext<Self>) -> Option<clock::Local>
where
T: Into<String>,
{
self.edit_internal([0..self.len()], text, false, cx)
}
pub fn edit<I, S, T>(
&mut self,
ranges_iter: I,
@ -2659,3 +2674,15 @@ pub fn contiguous_ranges(
}
})
}
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
}
}

View file

@ -33,7 +33,7 @@ type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>;
pub struct LanguageServer {
next_id: AtomicUsize,
outbound_tx: RwLock<Option<channel::Sender<Vec<u8>>>>,
outbound_tx: channel::Sender<Vec<u8>>,
capabilities: watch::Receiver<Option<ServerCapabilities>>,
notification_handlers: Arc<RwLock<HashMap<&'static str, NotificationHandler>>>,
response_handlers: Arc<Mutex<HashMap<usize, ResponseHandler>>>,
@ -213,7 +213,7 @@ impl LanguageServer {
response_handlers,
capabilities: capabilities_rx,
next_id: Default::default(),
outbound_tx: RwLock::new(Some(outbound_tx)),
outbound_tx,
executor: executor.clone(),
io_tasks: Mutex::new(Some((input_task, output_task))),
initialized: initialized_rx,
@ -296,37 +296,41 @@ impl LanguageServer {
let request = Self::request_internal::<request::Initialize>(
&this.next_id,
&this.response_handlers,
this.outbound_tx.read().as_ref(),
&this.outbound_tx,
params,
);
let response = request.await?;
Self::notify_internal::<notification::Initialized>(
this.outbound_tx.read().as_ref(),
&this.outbound_tx,
InitializedParams {},
)?;
Ok(response.capabilities)
}
pub fn shutdown(&self) -> Option<impl 'static + Send + Future<Output = Result<()>>> {
pub fn shutdown(&self) -> Option<impl 'static + Send + Future<Output = Option<()>>> {
if let Some(tasks) = self.io_tasks.lock().take() {
let response_handlers = self.response_handlers.clone();
let outbound_tx = self.outbound_tx.write().take();
let next_id = AtomicUsize::new(self.next_id.load(SeqCst));
let outbound_tx = self.outbound_tx.clone();
let mut output_done = self.output_done_rx.lock().take().unwrap();
Some(async move {
Self::request_internal::<request::Shutdown>(
&next_id,
&response_handlers,
outbound_tx.as_ref(),
(),
)
.await?;
Self::notify_internal::<notification::Exit>(outbound_tx.as_ref(), ())?;
drop(outbound_tx);
output_done.recv().await;
drop(tasks);
Ok(())
})
let shutdown_request = Self::request_internal::<request::Shutdown>(
&next_id,
&response_handlers,
&outbound_tx,
(),
);
let exit = Self::notify_internal::<notification::Exit>(&outbound_tx, ());
outbound_tx.close();
Some(
async move {
shutdown_request.await?;
exit?;
output_done.recv().await;
drop(tasks);
Ok(())
}
.log_err(),
)
} else {
None
}
@ -375,7 +379,7 @@ impl LanguageServer {
Self::request_internal::<T>(
&this.next_id,
&this.response_handlers,
this.outbound_tx.read().as_ref(),
&this.outbound_tx,
params,
)
.await
@ -385,7 +389,7 @@ impl LanguageServer {
fn request_internal<T: request::Request>(
next_id: &AtomicUsize,
response_handlers: &Mutex<HashMap<usize, ResponseHandler>>,
outbound_tx: Option<&channel::Sender<Vec<u8>>>,
outbound_tx: &channel::Sender<Vec<u8>>,
params: T::Params,
) -> impl 'static + Future<Output = Result<T::Result>>
where
@ -415,16 +419,8 @@ impl LanguageServer {
);
let send = outbound_tx
.as_ref()
.ok_or_else(|| {
anyhow!("tried to send a request to a language server that has been shut down")
})
.and_then(|outbound_tx| {
outbound_tx
.try_send(message)
.context("failed to write to language server's stdin")?;
Ok(())
});
.try_send(message)
.context("failed to write to language server's stdin");
async move {
send?;
rx.recv().await.unwrap()
@ -438,13 +434,13 @@ impl LanguageServer {
let this = self.clone();
async move {
this.initialized.clone().recv().await;
Self::notify_internal::<T>(this.outbound_tx.read().as_ref(), params)?;
Self::notify_internal::<T>(&this.outbound_tx, params)?;
Ok(())
}
}
fn notify_internal<T: notification::Notification>(
outbound_tx: Option<&channel::Sender<Vec<u8>>>,
outbound_tx: &channel::Sender<Vec<u8>>,
params: T::Params,
) -> Result<()> {
let message = serde_json::to_vec(&Notification {
@ -453,9 +449,6 @@ impl LanguageServer {
params,
})
.unwrap();
let outbound_tx = outbound_tx
.as_ref()
.ok_or_else(|| anyhow!("tried to notify a language server that has been shut down"))?;
outbound_tx.try_send(message)?;
Ok(())
}

View file

@ -259,7 +259,9 @@ impl OutlineView {
let editor = self.active_editor.read(cx);
let buffer = editor.buffer().read(cx).read(cx);
let cursor_offset = editor.newest_selection::<usize>(&buffer).head();
let cursor_offset = editor
.newest_selection_with_snapshot::<usize>(&buffer)
.head();
selected_index = self
.outline
.items

View file

@ -26,6 +26,7 @@ lsp = { path = "../lsp" }
rpc = { path = "../rpc" }
sum_tree = { path = "../sum_tree" }
util = { path = "../util" }
aho-corasick = "0.7"
anyhow = "1.0.38"
async-trait = "0.1"
futures = "0.3"
@ -36,6 +37,7 @@ log = "0.4"
parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] }
rand = "0.8.3"
regex = "1.5"
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1.0.64", features = ["preserve_order"] }
sha2 = "0.10"

View file

@ -18,6 +18,7 @@ pub trait Fs: Send + Sync {
async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>;
async fn load(&self, path: &Path) -> Result<String>;
async fn save(&self, path: &Path, text: &Rope) -> Result<()>;
async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
@ -121,6 +122,10 @@ impl Fs for RealFs {
}
}
async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
Ok(Box::new(std::fs::File::open(path)?))
}
async fn load(&self, path: &Path) -> Result<String> {
let mut file = smol::fs::File::open(path).await?;
let mut text = String::new();
@ -203,7 +208,6 @@ impl Fs for RealFs {
fn is_fake(&self) -> bool {
false
}
#[cfg(any(test, feature = "test-support"))]
fn as_fake(&self) -> &FakeFs {
panic!("called `RealFs::as_fake`")
@ -535,6 +539,11 @@ impl Fs for FakeFs {
Ok(())
}
async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
let text = self.load(path).await?;
Ok(Box::new(io::Cursor::new(text)))
}
async fn load(&self, path: &Path) -> Result<String> {
let path = normalize_path(path);
self.executor.simulate_random_delay().await;

View file

@ -1,4 +1,4 @@
use crate::{BufferRequestHandle, DocumentHighlight, Location, Project, ProjectTransaction};
use crate::{DocumentHighlight, Location, Project, ProjectTransaction};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use client::{proto, PeerId};
@ -48,7 +48,6 @@ pub(crate) trait LspCommand: 'static + Sized {
message: <Self::ProtoRequest as proto::RequestMessage>::Response,
project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>,
request_handle: BufferRequestHandle,
cx: AsyncAppContext,
) -> Result<Self::Response>;
fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64;
@ -162,7 +161,6 @@ impl LspCommand for PrepareRename {
message: proto::PrepareRenameResponse,
_: ModelHandle<Project>,
buffer: ModelHandle<Buffer>,
_: BufferRequestHandle,
mut cx: AsyncAppContext,
) -> Result<Option<Range<Anchor>>> {
if message.can_rename {
@ -279,7 +277,6 @@ impl LspCommand for PerformRename {
message: proto::PerformRenameResponse,
project: ModelHandle<Project>,
_: ModelHandle<Buffer>,
request_handle: BufferRequestHandle,
mut cx: AsyncAppContext,
) -> Result<ProjectTransaction> {
let message = message
@ -287,12 +284,7 @@ impl LspCommand for PerformRename {
.ok_or_else(|| anyhow!("missing transaction"))?;
project
.update(&mut cx, |project, cx| {
project.deserialize_project_transaction(
message,
self.push_to_history,
request_handle,
cx,
)
project.deserialize_project_transaction(message, self.push_to_history, cx)
})
.await
}
@ -435,16 +427,13 @@ impl LspCommand for GetDefinition {
message: proto::GetDefinitionResponse,
project: ModelHandle<Project>,
_: ModelHandle<Buffer>,
request_handle: BufferRequestHandle,
mut cx: AsyncAppContext,
) -> Result<Vec<Location>> {
let mut locations = Vec::new();
for location in message.locations {
let buffer = location.buffer.ok_or_else(|| anyhow!("missing buffer"))?;
let buffer = project
.update(&mut cx, |this, cx| {
this.deserialize_buffer(buffer, request_handle.clone(), cx)
})
.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
.await?;
let start = location
.start
@ -586,16 +575,13 @@ impl LspCommand for GetReferences {
message: proto::GetReferencesResponse,
project: ModelHandle<Project>,
_: ModelHandle<Buffer>,
request_handle: BufferRequestHandle,
mut cx: AsyncAppContext,
) -> Result<Vec<Location>> {
let mut locations = Vec::new();
for location in message.locations {
let buffer = location.buffer.ok_or_else(|| anyhow!("missing buffer"))?;
let target_buffer = project
.update(&mut cx, |this, cx| {
this.deserialize_buffer(buffer, request_handle.clone(), cx)
})
.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
.await?;
let start = location
.start
@ -720,7 +706,6 @@ impl LspCommand for GetDocumentHighlights {
message: proto::GetDocumentHighlightsResponse,
_: ModelHandle<Project>,
_: ModelHandle<Buffer>,
_: BufferRequestHandle,
_: AsyncAppContext,
) -> Result<Vec<DocumentHighlight>> {
Ok(message

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,227 @@
use aho_corasick::{AhoCorasick, AhoCorasickBuilder};
use anyhow::Result;
use client::proto;
use language::{char_kind, Rope};
use regex::{Regex, RegexBuilder};
use smol::future::yield_now;
use std::{
io::{BufRead, BufReader, Read},
ops::Range,
sync::Arc,
};
#[derive(Clone)]
pub enum SearchQuery {
Text {
search: Arc<AhoCorasick<usize>>,
query: Arc<str>,
whole_word: bool,
case_sensitive: bool,
},
Regex {
regex: Regex,
query: Arc<str>,
multiline: bool,
whole_word: bool,
case_sensitive: bool,
},
}
impl SearchQuery {
pub fn text(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Self {
let query = query.to_string();
let search = AhoCorasickBuilder::new()
.auto_configure(&[&query])
.ascii_case_insensitive(!case_sensitive)
.build(&[&query]);
Self::Text {
search: Arc::new(search),
query: Arc::from(query),
whole_word,
case_sensitive,
}
}
pub fn regex(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Result<Self> {
let mut query = query.to_string();
let initial_query = Arc::from(query.as_str());
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 multiline = query.contains("\n") || query.contains("\\n");
let regex = RegexBuilder::new(&query)
.case_insensitive(!case_sensitive)
.multi_line(multiline)
.build()?;
Ok(Self::Regex {
regex,
query: initial_query,
multiline,
whole_word,
case_sensitive,
})
}
pub fn from_proto(message: proto::SearchProject) -> Result<Self> {
if message.regex {
Self::regex(message.query, message.whole_word, message.case_sensitive)
} else {
Ok(Self::text(
message.query,
message.whole_word,
message.case_sensitive,
))
}
}
pub fn to_proto(&self, project_id: u64) -> proto::SearchProject {
proto::SearchProject {
project_id,
query: self.as_str().to_string(),
regex: self.is_regex(),
whole_word: self.whole_word(),
case_sensitive: self.case_sensitive(),
}
}
pub fn detect<T: Read>(&self, stream: T) -> Result<bool> {
if self.as_str().is_empty() {
return Ok(false);
}
match self {
Self::Text { search, .. } => {
let mat = search.stream_find_iter(stream).next();
match mat {
Some(Ok(_)) => Ok(true),
Some(Err(err)) => Err(err.into()),
None => Ok(false),
}
}
Self::Regex {
regex, multiline, ..
} => {
let mut reader = BufReader::new(stream);
if *multiline {
let mut text = String::new();
if let Err(err) = reader.read_to_string(&mut text) {
Err(err.into())
} else {
Ok(regex.find(&text).is_some())
}
} else {
for line in reader.lines() {
let line = line?;
if regex.find(&line).is_some() {
return Ok(true);
}
}
Ok(false)
}
}
}
}
pub async fn search(&self, rope: &Rope) -> Vec<Range<usize>> {
const YIELD_INTERVAL: usize = 20000;
if self.as_str().is_empty() {
return Default::default();
}
let mut matches = Vec::new();
match self {
Self::Text {
search, whole_word, ..
} => {
for (ix, mat) in search
.stream_find_iter(rope.bytes_in_range(0..rope.len()))
.enumerate()
{
if (ix + 1) % YIELD_INTERVAL == 0 {
yield_now().await;
}
let mat = mat.unwrap();
if *whole_word {
let prev_kind = rope.reversed_chars_at(mat.start()).next().map(char_kind);
let start_kind = char_kind(rope.chars_at(mat.start()).next().unwrap());
let end_kind = char_kind(rope.reversed_chars_at(mat.end()).next().unwrap());
let next_kind = rope.chars_at(mat.end()).next().map(char_kind);
if Some(start_kind) == prev_kind || Some(end_kind) == next_kind {
continue;
}
}
matches.push(mat.start()..mat.end())
}
}
Self::Regex {
regex, multiline, ..
} => {
if *multiline {
let text = rope.to_string();
for (ix, mat) in regex.find_iter(&text).enumerate() {
if (ix + 1) % YIELD_INTERVAL == 0 {
yield_now().await;
}
matches.push(mat.start()..mat.end());
}
} else {
let mut line = String::new();
let mut line_offset = 0;
for (chunk_ix, chunk) in rope.chunks().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();
matches.push(start..end);
}
line_offset += line.len() + 1;
line.clear();
}
line.push_str(text);
}
}
}
}
}
matches
}
pub fn as_str(&self) -> &str {
match self {
Self::Text { query, .. } => query.as_ref(),
Self::Regex { query, .. } => query.as_ref(),
}
}
pub fn whole_word(&self) -> bool {
match self {
Self::Text { whole_word, .. } => *whole_word,
Self::Regex { whole_word, .. } => *whole_word,
}
}
pub fn case_sensitive(&self) -> bool {
match self {
Self::Text { case_sensitive, .. } => *case_sensitive,
Self::Regex { case_sensitive, .. } => *case_sensitive,
}
}
pub fn is_regex(&self) -> bool {
matches!(self, Self::Regex { .. })
}
}

View file

@ -554,10 +554,6 @@ impl LocalWorktree {
Ok((tree, scan_states_tx))
}
pub fn abs_path(&self) -> &Arc<Path> {
&self.abs_path
}
pub fn contains_abs_path(&self, path: &Path) -> bool {
path.starts_with(&self.abs_path)
}
@ -1017,6 +1013,10 @@ impl Snapshot {
}
impl LocalSnapshot {
pub fn abs_path(&self) -> &Arc<Path> {
&self.abs_path
}
#[cfg(test)]
pub(crate) fn to_proto(
&self,

View file

@ -21,6 +21,7 @@ message Envelope {
LeaveProject leave_project = 15;
AddProjectCollaborator add_project_collaborator = 16;
RemoveProjectCollaborator remove_project_collaborator = 17;
GetDefinition get_definition = 18;
GetDefinitionResponse get_definition_response = 19;
GetReferences get_references = 20;
@ -61,22 +62,24 @@ message Envelope {
PrepareRenameResponse prepare_rename_response = 54;
PerformRename perform_rename = 55;
PerformRenameResponse perform_rename_response = 56;
SearchProject search_project = 57;
SearchProjectResponse search_project_response = 58;
GetChannels get_channels = 57;
GetChannelsResponse get_channels_response = 58;
JoinChannel join_channel = 59;
JoinChannelResponse join_channel_response = 60;
LeaveChannel leave_channel = 61;
SendChannelMessage send_channel_message = 62;
SendChannelMessageResponse send_channel_message_response = 63;
ChannelMessageSent channel_message_sent = 64;
GetChannelMessages get_channel_messages = 65;
GetChannelMessagesResponse get_channel_messages_response = 66;
GetChannels get_channels = 59;
GetChannelsResponse get_channels_response = 60;
JoinChannel join_channel = 61;
JoinChannelResponse join_channel_response = 62;
LeaveChannel leave_channel = 63;
SendChannelMessage send_channel_message = 64;
SendChannelMessageResponse send_channel_message_response = 65;
ChannelMessageSent channel_message_sent = 66;
GetChannelMessages get_channel_messages = 67;
GetChannelMessagesResponse get_channel_messages_response = 68;
UpdateContacts update_contacts = 67;
UpdateContacts update_contacts = 69;
GetUsers get_users = 68;
GetUsersResponse get_users_response = 69;
GetUsers get_users = 70;
GetUsersResponse get_users_response = 71;
}
}
@ -366,6 +369,18 @@ message PerformRenameResponse {
ProjectTransaction transaction = 2;
}
message SearchProject {
uint64 project_id = 1;
string query = 2;
bool regex = 3;
bool whole_word = 4;
bool case_sensitive = 5;
}
message SearchProjectResponse {
repeated Location locations = 1;
}
message CodeAction {
Anchor start = 1;
Anchor end = 2;

View file

@ -190,6 +190,8 @@ messages!(
(RegisterWorktree, Foreground),
(RemoveProjectCollaborator, Foreground),
(SaveBuffer, Foreground),
(SearchProject, Foreground),
(SearchProjectResponse, Foreground),
(SendChannelMessage, Foreground),
(SendChannelMessageResponse, Foreground),
(ShareProject, Foreground),
@ -230,6 +232,7 @@ request_messages!(
(RegisterProject, RegisterProjectResponse),
(RegisterWorktree, Ack),
(SaveBuffer, BufferSaved),
(SearchProject, SearchProjectResponse),
(SendChannelMessage, SendChannelMessageResponse),
(ShareProject, Ack),
(Test, Test),
@ -262,6 +265,7 @@ entity_messages!(
PrepareRename,
RemoveProjectCollaborator,
SaveBuffer,
SearchProject,
UnregisterWorktree,
UnshareProject,
UpdateBuffer,

View file

@ -1,25 +1,27 @@
[package]
name = "find"
name = "search"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/find.rs"
path = "src/search.rs"
[dependencies]
collections = { path = "../collections" }
editor = { path = "../editor" }
gpui = { path = "../gpui" }
language = { path = "../language" }
project = { path = "../project" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
aho-corasick = "0.7"
anyhow = "1.0"
log = "0.4"
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"] }
serde_json = { version = "1.0.64", features = ["preserve_order"] }
workspace = { path = "../workspace", features = ["test-support"] }
unindent = "0.1"

View file

@ -1,61 +1,45 @@
use aho_corasick::AhoCorasickBuilder;
use anyhow::Result;
use crate::{active_match_index, match_index_for_direction, Direction, SearchOption, SelectMatch};
use collections::HashMap;
use editor::{
char_kind, display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor, MultiBufferSnapshot,
};
use editor::{display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor};
use gpui::{
action, elements::*, keymap::Binding, platform::CursorStyle, Entity, MutableAppContext,
RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use language::AnchorRangeExt;
use postage::watch;
use regex::RegexBuilder;
use smol::future::yield_now;
use std::{
cmp::{self, Ordering},
ops::Range,
};
use project::search::SearchQuery;
use std::ops::Range;
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,
}
action!(ToggleSearchOption, SearchOption);
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")),
Binding::new("escape", Dismiss, Some("SearchBar")),
Binding::new("cmd-f", FocusEditor, Some("SearchBar")),
Binding::new("enter", SelectMatch(Direction::Next), Some("SearchBar")),
Binding::new(
"shift-enter",
SelectMatch(Direction::Prev),
Some("SearchBar"),
),
Binding::new("cmd-g", SelectMatch(Direction::Next), Some("Pane")),
Binding::new("cmd-shift-G", SelectMatch(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);
cx.add_action(SearchBar::deploy);
cx.add_action(SearchBar::dismiss);
cx.add_action(SearchBar::focus_editor);
cx.add_action(SearchBar::toggle_search_option);
cx.add_action(SearchBar::select_match);
cx.add_action(SearchBar::select_match_on_pane);
}
struct FindBar {
struct SearchBar {
settings: watch::Receiver<Settings>,
query_editor: ViewHandle<Editor>,
active_editor: Option<ViewHandle<Editor>>,
@ -63,20 +47,20 @@ struct FindBar {
active_editor_subscription: Option<Subscription>,
editors_with_matches: HashMap<WeakViewHandle<Editor>, Vec<Range<Anchor>>>,
pending_search: Option<Task<()>>,
case_sensitive_mode: bool,
whole_word_mode: bool,
regex_mode: bool,
case_sensitive: bool,
whole_word: bool,
regex: bool,
query_contains_error: bool,
dismissed: bool,
}
impl Entity for FindBar {
impl Entity for SearchBar {
type Event = ();
}
impl View for FindBar {
impl View for SearchBar {
fn ui_name() -> &'static str {
"FindBar"
"SearchBar"
}
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
@ -86,9 +70,9 @@ impl View for FindBar {
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
theme.search.invalid_editor
} else {
theme.find.editor.input.container
theme.search.editor.input.container
};
Flex::row()
.with_child(
@ -97,16 +81,16 @@ impl View for FindBar {
.with_style(editor_container)
.aligned()
.constrained()
.with_max_width(theme.find.editor.max_width)
.with_max_width(theme.search.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))
.with_child(self.render_search_option("Case", SearchOption::CaseSensitive, cx))
.with_child(self.render_search_option("Word", SearchOption::WholeWord, cx))
.with_child(self.render_search_option("Regex", SearchOption::Regex, cx))
.contained()
.with_style(theme.find.mode_button_group)
.with_style(theme.search.option_button_group)
.aligned()
.boxed(),
)
@ -126,22 +110,22 @@ impl View for FindBar {
};
Some(
Label::new(message, theme.find.match_index.text.clone())
Label::new(message, theme.search.match_index.text.clone())
.contained()
.with_style(theme.find.match_index.container)
.with_style(theme.search.match_index.container)
.aligned()
.boxed(),
)
}))
.contained()
.with_style(theme.find.container)
.with_style(theme.search.container)
.constrained()
.with_height(theme.workspace.toolbar.height)
.named("find bar")
.named("search bar")
}
}
impl Toolbar for FindBar {
impl Toolbar for SearchBar {
fn active_item_changed(
&mut self,
item: Option<Box<dyn ItemViewHandle>>,
@ -152,14 +136,15 @@ impl Toolbar for FindBar {
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(false, cx);
true
} else {
false
if editor.read(cx).searchable() {
self.active_editor_subscription =
Some(cx.subscribe(&editor, Self::on_active_editor_event));
self.active_editor = Some(editor);
self.update_matches(false, cx);
return true;
}
}
false
}
fn on_dismiss(&mut self, cx: &mut ViewContext<Self>) {
@ -172,13 +157,13 @@ impl Toolbar for FindBar {
}
}
impl FindBar {
impl SearchBar {
fn new(settings: watch::Receiver<Settings>, cx: &mut ViewContext<Self>) -> Self {
let query_editor = cx.add_view(|cx| {
Editor::auto_height(
2,
settings.clone(),
Some(|theme| theme.find.editor.input.clone()),
Some(|theme| theme.search.editor.input.clone()),
cx,
)
});
@ -191,9 +176,9 @@ impl FindBar {
active_editor_subscription: None,
active_match_index: None,
editors_with_matches: Default::default(),
case_sensitive_mode: false,
whole_word_mode: false,
regex_mode: false,
case_sensitive: false,
whole_word: false,
regex: false,
settings,
pending_search: None,
query_contains_error: false,
@ -210,27 +195,27 @@ impl FindBar {
});
}
fn render_mode_button(
fn render_search_option(
&self,
icon: &str,
mode: SearchMode,
search_option: SearchOption,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let theme = &self.settings.borrow().theme.find;
let is_active = self.is_mode_enabled(mode);
MouseEventHandler::new::<Self, _, _>(mode as usize, cx, |state, _| {
let theme = &self.settings.borrow().theme.search;
let is_active = self.is_search_option_enabled(search_option);
MouseEventHandler::new::<Self, _, _>(search_option 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,
(false, false) => &theme.option_button,
(false, true) => &theme.hovered_option_button,
(true, false) => &theme.active_option_button,
(true, true) => &theme.active_hovered_option_button,
};
Label::new(icon.to_string(), style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.on_click(move |cx| cx.dispatch_action(ToggleMode(mode)))
.on_click(move |cx| cx.dispatch_action(ToggleSearchOption(search_option)))
.with_cursor_style(CursorStyle::PointingHand)
.boxed()
}
@ -241,20 +226,20 @@ impl FindBar {
direction: Direction,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let theme = &self.settings.borrow().theme.find;
let theme = &self.settings.borrow().theme.search;
enum NavButton {}
MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, _| {
let style = if state.hovered {
&theme.hovered_mode_button
&theme.hovered_option_button
} else {
&theme.mode_button
&theme.option_button
};
Label::new(icon.to_string(), style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.on_click(move |cx| cx.dispatch_action(GoToMatch(direction)))
.on_click(move |cx| cx.dispatch_action(SelectMatch(direction)))
.with_cursor_style(CursorStyle::PointingHand)
.boxed()
}
@ -262,20 +247,20 @@ impl FindBar {
fn deploy(workspace: &mut Workspace, Deploy(focus): &Deploy, cx: &mut ViewContext<Workspace>) {
let settings = workspace.settings();
workspace.active_pane().update(cx, |pane, cx| {
pane.show_toolbar(cx, |cx| FindBar::new(settings, cx));
pane.show_toolbar(cx, |cx| SearchBar::new(settings, cx));
if let Some(find_bar) = pane
if let Some(search_bar) = pane
.active_toolbar()
.and_then(|toolbar| toolbar.downcast::<Self>())
{
find_bar.update(cx, |find_bar, _| find_bar.dismissed = false);
search_bar.update(cx, |search_bar, _| search_bar.dismissed = false);
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);
.newest_selection_with_snapshot::<usize>(&display_map.buffer_snapshot);
let mut text: String;
if selection.start == selection.end {
@ -295,22 +280,24 @@ impl FindBar {
}
if !text.is_empty() {
find_bar.update(cx, |find_bar, cx| find_bar.set_query(&text, cx));
search_bar.update(cx, |search_bar, cx| search_bar.set_query(&text, cx));
}
if *focus {
let query_editor = find_bar.read(cx).query_editor.clone();
let query_editor = search_bar.read(cx).query_editor.clone();
query_editor.update(cx, |query_editor, cx| {
query_editor.select_all(&editor::SelectAll, cx);
});
cx.focus(&find_bar);
cx.focus(&search_bar);
}
} else {
cx.propagate_action();
}
});
}
fn dismiss(pane: &mut Pane, _: &Dismiss, cx: &mut ViewContext<Pane>) {
if pane.toolbar::<FindBar>().is_some() {
if pane.toolbar::<SearchBar>().is_some() {
pane.dismiss_toolbar(cx);
}
}
@ -321,71 +308,55 @@ impl FindBar {
}
}
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 is_search_option_enabled(&self, search_option: SearchOption) -> bool {
match search_option {
SearchOption::WholeWord => self.whole_word,
SearchOption::CaseSensitive => self.case_sensitive,
SearchOption::Regex => self.regex,
}
}
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,
fn toggle_search_option(
&mut self,
ToggleSearchOption(search_option): &ToggleSearchOption,
cx: &mut ViewContext<Self>,
) {
let value = match search_option {
SearchOption::WholeWord => &mut self.whole_word,
SearchOption::CaseSensitive => &mut self.case_sensitive,
SearchOption::Regex => &mut self.regex,
};
*value = !*value;
self.update_matches(true, 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 {
fn select_match(&mut self, &SelectMatch(direction): &SelectMatch, cx: &mut ViewContext<Self>) {
if let Some(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().clone();
if let Some(ranges) = self.editors_with_matches.get(&cx.weak_handle()) {
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);
let new_index = match_index_for_direction(
ranges,
&editor.newest_anchor_selection().head(),
index,
direction,
&editor.buffer().read(cx).read(cx),
);
editor.select_ranges(
[ranges[new_index].clone()],
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 select_match_on_pane(pane: &mut Pane, action: &SelectMatch, cx: &mut ViewContext<Pane>) {
if let Some(search_bar) = pane.toolbar::<SearchBar>() {
search_bar.update(cx, |search_bar, cx| search_bar.select_match(action, cx));
}
}
@ -442,56 +413,81 @@ impl FindBar {
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))
let query = if self.regex {
match SearchQuery::regex(query, self.whole_word, self.case_sensitive) {
Ok(query) => query,
Err(_) => {
self.query_contains_error = true;
cx.notify();
return;
}
}
} else {
cx.background().spawn(async move {
Ok(search(buffer, query, case_sensitive, whole_word).await)
})
SearchQuery::text(query, self.whole_word, self.case_sensitive)
};
let ranges = cx.background().spawn(async move {
let mut ranges = Vec::new();
if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
ranges.extend(
query
.search(excerpt_buffer.as_rope())
.await
.into_iter()
.map(|range| {
buffer.anchor_after(range.start)
..buffer.anchor_before(range.end)
}),
);
} else {
for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
let excerpt_range = excerpt.range.to_offset(&excerpt.buffer);
let rope = excerpt.buffer.as_rope().slice(excerpt_range.clone());
ranges.extend(query.search(&rope).await.into_iter().map(|range| {
let start = excerpt
.buffer
.anchor_after(excerpt_range.start + range.start);
let end = excerpt
.buffer
.anchor_before(excerpt_range.start + range.end);
buffer.anchor_in_excerpt(excerpt.id.clone(), start)
..buffer.anchor_in_excerpt(excerpt.id.clone(), end)
}));
}
}
ranges
});
let editor = editor.downgrade();
self.pending_search = Some(cx.spawn(|this, mut cx| async move {
match ranges.await {
Ok(ranges) => {
if let Some(editor) = editor.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
this.editors_with_matches
.insert(editor.downgrade(), ranges.clone());
this.update_match_index(cx);
if !this.dismissed {
editor.update(cx, |editor, cx| {
let theme = &this.settings.borrow().theme.find;
self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
let ranges = ranges.await;
if let Some((this, editor)) = this.upgrade(&cx).zip(editor.upgrade(&cx)) {
this.update(&mut cx, |this, cx| {
this.editors_with_matches
.insert(editor.downgrade(), ranges.clone());
this.update_match_index(cx);
if !this.dismissed {
editor.update(cx, |editor, cx| {
let theme = &this.settings.borrow().theme.search;
if select_closest_match {
if let Some(match_ix) = this.active_match_index {
editor.select_ranges(
[ranges[match_ix].clone()],
Some(Autoscroll::Fit),
cx,
);
}
}
editor.highlight_ranges::<Self>(
ranges,
theme.match_background,
if select_closest_match {
if let Some(match_ix) = this.active_match_index {
editor.select_ranges(
[ranges[match_ix].clone()],
Some(Autoscroll::Fit),
cx,
);
});
}
}
editor.highlight_ranges::<Self>(
ranges,
theme.match_background,
cx,
);
});
}
}
Err(_) => {
this.update(&mut cx, |this, cx| {
this.query_contains_error = true;
cx.notify();
});
}
});
}
}));
}
@ -499,138 +495,22 @@ impl FindBar {
}
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 ranges = self.editors_with_matches.get(&editor.downgrade())?;
let editor = editor.read(cx);
let position = editor.newest_anchor_selection().head();
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)),
}
let new_index = self.active_editor.as_ref().and_then(|editor| {
let ranges = self.editors_with_matches.get(&editor.downgrade())?;
let editor = editor.read(cx);
active_match_index(
&ranges,
&editor.newest_anchor_selection().head(),
&editor.buffer().read(cx).read(cx),
)
});
if new_index != self.active_match_index {
self.active_match_index = new_index;
cx.notify();
}
}
}
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(), false)
.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::*;
@ -640,10 +520,10 @@ mod tests {
use unindent::Unindent as _;
#[gpui::test]
async fn test_find_simple(mut cx: TestAppContext) {
async fn test_search_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();
theme.search.match_background = Color::red();
let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap();
let settings = watch::channel_with(settings).1;
@ -663,16 +543,16 @@ mod tests {
Editor::for_buffer(buffer.clone(), None, settings.clone(), cx)
});
let find_bar = cx.add_view(Default::default(), |cx| {
let mut find_bar = FindBar::new(settings, cx);
find_bar.active_item_changed(Some(Box::new(editor.clone())), cx);
find_bar
let search_bar = cx.add_view(Default::default(), |cx| {
let mut search_bar = SearchBar::new(settings, cx);
search_bar.active_item_changed(Some(Box::new(editor.clone())), cx);
search_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);
search_bar.update(&mut cx, |search_bar, cx| {
search_bar.set_query("us", cx);
});
editor.next_notification(&cx).await;
editor.update(&mut cx, |editor, cx| {
@ -692,8 +572,8 @@ mod tests {
});
// Switch to a case sensitive search.
find_bar.update(&mut cx, |find_bar, cx| {
find_bar.toggle_mode(&ToggleMode(SearchMode::CaseSensitive), cx);
search_bar.update(&mut cx, |search_bar, cx| {
search_bar.toggle_search_option(&ToggleSearchOption(SearchOption::CaseSensitive), cx);
});
editor.next_notification(&cx).await;
editor.update(&mut cx, |editor, cx| {
@ -708,8 +588,8 @@ mod tests {
// 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);
search_bar.update(&mut cx, |search_bar, cx| {
search_bar.set_query("or", cx);
});
editor.next_notification(&cx).await;
editor.update(&mut cx, |editor, cx| {
@ -749,8 +629,8 @@ mod tests {
});
// Switch to a whole word search.
find_bar.update(&mut cx, |find_bar, cx| {
find_bar.toggle_mode(&ToggleMode(SearchMode::WholeWord), cx);
search_bar.update(&mut cx, |search_bar, cx| {
search_bar.toggle_search_option(&ToggleSearchOption(SearchOption::WholeWord), cx);
});
editor.next_notification(&cx).await;
editor.update(&mut cx, |editor, cx| {
@ -776,82 +656,82 @@ mod tests {
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);
search_bar.update(&mut cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(0));
search_bar.select_match(&SelectMatch(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));
search_bar.read_with(&cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(0));
});
find_bar.update(&mut cx, |find_bar, cx| {
find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
search_bar.update(&mut cx, |search_bar, cx| {
search_bar.select_match(&SelectMatch(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));
search_bar.read_with(&cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(1));
});
find_bar.update(&mut cx, |find_bar, cx| {
find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
search_bar.update(&mut cx, |search_bar, cx| {
search_bar.select_match(&SelectMatch(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));
search_bar.read_with(&cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(2));
});
find_bar.update(&mut cx, |find_bar, cx| {
find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
search_bar.update(&mut cx, |search_bar, cx| {
search_bar.select_match(&SelectMatch(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));
search_bar.read_with(&cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(0));
});
find_bar.update(&mut cx, |find_bar, cx| {
find_bar.go_to_match(&GoToMatch(Direction::Prev), cx);
search_bar.update(&mut cx, |search_bar, cx| {
search_bar.select_match(&SelectMatch(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));
search_bar.read_with(&cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(2));
});
find_bar.update(&mut cx, |find_bar, cx| {
find_bar.go_to_match(&GoToMatch(Direction::Prev), cx);
search_bar.update(&mut cx, |search_bar, cx| {
search_bar.select_match(&SelectMatch(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));
search_bar.read_with(&cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(1));
});
find_bar.update(&mut cx, |find_bar, cx| {
find_bar.go_to_match(&GoToMatch(Direction::Prev), cx);
search_bar.update(&mut cx, |search_bar, cx| {
search_bar.select_match(&SelectMatch(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));
search_bar.read_with(&cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(0));
});
// Park the cursor in between matches and ensure that going to the previous match selects
@ -859,16 +739,16 @@ mod tests {
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);
search_bar.update(&mut cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(1));
search_bar.select_match(&SelectMatch(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));
search_bar.read_with(&cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(0));
});
// Park the cursor in between matches and ensure that going to the next match selects the
@ -876,16 +756,16 @@ mod tests {
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);
search_bar.update(&mut cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(1));
search_bar.select_match(&SelectMatch(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));
search_bar.read_with(&cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(1));
});
// Park the cursor after the last match and ensure that going to the previous match selects
@ -893,16 +773,16 @@ mod tests {
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);
search_bar.update(&mut cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(2));
search_bar.select_match(&SelectMatch(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));
search_bar.read_with(&cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(2));
});
// Park the cursor after the last match and ensure that going to the next match selects the
@ -910,16 +790,16 @@ mod tests {
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);
search_bar.update(&mut cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(2));
search_bar.select_match(&SelectMatch(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));
search_bar.read_with(&cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(0));
});
// Park the cursor before the first match and ensure that going to the previous match
@ -927,16 +807,16 @@ mod tests {
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);
search_bar.update(&mut cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(0));
search_bar.select_match(&SelectMatch(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));
search_bar.read_with(&cx, |search_bar, _| {
assert_eq!(search_bar.active_match_index, Some(2));
});
}
}

View file

@ -0,0 +1,848 @@
use crate::{
active_match_index, match_index_for_direction, Direction, SearchOption, SelectMatch,
ToggleSearchOption,
};
use collections::HashMap;
use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll};
use gpui::{
action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity,
ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext,
ViewHandle, WeakModelHandle,
};
use postage::watch;
use project::{search::SearchQuery, Project};
use std::{
any::{Any, TypeId},
ops::Range,
path::PathBuf,
};
use util::ResultExt as _;
use workspace::{Item, ItemHandle, ItemNavHistory, ItemView, Settings, Workspace};
action!(Deploy);
action!(Search);
action!(SearchInNew);
action!(ToggleFocus);
const MAX_TAB_TITLE_LEN: usize = 24;
#[derive(Default)]
struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakModelHandle<ProjectSearch>>);
pub fn init(cx: &mut MutableAppContext) {
cx.add_app_state(ActiveSearches::default());
cx.add_bindings([
Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectSearchView")),
Binding::new("cmd-f", ToggleFocus, Some("ProjectSearchView")),
Binding::new("cmd-shift-F", Deploy, Some("Workspace")),
Binding::new("enter", Search, Some("ProjectSearchView")),
Binding::new("cmd-enter", SearchInNew, Some("ProjectSearchView")),
Binding::new(
"cmd-g",
SelectMatch(Direction::Next),
Some("ProjectSearchView"),
),
Binding::new(
"cmd-shift-G",
SelectMatch(Direction::Prev),
Some("ProjectSearchView"),
),
]);
cx.add_action(ProjectSearchView::deploy);
cx.add_action(ProjectSearchView::search);
cx.add_action(ProjectSearchView::search_in_new);
cx.add_action(ProjectSearchView::toggle_search_option);
cx.add_action(ProjectSearchView::select_match);
cx.add_action(ProjectSearchView::toggle_focus);
cx.capture_action(ProjectSearchView::tab);
}
struct ProjectSearch {
project: ModelHandle<Project>,
excerpts: ModelHandle<MultiBuffer>,
pending_search: Option<Task<Option<()>>>,
match_ranges: Vec<Range<Anchor>>,
active_query: Option<SearchQuery>,
}
struct ProjectSearchView {
model: ModelHandle<ProjectSearch>,
query_editor: ViewHandle<Editor>,
results_editor: ViewHandle<Editor>,
case_sensitive: bool,
whole_word: bool,
regex: bool,
query_contains_error: bool,
active_match_index: Option<usize>,
settings: watch::Receiver<Settings>,
}
impl Entity for ProjectSearch {
type Event = ();
}
impl ProjectSearch {
fn new(project: ModelHandle<Project>, cx: &mut ModelContext<Self>) -> Self {
let replica_id = project.read(cx).replica_id();
Self {
project,
excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)),
pending_search: Default::default(),
match_ranges: Default::default(),
active_query: None,
}
}
fn clone(&self, cx: &mut ModelContext<Self>) -> ModelHandle<Self> {
cx.add_model(|cx| Self {
project: self.project.clone(),
excerpts: self
.excerpts
.update(cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))),
pending_search: Default::default(),
match_ranges: self.match_ranges.clone(),
active_query: self.active_query.clone(),
})
}
fn search(&mut self, query: SearchQuery, cx: &mut ModelContext<Self>) {
let search = self
.project
.update(cx, |project, cx| project.search(query.clone(), cx));
self.active_query = Some(query);
self.match_ranges.clear();
self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
let matches = search.await.log_err()?;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
this.match_ranges.clear();
let mut matches = matches.into_iter().collect::<Vec<_>>();
matches
.sort_by_key(|(buffer, _)| buffer.read(cx).file().map(|file| file.path()));
this.excerpts.update(cx, |excerpts, cx| {
excerpts.clear(cx);
for (buffer, buffer_matches) in matches {
let ranges_to_highlight = excerpts.push_excerpts_with_context_lines(
buffer,
buffer_matches.clone(),
1,
cx,
);
this.match_ranges.extend(ranges_to_highlight);
}
});
this.pending_search.take();
cx.notify();
});
}
None
}));
cx.notify();
}
}
impl Item for ProjectSearch {
type View = ProjectSearchView;
fn build_view(
model: ModelHandle<Self>,
workspace: &Workspace,
nav_history: ItemNavHistory,
cx: &mut gpui::ViewContext<Self::View>,
) -> Self::View {
ProjectSearchView::new(model, Some(nav_history), workspace.settings(), cx)
}
fn project_path(&self) -> Option<project::ProjectPath> {
None
}
}
enum ViewEvent {
UpdateTab,
}
impl Entity for ProjectSearchView {
type Event = ViewEvent;
}
impl View for ProjectSearchView {
fn ui_name() -> &'static str {
"ProjectSearchView"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let model = &self.model.read(cx);
let results = if model.match_ranges.is_empty() {
let theme = &self.settings.borrow().theme;
let text = if self.query_editor.read(cx).text(cx).is_empty() {
""
} else if model.pending_search.is_some() {
"Searching..."
} else {
"No results"
};
Label::new(text.to_string(), theme.search.results_status.clone())
.aligned()
.contained()
.with_background_color(theme.editor.background)
.flexible(1., true)
.boxed()
} else {
ChildView::new(&self.results_editor)
.flexible(1., true)
.boxed()
};
Flex::column()
.with_child(self.render_query_editor(cx))
.with_child(results)
.boxed()
}
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
cx.update_app_state(|state: &mut ActiveSearches, cx| {
state.0.insert(
self.model.read(cx).project.downgrade(),
self.model.downgrade(),
)
});
if self.model.read(cx).match_ranges.is_empty() {
cx.focus(&self.query_editor);
} else {
self.focus_results_editor(cx);
}
}
}
impl ItemView for ProjectSearchView {
fn act_as_type(
&self,
type_id: TypeId,
self_handle: &ViewHandle<Self>,
_: &gpui::AppContext,
) -> Option<gpui::AnyViewHandle> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.into())
} else if type_id == TypeId::of::<Editor>() {
Some((&self.results_editor).into())
} else {
None
}
}
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
self.results_editor
.update(cx, |editor, cx| editor.deactivated(cx));
}
fn item(&self, _: &gpui::AppContext) -> Box<dyn ItemHandle> {
Box::new(self.model.clone())
}
fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox {
let settings = self.settings.borrow();
let search_theme = &settings.theme.search;
Flex::row()
.with_child(
Svg::new("icons/magnifier.svg")
.with_color(tab_theme.label.text.color)
.constrained()
.with_width(search_theme.tab_icon_width)
.aligned()
.boxed(),
)
.with_children(self.model.read(cx).active_query.as_ref().map(|query| {
let query_text = if query.as_str().len() > MAX_TAB_TITLE_LEN {
query.as_str()[..MAX_TAB_TITLE_LEN].to_string() + ""
} else {
query.as_str().to_string()
};
Label::new(query_text, tab_theme.label.clone())
.aligned()
.contained()
.with_margin_left(search_theme.tab_icon_spacing)
.boxed()
}))
.boxed()
}
fn project_path(&self, _: &gpui::AppContext) -> Option<project::ProjectPath> {
None
}
fn can_save(&self, _: &gpui::AppContext) -> bool {
true
}
fn is_dirty(&self, cx: &AppContext) -> bool {
self.results_editor.read(cx).is_dirty(cx)
}
fn has_conflict(&self, cx: &AppContext) -> bool {
self.results_editor.read(cx).has_conflict(cx)
}
fn save(
&mut self,
project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
self.results_editor
.update(cx, |editor, cx| editor.save(project, cx))
}
fn can_save_as(&self, _: &gpui::AppContext) -> bool {
false
}
fn save_as(
&mut self,
_: ModelHandle<Project>,
_: PathBuf,
_: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
unreachable!("save_as should not have been called")
}
fn clone_on_split(
&self,
nav_history: ItemNavHistory,
cx: &mut ViewContext<Self>,
) -> Option<Self>
where
Self: Sized,
{
let model = self.model.update(cx, |model, cx| model.clone(cx));
Some(Self::new(
model,
Some(nav_history),
self.settings.clone(),
cx,
))
}
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
self.results_editor
.update(cx, |editor, cx| editor.navigate(data, cx));
}
fn should_update_tab_on_event(event: &ViewEvent) -> bool {
matches!(event, ViewEvent::UpdateTab)
}
}
impl ProjectSearchView {
fn new(
model: ModelHandle<ProjectSearch>,
nav_history: Option<ItemNavHistory>,
settings: watch::Receiver<Settings>,
cx: &mut ViewContext<Self>,
) -> Self {
let project;
let excerpts;
let mut query_text = String::new();
let mut regex = false;
let mut case_sensitive = false;
let mut whole_word = false;
{
let model = model.read(cx);
project = model.project.clone();
excerpts = model.excerpts.clone();
if let Some(active_query) = model.active_query.as_ref() {
query_text = active_query.as_str().to_string();
regex = active_query.is_regex();
case_sensitive = active_query.case_sensitive();
whole_word = active_query.whole_word();
}
}
cx.observe(&model, |this, _, cx| this.model_changed(true, cx))
.detach();
let query_editor = cx.add_view(|cx| {
let mut editor = Editor::single_line(
settings.clone(),
Some(|theme| theme.search.editor.input.clone()),
cx,
);
editor.set_text(query_text, cx);
editor
});
let results_editor = cx.add_view(|cx| {
let mut editor = Editor::for_buffer(excerpts, Some(project), settings.clone(), cx);
editor.set_searchable(false);
editor.set_nav_history(nav_history);
editor
});
cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
.detach();
cx.subscribe(&results_editor, |this, _, event, cx| {
if matches!(event, editor::Event::SelectionsChanged) {
this.update_match_index(cx);
}
})
.detach();
let mut this = ProjectSearchView {
model,
query_editor,
results_editor,
case_sensitive,
whole_word,
regex,
query_contains_error: false,
active_match_index: None,
settings,
};
this.model_changed(false, cx);
this
}
// Re-activate the most recently activated search or the most recent if it has been closed.
// If no search exists in the workspace, create a new one.
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
// Clean up entries for dropped projects
cx.update_app_state(|state: &mut ActiveSearches, cx| {
state.0.retain(|project, _| project.is_upgradable(cx))
});
let active_search = cx
.app_state::<ActiveSearches>()
.0
.get(&workspace.project().downgrade());
let existing = active_search
.and_then(|active_search| {
workspace
.items_of_type::<ProjectSearch>(cx)
.find(|search| search == active_search)
})
.or_else(|| workspace.item_of_type::<ProjectSearch>(cx));
if let Some(existing) = existing {
workspace.activate_item(&existing, cx);
} else {
let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
workspace.open_item(model, cx);
}
}
fn search(&mut self, _: &Search, cx: &mut ViewContext<Self>) {
if let Some(query) = self.build_search_query(cx) {
self.model.update(cx, |model, cx| model.search(query, cx));
}
}
fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext<Workspace>) {
if let Some(search_view) = workspace
.active_item(cx)
.and_then(|item| item.downcast::<ProjectSearchView>())
{
let new_query = search_view.update(cx, |search_view, cx| {
let new_query = search_view.build_search_query(cx);
if new_query.is_some() {
if let Some(old_query) = search_view.model.read(cx).active_query.clone() {
search_view.query_editor.update(cx, |editor, cx| {
editor.set_text(old_query.as_str(), cx);
});
search_view.regex = old_query.is_regex();
search_view.whole_word = old_query.whole_word();
search_view.case_sensitive = old_query.case_sensitive();
}
}
new_query
});
if let Some(new_query) = new_query {
let model = cx.add_model(|cx| {
let mut model = ProjectSearch::new(workspace.project().clone(), cx);
model.search(new_query, cx);
model
});
workspace.open_item(model, cx);
}
}
}
fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> {
let text = self.query_editor.read(cx).text(cx);
if self.regex {
match SearchQuery::regex(text, self.whole_word, self.case_sensitive) {
Ok(query) => Some(query),
Err(_) => {
self.query_contains_error = true;
cx.notify();
None
}
}
} else {
Some(SearchQuery::text(
text,
self.whole_word,
self.case_sensitive,
))
}
}
fn toggle_search_option(
&mut self,
ToggleSearchOption(option): &ToggleSearchOption,
cx: &mut ViewContext<Self>,
) {
let value = match option {
SearchOption::WholeWord => &mut self.whole_word,
SearchOption::CaseSensitive => &mut self.case_sensitive,
SearchOption::Regex => &mut self.regex,
};
*value = !*value;
self.search(&Search, cx);
cx.notify();
}
fn select_match(&mut self, &SelectMatch(direction): &SelectMatch, cx: &mut ViewContext<Self>) {
if let Some(index) = self.active_match_index {
let model = self.model.read(cx);
let results_editor = self.results_editor.read(cx);
let new_index = match_index_for_direction(
&model.match_ranges,
&results_editor.newest_anchor_selection().head(),
index,
direction,
&results_editor.buffer().read(cx).read(cx),
);
let range_to_select = model.match_ranges[new_index].clone();
self.results_editor.update(cx, |editor, cx| {
editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx);
});
}
}
fn toggle_focus(&mut self, _: &ToggleFocus, cx: &mut ViewContext<Self>) {
if self.query_editor.is_focused(cx) {
if !self.model.read(cx).match_ranges.is_empty() {
self.focus_results_editor(cx);
}
} else {
self.focus_query_editor(cx);
}
}
fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
if self.query_editor.is_focused(cx) {
if !self.model.read(cx).match_ranges.is_empty() {
self.focus_results_editor(cx);
}
} else {
cx.propagate_action()
}
}
fn focus_query_editor(&self, cx: &mut ViewContext<Self>) {
self.query_editor.update(cx, |query_editor, cx| {
query_editor.select_all(&SelectAll, cx);
});
cx.focus(&self.query_editor);
}
fn focus_results_editor(&self, cx: &mut ViewContext<Self>) {
self.query_editor.update(cx, |query_editor, cx| {
let cursor = query_editor.newest_anchor_selection().head();
query_editor.select_ranges([cursor.clone()..cursor], None, cx);
});
cx.focus(&self.results_editor);
}
fn model_changed(&mut self, reset_selections: bool, cx: &mut ViewContext<Self>) {
let match_ranges = self.model.read(cx).match_ranges.clone();
if match_ranges.is_empty() {
self.active_match_index = None;
} else {
let theme = &self.settings.borrow().theme.search;
self.results_editor.update(cx, |editor, cx| {
if reset_selections {
editor.select_ranges(match_ranges.first().cloned(), Some(Autoscroll::Fit), cx);
}
editor.highlight_ranges::<Self>(match_ranges, theme.match_background, cx);
});
if self.query_editor.is_focused(cx) {
self.focus_results_editor(cx);
}
}
cx.emit(ViewEvent::UpdateTab);
cx.notify();
}
fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
let results_editor = self.results_editor.read(cx);
let new_index = active_match_index(
&self.model.read(cx).match_ranges,
&results_editor.newest_anchor_selection().head(),
&results_editor.buffer().read(cx).read(cx),
);
if self.active_match_index != new_index {
self.active_match_index = new_index;
cx.notify();
}
}
fn render_query_editor(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &self.settings.borrow().theme;
let editor_container = if self.query_contains_error {
theme.search.invalid_editor
} else {
theme.search.editor.input.container
};
Flex::row()
.with_child(
ChildView::new(&self.query_editor)
.contained()
.with_style(editor_container)
.aligned()
.constrained()
.with_max_width(theme.search.editor.max_width)
.boxed(),
)
.with_child(
Flex::row()
.with_child(self.render_option_button("Case", SearchOption::CaseSensitive, cx))
.with_child(self.render_option_button("Word", SearchOption::WholeWord, cx))
.with_child(self.render_option_button("Regex", SearchOption::Regex, cx))
.contained()
.with_style(theme.search.option_button_group)
.aligned()
.boxed(),
)
.with_children({
self.active_match_index.into_iter().flat_map(|match_ix| {
[
Flex::row()
.with_child(self.render_nav_button("<", Direction::Prev, cx))
.with_child(self.render_nav_button(">", Direction::Next, cx))
.aligned()
.boxed(),
Label::new(
format!(
"{}/{}",
match_ix + 1,
self.model.read(cx).match_ranges.len()
),
theme.search.match_index.text.clone(),
)
.contained()
.with_style(theme.search.match_index.container)
.aligned()
.boxed(),
]
})
})
.contained()
.with_style(theme.search.container)
.constrained()
.with_height(theme.workspace.toolbar.height)
.named("project search")
}
fn render_option_button(
&self,
icon: &str,
option: SearchOption,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let theme = &self.settings.borrow().theme.search;
let is_active = self.is_option_enabled(option);
MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, _| {
let style = match (is_active, state.hovered) {
(false, false) => &theme.option_button,
(false, true) => &theme.hovered_option_button,
(true, false) => &theme.active_option_button,
(true, true) => &theme.active_hovered_option_button,
};
Label::new(icon.to_string(), style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.on_click(move |cx| cx.dispatch_action(ToggleSearchOption(option)))
.with_cursor_style(CursorStyle::PointingHand)
.boxed()
}
fn is_option_enabled(&self, option: SearchOption) -> bool {
match option {
SearchOption::WholeWord => self.whole_word,
SearchOption::CaseSensitive => self.case_sensitive,
SearchOption::Regex => self.regex,
}
}
fn render_nav_button(
&self,
icon: &str,
direction: Direction,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let theme = &self.settings.borrow().theme.search;
enum NavButton {}
MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, _| {
let style = if state.hovered {
&theme.hovered_option_button
} else {
&theme.option_button
};
Label::new(icon.to_string(), style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.on_click(move |cx| cx.dispatch_action(SelectMatch(direction)))
.with_cursor_style(CursorStyle::PointingHand)
.boxed()
}
}
#[cfg(test)]
mod tests {
use super::*;
use editor::DisplayPoint;
use gpui::{color::Color, TestAppContext};
use project::FakeFs;
use serde_json::json;
use std::sync::Arc;
#[gpui::test]
async fn test_project_search(mut cx: TestAppContext) {
let fonts = cx.font_cache();
let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
theme.search.match_background = Color::red();
let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap();
let settings = watch::channel_with(settings).1;
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/dir",
json!({
"one.rs": "const ONE: usize = 1;",
"two.rs": "const TWO: usize = one::ONE + one::ONE;",
"three.rs": "const THREE: usize = one::ONE + two::TWO;",
"four.rs": "const FOUR: usize = one::ONE + three::THREE;",
}),
)
.await;
let project = Project::test(fs.clone(), &mut cx);
let (tree, _) = project
.update(&mut cx, |project, cx| {
project.find_or_create_local_worktree("/dir", false, cx)
})
.await
.unwrap();
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
let search = cx.add_model(|cx| ProjectSearch::new(project, cx));
let search_view = cx.add_view(Default::default(), |cx| {
ProjectSearchView::new(search.clone(), None, settings, cx)
});
search_view.update(&mut cx, |search_view, cx| {
search_view
.query_editor
.update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
search_view.search(&Search, cx);
});
search_view.next_notification(&cx).await;
search_view.update(&mut cx, |search_view, cx| {
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.display_text(cx)),
"\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;"
);
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.all_highlighted_ranges(cx)),
&[
(
DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35),
Color::red()
),
(
DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40),
Color::red()
),
(
DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9),
Color::red()
)
]
);
assert_eq!(search_view.active_match_index, Some(0));
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
);
search_view.select_match(&SelectMatch(Direction::Next), cx);
});
search_view.update(&mut cx, |search_view, cx| {
assert_eq!(search_view.active_match_index, Some(1));
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
);
search_view.select_match(&SelectMatch(Direction::Next), cx);
});
search_view.update(&mut cx, |search_view, cx| {
assert_eq!(search_view.active_match_index, Some(2));
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
);
search_view.select_match(&SelectMatch(Direction::Next), cx);
});
search_view.update(&mut cx, |search_view, cx| {
assert_eq!(search_view.active_match_index, Some(0));
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
);
search_view.select_match(&SelectMatch(Direction::Prev), cx);
});
search_view.update(&mut cx, |search_view, cx| {
assert_eq!(search_view.active_match_index, Some(2));
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
);
search_view.select_match(&SelectMatch(Direction::Prev), cx);
});
search_view.update(&mut cx, |search_view, cx| {
assert_eq!(search_view.active_match_index, Some(1));
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
);
});
}
}

View file

@ -0,0 +1,88 @@
use std::{
cmp::{self, Ordering},
ops::Range,
};
use editor::{Anchor, MultiBufferSnapshot};
use gpui::{action, MutableAppContext};
mod buffer_search;
mod project_search;
pub fn init(cx: &mut MutableAppContext) {
buffer_search::init(cx);
project_search::init(cx);
}
action!(ToggleSearchOption, SearchOption);
action!(SelectMatch, Direction);
#[derive(Clone, Copy)]
pub enum SearchOption {
WholeWord,
CaseSensitive,
Regex,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Prev,
Next,
}
pub(crate) fn active_match_index(
ranges: &[Range<Anchor>],
cursor: &Anchor,
buffer: &MultiBufferSnapshot,
) -> Option<usize> {
if ranges.is_empty() {
None
} else {
match ranges.binary_search_by(|probe| {
if probe.end.cmp(&cursor, &*buffer).unwrap().is_lt() {
Ordering::Less
} else if probe.start.cmp(&cursor, &*buffer).unwrap().is_gt() {
Ordering::Greater
} else {
Ordering::Equal
}
}) {
Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
}
}
}
pub(crate) fn match_index_for_direction(
ranges: &[Range<Anchor>],
cursor: &Anchor,
mut index: usize,
direction: Direction,
buffer: &MultiBufferSnapshot,
) -> usize {
if ranges[index].start.cmp(&cursor, &buffer).unwrap().is_gt() {
if direction == Direction::Prev {
if index == 0 {
index = ranges.len() - 1;
} else {
index -= 1;
}
}
} else if ranges[index].end.cmp(&cursor, &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;
}
};
index
}

View file

@ -12,7 +12,7 @@ use collections::{HashMap, HashSet};
use futures::{channel::mpsc, future::BoxFuture, FutureExt, SinkExt, StreamExt};
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use rpc::{
proto::{self, AnyTypedEnvelope, EnvelopedMessage, RequestMessage},
proto::{self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage},
Connection, ConnectionId, Peer, TypedEnvelope,
};
use sha1::{Digest as _, Sha1};
@ -77,25 +77,28 @@ impl Server {
.add_message_handler(Server::update_diagnostic_summary)
.add_message_handler(Server::disk_based_diagnostics_updating)
.add_message_handler(Server::disk_based_diagnostics_updated)
.add_request_handler(Server::get_definition)
.add_request_handler(Server::get_references)
.add_request_handler(Server::get_document_highlights)
.add_request_handler(Server::get_project_symbols)
.add_request_handler(Server::open_buffer_for_symbol)
.add_request_handler(Server::open_buffer)
.add_request_handler(Server::forward_project_request::<proto::GetDefinition>)
.add_request_handler(Server::forward_project_request::<proto::GetReferences>)
.add_request_handler(Server::forward_project_request::<proto::SearchProject>)
.add_request_handler(Server::forward_project_request::<proto::GetDocumentHighlights>)
.add_request_handler(Server::forward_project_request::<proto::GetProjectSymbols>)
.add_request_handler(Server::forward_project_request::<proto::OpenBufferForSymbol>)
.add_request_handler(Server::forward_project_request::<proto::OpenBuffer>)
.add_request_handler(Server::forward_project_request::<proto::GetCompletions>)
.add_request_handler(
Server::forward_project_request::<proto::ApplyCompletionAdditionalEdits>,
)
.add_request_handler(Server::forward_project_request::<proto::GetCodeActions>)
.add_request_handler(Server::forward_project_request::<proto::ApplyCodeAction>)
.add_request_handler(Server::forward_project_request::<proto::PrepareRename>)
.add_request_handler(Server::forward_project_request::<proto::PerformRename>)
.add_request_handler(Server::forward_project_request::<proto::FormatBuffers>)
.add_message_handler(Server::close_buffer)
.add_request_handler(Server::update_buffer)
.add_message_handler(Server::update_buffer_file)
.add_message_handler(Server::buffer_reloaded)
.add_message_handler(Server::buffer_saved)
.add_request_handler(Server::save_buffer)
.add_request_handler(Server::format_buffers)
.add_request_handler(Server::get_completions)
.add_request_handler(Server::apply_additional_edits_for_completion)
.add_request_handler(Server::get_code_actions)
.add_request_handler(Server::apply_code_action)
.add_request_handler(Server::prepare_rename)
.add_request_handler(Server::perform_rename)
.add_request_handler(Server::get_channels)
.add_request_handler(Server::get_users)
.add_request_handler(Server::join_channel)
@ -542,83 +545,16 @@ impl Server {
Ok(())
}
async fn get_definition(
async fn forward_project_request<T>(
self: Arc<Server>,
request: TypedEnvelope<proto::GetDefinition>,
) -> tide::Result<proto::GetDefinitionResponse> {
request: TypedEnvelope<T>,
) -> tide::Result<T::Response>
where
T: EntityMessage + RequestMessage,
{
let host_connection_id = self
.state()
.read_project(request.payload.project_id, request.sender_id)?
.host_connection_id;
Ok(self
.peer
.forward_request(request.sender_id, host_connection_id, request.payload)
.await?)
}
async fn get_references(
self: Arc<Server>,
request: TypedEnvelope<proto::GetReferences>,
) -> tide::Result<proto::GetReferencesResponse> {
let host_connection_id = self
.state()
.read_project(request.payload.project_id, request.sender_id)?
.host_connection_id;
Ok(self
.peer
.forward_request(request.sender_id, host_connection_id, request.payload)
.await?)
}
async fn get_document_highlights(
self: Arc<Server>,
request: TypedEnvelope<proto::GetDocumentHighlights>,
) -> tide::Result<proto::GetDocumentHighlightsResponse> {
let host_connection_id = self
.state()
.read_project(request.payload.project_id, request.sender_id)?
.host_connection_id;
Ok(self
.peer
.forward_request(request.sender_id, host_connection_id, request.payload)
.await?)
}
async fn get_project_symbols(
self: Arc<Server>,
request: TypedEnvelope<proto::GetProjectSymbols>,
) -> tide::Result<proto::GetProjectSymbolsResponse> {
let host_connection_id = self
.state()
.read_project(request.payload.project_id, request.sender_id)?
.host_connection_id;
Ok(self
.peer
.forward_request(request.sender_id, host_connection_id, request.payload)
.await?)
}
async fn open_buffer_for_symbol(
self: Arc<Server>,
request: TypedEnvelope<proto::OpenBufferForSymbol>,
) -> tide::Result<proto::OpenBufferForSymbolResponse> {
let host_connection_id = self
.state()
.read_project(request.payload.project_id, request.sender_id)?
.host_connection_id;
Ok(self
.peer
.forward_request(request.sender_id, host_connection_id, request.payload)
.await?)
}
async fn open_buffer(
self: Arc<Server>,
request: TypedEnvelope<proto::OpenBuffer>,
) -> tide::Result<proto::OpenBufferResponse> {
let host_connection_id = self
.state()
.read_project(request.payload.project_id, request.sender_id)?
.read_project(request.payload.remote_entity_id(), request.sender_id)?
.host_connection_id;
Ok(self
.peer
@ -665,104 +601,6 @@ impl Server {
Ok(response)
}
async fn format_buffers(
self: Arc<Server>,
request: TypedEnvelope<proto::FormatBuffers>,
) -> tide::Result<proto::FormatBuffersResponse> {
let host = self
.state()
.read_project(request.payload.project_id, request.sender_id)?
.host_connection_id;
Ok(self
.peer
.forward_request(request.sender_id, host, request.payload.clone())
.await?)
}
async fn get_completions(
self: Arc<Server>,
request: TypedEnvelope<proto::GetCompletions>,
) -> tide::Result<proto::GetCompletionsResponse> {
let host = self
.state()
.read_project(request.payload.project_id, request.sender_id)?
.host_connection_id;
Ok(self
.peer
.forward_request(request.sender_id, host, request.payload.clone())
.await?)
}
async fn apply_additional_edits_for_completion(
self: Arc<Server>,
request: TypedEnvelope<proto::ApplyCompletionAdditionalEdits>,
) -> tide::Result<proto::ApplyCompletionAdditionalEditsResponse> {
let host = self
.state()
.read_project(request.payload.project_id, request.sender_id)?
.host_connection_id;
Ok(self
.peer
.forward_request(request.sender_id, host, request.payload.clone())
.await?)
}
async fn get_code_actions(
self: Arc<Server>,
request: TypedEnvelope<proto::GetCodeActions>,
) -> tide::Result<proto::GetCodeActionsResponse> {
let host = self
.state()
.read_project(request.payload.project_id, request.sender_id)?
.host_connection_id;
Ok(self
.peer
.forward_request(request.sender_id, host, request.payload.clone())
.await?)
}
async fn apply_code_action(
self: Arc<Server>,
request: TypedEnvelope<proto::ApplyCodeAction>,
) -> tide::Result<proto::ApplyCodeActionResponse> {
let host = self
.state()
.read_project(request.payload.project_id, request.sender_id)?
.host_connection_id;
Ok(self
.peer
.forward_request(request.sender_id, host, request.payload.clone())
.await?)
}
async fn prepare_rename(
self: Arc<Server>,
request: TypedEnvelope<proto::PrepareRename>,
) -> tide::Result<proto::PrepareRenameResponse> {
let host = self
.state()
.read_project(request.payload.project_id, request.sender_id)?
.host_connection_id;
Ok(self
.peer
.forward_request(request.sender_id, host, request.payload.clone())
.await?)
}
async fn perform_rename(
self: Arc<Server>,
request: TypedEnvelope<proto::PerformRename>,
) -> tide::Result<proto::PerformRenameResponse> {
let host = self
.state()
.read_project(request.payload.project_id, request.sender_id)?
.host_connection_id;
Ok(self
.peer
.forward_request(request.sender_id, host, request.payload.clone())
.await?)
}
async fn update_buffer(
self: Arc<Server>,
request: TypedEnvelope<proto::UpdateBuffer>,
@ -1186,7 +1024,7 @@ mod tests {
LanguageConfig, LanguageRegistry, LanguageServerConfig, Point, ToLspPosition,
},
lsp,
project::{DiagnosticSummary, Project, ProjectPath},
project::{search::SearchQuery, DiagnosticSummary, Project, ProjectPath},
workspace::{Settings, Workspace, WorkspaceParams},
};
@ -1327,14 +1165,6 @@ mod tests {
// .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0)
// .await;
// Close the buffer as client A, see that the buffer is closed.
cx_a.update(move |_| drop(buffer_a));
project_a
.condition(&cx_a, |project, cx| {
!project.has_open_buffer((worktree_id, "b.txt"), cx)
})
.await;
// Dropping the client B's project removes client B from client A's collaborators.
cx_b.update(move |_| drop(project_b));
project_a
@ -2697,14 +2527,6 @@ mod tests {
);
});
assert_eq!(definitions_1[0].buffer, definitions_2[0].buffer);
cx_b.update(|_| {
drop(definitions_1);
drop(definitions_2);
});
project_b
.condition(&cx_b, |proj, cx| proj.worktrees(cx).count() == 1)
.await;
}
#[gpui::test(iterations = 10)]
@ -2843,6 +2665,118 @@ mod tests {
});
}
#[gpui::test(iterations = 10)]
async fn test_project_search(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
cx_a.foreground().forbid_parking();
let lang_registry = Arc::new(LanguageRegistry::new());
let fs = FakeFs::new(cx_a.background());
fs.insert_tree(
"/root-1",
json!({
".zed.toml": r#"collaborators = ["user_b"]"#,
"a": "hello world",
"b": "goodnight moon",
"c": "a world of goo",
"d": "world champion of clown world",
}),
)
.await;
fs.insert_tree(
"/root-2",
json!({
"e": "disney world is fun",
}),
)
.await;
// Connect to a server as 2 clients.
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let client_a = server.create_client(&mut cx_a, "user_a").await;
let client_b = server.create_client(&mut cx_b, "user_b").await;
// Share a project as client A
let project_a = cx_a.update(|cx| {
Project::local(
client_a.clone(),
client_a.user_store.clone(),
lang_registry.clone(),
fs.clone(),
cx,
)
});
let project_id = project_a.update(&mut cx_a, |p, _| p.next_remote_id()).await;
let (worktree_1, _) = project_a
.update(&mut cx_a, |p, cx| {
p.find_or_create_local_worktree("/root-1", false, cx)
})
.await
.unwrap();
worktree_1
.read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
.await;
let (worktree_2, _) = project_a
.update(&mut cx_a, |p, cx| {
p.find_or_create_local_worktree("/root-2", false, cx)
})
.await
.unwrap();
worktree_2
.read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
.await;
eprintln!("sharing");
project_a
.update(&mut cx_a, |p, cx| p.share(cx))
.await
.unwrap();
// Join the worktree as client B.
let project_b = Project::remote(
project_id,
client_b.clone(),
client_b.user_store.clone(),
lang_registry.clone(),
fs.clone(),
&mut cx_b.to_async(),
)
.await
.unwrap();
let results = project_b
.update(&mut cx_b, |project, cx| {
project.search(SearchQuery::text("world", false, false), cx)
})
.await
.unwrap();
let mut ranges_by_path = results
.into_iter()
.map(|(buffer, ranges)| {
buffer.read_with(&cx_b, |buffer, cx| {
let path = buffer.file().unwrap().full_path(cx);
let offset_ranges = ranges
.into_iter()
.map(|range| range.to_offset(buffer))
.collect::<Vec<_>>();
(path, offset_ranges)
})
})
.collect::<Vec<_>>();
ranges_by_path.sort_by_key(|(path, _)| path.clone());
assert_eq!(
ranges_by_path,
&[
(PathBuf::from("root-1/a"), vec![6..11]),
(PathBuf::from("root-1/c"), vec![2..7]),
(PathBuf::from("root-1/d"), vec![0..5, 24..29]),
(PathBuf::from("root-2/e"), vec![7..12]),
]
);
}
#[gpui::test(iterations = 10)]
async fn test_document_highlights(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
cx_a.foreground().forbid_parking();
@ -4418,23 +4352,21 @@ mod tests {
.project
.as_ref()
.unwrap()
.read_with(guest_cx, |project, _| {
.read_with(guest_cx, |project, cx| {
assert!(
!project.has_buffered_operations(),
"guest {} has buffered operations ",
!project.has_deferred_operations(cx),
"guest {} has deferred operations",
guest_id,
);
});
for guest_buffer in &guest_client.buffers {
let buffer_id = guest_buffer.read_with(guest_cx, |buffer, _| buffer.remote_id());
let host_buffer = host_project.read_with(&host_cx, |project, _| {
project
.shared_buffer(guest_client.peer_id, buffer_id)
.expect(&format!(
"host doest not have buffer for guest:{}, peer:{}, id:{}",
guest_id, guest_client.peer_id, buffer_id
))
let host_buffer = host_project.read_with(&host_cx, |project, cx| {
project.buffer_for_id(buffer_id, cx).expect(&format!(
"host does not have buffer for guest:{}, peer:{}, id:{}",
guest_id, guest_client.peer_id, buffer_id
))
});
assert_eq!(
guest_buffer.read_with(guest_cx, |buffer, _| buffer.text()),
@ -4829,8 +4761,9 @@ mod tests {
} else {
buffer.update(&mut cx, |buffer, cx| {
log::info!(
"Host: updating buffer {:?}",
buffer.file().unwrap().full_path(cx)
"Host: updating buffer {:?} ({})",
buffer.file().unwrap().full_path(cx),
buffer.remote_id()
);
buffer.randomly_edit(&mut *rng.lock(), 5, cx)
});
@ -4917,9 +4850,19 @@ mod tests {
project_path.1
);
let buffer = project
.update(&mut cx, |project, cx| project.open_buffer(project_path, cx))
.update(&mut cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})
.await
.unwrap();
log::info!(
"Guest {}: path in worktree {:?} {:?} {:?} opened with buffer id {:?}",
guest_id,
project_path.0,
worktree_root_name,
project_path.1,
buffer.read_with(&cx, |buffer, _| buffer.remote_id())
);
self.buffers.insert(buffer.clone());
buffer
} else {
@ -5008,7 +4951,7 @@ mod tests {
save.await;
}
}
40..=45 => {
40..=44 => {
let prepare_rename = project.update(&mut cx, |project, cx| {
log::info!(
"Guest {}: preparing rename for buffer {:?}",
@ -5028,10 +4971,10 @@ mod tests {
prepare_rename.await;
}
}
46..=49 => {
45..=49 => {
let definitions = project.update(&mut cx, |project, cx| {
log::info!(
"Guest {}: requesting defintions for buffer {:?}",
"Guest {}: requesting definitions for buffer {:?}",
guest_id,
buffer.read(cx).file().unwrap().full_path(cx)
);
@ -5049,7 +4992,7 @@ mod tests {
.extend(definitions.await.into_iter().map(|loc| loc.buffer));
}
}
50..=55 => {
50..=54 => {
let highlights = project.update(&mut cx, |project, cx| {
log::info!(
"Guest {}: requesting highlights for buffer {:?}",
@ -5069,6 +5012,22 @@ mod tests {
highlights.await;
}
}
55..=59 => {
let search = project.update(&mut cx, |project, cx| {
let query = rng.lock().gen_range('a'..='z');
log::info!("Guest {}: project-wide search {:?}", guest_id, query);
project.search(SearchQuery::text(query, false, false), cx)
});
let search = cx
.background()
.spawn(async move { search.await.expect("search request failed") });
if rng.lock().gen_bool(0.3) {
log::info!("Guest {}: detaching search request", guest_id);
search.detach();
} else {
self.buffers.extend(search.await.into_keys());
}
}
_ => {
buffer.update(&mut cx, |buffer, cx| {
log::info!(

View file

@ -34,13 +34,13 @@ where
stack: ArrayVec::new(),
position: D::default(),
did_seek: false,
at_end: false,
at_end: tree.is_empty(),
}
}
fn reset(&mut self) {
self.did_seek = false;
self.at_end = false;
self.at_end = self.tree.is_empty();
self.stack.truncate(0);
self.position = D::default();
}
@ -139,7 +139,7 @@ where
if self.at_end {
self.position = D::default();
self.descend_to_last_item(self.tree, cx);
self.at_end = false;
self.at_end = self.tree.is_empty();
} else {
while let Some(entry) = self.stack.pop() {
if entry.index > 0 {
@ -195,13 +195,15 @@ where
{
let mut descend = false;
if self.stack.is_empty() && !self.at_end {
self.stack.push(StackEntry {
tree: self.tree,
index: 0,
position: D::default(),
});
descend = true;
if self.stack.is_empty() {
if !self.at_end {
self.stack.push(StackEntry {
tree: self.tree,
index: 0,
position: D::default(),
});
descend = true;
}
self.did_seek = true;
}
@ -279,6 +281,10 @@ where
cx: &<T::Summary as Summary>::Context,
) {
self.did_seek = true;
if subtree.is_empty() {
return;
}
loop {
match subtree.0.as_ref() {
Node::Internal {
@ -298,7 +304,7 @@ where
subtree = child_trees.last().unwrap();
}
Node::Leaf { item_summaries, .. } => {
let last_index = item_summaries.len().saturating_sub(1);
let last_index = item_summaries.len() - 1;
for item_summary in &item_summaries[0..last_index] {
self.position.add_summary(item_summary, cx);
}

View file

@ -821,6 +821,14 @@ mod tests {
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), None);
assert_eq!(cursor.start().sum, 0);
cursor.prev(&());
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), None);
assert_eq!(cursor.start().sum, 0);
cursor.next(&());
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), None);
assert_eq!(cursor.start().sum, 0);
// Single-element tree
let mut tree = SumTree::<u8>::new();

View file

@ -48,6 +48,12 @@ impl Rope {
*self = new_rope;
}
pub fn slice(&self, range: Range<usize>) -> Rope {
let mut cursor = self.cursor(0);
cursor.seek_forward(range.start);
cursor.slice(range.end)
}
pub fn push(&mut self, text: &str) {
let mut new_chunks = SmallVec::<[_; 16]>::new();
let mut new_chunk = ArrayString::new();

View file

@ -24,7 +24,7 @@ pub struct Theme {
pub project_panel: ProjectPanel,
pub selector: Selector,
pub editor: Editor,
pub find: Find,
pub search: Search,
pub project_diagnostics: ProjectDiagnostics,
}
@ -95,18 +95,21 @@ pub struct Toolbar {
}
#[derive(Clone, Deserialize, Default)]
pub struct Find {
pub struct Search {
#[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 option_button_group: ContainerStyle,
pub option_button: ContainedText,
pub active_option_button: ContainedText,
pub hovered_option_button: ContainedText,
pub active_hovered_option_button: ContainedText,
pub match_background: Color,
pub match_index: ContainedText,
pub results_status: TextStyle,
pub tab_icon_width: f32,
pub tab_icon_spacing: f32,
}
#[derive(Clone, Deserialize, Default)]

View file

@ -123,6 +123,7 @@ enum NavigationMode {
Normal,
GoingBack,
GoingForward,
Disabled,
}
impl Default for NavigationMode {
@ -149,6 +150,10 @@ impl Pane {
}
}
pub fn nav_history(&self) -> &Rc<RefCell<NavHistory>> {
&self.nav_history
}
pub fn activate(&self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Activate);
}
@ -279,7 +284,7 @@ impl Pane {
item_view.added_to_pane(cx);
let item_idx = cmp::min(self.active_item_index + 1, self.item_views.len());
self.item_views
.insert(item_idx, (item_view.item_id(cx), item_view));
.insert(item_idx, (item_view.item(cx).id(), item_view));
self.activate_item(item_idx, cx);
cx.notify();
}
@ -662,6 +667,14 @@ impl ItemNavHistory {
}
impl NavHistory {
pub fn disable(&mut self) {
self.mode = NavigationMode::Disabled;
}
pub fn enable(&mut self) {
self.mode = NavigationMode::Normal;
}
pub fn pop_backward(&mut self) -> Option<NavigationEntry> {
self.backward_stack.pop_back()
}
@ -672,7 +685,7 @@ impl NavHistory {
fn pop(&mut self, mode: NavigationMode) -> Option<NavigationEntry> {
match mode {
NavigationMode::Normal => None,
NavigationMode::Normal | NavigationMode::Disabled => None,
NavigationMode::GoingBack => self.pop_backward(),
NavigationMode::GoingForward => self.pop_forward(),
}
@ -688,6 +701,7 @@ impl NavHistory {
item_view: Rc<dyn WeakItemViewHandle>,
) {
match self.mode {
NavigationMode::Disabled => {}
NavigationMode::Normal => {
if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
self.backward_stack.pop_front();

View file

@ -9,7 +9,7 @@ mod status_bar;
use anyhow::{anyhow, Result};
use client::{Authenticate, ChannelList, Client, User, UserStore};
use clock::ReplicaId;
use collections::HashSet;
use collections::BTreeMap;
use gpui::{
action,
color::Color,
@ -36,6 +36,7 @@ pub use status_bar::StatusItemView;
use std::{
any::{Any, TypeId},
cell::RefCell,
cmp::Reverse,
future::Future,
hash::{Hash, Hasher},
path::{Path, PathBuf},
@ -153,10 +154,10 @@ pub trait Item: Entity + Sized {
pub trait ItemView: View {
fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) {}
fn item_id(&self, cx: &AppContext) -> usize;
fn item(&self, cx: &AppContext) -> Box<dyn ItemHandle>;
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
fn clone_on_split(&self, _: ItemNavHistory, _: &mut ViewContext<Self>) -> Option<Self>
where
Self: Sized,
{
@ -225,11 +226,15 @@ pub trait WeakItemHandle {
}
pub trait ItemViewHandle: 'static {
fn item_id(&self, cx: &AppContext) -> usize;
fn item(&self, cx: &AppContext) -> Box<dyn ItemHandle>;
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>>;
fn clone_on_split(
&self,
nav_history: Rc<RefCell<NavHistory>>,
cx: &mut MutableAppContext,
) -> Option<Box<dyn ItemViewHandle>>;
fn added_to_pane(&mut self, cx: &mut ViewContext<Pane>);
fn deactivated(&self, cx: &mut MutableAppContext);
fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext);
@ -357,8 +362,8 @@ impl dyn ItemViewHandle {
}
impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
fn item_id(&self, cx: &AppContext) -> usize {
self.read(cx).item_id(cx)
fn item(&self, cx: &AppContext) -> Box<dyn ItemHandle> {
self.read(cx).item(cx)
}
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
@ -373,9 +378,15 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
Box::new(self.clone())
}
fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>> {
fn clone_on_split(
&self,
nav_history: Rc<RefCell<NavHistory>>,
cx: &mut MutableAppContext,
) -> Option<Box<dyn ItemViewHandle>> {
self.update(cx, |item, cx| {
cx.add_option_view(|cx| item.clone_on_split(cx))
cx.add_option_view(|cx| {
item.clone_on_split(ItemNavHistory::new(nav_history, &cx.handle()), cx)
})
})
.map(|handle| Box::new(handle) as Box<dyn ItemViewHandle>)
}
@ -559,7 +570,7 @@ pub struct Workspace {
status_bar: ViewHandle<StatusBar>,
project: ModelHandle<Project>,
path_openers: Arc<[Box<dyn PathOpener>]>,
items: HashSet<Box<dyn WeakItemHandle>>,
items: BTreeMap<Reverse<usize>, Box<dyn WeakItemHandle>>,
_observe_current_user: Task<()>,
}
@ -805,17 +816,26 @@ impl Workspace {
fn item_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
self.items
.iter()
.values()
.filter_map(|i| i.upgrade(cx))
.find(|i| i.project_path(cx).as_ref() == Some(path))
}
pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ModelHandle<T>> {
self.items
.iter()
.values()
.find_map(|i| i.upgrade(cx).and_then(|i| i.to_any().downcast()))
}
pub fn items_of_type<'a, T: Item>(
&'a self,
cx: &'a AppContext,
) -> impl 'a + Iterator<Item = ModelHandle<T>> {
self.items
.values()
.filter_map(|i| i.upgrade(cx).and_then(|i| i.to_any().downcast()))
}
pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemViewHandle>> {
self.active_pane().read(cx).active_item()
}
@ -955,7 +975,8 @@ impl Workspace {
where
T: 'static + ItemHandle,
{
self.items.insert(item_handle.downgrade());
self.items
.insert(Reverse(item_handle.id()), item_handle.downgrade());
pane.update(cx, |pane, cx| pane.open_item(item_handle, self, cx))
}
@ -1047,7 +1068,10 @@ impl Workspace {
let new_pane = self.add_pane(cx);
self.activate_pane(new_pane.clone(), cx);
if let Some(item) = pane.read(cx).active_item() {
if let Some(clone) = item.clone_on_split(cx.as_mut()) {
let nav_history = new_pane.read(cx).nav_history().clone();
if let Some(clone) = item.clone_on_split(nav_history, cx.as_mut()) {
let item = clone.item(cx).downgrade();
self.items.insert(Reverse(item.id()), item);
new_pane.update(cx, |new_pane, cx| new_pane.add_item_view(clone, cx));
}
}

View file

@ -36,7 +36,7 @@ contacts_panel = { path = "../contacts_panel" }
diagnostics = { path = "../diagnostics" }
editor = { path = "../editor" }
file_finder = { path = "../file_finder" }
find = { path = "../find" }
search = { path = "../search" }
fsevent = { path = "../fsevent" }
fuzzy = { path = "../fuzzy" }
go_to_line = { path = "../go_to_line" }
@ -88,7 +88,6 @@ smol = "1.2.5"
surf = "2.2"
tempdir = { version = "0.3.7", optional = true }
thiserror = "1.0.29"
time = "0.3"
tiny_http = "0.8"
toml = "0.5"
tree-sitter = "0.20.4"

View file

@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.0893 10.4092L8.34129 7.66113C8.93602 6.93311 9.26414 6.01641 9.26414 5.01562C9.26414 2.65928 7.35425 0.75 4.99851 0.75C2.64278 0.75 0.751343 2.65989 0.751343 5.01562C0.751343 7.37136 2.66103 9.28125 4.99851 9.28125C5.99909 9.28125 6.91702 8.93446 7.64402 8.35758L10.3921 11.1056C10.5069 11.2028 10.6341 11.25 10.7592 11.25C10.8843 11.25 11.011 11.2019 11.1072 11.1058C11.2985 10.9137 11.2985 10.602 11.0893 10.4092ZM1.73572 5.01562C1.73572 3.20643 3.20777 1.73438 5.01697 1.73438C6.82617 1.73438 8.29822 3.20643 8.29822 5.01562C8.29822 6.82482 6.82617 8.29688 5.01697 8.29688C3.20777 8.29688 1.73572 6.82441 1.73572 5.01562Z" fill="white" fill-opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 776 B

View file

@ -348,11 +348,14 @@ tab_icon_width = 13
tab_icon_spacing = 4
tab_summary_spacing = 10
[find]
[search]
match_background = "$state.highlighted_line"
background = "$surface.1"
results_status = { extends = "$text.0", size = 18 }
tab_icon_width = 14
tab_icon_spacing = 4
[find.mode_button]
[search.option_button]
extends = "$text.1"
padding = { left = 6, right = 6, top = 1, bottom = 1 }
corner_radius = 6
@ -361,26 +364,26 @@ border = { width = 1, color = "$border.0" }
margin.left = 1
margin.right = 1
[find.mode_button_group]
[search.option_button_group]
padding = { left = 2, right = 2 }
[find.active_mode_button]
extends = "$find.mode_button"
[search.active_option_button]
extends = "$search.option_button"
background = "$surface.2"
[find.hovered_mode_button]
extends = "$find.mode_button"
[search.hovered_option_button]
extends = "$search.option_button"
background = "$surface.2"
[find.active_hovered_mode_button]
extends = "$find.mode_button"
[search.active_hovered_option_button]
extends = "$search.option_button"
background = "$surface.2"
[find.match_index]
[search.match_index]
extends = "$text.1"
padding = 6
[find.editor]
[search.editor]
max_width = 400
background = "$surface.0"
corner_radius = 6
@ -391,6 +394,6 @@ placeholder_text = "$text.2"
selection = "$selection.host"
border = { width = 1, color = "$border.0" }
[find.invalid_editor]
extends = "$find.editor"
[search.invalid_editor]
extends = "$search.editor"
border = { width = 1, color = "$status.bad" }

View file

@ -60,7 +60,7 @@ fn main() {
project_symbols::init(cx);
project_panel::init(cx);
diagnostics::init(cx);
find::init(cx);
search::init(cx);
cx.spawn({
let client = client.clone();
|cx| async move {