logiguard fork v3: full patch set on verified 8c74db0 tree

Includes prior-session patches (carry forward so the app compiles):
  - crates/gpui/build.rs: cross-compile manifest fix
  - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method
  - crates/gpui/src/window.rs: Window::activate_with_token public API
  - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix

Plus the focus-serial fix:
  - serial.rs: SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; latest_serial_of()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 01:52:12 +03:30
commit b9819977a5
3984 changed files with 1487015 additions and 0 deletions

61
crates/search/Cargo.toml Normal file
View File

@@ -0,0 +1,61 @@
[package]
name = "search"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[features]
test-support = [
"editor/test-support",
"gpui/test-support",
"workspace/test-support",
]
[lints]
workspace = true
[lib]
path = "src/search.rs"
doctest = false
[dependencies]
anyhow.workspace = true
any_vec.workspace = true
bitflags.workspace = true
collections.workspace = true
editor.workspace = true
fs.workspace = true
futures-lite.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
menu.workspace = true
multi_buffer.workspace = true
project.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
theme.workspace = true
theme_settings.workspace = true
ui.workspace = true
util.workspace = true
util_macros.workspace = true
workspace.workspace = true
zed_actions.workspace = true
itertools.workspace = true
ztracing.workspace = true
tracing.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
lsp.workspace = true
pretty_assertions.workspace = true
unindent.workspace = true
workspace = { workspace = true, features = ["test-support"] }
[package.metadata.cargo-machete]
ignored = ["tracing"]

1
crates/search/LICENSE-GPL Symbolic link
View File

