Skip to repository content331 lines · 10.1 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:23:29.132Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
encoding_selector.rs
1mod active_buffer_encoding;
2pub use active_buffer_encoding::ActiveBufferEncoding;
3
4use editor::Editor;
5use encoding_rs::Encoding;
6use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
7use gpui::{
8 App, AppContext, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
9 InteractiveElement, ParentElement, Render, Task, WeakEntity, Window, actions,
10};
11use language::Buffer;
12use picker::{Picker, PickerDelegate};
13use std::sync::Arc;
14use ui::{HighlightedLabel, ListItem, ListItemSpacing, Toggleable, v_flex};
15use util::ResultExt;
16use workspace::{ModalView, Toast, Workspace, notifications::NotificationId};
17
18actions!(
19 encoding_selector,
20 [
21 /// Toggles the encoding selector modal.
22 Toggle
23 ]
24);
25
26pub fn init(cx: &mut App) {
27 cx.observe_new(EncodingSelector::register).detach();
28}
29
30pub struct EncodingSelector {
31 picker: Entity<Picker<EncodingSelectorDelegate>>,
32}
33
34impl EncodingSelector {
35 fn register(
36 workspace: &mut Workspace,
37 _window: Option<&mut Window>,
38 _: &mut Context<Workspace>,
39 ) {
40 workspace.register_action(move |workspace, _: &Toggle, window, cx| {
41 Self::toggle(workspace, window, cx);
42 });
43 }
44
45 pub fn toggle(
46 workspace: &mut Workspace,
47 window: &mut Window,
48 cx: &mut Context<Workspace>,
49 ) -> Option<()> {
50 let buffer = workspace
51 .active_item(cx)?
52 .act_as::<Editor>(cx)?
53 .read(cx)
54 .active_buffer(cx)?;
55
56 let buffer_handle = buffer.read(cx);
57 let project = workspace.project().read(cx);
58
59 if buffer_handle.is_dirty() {
60 workspace.show_toast(
61 Toast::new(
62 NotificationId::unique::<EncodingSelector>(),
63 "Save file to change encoding",
64 ),
65 cx,
66 );
67 return Some(());
68 }
69 if project.is_shared() {
70 workspace.show_toast(
71 Toast::new(
72 NotificationId::unique::<EncodingSelector>(),
73 "Cannot change encoding during collaboration",
74 ),
75 cx,
76 );
77 return Some(());
78 }
79 if project.is_via_remote_server() {
80 workspace.show_toast(
81 Toast::new(
82 NotificationId::unique::<EncodingSelector>(),
83 "Cannot change encoding of remote server file",
84 ),
85 cx,
86 );
87 return Some(());
88 }
89
90 workspace.toggle_modal(window, cx, move |window, cx| {
91 EncodingSelector::new(buffer, window, cx)
92 });
93 Some(())
94 }
95
96 fn new(buffer: Entity<Buffer>, window: &mut Window, cx: &mut Context<Self>) -> Self {
97 let delegate = EncodingSelectorDelegate::new(cx.entity().downgrade(), buffer);
98 let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
99 Self { picker }
100 }
101}
102
103impl Render for EncodingSelector {
104 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl gpui::IntoElement {
105 v_flex()
106 .key_context("EncodingSelector")
107 .child(self.picker.clone())
108 }
109}
110
111impl Focusable for EncodingSelector {
112 fn focus_handle(&self, cx: &App) -> FocusHandle {
113 self.picker.focus_handle(cx)
114 }
115}
116
117impl EventEmitter<DismissEvent> for EncodingSelector {}
118impl ModalView for EncodingSelector {}
119
120pub struct EncodingSelectorDelegate {
121 encoding_selector: WeakEntity<EncodingSelector>,
122 buffer: Entity<Buffer>,
123 encodings: Vec<&'static Encoding>,
124 match_candidates: Arc<Vec<StringMatchCandidate>>,
125 matches: Vec<StringMatch>,
126 selected_index: usize,
127}
128
129impl EncodingSelectorDelegate {
130 fn new(encoding_selector: WeakEntity<EncodingSelector>, buffer: Entity<Buffer>) -> Self {
131 let encodings = available_encodings();
132 let match_candidates = encodings
133 .iter()
134 .enumerate()
135 .map(|(id, enc)| StringMatchCandidate::new(id, enc.name()))
136 .collect::<Vec<_>>();
137 Self {
138 encoding_selector,
139 buffer,
140 encodings,
141 match_candidates: Arc::new(match_candidates),
142 matches: vec![],
143 selected_index: 0,
144 }
145 }
146
147 fn render_data_for_match(&self, mat: &StringMatch, cx: &App) -> String {
148 let candidate_encoding = self.encodings[mat.candidate_id];
149 let current_encoding = self.buffer.read(cx).encoding();
150
151 if candidate_encoding.name() == current_encoding.name() {
152 format!("{} (current)", candidate_encoding.name())
153 } else {
154 candidate_encoding.name().to_string()
155 }
156 }
157}
158
159fn available_encodings() -> Vec<&'static Encoding> {
160 let mut encodings = vec![
161 // Unicode
162 encoding_rs::UTF_8,
163 encoding_rs::UTF_16LE,
164 encoding_rs::UTF_16BE,
165 // Japanese
166 encoding_rs::SHIFT_JIS,
167 encoding_rs::EUC_JP,
168 encoding_rs::ISO_2022_JP,
169 // Chinese
170 encoding_rs::GBK,
171 encoding_rs::GB18030,
172 encoding_rs::BIG5,
173 // Korean
174 encoding_rs::EUC_KR,
175 // Windows / Single Byte Series
176 encoding_rs::WINDOWS_1252, // Western (ISO-8859-1 unified)
177 encoding_rs::WINDOWS_1250, // Central European
178 encoding_rs::WINDOWS_1251, // Cyrillic
179 encoding_rs::WINDOWS_1253, // Greek
180 encoding_rs::WINDOWS_1254, // Turkish (ISO-8859-9 unified)
181 encoding_rs::WINDOWS_1255, // Hebrew
182 encoding_rs::WINDOWS_1256, // Arabic
183 encoding_rs::WINDOWS_1257, // Baltic
184 encoding_rs::WINDOWS_1258, // Vietnamese
185 encoding_rs::WINDOWS_874, // Thai
186 // ISO-8859 Series (others)
187 encoding_rs::ISO_8859_2,
188 encoding_rs::ISO_8859_3,
189 encoding_rs::ISO_8859_4,
190 encoding_rs::ISO_8859_5,
191 encoding_rs::ISO_8859_6,
192 encoding_rs::ISO_8859_7,
193 encoding_rs::ISO_8859_8,
194 encoding_rs::ISO_8859_8_I, // Logical Hebrew
195 encoding_rs::ISO_8859_10,
196 encoding_rs::ISO_8859_13,
197 encoding_rs::ISO_8859_14,
198 encoding_rs::ISO_8859_15,
199 encoding_rs::ISO_8859_16,
200 // Cyrillic / Legacy Misc
201 encoding_rs::KOI8_R,
202 encoding_rs::KOI8_U,
203 encoding_rs::IBM866,
204 encoding_rs::MACINTOSH,
205 encoding_rs::X_MAC_CYRILLIC,
206 // NOTE: The following encodings are intentionally excluded from the list:
207 //
208 // 1. encoding_rs::REPLACEMENT
209 // Used internally for decoding errors. Not suitable for user selection.
210 //
211 // 2. encoding_rs::X_USER_DEFINED
212 // Used for binary data emulation (legacy web behavior). Not for general text editing.
213 ];
214
215 encodings.sort_by_key(|enc| enc.name());
216
217 encodings
218}
219
220impl PickerDelegate for EncodingSelectorDelegate {
221 type ListItem = ListItem;
222
223 fn name() -> &'static str {
224 "encoding selector"
225 }
226
227 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
228 "Reopen with encoding...".into()
229 }
230
231 fn match_count(&self) -> usize {
232 self.matches.len()
233 }
234
235 fn selected_index(&self) -> usize {
236 self.selected_index
237 }
238
239 fn set_selected_index(
240 &mut self,
241 ix: usize,
242 _window: &mut Window,
243 _: &mut Context<Picker<Self>>,
244 ) {
245 self.selected_index = ix;
246 }
247
248 fn update_matches(
249 &mut self,
250 query: String,
251 window: &mut Window,
252 cx: &mut Context<Picker<Self>>,
253 ) -> Task<()> {
254 let background = cx.background_executor().clone();
255 let candidates = self.match_candidates.clone();
256
257 cx.spawn_in(window, async move |this, cx| {
258 let matches = if query.is_empty() {
259 candidates
260 .iter()
261 .enumerate()
262 .map(|(index, candidate)| StringMatch {
263 candidate_id: index,
264 string: candidate.string.clone(),
265 positions: Vec::new(),
266 score: 0.0,
267 })
268 .collect()
269 } else {
270 match_strings(
271 &candidates,
272 &query,
273 false,
274 true,
275 100,
276 &Default::default(),
277 background,
278 )
279 .await
280 };
281
282 this.update(cx, |this, cx| {
283 let delegate = &mut this.delegate;
284 delegate.matches = matches;
285 delegate.selected_index = delegate
286 .selected_index
287 .min(delegate.matches.len().saturating_sub(1));
288 cx.notify();
289 })
290 .log_err();
291 })
292 }
293
294 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
295 if let Some(mat) = self.matches.get(self.selected_index) {
296 let selected_encoding = self.encodings[mat.candidate_id];
297
298 self.buffer.update(cx, |buffer, cx| {
299 let _ = buffer.reload_with_encoding(selected_encoding, cx);
300 });
301 }
302 self.dismissed(window, cx);
303 }
304
305 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
306 self.encoding_selector
307 .update(cx, |_, cx| cx.emit(DismissEvent))
308 .log_err();
309 }
310
311 fn render_match(
312 &self,
313 ix: usize,
314 selected: bool,
315 _: &mut Window,
316 cx: &mut Context<Picker<Self>>,
317 ) -> Option<Self::ListItem> {
318 let mat = &self.matches.get(ix)?;
319
320 let label = self.render_data_for_match(mat, cx);
321
322 Some(
323 ListItem::new(ix)
324 .inset(true)
325 .spacing(ListItemSpacing::Sparse)
326 .toggle_state(selected)
327 .child(HighlightedLabel::new(label, mat.positions.clone())),
328 )
329 }
330}
331