logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled

Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree
with a 3-file patch applied (no upstream history).

Patch (crates/gpui_linux/src/linux/wayland/):
  - serial.rs: add SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; add latest_serial_of()
  - window.rs: activate() uses keyboard-enter serial (Mutter focus gate)

Mutter honors window activation only when the token carries the keyboard-
focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial.
See docs/tray-window-focus-wayland.md in logiguard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 02:22:17 +03:30
commit b72a46db68
3984 changed files with 1583326 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
[package]
name = "edit_prediction_ui"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/edit_prediction_ui.rs"
doctest = false
[dependencies]
anyhow.workspace = true
buffer_diff.workspace = true
collections.workspace = true
time.workspace = true
client.workspace = true
cloud_llm_client.workspace = true
codestral.workspace = true
command_palette_hooks.workspace = true
copilot.workspace = true
copilot_chat.workspace = true
copilot_ui.workspace = true
edit_prediction_types.workspace = true
edit_prediction.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
indoc.workspace = true
language.workspace = true
markdown.workspace = true
menu.workspace = true
multi_buffer.workspace = true
paths.workspace = true
project.workspace = true
regex.workspace = true
settings.workspace = true
telemetry.workspace = true
text.workspace = true
theme_settings.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
zed_actions.workspace = true
zeta_prompt.workspace = true
[dev-dependencies]
copilot = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
futures.workspace = true
indoc.workspace = true
project = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,417 @@
use std::{
any::TypeId,
collections::VecDeque,
ops::Add,
sync::Arc,
time::{Duration, Instant},
};
use anyhow::Result;
use client::{Client, UserStore};
use editor::{
Editor, PathKey,
display_map::{BlockPlacement, BlockProperties, BlockStyle},
};
use futures::StreamExt as _;
use gpui::{
Animation, AnimationExt, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle,
Focusable, InteractiveElement as _, IntoElement as _, ParentElement as _, SharedString,
Styled as _, Task, TextAlign, Window, actions, div, pulsating_between,
};
use multi_buffer::{Anchor, MultiBuffer};
use project::Project;
use text::Point;
use ui::{
ButtonCommon, Clickable, Disableable, FluentBuilder as _, IconButton, IconName,
StyledTypography as _, h_flex, v_flex,
};
use edit_prediction::{
ContextRetrievalFinishedDebugEvent, ContextRetrievalStartedDebugEvent, DebugEvent,
EditPredictionStore,
};
use workspace::Item;
pub struct EditPredictionContextView {
empty_focus_handle: FocusHandle,
project: Entity<Project>,
store: Entity<EditPredictionStore>,
runs: VecDeque<RetrievalRun>,
current_ix: usize,
_update_task: Task<Result<()>>,
}
#[derive(Debug)]
struct RetrievalRun {
editor: Entity<Editor>,
started_at: Instant,
metadata: Vec<(&'static str, SharedString)>,
finished_at: Option<Instant>,
}
actions!(
dev,
[
/// Go to the previous context retrieval run
EditPredictionContextGoBack,
/// Go to the next context retrieval run
EditPredictionContextGoForward
]
);
impl EditPredictionContextView {
pub fn new(
project: Entity<Project>,
client: &Arc<Client>,
user_store: &Entity<UserStore>,
window: &mut gpui::Window,
cx: &mut Context<Self>,
) -> Self {
let store = EditPredictionStore::global(client, user_store, cx);
let mut debug_rx = store.update(cx, |store, cx| store.debug_info(&project, cx));
let _update_task = cx.spawn_in(window, async move |this, cx| {
while let Some(event) = debug_rx.next().await {
this.update_in(cx, |this, window, cx| {
this.handle_store_event(event, window, cx)
})?;
}
Ok(())
});
Self {
empty_focus_handle: cx.focus_handle(),
project,
runs: VecDeque::new(),
current_ix: 0,
store,
_update_task,
}
}
fn handle_store_event(
&mut self,
event: DebugEvent,
window: &mut gpui::Window,
cx: &mut Context<Self>,
) {
match event {
DebugEvent::ContextRetrievalStarted(info) => {
if info.project_entity_id == self.project.entity_id() {
self.handle_context_retrieval_started(info, window, cx);
}
}
DebugEvent::ContextRetrievalFinished(info) => {
if info.project_entity_id == self.project.entity_id() {
self.handle_context_retrieval_finished(info, window, cx);
}
}
DebugEvent::EditPredictionStarted(_) => {}
DebugEvent::EditPredictionFinished(_) => {}
}
}
fn handle_context_retrieval_started(
&mut self,
info: ContextRetrievalStartedDebugEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self
.runs
.back()
.is_some_and(|run| run.finished_at.is_none())
{
self.runs.pop_back();
}
let multibuffer = cx.new(|_| MultiBuffer::new(language::Capability::ReadOnly));
let editor = cx
.new(|cx| Editor::for_multibuffer(multibuffer, Some(self.project.clone()), window, cx));
if self.runs.len() == 32 {
self.runs.pop_front();
}
self.runs.push_back(RetrievalRun {
editor,
started_at: info.timestamp,
finished_at: None,
metadata: Vec::new(),
});
cx.notify();
}
fn handle_context_retrieval_finished(
&mut self,
info: ContextRetrievalFinishedDebugEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(run) = self.runs.back_mut() else {
return;
};
run.finished_at = Some(info.timestamp);
run.metadata = info.metadata;
let related_files = self.store.update(cx, |store, cx| {
store.context_for_project_with_buffers(&self.project, cx)
});
let editor = run.editor.clone();
let multibuffer = run.editor.read(cx).buffer().clone();
if self.current_ix + 2 == self.runs.len() {
self.current_ix += 1;
}
cx.spawn_in(window, async move |this, cx| {
let mut paths: Vec<(PathKey, _, Vec<_>, Vec<usize>, usize)> = Vec::new();
for (related_file, buffer) in related_files {
let orders = related_file
.excerpts
.iter()
.map(|excerpt| excerpt.order)
.collect::<Vec<_>>();
let min_order = orders.iter().copied().min().unwrap_or(usize::MAX);
let point_ranges = related_file
.excerpts
.iter()
.map(|excerpt| {
Point::new(excerpt.row_range.start, 0)..Point::new(excerpt.row_range.end, 0)
})
.collect::<Vec<_>>();
cx.update(|_, cx| {
let path = if let Some(file) = buffer.read(cx).file() {
PathKey::with_sort_prefix(min_order as u64, file.path().clone())
} else {
PathKey::for_buffer(&buffer, cx)
};
paths.push((path, buffer, point_ranges, orders, min_order));
})?;
}
paths.sort_by_key(|(_, _, _, _, min_order)| *min_order);
let mut excerpt_anchors_with_orders: Vec<(Anchor, usize)> = Vec::new();
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.clear(cx);
for (path, buffer, ranges, orders, _) in paths {
multibuffer.set_excerpts_for_path(path, buffer.clone(), ranges.clone(), 0, cx);
let snapshot = multibuffer.snapshot(cx);
let buffer_snapshot = buffer.read(cx).snapshot();
for (range, order) in ranges.into_iter().zip(orders) {
let text_anchor = buffer_snapshot.anchor_range_inside(range);
if let Some(start) = snapshot.anchor_in_buffer(text_anchor.start) {
excerpt_anchors_with_orders.push((start, order));
}
}
}
});
editor.update_in(cx, |editor, window, cx| {
let blocks = excerpt_anchors_with_orders
.into_iter()
.map(|(anchor, order)| {
let label = SharedString::from(format!("order: {order}"));
BlockProperties {
placement: BlockPlacement::Above(anchor),
height: Some(1),
style: BlockStyle::Sticky,
render: Arc::new(move |cx| {
div()
.pl(cx.anchor_x)
.text_ui_xs(cx)
.text_color(cx.editor_style.status.info)
.child(label.clone())
.into_any_element()
}),
priority: 0,
}
})
.collect::<Vec<_>>();
editor.insert_blocks(blocks, None, cx);
editor.move_to_beginning(&Default::default(), window, cx);
})?;
this.update(cx, |_, cx| cx.notify())
})
.detach();
}
fn handle_go_back(
&mut self,
_: &EditPredictionContextGoBack,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.current_ix = self.current_ix.saturating_sub(1);
cx.focus_self(window);
cx.notify();
}
fn handle_go_forward(
&mut self,
_: &EditPredictionContextGoForward,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.current_ix = self
.current_ix
.add(1)
.min(self.runs.len().saturating_sub(1));
cx.focus_self(window);
cx.notify();
}
fn render_informational_footer(
&self,
cx: &mut Context<'_, EditPredictionContextView>,
) -> ui::Div {
let run = &self.runs[self.current_ix];
let new_run_started = self
.runs
.back()
.map_or(false, |latest_run| latest_run.finished_at.is_none());
h_flex()
.p_2()
.w_full()
.font_buffer(cx)
.text_xs()
.border_t_1()
.gap_2()
.child(v_flex().h_full().flex_1().child({
let t0 = run.started_at;
let mut table = ui::Table::new(2).width(ui::px(300.)).no_ui_font();
for (key, value) in &run.metadata {
table = table.row(vec![
key.into_any_element(),
value.clone().into_any_element(),
])
}
table = table.row(vec![
"Total Time".into_any_element(),
format!("{} ms", (run.finished_at.unwrap_or(t0) - t0).as_millis())
.into_any_element(),
]);
table
}))
.child(
v_flex().h_full().text_align(TextAlign::Right).child(
h_flex()
.justify_end()
.child(
IconButton::new("go-back", IconName::ChevronLeft)
.disabled(self.current_ix == 0 || self.runs.len() < 2)
.tooltip(ui::Tooltip::for_action_title(
"Go to previous run",
&EditPredictionContextGoBack,
))
.on_click(cx.listener(|this, _, window, cx| {
this.handle_go_back(&EditPredictionContextGoBack, window, cx);
})),
)
.child(
div()
.child(format!("{}/{}", self.current_ix + 1, self.runs.len()))
.map(|this| {
if new_run_started {
this.with_animation(
"pulsating-count",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.opacity(delta),
)
.into_any_element()
} else {
this.into_any_element()
}
}),
)
.child(
IconButton::new("go-forward", IconName::ChevronRight)
.disabled(self.current_ix + 1 == self.runs.len())
.tooltip(ui::Tooltip::for_action_title(
"Go to next run",
&EditPredictionContextGoBack,
))
.on_click(cx.listener(|this, _, window, cx| {
this.handle_go_forward(
&EditPredictionContextGoForward,
window,
cx,
);
})),
),
),
)
}
}
impl Focusable for EditPredictionContextView {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.runs
.get(self.current_ix)
.map(|run| run.editor.read(cx).focus_handle(cx))
.unwrap_or_else(|| self.empty_focus_handle.clone())
}
}
impl EventEmitter<()> for EditPredictionContextView {}
impl Item for EditPredictionContextView {
type Event = ();
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
"Edit Prediction Context".into()
}
fn buffer_kind(&self, _cx: &App) -> workspace::item::ItemBufferKind {
workspace::item::ItemBufferKind::Multibuffer
}
fn act_as_type<'a>(
&'a self,
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<gpui::AnyEntity> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.clone().into())
} else if type_id == TypeId::of::<Editor>() {
Some(self.runs.get(self.current_ix)?.editor.clone().into())
} else {
None
}
}
}
impl gpui::Render for EditPredictionContextView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
v_flex()
.key_context("EditPredictionContext")
.on_action(cx.listener(Self::handle_go_back))
.on_action(cx.listener(Self::handle_go_forward))
.size_full()
.map(|this| {
if self.runs.is_empty() {
this.child(
v_flex()
.size_full()
.justify_center()
.items_center()
.child("No retrieval runs yet"),
)
} else {
this.child(self.runs[self.current_ix].editor.clone())
.child(self.render_informational_footer(cx))
}
})
}
}