@@ -0,0 +1 @@
../../LICENSE-GPL

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,233 @@
use gpui::{Action, App, Context, Div, Entity, InteractiveElement, Window, div};
use workspace::{Pane, Workspace};
use crate::BufferSearchBar;
/// Registrar inverts the dependency between search and its downstream user, allowing said downstream user to register search action without knowing exactly what those actions are.
pub trait SearchActionsRegistrar {
fn register_handler<A: Action>(&mut self, callback: impl ActionExecutor<A>);
}
type SearchBarActionCallback<A> =
fn(&mut BufferSearchBar, &A, &mut Window, &mut Context<BufferSearchBar>);
type GetSearchBar<T> =
for<'a, 'b> fn(&'a T, &'a mut Window, &mut Context<'b, T>) -> Option<Entity<BufferSearchBar>>;
/// Registers search actions on a div that can be taken out.
pub struct DivRegistrar<'a, 'b, T: 'static> {
div: Option<Div>,
cx: &'a mut Context<'b, T>,
search_getter: GetSearchBar<T>,
}
impl<'a, 'b, T: 'static> DivRegistrar<'a, 'b, T> {
pub fn new(search_getter: GetSearchBar<T>, cx: &'a mut Context<'b, T>) -> Self {
Self {
div: Some(div()),
cx,
search_getter,
}
}
pub fn into_div(self) -> Div {
// This option is always Some; it's an option in the first place because we want to call methods
// on div that require ownership.
self.div.unwrap()
}
}
impl<T: 'static> SearchActionsRegistrar for DivRegistrar<'_, '_, T> {
fn register_handler<A: Action>(&mut self, callback: impl ActionExecutor<A>) {
let getter = self.search_getter;
self.div = self.div.take().map(|div| {
div.on_action(self.cx.listener(move |this, action, window, cx| {
let should_notify = (getter)(this, window, cx)
.map(|search_bar| {
search_bar.update(cx, |search_bar, cx| {
callback.execute(search_bar, action, window, cx)
})
})
.unwrap_or(false);
if should_notify {
cx.notify();
} else {
cx.propagate();
}
}))
});
}
}
pub struct PaneDivRegistrar {
div: Option<Div>,
pane: Entity<Pane>,
}
impl PaneDivRegistrar {
pub fn new(div: Div, pane: Entity<Pane>) -> Self {
Self {
div: Some(div),
pane,
}
}
pub fn into_div(self) -> Div {
self.div.unwrap()
}
}
impl SearchActionsRegistrar for PaneDivRegistrar {
fn register_handler<A: Action>(&mut self, callback: impl ActionExecutor<A>) {
let pane = self.pane.clone();
self.div = self.div.take().map(|div| {
div.on_action(move |action: &A, window: &mut Window, cx: &mut App| {
let search_bar = pane
.read(cx)
.toolbar()
.read(cx)
.item_of_type::<BufferSearchBar>();
let should_notify = search_bar
.map(|search_bar| {
search_bar.update(cx, |search_bar, cx| {
callback.execute(search_bar, action, window, cx)
})
})
.unwrap_or(false);
if should_notify {
pane.update(cx, |_, cx| cx.notify());
} else {
cx.propagate();
}
})
});
}
}
pub fn register_pane_search_actions(div: Div, pane: Entity<Pane>) -> Div {
let mut registrar = PaneDivRegistrar::new(div, pane);
BufferSearchBar::register(&mut registrar);
registrar.into_div()
}
/// Register actions for an active pane.
impl SearchActionsRegistrar for Workspace {
fn register_handler<A: Action>(&mut self, callback: impl ActionExecutor<A>) {
self.register_action(move |workspace, action: &A, window, cx| {
if workspace.has_active_modal(window, cx) && !workspace.hide_modal(window, cx) {
cx.propagate();
return;
}
let pane = workspace.active_pane();
let callback = callback.clone();
pane.update(cx, |this, cx| {
this.toolbar().update(cx, move |this, cx| {
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
let should_notify = search_bar.update(cx, move |search_bar, cx| {
callback.execute(search_bar, action, window, cx)
});
if should_notify {
cx.notify();
} else {
cx.propagate();
}
}
})
});
});
}
}
type DidHandleAction = bool;
/// Potentially executes the underlying action if some preconditions are met (e.g. buffer search bar is visible)
pub trait ActionExecutor<A: Action>: 'static + Clone {
fn execute(
&self,
search_bar: &mut BufferSearchBar,
action: &A,
window: &mut Window,
cx: &mut Context<BufferSearchBar>,
) -> DidHandleAction;
}
/// Run an action when the search bar has been dismissed from the panel.
pub struct ForDismissed<A>(pub(super) SearchBarActionCallback<A>);
impl<A> Clone for ForDismissed<A> {
fn clone(&self) -> Self {
Self(self.0)
}
}
impl<A: Action> ActionExecutor<A> for ForDismissed<A> {
fn execute(
&self,
search_bar: &mut BufferSearchBar,
action: &A,
window: &mut Window,
cx: &mut Context<BufferSearchBar>,
) -> DidHandleAction {
if search_bar.is_dismissed() {
self.0(search_bar, action, window, cx);
true
} else {
false
}
}
}
/// Run an action when the search bar is deployed.
pub struct ForDeployed<A>(pub(super) SearchBarActionCallback<A>);
impl<A> Clone for ForDeployed<A> {
fn clone(&self) -> Self {
Self(self.0)
}
}
impl<A: Action> ActionExecutor<A> for ForDeployed<A> {
fn execute(
&self,
search_bar: &mut BufferSearchBar,
action: &A,
window: &mut Window,
cx: &mut Context<BufferSearchBar>,
) -> DidHandleAction {
if search_bar.is_dismissed() || search_bar.active_searchable_item.is_none() {
false
} else {
self.0(search_bar, action, window, cx);
true
}
}
}
/// Run an action when the search bar has any matches or a pending external query,
/// regardless of whether it is visible or not.
pub struct WithResultsOrExternalQuery<A>(pub(super) SearchBarActionCallback<A>);
impl<A> Clone for WithResultsOrExternalQuery<A> {
fn clone(&self) -> Self {
Self(self.0)
}
}
impl<A: Action> ActionExecutor<A> for WithResultsOrExternalQuery<A> {
fn execute(
&self,
search_bar: &mut BufferSearchBar,
action: &A,
window: &mut Window,
cx: &mut Context<BufferSearchBar>,
) -> DidHandleAction {
#[cfg(not(target_os = "macos"))]
let has_external_query = false;
#[cfg(target_os = "macos")]
let has_external_query = search_bar.pending_external_query.is_some();
if has_external_query || search_bar.active_match_index.is_some() {
self.0(search_bar, action, window, cx);
true
} else {
false
}
}
}

