From fae2f8ff1cea01bc234c7cbecd81b4cf5eb29739 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Tue, 19 Aug 2025 20:23:02 +0530 Subject: [PATCH 1/7] Add a status indicator to indicate the current file's encoding. When clicked a modal view opens that lets user choose to either reopen or save a file with a particular encoding. The actual implementations are incomplete --- Cargo.lock | 14 ++ Cargo.toml | 4 +- crates/encodings/Cargo.toml | 17 +++ crates/encodings/src/lib.rs | 210 ++++++++++++++++++++++++++++++ crates/workspace/src/workspace.rs | 1 + crates/zed/Cargo.toml | 1 + crates/zed/src/zed.rs | 3 + 7 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 crates/encodings/Cargo.toml create mode 100644 crates/encodings/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 42649b137f..e6f1aabf84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5203,6 +5203,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "encodings" +version = "0.1.0" +dependencies = [ + "fuzzy", + "gpui", + "language", + "picker", + "ui", + "util", + "workspace", +] + [[package]] name = "endi" version = "1.1.0" @@ -20437,6 +20450,7 @@ dependencies = [ "diagnostics", "edit_prediction_button", "editor", + "encodings", "env_logger 0.11.8", "extension", "extension_host", diff --git a/Cargo.toml b/Cargo.toml index 6ec243a9b9..e4d8638bc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ members = [ "crates/diagnostics", "crates/docs_preprocessor", "crates/editor", + "crates/encodings", "crates/eval", "crates/explorer_command_injector", "crates/extension", @@ -214,7 +215,7 @@ members = [ # "tooling/workspace-hack", - "tooling/xtask", + "tooling/xtask", "crates/encodings", ] default-members = ["crates/zed"] @@ -309,6 +310,7 @@ icons = { path = "crates/icons" } image_viewer = { path = "crates/image_viewer" } edit_prediction = { path = "crates/edit_prediction" } edit_prediction_button = { path = "crates/edit_prediction_button" } +encodings = {path = "crates/encodings"} inspector_ui = { path = "crates/inspector_ui" } install_cli = { path = "crates/install_cli" } jj = { path = "crates/jj" } diff --git a/crates/encodings/Cargo.toml b/crates/encodings/Cargo.toml new file mode 100644 index 0000000000..d49dff06ec --- /dev/null +++ b/crates/encodings/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "encodings" +version = "0.1.0" +publish.workspace = true +edition.workspace = true + +[dependencies] +ui.workspace = true +workspace.workspace = true +gpui.workspace = true +picker.workspace = true +language.workspace = true +util.workspace = true +fuzzy.workspace = true + +[lints] +workspace = true diff --git a/crates/encodings/src/lib.rs b/crates/encodings/src/lib.rs new file mode 100644 index 0000000000..23b7429526 --- /dev/null +++ b/crates/encodings/src/lib.rs @@ -0,0 +1,210 @@ +use std::sync::Weak; +use std::sync::atomic::AtomicBool; + +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{AppContext, ClickEvent, DismissEvent, Entity, EventEmitter, Focusable, WeakEntity}; +use language::Buffer; +use picker::{Picker, PickerDelegate}; +use ui::{ + Button, ButtonCommon, Context, Label, LabelSize, ListItem, Render, Styled, Tooltip, Window, + div, rems, v_flex, +}; +use ui::{Clickable, ParentElement}; +use util::ResultExt; +use workspace::{ItemHandle, ModalView, StatusItemView, Workspace}; + +pub enum Encoding { + Utf8(WeakEntity), +} + +impl Encoding { + pub fn as_str(&self) -> &str { + match &self { + Encoding::Utf8(_) => "UTF-8", + } + } +} + +impl EncodingSaveOrReopenSelector { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let delegate = EncodingSaveOrReopenDelegate::new(cx.entity().downgrade()); + + let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); + + Self { picker } + } + + pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { + workspace.toggle_modal(window, cx, |window, cx| { + EncodingSaveOrReopenSelector::new(window, cx) + }); + } +} + +pub struct EncodingSaveOrReopenSelector { + picker: Entity>, +} + +impl Focusable for EncodingSaveOrReopenSelector { + fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl Render for EncodingSaveOrReopenSelector { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl ui::IntoElement { + v_flex().w(rems(34.0)).child(self.picker.clone()) + } +} + +impl ModalView for EncodingSaveOrReopenSelector {} + +impl EventEmitter for EncodingSaveOrReopenSelector {} + +pub struct EncodingSaveOrReopenDelegate { + encoding_selector: WeakEntity, + current_selection: usize, + matches: Vec, + pub actions: Vec, +} + +impl EncodingSaveOrReopenDelegate { + pub fn new(selector: WeakEntity) -> Self { + Self { + encoding_selector: selector, + current_selection: 0, + matches: Vec::new(), + actions: vec![ + StringMatchCandidate::new(0, "Save with encoding"), + StringMatchCandidate::new(1, "Reopen with encoding"), + ], + } + } + + pub fn get_actions(&self) -> (&str, &str) { + (&self.actions[0].string, &self.actions[1].string) + } +} + +impl PickerDelegate for EncodingSaveOrReopenDelegate { + type ListItem = ListItem; + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.current_selection + } + + fn set_selected_index( + &mut self, + ix: usize, + _window: &mut Window, + _cx: &mut Context>, + ) { + self.current_selection = ix; + } + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc { + "Select an action...".into() + } + + fn update_matches( + &mut self, + query: String, + window: &mut Window, + cx: &mut Context>, + ) -> gpui::Task<()> { + let executor = cx.background_executor().clone(); + let actions = self.actions.clone(); + + cx.spawn_in(window, async move |this, cx| { + let matches = if query.is_empty() { + actions + .into_iter() + .enumerate() + .map(|(index, value)| StringMatch { + candidate_id: index, + score: 0.0, + positions: vec![], + string: value.string, + }) + .collect::>() + } else { + fuzzy::match_strings( + &actions, + &query, + false, + false, + 2, + &AtomicBool::new(false), + executor, + ) + .await + }; + + this.update(cx, |picker, cx| { + let delegate = &mut picker.delegate; + delegate.current_selection = matches.len().saturating_sub(1); + delegate.matches = matches; + cx.notify(); + }) + .log_err(); + }) + } + + fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) {} + + fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { + self.encoding_selector + .update(cx, |_, cx| cx.emit(DismissEvent)) + .log_err(); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + window: &mut Window, + cx: &mut Context>, + ) -> Option { + Some(ListItem::new(ix).child(Label::new(&self.matches[ix].string))) + } +} + +fn get_current_encoding() -> &'static str { + "UTF-8" +} + +impl Render for Encoding { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { + let encoding_indicator = div(); + + encoding_indicator.child( + Button::new("encoding", get_current_encoding()) + .label_size(LabelSize::Small) + .tooltip(Tooltip::text("Select Encoding")) + .on_click(cx.listener(|encoding, _: &ClickEvent, window, cx| { + if let Some(workspace) = match encoding { + Encoding::Utf8(workspace) => workspace.upgrade(), + } { + workspace.update(cx, |workspace, cx| { + EncodingSaveOrReopenSelector::toggle(workspace, window, cx) + }) + } else { + } + })), + ) + } +} + +impl StatusItemView for Encoding { + fn set_active_pane_item( + &mut self, + _active_pane_item: Option<&dyn ItemHandle>, + _window: &mut Window, + _cx: &mut Context, + ) { + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 044601df97..f2828dbc3f 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -118,6 +118,7 @@ use crate::persistence::{ model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup}, }; + pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200); static ZED_WINDOW_SIZE: LazyLock>> = LazyLock::new(|| { diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6f4ead9ebb..a91f13ce58 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -55,6 +55,7 @@ debugger_tools.workspace = true debugger_ui.workspace = true diagnostics.workspace = true editor.workspace = true +encodings.workspace = true env_logger.workspace = true extension.workspace = true extension_host.workspace = true diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 638e1dca0e..e780d9d5a0 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -397,6 +397,8 @@ pub fn initialize_workspace( } }); + let encoding_indicator = cx.new(|_cx| encodings::Encoding::Utf8(workspace.weak_handle())); + let cursor_position = cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace)); workspace.status_bar().update(cx, |status_bar, cx| { @@ -409,6 +411,7 @@ pub fn initialize_workspace( status_bar.add_right_item(active_toolchain_language, window, cx); status_bar.add_right_item(vim_mode_indicator, window, cx); status_bar.add_right_item(cursor_position, window, cx); + status_bar.add_right_item(encoding_indicator, window, cx); status_bar.add_right_item(image_info, window, cx); }); From 5723987b59323f33e43c8eb4f32f181a86aafa0b Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sat, 23 Aug 2025 20:03:26 +0530 Subject: [PATCH 2/7] Implement the actual encoding selector. There are currently only two encodings in the selector used as placeholders, but more will be added in the future. As of now, the encoding picker is not actually triggered. --- Cargo.lock | 68 +++++- Cargo.toml | 16 -- crates/encodings/Cargo.toml | 2 +- crates/encodings/src/lib.rs | 196 +++--------------- crates/encodings/src/selectors.rs | 331 ++++++++++++++++++++++++++++++ crates/fs/Cargo.toml | 2 + crates/fs/src/encodings.rs | 21 ++ crates/language/Cargo.toml | 1 + crates/language/src/buffer.rs | 2 + crates/zed/src/zed.rs | 5 +- 10 files changed, 453 insertions(+), 191 deletions(-) create mode 100644 crates/encodings/src/selectors.rs create mode 100644 crates/fs/src/encodings.rs diff --git a/Cargo.lock b/Cargo.lock index e6f1aabf84..8326916c01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5194,6 +5194,70 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -5207,9 +5271,9 @@ dependencies = [ name = "encodings" version = "0.1.0" dependencies = [ + "editor", "fuzzy", "gpui", - "language", "picker", "ui", "util", @@ -6055,6 +6119,7 @@ dependencies = [ "async-trait", "cocoa 0.26.0", "collections", + "encoding", "fsevent", "futures 0.3.31", "git", @@ -9035,6 +9100,7 @@ dependencies = [ "ctor", "diffy", "ec4rs", + "encoding", "fs", "futures 0.3.31", "fuzzy", diff --git a/Cargo.toml b/Cargo.toml index e4d8638bc2..7ba19fdcd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -238,7 +238,6 @@ activity_indicator = { path = "crates/activity_indicator" } agent_ui = { path = "crates/agent_ui" } agent_settings = { path = "crates/agent_settings" } agent_servers = { path = "crates/agent_servers" } -ai = { path = "crates/ai" } ai_onboarding = { path = "crates/ai_onboarding" } anthropic = { path = "crates/anthropic" } askpass = { path = "crates/askpass" } @@ -250,7 +249,6 @@ assistant_tool = { path = "crates/assistant_tool" } assistant_tools = { path = "crates/assistant_tools" } audio = { path = "crates/audio" } auto_update = { path = "crates/auto_update" } -auto_update_helper = { path = "crates/auto_update_helper" } auto_update_ui = { path = "crates/auto_update_ui" } aws_http_client = { path = "crates/aws_http_client" } bedrock = { path = "crates/bedrock" } @@ -264,7 +262,6 @@ clock = { path = "crates/clock" } cloud_api_client = { path = "crates/cloud_api_client" } cloud_api_types = { path = "crates/cloud_api_types" } cloud_llm_client = { path = "crates/cloud_llm_client" } -collab = { path = "crates/collab" } collab_ui = { path = "crates/collab_ui" } collections = { path = "crates/collections" } command_palette = { path = "crates/command_palette" } @@ -348,8 +345,6 @@ outline_panel = { path = "crates/outline_panel" } panel = { path = "crates/panel" } paths = { path = "crates/paths" } picker = { path = "crates/picker" } -plugin = { path = "crates/plugin" } -plugin_macros = { path = "crates/plugin_macros" } prettier = { path = "crates/prettier" } settings_profile_selector = { path = "crates/settings_profile_selector" } project = { path = "crates/project" } @@ -370,7 +365,6 @@ rope = { path = "crates/rope" } rpc = { path = "crates/rpc" } rules_library = { path = "crates/rules_library" } search = { path = "crates/search" } -semantic_index = { path = "crates/semantic_index" } semantic_version = { path = "crates/semantic_version" } session = { path = "crates/session" } settings = { path = "crates/settings" } @@ -381,7 +375,6 @@ snippets_ui = { path = "crates/snippets_ui" } sqlez = { path = "crates/sqlez" } sqlez_macros = { path = "crates/sqlez_macros" } story = { path = "crates/story" } -storybook = { path = "crates/storybook" } streaming_diff = { path = "crates/streaming_diff" } sum_tree = { path = "crates/sum_tree" } supermaven = { path = "crates/supermaven" } @@ -397,7 +390,6 @@ terminal_view = { path = "crates/terminal_view" } text = { path = "crates/text" } theme = { path = "crates/theme" } theme_extension = { path = "crates/theme_extension" } -theme_importer = { path = "crates/theme_importer" } theme_selector = { path = "crates/theme_selector" } time_format = { path = "crates/time_format" } title_bar = { path = "crates/title_bar" } @@ -469,7 +461,6 @@ ciborium = "0.2" circular-buffer = "1.0" clap = { version = "4.4", features = ["derive"] } cocoa = "0.26" -cocoa-foundation = "0.2.0" convert_case = "0.8.0" core-foundation = "0.10.0" core-foundation-sys = "0.8.6" @@ -545,7 +536,6 @@ pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } -pet-pixi = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } portable-pty = "0.9.0" @@ -668,7 +658,6 @@ wasmtime = { version = "29", default-features = false, features = [ wasmtime-wasi = "29" which = "6.0.0" windows-core = "0.61" -wit-component = "0.221" workspace-hack = "0.1.0" yawc = "0.2.5" zstd = "0.11" @@ -742,11 +731,7 @@ codegen-units = 16 [profile.dev.package] taffy = { opt-level = 3 } cranelift-codegen = { opt-level = 3 } -cranelift-codegen-meta = { opt-level = 3 } -cranelift-codegen-shared = { opt-level = 3 } resvg = { opt-level = 3 } -rustybuzz = { opt-level = 3 } -ttf-parser = { opt-level = 3 } wasmtime-cranelift = { opt-level = 3 } wasmtime = { opt-level = 3 } # Build single-source-file crates with cg=1 as it helps make `cargo build` of a whole workspace a bit faster @@ -756,7 +741,6 @@ breadcrumbs = { codegen-units = 1 } collections = { codegen-units = 1 } command_palette = { codegen-units = 1 } command_palette_hooks = { codegen-units = 1 } -extension_cli = { codegen-units = 1 } feature_flags = { codegen-units = 1 } file_icons = { codegen-units = 1 } fsevent = { codegen-units = 1 } diff --git a/crates/encodings/Cargo.toml b/crates/encodings/Cargo.toml index d49dff06ec..2d75395810 100644 --- a/crates/encodings/Cargo.toml +++ b/crates/encodings/Cargo.toml @@ -9,9 +9,9 @@ ui.workspace = true workspace.workspace = true gpui.workspace = true picker.workspace = true -language.workspace = true util.workspace = true fuzzy.workspace = true +editor.workspace = true [lints] workspace = true diff --git a/crates/encodings/src/lib.rs b/crates/encodings/src/lib.rs index 23b7429526..bfecfceea3 100644 --- a/crates/encodings/src/lib.rs +++ b/crates/encodings/src/lib.rs @@ -1,194 +1,42 @@ -use std::sync::Weak; -use std::sync::atomic::AtomicBool; - -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{AppContext, ClickEvent, DismissEvent, Entity, EventEmitter, Focusable, WeakEntity}; -use language::Buffer; -use picker::{Picker, PickerDelegate}; -use ui::{ - Button, ButtonCommon, Context, Label, LabelSize, ListItem, Render, Styled, Tooltip, Window, - div, rems, v_flex, -}; +use editor::Editor; +use gpui::{ClickEvent, Entity, WeakEntity}; +use ui::{Button, ButtonCommon, Context, LabelSize, Render, Tooltip, Window, div}; use ui::{Clickable, ParentElement}; -use util::ResultExt; -use workspace::{ItemHandle, ModalView, StatusItemView, Workspace}; +use workspace::{ItemHandle, StatusItemView, Workspace}; + +use crate::selectors::save_or_reopen::{EncodingSaveOrReopenSelector, get_current_encoding}; pub enum Encoding { - Utf8(WeakEntity), + Utf8, + Iso8859_1, } impl Encoding { pub fn as_str(&self) -> &str { match &self { - Encoding::Utf8(_) => "UTF-8", + Encoding::Utf8 => "UTF-8", + Encoding::Iso8859_1 => "ISO 8859-1", } } } -impl EncodingSaveOrReopenSelector { - pub fn new(window: &mut Window, cx: &mut Context) -> Self { - let delegate = EncodingSaveOrReopenDelegate::new(cx.entity().downgrade()); - - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); - - Self { picker } - } - - pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { - workspace.toggle_modal(window, cx, |window, cx| { - EncodingSaveOrReopenSelector::new(window, cx) - }); - } +pub struct EncodingIndicator { + pub encoding: Encoding, + pub workspace: WeakEntity, } -pub struct EncodingSaveOrReopenSelector { - picker: Entity>, -} +pub mod selectors; -impl Focusable for EncodingSaveOrReopenSelector { - fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl Render for EncodingSaveOrReopenSelector { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl ui::IntoElement { - v_flex().w(rems(34.0)).child(self.picker.clone()) - } -} - -impl ModalView for EncodingSaveOrReopenSelector {} - -impl EventEmitter for EncodingSaveOrReopenSelector {} - -pub struct EncodingSaveOrReopenDelegate { - encoding_selector: WeakEntity, - current_selection: usize, - matches: Vec, - pub actions: Vec, -} - -impl EncodingSaveOrReopenDelegate { - pub fn new(selector: WeakEntity) -> Self { - Self { - encoding_selector: selector, - current_selection: 0, - matches: Vec::new(), - actions: vec![ - StringMatchCandidate::new(0, "Save with encoding"), - StringMatchCandidate::new(1, "Reopen with encoding"), - ], - } - } - - pub fn get_actions(&self) -> (&str, &str) { - (&self.actions[0].string, &self.actions[1].string) - } -} - -impl PickerDelegate for EncodingSaveOrReopenDelegate { - type ListItem = ListItem; - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.current_selection - } - - fn set_selected_index( - &mut self, - ix: usize, - _window: &mut Window, - _cx: &mut Context>, - ) { - self.current_selection = ix; - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc { - "Select an action...".into() - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> gpui::Task<()> { - let executor = cx.background_executor().clone(); - let actions = self.actions.clone(); - - cx.spawn_in(window, async move |this, cx| { - let matches = if query.is_empty() { - actions - .into_iter() - .enumerate() - .map(|(index, value)| StringMatch { - candidate_id: index, - score: 0.0, - positions: vec![], - string: value.string, - }) - .collect::>() - } else { - fuzzy::match_strings( - &actions, - &query, - false, - false, - 2, - &AtomicBool::new(false), - executor, - ) - .await - }; - - this.update(cx, |picker, cx| { - let delegate = &mut picker.delegate; - delegate.current_selection = matches.len().saturating_sub(1); - delegate.matches = matches; - cx.notify(); - }) - .log_err(); - }) - } - - fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) {} - - fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { - self.encoding_selector - .update(cx, |_, cx| cx.emit(DismissEvent)) - .log_err(); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - window: &mut Window, - cx: &mut Context>, - ) -> Option { - Some(ListItem::new(ix).child(Label::new(&self.matches[ix].string))) - } -} - -fn get_current_encoding() -> &'static str { - "UTF-8" -} - -impl Render for Encoding { +impl Render for EncodingIndicator { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { - let encoding_indicator = div(); + let status_element = div(); - encoding_indicator.child( + status_element.child( Button::new("encoding", get_current_encoding()) .label_size(LabelSize::Small) .tooltip(Tooltip::text("Select Encoding")) - .on_click(cx.listener(|encoding, _: &ClickEvent, window, cx| { - if let Some(workspace) = match encoding { - Encoding::Utf8(workspace) => workspace.upgrade(), - } { + .on_click(cx.listener(|indicator, _: &ClickEvent, window, cx| { + if let Some(workspace) = indicator.workspace.upgrade() { workspace.update(cx, |workspace, cx| { EncodingSaveOrReopenSelector::toggle(workspace, window, cx) }) @@ -199,7 +47,11 @@ impl Render for Encoding { } } -impl StatusItemView for Encoding { +impl EncodingIndicator { + pub fn get_current_encoding(&self, cx: &mut Context, editor: WeakEntity) {} +} + +impl StatusItemView for EncodingIndicator { fn set_active_pane_item( &mut self, _active_pane_item: Option<&dyn ItemHandle>, diff --git a/crates/encodings/src/selectors.rs b/crates/encodings/src/selectors.rs new file mode 100644 index 0000000000..368f85d584 --- /dev/null +++ b/crates/encodings/src/selectors.rs @@ -0,0 +1,331 @@ +pub mod save_or_reopen { + use gpui::Styled; + use gpui::{AppContext, ParentElement}; + use picker::Picker; + use picker::PickerDelegate; + use std::sync::atomic::AtomicBool; + use util::ResultExt; + + use fuzzy::{StringMatch, StringMatchCandidate}; + use gpui::{DismissEvent, Entity, EventEmitter, Focusable, WeakEntity}; + + use ui::{Context, Label, ListItem, Render, Window, rems, v_flex}; + use workspace::{ModalView, Workspace}; + + pub struct EncodingSaveOrReopenSelector { + picker: Entity>, + } + + impl EncodingSaveOrReopenSelector { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let delegate = EncodingSaveOrReopenDelegate::new(cx.entity().downgrade()); + + let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); + + Self { picker } + } + + pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { + workspace.toggle_modal(window, cx, |window, cx| { + EncodingSaveOrReopenSelector::new(window, cx) + }); + } + } + + impl Focusable for EncodingSaveOrReopenSelector { + fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle { + self.picker.focus_handle(cx) + } + } + + impl Render for EncodingSaveOrReopenSelector { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl ui::IntoElement { + v_flex().w(rems(34.0)).child(self.picker.clone()) + } + } + + impl ModalView for EncodingSaveOrReopenSelector {} + + impl EventEmitter for EncodingSaveOrReopenSelector {} + + pub struct EncodingSaveOrReopenDelegate { + encoding_selector: WeakEntity, + current_selection: usize, + matches: Vec, + pub actions: Vec, + } + + impl EncodingSaveOrReopenDelegate { + pub fn new(selector: WeakEntity) -> Self { + Self { + encoding_selector: selector, + current_selection: 0, + matches: Vec::new(), + actions: vec![ + StringMatchCandidate::new(0, "Save with encoding"), + StringMatchCandidate::new(1, "Reopen with encoding"), + ], + } + } + + pub fn get_actions(&self) -> (&str, &str) { + (&self.actions[0].string, &self.actions[1].string) + } + } + + impl PickerDelegate for EncodingSaveOrReopenDelegate { + type ListItem = ListItem; + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.current_selection + } + + fn set_selected_index( + &mut self, + ix: usize, + _window: &mut Window, + _cx: &mut Context>, + ) { + self.current_selection = ix; + } + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc { + "Select an action...".into() + } + + fn update_matches( + &mut self, + query: String, + window: &mut Window, + cx: &mut Context>, + ) -> gpui::Task<()> { + let executor = cx.background_executor().clone(); + let actions = self.actions.clone(); + + cx.spawn_in(window, async move |this, cx| { + let matches = if query.is_empty() { + actions + .into_iter() + .enumerate() + .map(|(index, value)| StringMatch { + candidate_id: index, + score: 0.0, + positions: vec![], + string: value.string, + }) + .collect::>() + } else { + fuzzy::match_strings( + &actions, + &query, + false, + false, + 2, + &AtomicBool::new(false), + executor, + ) + .await + }; + + this.update(cx, |picker, cx| { + let delegate = &mut picker.delegate; + delegate.current_selection = matches.len().saturating_sub(1); + delegate.matches = matches; + cx.notify(); + }) + .log_err(); + }) + } + + fn confirm( + &mut self, + secondary: bool, + window: &mut Window, + cx: &mut Context>, + ) { + } + + fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { + self.encoding_selector + .update(cx, |_, cx| cx.emit(DismissEvent)) + .log_err(); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + window: &mut Window, + cx: &mut Context>, + ) -> Option { + Some(ListItem::new(ix).child(Label::new(&self.matches[ix].string))) + } + } + + pub fn get_current_encoding() -> &'static str { + "UTF-8" + } +} + +pub mod encoding { + use std::sync::atomic::AtomicBool; + + use fuzzy::{StringMatch, StringMatchCandidate}; + use gpui::{ + AppContext, BackgroundExecutor, DismissEvent, Entity, EventEmitter, Focusable, WeakEntity, + }; + use picker::{Picker, PickerDelegate}; + use ui::{Context, Label, ListItem, ParentElement, Render, Styled, Window, rems, v_flex}; + use util::{ResultExt, TryFutureExt}; + use workspace::{ModalView, Workspace}; + + pub struct EncodingSelector { + pub picker: Entity>, + } + + pub struct EncodingSelectorDelegate { + current_selection: usize, + encodings: Vec, + matches: Vec, + selector: WeakEntity, + } + + impl EncodingSelectorDelegate { + pub fn new(selector: WeakEntity) -> EncodingSelectorDelegate { + EncodingSelectorDelegate { + current_selection: 0, + encodings: vec![ + StringMatchCandidate::new(0, "UTF-8"), + StringMatchCandidate::new(1, "ISO 8859-1"), + ], + matches: Vec::new(), + selector, + } + } + } + + impl PickerDelegate for EncodingSelectorDelegate { + type ListItem = ListItem; + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.current_selection + } + + fn set_selected_index( + &mut self, + ix: usize, + window: &mut Window, + cx: &mut Context>, + ) { + self.current_selection = ix; + } + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc { + "Select an encoding...".into() + } + + fn update_matches( + &mut self, + query: String, + window: &mut Window, + cx: &mut Context>, + ) -> gpui::Task<()> { + let executor = cx.background_executor().clone(); + let encodings = self.encodings.clone(); + let current_selection = self.current_selection; + + cx.spawn_in(window, async move |picker, cx| { + let matches: Vec; + + if query.is_empty() { + matches = encodings + .into_iter() + .enumerate() + .map(|(index, value)| StringMatch { + candidate_id: index, + score: 0.0, + positions: Vec::new(), + string: value.string, + }) + .collect(); + } else { + matches = fuzzy::match_strings( + &encodings, + &query, + false, + false, + 0, + &AtomicBool::new(false), + executor, + ) + .await + } + }) + } + + fn confirm( + &mut self, + secondary: bool, + window: &mut Window, + cx: &mut Context>, + ) { + } + + fn dismissed(&mut self, window: &mut Window, cx: &mut Context>) { + self.selector + .update(cx, |_, cx| cx.emit(DismissEvent)) + .log_err(); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + window: &mut Window, + cx: &mut Context>, + ) -> Option { + Some(ListItem::new(ix).child(Label::new(&self.matches[ix].string))) + } + } + + impl EncodingSelector { + pub fn new(window: &mut Window, cx: &mut Context) -> EncodingSelector { + let delegate = EncodingSelectorDelegate::new(cx.entity().downgrade()); + let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); + + EncodingSelector { picker: picker } + } + + pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { + workspace.toggle_modal(window, cx, |window, cx| EncodingSelector::new(window, cx)); + } + } + + impl EventEmitter for EncodingSelector {} + + impl Focusable for EncodingSelector { + fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle { + cx.focus_handle() + } + } + + impl ModalView for EncodingSelector {} + + impl Render for EncodingSelector { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl ui::IntoElement { + v_flex().w(rems(34.0)).child(self.picker.clone()) + } + } +} diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 1d4161134e..6476c67636 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -34,6 +34,8 @@ text.workspace = true time.workspace = true util.workspace = true workspace-hack.workspace = true +encoding = "0.2.33" + [target.'cfg(target_os = "macos")'.dependencies] fsevent.workspace = true diff --git a/crates/fs/src/encodings.rs b/crates/fs/src/encodings.rs new file mode 100644 index 0000000000..8fb38ff24f --- /dev/null +++ b/crates/fs/src/encodings.rs @@ -0,0 +1,21 @@ +use encoding::Encoding; + +pub enum CharacterEncoding { + Utf8, + Iso8859_1, + Cp865, +} + +pub fn to_utf8<'a>(input: Vec, encoding: &'a impl encoding::Encoding) -> String { + match encoding.decode(&input, encoding::DecoderTrap::Strict) { + Ok(v) => return v, + Err(_) => panic!(), + } +} + +pub fn to<'a>(input: String, target: &'a impl encoding::Encoding) -> Vec { + match target.encode(&input, encoding::EncoderTrap::Strict) { + Ok(v) => v, + Err(_) => panic!(), + } +} diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 4ab56d6647..fafd1fdcb7 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -70,6 +70,7 @@ util.workspace = true watch.workspace = true workspace-hack.workspace = true diffy = "0.4.2" +encoding = "0.2.33" [dev-dependencies] collections = { workspace = true, features = ["test-support"] } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 4ddc2b3018..85c24ef0ac 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -127,6 +127,7 @@ pub struct Buffer { has_unsaved_edits: Cell<(clock::Global, bool)>, change_bits: Vec>>, _subscriptions: Vec, + encoding: &'static dyn encoding::Encoding, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -958,6 +959,7 @@ impl Buffer { has_conflict: false, change_bits: Default::default(), _subscriptions: Vec::new(), + encoding: encoding::all::UTF_8, } } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index e780d9d5a0..36189c1110 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -397,7 +397,10 @@ pub fn initialize_workspace( } }); - let encoding_indicator = cx.new(|_cx| encodings::Encoding::Utf8(workspace.weak_handle())); + let encoding_indicator = cx.new(|_cx| encodings::EncodingIndicator { + encoding: encodings::Encoding::Utf8, + workspace: workspace_handle.downgrade(), + }); let cursor_position = cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace)); From 5fec768e373f7548c3b5d16587d8146e425bb2b3 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sun, 24 Aug 2025 17:16:02 +0530 Subject: [PATCH 3/7] refactor: `encoding` in `EncodingIndicator` is now an optional trait object feat: Add all supported encodings, and open the encoding selector when an action(save or reopen) is chosen. --- ..gitignore.swp | 0 Cargo.lock | 2 + crates/encodings/Cargo.toml | 1 + crates/encodings/src/lib.rs | 63 ++++++--- crates/encodings/src/selectors.rs | 206 ++++++++++++++++++++++++------ crates/language/src/buffer.rs | 2 +- crates/zed/Cargo.toml | 1 + crates/zed/src/zed.rs | 6 +- 8 files changed, 221 insertions(+), 60 deletions(-) create mode 100644 ..gitignore.swp diff --git a/..gitignore.swp b/..gitignore.swp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Cargo.lock b/Cargo.lock index 8326916c01..0002c8be84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5272,6 +5272,7 @@ name = "encodings" version = "0.1.0" dependencies = [ "editor", + "encoding", "fuzzy", "gpui", "picker", @@ -20516,6 +20517,7 @@ dependencies = [ "diagnostics", "edit_prediction_button", "editor", + "encoding", "encodings", "env_logger 0.11.8", "extension", diff --git a/crates/encodings/Cargo.toml b/crates/encodings/Cargo.toml index 2d75395810..4b8d877a3a 100644 --- a/crates/encodings/Cargo.toml +++ b/crates/encodings/Cargo.toml @@ -12,6 +12,7 @@ picker.workspace = true util.workspace = true fuzzy.workspace = true editor.workspace = true +encoding = "0.2.33" [lints] workspace = true diff --git a/crates/encodings/src/lib.rs b/crates/encodings/src/lib.rs index bfecfceea3..6387919770 100644 --- a/crates/encodings/src/lib.rs +++ b/crates/encodings/src/lib.rs @@ -1,28 +1,16 @@ use editor::Editor; -use gpui::{ClickEvent, Entity, WeakEntity}; +use encoding::Encoding; +use gpui::{ClickEvent, Entity, Subscription, WeakEntity}; use ui::{Button, ButtonCommon, Context, LabelSize, Render, Tooltip, Window, div}; use ui::{Clickable, ParentElement}; use workspace::{ItemHandle, StatusItemView, Workspace}; use crate::selectors::save_or_reopen::{EncodingSaveOrReopenSelector, get_current_encoding}; -pub enum Encoding { - Utf8, - Iso8859_1, -} - -impl Encoding { - pub fn as_str(&self) -> &str { - match &self { - Encoding::Utf8 => "UTF-8", - Encoding::Iso8859_1 => "ISO 8859-1", - } - } -} - pub struct EncodingIndicator { - pub encoding: Encoding, + pub encoding: Option<&'static dyn Encoding>, pub workspace: WeakEntity, + observe: Option, } pub mod selectors; @@ -49,14 +37,51 @@ impl Render for EncodingIndicator { impl EncodingIndicator { pub fn get_current_encoding(&self, cx: &mut Context, editor: WeakEntity) {} + + pub fn new( + encoding: Option<&'static dyn encoding::Encoding>, + workspace: WeakEntity, + observe: Option, + ) -> EncodingIndicator { + EncodingIndicator { + encoding, + workspace, + observe, + } + } + + pub fn update( + &mut self, + editor: Entity, + _: &mut Window, + cx: &mut Context, + ) { + let editor = editor.read(cx); + if let Some((_, buffer, _)) = editor.active_excerpt(cx) { + let encoding = buffer.read(cx).encoding; + self.encoding = Some(encoding); + } + + cx.notify(); + } } impl StatusItemView for EncodingIndicator { fn set_active_pane_item( &mut self, - _active_pane_item: Option<&dyn ItemHandle>, - _window: &mut Window, - _cx: &mut Context, + active_pane_item: Option<&dyn ItemHandle>, + window: &mut Window, + cx: &mut Context, ) { + match active_pane_item.and_then(|item| item.downcast::()) { + Some(editor) => { + self.observe = Some(cx.observe_in(&editor, window, Self::update)); + self.update(editor, window, cx); + } + None => { + self.encoding = None; + self.observe = None; + } + } } } diff --git a/crates/encodings/src/selectors.rs b/crates/encodings/src/selectors.rs index 368f85d584..fd06c518ef 100644 --- a/crates/encodings/src/selectors.rs +++ b/crates/encodings/src/selectors.rs @@ -3,31 +3,49 @@ pub mod save_or_reopen { use gpui::{AppContext, ParentElement}; use picker::Picker; use picker::PickerDelegate; + use std::cell::RefCell; + use std::ops::{Deref, DerefMut}; + use std::rc::Rc; + use std::sync::Arc; use std::sync::atomic::AtomicBool; use util::ResultExt; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{DismissEvent, Entity, EventEmitter, Focusable, WeakEntity}; - use ui::{Context, Label, ListItem, Render, Window, rems, v_flex}; + use ui::{Context, HighlightedLabel, Label, ListItem, Render, Window, rems, v_flex}; use workspace::{ModalView, Workspace}; + use crate::selectors::encoding::{Action, EncodingSelector, EncodingSelectorDelegate}; + pub struct EncodingSaveOrReopenSelector { picker: Entity>, + pub current_selection: usize, + workspace: WeakEntity, } impl EncodingSaveOrReopenSelector { - pub fn new(window: &mut Window, cx: &mut Context) -> Self { - let delegate = EncodingSaveOrReopenDelegate::new(cx.entity().downgrade()); + pub fn new( + window: &mut Window, + cx: &mut Context, + workspace: WeakEntity, + ) -> Self { + let delegate = + EncodingSaveOrReopenDelegate::new(cx.entity().downgrade(), workspace.clone()); let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); - Self { picker } + Self { + picker, + current_selection: 0, + workspace, + } } pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { + let weak_workspace = workspace.weak_handle(); workspace.toggle_modal(window, cx, |window, cx| { - EncodingSaveOrReopenSelector::new(window, cx) + EncodingSaveOrReopenSelector::new(window, cx, weak_workspace) }); } } @@ -53,28 +71,57 @@ pub mod save_or_reopen { impl EventEmitter for EncodingSaveOrReopenSelector {} pub struct EncodingSaveOrReopenDelegate { - encoding_selector: WeakEntity, + selector: WeakEntity, current_selection: usize, matches: Vec, pub actions: Vec, + workspace: WeakEntity, } impl EncodingSaveOrReopenDelegate { - pub fn new(selector: WeakEntity) -> Self { + pub fn new( + selector: WeakEntity, + workspace: WeakEntity, + ) -> Self { Self { - encoding_selector: selector, + selector, current_selection: 0, matches: Vec::new(), actions: vec![ StringMatchCandidate::new(0, "Save with encoding"), StringMatchCandidate::new(1, "Reopen with encoding"), ], + workspace, } } pub fn get_actions(&self) -> (&str, &str) { (&self.actions[0].string, &self.actions[1].string) } + + pub fn post_selection( + &self, + cx: &mut Context>, + window: &mut Window, + ) { + if self.current_selection == 0 { + if let Some(workspace) = self.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(window, cx, |window, cx| { + EncodingSelector::new(window, cx, Action::Save) + }) + }); + } + } else if self.current_selection == 1 { + if let Some(workspace) = self.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(window, cx, |window, cx| { + EncodingSelector::new(window, cx, Action::Reopen) + }) + }); + } + } + } } impl PickerDelegate for EncodingSaveOrReopenDelegate { @@ -92,9 +139,14 @@ pub mod save_or_reopen { &mut self, ix: usize, _window: &mut Window, - _cx: &mut Context>, + cx: &mut Context>, ) { self.current_selection = ix; + self.selector + .update(cx, |selector, cx| { + selector.current_selection = ix; + }) + .log_err(); } fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc { @@ -137,24 +189,31 @@ pub mod save_or_reopen { this.update(cx, |picker, cx| { let delegate = &mut picker.delegate; - delegate.current_selection = matches.len().saturating_sub(1); delegate.matches = matches; + delegate.current_selection = delegate + .current_selection + .min(delegate.matches.len().saturating_sub(1)); + delegate + .selector + .update(cx, |selector, cx| { + selector.current_selection = delegate.current_selection + }) + .log_err(); cx.notify(); }) .log_err(); }) } - fn confirm( - &mut self, - secondary: bool, - window: &mut Window, - cx: &mut Context>, - ) { + fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { + self.dismissed(window, cx); + if self.selector.is_upgradable() { + self.post_selection(cx, window); + } } fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { - self.encoding_selector + self.selector .update(cx, |_, cx| cx.emit(DismissEvent)) .log_err(); } @@ -162,11 +221,18 @@ pub mod save_or_reopen { fn render_match( &self, ix: usize, - selected: bool, - window: &mut Window, - cx: &mut Context>, + _: bool, + _: &mut Window, + _: &mut Context>, ) -> Option { - Some(ListItem::new(ix).child(Label::new(&self.matches[ix].string))) + Some( + ListItem::new(ix) + .child(HighlightedLabel::new( + &self.matches[ix].string, + self.matches[ix].positions.clone(), + )) + .spacing(ui::ListItemSpacing::Sparse), + ) } } @@ -176,19 +242,28 @@ pub mod save_or_reopen { } pub mod encoding { - use std::sync::atomic::AtomicBool; + use std::{ + ops::DerefMut, + rc::{Rc, Weak}, + sync::{Arc, atomic::AtomicBool}, + }; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - AppContext, BackgroundExecutor, DismissEvent, Entity, EventEmitter, Focusable, WeakEntity, + AppContext, BackgroundExecutor, DismissEvent, Entity, EventEmitter, Focusable, Length, + WeakEntity, actions, }; use picker::{Picker, PickerDelegate}; - use ui::{Context, Label, ListItem, ParentElement, Render, Styled, Window, rems, v_flex}; + use ui::{ + Context, DefiniteLength, HighlightedLabel, Label, ListItem, ListItemSpacing, ParentElement, + Render, Styled, Window, rems, v_flex, + }; use util::{ResultExt, TryFutureExt}; use workspace::{ModalView, Workspace}; pub struct EncodingSelector { - pub picker: Entity>, + picker: Entity>, + action: Action, } pub struct EncodingSelectorDelegate { @@ -204,7 +279,44 @@ pub mod encoding { current_selection: 0, encodings: vec![ StringMatchCandidate::new(0, "UTF-8"), - StringMatchCandidate::new(1, "ISO 8859-1"), + StringMatchCandidate::new(1, "UTF-16 LE"), + StringMatchCandidate::new(2, "UTF-16 BE"), + StringMatchCandidate::new(3, "IBM866"), + StringMatchCandidate::new(4, "ISO 8859-1"), + StringMatchCandidate::new(5, "ISO 8859-2"), + StringMatchCandidate::new(6, "ISO 8859-3"), + StringMatchCandidate::new(7, "ISO 8859-4"), + StringMatchCandidate::new(8, "ISO 8859-5"), + StringMatchCandidate::new(9, "ISO 8859-6"), + StringMatchCandidate::new(10, "ISO 8859-7"), + StringMatchCandidate::new(11, "ISO 8859-8"), + StringMatchCandidate::new(12, "ISO 8859-10"), + StringMatchCandidate::new(13, "ISO 8859-13"), + StringMatchCandidate::new(14, "ISO 8859-14"), + StringMatchCandidate::new(15, "ISO 8859-15"), + StringMatchCandidate::new(16, "ISO 8859-16"), + StringMatchCandidate::new(17, "KOI8-R"), + StringMatchCandidate::new(18, "KOI8-U"), + StringMatchCandidate::new(19, "MacRoman"), + StringMatchCandidate::new(20, "Mac Cyrillic"), + StringMatchCandidate::new(21, "Windows-874"), + StringMatchCandidate::new(22, "Windows-1250"), + StringMatchCandidate::new(23, "Windows-1251"), + StringMatchCandidate::new(24, "Windows-1252"), + StringMatchCandidate::new(25, "Windows-1253"), + StringMatchCandidate::new(26, "Windows-1254"), + StringMatchCandidate::new(27, "Windows-1255"), + StringMatchCandidate::new(28, "Windows-1256"), + StringMatchCandidate::new(29, "Windows-1257"), + StringMatchCandidate::new(30, "Windows-1258"), + StringMatchCandidate::new(31, "EUC-KR"), + StringMatchCandidate::new(32, "EUC-JP"), + StringMatchCandidate::new(33, "Shift_JIS"), + StringMatchCandidate::new(34, "ISO 2022-JP"), + StringMatchCandidate::new(35, "GBK"), + StringMatchCandidate::new(36, "GB18030"), + StringMatchCandidate::new(37, "Big5"), + StringMatchCandidate::new(38, "HZ-GB-2312"), ], matches: Vec::new(), selector, @@ -244,7 +356,6 @@ pub mod encoding { ) -> gpui::Task<()> { let executor = cx.background_executor().clone(); let encodings = self.encodings.clone(); - let current_selection = self.current_selection; cx.spawn_in(window, async move |picker, cx| { let matches: Vec; @@ -264,14 +375,25 @@ pub mod encoding { matches = fuzzy::match_strings( &encodings, &query, + true, false, - false, - 0, + 38, &AtomicBool::new(false), executor, ) .await } + + picker + .update(cx, |picker, cx| { + let delegate = &mut picker.delegate; + delegate.matches = matches; + delegate.current_selection = delegate + .current_selection + .min(delegate.matches.len().saturating_sub(1)); + cx.notify(); + }) + .log_err(); }) } @@ -296,20 +418,32 @@ pub mod encoding { window: &mut Window, cx: &mut Context>, ) -> Option { - Some(ListItem::new(ix).child(Label::new(&self.matches[ix].string))) + Some( + ListItem::new(ix) + .child(HighlightedLabel::new( + &self.matches[ix].string, + self.matches[ix].positions.clone(), + )) + .spacing(ListItemSpacing::Sparse), + ) } } + pub enum Action { + Save, + Reopen, + } + impl EncodingSelector { - pub fn new(window: &mut Window, cx: &mut Context) -> EncodingSelector { + pub fn new( + window: &mut Window, + cx: &mut Context, + action: Action, + ) -> EncodingSelector { let delegate = EncodingSelectorDelegate::new(cx.entity().downgrade()); let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); - EncodingSelector { picker: picker } - } - - pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { - workspace.toggle_modal(window, cx, |window, cx| EncodingSelector::new(window, cx)); + EncodingSelector { picker, action } } } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 85c24ef0ac..610e3f4aaf 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -127,7 +127,7 @@ pub struct Buffer { has_unsaved_edits: Cell<(clock::Global, bool)>, change_bits: Vec>>, _subscriptions: Vec, - encoding: &'static dyn encoding::Encoding, + pub encoding: &'static dyn encoding::Encoding, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index a91f13ce58..56db114554 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -168,6 +168,7 @@ zed_actions.workspace = true zeta.workspace = true zlog.workspace = true zlog_settings.workspace = true +encoding = "0.2.33" [target.'cfg(target_os = "windows")'.dependencies] windows.workspace = true diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 36189c1110..403fed5978 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -397,10 +397,8 @@ pub fn initialize_workspace( } }); - let encoding_indicator = cx.new(|_cx| encodings::EncodingIndicator { - encoding: encodings::Encoding::Utf8, - workspace: workspace_handle.downgrade(), - }); + let encoding_indicator = + cx.new(|_cx| encodings::EncodingIndicator::new(None, workspace.weak_handle(), None)); let cursor_position = cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace)); From 43357f689c69d9ae9e93f00bcf77d99552d642a6 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sun, 24 Aug 2025 20:34:33 +0530 Subject: [PATCH 4/7] Make the status bar encoding indicator update the encoding when an encoding from the selector is chosen. --- Cargo.lock | 1 + crates/encodings/Cargo.toml | 1 + crates/encodings/src/lib.rs | 100 +++++++++++++++++++++++++++++- crates/encodings/src/selectors.rs | 55 ++++++++++++---- 4 files changed, 144 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0002c8be84..18b606eb24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5275,6 +5275,7 @@ dependencies = [ "encoding", "fuzzy", "gpui", + "language", "picker", "ui", "util", diff --git a/crates/encodings/Cargo.toml b/crates/encodings/Cargo.toml index 4b8d877a3a..70b11dd545 100644 --- a/crates/encodings/Cargo.toml +++ b/crates/encodings/Cargo.toml @@ -13,6 +13,7 @@ util.workspace = true fuzzy.workspace = true editor.workspace = true encoding = "0.2.33" +language.workspace = true [lints] workspace = true diff --git a/crates/encodings/src/lib.rs b/crates/encodings/src/lib.rs index 6387919770..169b4bf350 100644 --- a/crates/encodings/src/lib.rs +++ b/crates/encodings/src/lib.rs @@ -1,5 +1,12 @@ use editor::Editor; use encoding::Encoding; +use encoding::all::{ + BIG5_2003, EUC_JP, GB18030, GBK, HZ, IBM866, ISO_2022_JP, ISO_8859_1, ISO_8859_2, ISO_8859_3, + ISO_8859_4, ISO_8859_5, ISO_8859_6, ISO_8859_7, ISO_8859_8, ISO_8859_10, ISO_8859_13, + ISO_8859_14, ISO_8859_15, ISO_8859_16, KOI8_R, KOI8_U, MAC_CYRILLIC, MAC_ROMAN, UTF_8, + UTF_16BE, UTF_16LE, WINDOWS_874, WINDOWS_949, WINDOWS_1250, WINDOWS_1251, WINDOWS_1252, + WINDOWS_1253, WINDOWS_1254, WINDOWS_1255, WINDOWS_1256, WINDOWS_1257, WINDOWS_1258, +}; use gpui::{ClickEvent, Entity, Subscription, WeakEntity}; use ui::{Button, ButtonCommon, Context, LabelSize, Render, Tooltip, Window, div}; use ui::{Clickable, ParentElement}; @@ -20,7 +27,7 @@ impl Render for EncodingIndicator { let status_element = div(); status_element.child( - Button::new("encoding", get_current_encoding()) + Button::new("encoding", encoding_name(self.encoding.unwrap_or(UTF_8))) .label_size(LabelSize::Small) .tooltip(Tooltip::text("Select Encoding")) .on_click(cx.listener(|indicator, _: &ClickEvent, window, cx| { @@ -85,3 +92,94 @@ impl StatusItemView for EncodingIndicator { } } } + +pub fn encoding_name(encoding: &'static dyn Encoding) -> String { + let name = encoding.name(); + + match () { + () if name == UTF_8.name() => "UTF-8", + () if name == UTF_16LE.name() => "UTF-16 LE", + () if name == UTF_16BE.name() => "UTF-16 BE", + () if name == IBM866.name() => "IBM866", + () if name == ISO_8859_1.name() => "ISO 8859-1", + () if name == ISO_8859_2.name() => "ISO 8859-2", + () if name == ISO_8859_3.name() => "ISO 8859-3", + () if name == ISO_8859_4.name() => "ISO 8859-4", + () if name == ISO_8859_5.name() => "ISO 8859-5", + () if name == ISO_8859_6.name() => "ISO 8859-6", + () if name == ISO_8859_7.name() => "ISO 8859-7", + () if name == ISO_8859_8.name() => "ISO 8859-8", + () if name == ISO_8859_10.name() => "ISO 8859-10", + () if name == ISO_8859_13.name() => "ISO 8859-13", + () if name == ISO_8859_14.name() => "ISO 8859-14", + () if name == ISO_8859_15.name() => "ISO 8859-15", + () if name == ISO_8859_16.name() => "ISO 8859-16", + () if name == KOI8_R.name() => "KOI8-R", + () if name == KOI8_U.name() => "KOI8-U", + () if name == MAC_ROMAN.name() => "MacRoman", + () if name == MAC_CYRILLIC.name() => "Mac Cyrillic", + () if name == WINDOWS_874.name() => "Windows-874", + () if name == WINDOWS_1250.name() => "Windows-1250", + () if name == WINDOWS_1251.name() => "Windows-1251", + () if name == WINDOWS_1252.name() => "Windows-1252", + () if name == WINDOWS_1253.name() => "Windows-1253", + () if name == WINDOWS_1254.name() => "Windows-1254", + () if name == WINDOWS_1255.name() => "Windows-1255", + () if name == WINDOWS_1256.name() => "Windows-1256", + () if name == WINDOWS_1257.name() => "Windows-1257", + () if name == WINDOWS_1258.name() => "Windows-1258", + () if name == WINDOWS_949.name() => "Windows-949", + () if name == EUC_JP.name() => "EUC-JP", + () if name == ISO_2022_JP.name() => "ISO 2022-JP", + () if name == GBK.name() => "GBK", + () if name == GB18030.name() => "GB18030", + () if name == BIG5_2003.name() => "Big5", + () if name == HZ.name() => "HZ-GB-2312", + _ => "", + } + .to_string() +} + +pub fn encoding_from_index(index: usize) -> &'static dyn Encoding { + match index { + 0 => UTF_8, + 1 => UTF_16LE, + 2 => UTF_16BE, + 3 => IBM866, + 4 => ISO_8859_1, + 5 => ISO_8859_2, + 6 => ISO_8859_3, + 7 => ISO_8859_4, + 8 => ISO_8859_5, + 9 => ISO_8859_6, + 10 => ISO_8859_7, + 11 => ISO_8859_8, + 12 => ISO_8859_10, + 13 => ISO_8859_13, + 14 => ISO_8859_14, + 15 => ISO_8859_15, + 16 => ISO_8859_16, + 17 => KOI8_R, + 18 => KOI8_U, + 19 => MAC_ROMAN, + 20 => MAC_CYRILLIC, + 21 => WINDOWS_874, + 22 => WINDOWS_1250, + 23 => WINDOWS_1251, + 24 => WINDOWS_1252, + 25 => WINDOWS_1253, + 26 => WINDOWS_1254, + 27 => WINDOWS_1255, + 28 => WINDOWS_1256, + 29 => WINDOWS_1257, + 30 => WINDOWS_1258, + 31 => WINDOWS_949, + 32 => EUC_JP, + 33 => ISO_2022_JP, + 34 => GBK, + 35 => GB18030, + 36 => BIG5_2003, + 37 => HZ, + _ => UTF_8, + } +} diff --git a/crates/encodings/src/selectors.rs b/crates/encodings/src/selectors.rs index fd06c518ef..cdc0702e53 100644 --- a/crates/encodings/src/selectors.rs +++ b/crates/encodings/src/selectors.rs @@ -1,4 +1,5 @@ pub mod save_or_reopen { + use editor::Editor; use gpui::Styled; use gpui::{AppContext, ParentElement}; use picker::Picker; @@ -103,24 +104,40 @@ pub mod save_or_reopen { &self, cx: &mut Context>, window: &mut Window, - ) { + ) -> Option<()> { if self.current_selection == 0 { if let Some(workspace) = self.workspace.upgrade() { + let (_, buffer, _) = workspace + .read(cx) + .active_item(cx)? + .act_as::(cx)? + .read(cx) + .active_excerpt(cx)?; + workspace.update(cx, |workspace, cx| { workspace.toggle_modal(window, cx, |window, cx| { - EncodingSelector::new(window, cx, Action::Save) + EncodingSelector::new(window, cx, Action::Save, buffer.downgrade()) }) }); } } else if self.current_selection == 1 { if let Some(workspace) = self.workspace.upgrade() { + let (_, buffer, _) = workspace + .read(cx) + .active_item(cx)? + .act_as::(cx)? + .read(cx) + .active_excerpt(cx)?; + workspace.update(cx, |workspace, cx| { workspace.toggle_modal(window, cx, |window, cx| { - EncodingSelector::new(window, cx, Action::Reopen) + EncodingSelector::new(window, cx, Action::Reopen, buffer.downgrade()) }) }); } } + + Some(()) } } @@ -253,6 +270,7 @@ pub mod encoding { AppContext, BackgroundExecutor, DismissEvent, Entity, EventEmitter, Focusable, Length, WeakEntity, actions, }; + use language::Buffer; use picker::{Picker, PickerDelegate}; use ui::{ Context, DefiniteLength, HighlightedLabel, Label, ListItem, ListItemSpacing, ParentElement, @@ -261,6 +279,8 @@ pub mod encoding { use util::{ResultExt, TryFutureExt}; use workspace::{ModalView, Workspace}; + use crate::encoding_from_index; + pub struct EncodingSelector { picker: Entity>, action: Action, @@ -271,10 +291,14 @@ pub mod encoding { encodings: Vec, matches: Vec, selector: WeakEntity, + buffer: WeakEntity, } impl EncodingSelectorDelegate { - pub fn new(selector: WeakEntity) -> EncodingSelectorDelegate { + pub fn new( + selector: WeakEntity, + buffer: WeakEntity, + ) -> EncodingSelectorDelegate { EncodingSelectorDelegate { current_selection: 0, encodings: vec![ @@ -309,17 +333,17 @@ pub mod encoding { StringMatchCandidate::new(28, "Windows-1256"), StringMatchCandidate::new(29, "Windows-1257"), StringMatchCandidate::new(30, "Windows-1258"), - StringMatchCandidate::new(31, "EUC-KR"), + StringMatchCandidate::new(31, "Windows-949"), StringMatchCandidate::new(32, "EUC-JP"), - StringMatchCandidate::new(33, "Shift_JIS"), - StringMatchCandidate::new(34, "ISO 2022-JP"), - StringMatchCandidate::new(35, "GBK"), - StringMatchCandidate::new(36, "GB18030"), - StringMatchCandidate::new(37, "Big5"), - StringMatchCandidate::new(38, "HZ-GB-2312"), + StringMatchCandidate::new(33, "ISO 2022-JP"), + StringMatchCandidate::new(34, "GBK"), + StringMatchCandidate::new(35, "GB18030"), + StringMatchCandidate::new(36, "Big5"), + StringMatchCandidate::new(37, "HZ-GB-2312"), ], matches: Vec::new(), selector, + buffer, } } } @@ -403,6 +427,12 @@ pub mod encoding { window: &mut Window, cx: &mut Context>, ) { + if let Some(buffer) = self.buffer.upgrade() { + buffer.update(cx, |buffer, cx| { + buffer.encoding = encoding_from_index(self.current_selection) + }); + } + self.dismissed(window, cx); } fn dismissed(&mut self, window: &mut Window, cx: &mut Context>) { @@ -439,8 +469,9 @@ pub mod encoding { window: &mut Window, cx: &mut Context, action: Action, + buffer: WeakEntity, ) -> EncodingSelector { - let delegate = EncodingSelectorDelegate::new(cx.entity().downgrade()); + let delegate = EncodingSelectorDelegate::new(cx.entity().downgrade(), buffer); let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); EncodingSelector { picker, action } From 0e1f9f689c81f4ce53a4025026e9e60e041d07e8 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Mon, 25 Aug 2025 12:39:12 +0530 Subject: [PATCH 5/7] =?UTF-8?q?Add=20a=20new=20`load=5Fwith=5Fencoding`=20?= =?UTF-8?q?function=20to=20handle=20files=20with=20various=20encodings.=20?= =?UTF-8?q?Modified=20`Buffer::reload`=20in=20`buffer.rs`=20to=20use=20thi?= =?UTF-8?q?s=20new=20function,=20allowing=20Zed=20to=20open=20files=20with?= =?UTF-8?q?=20any=20encoding=20in=20UTF-8=20mode.=20Files=20with=20charact?= =?UTF-8?q?ers=20that=20are=20invalid=20in=20UTF-8=20will=20have=20those?= =?UTF-8?q?=20bytes=20replaced=20with=20the=20=EF=BF=BD=20character.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comments and documentation. --- Cargo.lock | 2 ++ crates/copilot/Cargo.toml | 2 ++ crates/copilot/src/copilot.rs | 5 ++++ crates/encodings/src/lib.rs | 6 +++- crates/encodings/src/selectors.rs | 7 +++++ crates/fs/src/encodings.rs | 47 ++++++++++++++++++++++--------- crates/fs/src/fs.rs | 21 ++++++++++++-- crates/language/src/buffer.rs | 22 +++++++++++++-- crates/worktree/Cargo.toml | 2 ++ crates/worktree/src/worktree.rs | 19 ++++++++++++- 10 files changed, 112 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 18b606eb24..8c0d73d9b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3643,6 +3643,7 @@ dependencies = [ "dirs 4.0.0", "edit_prediction", "editor", + "encoding", "fs", "futures 0.3.31", "gpui", @@ -20107,6 +20108,7 @@ dependencies = [ "anyhow", "clock", "collections", + "encoding", "fs", "futures 0.3.31", "fuzzy", diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 0fc119f311..4a3a6b5c8e 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -53,6 +53,8 @@ util.workspace = true workspace.workspace = true workspace-hack.workspace = true itertools.workspace = true +encoding = "0.2.33" + [target.'cfg(windows)'.dependencies] async-std = { version = "1.12.0", features = ["unstable"] } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index b7d8423fd7..355df6b41e 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1193,6 +1193,7 @@ async fn get_copilot_lsp(fs: Arc, node_runtime: NodeRuntime) -> anyhow:: #[cfg(test)] mod tests { use super::*; + use encoding::Encoding; use gpui::TestAppContext; use util::path; @@ -1406,6 +1407,10 @@ mod tests { fn load_bytes(&self, _cx: &App) -> Task>> { unimplemented!() } + + fn load_with_encoding(&self, _: &App, _: &'static dyn Encoding) -> Task> { + unimplemented!() + } } } diff --git a/crates/encodings/src/lib.rs b/crates/encodings/src/lib.rs index 169b4bf350..5315515998 100644 --- a/crates/encodings/src/lib.rs +++ b/crates/encodings/src/lib.rs @@ -14,10 +14,11 @@ use workspace::{ItemHandle, StatusItemView, Workspace}; use crate::selectors::save_or_reopen::{EncodingSaveOrReopenSelector, get_current_encoding}; +/// A status bar item that shows the current file encoding and allows changing it. pub struct EncodingIndicator { pub encoding: Option<&'static dyn Encoding>, pub workspace: WeakEntity, - observe: Option, + observe: Option, // Subscription to observe changes in the active editor } pub mod selectors; @@ -93,6 +94,7 @@ impl StatusItemView for EncodingIndicator { } } +/// Get a human-readable name for the given encoding. pub fn encoding_name(encoding: &'static dyn Encoding) -> String { let name = encoding.name(); @@ -140,6 +142,8 @@ pub fn encoding_name(encoding: &'static dyn Encoding) -> String { .to_string() } +/// Get an encoding from its index in the predefined list. +/// If the index is out of range, UTF-8 is returned as a default. pub fn encoding_from_index(index: usize) -> &'static dyn Encoding { match index { 0 => UTF_8, diff --git a/crates/encodings/src/selectors.rs b/crates/encodings/src/selectors.rs index cdc0702e53..c25b56be56 100644 --- a/crates/encodings/src/selectors.rs +++ b/crates/encodings/src/selectors.rs @@ -19,6 +19,8 @@ pub mod save_or_reopen { use crate::selectors::encoding::{Action, EncodingSelector, EncodingSelectorDelegate}; + /// A modal view that allows the user to select between saving with a different encoding or + /// reopening with a different encoding. pub struct EncodingSaveOrReopenSelector { picker: Entity>, pub current_selection: usize, @@ -43,6 +45,8 @@ pub mod save_or_reopen { } } + /// Toggle the modal view for selecting between saving with a different encoding or + /// reopening with a different encoding. pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { let weak_workspace = workspace.weak_handle(); workspace.toggle_modal(window, cx, |window, cx| { @@ -100,6 +104,7 @@ pub mod save_or_reopen { (&self.actions[0].string, &self.actions[1].string) } + /// Handle the action selected by the user. pub fn post_selection( &self, cx: &mut Context>, @@ -281,6 +286,7 @@ pub mod encoding { use crate::encoding_from_index; + /// A modal view that allows the user to select an encoding from a list of encodings. pub struct EncodingSelector { picker: Entity>, action: Action, @@ -459,6 +465,7 @@ pub mod encoding { } } + /// The action to perform after selecting an encoding. pub enum Action { Save, Reopen, diff --git a/crates/fs/src/encodings.rs b/crates/fs/src/encodings.rs index 8fb38ff24f..b0a1264a14 100644 --- a/crates/fs/src/encodings.rs +++ b/crates/fs/src/encodings.rs @@ -1,21 +1,40 @@ +use anyhow::{Error, Result}; + use encoding::Encoding; -pub enum CharacterEncoding { - Utf8, - Iso8859_1, - Cp865, -} +/// A wrapper around `encoding::Encoding` to implement `Send` and `Sync`. +/// Since the reference is static, it is safe to send it across threads. +pub struct EncodingWrapper(&'static dyn Encoding); -pub fn to_utf8<'a>(input: Vec, encoding: &'a impl encoding::Encoding) -> String { - match encoding.decode(&input, encoding::DecoderTrap::Strict) { - Ok(v) => return v, - Err(_) => panic!(), +unsafe impl Send for EncodingWrapper {} +unsafe impl Sync for EncodingWrapper {} + +impl EncodingWrapper { + pub fn new(encoding: &'static dyn Encoding) -> EncodingWrapper { + EncodingWrapper(encoding) + } + + pub async fn decode(&self, input: Vec) -> Result { + match self.0.decode(&input, encoding::DecoderTrap::Replace) { + Ok(v) => Ok(v), + Err(e) => Err(Error::msg(e.to_string())), + } + } + + pub async fn encode(&self, input: String) -> Result> { + match self.0.encode(&input, encoding::EncoderTrap::Replace) { + Ok(v) => Ok(v), + Err(e) => Err(Error::msg(e.to_string())), + } } } -pub fn to<'a>(input: String, target: &'a impl encoding::Encoding) -> Vec { - match target.encode(&input, encoding::EncoderTrap::Strict) { - Ok(v) => v, - Err(_) => panic!(), - } +/// Convert a byte vector from a specified encoding to a UTF-8 string. +pub async fn to_utf8<'a>(input: Vec, encoding: EncodingWrapper) -> Result { + Ok(encoding.decode(input).await?) +} + +/// Convert a UTF-8 string to a byte vector in a specified encoding. +pub async fn from_utf8<'a>(input: String, target: EncodingWrapper) -> Result> { + Ok(target.encode(input).await?) } diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 75312c5c0c..cb3b649b76 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -1,6 +1,7 @@ #[cfg(target_os = "macos")] mod mac_watcher; +pub mod encodings; #[cfg(not(target_os = "macos"))] pub mod fs_watcher; @@ -54,6 +55,8 @@ use smol::io::AsyncReadExt; #[cfg(any(test, feature = "test-support"))] use std::ffi::OsStr; +use crate::encodings::EncodingWrapper; + pub trait Watcher: Send + Sync { fn add(&self, path: &Path) -> Result<()>; fn remove(&self, path: &Path) -> Result<()>; @@ -108,6 +111,16 @@ pub trait Fs: Send + Sync { async fn load(&self, path: &Path) -> Result { Ok(String::from_utf8(self.load_bytes(path).await?)?) } + + /// Load a file with the specified encoding, returning a UTF-8 string. + async fn load_with_encoding( + &self, + path: PathBuf, + encoding: EncodingWrapper, + ) -> anyhow::Result { + Ok(encodings::to_utf8(self.load_bytes(path.as_path()).await?, encoding).await?) + } + async fn load_bytes(&self, path: &Path) -> Result>; async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>; async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>; @@ -539,8 +552,12 @@ impl Fs for RealFs { async fn load(&self, path: &Path) -> Result { let path = path.to_path_buf(); - let text = smol::unblock(|| std::fs::read_to_string(path)).await?; - Ok(text) + let encoding = EncodingWrapper::new(encoding::all::UTF_8); + let text = + smol::unblock(async || Ok(encodings::to_utf8(std::fs::read(path)?, encoding).await?)) + .await + .await; + text } async fn load_bytes(&self, path: &Path) -> Result> { let path = path.to_path_buf(); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 610e3f4aaf..44a5dacc2d 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -21,7 +21,8 @@ use anyhow::{Context as _, Result}; pub use clock::ReplicaId; use clock::{AGENT_REPLICA_ID, Lamport}; use collections::HashMap; -use fs::MTime; +use encoding::Encoding; +use fs::{Fs, MTime, RealFs}; use futures::channel::oneshot; use gpui::{ App, AppContext as _, Context, Entity, EventEmitter, HighlightStyle, SharedString, StyledText, @@ -401,6 +402,10 @@ pub trait LocalFile: File { /// Loads the file's contents from disk. fn load_bytes(&self, cx: &App) -> Task>>; + + /// Loads the file contents from disk, decoding them with the given encoding. + fn load_with_encoding(&self, cx: &App, encoding: &'static dyn Encoding) + -> Task>; } /// The auto-indent behavior associated with an editing operation. @@ -1276,12 +1281,15 @@ impl Buffer { /// Reloads the contents of the buffer from disk. pub fn reload(&mut self, cx: &Context) -> oneshot::Receiver> { let (tx, rx) = futures::channel::oneshot::channel(); + let encoding = self.encoding.clone(); let prev_version = self.text.version(); self.reload_task = Some(cx.spawn(async move |this, cx| { let Some((new_mtime, new_text)) = this.update(cx, |this, cx| { let file = this.file.as_ref()?.as_local()?; - - Some((file.disk_state().mtime(), file.load(cx))) + Some(( + file.disk_state().mtime(), + file.load_with_encoding(cx, encoding), + )) })? else { return Ok(()); @@ -4967,6 +4975,14 @@ impl LocalFile for TestFile { fn load_bytes(&self, _cx: &App) -> Task>> { unimplemented!() } + + fn load_with_encoding( + &self, + cx: &App, + encoding: &'static dyn Encoding, + ) -> Task> { + unimplemented!() + } } pub(crate) fn contiguous_ranges( diff --git a/crates/worktree/Cargo.toml b/crates/worktree/Cargo.toml index db264fe3aa..6dd398dfc8 100644 --- a/crates/worktree/Cargo.toml +++ b/crates/worktree/Cargo.toml @@ -48,6 +48,8 @@ sum_tree.workspace = true text.workspace = true util.workspace = true workspace-hack.workspace = true +encoding = "0.2.33" + [dev-dependencies] clock = { workspace = true, features = ["test-support"] } diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index cf61ee2669..c255877b3a 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -7,7 +7,11 @@ use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{Context as _, Result, anyhow}; use clock::ReplicaId; use collections::{HashMap, HashSet, VecDeque}; -use fs::{Fs, MTime, PathEvent, RemoveOptions, Watcher, copy_recursive, read_dir_items}; +use encoding::Encoding; +use fs::{ + Fs, MTime, PathEvent, RemoveOptions, Watcher, copy_recursive, encodings::EncodingWrapper, + read_dir_items, +}; use futures::{ FutureExt as _, Stream, StreamExt, channel::{ @@ -3361,6 +3365,19 @@ impl language::LocalFile for File { let fs = worktree.fs.clone(); cx.background_spawn(async move { fs.load_bytes(&abs_path?).await }) } + + fn load_with_encoding( + &self, + cx: &App, + encoding: &'static dyn Encoding, + ) -> Task> { + let worktree = self.worktree.read(cx).as_local().unwrap(); + let path = worktree.absolutize(&self.path); + let fs = worktree.fs.clone(); + + let encoding = EncodingWrapper::new(encoding); + cx.background_spawn(async move { fs.load_with_encoding(path?, encoding).await }) + } } impl File { From a3f5e91f0fe32d6a375c0bccef913d3f9c904ca9 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Tue, 26 Aug 2025 22:38:47 +0530 Subject: [PATCH 6/7] Add support for saving and opening files in different encodings. The implementation is now complete. --- Cargo.lock | 10 ++ Cargo.toml | 1 + crates/agent2/Cargo.toml | 2 + crates/agent2/src/tools/edit_file_tool.rs | 5 +- crates/assistant_tools/Cargo.toml | 1 + crates/assistant_tools/src/edit_file_tool.rs | 5 +- crates/collab/Cargo.toml | 1 + crates/collab/src/tests/integration_tests.rs | 5 +- .../random_project_collaboration_tests.rs | 10 +- crates/copilot/Cargo.toml | 2 +- crates/encodings/Cargo.toml | 3 +- crates/encodings/src/lib.rs | 50 +++++++- crates/encodings/src/selectors.rs | 115 +++++++++++------- crates/extension_host/Cargo.toml | 1 + crates/extension_host/src/extension_host.rs | 16 ++- crates/fs/Cargo.toml | 4 +- crates/fs/src/encodings.rs | 58 ++++++++- crates/fs/src/fs.rs | 33 ++++- crates/git_ui/Cargo.toml | 1 + crates/git_ui/src/file_diff_view.rs | 5 +- crates/language/Cargo.toml | 4 +- crates/language/src/buffer.rs | 10 +- crates/project/Cargo.toml | 2 + crates/project/src/buffer_store.rs | 4 +- crates/project/src/prettier_store.rs | 5 +- crates/project/src/project_tests.rs | 26 +++- crates/remote_server/Cargo.toml | 1 + .../remote_server/src/remote_editing_tests.rs | 6 +- crates/vim/Cargo.toml | 1 + crates/workspace/Cargo.toml | 1 + crates/workspace/src/workspace.rs | 13 +- crates/worktree/Cargo.toml | 2 +- crates/worktree/src/worktree.rs | 11 +- crates/worktree/src/worktree_tests.rs | 28 ++++- crates/zed/Cargo.toml | 2 +- crates/zed/src/zed.rs | 10 ++ 36 files changed, 362 insertions(+), 92 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c0d73d9b6..83a14b5867 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -228,6 +228,7 @@ dependencies = [ "ctor", "db", "editor", + "encoding", "env_logger 0.11.8", "fs", "futures 0.3.31", @@ -961,6 +962,7 @@ dependencies = [ "derive_more", "diffy", "editor", + "encoding", "feature_flags", "fs", "futures 0.3.31", @@ -3302,6 +3304,7 @@ dependencies = [ "dashmap 6.1.0", "debugger_ui", "editor", + "encoding", "envy", "extension", "file_finder", @@ -5272,6 +5275,7 @@ dependencies = [ name = "encodings" version = "0.1.0" dependencies = [ + "anyhow", "editor", "encoding", "fuzzy", @@ -5626,6 +5630,7 @@ dependencies = [ "criterion", "ctor", "dap", + "encoding", "extension", "fs", "futures 0.3.31", @@ -6136,6 +6141,7 @@ dependencies = [ "paths", "proto", "rope", + "schemars", "serde", "serde_json", "smol", @@ -6561,6 +6567,7 @@ dependencies = [ "ctor", "db", "editor", + "encoding", "futures 0.3.31", "fuzzy", "git", @@ -12644,6 +12651,7 @@ dependencies = [ "context_server", "dap", "dap_adapters", + "encoding", "extension", "fancy-regex 0.14.0", "fs", @@ -13567,6 +13575,7 @@ dependencies = [ "dap_adapters", "debug_adapter_extension", "editor", + "encoding", "env_logger 0.11.8", "extension", "extension_host", @@ -19878,6 +19887,7 @@ dependencies = [ "component", "dap", "db", + "encoding", "fs", "futures 0.3.31", "gpui", diff --git a/Cargo.toml b/Cargo.toml index 7ba19fdcd8..a45f594d31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -477,6 +477,7 @@ documented = "0.9.1" dotenvy = "0.15.0" ec4rs = "1.1" emojis = "0.6.1" +encoding = "0.2.33" env_logger = "0.11" exec = "0.3.1" fancy-regex = "0.14.0" diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 68246a96b0..2de49d11d1 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -32,6 +32,7 @@ cloud_llm_client.workspace = true collections.workspace = true context_server.workspace = true db.workspace = true +encoding.workspace = true fs.workspace = true futures.workspace = true git.workspace = true @@ -72,6 +73,7 @@ which.workspace = true workspace-hack.workspace = true zstd.workspace = true + [dev-dependencies] agent = { workspace = true, "features" = ["test-support"] } agent_servers = { workspace = true, "features" = ["test-support"] } diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index f86bfd25f7..1f0850304f 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -523,7 +523,8 @@ mod tests { use super::*; use crate::{ContextServerRegistry, Templates}; use client::TelemetrySettings; - use fs::Fs; + use encoding::all::UTF_8; + use fs::{Fs, encodings::EncodingWrapper}; use gpui::{TestAppContext, UpdateGlobal}; use language_model::fake_provider::FakeLanguageModel; use prompt_store::ProjectContext; @@ -705,6 +706,7 @@ mod tests { path!("/root/src/main.rs").as_ref(), &"initial content".into(), language::LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -873,6 +875,7 @@ mod tests { path!("/root/src/main.rs").as_ref(), &"initial content".into(), language::LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); diff --git a/crates/assistant_tools/Cargo.toml b/crates/assistant_tools/Cargo.toml index 5a8ca8a5e9..e48dc9a0c5 100644 --- a/crates/assistant_tools/Cargo.toml +++ b/crates/assistant_tools/Cargo.toml @@ -28,6 +28,7 @@ component.workspace = true derive_more.workspace = true diffy = "0.4.2" editor.workspace = true +encoding.workspace = true feature_flags.workspace = true futures.workspace = true gpui.workspace = true diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 95b01c40eb..6b06ce03c5 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -1231,8 +1231,9 @@ async fn build_buffer_diff( #[cfg(test)] mod tests { use super::*; - use ::fs::Fs; + use ::fs::{Fs, encodings::EncodingWrapper}; use client::TelemetrySettings; + use encoding::all::UTF_8; use gpui::{TestAppContext, UpdateGlobal}; use language_model::fake_provider::FakeLanguageModel; use serde_json::json; @@ -1501,6 +1502,7 @@ mod tests { path!("/root/src/main.rs").as_ref(), &"initial content".into(), language::LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -1670,6 +1672,7 @@ mod tests { path!("/root/src/main.rs").as_ref(), &"initial content".into(), language::LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 4fccd3be7f..13b7932b95 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -31,6 +31,7 @@ chrono.workspace = true clock.workspace = true collections.workspace = true dashmap.workspace = true +encoding.workspace = true envy = "0.4.2" futures.workspace = true gpui.workspace = true diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 5c73253048..bc67f1351a 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -12,7 +12,8 @@ use buffer_diff::{DiffHunkSecondaryStatus, DiffHunkStatus, assert_hunks}; use call::{ActiveCall, ParticipantLocation, Room, room}; use client::{RECEIVE_TIMEOUT, User}; use collections::{HashMap, HashSet}; -use fs::{FakeFs, Fs as _, RemoveOptions}; +use encoding::all::UTF_8; +use fs::{FakeFs, Fs as _, RemoveOptions, encodings::EncodingWrapper}; use futures::{StreamExt as _, channel::mpsc}; use git::status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode}; use gpui::{ @@ -3706,6 +3707,7 @@ async fn test_buffer_reloading( path!("/dir/a.txt").as_ref(), &new_contents, LineEnding::Windows, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4472,6 +4474,7 @@ async fn test_reloading_buffer_manually( path!("/a/a.rs").as_ref(), &Rope::from("let seven = 7;"), LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index ac5c4c54ca..1c36e9f33e 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -5,7 +5,8 @@ use async_trait::async_trait; use call::ActiveCall; use collections::{BTreeMap, HashMap}; use editor::Bias; -use fs::{FakeFs, Fs as _}; +use encoding::all::UTF_8; +use fs::{FakeFs, Fs as _, encodings::EncodingWrapper}; use git::status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode}; use gpui::{BackgroundExecutor, Entity, TestAppContext}; use language::{ @@ -924,7 +925,12 @@ impl RandomizedTest for ProjectCollaborationTest { client .fs() - .save(&path, &content.as_str().into(), text::LineEnding::Unix) + .save( + &path, + &content.as_str().into(), + text::LineEnding::Unix, + EncodingWrapper::new(UTF_8), + ) .await .unwrap(); } diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 4a3a6b5c8e..ca8e4ae85d 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -30,6 +30,7 @@ client.workspace = true collections.workspace = true command_palette_hooks.workspace = true dirs.workspace = true +encoding.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true @@ -53,7 +54,6 @@ util.workspace = true workspace.workspace = true workspace-hack.workspace = true itertools.workspace = true -encoding = "0.2.33" [target.'cfg(windows)'.dependencies] diff --git a/crates/encodings/Cargo.toml b/crates/encodings/Cargo.toml index 70b11dd545..dd47679500 100644 --- a/crates/encodings/Cargo.toml +++ b/crates/encodings/Cargo.toml @@ -5,6 +5,7 @@ publish.workspace = true edition.workspace = true [dependencies] +anyhow.workspace = true ui.workspace = true workspace.workspace = true gpui.workspace = true @@ -12,7 +13,7 @@ picker.workspace = true util.workspace = true fuzzy.workspace = true editor.workspace = true -encoding = "0.2.33" +encoding.workspace = true language.workspace = true [lints] diff --git a/crates/encodings/src/lib.rs b/crates/encodings/src/lib.rs index 5315515998..91c1e8799a 100644 --- a/crates/encodings/src/lib.rs +++ b/crates/encodings/src/lib.rs @@ -1,3 +1,4 @@ +///! A crate for handling file encodings in the text editor. use editor::Editor; use encoding::Encoding; use encoding::all::{ @@ -12,7 +13,7 @@ use ui::{Button, ButtonCommon, Context, LabelSize, Render, Tooltip, Window, div} use ui::{Clickable, ParentElement}; use workspace::{ItemHandle, StatusItemView, Workspace}; -use crate::selectors::save_or_reopen::{EncodingSaveOrReopenSelector, get_current_encoding}; +use crate::selectors::save_or_reopen::EncodingSaveOrReopenSelector; /// A status bar item that shows the current file encoding and allows changing it. pub struct EncodingIndicator { @@ -44,8 +45,6 @@ impl Render for EncodingIndicator { } impl EncodingIndicator { - pub fn get_current_encoding(&self, cx: &mut Context, editor: WeakEntity) {} - pub fn new( encoding: Option<&'static dyn encoding::Encoding>, workspace: WeakEntity, @@ -187,3 +186,48 @@ pub fn encoding_from_index(index: usize) -> &'static dyn Encoding { _ => UTF_8, } } + +/// Get an encoding from its name. +pub fn encoding_from_name(name: &str) -> &'static dyn Encoding { + match name { + "UTF-8" => UTF_8, + "UTF-16 LE" => UTF_16LE, + "UTF-16 BE" => UTF_16BE, + "IBM866" => IBM866, + "ISO 8859-1" => ISO_8859_1, + "ISO 8859-2" => ISO_8859_2, + "ISO 8859-3" => ISO_8859_3, + "ISO 8859-4" => ISO_8859_4, + "ISO 8859-5" => ISO_8859_5, + "ISO 8859-6" => ISO_8859_6, + "ISO 8859-7" => ISO_8859_7, + "ISO 8859-8" => ISO_8859_8, + "ISO 8859-10" => ISO_8859_10, + "ISO 8859-13" => ISO_8859_13, + "ISO 8859-14" => ISO_8859_14, + "ISO 8859-15" => ISO_8859_15, + "ISO 8859-16" => ISO_8859_16, + "KOI8-R" => KOI8_R, + "KOI8-U" => KOI8_U, + "MacRoman" => MAC_ROMAN, + "Mac Cyrillic" => MAC_CYRILLIC, + "Windows-874" => WINDOWS_874, + "Windows-1250" => WINDOWS_1250, + "Windows-1251" => WINDOWS_1251, + "Windows-1252" => WINDOWS_1252, + "Windows-1253" => WINDOWS_1253, + "Windows-1254" => WINDOWS_1254, + "Windows-1255" => WINDOWS_1255, + "Windows-1256" => WINDOWS_1256, + "Windows-1257" => WINDOWS_1257, + "Windows-1258" => WINDOWS_1258, + "Windows-949" => WINDOWS_949, + "EUC-JP" => EUC_JP, + "ISO 2022-JP" => ISO_2022_JP, + "GBK" => GBK, + "GB18030" => GB18030, + "Big5" => BIG5_2003, + "HZ-GB-2312" => HZ, + _ => UTF_8, // Default to UTF-8 for unknown names + } +} diff --git a/crates/encodings/src/selectors.rs b/crates/encodings/src/selectors.rs index c25b56be56..9cca1551ec 100644 --- a/crates/encodings/src/selectors.rs +++ b/crates/encodings/src/selectors.rs @@ -1,30 +1,28 @@ +/// This module contains the encoding selectors for saving or reopening files with a different encoding. +/// It provides a modal view that allows the user to choose between saving with a different encoding +/// or reopening with a different encoding, and then selecting the desired encoding from a list. pub mod save_or_reopen { use editor::Editor; use gpui::Styled; use gpui::{AppContext, ParentElement}; use picker::Picker; use picker::PickerDelegate; - use std::cell::RefCell; - use std::ops::{Deref, DerefMut}; - use std::rc::Rc; - use std::sync::Arc; use std::sync::atomic::AtomicBool; use util::ResultExt; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{DismissEvent, Entity, EventEmitter, Focusable, WeakEntity}; - use ui::{Context, HighlightedLabel, Label, ListItem, Render, Window, rems, v_flex}; + use ui::{Context, HighlightedLabel, ListItem, Render, Window, rems, v_flex}; use workspace::{ModalView, Workspace}; - use crate::selectors::encoding::{Action, EncodingSelector, EncodingSelectorDelegate}; + use crate::selectors::encoding::{Action, EncodingSelector}; /// A modal view that allows the user to select between saving with a different encoding or /// reopening with a different encoding. pub struct EncodingSaveOrReopenSelector { picker: Entity>, pub current_selection: usize, - workspace: WeakEntity, } impl EncodingSaveOrReopenSelector { @@ -41,7 +39,6 @@ pub mod save_or_reopen { Self { picker, current_selection: 0, - workspace, } } @@ -119,9 +116,17 @@ pub mod save_or_reopen { .read(cx) .active_excerpt(cx)?; + let weak_workspace = workspace.read(cx).weak_handle(); + workspace.update(cx, |workspace, cx| { workspace.toggle_modal(window, cx, |window, cx| { - EncodingSelector::new(window, cx, Action::Save, buffer.downgrade()) + EncodingSelector::new( + window, + cx, + Action::Save, + buffer.downgrade(), + weak_workspace, + ) }) }); } @@ -134,9 +139,17 @@ pub mod save_or_reopen { .read(cx) .active_excerpt(cx)?; + let weak_workspace = workspace.read(cx).weak_handle(); + workspace.update(cx, |workspace, cx| { workspace.toggle_modal(window, cx, |window, cx| { - EncodingSelector::new(window, cx, Action::Reopen, buffer.downgrade()) + EncodingSelector::new( + window, + cx, + Action::Reopen, + buffer.downgrade(), + weak_workspace, + ) }) }); } @@ -165,7 +178,7 @@ pub mod save_or_reopen { ) { self.current_selection = ix; self.selector - .update(cx, |selector, cx| { + .update(cx, |selector, _cx| { selector.current_selection = ix; }) .log_err(); @@ -217,7 +230,7 @@ pub mod save_or_reopen { .min(delegate.matches.len().saturating_sub(1)); delegate .selector - .update(cx, |selector, cx| { + .update(cx, |selector, _cx| { selector.current_selection = delegate.current_selection }) .log_err(); @@ -263,33 +276,27 @@ pub mod save_or_reopen { } } +/// This module contains the encoding selector for choosing an encoding to save or reopen a file with. pub mod encoding { - use std::{ - ops::DerefMut, - rc::{Rc, Weak}, - sync::{Arc, atomic::AtomicBool}, - }; + use std::sync::atomic::AtomicBool; use fuzzy::{StringMatch, StringMatchCandidate}; - use gpui::{ - AppContext, BackgroundExecutor, DismissEvent, Entity, EventEmitter, Focusable, Length, - WeakEntity, actions, - }; + use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, WeakEntity}; use language::Buffer; use picker::{Picker, PickerDelegate}; use ui::{ - Context, DefiniteLength, HighlightedLabel, Label, ListItem, ListItemSpacing, ParentElement, - Render, Styled, Window, rems, v_flex, + Context, HighlightedLabel, ListItem, ListItemSpacing, ParentElement, Render, Styled, + Window, rems, v_flex, }; use util::{ResultExt, TryFutureExt}; use workspace::{ModalView, Workspace}; - use crate::encoding_from_index; + use crate::encoding_from_name; /// A modal view that allows the user to select an encoding from a list of encodings. pub struct EncodingSelector { picker: Entity>, - action: Action, + workspace: WeakEntity, } pub struct EncodingSelectorDelegate { @@ -298,12 +305,14 @@ pub mod encoding { matches: Vec, selector: WeakEntity, buffer: WeakEntity, + action: Action, } impl EncodingSelectorDelegate { pub fn new( selector: WeakEntity, buffer: WeakEntity, + action: Action, ) -> EncodingSelectorDelegate { EncodingSelectorDelegate { current_selection: 0, @@ -350,6 +359,7 @@ pub mod encoding { matches: Vec::new(), selector, buffer, + action, } } } @@ -365,12 +375,7 @@ pub mod encoding { self.current_selection } - fn set_selected_index( - &mut self, - ix: usize, - window: &mut Window, - cx: &mut Context>, - ) { + fn set_selected_index(&mut self, ix: usize, _: &mut Window, _: &mut Context>) { self.current_selection = ix; } @@ -427,21 +432,40 @@ pub mod encoding { }) } - fn confirm( - &mut self, - secondary: bool, - window: &mut Window, - cx: &mut Context>, - ) { + fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { if let Some(buffer) = self.buffer.upgrade() { buffer.update(cx, |buffer, cx| { - buffer.encoding = encoding_from_index(self.current_selection) + buffer.encoding = + encoding_from_name(self.matches[self.current_selection].string.as_str()); + if self.action == Action::Reopen { + let executor = cx.background_executor().clone(); + executor.spawn(buffer.reload(cx)).detach(); + } else if self.action == Action::Save { + let executor = cx.background_executor().clone(); + + let workspace = self + .selector + .upgrade() + .unwrap() + .read(cx) + .workspace + .upgrade() + .unwrap(); + + executor + .spawn(workspace.update(cx, |workspace, cx| { + workspace + .save_active_item(workspace::SaveIntent::Save, window, cx) + .log_err() + })) + .detach(); + } }); } self.dismissed(window, cx); } - fn dismissed(&mut self, window: &mut Window, cx: &mut Context>) { + fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { self.selector .update(cx, |_, cx| cx.emit(DismissEvent)) .log_err(); @@ -450,9 +474,9 @@ pub mod encoding { fn render_match( &self, ix: usize, - selected: bool, - window: &mut Window, - cx: &mut Context>, + _: bool, + _: &mut Window, + _: &mut Context>, ) -> Option { Some( ListItem::new(ix) @@ -466,6 +490,7 @@ pub mod encoding { } /// The action to perform after selecting an encoding. + #[derive(PartialEq, Clone)] pub enum Action { Save, Reopen, @@ -477,11 +502,13 @@ pub mod encoding { cx: &mut Context, action: Action, buffer: WeakEntity, + workspace: WeakEntity, ) -> EncodingSelector { - let delegate = EncodingSelectorDelegate::new(cx.entity().downgrade(), buffer); + let delegate = + EncodingSelectorDelegate::new(cx.entity().downgrade(), buffer, action.clone()); let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); - EncodingSelector { picker, action } + EncodingSelector { picker, workspace } } } diff --git a/crates/extension_host/Cargo.toml b/crates/extension_host/Cargo.toml index c933d253c6..ef65c2e3d6 100644 --- a/crates/extension_host/Cargo.toml +++ b/crates/extension_host/Cargo.toml @@ -23,6 +23,7 @@ async-trait.workspace = true client.workspace = true collections.workspace = true dap.workspace = true +encoding.workspace = true extension.workspace = true fs.workspace = true futures.workspace = true diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index fde0aeac94..a5411c923f 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -12,6 +12,7 @@ use async_tar::Archive; use client::ExtensionProvides; use client::{Client, ExtensionMetadata, GetExtensionsResponse, proto, telemetry::Telemetry}; use collections::{BTreeMap, BTreeSet, HashMap, HashSet, btree_map}; +use encoding::all::UTF_8; pub use extension::ExtensionManifest; use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; use extension::{ @@ -20,6 +21,7 @@ use extension::{ ExtensionLanguageServerProxy, ExtensionSlashCommandProxy, ExtensionSnippetProxy, ExtensionThemeProxy, }; +use fs::encodings::EncodingWrapper; use fs::{Fs, RemoveOptions}; use futures::future::join_all; use futures::{ @@ -1468,10 +1470,15 @@ impl ExtensionStore { } if let Ok(index_json) = serde_json::to_string_pretty(&index) { - fs.save(&index_path, &index_json.as_str().into(), Default::default()) - .await - .context("failed to save extension index") - .log_err(); + fs.save( + &index_path, + &index_json.as_str().into(), + Default::default(), + EncodingWrapper::new(UTF_8), + ) + .await + .context("failed to save extension index") + .log_err(); } log::info!("rebuilt extension index in {:?}", start_time.elapsed()); @@ -1636,6 +1643,7 @@ impl ExtensionStore { &tmp_dir.join(EXTENSION_TOML), &Rope::from(manifest_toml), language::LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await?; } else { diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 6476c67636..05654fa2cc 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -16,6 +16,7 @@ anyhow.workspace = true async-tar.workspace = true async-trait.workspace = true collections.workspace = true +encoding.workspace = true futures.workspace = true git.workspace = true gpui.workspace = true @@ -34,7 +35,8 @@ text.workspace = true time.workspace = true util.workspace = true workspace-hack.workspace = true -encoding = "0.2.33" +schemars.workspace = true + [target.'cfg(target_os = "macos")'.dependencies] diff --git a/crates/fs/src/encodings.rs b/crates/fs/src/encodings.rs index b0a1264a14..8aecbcb764 100644 --- a/crates/fs/src/encodings.rs +++ b/crates/fs/src/encodings.rs @@ -1,14 +1,70 @@ -use anyhow::{Error, Result}; +//! Encoding and decoding utilities using the `encoding` crate. +use std::fmt::Debug; +use anyhow::{Error, Result}; use encoding::Encoding; +use serde::{Deserialize, de::Visitor}; /// A wrapper around `encoding::Encoding` to implement `Send` and `Sync`. /// Since the reference is static, it is safe to send it across threads. pub struct EncodingWrapper(&'static dyn Encoding); +impl Debug for EncodingWrapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("EncodingWrapper") + .field(&self.0.name()) + .finish() + } +} + +pub struct EncodingWrapperVisitor; + +impl<'vi> Visitor<'vi> for EncodingWrapperVisitor { + type Value = EncodingWrapper; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a valid encoding name") + } + + fn visit_str(self, encoding: &str) -> Result { + Ok(EncodingWrapper( + encoding::label::encoding_from_whatwg_label(encoding) + .ok_or_else(|| serde::de::Error::custom("Invalid Encoding"))?, + )) + } + + fn visit_string(self, encoding: String) -> Result { + Ok(EncodingWrapper( + encoding::label::encoding_from_whatwg_label(&encoding) + .ok_or_else(|| serde::de::Error::custom("Invalid Encoding"))?, + )) + } +} + +impl<'de> Deserialize<'de> for EncodingWrapper { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(EncodingWrapperVisitor) + } +} + +impl PartialEq for EncodingWrapper { + fn eq(&self, other: &Self) -> bool { + self.0.name() == other.0.name() + } +} + unsafe impl Send for EncodingWrapper {} unsafe impl Sync for EncodingWrapper {} +impl Clone for EncodingWrapper { + fn clone(&self) -> Self { + EncodingWrapper(self.0) + } +} + impl EncodingWrapper { pub fn new(encoding: &'static dyn Encoding) -> EncodingWrapper { EncodingWrapper(encoding) diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index cb3b649b76..06d897681c 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -56,6 +56,7 @@ use smol::io::AsyncReadExt; use std::ffi::OsStr; use crate::encodings::EncodingWrapper; +use crate::encodings::from_utf8; pub trait Watcher: Send + Sync { fn add(&self, path: &Path) -> Result<()>; @@ -123,7 +124,13 @@ pub trait Fs: Send + Sync { async fn load_bytes(&self, path: &Path) -> Result>; async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>; - async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>; + async fn save( + &self, + path: &Path, + text: &Rope, + line_ending: LineEnding, + encoding: EncodingWrapper, + ) -> Result<()>; async fn write(&self, path: &Path, content: &[u8]) -> Result<()>; async fn canonicalize(&self, path: &Path) -> Result; async fn is_file(&self, path: &Path) -> bool; @@ -611,7 +618,13 @@ impl Fs for RealFs { Ok(()) } - async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> { + async fn save( + &self, + path: &Path, + text: &Rope, + line_ending: LineEnding, + encoding: EncodingWrapper, + ) -> Result<()> { let buffer_size = text.summary().len.min(10 * 1024); if let Some(path) = path.parent() { self.create_dir(path).await?; @@ -619,7 +632,9 @@ impl Fs for RealFs { let file = smol::fs::File::create(path).await?; let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file); for chunk in chunks(text, line_ending) { - writer.write_all(chunk.as_bytes()).await?; + writer + .write_all(&from_utf8(chunk.to_string(), encoding.clone()).await?) + .await?; } writer.flush().await?; Ok(()) @@ -2290,14 +2305,22 @@ impl Fs for FakeFs { Ok(()) } - async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> { + async fn save( + &self, + path: &Path, + text: &Rope, + line_ending: LineEnding, + encoding: EncodingWrapper, + ) -> Result<()> { + use crate::encodings::from_utf8; + self.simulate_random_delay().await; let path = normalize_path(path); let content = chunks(text, line_ending).collect::(); if let Some(path) = path.parent() { self.create_dir(path).await?; } - self.write_file_internal(path, content.into_bytes(), false)?; + self.write_file_internal(path, from_utf8(content, encoding).await?, false)?; Ok(()) } diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 35f7a60354..2314279bb8 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -29,6 +29,7 @@ command_palette_hooks.workspace = true component.workspace = true db.workspace = true editor.workspace = true +encoding.workspace = true futures.workspace = true fuzzy.workspace = true git.workspace = true diff --git a/crates/git_ui/src/file_diff_view.rs b/crates/git_ui/src/file_diff_view.rs index a320888b3b..4b1446c071 100644 --- a/crates/git_ui/src/file_diff_view.rs +++ b/crates/git_ui/src/file_diff_view.rs @@ -362,8 +362,9 @@ impl Render for FileDiffView { mod tests { use super::*; use editor::test::editor_test_context::assert_state_with_diff; + use encoding::all::UTF_8; use gpui::TestAppContext; - use project::{FakeFs, Fs, Project}; + use project::{FakeFs, Fs, Project, encodings::EncodingWrapper}; use settings::{Settings, SettingsStore}; use std::path::PathBuf; use unindent::unindent; @@ -444,6 +445,7 @@ mod tests { ) .into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -479,6 +481,7 @@ mod tests { ) .into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index fafd1fdcb7..89bda420ea 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -31,7 +31,9 @@ anyhow.workspace = true async-trait.workspace = true clock.workspace = true collections.workspace = true +diffy = "0.4.2" ec4rs.workspace = true +encoding.workspace = true fs.workspace = true futures.workspace = true fuzzy.workspace = true @@ -69,8 +71,6 @@ unicase = "2.6" util.workspace = true watch.workspace = true workspace-hack.workspace = true -diffy = "0.4.2" -encoding = "0.2.33" [dev-dependencies] collections = { workspace = true, features = ["test-support"] } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 44a5dacc2d..c3c7f3ab92 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -22,7 +22,7 @@ pub use clock::ReplicaId; use clock::{AGENT_REPLICA_ID, Lamport}; use collections::HashMap; use encoding::Encoding; -use fs::{Fs, MTime, RealFs}; +use fs::MTime; use futures::channel::oneshot; use gpui::{ App, AppContext as _, Context, Entity, EventEmitter, HighlightStyle, SharedString, StyledText, @@ -1281,7 +1281,7 @@ impl Buffer { /// Reloads the contents of the buffer from disk. pub fn reload(&mut self, cx: &Context) -> oneshot::Receiver> { let (tx, rx) = futures::channel::oneshot::channel(); - let encoding = self.encoding.clone(); + let encoding = self.encoding; let prev_version = self.text.version(); self.reload_task = Some(cx.spawn(async move |this, cx| { let Some((new_mtime, new_text)) = this.update(cx, |this, cx| { @@ -4976,11 +4976,7 @@ impl LocalFile for TestFile { unimplemented!() } - fn load_with_encoding( - &self, - cx: &App, - encoding: &'static dyn Encoding, - ) -> Task> { + fn load_with_encoding(&self, _: &App, _: &'static dyn Encoding) -> Task> { unimplemented!() } } diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 57d6d6ca28..a2793c21e0 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -39,6 +39,7 @@ clock.workspace = true collections.workspace = true context_server.workspace = true dap.workspace = true +encoding.workspace = true extension.workspace = true fancy-regex.workspace = true fs.workspace = true @@ -90,6 +91,7 @@ worktree.workspace = true zlog.workspace = true workspace-hack.workspace = true + [dev-dependencies] client = { workspace = true, features = ["test-support"] } collections = { workspace = true, features = ["test-support"] } diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 295bad6e59..790ef4304d 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -372,6 +372,8 @@ impl LocalBufferStore { let version = buffer.version(); let buffer_id = buffer.remote_id(); let file = buffer.file().cloned(); + let encoding = buffer.encoding; + if file .as_ref() .is_some_and(|file| file.disk_state() == DiskState::New) @@ -380,7 +382,7 @@ impl LocalBufferStore { } let save = worktree.update(cx, |worktree, cx| { - worktree.write_file(path.as_ref(), text, line_ending, cx) + worktree.write_file(path.as_ref(), text, line_ending, cx, encoding) }); cx.spawn(async move |this, cx| { diff --git a/crates/project/src/prettier_store.rs b/crates/project/src/prettier_store.rs index 3ae5dc24ae..3cc0a58adb 100644 --- a/crates/project/src/prettier_store.rs +++ b/crates/project/src/prettier_store.rs @@ -7,7 +7,8 @@ use std::{ use anyhow::{Context as _, Result, anyhow}; use collections::{HashMap, HashSet}; -use fs::Fs; +use encoding::all::UTF_8; +use fs::{Fs, encodings::EncodingWrapper}; use futures::{ FutureExt, future::{self, Shared}, @@ -941,10 +942,12 @@ async fn install_prettier_packages( async fn save_prettier_server_file(fs: &dyn Fs) -> anyhow::Result<()> { let prettier_wrapper_path = default_prettier_dir().join(prettier::PRETTIER_SERVER_FILE); + let encoding_wrapper = EncodingWrapper::new(UTF_8); fs.save( &prettier_wrapper_path, &text::Rope::from(prettier::PRETTIER_SERVER_JS), text::LineEnding::Unix, + encoding_wrapper, ) .await .with_context(|| { diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 6dcd07482e..38b4cfe52d 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -9,7 +9,8 @@ use buffer_diff::{ BufferDiffEvent, CALCULATE_DIFF_TASK, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind, assert_hunks, }; -use fs::FakeFs; +use encoding::all::UTF_8; +use fs::{FakeFs, encodings::EncodingWrapper}; use futures::{StreamExt, future}; use git::{ GitHostingProviderRegistry, @@ -1448,10 +1449,14 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon ) .await .unwrap(); + + let encoding_wrapper = EncodingWrapper::new(UTF_8); + fs.save( path!("/the-root/Cargo.lock").as_ref(), &"".into(), Default::default(), + encoding_wrapper.clone(), ) .await .unwrap(); @@ -1459,6 +1464,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon path!("/the-stdlib/LICENSE").as_ref(), &"".into(), Default::default(), + encoding_wrapper.clone(), ) .await .unwrap(); @@ -1466,6 +1472,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon path!("/the/stdlib/src/string.rs").as_ref(), &"".into(), Default::default(), + encoding_wrapper, ) .await .unwrap(); @@ -3941,12 +3948,15 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) // the next file change occurs. cx.executor().deprioritize(*language::BUFFER_DIFF_TASK); + let encoding_wrapper = EncodingWrapper::new(UTF_8); + // Change the buffer's file on disk, and then wait for the file change // to be detected by the worktree, so that the buffer starts reloading. fs.save( path!("/dir/file1").as_ref(), &"the first contents".into(), Default::default(), + encoding_wrapper.clone(), ) .await .unwrap(); @@ -3958,6 +3968,7 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) path!("/dir/file1").as_ref(), &"the second contents".into(), Default::default(), + encoding_wrapper, ) .await .unwrap(); @@ -3996,12 +4007,15 @@ async fn test_edit_buffer_while_it_reloads(cx: &mut gpui::TestAppContext) { // the next file change occurs. cx.executor().deprioritize(*language::BUFFER_DIFF_TASK); + let encoding_wrapper = EncodingWrapper::new(UTF_8); + // Change the buffer's file on disk, and then wait for the file change // to be detected by the worktree, so that the buffer starts reloading. fs.save( path!("/dir/file1").as_ref(), &"the first contents".into(), Default::default(), + encoding_wrapper, ) .await .unwrap(); @@ -4603,10 +4617,14 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { let (new_contents, new_offsets) = marked_text_offsets("oneˇ\nthree ˇFOURˇ five\nsixtyˇ seven\n"); + + let encoding_wrapper = EncodingWrapper::new(UTF_8); + fs.save( path!("/dir/the-file").as_ref(), &new_contents.as_str().into(), LineEnding::Unix, + encoding_wrapper, ) .await .unwrap(); @@ -4634,11 +4652,14 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { assert!(!buffer.has_conflict()); }); + let encoding_wrapper = EncodingWrapper::new(UTF_8); + // Change the file on disk again, adding blank lines to the beginning. fs.save( path!("/dir/the-file").as_ref(), &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(), LineEnding::Unix, + encoding_wrapper, ) .await .unwrap(); @@ -4685,12 +4706,15 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { assert_eq!(buffer.line_ending(), LineEnding::Windows); }); + let encoding_wrapper = EncodingWrapper::new(UTF_8); + // Change a file's line endings on disk from unix to windows. The buffer's // state updates correctly. fs.save( path!("/dir/file1").as_ref(), &"aaa\nb\nc\n".into(), LineEnding::Windows, + encoding_wrapper, ) .await .unwrap(); diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 5dbb9a2771..a8817eabb6 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -30,6 +30,7 @@ clap.workspace = true client.workspace = true dap_adapters.workspace = true debug_adapter_extension.workspace = true +encoding.workspace = true env_logger.workspace = true extension.workspace = true extension_host.workspace = true diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 69fae7f399..bbf2e07c9c 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -6,10 +6,11 @@ use assistant_tool::{Tool as _, ToolResultContent}; use assistant_tools::{ReadFileTool, ReadFileToolInput}; use client::{Client, UserStore}; use clock::FakeSystemClock; +use encoding::all::UTF_8; use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModel}; use extension::ExtensionHostProxy; -use fs::{FakeFs, Fs}; +use fs::{FakeFs, Fs, encodings::EncodingWrapper}; use gpui::{AppContext as _, Entity, SemanticVersion, TestAppContext}; use http_client::{BlockedHttpClient, FakeHttpClient}; use language::{ @@ -123,6 +124,7 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test path!("/code/project1/src/main.rs").as_ref(), &"fn main() {}".into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -763,6 +765,7 @@ async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppCont &PathBuf::from(path!("/code/project1/src/lib.rs")), &("bangles".to_string().into()), LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -778,6 +781,7 @@ async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppCont &PathBuf::from(path!("/code/project1/src/lib.rs")), &("bloop".to_string().into()), LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 434b14b07c..c9d98a0019 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -52,6 +52,7 @@ workspace.workspace = true zed_actions.workspace = true workspace-hack.workspace = true + [dev-dependencies] assets.workspace = true command_palette.workspace = true diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 869aa5322e..7cc8304989 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -35,6 +35,7 @@ clock.workspace = true collections.workspace = true component.workspace = true db.workspace = true +encoding.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f2828dbc3f..0b59622578 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -19,6 +19,8 @@ mod workspace_settings; pub use crate::notifications::NotificationFrame; pub use dock::Panel; +use encoding::all::UTF_8; +use fs::encodings::EncodingWrapper; pub use path_list::PathList; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; @@ -118,7 +120,6 @@ use crate::persistence::{ model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup}, }; - pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200); static ZED_WINDOW_SIZE: LazyLock>> = LazyLock::new(|| { @@ -7233,8 +7234,14 @@ pub fn create_and_open_local_file( let fs = workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?; if !fs.is_file(path).await { fs.create_file(path, Default::default()).await?; - fs.save(path, &default_content(), Default::default()) - .await?; + let encoding_wrapper = EncodingWrapper::new(UTF_8); + fs.save( + path, + &default_content(), + Default::default(), + encoding_wrapper, + ) + .await?; } let mut items = workspace diff --git a/crates/worktree/Cargo.toml b/crates/worktree/Cargo.toml index 6dd398dfc8..8b6e46ca3e 100644 --- a/crates/worktree/Cargo.toml +++ b/crates/worktree/Cargo.toml @@ -26,6 +26,7 @@ test-support = [ anyhow.workspace = true clock.workspace = true collections.workspace = true +encoding.workspace = true fs.workspace = true futures.workspace = true fuzzy.workspace = true @@ -48,7 +49,6 @@ sum_tree.workspace = true text.workspace = true util.workspace = true workspace-hack.workspace = true -encoding = "0.2.33" [dev-dependencies] diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index c255877b3a..ce1eb0e818 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -835,9 +835,10 @@ impl Worktree { text: Rope, line_ending: LineEnding, cx: &Context, + encoding: &'static dyn Encoding, ) -> Task>> { match self { - Worktree::Local(this) => this.write_file(path, text, line_ending, cx), + Worktree::Local(this) => this.write_file(path, text, line_ending, cx, encoding), Worktree::Remote(_) => { Task::ready(Err(anyhow!("remote worktree can't yet write files"))) } @@ -1648,6 +1649,7 @@ impl LocalWorktree { text: Rope, line_ending: LineEnding, cx: &Context, + encoding: &'static dyn Encoding, ) -> Task>> { let path = path.into(); let fs = self.fs.clone(); @@ -1656,10 +1658,15 @@ impl LocalWorktree { return Task::ready(Err(anyhow!("invalid path {path:?}"))); }; + let encoding_wrapper = EncodingWrapper::new(encoding); + let write = cx.background_spawn({ let fs = fs.clone(); let abs_path = abs_path.clone(); - async move { fs.save(&abs_path, &text, line_ending).await } + async move { + fs.save(&abs_path, &text, line_ending, encoding_wrapper) + .await + } }); cx.spawn(async move |this, cx| { diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index c46e14f077..9c6564d7a4 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -3,7 +3,8 @@ use crate::{ worktree_settings::WorktreeSettings, }; use anyhow::Result; -use fs::{FakeFs, Fs, RealFs, RemoveOptions}; +use encoding::all::UTF_8; +use fs::{FakeFs, Fs, RealFs, RemoveOptions, encodings::EncodingWrapper}; use git::GITIGNORE; use gpui::{AppContext as _, BackgroundExecutor, BorrowAppContext, Context, Task, TestAppContext}; use parking_lot::Mutex; @@ -642,9 +643,15 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) { // Update the gitignore so that node_modules is no longer ignored, // but a subdirectory is ignored - fs.save("/root/.gitignore".as_ref(), &"e".into(), Default::default()) - .await - .unwrap(); + let encoding_wrapper = fs::encodings::EncodingWrapper::new(UTF_8); + fs.save( + "/root/.gitignore".as_ref(), + &"e".into(), + Default::default(), + encoding_wrapper, + ) + .await + .unwrap(); cx.executor().run_until_parked(); // All of the directories that are no longer ignored are now loaded. @@ -715,6 +722,7 @@ async fn test_write_file(cx: &mut TestAppContext) { "hello".into(), Default::default(), cx, + UTF_8, ) }) .await @@ -726,6 +734,7 @@ async fn test_write_file(cx: &mut TestAppContext) { "world".into(), Default::default(), cx, + UTF_8, ) }) .await @@ -1746,8 +1755,13 @@ fn randomly_mutate_worktree( }) } else { log::info!("overwriting file {:?} ({})", entry.path, entry.id.0); - let task = - worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx); + let task = worktree.write_file( + entry.path.clone(), + "".into(), + Default::default(), + cx, + UTF_8, + ); cx.background_spawn(async move { task.await?; Ok(()) @@ -1834,10 +1848,12 @@ async fn randomly_mutate_fs( ignore_path.strip_prefix(root_path).unwrap(), ignore_contents ); + let encoding_wrapper = EncodingWrapper::new(UTF_8); fs.save( &ignore_path, &ignore_contents.as_str().into(), Default::default(), + encoding_wrapper, ) .await .unwrap(); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 56db114554..21fe5798f4 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -55,6 +55,7 @@ debugger_tools.workspace = true debugger_ui.workspace = true diagnostics.workspace = true editor.workspace = true +encoding.workspace = true encodings.workspace = true env_logger.workspace = true extension.workspace = true @@ -168,7 +169,6 @@ zed_actions.workspace = true zeta.workspace = true zlog.workspace = true zlog_settings.workspace = true -encoding = "0.2.33" [target.'cfg(target_os = "windows")'.dependencies] windows.workspace = true diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 403fed5978..e27b796f99 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1937,6 +1937,8 @@ mod tests { use assets::Assets; use collections::HashSet; use editor::{DisplayPoint, Editor, SelectionEffects, display_map::DisplayRow}; + use encoding::all::UTF_8; + use fs::encodings::EncodingWrapper; use gpui::{ Action, AnyWindowHandle, App, AssetSource, BorrowAppContext, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext, WindowHandle, actions, @@ -4128,6 +4130,7 @@ mod tests { "/settings.json".as_ref(), &r#"{"base_keymap": "Atom"}"#.into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4138,6 +4141,7 @@ mod tests { "/keymap.json".as_ref(), &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4186,6 +4190,7 @@ mod tests { "/keymap.json".as_ref(), &r#"[{"bindings": {"backspace": "test_only::ActionB"}}]"#.into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4206,6 +4211,7 @@ mod tests { "/settings.json".as_ref(), &r#"{"base_keymap": "JetBrains"}"#.into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4246,6 +4252,7 @@ mod tests { "/settings.json".as_ref(), &r#"{"base_keymap": "Atom"}"#.into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4255,6 +4262,7 @@ mod tests { "/keymap.json".as_ref(), &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4298,6 +4306,7 @@ mod tests { "/keymap.json".as_ref(), &r#"[{"bindings": {"backspace": null}}]"#.into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4318,6 +4327,7 @@ mod tests { "/settings.json".as_ref(), &r#"{"base_keymap": "JetBrains"}"#.into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); From 3b20229fa1ca6962cb421dad0db313626fdc3457 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Tue, 26 Aug 2025 23:54:57 +0530 Subject: [PATCH 7/7] feat: Make the encoding indicator appear only when an editor is open. feat: Enable the user to choose whether or not the encoding indicator should be displayed by enabling or disabling `encoding_indicator` in `settings.json` --- Cargo.lock | 1 + assets/settings/default.json | 4 +++- crates/editor/src/editor_settings.rs | 9 +++++++++ crates/encodings/Cargo.toml | 13 +++++++------ crates/encodings/src/lib.rs | 13 ++++++++++++- docs/src/configuring-zed.md | 3 ++- 6 files changed, 34 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 83a14b5867..d6ce0130ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5282,6 +5282,7 @@ dependencies = [ "gpui", "language", "picker", + "settings", "ui", "util", "workspace", diff --git a/assets/settings/default.json b/assets/settings/default.json index 804198090f..399eb1892f 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1258,7 +1258,9 @@ // Whether to show the active language button in the status bar. "active_language_button": true, // Whether to show the cursor position button in the status bar. - "cursor_position_button": true + "cursor_position_button": true, + // Whether to show the encoding indicator in the status bar. + "encoding_indicator": true }, // Settings specific to the terminal "terminal": { diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 1d7e04cae0..29a5dd43d4 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -136,6 +136,11 @@ pub struct StatusBar { /// /// Default: true pub cursor_position_button: bool, + + /// Whether to show the encoding indicator in the status bar. + /// + /// Default: true + pub encoding_indicator: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -593,6 +598,10 @@ pub struct StatusBarContent { /// /// Default: true pub cursor_position_button: Option, + /// Whether to show the encoding indicator in the status bar. + /// + /// Default: true + pub encoding_indicator: Option, } // Toolbar related settings diff --git a/crates/encodings/Cargo.toml b/crates/encodings/Cargo.toml index dd47679500..a4c2b959e8 100644 --- a/crates/encodings/Cargo.toml +++ b/crates/encodings/Cargo.toml @@ -6,15 +6,16 @@ edition.workspace = true [dependencies] anyhow.workspace = true -ui.workspace = true -workspace.workspace = true -gpui.workspace = true -picker.workspace = true -util.workspace = true -fuzzy.workspace = true editor.workspace = true encoding.workspace = true +fuzzy.workspace = true +gpui.workspace = true language.workspace = true +picker.workspace = true +settings.workspace = true +ui.workspace = true +util.workspace = true +workspace.workspace = true [lints] workspace = true diff --git a/crates/encodings/src/lib.rs b/crates/encodings/src/lib.rs index 91c1e8799a..9715a48d94 100644 --- a/crates/encodings/src/lib.rs +++ b/crates/encodings/src/lib.rs @@ -1,5 +1,5 @@ ///! A crate for handling file encodings in the text editor. -use editor::Editor; +use editor::{Editor, EditorSettings}; use encoding::Encoding; use encoding::all::{ BIG5_2003, EUC_JP, GB18030, GBK, HZ, IBM866, ISO_2022_JP, ISO_8859_1, ISO_8859_2, ISO_8859_3, @@ -9,6 +9,7 @@ use encoding::all::{ WINDOWS_1253, WINDOWS_1254, WINDOWS_1255, WINDOWS_1256, WINDOWS_1257, WINDOWS_1258, }; use gpui::{ClickEvent, Entity, Subscription, WeakEntity}; +use settings::Settings; use ui::{Button, ButtonCommon, Context, LabelSize, Render, Tooltip, Window, div}; use ui::{Clickable, ParentElement}; use workspace::{ItemHandle, StatusItemView, Workspace}; @@ -20,6 +21,7 @@ pub struct EncodingIndicator { pub encoding: Option<&'static dyn Encoding>, pub workspace: WeakEntity, observe: Option, // Subscription to observe changes in the active editor + show: bool, } pub mod selectors; @@ -28,6 +30,12 @@ impl Render for EncodingIndicator { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { let status_element = div(); + if (EditorSettings::get_global(cx).status_bar.encoding_indicator == false) + || (self.show == false) + { + return status_element; + } + status_element.child( Button::new("encoding", encoding_name(self.encoding.unwrap_or(UTF_8))) .label_size(LabelSize::Small) @@ -54,6 +62,7 @@ impl EncodingIndicator { encoding, workspace, observe, + show: true, } } @@ -84,10 +93,12 @@ impl StatusItemView for EncodingIndicator { Some(editor) => { self.observe = Some(cx.observe_in(&editor, window, Self::update)); self.update(editor, window, cx); + self.show = true; } None => { self.encoding = None; self.observe = None; + self.show = false; } } } diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index a8a4689689..a7ec556ffe 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1278,7 +1278,8 @@ Each option controls displaying of a particular toolbar element. If all elements ```json "status_bar": { "active_language_button": true, - "cursor_position_button": true + "cursor_position_button": true, + "encoding_indicator": true, }, ```