Merge branch 'main' into chat-again

This commit is contained in:
Max Brunsfeld 2023-09-12 11:26:54 -07:00
commit d97c9d8dfd
20 changed files with 769 additions and 189 deletions

View File

@ -12,16 +12,12 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
```
sudo xcodebuild -license
```
* Install rustup (rust, cargo, etc.)
```
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
* Install homebrew and node
* Install homebrew, node and rustup-init (rutup, rust, cargo, etc.)
```
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install node
brew install node rustup-init
rustup-init # follow the installation steps
```
* Install postgres and configure the database

View File

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.5 7V9.5M9.5 12V9.5M12 9.5H9.5M7 9.5H9.5M9.5 9.5L11.1667 7.83333M9.5 9.5L7.83333 11.1667M9.5 9.5L11.1667 11.1667M9.5 9.5L7.83333 7.83333" stroke="#11181C" stroke-width="1.25" stroke-linecap="round"/>
<path d="M2.19366 3.84943C2.19188 4.26418 2.32864 4.59864 2.60673 4.84707C2.88052 5.09166 3.25136 5.26933 3.71609 5.3824C3.71616 5.38242 3.71623 5.38243 3.7163 5.38245L4.30919 5.53134L4.30919 5.53134L4.30965 5.53145C4.50649 5.57891 4.67124 5.63133 4.80447 5.68843L4.80469 5.68852C4.93838 5.74508 5.03564 5.81206 5.10001 5.8877L5.10001 5.8877L5.10041 5.88816C5.16432 5.96142 5.19716 6.05222 5.19716 6.16389C5.19716 6.28412 5.1609 6.38933 5.0882 6.48141C5.01496 6.57418 4.91031 6.64838 4.77141 6.70259L4.77121 6.70266C4.63472 6.75659 4.47185 6.7843 4.28146 6.7843C4.08801 6.7843 3.91607 6.75496 3.76491 6.69726C3.61654 6.6382 3.49924 6.55209 3.41132 6.43942C3.3502 6.35821 3.30747 6.26204 3.28375 6.14992C3.26238 6.04888 3.1772 5.96225 3.06518 5.96225H2.26366C2.14682 5.96225 2.04842 6.05919 2.0592 6.18012C2.08842 6.50802 2.1826 6.79102 2.34331 7.02735L2.34352 7.02767C2.53217 7.30057 2.79377 7.50587 3.12633 7.64399L3.12642 7.64402C3.46009 7.78185 3.84993 7.85 4.29476 7.85C4.74293 7.85 5.12859 7.7828 5.45023 7.64651L5.45036 7.64646C5.77328 7.50857 6.02259 7.31417 6.19551 7.06217C6.37037 6.80817 6.4579 6.50901 6.45972 6.16682L6.45972 6.16616C6.4579 5.9333 6.41513 5.72482 6.33012 5.54178C6.2474 5.35987 6.13061 5.20175 5.98007 5.06773C5.83038 4.93448 5.65389 4.82273 5.4511 4.7322C5.24919 4.64206 5.02795 4.57016 4.78757 4.51632L4.29841 4.39935L4.29841 4.39934L4.29771 4.39919C4.18081 4.37301 4.07116 4.34168 3.9687 4.30523C3.86715 4.26734 3.77847 4.22375 3.70232 4.17471C3.62796 4.12508 3.57037 4.06717 3.52849 4.00124C3.49012 3.93815 3.47157 3.86312 3.47481 3.77407L3.47484 3.77407V3.77225C3.47484 3.66563 3.50527 3.57146 3.56612 3.48808C3.6287 3.40475 3.71977 3.33801 3.84235 3.28931L3.84235 3.28932L3.84289 3.28909C3.96465 3.23906 4.1165 3.21304 4.30008 3.21304C4.57006 3.21304 4.77746 3.27105 4.92754 3.38154C5.04235 3.46608 5.11838 3.57594 5.15673 3.71259C5.18352 3.80802 5.26636 3.89142 5.37611 3.89142H6.17259C6.28852 3.89142 6.38806 3.7953 6.37515 3.67382C6.34686 3.4077 6.26051 3.16831 6.1158 2.95658C5.94159 2.70169 5.6982 2.50368 5.38762 2.36201L5.36687 2.4075M2.19366 3.84943C2.19187 3.51004 2.28242 3.21139 2.46644 2.9556L2.46658 2.9554C2.65148 2.70093 2.90447 2.50326 3.22368 2.36179C3.54316 2.2202 3.90494 2.15 4.30807 2.15C4.71809 2.15 5.07841 2.22014 5.38773 2.36206L5.36687 2.4075M2.19366 3.84943C2.19366 3.84951 2.19366 3.84959 2.19366 3.84967L2.24366 3.8494L2.19366 3.84918C2.19366 3.84926 2.19366 3.84935 2.19366 3.84943ZM5.36687 2.4075C5.06537 2.26917 4.71244 2.2 4.30807 2.2C3.91079 2.2 3.55608 2.26917 3.24394 2.4075C2.93179 2.54584 2.68616 2.73827 2.50703 2.9848L3.82389 3.24285L3.82389 3.24285C3.95336 3.18964 4.11209 3.16304 4.30008 3.16304C4.57676 3.16304 4.79579 3.22245 4.95718 3.34128C5.08094 3.43239 5.1635 3.55166 5.20487 3.69908C5.2271 3.77827 5.29386 3.84142 5.37611 3.84142H6.17259C6.26198 3.84142 6.33488 3.76799 6.32543 3.6791C6.29797 3.4208 6.21433 3.18936 6.07452 2.9848C5.90603 2.73827 5.67015 2.54584 5.36687 2.4075ZM4.78958 6.74917C4.64593 6.80592 4.47655 6.8343 4.28146 6.8343C4.08283 6.8343 3.90458 6.80415 3.74674 6.74384C3.59067 6.68177 3.46563 6.59043 3.37163 6.46983L4.78958 6.74917ZM4.78958 6.74917C4.93502 6.69241 5.04764 6.61349 5.12745 6.5124M4.78958 6.74917L5.12745 6.5124M5.12745 6.5124C5.20726 6.4113 5.24716 6.29514 5.24716 6.16389M5.12745 6.5124L5.24716 6.16389M5.24716 6.16389C5.24716 6.04152 5.2108 5.93865 5.13809 5.85529L5.24716 6.16389Z" fill="#687076" stroke="#687076" stroke-width="0.1"/>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -16,7 +16,7 @@ use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
SelectionGoal,
};
use project::{FormatTrigger, Item as _, Project, ProjectPath};
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
use rpc::proto::{self, update_view};
use smallvec::SmallVec;
use std::{
@ -26,6 +26,7 @@ use std::{
iter,
ops::Range,
path::{Path, PathBuf},
sync::Arc,
};
use text::Selection;
use util::{
@ -978,7 +979,26 @@ impl SearchableItem for Editor {
}
self.change_selections(None, cx, |s| s.select_ranges(ranges));
}
fn replace(
&mut self,
identifier: &Self::Match,
query: &SearchQuery,
cx: &mut ViewContext<Self>,
) {
let text = self.buffer.read(cx);
let text = text.snapshot(cx);
let text = text.text_for_range(identifier.clone()).collect::<Vec<_>>();
let text: Cow<_> = if text.len() == 1 {
text.first().cloned().unwrap().into()
} else {
let joined_chunks = text.join("");
joined_chunks.into()
};
if let Some(replacement) = query.replacement(&text) {
self.edit([(identifier.clone(), Arc::from(&*replacement))], cx);
}
}
fn match_index_for_direction(
&mut self,
matches: &Vec<Range<Anchor>>,
@ -1030,7 +1050,7 @@ impl SearchableItem for Editor {
fn find_matches(
&mut self,
query: project::search::SearchQuery,
query: Arc<project::search::SearchQuery>,
cx: &mut ViewContext<Self>,
) -> Task<Vec<Range<Anchor>>> {
let buffer = self.buffer().read(cx).snapshot(cx);

View File

@ -13,7 +13,7 @@ use gpui::{
use isahc::Request;
use language::Buffer;
use postage::prelude::Stream;
use project::Project;
use project::{search::SearchQuery, Project};
use regex::Regex;
use serde::Serialize;
use smallvec::SmallVec;
@ -418,10 +418,13 @@ impl SearchableItem for FeedbackEditor {
self.editor
.update(cx, |e, cx| e.select_matches(matches, cx))
}
fn replace(&mut self, matches: &Self::Match, query: &SearchQuery, cx: &mut ViewContext<Self>) {
self.editor
.update(cx, |e, cx| e.replace(matches, query, cx));
}
fn find_matches(
&mut self,
query: project::search::SearchQuery,
query: Arc<project::search::SearchQuery>,
cx: &mut ViewContext<Self>,
) -> Task<Vec<Self::Match>> {
self.editor

View File

@ -28,24 +28,24 @@ fn generate_methods() -> Vec<TokenStream2> {
let mut methods = Vec::new();
for (prefix, auto_allowed, fields) in box_prefixes() {
for (suffix, length_tokens) in box_suffixes() {
for (suffix, length_tokens, doc_string) in box_suffixes() {
if auto_allowed || suffix != "auto" {
let method = generate_method(prefix, suffix, &fields, length_tokens);
let method = generate_method(prefix, suffix, &fields, length_tokens, doc_string);
methods.push(method);
}
}
}
for (prefix, fields) in corner_prefixes() {
for (suffix, radius_tokens) in corner_suffixes() {
let method = generate_method(prefix, suffix, &fields, radius_tokens);
for (suffix, radius_tokens, doc_string) in corner_suffixes() {
let method = generate_method(prefix, suffix, &fields, radius_tokens, doc_string);
methods.push(method);
}
}
for (prefix, fields) in border_prefixes() {
for (suffix, width_tokens) in border_suffixes() {
let method = generate_method(prefix, suffix, &fields, width_tokens);
for (suffix, width_tokens, doc_string) in border_suffixes() {
let method = generate_method(prefix, suffix, &fields, width_tokens, doc_string);
methods.push(method);
}
}
@ -58,6 +58,7 @@ fn generate_method(
suffix: &'static str,
fields: &Vec<TokenStream2>,
length_tokens: TokenStream2,
doc_string: &'static str,
) -> TokenStream2 {
let method_name = if suffix.is_empty() {
format_ident!("{}", prefix)
@ -75,6 +76,7 @@ fn generate_method(
.collect::<Vec<_>>();
let method = quote! {
#[doc = #doc_string]
fn #method_name(mut self) -> Self where Self: std::marker::Sized {
let mut style = self.declared_style();
#(#field_assignments)*
@ -160,55 +162,52 @@ fn box_prefixes() -> Vec<(&'static str, bool, Vec<TokenStream2>)> {
]
}
fn box_suffixes() -> Vec<(&'static str, TokenStream2)> {
fn box_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
vec![
("0", quote! { pixels(0.) }),
("0p5", quote! { rems(0.125) }),
("1", quote! { rems(0.25) }),
("1p5", quote! { rems(0.375) }),
("2", quote! { rems(0.5) }),
("2p5", quote! { rems(0.625) }),
("3", quote! { rems(0.75) }),
("3p5", quote! { rems(0.875) }),
("4", quote! { rems(1.) }),
("5", quote! { rems(1.25) }),
("6", quote! { rems(1.5) }),
("7", quote! { rems(1.75) }),
("8", quote! { rems(2.0) }),
("9", quote! { rems(2.25) }),
("10", quote! { rems(2.5) }),
("11", quote! { rems(2.75) }),
("12", quote! { rems(3.) }),
("16", quote! { rems(4.) }),
("20", quote! { rems(5.) }),
("24", quote! { rems(6.) }),
("32", quote! { rems(8.) }),
("40", quote! { rems(10.) }),
("48", quote! { rems(12.) }),
("56", quote! { rems(14.) }),
("64", quote! { rems(16.) }),
("72", quote! { rems(18.) }),
("80", quote! { rems(20.) }),
("96", quote! { rems(24.) }),
("auto", quote! { auto() }),
("px", quote! { pixels(1.) }),
("full", quote! { relative(1.) }),
("1_2", quote! { relative(0.5) }),
("1_3", quote! { relative(1./3.) }),
("2_3", quote! { relative(2./3.) }),
("1_4", quote! { relative(0.25) }),
("2_4", quote! { relative(0.5) }),
("3_4", quote! { relative(0.75) }),
("1_5", quote! { relative(0.2) }),
("2_5", quote! { relative(0.4) }),
("3_5", quote! { relative(0.6) }),
("4_5", quote! { relative(0.8) }),
("1_6", quote! { relative(1./6.) }),
("5_6", quote! { relative(5./6.) }),
("1_12", quote! { relative(1./12.) }),
// ("screen_50", quote! { DefiniteLength::Vh(50.0) }),
// ("screen_75", quote! { DefiniteLength::Vh(75.0) }),
// ("screen", quote! { DefiniteLength::Vh(100.0) }),
("0", quote! { pixels(0.) }, "0px"),
("0p5", quote! { rems(0.125) }, "2px (0.125rem)"),
("1", quote! { rems(0.25) }, "4px (0.25rem)"),
("1p5", quote! { rems(0.375) }, "6px (0.375rem)"),
("2", quote! { rems(0.5) }, "8px (0.5rem)"),
("2p5", quote! { rems(0.625) }, "10px (0.625rem)"),
("3", quote! { rems(0.75) }, "12px (0.75rem)"),
("3p5", quote! { rems(0.875) }, "14px (0.875rem)"),
("4", quote! { rems(1.) }, "16px (1rem)"),
("5", quote! { rems(1.25) }, "20px (1.25rem)"),
("6", quote! { rems(1.5) }, "24px (1.5rem)"),
("7", quote! { rems(1.75) }, "28px (1.75rem)"),
("8", quote! { rems(2.0) }, "32px (2rem)"),
("9", quote! { rems(2.25) }, "36px (2.25rem)"),
("10", quote! { rems(2.5) }, "40px (2.5rem)"),
("11", quote! { rems(2.75) }, "44px (2.75rem)"),
("12", quote! { rems(3.) }, "48px (3rem)"),
("16", quote! { rems(4.) }, "64px (4rem)"),
("20", quote! { rems(5.) }, "80px (5rem)"),
("24", quote! { rems(6.) }, "96px (6rem)"),
("32", quote! { rems(8.) }, "128px (8rem)"),
("40", quote! { rems(10.) }, "160px (10rem)"),
("48", quote! { rems(12.) }, "192px (12rem)"),
("56", quote! { rems(14.) }, "224px (14rem)"),
("64", quote! { rems(16.) }, "256px (16rem)"),
("72", quote! { rems(18.) }, "288px (18rem)"),
("80", quote! { rems(20.) }, "320px (20rem)"),
("96", quote! { rems(24.) }, "384px (24rem)"),
("auto", quote! { auto() }, "Auto"),
("px", quote! { pixels(1.) }, "1px"),
("full", quote! { relative(1.) }, "100%"),
("1_2", quote! { relative(0.5) }, "50% (1/2)"),
("1_3", quote! { relative(1./3.) }, "33% (1/3)"),
("2_3", quote! { relative(2./3.) }, "66% (2/3)"),
("1_4", quote! { relative(0.25) }, "25% (1/4)"),
("2_4", quote! { relative(0.5) }, "50% (2/4)"),
("3_4", quote! { relative(0.75) }, "75% (3/4)"),
("1_5", quote! { relative(0.2) }, "20% (1/5)"),
("2_5", quote! { relative(0.4) }, "40% (2/5)"),
("3_5", quote! { relative(0.6) }, "60% (3/5)"),
("4_5", quote! { relative(0.8) }, "80% (4/5)"),
("1_6", quote! { relative(1./6.) }, "16% (1/6)"),
("5_6", quote! { relative(5./6.) }, "80% (5/6)"),
("1_12", quote! { relative(1./12.) }, "8% (1/12)"),
]
}
@ -258,16 +257,16 @@ fn corner_prefixes() -> Vec<(&'static str, Vec<TokenStream2>)> {
]
}
fn corner_suffixes() -> Vec<(&'static str, TokenStream2)> {
fn corner_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
vec![
("none", quote! { pixels(0.) }),
("sm", quote! { rems(0.125) }),
("md", quote! { rems(0.25) }),
("lg", quote! { rems(0.5) }),
("xl", quote! { rems(0.75) }),
("2xl", quote! { rems(1.) }),
("3xl", quote! { rems(1.5) }),
("full", quote! { pixels(9999.) }),
("none", quote! { pixels(0.) }, "0px"),
("sm", quote! { rems(0.125) }, "2px (0.125rem)"),
("md", quote! { rems(0.25) }, "4px (0.25rem)"),
("lg", quote! { rems(0.5) }, "8px (0.5rem)"),
("xl", quote! { rems(0.75) }, "12px (0.75rem)"),
("2xl", quote! { rems(1.) }, "16px (1rem)"),
("3xl", quote! { rems(1.5) }, "24px (1.5rem)"),
("full", quote! { pixels(9999.) }, "9999px"),
]
}
@ -303,25 +302,25 @@ fn border_prefixes() -> Vec<(&'static str, Vec<TokenStream2>)> {
]
}
fn border_suffixes() -> Vec<(&'static str, TokenStream2)> {
fn border_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
vec![
("", quote! { pixels(1.) }),
("0", quote! { pixels(0.) }),
("1", quote! { pixels(1.) }),
("2", quote! { pixels(2.) }),
("3", quote! { pixels(3.) }),
("4", quote! { pixels(4.) }),
("5", quote! { pixels(5.) }),
("6", quote! { pixels(6.) }),
("7", quote! { pixels(7.) }),
("8", quote! { pixels(8.) }),
("9", quote! { pixels(9.) }),
("10", quote! { pixels(10.) }),
("11", quote! { pixels(11.) }),
("12", quote! { pixels(12.) }),
("16", quote! { pixels(16.) }),
("20", quote! { pixels(20.) }),
("24", quote! { pixels(24.) }),
("32", quote! { pixels(32.) }),
("", quote! { pixels(1.)}, "1px"),
("0", quote! { pixels(0.)}, "0px"),
("1", quote! { pixels(1.) }, "1px"),
("2", quote! { pixels(2.) }, "2px"),
("3", quote! { pixels(3.) }, "3px"),
("4", quote! { pixels(4.) }, "4px"),
("5", quote! { pixels(5.) }, "5px"),
("6", quote! { pixels(6.) }, "6px"),
("7", quote! { pixels(7.) }, "7px"),
("8", quote! { pixels(8.) }, "8px"),
("9", quote! { pixels(9.) }, "9px"),
("10", quote! { pixels(10.) }, "10px"),
("11", quote! { pixels(11.) }, "11px"),
("12", quote! { pixels(12.) }, "12px"),
("16", quote! { pixels(16.) }, "16px"),
("20", quote! { pixels(20.) }, "20px"),
("24", quote! { pixels(24.) }, "24px"),
("32", quote! { pixels(32.) }, "32px"),
]
}

View File

@ -13,7 +13,7 @@ use gpui::{
};
use language::{Buffer, LanguageServerId, LanguageServerName};
use lsp::IoKind;
use project::{Project, Worktree};
use project::{search::SearchQuery, Project, Worktree};
use std::{borrow::Cow, sync::Arc};
use theme::{ui, Theme};
use workspace::{
@ -524,12 +524,24 @@ impl SearchableItem for LspLogView {
fn find_matches(
&mut self,
query: project::search::SearchQuery,
query: Arc<project::search::SearchQuery>,
cx: &mut ViewContext<Self>,
) -> gpui::Task<Vec<Self::Match>> {
self.editor.update(cx, |e, cx| e.find_matches(query, cx))
}
fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>) {
// Since LSP Log is read-only, it doesn't make sense to support replace operation.
}
fn supported_options() -> workspace::searchable::SearchOptions {
workspace::searchable::SearchOptions {
case: true,
word: true,
regex: true,
// LSP log is read-only.
replacement: false,
}
}
fn active_match_index(
&mut self,
matches: Vec<Self::Match>,

View File

@ -7,6 +7,7 @@ use language::{char_kind, BufferSnapshot};
use regex::{Regex, RegexBuilder};
use smol::future::yield_now;
use std::{
borrow::Cow,
io::{BufRead, BufReader, Read},
ops::Range,
path::{Path, PathBuf},
@ -35,6 +36,7 @@ impl SearchInputs {
pub enum SearchQuery {
Text {
search: Arc<AhoCorasick<usize>>,
replacement: Option<String>,
whole_word: bool,
case_sensitive: bool,
inner: SearchInputs,
@ -42,7 +44,7 @@ pub enum SearchQuery {
Regex {
regex: Regex,
replacement: Option<String>,
multiline: bool,
whole_word: bool,
case_sensitive: bool,
@ -95,6 +97,7 @@ impl SearchQuery {
};
Self::Text {
search: Arc::new(search),
replacement: None,
whole_word,
case_sensitive,
inner,
@ -130,6 +133,7 @@ impl SearchQuery {
};
Ok(Self::Regex {
regex,
replacement: None,
multiline,
whole_word,
case_sensitive,
@ -156,7 +160,21 @@ impl SearchQuery {
))
}
}
pub fn with_replacement(mut self, new_replacement: Option<String>) -> Self {
match self {
Self::Text {
ref mut replacement,
..
}
| Self::Regex {
ref mut replacement,
..
} => {
*replacement = new_replacement;
self
}
}
}
pub fn to_proto(&self, project_id: u64) -> proto::SearchProject {
proto::SearchProject {
project_id,
@ -214,7 +232,20 @@ impl SearchQuery {
}
}
}
pub fn replacement<'a>(&self, text: &'a str) -> Option<Cow<'a, str>> {
match self {
SearchQuery::Text { replacement, .. } => replacement.clone().map(Cow::from),
SearchQuery::Regex {
regex, replacement, ..
} => {
if let Some(replacement) = replacement {
Some(regex.replace(text, replacement))
} else {
None
}
}
}
}
pub async fn search(
&self,
buffer: &BufferSnapshot,

View File

@ -2,19 +2,16 @@ use crate::{
history::SearchHistory,
mode::{next_mode, SearchMode, Side},
search_bar::{render_nav_button, render_search_mode_button},
CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches,
SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord,
CycleMode, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions,
SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace,
ToggleWholeWord,
};
use collections::HashMap;
use editor::Editor;
use futures::channel::oneshot;
use gpui::{
actions,
elements::*,
impl_actions,
platform::{CursorStyle, MouseButton},
Action, AnyViewHandle, AppContext, Entity, Subscription, Task, View, ViewContext, ViewHandle,
WindowContext,
actions, elements::*, impl_actions, Action, AnyViewHandle, AppContext, Entity, Subscription,
Task, View, ViewContext, ViewHandle, WindowContext,
};
use project::search::SearchQuery;
use serde::Deserialize;
@ -54,6 +51,11 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(BufferSearchBar::previous_history_query);
cx.add_action(BufferSearchBar::cycle_mode);
cx.add_action(BufferSearchBar::cycle_mode_on_pane);
cx.add_action(BufferSearchBar::replace_all);
cx.add_action(BufferSearchBar::replace_next);
cx.add_action(BufferSearchBar::replace_all_on_pane);
cx.add_action(BufferSearchBar::replace_next_on_pane);
cx.add_action(BufferSearchBar::toggle_replace);
add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
}
@ -73,9 +75,11 @@ fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContex
pub struct BufferSearchBar {
query_editor: ViewHandle<Editor>,
replacement_editor: ViewHandle<Editor>,
active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
active_match_index: Option<usize>,
active_searchable_item_subscription: Option<Subscription>,
active_search: Option<Arc<SearchQuery>>,
searchable_items_with_matches:
HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
pending_search: Option<Task<()>>,
@ -85,6 +89,7 @@ pub struct BufferSearchBar {
dismissed: bool,
search_history: SearchHistory,
current_mode: SearchMode,
replace_is_active: bool,
}
impl Entity for BufferSearchBar {
@ -156,6 +161,9 @@ impl View for BufferSearchBar {
self.query_editor.update(cx, |editor, cx| {
editor.set_placeholder_text(new_placeholder_text, cx);
});
self.replacement_editor.update(cx, |editor, cx| {
editor.set_placeholder_text("Replace with...", cx);
});
let search_button_for_mode = |mode, side, cx: &mut ViewContext<BufferSearchBar>| {
let is_active = self.current_mode == mode;
@ -212,7 +220,6 @@ impl View for BufferSearchBar {
cx,
)
};
let query_column = Flex::row()
.with_child(
Svg::for_style(theme.search.editor_icon.clone().icon)
@ -243,7 +250,57 @@ impl View for BufferSearchBar {
.with_max_width(theme.search.editor.max_width)
.with_height(theme.search.search_bar_row_height)
.flex(1., false);
let should_show_replace_input = self.replace_is_active && supported_options.replacement;
let replacement = should_show_replace_input.then(|| {
Flex::row()
.with_child(
Svg::for_style(theme.search.replace_icon.clone().icon)
.contained()
.with_style(theme.search.replace_icon.clone().container),
)
.with_child(ChildView::new(&self.replacement_editor, cx).flex(1., true))
.align_children_center()
.flex(1., true)
.contained()
.with_style(query_container_style)
.constrained()
.with_min_width(theme.search.editor.min_width)
.with_max_width(theme.search.editor.max_width)
.with_height(theme.search.search_bar_row_height)
.flex(1., false)
});
let replace_all = should_show_replace_input.then(|| {
super::replace_action(
ReplaceAll,
"Replace all",
"icons/replace_all.svg",
theme.tooltip.clone(),
theme.search.action_button.clone(),
)
});
let replace_next = should_show_replace_input.then(|| {
super::replace_action(
ReplaceNext,
"Replace next",
"icons/replace_next.svg",
theme.tooltip.clone(),
theme.search.action_button.clone(),
)
});
let switches_column = supported_options.replacement.then(|| {
Flex::row()
.align_children_center()
.with_child(super::toggle_replace_button(
self.replace_is_active,
theme.tooltip.clone(),
theme.search.option_button_component.clone(),
))
.constrained()
.with_height(theme.search.search_bar_row_height)
.contained()
.with_style(theme.search.option_button_group)
});
let mode_column = Flex::row()
.with_child(search_button_for_mode(
SearchMode::Text,
@ -261,7 +318,10 @@ impl View for BufferSearchBar {
.with_height(theme.search.search_bar_row_height);
let nav_column = Flex::row()
.with_child(self.render_action_button("all", cx))
.align_children_center()
.with_children(replace_next)
.with_children(replace_all)
.with_child(self.render_action_button("icons/select-all.svg", cx))
.with_child(Flex::row().with_children(match_count))
.with_child(nav_button_for_direction("<", Direction::Prev, cx))
.with_child(nav_button_for_direction(">", Direction::Next, cx))
@ -271,6 +331,8 @@ impl View for BufferSearchBar {
Flex::row()
.with_child(query_column)
.with_children(switches_column)
.with_children(replacement)
.with_child(mode_column)
.with_child(nav_column)
.contained()
@ -345,9 +407,18 @@ impl BufferSearchBar {
});
cx.subscribe(&query_editor, Self::on_query_editor_event)
.detach();
let replacement_editor = cx.add_view(|cx| {
Editor::auto_height(
2,
Some(Arc::new(|theme| theme.search.editor.input.clone())),
cx,
)
});
// cx.subscribe(&replacement_editor, Self::on_query_editor_event)
// .detach();
Self {
query_editor,
replacement_editor,
active_searchable_item: None,
active_searchable_item_subscription: None,
active_match_index: None,
@ -359,6 +430,8 @@ impl BufferSearchBar {
dismissed: true,
search_history: SearchHistory::default(),
current_mode: SearchMode::default(),
active_search: None,
replace_is_active: false,
}
}
@ -441,7 +514,9 @@ impl BufferSearchBar {
pub fn query(&self, cx: &WindowContext) -> String {
self.query_editor.read(cx).text(cx)
}
pub fn replacement(&self, cx: &WindowContext) -> String {
self.replacement_editor.read(cx).text(cx)
}
pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
self.active_searchable_item
.as_ref()
@ -477,37 +552,16 @@ impl BufferSearchBar {
) -> AnyElement<Self> {
let tooltip = "Select All Matches";
let tooltip_style = theme::current(cx).tooltip.clone();
let action_type_id = 0_usize;
let has_matches = self.active_match_index.is_some();
let cursor_style = if has_matches {
CursorStyle::PointingHand
} else {
CursorStyle::default()
};
enum ActionButton {}
MouseEventHandler::new::<ActionButton, _>(action_type_id, cx, |state, cx| {
let theme = theme::current(cx);
let style = theme
.search
.action_button
.in_state(has_matches)
.style_for(state);
Label::new(icon, style.text.clone())
.aligned()
.contained()
.with_style(style.container)
})
.on_click(MouseButton::Left, move |_, this, cx| {
this.select_all_matches(&SelectAllMatches, cx)
})
.with_cursor_style(cursor_style)
.with_tooltip::<ActionButton>(
action_type_id,
tooltip.to_string(),
Some(Box::new(SelectAllMatches)),
tooltip_style,
cx,
)
let theme = theme::current(cx);
let style = theme.search.action_button.clone();
gpui::elements::Component::element(SafeStylable::with_style(
theme::components::action_button::Button::action(SelectAllMatches)
.with_tooltip(tooltip, tooltip_style)
.with_contents(theme::components::svg::Svg::new(icon)),
style,
))
.into_any()
}
@ -688,6 +742,7 @@ impl BufferSearchBar {
let (done_tx, done_rx) = oneshot::channel();
let query = self.query(cx);
self.pending_search.take();
if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
if query.is_empty() {
self.active_match_index.take();
@ -695,7 +750,7 @@ impl BufferSearchBar {
let _ = done_tx.send(());
cx.notify();
} else {
let query = if self.current_mode == SearchMode::Regex {
let query: Arc<_> = if self.current_mode == SearchMode::Regex {
match SearchQuery::regex(
query,
self.search_options.contains(SearchOptions::WHOLE_WORD),
@ -703,7 +758,8 @@ impl BufferSearchBar {
Vec::new(),
Vec::new(),
) {
Ok(query) => query,
Ok(query) => query
.with_replacement(Some(self.replacement(cx)).filter(|s| !s.is_empty())),
Err(_) => {
self.query_contains_error = true;
cx.notify();
@ -718,8 +774,10 @@ impl BufferSearchBar {
Vec::new(),
Vec::new(),
)
};
.with_replacement(Some(self.replacement(cx)).filter(|s| !s.is_empty()))
}
.into();
self.active_search = Some(query.clone());
let query_text = query.as_str().to_string();
let matches = active_searchable_item.find_matches(query, cx);
@ -810,6 +868,63 @@ impl BufferSearchBar {
cx.propagate_action();
}
}
fn toggle_replace(&mut self, _: &ToggleReplace, _: &mut ViewContext<Self>) {
if let Some(_) = &self.active_searchable_item {
self.replace_is_active = !self.replace_is_active;
}
}
fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
if !self.dismissed && self.active_search.is_some() {
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
if let Some(query) = self.active_search.as_ref() {
if let Some(matches) = self
.searchable_items_with_matches
.get(&searchable_item.downgrade())
{
if let Some(active_index) = self.active_match_index {
let query = query.as_ref().clone().with_replacement(
Some(self.replacement(cx)).filter(|rep| !rep.is_empty()),
);
searchable_item.replace(&matches[active_index], &query, cx);
}
self.focus_editor(&FocusEditor, cx);
}
}
}
}
}
fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
if !self.dismissed && self.active_search.is_some() {
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
if let Some(query) = self.active_search.as_ref() {
if let Some(matches) = self
.searchable_items_with_matches
.get(&searchable_item.downgrade())
{
let query = query.as_ref().clone().with_replacement(
Some(self.replacement(cx)).filter(|rep| !rep.is_empty()),
);
for m in matches {
searchable_item.replace(m, &query, cx);
}
self.focus_editor(&FocusEditor, cx);
}
}
}
}
}
fn replace_next_on_pane(pane: &mut Pane, action: &ReplaceNext, cx: &mut ViewContext<Pane>) {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |bar, cx| bar.replace_next(action, cx));
}
}
fn replace_all_on_pane(pane: &mut Pane, action: &ReplaceAll, cx: &mut ViewContext<Pane>) {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |bar, cx| bar.replace_all(action, cx));
}
}
}
#[cfg(test)]
@ -1539,4 +1654,109 @@ mod tests {
assert_eq!(search_bar.search_options, SearchOptions::NONE);
});
}
#[gpui::test]
async fn test_replace_simple(cx: &mut TestAppContext) {
let (editor, search_bar) = init_test(cx);
search_bar
.update(cx, |search_bar, cx| {
search_bar.search("expression", None, cx)
})
.await
.unwrap();
search_bar.update(cx, |search_bar, cx| {
search_bar.replacement_editor.update(cx, |editor, cx| {
// We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
editor.set_text("expr$1", cx);
});
search_bar.replace_all(&ReplaceAll, cx)
});
assert_eq!(
editor.read_with(cx, |this, cx| { this.text(cx) }),
r#"
A regular expr$1 (shortened as regex or regexp;[1] also referred to as
rational expr$1[2][3]) is a sequence of characters that specifies a search
pattern in text. Usually such patterns are used by string-searching algorithms
for "find" or "find and replace" operations on strings, or for input validation.
"#
.unindent()
);
// Search for word boundaries and replace just a single one.
search_bar
.update(cx, |search_bar, cx| {
search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
})
.await
.unwrap();
search_bar.update(cx, |search_bar, cx| {
search_bar.replacement_editor.update(cx, |editor, cx| {
editor.set_text("banana", cx);
});
search_bar.replace_next(&ReplaceNext, cx)
});
// Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
assert_eq!(
editor.read_with(cx, |this, cx| { this.text(cx) }),
r#"
A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
rational expr$1[2][3]) is a sequence of characters that specifies a search
pattern in text. Usually such patterns are used by string-searching algorithms
for "find" or "find and replace" operations on strings, or for input validation.
"#
.unindent()
);
// Let's turn on regex mode.
search_bar
.update(cx, |search_bar, cx| {
search_bar.activate_search_mode(SearchMode::Regex, cx);
search_bar.search("\\[([^\\]]+)\\]", None, cx)
})
.await
.unwrap();
search_bar.update(cx, |search_bar, cx| {
search_bar.replacement_editor.update(cx, |editor, cx| {
editor.set_text("${1}number", cx);
});
search_bar.replace_all(&ReplaceAll, cx)
});
assert_eq!(
editor.read_with(cx, |this, cx| { this.text(cx) }),
r#"
A regular expr$1 (shortened as regex banana regexp;1number also referred to as
rational expr$12number3number) is a sequence of characters that specifies a search
pattern in text. Usually such patterns are used by string-searching algorithms
for "find" or "find and replace" operations on strings, or for input validation.
"#
.unindent()
);
// Now with a whole-word twist.
search_bar
.update(cx, |search_bar, cx| {
search_bar.activate_search_mode(SearchMode::Regex, cx);
search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx)
})
.await
.unwrap();
search_bar.update(cx, |search_bar, cx| {
search_bar.replacement_editor.update(cx, |editor, cx| {
editor.set_text("things", cx);
});
search_bar.replace_all(&ReplaceAll, cx)
});
// The only word affected by this edit should be `algorithms`, even though there's a bunch
// of words in this text that would match this regex if not for WHOLE_WORD.
assert_eq!(
editor.read_with(cx, |this, cx| { this.text(cx) }),
r#"
A regular expr$1 (shortened as regex banana regexp;1number also referred to as
rational expr$12number3number) is a sequence of characters that specifies a search
pattern in text. Usually such patterns are used by string-searching things
for "find" or "find and replace" operations on strings, or for input validation.
"#
.unindent()
);
}
}

View File

@ -8,7 +8,9 @@ use gpui::{
pub use mode::SearchMode;
use project::search::SearchQuery;
pub use project_search::{ProjectSearchBar, ProjectSearchView};
use theme::components::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle};
use theme::components::{
action_button::Button, svg::Svg, ComponentExt, IconButtonStyle, ToggleIconButtonStyle,
};
pub mod buffer_search;
mod history;
@ -27,6 +29,7 @@ actions!(
CycleMode,
ToggleWholeWord,
ToggleCaseSensitive,
ToggleReplace,
SelectNextMatch,
SelectPrevMatch,
SelectAllMatches,
@ -34,7 +37,9 @@ actions!(
PreviousHistoryQuery,
ActivateTextMode,
ActivateSemanticMode,
ActivateRegexMode
ActivateRegexMode,
ReplaceAll,
ReplaceNext
]
);
@ -98,3 +103,32 @@ impl SearchOptions {
.into_any()
}
}
fn toggle_replace_button<V: View>(
active: bool,
tooltip_style: TooltipStyle,
button_style: ToggleIconButtonStyle,
) -> AnyElement<V> {
Button::dynamic_action(Box::new(ToggleReplace))
.with_tooltip("Toggle replace", tooltip_style)
.with_contents(theme::components::svg::Svg::new("icons/replace.svg"))
.toggleable(active)
.with_style(button_style)
.element()
.into_any()
}
fn replace_action<V: View>(
action: impl Action,
name: &'static str,
icon_path: &'static str,
tooltip_style: TooltipStyle,
button_style: IconButtonStyle,
) -> AnyElement<V> {
Button::dynamic_action(Box::new(action))
.with_tooltip(name, tooltip_style)
.with_contents(theme::components::svg::Svg::new(icon_path))
.with_style(button_style)
.element()
.into_any()
}

View File

@ -4,6 +4,12 @@ use gpui2::{
};
use std::{marker::PhantomData, rc::Rc};
mod icon_button;
mod tab;
pub(crate) use icon_button::{icon_button, ButtonVariant};
pub(crate) use tab::tab;
struct ButtonHandlers<V, D> {
click: Option<Rc<dyn Fn(&mut V, &D, &mut EventContext<V>)>>,
}

View File

@ -0,0 +1,50 @@
use crate::theme::theme;
use gpui2::elements::svg;
use gpui2::style::{StyleHelpers, Styleable};
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
#[derive(Element)]
pub(crate) struct IconButton {
path: &'static str,
variant: ButtonVariant,
}
#[derive(PartialEq)]
pub enum ButtonVariant {
Ghost,
Filled,
}
pub fn icon_button<V: 'static>(path: &'static str, variant: ButtonVariant) -> impl Element<V> {
IconButton { path, variant }
}
impl IconButton {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let mut div = div();
if self.variant == ButtonVariant::Filled {
div = div.fill(theme.highest.on.default.background);
}
div.w_7()
.h_6()
.flex()
.items_center()
.justify_center()
.rounded_md()
.hover()
.fill(theme.highest.base.hovered.background)
.active()
.fill(theme.highest.base.pressed.background)
.child(
svg()
.path(self.path)
.w_4()
.h_4()
.fill(theme.highest.variant.default.foreground),
)
}
}

View File

@ -0,0 +1,55 @@
use crate::theme::theme;
use gpui2::style::{StyleHelpers, Styleable};
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
#[derive(Element)]
pub(crate) struct Tab {
title: &'static str,
enabled: bool,
}
pub fn tab<V: 'static>(title: &'static str, enabled: bool) -> impl Element<V> {
Tab { title, enabled }
}
impl Tab {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
.px_2()
.py_0p5()
.flex()
.items_center()
.justify_center()
.rounded_lg()
.fill(if self.enabled {
theme.highest.on.default.background
} else {
theme.highest.base.default.background
})
.hover()
.fill(if self.enabled {
theme.highest.on.hovered.background
} else {
theme.highest.base.hovered.background
})
.active()
.fill(if self.enabled {
theme.highest.on.pressed.background
} else {
theme.highest.base.pressed.background
})
.child(
div()
.text_sm()
.text_color(if self.enabled {
theme.highest.base.default.foreground
} else {
theme.highest.variant.default.foreground
})
.child(self.title),
)
}
}

View File

@ -0,0 +1,3 @@
mod tab_bar;
pub(crate) use tab_bar::tab_bar;

View File

@ -0,0 +1,82 @@
use std::marker::PhantomData;
use crate::components::{icon_button, tab, ButtonVariant};
use crate::theme::theme;
use gpui2::elements::div::ScrollState;
use gpui2::style::StyleHelpers;
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
#[derive(Element)]
pub struct TabBar<V: 'static> {
view_type: PhantomData<V>,
scroll_state: ScrollState,
}
pub fn tab_bar<V: 'static>(scroll_state: ScrollState) -> TabBar<V> {
TabBar {
view_type: PhantomData,
scroll_state,
}
}
impl<V: 'static> TabBar<V> {
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
.w_full()
.flex()
// Left Side
.child(
div()
.px_1()
.flex()
.flex_none()
.gap_2()
// Nav Buttons
.child(
div()
.flex()
.items_center()
.gap_px()
.child(icon_button("icons/arrow_left.svg", ButtonVariant::Filled))
.child(icon_button("icons/arrow_right.svg", ButtonVariant::Ghost)),
),
)
.child(
div().w_0().flex_1().h_full().child(
div()
.flex()
.gap_px()
.overflow_x_scroll(self.scroll_state.clone())
.child(tab("Cargo.toml", false))
.child(tab("Channels Panel", true))
.child(tab("channels_panel.rs", false))
.child(tab("workspace.rs", false))
.child(tab("icon_button.rs", false))
.child(tab("storybook.rs", false))
.child(tab("theme.rs", false))
.child(tab("theme_registry.rs", false))
.child(tab("styleable_helpers.rs", false)),
),
)
// Right Side
.child(
div()
.px_1()
.flex()
.flex_none()
.gap_2()
// Nav Buttons
.child(
div()
.flex()
.items_center()
.gap_px()
.child(icon_button("icons/plus.svg", ButtonVariant::Ghost))
.child(icon_button("icons/split.svg", ButtonVariant::Ghost)),
),
)
}
}

View File

@ -12,6 +12,7 @@ use simplelog::SimpleLogger;
mod collab_panel;
mod components;
mod element_ext;
mod modules;
mod theme;
mod workspace;
@ -34,13 +35,13 @@ fn main() {
cx.add_window(
gpui2::WindowOptions {
bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(1400., 900.))),
bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(1600., 900.))),
center: true,
..Default::default()
},
|cx| {
view(|cx| {
cx.enable_inspector();
// cx.enable_inspector();
storybook(&mut ViewContext::new(cx))
})
},

View File

@ -1,4 +1,4 @@
use crate::{collab_panel::collab_panel, theme::theme};
use crate::{collab_panel::collab_panel, modules::tab_bar, theme::theme};
use gpui2::{
elements::{div, div::ScrollState, img, svg},
style::{StyleHelpers, Styleable},
@ -9,6 +9,7 @@ use gpui2::{
struct WorkspaceElement {
left_scroll_state: ScrollState,
right_scroll_state: ScrollState,
tab_bar_scroll_state: ScrollState,
}
pub fn workspace<V: 'static>() -> impl Element<V> {
@ -38,7 +39,19 @@ impl WorkspaceElement {
.flex_row()
.overflow_hidden()
.child(collab_panel(self.left_scroll_state.clone()))
.child(div().h_full().flex_1())
.child(
div()
.h_full()
.flex_1()
.fill(theme.highest.base.default.background)
.child(
div()
.flex()
.flex_col()
.flex_1()
.child(tab_bar(self.tab_bar_scroll_state.clone())),
),
)
.child(collab_panel(self.right_scroll_state.clone())),
)
.child(statusbar())

View File

@ -18,7 +18,7 @@ use gpui::{
ViewHandle, WeakViewHandle,
};
use language::Bias;
use project::{LocalWorktree, Project};
use project::{search::SearchQuery, LocalWorktree, Project};
use serde::Deserialize;
use smallvec::{smallvec, SmallVec};
use smol::Timer;
@ -26,6 +26,7 @@ use std::{
borrow::Cow,
ops::RangeInclusive,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use terminal::{
@ -380,10 +381,10 @@ impl TerminalView {
pub fn find_matches(
&mut self,
query: project::search::SearchQuery,
query: Arc<project::search::SearchQuery>,
cx: &mut ViewContext<Self>,
) -> Task<Vec<RangeInclusive<Point>>> {
let searcher = regex_search_for_query(query);
let searcher = regex_search_for_query(&query);
if let Some(searcher) = searcher {
self.terminal
@ -486,7 +487,7 @@ fn possible_open_targets(
.collect()
}
pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option<RegexSearch> {
pub fn regex_search_for_query(query: &project::search::SearchQuery) -> Option<RegexSearch> {
let query = query.as_str();
let searcher = RegexSearch::new(&query);
searcher.ok()
@ -798,6 +799,7 @@ impl SearchableItem for TerminalView {
case: false,
word: false,
regex: false,
replacement: false,
}
}
@ -851,10 +853,10 @@ impl SearchableItem for TerminalView {
/// Get all of the matches for this query, should be done on the background
fn find_matches(
&mut self,
query: project::search::SearchQuery,
query: Arc<project::search::SearchQuery>,
cx: &mut ViewContext<Self>,
) -> Task<Vec<Self::Match>> {
if let Some(searcher) = regex_search_for_query(query) {
if let Some(searcher) = regex_search_for_query(&query) {
self.terminal()
.update(cx, |term, cx| term.find_matches(searcher, cx))
} else {
@ -898,6 +900,9 @@ impl SearchableItem for TerminalView {
res
}
fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>) {
// Replacement is not supported in terminal view, so this is a no-op.
}
}
///Get's the working directory for the given workspace, respecting the user's settings.

View File

@ -3,7 +3,9 @@ mod theme_registry;
mod theme_settings;
pub mod ui;
use components::{action_button::ButtonStyle, disclosure::DisclosureStyle, ToggleIconButtonStyle};
use components::{
action_button::ButtonStyle, disclosure::DisclosureStyle, IconButtonStyle, ToggleIconButtonStyle,
};
use gpui::{
color::Color,
elements::{Border, ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle},
@ -440,9 +442,7 @@ pub struct Search {
pub include_exclude_editor: FindEditor,
pub invalid_include_exclude_editor: ContainerStyle,
pub include_exclude_inputs: ContainedText,
pub option_button: Toggleable<Interactive<IconButton>>,
pub option_button_component: ToggleIconButtonStyle,
pub action_button: Toggleable<Interactive<ContainedText>>,
pub match_background: Color,
pub match_index: ContainedText,
pub major_results_status: TextStyle,
@ -454,6 +454,10 @@ pub struct Search {
pub search_row_spacing: f32,
pub option_button_height: f32,
pub modes_container: ContainerStyle,
pub replace_icon: IconStyle,
// Used for filters and replace
pub option_button: Toggleable<Interactive<IconButton>>,
pub action_button: IconButtonStyle,
}
#[derive(Clone, Deserialize, Default, JsonSchema)]

View File

@ -1,4 +1,4 @@
use std::any::Any;
use std::{any::Any, sync::Arc};
use gpui::{
AnyViewHandle, AnyWeakViewHandle, AppContext, Subscription, Task, ViewContext, ViewHandle,
@ -25,6 +25,8 @@ pub struct SearchOptions {
pub case: bool,
pub word: bool,
pub regex: bool,
/// Specifies whether the item supports search & replace.
pub replacement: bool,
}
pub trait SearchableItem: Item {
@ -35,6 +37,7 @@ pub trait SearchableItem: Item {
case: true,
word: true,
regex: true,
replacement: true,
}
}
fn to_search_event(
@ -52,6 +55,7 @@ pub trait SearchableItem: Item {
cx: &mut ViewContext<Self>,
);
fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>);
fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>);
fn match_index_for_direction(
&mut self,
matches: &Vec<Self::Match>,
@ -74,7 +78,7 @@ pub trait SearchableItem: Item {
}
fn find_matches(
&mut self,
query: SearchQuery,
query: Arc<SearchQuery>,
cx: &mut ViewContext<Self>,
) -> Task<Vec<Self::Match>>;
fn active_match_index(
@ -103,6 +107,7 @@ pub trait SearchableItemHandle: ItemHandle {
cx: &mut WindowContext,
);
fn select_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut WindowContext);
fn replace(&self, _: &Box<dyn Any + Send>, _: &SearchQuery, _: &mut WindowContext);
fn match_index_for_direction(
&self,
matches: &Vec<Box<dyn Any + Send>>,
@ -113,7 +118,7 @@ pub trait SearchableItemHandle: ItemHandle {
) -> usize;
fn find_matches(
&self,
query: SearchQuery,
query: Arc<SearchQuery>,
cx: &mut WindowContext,
) -> Task<Vec<Box<dyn Any + Send>>>;
fn active_match_index(
@ -189,7 +194,7 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
}
fn find_matches(
&self,
query: SearchQuery,
query: Arc<SearchQuery>,
cx: &mut WindowContext,
) -> Task<Vec<Box<dyn Any + Send>>> {
let matches = self.update(cx, |this, cx| this.find_matches(query, cx));
@ -209,6 +214,11 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
let matches = downcast_matches(matches);
self.update(cx, |this, cx| this.active_match_index(matches, cx))
}
fn replace(&self, matches: &Box<dyn Any + Send>, query: &SearchQuery, cx: &mut WindowContext) {
let matches = matches.downcast_ref().unwrap();
self.update(cx, |this, cx| this.replace(matches, query, cx))
}
}
fn downcast_matches<T: Any + Clone>(matches: &Vec<Box<dyn Any + Send>>) -> Vec<T> {

View File

@ -30,9 +30,6 @@ export default function search(): any {
selection: theme.players[0],
text: text(theme.highest, "mono", "default"),
border: border(theme.highest),
margin: {
right: SEARCH_ROW_SPACING,
},
padding: {
top: 4,
bottom: 4,
@ -125,7 +122,7 @@ export default function search(): any {
button_width: 32,
background: background(theme.highest, "on"),
corner_radius: 2,
corner_radius: 6,
margin: { right: 2 },
border: {
width: 1,
@ -185,26 +182,6 @@ export default function search(): any {
},
},
}),
// Search tool buttons
// HACK: This is not how disabled elements should be created
// Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled
action_button: toggleable({
state: {
inactive: text_button({
variant: "ghost",
layer: theme.highest,
disabled: true,
margin: { right: SEARCH_ROW_SPACING },
text_properties: { size: "sm" },
}),
active: text_button({
variant: "ghost",
layer: theme.highest,
margin: { right: SEARCH_ROW_SPACING },
text_properties: { size: "sm" },
}),
},
}),
editor,
invalid_editor: {
...editor,
@ -218,6 +195,7 @@ export default function search(): any {
match_index: {
...text(theme.highest, "mono", { size: "sm" }),
padding: {
left: SEARCH_ROW_SPACING,
right: SEARCH_ROW_SPACING,
},
},
@ -398,6 +376,59 @@ export default function search(): any {
search_row_spacing: 8,
option_button_height: 22,
modes_container: {},
replace_icon: {
icon: {
color: foreground(theme.highest, "disabled"),
asset: "icons/replace.svg",
dimensions: {
width: 14,
height: 14,
},
},
container: {
margin: { right: 4 },
padding: { left: 1, right: 1 },
},
},
action_button: interactive({
base: {
icon_size: 14,
color: foreground(theme.highest, "variant"),
button_width: 32,
background: background(theme.highest, "on"),
corner_radius: 6,
margin: { right: 2 },
border: {
width: 1,
color: background(theme.highest, "on"),
},
padding: {
left: 4,
right: 4,
top: 4,
bottom: 4,
},
},
state: {
hovered: {
...text(theme.highest, "mono", "variant", "hovered"),
background: background(theme.highest, "on", "hovered"),
border: {
width: 1,
color: background(theme.highest, "on", "hovered"),
},
},
clicked: {
...text(theme.highest, "mono", "variant", "pressed"),
background: background(theme.highest, "on", "pressed"),
border: {
width: 1,
color: background(theme.highest, "on", "pressed"),
},
},
},
}),
...search_results(),
}
}