File diff suppressed because it is too large Load Diff

204
crates/search/src/search.rs Normal file
View File

@@ -0,0 +1,204 @@
use bitflags::bitflags;
pub use buffer_search::BufferSearchBar;
pub use editor::HighlightKey;
use editor::SearchSettings;
use gpui::{Action, App, ClickEvent, FocusHandle, IntoElement, actions};
use project::search::SearchQuery;
pub use project_search::ProjectSearchView;
use ui::{ButtonStyle, IconButton, IconButtonShape};
use ui::{Tooltip, prelude::*};
use workspace::notifications::NotificationId;
use workspace::{Toast, Workspace};
pub use zed_actions::search::ToggleIncludeIgnored;
pub use search_status_button::SEARCH_ICON;
use crate::project_search::ProjectSearchBar;
pub mod buffer_search;
pub mod project_search;
pub(crate) mod search_bar;
pub mod search_status_button;
pub fn init(cx: &mut App) {
menu::init();
buffer_search::init(cx);
project_search::init(cx);
}
actions!(
search,
[
/// Focuses on the search input field.
FocusSearch,
/// Toggles whole word matching.
ToggleWholeWord,
/// Toggles case-sensitive search.
ToggleCaseSensitive,
/// Toggles regular expression mode.
ToggleRegex,
/// Toggles the replace interface.
ToggleReplace,
/// Toggles searching within selection only.
ToggleSelection,
/// Selects the next search match.
SelectNextMatch,
/// Selects the previous search match.
SelectPreviousMatch,
/// Selects all search matches.
SelectAllMatches,
/// Cycles through search modes.
CycleMode,
/// Navigates to the next query in search history.
NextHistoryQuery,
/// Navigates to the previous query in search history.
PreviousHistoryQuery,
/// Replaces all matches.
ReplaceAll,
/// Replaces the next match.
ReplaceNext,
]
);
bitflags! {
#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
pub struct SearchOptions: u8 {
const NONE = 0;
const WHOLE_WORD = 1 << SearchOption::WholeWord as u8;
const CASE_SENSITIVE = 1 << SearchOption::CaseSensitive as u8;
const INCLUDE_IGNORED = 1 << SearchOption::IncludeIgnored as u8;
const REGEX = 1 << SearchOption::Regex as u8;
const ONE_MATCH_PER_LINE = 1 << SearchOption::OneMatchPerLine as u8;
/// If set, reverse direction when finding the active match
const BACKWARDS = 1 << SearchOption::Backwards as u8;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum SearchOption {
WholeWord = 0,
CaseSensitive,
IncludeIgnored,
Regex,
OneMatchPerLine,
Backwards,
}
pub enum SearchSource<'a, 'b> {
Buffer,
Project(&'a Context<'b, ProjectSearchBar>),
}
impl SearchOption {
pub fn as_options(&self) -> SearchOptions {
SearchOptions::from_bits(1 << *self as u8).unwrap()
}
pub fn label(&self) -> &'static str {
match self {
SearchOption::WholeWord => "Match Whole Words",
SearchOption::CaseSensitive => "Match Case Sensitivity",
SearchOption::IncludeIgnored => "Also search files ignored by configuration",
SearchOption::Regex => "Use Regular Expressions",
SearchOption::OneMatchPerLine => "One Match Per Line",
SearchOption::Backwards => "Search Backwards",
}
}
pub fn icon(&self) -> ui::IconName {
match self {
SearchOption::WholeWord => ui::IconName::WholeWord,
SearchOption::CaseSensitive => ui::IconName::CaseSensitive,
SearchOption::IncludeIgnored => ui::IconName::Sliders,
SearchOption::Regex => ui::IconName::Regex,
_ => panic!("{self:?} is not a named SearchOption"),
}
}
pub fn to_toggle_action(self) -> &'static dyn Action {
match self {
SearchOption::WholeWord => &ToggleWholeWord,
SearchOption::CaseSensitive => &ToggleCaseSensitive,
SearchOption::IncludeIgnored => &ToggleIncludeIgnored,
SearchOption::Regex => &ToggleRegex,
_ => panic!("{self:?} is not a toggle action"),
}
}
pub fn as_button(
&self,
active: SearchOptions,
search_source: SearchSource,
focus_handle: FocusHandle,
) -> impl IntoElement {
let action = self.to_toggle_action();
let label = self.label();
IconButton::new(
(label, matches!(search_source, SearchSource::Buffer) as u32),
self.icon(),
)
.map(|button| match search_source {
SearchSource::Buffer => {
let focus_handle = focus_handle.clone();
button.on_click(move |_: &ClickEvent, window, cx| {
if !focus_handle.is_focused(window) {
window.focus(&focus_handle, cx);
}
window.dispatch_action(action.boxed_clone(), cx);
})
}
SearchSource::Project(cx) => {
let options = self.as_options();
button.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
this.toggle_search_option(options, window, cx);
}))
}
})
.style(ButtonStyle::Subtle)
.shape(IconButtonShape::Square)
.toggle_state(active.contains(self.as_options()))
.tooltip(move |_window, cx| Tooltip::for_action_in(label, action, &focus_handle, cx))
}
}
impl SearchOptions {
pub fn none() -> SearchOptions {
SearchOptions::NONE
}
pub fn from_query(query: &SearchQuery) -> SearchOptions {
let mut options = SearchOptions::NONE;
options.set(SearchOptions::WHOLE_WORD, query.whole_word());
options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
options.set(SearchOptions::INCLUDE_IGNORED, query.include_ignored());
options.set(SearchOptions::REGEX, query.is_regex());
options
}
pub fn from_settings(settings: &SearchSettings) -> SearchOptions {
let mut options = SearchOptions::NONE;
options.set(SearchOptions::WHOLE_WORD, settings.whole_word);
options.set(SearchOptions::CASE_SENSITIVE, settings.case_sensitive);
options.set(SearchOptions::INCLUDE_IGNORED, settings.include_ignored);
options.set(SearchOptions::REGEX, settings.regex);
options
}
}
pub(crate) fn show_no_more_matches(window: &mut Window, cx: &mut App) {
window.defer(cx, |window, cx| {
struct NotifType();
let notification_id = NotificationId::unique::<NotifType>();
let Some(workspace) = Workspace::for_window(window, cx) else {
return;
};
workspace.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(notification_id.clone(), "No more matches").autohide(),
cx,
);
})
});
}