View File

@@ -0,0 +1,198 @@
mod edit_prediction_button;
mod edit_prediction_context_view;
mod rate_prediction_modal;
use command_palette_hooks::CommandPaletteFilter;
use edit_prediction::{EditPredictionStore, ResetOnboarding, capture_example};
use edit_prediction_context_view::EditPredictionContextView;
use editor::Editor;
use feature_flags::FeatureFlagAppExt as _;
use gpui::TaskExt;
use gpui::actions;
use language::language_settings::AllLanguageSettings;
use project::DisableAiSettings;
use rate_prediction_modal::RatePredictionsModal;
use settings::{Settings as _, SettingsStore};
use std::any::{Any as _, TypeId};
use ui::{App, prelude::*};
use workspace::{SplitDirection, Workspace};
pub use edit_prediction_button::{
EditPredictionButton, ToggleMenu, get_available_providers, set_completion_provider,
};
use crate::rate_prediction_modal::PredictEditsRatePredictionsFeatureFlag;
actions!(
dev,
[
/// Opens the edit prediction context view.
OpenEditPredictionContextView,
]
);
actions!(
edit_prediction,
[
/// Opens the rate completions modal.
RatePredictions,
/// Captures an ExampleSpec from the current editing session and opens it as Markdown.
CaptureExample,
]
);
pub fn init(cx: &mut App) {
feature_gate_predict_edits_actions(cx);
cx.observe_new(move |workspace: &mut Workspace, _, _cx| {
workspace.register_action(|workspace, _: &RatePredictions, window, cx| {
if cx.has_flag::<PredictEditsRatePredictionsFeatureFlag>() {
RatePredictionsModal::toggle(workspace, window, cx);
}
});
workspace.register_action(|workspace, _: &CaptureExample, window, cx| {
capture_example_as_markdown(workspace, window, cx);
});
workspace.register_action_renderer(|div, _, _, cx| {
div.on_action(cx.listener(
move |workspace, _: &OpenEditPredictionContextView, window, cx| {
let project = workspace.project();
workspace.split_item(
SplitDirection::Right,
Box::new(cx.new(|cx| {
EditPredictionContextView::new(
project.clone(),
workspace.client(),
workspace.user_store(),
window,
cx,
)
})),
window,
cx,
);
},
))
});
})
.detach();
}
fn feature_gate_predict_edits_actions(cx: &mut App) {
let rate_completion_action_types = [TypeId::of::<RatePredictions>()];
let reset_onboarding_action_types = [TypeId::of::<ResetOnboarding>()];
let all_action_types = [
TypeId::of::<RatePredictions>(),
TypeId::of::<CaptureExample>(),
TypeId::of::<edit_prediction::ResetOnboarding>(),
zed_actions::OpenZedPredictOnboarding.type_id(),
TypeId::of::<edit_prediction::ClearHistory>(),
TypeId::of::<rate_prediction_modal::ThumbsUpActivePrediction>(),
TypeId::of::<rate_prediction_modal::ThumbsDownActivePrediction>(),
TypeId::of::<rate_prediction_modal::NextEdit>(),
TypeId::of::<rate_prediction_modal::PreviousEdit>(),
];
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_action_types(&rate_completion_action_types);
filter.hide_action_types(&reset_onboarding_action_types);
filter.hide_action_types(&[zed_actions::OpenZedPredictOnboarding.type_id()]);
});
cx.observe_global::<SettingsStore>(move |cx| {
let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
let has_feature_flag = cx.has_flag::<PredictEditsRatePredictionsFeatureFlag>();
CommandPaletteFilter::update_global(cx, |filter, _cx| {
if is_ai_disabled {
filter.hide_action_types(&all_action_types);
} else if has_feature_flag {
filter.show_action_types(&rate_completion_action_types);
} else {
filter.hide_action_types(&rate_completion_action_types);
}
});
})
.detach();
cx.observe_flag::<PredictEditsRatePredictionsFeatureFlag, _>(move |value, cx| {
if !DisableAiSettings::get_global(cx).disable_ai {
if *value {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.show_action_types(&rate_completion_action_types);
});
} else {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_action_types(&rate_completion_action_types);
});
}
}
})
.detach();
}
fn capture_example_as_markdown(
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Option<()> {
let markdown_language = workspace
.app_state()
.languages
.language_for_name("Markdown");
let fs = workspace.app_state().fs.clone();
let project = workspace.project().clone();
let editor = workspace.active_item_as::<Editor>(cx)?;
let editor = editor.read(cx);
let (buffer, cursor_anchor) = editor
.buffer()
.read(cx)
.text_anchor_for_position(editor.selections.newest_anchor().head(), cx)?;
let ep_store = EditPredictionStore::try_global(cx)?;
let events = ep_store.update(cx, |store, cx| store.edit_history_for_project(&project, cx));
let example = capture_example(project.clone(), buffer, cursor_anchor, events, true, cx)?;
let examples_dir = AllLanguageSettings::get_global(cx)
.edit_predictions
.examples_dir
.clone();
cx.spawn_in(window, async move |workspace_entity, cx| {
let markdown_language = markdown_language.await?;
let example_spec = example.await?;
let buffer = if let Some(dir) = examples_dir {
fs.create_dir(&dir).await.ok();
let mut path = dir.join(&example_spec.name.replace(' ', "--").replace(':', "-"));
path.set_extension("md");
project
.update(cx, |project, cx| project.open_local_buffer(&path, cx))
.await?
} else {
project
.update(cx, |project, cx| {
project.create_buffer(Some(markdown_language.clone()), false, cx)
})
.await?
};
buffer.update(cx, |buffer, cx| {
buffer.set_text(example_spec.to_markdown(), cx);
buffer.set_language(Some(markdown_language), cx);
});
workspace_entity.update_in(cx, |workspace, window, cx| {
workspace.add_item_to_active_pane(
Box::new(
cx.new(|cx| Editor::for_buffer(buffer, Some(project.clone()), window, cx)),
),
None,
true,
window,
cx,
);
})
})
.detach_and_log_err(cx);
None
}

File diff suppressed because it is too large Load Diff