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:
25
crates/encoding_selector/Cargo.toml
Normal file
25
crates/encoding_selector/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "encoding_selector"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/encoding_selector.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
editor.workspace = true
|
||||
encoding_rs.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
1
crates/encoding_selector/LICENSE-GPL
Symbolic link
1
crates/encoding_selector/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
143
crates/encoding_selector/src/active_buffer_encoding.rs
Normal file
143
crates/encoding_selector/src/active_buffer_encoding.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
use crate::{EncodingSelector, Toggle};
|
||||
|
||||
use editor::Editor;
|
||||
use encoding_rs::{Encoding, UTF_8};
|
||||
use gpui::{
|
||||
App, Context, Entity, IntoElement, ParentElement, Render, Styled, Subscription, WeakEntity,
|
||||
Window, div,
|
||||
};
|
||||
use project::Project;
|
||||
use ui::{Button, ButtonCommon, Clickable, LabelSize, Tooltip};
|
||||
use workspace::{
|
||||
EncodingDisplayOptions, HideStatusItem, StatusBarSettings, StatusItemView, Workspace,
|
||||
item::{ItemHandle, Settings},
|
||||
};
|
||||
|
||||
pub struct ActiveBufferEncoding {
|
||||
active_encoding: Option<&'static Encoding>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
_observe_active_editor: Option<Subscription>,
|
||||
has_bom: bool,
|
||||
is_dirty: bool,
|
||||
is_shared: bool,
|
||||
is_via_remote_server: bool,
|
||||
}
|
||||
|
||||
impl ActiveBufferEncoding {
|
||||
pub fn new(workspace: &Workspace) -> Self {
|
||||
Self {
|
||||
active_encoding: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
project: workspace.project().clone(),
|
||||
_observe_active_editor: None,
|
||||
has_bom: false,
|
||||
is_dirty: false,
|
||||
is_shared: false,
|
||||
is_via_remote_server: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_encoding(&mut self, editor: Entity<Editor>, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.active_encoding = None;
|
||||
self.has_bom = false;
|
||||
self.is_dirty = false;
|
||||
|
||||
let project = self.project.read(cx);
|
||||
self.is_shared = project.is_shared();
|
||||
self.is_via_remote_server = project.is_via_remote_server();
|
||||
|
||||
if let Some(buffer) = editor.read(cx).active_buffer(cx) {
|
||||
let buffer = buffer.read(cx);
|
||||
self.active_encoding = Some(buffer.encoding());
|
||||
self.has_bom = buffer.has_bom();
|
||||
self.is_dirty = buffer.is_dirty();
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ActiveBufferEncoding {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let Some(active_encoding) = self.active_encoding else {
|
||||
return div().hidden();
|
||||
};
|
||||
|
||||
let display_option = StatusBarSettings::get_global(cx).active_encoding_button;
|
||||
let is_utf8 = active_encoding == UTF_8;
|
||||
if !display_option.should_show(is_utf8, self.has_bom) {
|
||||
return div().hidden();
|
||||
}
|
||||
|
||||
let mut text = active_encoding.name().to_string();
|
||||
if self.has_bom {
|
||||
text.push_str(" (BOM)");
|
||||
}
|
||||
|
||||
let (disabled, tooltip_text) = if self.is_dirty {
|
||||
(true, "Save file to change encoding")
|
||||
} else if self.is_shared {
|
||||
(true, "Cannot change encoding during collaboration")
|
||||
} else if self.is_via_remote_server {
|
||||
(true, "Cannot change encoding of remote server file")
|
||||
} else {
|
||||
(false, "Reopen with Encoding")
|
||||
};
|
||||
|
||||
div().child(
|
||||
Button::new("change-encoding", text)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
if disabled {
|
||||
return;
|
||||
}
|
||||
if let Some(workspace) = this.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
EncodingSelector::toggle(workspace, window, cx)
|
||||
});
|
||||
}
|
||||
}))
|
||||
.tooltip(move |_window, cx| {
|
||||
if disabled {
|
||||
Tooltip::text(tooltip_text)(_window, cx)
|
||||
} else {
|
||||
Tooltip::for_action(tooltip_text, &Toggle, cx)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for ActiveBufferEncoding {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
|
||||
self._observe_active_editor =
|
||||
Some(cx.observe_in(&editor, window, Self::update_encoding));
|
||||
self.update_encoding(editor, window, cx);
|
||||
} else {
|
||||
self.active_encoding = None;
|
||||
self.has_bom = false;
|
||||
self.is_dirty = false;
|
||||
self.is_shared = false;
|
||||
self.is_via_remote_server = false;
|
||||
self._observe_active_editor = None;
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn hide_setting(&self, _: &App) -> Option<HideStatusItem> {
|
||||
Some(HideStatusItem::new(|settings| {
|
||||
settings
|
||||
.status_bar
|
||||
.get_or_insert_default()
|
||||
.active_encoding_button = Some(EncodingDisplayOptions::Disabled);
|
||||
}))
|
||||
}
|
||||
}
|
||||
327
crates/encoding_selector/src/encoding_selector.rs
Normal file
327
crates/encoding_selector/src/encoding_selector.rs
Normal file
@@ -0,0 +1,327 @@
|
||||
mod active_buffer_encoding;
|
||||
pub use active_buffer_encoding::ActiveBufferEncoding;
|
||||
|
||||
use editor::Editor;
|
||||
use encoding_rs::Encoding;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
App, AppContext, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, ParentElement, Render, Styled, Task, WeakEntity, Window, actions,
|
||||
};
|
||||
use language::Buffer;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
use ui::{HighlightedLabel, ListItem, ListItemSpacing, Toggleable, v_flex};
|
||||
use util::ResultExt;
|
||||
use workspace::{ModalView, Toast, Workspace, notifications::NotificationId};
|
||||
|
||||
actions!(
|
||||
encoding_selector,
|
||||
[
|
||||
/// Toggles the encoding selector modal.
|
||||
Toggle
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.observe_new(EncodingSelector::register).detach();
|
||||
}
|
||||
|
||||
pub struct EncodingSelector {
|
||||
picker: Entity<Picker<EncodingSelectorDelegate>>,
|
||||
}
|
||||
|
||||
impl EncodingSelector {
|
||||
fn register(
|
||||
workspace: &mut Workspace,
|
||||
_window: Option<&mut Window>,
|
||||
_: &mut Context<Workspace>,
|
||||
) {
|
||||
workspace.register_action(move |workspace, _: &Toggle, window, cx| {
|
||||
Self::toggle(workspace, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn toggle(
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Option<()> {
|
||||
let buffer = workspace
|
||||
.active_item(cx)?
|
||||
.act_as::<Editor>(cx)?
|
||||
.read(cx)
|
||||
.active_buffer(cx)?;
|
||||
|
||||
let buffer_handle = buffer.read(cx);
|
||||
let project = workspace.project().read(cx);
|
||||
|
||||
if buffer_handle.is_dirty() {
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<EncodingSelector>(),
|
||||
"Save file to change encoding",
|
||||
),
|
||||
cx,
|
||||
);
|
||||
return Some(());
|
||||
}
|
||||
if project.is_shared() {
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<EncodingSelector>(),
|
||||
"Cannot change encoding during collaboration",
|
||||
),
|
||||
cx,
|
||||
);
|
||||
return Some(());
|
||||
}
|
||||
if project.is_via_remote_server() {
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<EncodingSelector>(),
|
||||
"Cannot change encoding of remote server file",
|
||||
),
|
||||
cx,
|
||||
);
|
||||
return Some(());
|
||||
}
|
||||
|
||||
workspace.toggle_modal(window, cx, move |window, cx| {
|
||||
EncodingSelector::new(buffer, window, cx)
|
||||
});
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn new(buffer: Entity<Buffer>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let delegate = EncodingSelectorDelegate::new(cx.entity().downgrade(), buffer);
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for EncodingSelector {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl gpui::IntoElement {
|
||||
v_flex()
|
||||
.key_context("EncodingSelector")
|
||||
.w(gpui::rems(34.))
|
||||
.child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for EncodingSelector {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for EncodingSelector {}
|
||||
impl ModalView for EncodingSelector {}
|
||||
|
||||
pub struct EncodingSelectorDelegate {
|
||||
encoding_selector: WeakEntity<EncodingSelector>,
|
||||
buffer: Entity<Buffer>,
|
||||
encodings: Vec<&'static Encoding>,
|
||||
match_candidates: Arc<Vec<StringMatchCandidate>>,
|
||||
matches: Vec<StringMatch>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl EncodingSelectorDelegate {
|
||||
fn new(encoding_selector: WeakEntity<EncodingSelector>, buffer: Entity<Buffer>) -> Self {
|
||||
let encodings = available_encodings();
|
||||
let match_candidates = encodings
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, enc)| StringMatchCandidate::new(id, enc.name()))
|
||||
.collect::<Vec<_>>();
|
||||
Self {
|
||||
encoding_selector,
|
||||
buffer,
|
||||
encodings,
|
||||
match_candidates: Arc::new(match_candidates),
|
||||
matches: vec![],
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_data_for_match(&self, mat: &StringMatch, cx: &App) -> String {
|
||||
let candidate_encoding = self.encodings[mat.candidate_id];
|
||||
let current_encoding = self.buffer.read(cx).encoding();
|
||||
|
||||
if candidate_encoding.name() == current_encoding.name() {
|
||||
format!("{} (current)", candidate_encoding.name())
|
||||
} else {
|
||||
candidate_encoding.name().to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn available_encodings() -> Vec<&'static Encoding> {
|
||||
let mut encodings = vec![
|
||||
// Unicode
|
||||
encoding_rs::UTF_8,
|
||||
encoding_rs::UTF_16LE,
|
||||
encoding_rs::UTF_16BE,
|
||||
// Japanese
|
||||
encoding_rs::SHIFT_JIS,
|
||||
encoding_rs::EUC_JP,
|
||||
encoding_rs::ISO_2022_JP,
|
||||
// Chinese
|
||||
encoding_rs::GBK,
|
||||
encoding_rs::GB18030,
|
||||
encoding_rs::BIG5,
|
||||
// Korean
|
||||
encoding_rs::EUC_KR,
|
||||
// Windows / Single Byte Series
|
||||
encoding_rs::WINDOWS_1252, // Western (ISO-8859-1 unified)
|
||||
encoding_rs::WINDOWS_1250, // Central European
|
||||
encoding_rs::WINDOWS_1251, // Cyrillic
|
||||
encoding_rs::WINDOWS_1253, // Greek
|
||||
encoding_rs::WINDOWS_1254, // Turkish (ISO-8859-9 unified)
|
||||
encoding_rs::WINDOWS_1255, // Hebrew
|
||||
encoding_rs::WINDOWS_1256, // Arabic
|
||||
encoding_rs::WINDOWS_1257, // Baltic
|
||||
encoding_rs::WINDOWS_1258, // Vietnamese
|
||||
encoding_rs::WINDOWS_874, // Thai
|
||||
// ISO-8859 Series (others)
|
||||
encoding_rs::ISO_8859_2,
|
||||
encoding_rs::ISO_8859_3,
|
||||
encoding_rs::ISO_8859_4,
|
||||
encoding_rs::ISO_8859_5,
|
||||
encoding_rs::ISO_8859_6,
|
||||
encoding_rs::ISO_8859_7,
|
||||
encoding_rs::ISO_8859_8,
|
||||
encoding_rs::ISO_8859_8_I, // Logical Hebrew
|
||||
encoding_rs::ISO_8859_10,
|
||||
encoding_rs::ISO_8859_13,
|
||||
encoding_rs::ISO_8859_14,
|
||||
encoding_rs::ISO_8859_15,
|
||||
encoding_rs::ISO_8859_16,
|
||||
// Cyrillic / Legacy Misc
|
||||
encoding_rs::KOI8_R,
|
||||
encoding_rs::KOI8_U,
|
||||
encoding_rs::IBM866,
|
||||
encoding_rs::MACINTOSH,
|
||||
encoding_rs::X_MAC_CYRILLIC,
|
||||
// NOTE: The following encodings are intentionally excluded from the list:
|
||||
//
|
||||
// 1. encoding_rs::REPLACEMENT
|
||||
// Used internally for decoding errors. Not suitable for user selection.
|
||||
//
|
||||
// 2. encoding_rs::X_USER_DEFINED
|
||||
// Used for binary data emulation (legacy web behavior). Not for general text editing.
|
||||
];
|
||||
|
||||
encodings.sort_by_key(|enc| enc.name());
|
||||
|
||||
encodings
|
||||
}
|
||||
|
||||
impl PickerDelegate for EncodingSelectorDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
"Reopen with encoding...".into()
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_window: &mut Window,
|
||||
_: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let background = cx.background_executor().clone();
|
||||
let candidates = self.match_candidates.clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let matches = if query.is_empty() {
|
||||
candidates
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, candidate)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: candidate.string.clone(),
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
true,
|
||||
100,
|
||||
&Default::default(),
|
||||
background,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let delegate = &mut this.delegate;
|
||||
delegate.matches = matches;
|
||||
delegate.selected_index = delegate
|
||||
.selected_index
|
||||
.min(delegate.matches.len().saturating_sub(1));
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
if let Some(mat) = self.matches.get(self.selected_index) {
|
||||
let selected_encoding = self.encodings[mat.candidate_id];
|
||||
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
let _ = buffer.reload_with_encoding(selected_encoding, cx);
|
||||
});
|
||||
}
|
||||
self.dismissed(window, cx);
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
self.encoding_selector
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let mat = &self.matches.get(ix)?;
|
||||
|
||||
let label = self.render_data_for_match(mat, cx);
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(HighlightedLabel::new(label, mat.positions.clone())),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user