View File

@@ -0,0 +1,139 @@
use editor::{Editor, EditorElement, EditorStyle, MultiBufferOffset, ToOffset};
use gpui::{Action, App, Entity, FocusHandle, Hsla, IntoElement, TextStyle};
use settings::Settings;
use theme_settings::ThemeSettings;
use ui::{IconButton, IconButtonShape};
use ui::{Tooltip, prelude::*};
pub(super) enum HistoryNavigationDirection {
Previous,
Next,
}
pub(super) fn should_navigate_history(
editor: &Entity<Editor>,
direction: HistoryNavigationDirection,
cx: &App,
) -> bool {
let editor_ref = editor.read(cx);
let snapshot = editor_ref.buffer().read(cx).snapshot(cx);
if snapshot.max_point().row == 0 {
return true;
}
let selections = editor_ref.selections.disjoint_anchors();
if let [selection] = selections {
let offset = selection.end.to_offset(&snapshot);
match direction {
HistoryNavigationDirection::Previous => offset == MultiBufferOffset(0),
HistoryNavigationDirection::Next => offset == snapshot.len(),
}
} else {
true
}
}
pub(super) enum ActionButtonState {
Disabled,
Toggled,
}
pub(super) fn render_action_button(
id_prefix: &'static str,
icon: ui::IconName,
button_state: Option<ActionButtonState>,
tooltip: &'static str,
action: &'static dyn Action,
focus_handle: FocusHandle,
) -> impl IntoElement {
IconButton::new(
SharedString::from(format!("{id_prefix}-{}", action.name())),
icon,
)
.shape(IconButtonShape::Square)
.on_click({
let focus_handle = focus_handle.clone();
move |_, window, cx| {
if !focus_handle.is_focused(window) {
window.focus(&focus_handle, cx);
}
window.dispatch_action(action.boxed_clone(), cx);
}
})
.tooltip(move |_window, cx| Tooltip::for_action_in(tooltip, action, &focus_handle, cx))
.when_some(button_state, |this, state| match state {
ActionButtonState::Toggled => this.toggle_state(true),
ActionButtonState::Disabled => this.disabled(true),
})
}
pub(crate) fn input_base_styles(border_color: Hsla, map: impl FnOnce(Div) -> Div) -> Div {
h_flex()
.map(map)
.min_w_32()
.min_h_8()
.pl_2()
.pr_1()
.border_1()
.border_color(border_color)
.rounded_md()
}
pub(crate) fn filter_search_results_input(
border_color: Hsla,
map: impl FnOnce(Div) -> Div,
cx: &App,
) -> Div {
input_base_styles(border_color, map).pl_0().child(
h_flex()
.mr_2()
.px_2()
.h_full()
.border_r_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().text_accent.opacity(0.05))
.child(Label::new("Find in Results").color(Color::Muted)),
)
}
pub(crate) fn render_text_input(
editor: &Entity<Editor>,
color_override: Option<Color>,
app: &App,
) -> impl IntoElement {
let (color, use_syntax) = if editor.read(app).read_only(app) {
(app.theme().colors().text_disabled, false)
} else {
match color_override {
Some(color_override) => (color_override.color(app), false),
None => (app.theme().colors().text, true),
}
};
let settings = ThemeSettings::get_global(app);
let text_style = TextStyle {
color,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_fallbacks: settings.buffer_font.fallbacks.clone(),
font_size: rems(0.875).into(),
font_weight: settings.buffer_font.weight,
line_height: relative(1.3),
..TextStyle::default()
};
let mut editor_style = EditorStyle {
background: app.theme().colors().toolbar_background,
local_player: app.theme().players().local(),
text: text_style,
..EditorStyle::default()
};
if use_syntax {
editor_style.syntax = app.theme().syntax().clone();
}
EditorElement::new(editor, editor_style)
}
/// This element makes all search inputs align as if they were in the same column
pub(crate) fn alignment_element() -> Div {
div().size_5().flex_none().ml_0p5()
}

View File

@@ -0,0 +1,71 @@
use editor::EditorSettings;
use gpui::{App, FocusHandle};
use settings::Settings as _;
use ui::{ButtonCommon, Clickable, Context, Render, Tooltip, Window, prelude::*};
use workspace::{HideStatusItem, ItemHandle, StatusItemView};
pub const SEARCH_ICON: IconName = IconName::MagnifyingGlass;
pub struct SearchButton {
pane_item_focus_handle: Option<FocusHandle>,
}
impl SearchButton {
pub fn new() -> Self {
Self {
pane_item_focus_handle: None,
}
}
}
impl Render for SearchButton {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
let button = div();
if !EditorSettings::get_global(cx).search.button {
return button.hidden();
}
let focus_handle = self.pane_item_focus_handle.clone();
button.child(
IconButton::new("project-search-indicator", SEARCH_ICON)
.icon_size(IconSize::Small)
.tooltip(move |_window, cx| {
if let Some(focus_handle) = &focus_handle {
Tooltip::for_action_in(
"Project Search",
&workspace::DeploySearch::default(),
focus_handle,
cx,
)
} else {
Tooltip::for_action(
"Project Search",
&workspace::DeploySearch::default(),
cx,
)
}
})
.on_click(cx.listener(|_this, _, window, cx| {
window.dispatch_action(Box::new(workspace::DeploySearch::default()), cx);
})),
)
}
}
impl StatusItemView for SearchButton {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.pane_item_focus_handle = active_pane_item.map(|item| item.item_focus_handle(cx));
}
fn hide_setting(&self, _: &App) -> Option<HideStatusItem> {
Some(HideStatusItem::new(|settings| {
settings.editor.search.get_or_insert_default().button = Some(false);
}))
}
}