From 678605a0cd823a277a35c4d4886dc89dc1d939c4 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Tue, 19 Aug 2025 20:23:02 +0530 Subject: [PATCH 01/43] 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 | 3 + 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(+) create mode 100644 crates/encodings/Cargo.toml create mode 100644 crates/encodings/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index ec55e4af77f78a..a24c50bbd3cde6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5520,6 +5520,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" @@ -21176,6 +21189,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 369082ff16736f..d2466c8285f320 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ members = [ "crates/zeta2_tools", "crates/editor", "crates/eval", + "crates/encodings", "crates/explorer_command_injector", "crates/extension", "crates/extension_api", @@ -221,6 +222,7 @@ members = [ "tooling/perf", "tooling/xtask", + "crates/encodings", ] default-members = ["crates/zed"] @@ -315,6 +317,7 @@ edit_prediction = { path = "crates/edit_prediction" } edit_prediction_button = { path = "crates/edit_prediction_button" } edit_prediction_context = { path = "crates/edit_prediction_context" } zeta2_tools = { path = "crates/zeta2_tools" } +encodings = {path = "crates/encodings"} inspector_ui = { path = "crates/inspector_ui" } install_cli = { path = "crates/install_cli" } journal = { path = "crates/journal" } diff --git a/crates/encodings/Cargo.toml b/crates/encodings/Cargo.toml new file mode 100644 index 00000000000000..d49dff06ec93e8 --- /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 00000000000000..23b74295264a2d --- /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 62e29f215146c0..28dabeb5f63c7e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -132,6 +132,7 @@ use crate::persistence::{ }; use crate::{item::ItemBufferKind, notifications::NotificationId}; + 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 9f6196c1482bcf..e9a8046c2ef664 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -52,6 +52,7 @@ debugger_ui.workspace = true diagnostics.workspace = true editor.workspace = true zeta2_tools.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 bd0a600ce52a26..4bdd93460b66d9 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -443,6 +443,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)); let line_ending_indicator = @@ -458,6 +460,7 @@ pub fn initialize_workspace( status_bar.add_right_item(line_ending_indicator, 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 18cd271734406bf4a92cef68e1b771b9ff581670 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sat, 23 Aug 2025 20:03:26 +0530 Subject: [PATCH 02/43] 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 | 11 - 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(+), 186 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 a24c50bbd3cde6..deb3e4cf4e830b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5511,6 +5511,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" @@ -5524,9 +5588,9 @@ dependencies = [ name = "encodings" version = "0.1.0" dependencies = [ + "editor", "fuzzy", "gpui", - "language", "picker", "ui", "util", @@ -6409,6 +6473,7 @@ dependencies = [ "async-trait", "cocoa 0.26.0", "collections", + "encoding", "fsevent", "futures 0.3.31", "git", @@ -8784,6 +8849,7 @@ dependencies = [ "ctor", "diffy", "ec4rs", + "encoding", "fs", "futures 0.3.31", "fuzzy", diff --git a/Cargo.toml b/Cargo.toml index d2466c8285f320..bf29873d2f1f55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -244,7 +244,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" } @@ -254,7 +253,6 @@ assistant_slash_command = { path = "crates/assistant_slash_command" } assistant_slash_commands = { path = "crates/assistant_slash_commands" } 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" } @@ -358,8 +356,6 @@ panel = { path = "crates/panel" } paths = { path = "crates/paths" } perf = { path = "tooling/perf" } 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" } @@ -393,7 +389,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" } @@ -410,7 +405,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" } @@ -793,11 +787,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 @@ -807,7 +797,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 d49dff06ec93e8..2d753958109211 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 23b74295264a2d..bfecfceea32b4d 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 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()) - } +pub struct EncodingIndicator { + pub encoding: Encoding, + pub workspace: WeakEntity, } -impl ModalView for EncodingSaveOrReopenSelector {} +pub mod selectors; -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 00000000000000..368f85d5843234 --- /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 d6413cb7a07b5a..176241a64a253a 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -33,6 +33,8 @@ tempfile.workspace = true text.workspace = true time.workspace = true util.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 00000000000000..8fb38ff24f04ea --- /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 ffc5ad85d14c29..269ed731b8f01e 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -68,6 +68,7 @@ util.workspace = true watch.workspace = true zlog.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 d67434741032ae..b9f60065a3c0e4 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -126,6 +126,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)] @@ -1006,6 +1007,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 4bdd93460b66d9..ad6749b2c84833 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -443,7 +443,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 6c25c996001c12edce4825ccaeaeb5dc27184665 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sun, 24 Aug 2025 17:16:02 +0530 Subject: [PATCH 03/43] 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 00000000000000..e69de29bb2d1d6 diff --git a/Cargo.lock b/Cargo.lock index deb3e4cf4e830b..b4a5ac6ce4bb2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5589,6 +5589,7 @@ name = "encodings" version = "0.1.0" dependencies = [ "editor", + "encoding", "fuzzy", "gpui", "picker", @@ -21255,6 +21256,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 2d753958109211..4b8d877a3aa88c 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 bfecfceea32b4d..638791977035ee 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 368f85d5843234..fd06c518ef6284 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 b9f60065a3c0e4..18dbf40c14987a 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -126,7 +126,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 e9a8046c2ef664..8fa237b7f98cb3 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -166,6 +166,7 @@ zeta.workspace = true zeta2.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 ad6749b2c84833..735dce07ece59c 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -443,10 +443,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 ee70462b696855ca2b8af89421ddfe5cbfeeeb82 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sun, 24 Aug 2025 20:34:33 +0530 Subject: [PATCH 04/43] 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 b4a5ac6ce4bb2f..e1409f1c6c97a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5592,6 +5592,7 @@ dependencies = [ "encoding", "fuzzy", "gpui", + "language", "picker", "ui", "util", diff --git a/crates/encodings/Cargo.toml b/crates/encodings/Cargo.toml index 4b8d877a3aa88c..70b11dd545df5a 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 638791977035ee..169b4bf350d0cc 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 fd06c518ef6284..cdc0702e539f46 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 d3e95b9f528af92f08fe475cbae5ae0033647f4a Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Mon, 25 Aug 2025 12:39:12 +0530 Subject: [PATCH 05/43] =?UTF-8?q?Add=20a=20new=20`load=5Fwith=5Fencoding`?= =?UTF-8?q?=20function=20to=20handle=20files=20with=20various=20encodings.?= =?UTF-8?q?=20Modified=20`Buffer::reload`=20in=20`buffer.rs`=20to=20use=20?= =?UTF-8?q?this=20new=20function,=20allowing=20Zed=20to=20open=20files=20w?= =?UTF-8?q?ith=20any=20encoding=20in=20UTF-8=20mode.=20Files=20with=20char?= =?UTF-8?q?acters=20that=20are=20invalid=20in=20UTF-8=20will=20have=20thos?= =?UTF-8?q?e=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, 111 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1409f1c6c97a2..8820eb70363099 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3720,6 +3720,7 @@ dependencies = [ "dirs 4.0.0", "edit_prediction", "editor", + "encoding", "fs", "futures 0.3.31", "gpui", @@ -20848,6 +20849,7 @@ dependencies = [ "async-lock 2.8.0", "clock", "collections", + "encoding", "fs", "futures 0.3.31", "fuzzy", diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index d9ea4709eadcfa..01a1f8d70b9d2b 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -53,6 +53,8 @@ ui.workspace = true util.workspace = true workspace.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 41c8a17c2d251e..4a3f891f54912d 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1241,6 +1241,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, paths::PathStyle, rel_path::rel_path}; @@ -1458,6 +1459,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 169b4bf350d0cc..53155159980f61 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 cdc0702e539f46..c25b56be56be2d 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 8fb38ff24f04ea..b0a1264a144268 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); + +unsafe impl Send for EncodingWrapper {} +unsafe impl Sync for EncodingWrapper {} -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!(), +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 fn to<'a>(input: String, target: &'a impl encoding::Encoding) -> Vec { - match target.encode(&input, encoding::EncoderTrap::Strict) { - Ok(v) => v, - Err(_) => panic!(), + 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())), + } } } + +/// 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 c794303ef71232..c97b2b671cd3a4 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; @@ -60,6 +61,7 @@ use std::ffi::OsStr; #[cfg(any(test, feature = "test-support"))] pub use fake_git_repo::{LOAD_HEAD_TEXT_TASK, LOAD_INDEX_TEXT_TASK}; +use crate::encodings::EncodingWrapper; pub trait Watcher: Send + Sync { fn add(&self, path: &Path) -> Result<()>; @@ -115,6 +117,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<()>; @@ -599,9 +611,12 @@ impl Fs for RealFs { async fn load(&self, path: &Path) -> Result { let path = path.to_path_buf(); - self.executor - .spawn(async move { Ok(std::fs::read_to_string(path)?) }) - .await + 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> { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 18dbf40c14987a..a1af6990bf4647 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -21,7 +21,8 @@ use anyhow::{Context as _, Result}; use clock::Lamport; pub use clock::ReplicaId; use collections::HashMap; -use fs::MTime; +use encoding::Encoding; +use fs::{Fs, MTime, RealFs}; use futures::channel::oneshot; use gpui::{ App, AppContext as _, BackgroundExecutor, Context, Entity, EventEmitter, HighlightStyle, @@ -417,6 +418,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. @@ -1343,12 +1348,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(()); @@ -5229,6 +5237,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 6d132fbd2cb8c7..e16b0292060afa 100644 --- a/crates/worktree/Cargo.toml +++ b/crates/worktree/Cargo.toml @@ -47,6 +47,8 @@ smol.workspace = true sum_tree.workspace = true text.workspace = true util.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 a4d3f61141c8b0..0fd03f321a6f31 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::{ @@ -3117,6 +3121,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 12e21b57965ea6c76db33cbfbb59b3cd018501a3 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Tue, 26 Aug 2025 22:38:47 +0530 Subject: [PATCH 06/43] Add support for saving and opening files in different encodings. The implementation is now complete. --- Cargo.lock | 9 + Cargo.toml | 1 + crates/agent/src/tools/edit_file_tool.rs | 5 +- crates/agent2/Cargo.toml | 104 + crates/assistant_tools/Cargo.toml | 94 + crates/assistant_tools/src/edit_file_tool.rs | 2439 +++++++++++++++++ crates/collab/Cargo.toml | 1 + crates/collab/src/tests/integration_tests.rs | 5 +- .../random_project_collaboration_tests.rs | 4 +- 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 | 4 + crates/fs/Cargo.toml | 1 + 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 | 4 + crates/language/Cargo.toml | 2 + 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 | 7 +- crates/vim/Cargo.toml | 1 + crates/workspace/Cargo.toml | 1 + crates/workspace/src/workspace.rs | 13 +- crates/worktree/Cargo.toml | 1 + crates/worktree/src/worktree.rs | 11 +- crates/worktree/src/worktree_tests.rs | 9 +- crates/zed/Cargo.toml | 2 +- crates/zed/src/zed.rs | 10 + 36 files changed, 2962 insertions(+), 77 deletions(-) create mode 100644 crates/agent2/Cargo.toml create mode 100644 crates/assistant_tools/Cargo.toml create mode 100644 crates/assistant_tools/src/edit_file_tool.rs diff --git a/Cargo.lock b/Cargo.lock index 8820eb70363099..d466c739f2aebf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,6 +153,7 @@ dependencies = [ "db", "derive_more 0.99.20", "editor", + "encoding", "env_logger 0.11.8", "fs", "futures 0.3.31", @@ -3354,6 +3355,7 @@ dependencies = [ "dashmap 6.1.0", "debugger_ui", "editor", + "encoding", "envy", "extension", "file_finder", @@ -5589,6 +5591,7 @@ dependencies = [ name = "encodings" version = "0.1.0" dependencies = [ + "anyhow", "editor", "encoding", "fuzzy", @@ -5977,6 +5980,7 @@ dependencies = [ "criterion", "ctor", "dap", + "encoding", "extension", "fs", "futures 0.3.31", @@ -6490,6 +6494,7 @@ dependencies = [ "paths", "proto", "rope", + "schemars", "serde", "serde_json", "smol", @@ -7178,6 +7183,7 @@ dependencies = [ "ctor", "db", "editor", + "encoding", "futures 0.3.31", "fuzzy", "git", @@ -13068,6 +13074,7 @@ dependencies = [ "context_server", "dap", "dap_adapters", + "encoding", "extension", "fancy-regex 0.14.0", "fs", @@ -14043,6 +14050,7 @@ dependencies = [ "dap_adapters", "debug_adapter_extension", "editor", + "encoding", "env_logger 0.11.8", "extension", "extension_host", @@ -20807,6 +20815,7 @@ dependencies = [ "component", "dap", "db", + "encoding", "fs", "futures 0.3.31", "gpui", diff --git a/Cargo.toml b/Cargo.toml index bf29873d2f1f55..155099bfb5920f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -498,6 +498,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/agent/src/tools/edit_file_tool.rs b/crates/agent/src/tools/edit_file_tool.rs index 078273dbb8a439..aea3d64c39ccb5 100644 --- a/crates/agent/src/tools/edit_file_tool.rs +++ b/crates/agent/src/tools/edit_file_tool.rs @@ -563,7 +563,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; @@ -744,6 +745,7 @@ mod tests { path!("/root/src/main.rs").as_ref(), &Rope::from_str_small("initial content"), language::LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -911,6 +913,7 @@ mod tests { path!("/root/src/main.rs").as_ref(), &Rope::from_str_small("initial content"), language::LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml new file mode 100644 index 00000000000000..a6b82f29b1e9cd --- /dev/null +++ b/crates/agent2/Cargo.toml @@ -0,0 +1,104 @@ +[package] +name = "agent2" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lib] +path = "src/agent2.rs" + +[features] +test-support = ["db/test-support"] +e2e = [] + +[lints] +workspace = true + +[dependencies] +acp_thread.workspace = true +action_log.workspace = true +agent.workspace = true +agent-client-protocol.workspace = true +agent_servers.workspace = true +agent_settings.workspace = true +anyhow.workspace = true +assistant_context.workspace = true +assistant_tool.workspace = true +assistant_tools.workspace = true +chrono.workspace = true +client.workspace = true +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 +gpui.workspace = true +handlebars = { workspace = true, features = ["rust-embed"] } +html_to_markdown.workspace = true +http_client.workspace = true +indoc.workspace = true +itertools.workspace = true +language.workspace = true +language_model.workspace = true +language_models.workspace = true +log.workspace = true +open.workspace = true +parking_lot.workspace = true +paths.workspace = true +project.workspace = true +prompt_store.workspace = true +rust-embed.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +settings.workspace = true +smol.workspace = true +sqlez.workspace = true +task.workspace = true +telemetry.workspace = true +terminal.workspace = true +thiserror.workspace = true +text.workspace = true +ui.workspace = true +util.workspace = true +uuid.workspace = true +watch.workspace = true +web_search.workspace = true +workspace-hack.workspace = true +zed_env_vars.workspace = true +zstd.workspace = true + + +[dev-dependencies] +agent = { workspace = true, "features" = ["test-support"] } +agent_servers = { workspace = true, "features" = ["test-support"] } +assistant_context = { workspace = true, "features" = ["test-support"] } +ctor.workspace = true +client = { workspace = true, "features" = ["test-support"] } +clock = { workspace = true, "features" = ["test-support"] } +context_server = { workspace = true, "features" = ["test-support"] } +db = { workspace = true, "features" = ["test-support"] } +editor = { workspace = true, "features" = ["test-support"] } +env_logger.workspace = true +fs = { workspace = true, "features" = ["test-support"] } +git = { workspace = true, "features" = ["test-support"] } +gpui = { workspace = true, "features" = ["test-support"] } +gpui_tokio.workspace = true +language = { workspace = true, "features" = ["test-support"] } +language_model = { workspace = true, "features" = ["test-support"] } +lsp = { workspace = true, "features" = ["test-support"] } +pretty_assertions.workspace = true +project = { workspace = true, "features" = ["test-support"] } +reqwest_client.workspace = true +settings = { workspace = true, "features" = ["test-support"] } +tempfile.workspace = true +terminal = { workspace = true, "features" = ["test-support"] } +theme = { workspace = true, "features" = ["test-support"] } +tree-sitter-rust.workspace = true +unindent = { workspace = true } +worktree = { workspace = true, "features" = ["test-support"] } +zlog.workspace = true diff --git a/crates/assistant_tools/Cargo.toml b/crates/assistant_tools/Cargo.toml new file mode 100644 index 00000000000000..e48dc9a0c5edec --- /dev/null +++ b/crates/assistant_tools/Cargo.toml @@ -0,0 +1,94 @@ +[package] +name = "assistant_tools" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/assistant_tools.rs" + +[features] +eval = [] + +[dependencies] +action_log.workspace = true +agent_settings.workspace = true +anyhow.workspace = true +assistant_tool.workspace = true +buffer_diff.workspace = true +chrono.workspace = true +client.workspace = true +cloud_llm_client.workspace = true +collections.workspace = true +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 +handlebars = { workspace = true, features = ["rust-embed"] } +html_to_markdown.workspace = true +http_client.workspace = true +indoc.workspace = true +itertools.workspace = true +language.workspace = true +language_model.workspace = true +log.workspace = true +lsp.workspace = true +markdown.workspace = true +open.workspace = true +paths.workspace = true +portable-pty.workspace = true +project.workspace = true +prompt_store.workspace = true +regex.workspace = true +rust-embed.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +settings.workspace = true +smallvec.workspace = true +streaming_diff.workspace = true +strsim.workspace = true +task.workspace = true +terminal.workspace = true +terminal_view.workspace = true +theme.workspace = true +ui.workspace = true +util.workspace = true +watch.workspace = true +web_search.workspace = true +which.workspace = true +workspace-hack.workspace = true +workspace.workspace = true + +[dev-dependencies] +lsp = { workspace = true, features = ["test-support"] } +client = { workspace = true, features = ["test-support"] } +clock = { workspace = true, features = ["test-support"] } +collections = { workspace = true, features = ["test-support"] } +gpui = { workspace = true, features = ["test-support"] } +gpui_tokio.workspace = true +fs = { workspace = true, features = ["test-support"] } +language = { workspace = true, features = ["test-support"] } +language_model = { workspace = true, features = ["test-support"] } +language_models.workspace = true +project = { workspace = true, features = ["test-support"] } +rand.workspace = true +pretty_assertions.workspace = true +reqwest_client.workspace = true +settings = { workspace = true, features = ["test-support"] } +smol.workspace = true +task = { workspace = true, features = ["test-support"]} +tempfile.workspace = true +theme.workspace = true +tree-sitter-rust.workspace = true +workspace = { workspace = true, features = ["test-support"] } +unindent.workspace = true +zlog.workspace = true diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs new file mode 100644 index 00000000000000..3fa63e59ef7cdc --- /dev/null +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -0,0 +1,2439 @@ +use crate::{ + Templates, + edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat}, + schema::json_schema_for, + ui::{COLLAPSED_LINES, ToolOutputPreview}, +}; +use action_log::ActionLog; +use agent_settings; +use anyhow::{Context as _, Result, anyhow}; +use assistant_tool::{ + AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus, +}; +use buffer_diff::{BufferDiff, BufferDiffSnapshot}; +use editor::{ + Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, multibuffer_context_lines, +}; +use futures::StreamExt; +use gpui::{ + Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task, + TextStyleRefinement, WeakEntity, pulsating_between, px, +}; +use indoc::formatdoc; +use language::{ + Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Point, Rope, + TextBuffer, + language_settings::{self, FormatOnSave, SoftWrap}, +}; +use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; +use markdown::{Markdown, MarkdownElement, MarkdownStyle}; +use paths; +use project::{ + Project, ProjectPath, + lsp_store::{FormatTrigger, LspFormatTarget}, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Settings; +use std::{ + cmp::Reverse, + collections::HashSet, + ops::Range, + path::{Path, PathBuf}, + sync::Arc, + time::Duration, +}; +use theme::ThemeSettings; +use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*}; +use util::ResultExt; +use workspace::Workspace; + +pub struct EditFileTool; + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct EditFileToolInput { + /// A one-line, user-friendly markdown description of the edit. This will be + /// shown in the UI and also passed to another model to perform the edit. + /// + /// Be terse, but also descriptive in what you want to achieve with this + /// edit. Avoid generic instructions. + /// + /// NEVER mention the file path in this description. + /// + /// Fix API endpoint URLs + /// Update copyright year in `page_footer` + /// + /// Make sure to include this field before all the others in the input object + /// so that we can display it immediately. + pub display_description: String, + + /// The full path of the file to create or modify in the project. + /// + /// WARNING: When specifying which file path need changing, you MUST + /// start each path with one of the project's root directories. + /// + /// The following examples assume we have two root directories in the project: + /// - /a/b/backend + /// - /c/d/frontend + /// + /// + /// `backend/src/main.rs` + /// + /// Notice how the file path starts with `backend`. Without that, the path + /// would be ambiguous and the call would fail! + /// + /// + /// + /// `frontend/db.js` + /// + pub path: PathBuf, + + /// The mode of operation on the file. Possible values: + /// - 'edit': Make granular edits to an existing file. + /// - 'create': Create a new file if it doesn't exist. + /// - 'overwrite': Replace the entire contents of an existing file. + /// + /// When a file already exists or you just created it, prefer editing + /// it as opposed to recreating it from scratch. + pub mode: EditFileMode, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum EditFileMode { + Edit, + Create, + Overwrite, +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct EditFileToolOutput { + pub original_path: PathBuf, + pub new_text: String, + pub old_text: Arc, + pub raw_output: Option, +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +struct PartialInput { + #[serde(default)] + path: String, + #[serde(default)] + display_description: String, +} + +const DEFAULT_UI_TEXT: &str = "Editing file"; + +impl Tool for EditFileTool { + fn name(&self) -> String { + "edit_file".into() + } + + fn needs_confirmation( + &self, + input: &serde_json::Value, + project: &Entity, + cx: &App, + ) -> bool { + if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions { + return false; + } + + let Ok(input) = serde_json::from_value::(input.clone()) else { + // If it's not valid JSON, it's going to error and confirming won't do anything. + return false; + }; + + // If any path component matches the local settings folder, then this could affect + // the editor in ways beyond the project source, so prompt. + let local_settings_folder = paths::local_settings_folder_relative_path(); + let path = Path::new(&input.path); + if path + .components() + .any(|component| component.as_os_str() == local_settings_folder.as_os_str()) + { + return true; + } + + // It's also possible that the global config dir is configured to be inside the project, + // so check for that edge case too. + if let Ok(canonical_path) = std::fs::canonicalize(&input.path) + && canonical_path.starts_with(paths::config_dir()) + { + return true; + } + + // Check if path is inside the global config directory + // First check if it's already inside project - if not, try to canonicalize + let project_path = project.read(cx).find_project_path(&input.path, cx); + + // If the path is inside the project, and it's not one of the above edge cases, + // then no confirmation is necessary. Otherwise, confirmation is necessary. + project_path.is_none() + } + + fn may_perform_edits(&self) -> bool { + true + } + + fn description(&self) -> String { + include_str!("edit_file_tool/description.md").to_string() + } + + fn icon(&self) -> IconName { + IconName::ToolPencil + } + + fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { + json_schema_for::(format) + } + + fn ui_text(&self, input: &serde_json::Value) -> String { + match serde_json::from_value::(input.clone()) { + Ok(input) => { + let path = Path::new(&input.path); + let mut description = input.display_description.clone(); + + // Add context about why confirmation may be needed + let local_settings_folder = paths::local_settings_folder_relative_path(); + if path + .components() + .any(|c| c.as_os_str() == local_settings_folder.as_os_str()) + { + description.push_str(" (local settings)"); + } else if let Ok(canonical_path) = std::fs::canonicalize(&input.path) + && canonical_path.starts_with(paths::config_dir()) + { + description.push_str(" (global settings)"); + } + + description + } + Err(_) => "Editing file".to_string(), + } + } + + fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String { + if let Some(input) = serde_json::from_value::(input.clone()).ok() { + let description = input.display_description.trim(); + if !description.is_empty() { + return description.to_string(); + } + + let path = input.path.trim(); + if !path.is_empty() { + return path.to_string(); + } + } + + DEFAULT_UI_TEXT.to_string() + } + + fn run( + self: Arc, + input: serde_json::Value, + request: Arc, + project: Entity, + action_log: Entity, + model: Arc, + window: Option, + cx: &mut App, + ) -> ToolResult { + let input = match serde_json::from_value::(input) { + Ok(input) => input, + Err(err) => return Task::ready(Err(anyhow!(err))).into(), + }; + + let project_path = match resolve_path(&input, project.clone(), cx) { + Ok(path) => path, + Err(err) => return Task::ready(Err(anyhow!(err))).into(), + }; + + let card = window.and_then(|window| { + window + .update(cx, |_, window, cx| { + cx.new(|cx| { + EditFileToolCard::new(input.path.clone(), project.clone(), window, cx) + }) + }) + .ok() + }); + + let card_clone = card.clone(); + let action_log_clone = action_log.clone(); + let task = cx.spawn(async move |cx: &mut AsyncApp| { + let edit_format = EditFormat::from_model(model.clone())?; + let edit_agent = EditAgent::new( + model, + project.clone(), + action_log_clone, + Templates::new(), + edit_format, + ); + + let buffer = project + .update(cx, |project, cx| { + project.open_buffer(project_path.clone(), cx) + })? + .await?; + + let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; + let old_text = cx + .background_spawn({ + let old_snapshot = old_snapshot.clone(); + async move { Arc::new(old_snapshot.text()) } + }) + .await; + + if let Some(card) = card_clone.as_ref() { + card.update(cx, |card, cx| card.initialize(buffer.clone(), cx))?; + } + + let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) { + edit_agent.edit( + buffer.clone(), + input.display_description.clone(), + &request, + cx, + ) + } else { + edit_agent.overwrite( + buffer.clone(), + input.display_description.clone(), + &request, + cx, + ) + }; + + let mut hallucinated_old_text = false; + let mut ambiguous_ranges = Vec::new(); + while let Some(event) = events.next().await { + match event { + EditAgentOutputEvent::Edited { .. } => { + if let Some(card) = card_clone.as_ref() { + card.update(cx, |card, cx| card.update_diff(cx))?; + } + } + EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true, + EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges, + EditAgentOutputEvent::ResolvingEditRange(range) => { + if let Some(card) = card_clone.as_ref() { + card.update(cx, |card, cx| card.reveal_range(range, cx))?; + } + } + } + } + let agent_output = output.await?; + + // If format_on_save is enabled, format the buffer + let format_on_save_enabled = buffer + .read_with(cx, |buffer, cx| { + let settings = language_settings::language_settings( + buffer.language().map(|l| l.name()), + buffer.file(), + cx, + ); + !matches!(settings.format_on_save, FormatOnSave::Off) + }) + .unwrap_or(false); + + if format_on_save_enabled { + action_log.update(cx, |log, cx| { + log.buffer_edited(buffer.clone(), cx); + })?; + let format_task = project.update(cx, |project, cx| { + project.format( + HashSet::from_iter([buffer.clone()]), + LspFormatTarget::Buffers, + false, // Don't push to history since the tool did it. + FormatTrigger::Save, + cx, + ) + })?; + format_task.await.log_err(); + } + + project + .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))? + .await?; + + // Notify the action log that we've edited the buffer (*after* formatting has completed). + action_log.update(cx, |log, cx| { + log.buffer_edited(buffer.clone(), cx); + })?; + + let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; + let (new_text, diff) = cx + .background_spawn({ + let new_snapshot = new_snapshot.clone(); + let old_text = old_text.clone(); + async move { + let new_text = new_snapshot.text(); + let diff = language::unified_diff(&old_text, &new_text); + + (new_text, diff) + } + }) + .await; + + let output = EditFileToolOutput { + original_path: project_path.path.to_path_buf(), + new_text, + old_text, + raw_output: Some(agent_output), + }; + + if let Some(card) = card_clone { + card.update(cx, |card, cx| { + card.update_diff(cx); + card.finalize(cx) + }) + .log_err(); + } + + let input_path = input.path.display(); + if diff.is_empty() { + anyhow::ensure!( + !hallucinated_old_text, + formatdoc! {" + Some edits were produced but none of them could be applied. + Read the relevant sections of {input_path} again so that + I can perform the requested edits. + "} + ); + anyhow::ensure!( + ambiguous_ranges.is_empty(), + { + let line_numbers = ambiguous_ranges + .iter() + .map(|range| range.start.to_string()) + .collect::>() + .join(", "); + formatdoc! {" + matches more than one position in the file (lines: {line_numbers}). Read the + relevant sections of {input_path} again and extend so + that I can perform the requested edits. + "} + } + ); + Ok(ToolResultOutput { + content: ToolResultContent::Text("No edits were made.".into()), + output: serde_json::to_value(output).ok(), + }) + } else { + Ok(ToolResultOutput { + content: ToolResultContent::Text(format!( + "Edited {}:\n\n```diff\n{}\n```", + input_path, diff + )), + output: serde_json::to_value(output).ok(), + }) + } + }); + + ToolResult { + output: task, + card: card.map(AnyToolCard::from), + } + } + + fn deserialize_card( + self: Arc, + output: serde_json::Value, + project: Entity, + window: &mut Window, + cx: &mut App, + ) -> Option { + let output = match serde_json::from_value::(output) { + Ok(output) => output, + Err(_) => return None, + }; + + let card = cx.new(|cx| { + EditFileToolCard::new(output.original_path.clone(), project.clone(), window, cx) + }); + + cx.spawn({ + let path: Arc = output.original_path.into(); + let language_registry = project.read(cx).languages().clone(); + let card = card.clone(); + async move |cx| { + let buffer = + build_buffer(output.new_text, path.clone(), &language_registry, cx).await?; + let buffer_diff = + build_buffer_diff(output.old_text.clone(), &buffer, &language_registry, cx) + .await?; + card.update(cx, |card, cx| { + card.multibuffer.update(cx, |multibuffer, cx| { + let snapshot = buffer.read(cx).snapshot(); + let diff = buffer_diff.read(cx); + let diff_hunk_ranges = diff + .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx) + .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot)) + .collect::>(); + + multibuffer.set_excerpts_for_path( + PathKey::for_buffer(&buffer, cx), + buffer, + diff_hunk_ranges, + multibuffer_context_lines(cx), + cx, + ); + multibuffer.add_diff(buffer_diff, cx); + let end = multibuffer.len(cx); + card.total_lines = + Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1); + }); + + cx.notify(); + })?; + anyhow::Ok(()) + } + }) + .detach_and_log_err(cx); + + Some(card.into()) + } +} + +/// Validate that the file path is valid, meaning: +/// +/// - For `edit` and `overwrite`, the path must point to an existing file. +/// - For `create`, the file must not already exist, but it's parent dir must exist. +fn resolve_path( + input: &EditFileToolInput, + project: Entity, + cx: &mut App, +) -> Result { + let project = project.read(cx); + + match input.mode { + EditFileMode::Edit | EditFileMode::Overwrite => { + let path = project + .find_project_path(&input.path, cx) + .context("Can't edit file: path not found")?; + + let entry = project + .entry_for_path(&path, cx) + .context("Can't edit file: path not found")?; + + anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory"); + Ok(path) + } + + EditFileMode::Create => { + if let Some(path) = project.find_project_path(&input.path, cx) { + anyhow::ensure!( + project.entry_for_path(&path, cx).is_none(), + "Can't create file: file already exists" + ); + } + + let parent_path = input + .path + .parent() + .context("Can't create file: incorrect path")?; + + let parent_project_path = project.find_project_path(&parent_path, cx); + + let parent_entry = parent_project_path + .as_ref() + .and_then(|path| project.entry_for_path(path, cx)) + .context("Can't create file: parent directory doesn't exist")?; + + anyhow::ensure!( + parent_entry.is_dir(), + "Can't create file: parent is not a directory" + ); + + let file_name = input + .path + .file_name() + .context("Can't create file: invalid filename")?; + + let new_file_path = parent_project_path.map(|parent| ProjectPath { + path: Arc::from(parent.path.join(file_name)), + ..parent + }); + + new_file_path.context("Can't create file") + } + } +} + +pub struct EditFileToolCard { + path: PathBuf, + editor: Entity, + multibuffer: Entity, + project: Entity, + buffer: Option>, + base_text: Option>, + buffer_diff: Option>, + revealed_ranges: Vec>, + diff_task: Option>>, + preview_expanded: bool, + error_expanded: Option>, + full_height_expanded: bool, + total_lines: Option, +} + +impl EditFileToolCard { + pub fn new(path: PathBuf, project: Entity, window: &mut Window, cx: &mut App) -> Self { + let expand_edit_card = agent_settings::AgentSettings::get_global(cx).expand_edit_card; + let multibuffer = cx.new(|_| MultiBuffer::without_headers(Capability::ReadOnly)); + + let editor = cx.new(|cx| { + let mut editor = Editor::new( + EditorMode::Full { + scale_ui_elements_with_buffer_font_size: false, + show_active_line_background: false, + sized_by_content: true, + }, + multibuffer.clone(), + Some(project.clone()), + window, + cx, + ); + editor.set_show_gutter(false, cx); + editor.disable_inline_diagnostics(); + editor.disable_expand_excerpt_buttons(cx); + // Keep horizontal scrollbar so user can scroll horizontally if needed + editor.set_show_vertical_scrollbar(false, cx); + editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx); + editor.set_soft_wrap_mode(SoftWrap::None, cx); + editor.scroll_manager.set_forbid_vertical_scroll(true); + editor.set_show_indent_guides(false, cx); + editor.set_read_only(true); + editor.set_show_breakpoints(false, cx); + editor.set_show_code_actions(false, cx); + editor.set_show_git_diff_gutter(false, cx); + editor.set_expand_all_diff_hunks(cx); + editor + }); + Self { + path, + project, + editor, + multibuffer, + buffer: None, + base_text: None, + buffer_diff: None, + revealed_ranges: Vec::new(), + diff_task: None, + preview_expanded: true, + error_expanded: None, + full_height_expanded: expand_edit_card, + total_lines: None, + } + } + + pub fn initialize(&mut self, buffer: Entity, cx: &mut App) { + let buffer_snapshot = buffer.read(cx).snapshot(); + let base_text = buffer_snapshot.text(); + let language_registry = buffer.read(cx).language_registry(); + let text_snapshot = buffer.read(cx).text_snapshot(); + + // Create a buffer diff with the current text as the base + let buffer_diff = cx.new(|cx| { + let mut diff = BufferDiff::new(&text_snapshot, cx); + let _ = diff.set_base_text( + buffer_snapshot.clone(), + language_registry, + text_snapshot, + cx, + ); + diff + }); + + self.buffer = Some(buffer); + self.base_text = Some(base_text.into()); + self.buffer_diff = Some(buffer_diff.clone()); + + // Add the diff to the multibuffer + self.multibuffer + .update(cx, |multibuffer, cx| multibuffer.add_diff(buffer_diff, cx)); + } + + pub fn is_loading(&self) -> bool { + self.total_lines.is_none() + } + + pub fn update_diff(&mut self, cx: &mut Context) { + let Some(buffer) = self.buffer.as_ref() else { + return; + }; + let Some(buffer_diff) = self.buffer_diff.as_ref() else { + return; + }; + + let buffer = buffer.clone(); + let buffer_diff = buffer_diff.clone(); + let base_text = self.base_text.clone(); + self.diff_task = Some(cx.spawn(async move |this, cx| { + let text_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot())?; + let diff_snapshot = BufferDiff::update_diff( + buffer_diff.clone(), + text_snapshot.clone(), + base_text, + false, + false, + None, + None, + cx, + ) + .await?; + buffer_diff.update(cx, |diff, cx| { + diff.set_snapshot(diff_snapshot, &text_snapshot, cx) + })?; + this.update(cx, |this, cx| this.update_visible_ranges(cx)) + })); + } + + pub fn reveal_range(&mut self, range: Range, cx: &mut Context) { + self.revealed_ranges.push(range); + self.update_visible_ranges(cx); + } + + fn update_visible_ranges(&mut self, cx: &mut Context) { + let Some(buffer) = self.buffer.as_ref() else { + return; + }; + + let ranges = self.excerpt_ranges(cx); + self.total_lines = self.multibuffer.update(cx, |multibuffer, cx| { + multibuffer.set_excerpts_for_path( + PathKey::for_buffer(buffer, cx), + buffer.clone(), + ranges, + multibuffer_context_lines(cx), + cx, + ); + let end = multibuffer.len(cx); + Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1) + }); + cx.notify(); + } + + fn excerpt_ranges(&self, cx: &App) -> Vec> { + let Some(buffer) = self.buffer.as_ref() else { + return Vec::new(); + }; + let Some(diff) = self.buffer_diff.as_ref() else { + return Vec::new(); + }; + + let buffer = buffer.read(cx); + let diff = diff.read(cx); + let mut ranges = diff + .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx) + .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer)) + .collect::>(); + ranges.extend( + self.revealed_ranges + .iter() + .map(|range| range.to_point(buffer)), + ); + ranges.sort_unstable_by_key(|range| (range.start, Reverse(range.end))); + + // Merge adjacent ranges + let mut ranges = ranges.into_iter().peekable(); + let mut merged_ranges = Vec::new(); + while let Some(mut range) = ranges.next() { + while let Some(next_range) = ranges.peek() { + if range.end >= next_range.start { + range.end = range.end.max(next_range.end); + ranges.next(); + } else { + break; + } + } + + merged_ranges.push(range); + } + merged_ranges + } + + pub fn finalize(&mut self, cx: &mut Context) -> Result<()> { + let ranges = self.excerpt_ranges(cx); + let buffer = self.buffer.take().context("card was already finalized")?; + let base_text = self + .base_text + .take() + .context("card was already finalized")?; + let language_registry = self.project.read(cx).languages().clone(); + + // Replace the buffer in the multibuffer with the snapshot + let buffer = cx.new(|cx| { + let language = buffer.read(cx).language().cloned(); + let buffer = TextBuffer::new_normalized( + 0, + cx.entity_id().as_non_zero_u64().into(), + buffer.read(cx).line_ending(), + buffer.read(cx).as_rope().clone(), + ); + let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite); + buffer.set_language(language, cx); + buffer + }); + + let buffer_diff = cx.spawn({ + let buffer = buffer.clone(); + async move |_this, cx| { + build_buffer_diff(base_text, &buffer, &language_registry, cx).await + } + }); + + cx.spawn(async move |this, cx| { + let buffer_diff = buffer_diff.await?; + this.update(cx, |this, cx| { + this.multibuffer.update(cx, |multibuffer, cx| { + let path_key = PathKey::for_buffer(&buffer, cx); + multibuffer.clear(cx); + multibuffer.set_excerpts_for_path( + path_key, + buffer, + ranges, + multibuffer_context_lines(cx), + cx, + ); + multibuffer.add_diff(buffer_diff.clone(), cx); + }); + + cx.notify(); + }) + }) + .detach_and_log_err(cx); + Ok(()) + } +} + +impl ToolCard for EditFileToolCard { + fn render( + &mut self, + status: &ToolUseStatus, + window: &mut Window, + workspace: WeakEntity, + cx: &mut Context, + ) -> impl IntoElement { + let error_message = match status { + ToolUseStatus::Error(err) => Some(err), + _ => None, + }; + + let running_or_pending = match status { + ToolUseStatus::Running | ToolUseStatus::Pending => Some(()), + _ => None, + }; + + let should_show_loading = running_or_pending.is_some() && !self.full_height_expanded; + + let path_label_button = h_flex() + .id(("edit-tool-path-label-button", self.editor.entity_id())) + .w_full() + .max_w_full() + .px_1() + .gap_0p5() + .cursor_pointer() + .rounded_sm() + .opacity(0.8) + .hover(|label| { + label + .opacity(1.) + .bg(cx.theme().colors().element_hover.opacity(0.5)) + }) + .tooltip(Tooltip::text("Jump to File")) + .child( + h_flex() + .child( + Icon::new(IconName::ToolPencil) + .size(IconSize::Small) + .color(Color::Muted), + ) + .child( + div() + .text_size(rems(0.8125)) + .child(self.path.display().to_string()) + .ml_1p5() + .mr_0p5(), + ) + .child( + Icon::new(IconName::ArrowUpRight) + .size(IconSize::Small) + .color(Color::Ignored), + ), + ) + .on_click({ + let path = self.path.clone(); + move |_, window, cx| { + workspace + .update(cx, { + |workspace, cx| { + let Some(project_path) = + workspace.project().read(cx).find_project_path(&path, cx) + else { + return; + }; + let open_task = + workspace.open_path(project_path, None, true, window, cx); + window + .spawn(cx, async move |cx| { + let item = open_task.await?; + if let Some(active_editor) = item.downcast::() { + active_editor + .update_in(cx, |editor, window, cx| { + let snapshot = + editor.buffer().read(cx).snapshot(cx); + let first_hunk = editor + .diff_hunks_in_ranges( + &[editor::Anchor::min() + ..editor::Anchor::max()], + &snapshot, + ) + .next(); + if let Some(first_hunk) = first_hunk { + let first_hunk_start = + first_hunk.multi_buffer_range().start; + editor.change_selections( + Default::default(), + window, + cx, + |selections| { + selections.select_anchor_ranges([ + first_hunk_start + ..first_hunk_start, + ]); + }, + ) + } + }) + .log_err(); + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + }) + .ok(); + } + }) + .into_any_element(); + + let codeblock_header_bg = cx + .theme() + .colors() + .element_background + .blend(cx.theme().colors().editor_foreground.opacity(0.025)); + + let codeblock_header = h_flex() + .flex_none() + .p_1() + .gap_1() + .justify_between() + .rounded_t_md() + .when(error_message.is_none(), |header| { + header.bg(codeblock_header_bg) + }) + .child(path_label_button) + .when(should_show_loading, |header| { + header.pr_1p5().child( + Icon::new(IconName::ArrowCircle) + .size(IconSize::XSmall) + .color(Color::Info) + .with_rotate_animation(2), + ) + }) + .when_some(error_message, |header, error_message| { + header.child( + h_flex() + .gap_1() + .child( + Icon::new(IconName::Close) + .size(IconSize::Small) + .color(Color::Error), + ) + .child( + Disclosure::new( + ("edit-file-error-disclosure", self.editor.entity_id()), + self.error_expanded.is_some(), + ) + .opened_icon(IconName::ChevronUp) + .closed_icon(IconName::ChevronDown) + .on_click(cx.listener({ + let error_message = error_message.clone(); + + move |this, _event, _window, cx| { + if this.error_expanded.is_some() { + this.error_expanded.take(); + } else { + this.error_expanded = Some(cx.new(|cx| { + Markdown::new(error_message.clone(), None, None, cx) + })) + } + cx.notify(); + } + })), + ), + ) + }) + .when(error_message.is_none() && !self.is_loading(), |header| { + header.child( + Disclosure::new( + ("edit-file-disclosure", self.editor.entity_id()), + self.preview_expanded, + ) + .opened_icon(IconName::ChevronUp) + .closed_icon(IconName::ChevronDown) + .on_click(cx.listener( + move |this, _event, _window, _cx| { + this.preview_expanded = !this.preview_expanded; + }, + )), + ) + }); + + let (editor, editor_line_height) = self.editor.update(cx, |editor, cx| { + let line_height = editor + .style() + .map(|style| style.text.line_height_in_pixels(window.rem_size())) + .unwrap_or_default(); + + editor.set_text_style_refinement(TextStyleRefinement { + font_size: Some( + TextSize::Small + .rems(cx) + .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx)) + .into(), + ), + ..TextStyleRefinement::default() + }); + let element = editor.render(window, cx); + (element.into_any_element(), line_height) + }); + + let border_color = cx.theme().colors().border.opacity(0.6); + + let waiting_for_diff = { + let styles = [ + ("w_4_5", (0.1, 0.85), 2000), + ("w_1_4", (0.2, 0.75), 2200), + ("w_2_4", (0.15, 0.64), 1900), + ("w_3_5", (0.25, 0.72), 2300), + ("w_2_5", (0.3, 0.56), 1800), + ]; + + let mut container = v_flex() + .p_3() + .gap_1() + .border_t_1() + .rounded_b_md() + .border_color(border_color) + .bg(cx.theme().colors().editor_background); + + for (width_method, pulse_range, duration_ms) in styles.iter() { + let (min_opacity, max_opacity) = *pulse_range; + let placeholder = match *width_method { + "w_4_5" => div().w_3_4(), + "w_1_4" => div().w_1_4(), + "w_2_4" => div().w_2_4(), + "w_3_5" => div().w_3_5(), + "w_2_5" => div().w_2_5(), + _ => div().w_1_2(), + } + .id("loading_div") + .h_1() + .rounded_full() + .bg(cx.theme().colors().element_active) + .with_animation( + "loading_pulsate", + Animation::new(Duration::from_millis(*duration_ms)) + .repeat() + .with_easing(pulsating_between(min_opacity, max_opacity)), + |label, delta| label.opacity(delta), + ); + + container = container.child(placeholder); + } + + container + }; + + v_flex() + .mb_2() + .border_1() + .when(error_message.is_some(), |card| card.border_dashed()) + .border_color(border_color) + .rounded_md() + .overflow_hidden() + .child(codeblock_header) + .when_some(self.error_expanded.as_ref(), |card, error_markdown| { + card.child( + v_flex() + .p_2() + .gap_1() + .border_t_1() + .border_dashed() + .border_color(border_color) + .bg(cx.theme().colors().editor_background) + .rounded_b_md() + .child( + Label::new("Error") + .size(LabelSize::XSmall) + .color(Color::Error), + ) + .child( + div() + .rounded_md() + .text_ui_sm(cx) + .bg(cx.theme().colors().editor_background) + .child(MarkdownElement::new( + error_markdown.clone(), + markdown_style(window, cx), + )), + ), + ) + }) + .when(self.is_loading() && error_message.is_none(), |card| { + card.child(waiting_for_diff) + }) + .when(self.preview_expanded && !self.is_loading(), |card| { + let editor_view = v_flex() + .relative() + .h_full() + .when(!self.full_height_expanded, |editor_container| { + editor_container.max_h(px(COLLAPSED_LINES as f32 * editor_line_height.0)) + }) + .overflow_hidden() + .border_t_1() + .border_color(border_color) + .bg(cx.theme().colors().editor_background) + .child(editor); + + card.child( + ToolOutputPreview::new(editor_view.into_any_element(), self.editor.entity_id()) + .with_total_lines(self.total_lines.unwrap_or(0) as usize) + .toggle_state(self.full_height_expanded) + .with_collapsed_fade() + .on_toggle({ + let this = cx.entity().downgrade(); + move |is_expanded, _window, cx| { + if let Some(this) = this.upgrade() { + this.update(cx, |this, _cx| { + this.full_height_expanded = is_expanded; + }); + } + } + }), + ) + }) + } +} + +fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle { + let theme_settings = ThemeSettings::get_global(cx); + let ui_font_size = TextSize::Default.rems(cx); + let mut text_style = window.text_style(); + + text_style.refine(&TextStyleRefinement { + font_family: Some(theme_settings.ui_font.family.clone()), + font_fallbacks: theme_settings.ui_font.fallbacks.clone(), + font_features: Some(theme_settings.ui_font.features.clone()), + font_size: Some(ui_font_size.into()), + color: Some(cx.theme().colors().text), + ..Default::default() + }); + + MarkdownStyle { + base_text_style: text_style.clone(), + selection_background_color: cx.theme().colors().element_selection_background, + ..Default::default() + } +} + +async fn build_buffer( + mut text: String, + path: Arc, + language_registry: &Arc, + cx: &mut AsyncApp, +) -> Result> { + let line_ending = LineEnding::detect(&text); + LineEnding::normalize(&mut text); + let text = Rope::from(text); + let language = cx + .update(|_cx| language_registry.language_for_file_path(&path))? + .await + .ok(); + let buffer = cx.new(|cx| { + let buffer = TextBuffer::new_normalized( + 0, + cx.entity_id().as_non_zero_u64().into(), + line_ending, + text, + ); + let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite); + buffer.set_language(language, cx); + buffer + })?; + Ok(buffer) +} + +async fn build_buffer_diff( + old_text: Arc, + buffer: &Entity, + language_registry: &Arc, + cx: &mut AsyncApp, +) -> Result> { + let buffer = cx.update(|cx| buffer.read(cx).snapshot())?; + + let old_text_rope = cx + .background_spawn({ + let old_text = old_text.clone(); + async move { Rope::from(old_text.as_str()) } + }) + .await; + let base_buffer = cx + .update(|cx| { + Buffer::build_snapshot( + old_text_rope, + buffer.language().cloned(), + Some(language_registry.clone()), + cx, + ) + })? + .await; + + let diff_snapshot = cx + .update(|cx| { + BufferDiffSnapshot::new_with_base_buffer( + buffer.text.clone(), + Some(old_text), + base_buffer, + cx, + ) + })? + .await; + + let secondary_diff = cx.new(|cx| { + let mut diff = BufferDiff::new(&buffer, cx); + diff.set_snapshot(diff_snapshot.clone(), &buffer, cx); + diff + })?; + + cx.new(|cx| { + let mut diff = BufferDiff::new(&buffer.text, cx); + diff.set_snapshot(diff_snapshot, &buffer, cx); + diff.set_secondary_diff(secondary_diff); + diff + }) +} + +#[cfg(test)] +mod tests { + use super::*; + 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; + use settings::SettingsStore; + use std::fs; + use util::path; + + #[gpui::test] + async fn test_edit_nonexistent_file(cx: &mut TestAppContext) { + init_test(cx); + + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree("/root", json!({})).await; + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let model = Arc::new(FakeLanguageModel::default()); + let result = cx + .update(|cx| { + let input = serde_json::to_value(EditFileToolInput { + display_description: "Some edit".into(), + path: "root/nonexistent_file.txt".into(), + mode: EditFileMode::Edit, + }) + .unwrap(); + Arc::new(EditFileTool) + .run( + input, + Arc::default(), + project.clone(), + action_log, + model, + None, + cx, + ) + .output + }) + .await; + assert_eq!( + result.unwrap_err().to_string(), + "Can't edit file: path not found" + ); + } + + #[gpui::test] + async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) { + let mode = &EditFileMode::Create; + + let result = test_resolve_path(mode, "root/new.txt", cx); + assert_resolved_path_eq(result.await, "new.txt"); + + let result = test_resolve_path(mode, "new.txt", cx); + assert_resolved_path_eq(result.await, "new.txt"); + + let result = test_resolve_path(mode, "dir/new.txt", cx); + assert_resolved_path_eq(result.await, "dir/new.txt"); + + let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx); + assert_eq!( + result.await.unwrap_err().to_string(), + "Can't create file: file already exists" + ); + + let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx); + assert_eq!( + result.await.unwrap_err().to_string(), + "Can't create file: parent directory doesn't exist" + ); + } + + #[gpui::test] + async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) { + let mode = &EditFileMode::Edit; + + let path_with_root = "root/dir/subdir/existing.txt"; + let path_without_root = "dir/subdir/existing.txt"; + let result = test_resolve_path(mode, path_with_root, cx); + assert_resolved_path_eq(result.await, path_without_root); + + let result = test_resolve_path(mode, path_without_root, cx); + assert_resolved_path_eq(result.await, path_without_root); + + let result = test_resolve_path(mode, "root/nonexistent.txt", cx); + assert_eq!( + result.await.unwrap_err().to_string(), + "Can't edit file: path not found" + ); + + let result = test_resolve_path(mode, "root/dir", cx); + assert_eq!( + result.await.unwrap_err().to_string(), + "Can't edit file: path is a directory" + ); + } + + async fn test_resolve_path( + mode: &EditFileMode, + path: &str, + cx: &mut TestAppContext, + ) -> anyhow::Result { + init_test(cx); + + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree( + "/root", + json!({ + "dir": { + "subdir": { + "existing.txt": "hello" + } + } + }), + ) + .await; + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + + let input = EditFileToolInput { + display_description: "Some edit".into(), + path: path.into(), + mode: mode.clone(), + }; + + cx.update(|cx| resolve_path(&input, project, cx)) + } + + fn assert_resolved_path_eq(path: anyhow::Result, expected: &str) { + let actual = path + .expect("Should return valid path") + .path + .to_str() + .unwrap() + .replace("\\", "/"); // Naive Windows paths normalization + assert_eq!(actual, expected); + } + + #[test] + fn still_streaming_ui_text_with_path() { + let input = json!({ + "path": "src/main.rs", + "display_description": "", + "old_string": "old code", + "new_string": "new code" + }); + + assert_eq!(EditFileTool.still_streaming_ui_text(&input), "src/main.rs"); + } + + #[test] + fn still_streaming_ui_text_with_description() { + let input = json!({ + "path": "", + "display_description": "Fix error handling", + "old_string": "old code", + "new_string": "new code" + }); + + assert_eq!( + EditFileTool.still_streaming_ui_text(&input), + "Fix error handling", + ); + } + + #[test] + fn still_streaming_ui_text_with_path_and_description() { + let input = json!({ + "path": "src/main.rs", + "display_description": "Fix error handling", + "old_string": "old code", + "new_string": "new code" + }); + + assert_eq!( + EditFileTool.still_streaming_ui_text(&input), + "Fix error handling", + ); + } + + #[test] + fn still_streaming_ui_text_no_path_or_description() { + let input = json!({ + "path": "", + "display_description": "", + "old_string": "old code", + "new_string": "new code" + }); + + assert_eq!( + EditFileTool.still_streaming_ui_text(&input), + DEFAULT_UI_TEXT, + ); + } + + #[test] + fn still_streaming_ui_text_with_null() { + let input = serde_json::Value::Null; + + assert_eq!( + EditFileTool.still_streaming_ui_text(&input), + DEFAULT_UI_TEXT, + ); + } + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + language::init(cx); + TelemetrySettings::register(cx); + agent_settings::AgentSettings::register(cx); + Project::init_settings(cx); + }); + } + + fn init_test_with_config(cx: &mut TestAppContext, data_dir: &Path) { + cx.update(|cx| { + // Set custom data directory (config will be under data_dir/config) + paths::set_custom_data_dir(data_dir.to_str().unwrap()); + + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + language::init(cx); + TelemetrySettings::register(cx); + agent_settings::AgentSettings::register(cx); + Project::init_settings(cx); + }); + } + + #[gpui::test] + async fn test_format_on_save(cx: &mut TestAppContext) { + init_test(cx); + + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree("/root", json!({"src": {}})).await; + + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + + // Set up a Rust language with LSP formatting support + let rust_language = Arc::new(language::Language::new( + language::LanguageConfig { + name: "Rust".into(), + matcher: language::LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + ..Default::default() + }, + None, + )); + + // Register the language and fake LSP + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_language); + + let mut fake_language_servers = language_registry.register_fake_lsp( + "Rust", + language::FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + }, + ); + + // Create the file + fs.save( + path!("/root/src/main.rs").as_ref(), + &"initial content".into(), + language::LineEnding::Unix, + EncodingWrapper::new(UTF_8), + ) + .await + .unwrap(); + + // Open the buffer to trigger LSP initialization + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer(path!("/root/src/main.rs"), cx) + }) + .await + .unwrap(); + + // Register the buffer with language servers + let _handle = project.update(cx, |project, cx| { + project.register_buffer_with_language_servers(&buffer, cx) + }); + + const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\n"; + const FORMATTED_CONTENT: &str = + "This file was formatted by the fake formatter in the test.\n"; + + // Get the fake language server and set up formatting handler + let fake_language_server = fake_language_servers.next().await.unwrap(); + fake_language_server.set_request_handler::({ + |_, _| async move { + Ok(Some(vec![lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 0)), + new_text: FORMATTED_CONTENT.to_string(), + }])) + } + }); + + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let model = Arc::new(FakeLanguageModel::default()); + + // First, test with format_on_save enabled + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings::( + cx, + |settings| { + settings.defaults.format_on_save = Some(FormatOnSave::On); + settings.defaults.formatter = + Some(language::language_settings::SelectedFormatter::Auto); + }, + ); + }); + }); + + // Have the model stream unformatted content + let edit_result = { + let edit_task = cx.update(|cx| { + let input = serde_json::to_value(EditFileToolInput { + display_description: "Create main function".into(), + path: "root/src/main.rs".into(), + mode: EditFileMode::Overwrite, + }) + .unwrap(); + Arc::new(EditFileTool) + .run( + input, + Arc::default(), + project.clone(), + action_log.clone(), + model.clone(), + None, + cx, + ) + .output + }); + + // Stream the unformatted content + cx.executor().run_until_parked(); + model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); + model.end_last_completion_stream(); + + edit_task.await + }; + assert!(edit_result.is_ok()); + + // Wait for any async operations (e.g. formatting) to complete + cx.executor().run_until_parked(); + + // Read the file to verify it was formatted automatically + let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); + assert_eq!( + // Ignore carriage returns on Windows + new_content.replace("\r\n", "\n"), + FORMATTED_CONTENT, + "Code should be formatted when format_on_save is enabled" + ); + + let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count()); + + assert_eq!( + stale_buffer_count, 0, + "BUG: Buffer is incorrectly marked as stale after format-on-save. Found {} stale buffers. \ + This causes the agent to think the file was modified externally when it was just formatted.", + stale_buffer_count + ); + + // Next, test with format_on_save disabled + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings::( + cx, + |settings| { + settings.defaults.format_on_save = Some(FormatOnSave::Off); + }, + ); + }); + }); + + // Stream unformatted edits again + let edit_result = { + let edit_task = cx.update(|cx| { + let input = serde_json::to_value(EditFileToolInput { + display_description: "Update main function".into(), + path: "root/src/main.rs".into(), + mode: EditFileMode::Overwrite, + }) + .unwrap(); + Arc::new(EditFileTool) + .run( + input, + Arc::default(), + project.clone(), + action_log.clone(), + model.clone(), + None, + cx, + ) + .output + }); + + // Stream the unformatted content + cx.executor().run_until_parked(); + model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); + model.end_last_completion_stream(); + + edit_task.await + }; + assert!(edit_result.is_ok()); + + // Wait for any async operations (e.g. formatting) to complete + cx.executor().run_until_parked(); + + // Verify the file was not formatted + let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); + assert_eq!( + // Ignore carriage returns on Windows + new_content.replace("\r\n", "\n"), + UNFORMATTED_CONTENT, + "Code should not be formatted when format_on_save is disabled" + ); + } + + #[gpui::test] + async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) { + init_test(cx); + + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree("/root", json!({"src": {}})).await; + + // Create a simple file with trailing whitespace + fs.save( + path!("/root/src/main.rs").as_ref(), + &"initial content".into(), + language::LineEnding::Unix, + EncodingWrapper::new(UTF_8), + ) + .await + .unwrap(); + + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let model = Arc::new(FakeLanguageModel::default()); + + // First, test with remove_trailing_whitespace_on_save enabled + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings::( + cx, + |settings| { + settings.defaults.remove_trailing_whitespace_on_save = Some(true); + }, + ); + }); + }); + + const CONTENT_WITH_TRAILING_WHITESPACE: &str = + "fn main() { \n println!(\"Hello!\"); \n}\n"; + + // Have the model stream content that contains trailing whitespace + let edit_result = { + let edit_task = cx.update(|cx| { + let input = serde_json::to_value(EditFileToolInput { + display_description: "Create main function".into(), + path: "root/src/main.rs".into(), + mode: EditFileMode::Overwrite, + }) + .unwrap(); + Arc::new(EditFileTool) + .run( + input, + Arc::default(), + project.clone(), + action_log.clone(), + model.clone(), + None, + cx, + ) + .output + }); + + // Stream the content with trailing whitespace + cx.executor().run_until_parked(); + model.send_last_completion_stream_text_chunk( + CONTENT_WITH_TRAILING_WHITESPACE.to_string(), + ); + model.end_last_completion_stream(); + + edit_task.await + }; + assert!(edit_result.is_ok()); + + // Wait for any async operations (e.g. formatting) to complete + cx.executor().run_until_parked(); + + // Read the file to verify trailing whitespace was removed automatically + assert_eq!( + // Ignore carriage returns on Windows + fs.load(path!("/root/src/main.rs").as_ref()) + .await + .unwrap() + .replace("\r\n", "\n"), + "fn main() {\n println!(\"Hello!\");\n}\n", + "Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled" + ); + + // Next, test with remove_trailing_whitespace_on_save disabled + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings::( + cx, + |settings| { + settings.defaults.remove_trailing_whitespace_on_save = Some(false); + }, + ); + }); + }); + + // Stream edits again with trailing whitespace + let edit_result = { + let edit_task = cx.update(|cx| { + let input = serde_json::to_value(EditFileToolInput { + display_description: "Update main function".into(), + path: "root/src/main.rs".into(), + mode: EditFileMode::Overwrite, + }) + .unwrap(); + Arc::new(EditFileTool) + .run( + input, + Arc::default(), + project.clone(), + action_log.clone(), + model.clone(), + None, + cx, + ) + .output + }); + + // Stream the content with trailing whitespace + cx.executor().run_until_parked(); + model.send_last_completion_stream_text_chunk( + CONTENT_WITH_TRAILING_WHITESPACE.to_string(), + ); + model.end_last_completion_stream(); + + edit_task.await + }; + assert!(edit_result.is_ok()); + + // Wait for any async operations (e.g. formatting) to complete + cx.executor().run_until_parked(); + + // Verify the file still has trailing whitespace + // Read the file again - it should still have trailing whitespace + let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); + assert_eq!( + // Ignore carriage returns on Windows + final_content.replace("\r\n", "\n"), + CONTENT_WITH_TRAILING_WHITESPACE, + "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled" + ); + } + + #[gpui::test] + async fn test_needs_confirmation(cx: &mut TestAppContext) { + init_test(cx); + let tool = Arc::new(EditFileTool); + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree("/root", json!({})).await; + + // Test 1: Path with .zed component should require confirmation + let input_with_zed = json!({ + "display_description": "Edit settings", + "path": ".zed/settings.json", + "mode": "edit" + }); + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + cx.update(|cx| { + assert!( + tool.needs_confirmation(&input_with_zed, &project, cx), + "Path with .zed component should require confirmation" + ); + }); + + // Test 2: Absolute path should require confirmation + let input_absolute = json!({ + "display_description": "Edit file", + "path": "/etc/hosts", + "mode": "edit" + }); + cx.update(|cx| { + assert!( + tool.needs_confirmation(&input_absolute, &project, cx), + "Absolute path should require confirmation" + ); + }); + + // Test 3: Relative path without .zed should not require confirmation + let input_relative = json!({ + "display_description": "Edit file", + "path": "root/src/main.rs", + "mode": "edit" + }); + cx.update(|cx| { + assert!( + !tool.needs_confirmation(&input_relative, &project, cx), + "Relative path without .zed should not require confirmation" + ); + }); + + // Test 4: Path with .zed in the middle should require confirmation + let input_zed_middle = json!({ + "display_description": "Edit settings", + "path": "root/.zed/tasks.json", + "mode": "edit" + }); + cx.update(|cx| { + assert!( + tool.needs_confirmation(&input_zed_middle, &project, cx), + "Path with .zed in any component should require confirmation" + ); + }); + + // Test 5: When always_allow_tool_actions is enabled, no confirmation needed + cx.update(|cx| { + let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); + settings.always_allow_tool_actions = true; + agent_settings::AgentSettings::override_global(settings, cx); + + assert!( + !tool.needs_confirmation(&input_with_zed, &project, cx), + "When always_allow_tool_actions is true, no confirmation should be needed" + ); + assert!( + !tool.needs_confirmation(&input_absolute, &project, cx), + "When always_allow_tool_actions is true, no confirmation should be needed for absolute paths" + ); + }); + } + + #[gpui::test] + async fn test_ui_text_shows_correct_context(cx: &mut TestAppContext) { + // Set up a custom config directory for testing + let temp_dir = tempfile::tempdir().unwrap(); + init_test_with_config(cx, temp_dir.path()); + + let tool = Arc::new(EditFileTool); + + // Test ui_text shows context for various paths + let test_cases = vec![ + ( + json!({ + "display_description": "Update config", + "path": ".zed/settings.json", + "mode": "edit" + }), + "Update config (local settings)", + ".zed path should show local settings context", + ), + ( + json!({ + "display_description": "Fix bug", + "path": "src/.zed/local.json", + "mode": "edit" + }), + "Fix bug (local settings)", + "Nested .zed path should show local settings context", + ), + ( + json!({ + "display_description": "Update readme", + "path": "README.md", + "mode": "edit" + }), + "Update readme", + "Normal path should not show additional context", + ), + ( + json!({ + "display_description": "Edit config", + "path": "config.zed", + "mode": "edit" + }), + "Edit config", + ".zed as extension should not show context", + ), + ]; + + for (input, expected_text, description) in test_cases { + cx.update(|_cx| { + let ui_text = tool.ui_text(&input); + assert_eq!(ui_text, expected_text, "Failed for case: {}", description); + }); + } + } + + #[gpui::test] + async fn test_needs_confirmation_outside_project(cx: &mut TestAppContext) { + init_test(cx); + let tool = Arc::new(EditFileTool); + let fs = project::FakeFs::new(cx.executor()); + + // Create a project in /project directory + fs.insert_tree("/project", json!({})).await; + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + + // Test file outside project requires confirmation + let input_outside = json!({ + "display_description": "Edit file", + "path": "/outside/file.txt", + "mode": "edit" + }); + cx.update(|cx| { + assert!( + tool.needs_confirmation(&input_outside, &project, cx), + "File outside project should require confirmation" + ); + }); + + // Test file inside project doesn't require confirmation + let input_inside = json!({ + "display_description": "Edit file", + "path": "project/file.txt", + "mode": "edit" + }); + cx.update(|cx| { + assert!( + !tool.needs_confirmation(&input_inside, &project, cx), + "File inside project should not require confirmation" + ); + }); + } + + #[gpui::test] + async fn test_needs_confirmation_config_paths(cx: &mut TestAppContext) { + // Set up a custom data directory for testing + let temp_dir = tempfile::tempdir().unwrap(); + init_test_with_config(cx, temp_dir.path()); + + let tool = Arc::new(EditFileTool); + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree("/home/user/myproject", json!({})).await; + let project = Project::test(fs.clone(), [path!("/home/user/myproject").as_ref()], cx).await; + + // Get the actual local settings folder name + let local_settings_folder = paths::local_settings_folder_relative_path(); + + // Test various config path patterns + let test_cases = vec![ + ( + format!("{}/settings.json", local_settings_folder.display()), + true, + "Top-level local settings file".to_string(), + ), + ( + format!( + "myproject/{}/settings.json", + local_settings_folder.display() + ), + true, + "Local settings in project path".to_string(), + ), + ( + format!("src/{}/config.toml", local_settings_folder.display()), + true, + "Local settings in subdirectory".to_string(), + ), + ( + ".zed.backup/file.txt".to_string(), + true, + ".zed.backup is outside project".to_string(), + ), + ( + "my.zed/file.txt".to_string(), + true, + "my.zed is outside project".to_string(), + ), + ( + "myproject/src/file.zed".to_string(), + false, + ".zed as file extension".to_string(), + ), + ( + "myproject/normal/path/file.rs".to_string(), + false, + "Normal file without config paths".to_string(), + ), + ]; + + for (path, should_confirm, description) in test_cases { + let input = json!({ + "display_description": "Edit file", + "path": path, + "mode": "edit" + }); + cx.update(|cx| { + assert_eq!( + tool.needs_confirmation(&input, &project, cx), + should_confirm, + "Failed for case: {} - path: {}", + description, + path + ); + }); + } + } + + #[gpui::test] + async fn test_needs_confirmation_global_config(cx: &mut TestAppContext) { + // Set up a custom data directory for testing + let temp_dir = tempfile::tempdir().unwrap(); + init_test_with_config(cx, temp_dir.path()); + + let tool = Arc::new(EditFileTool); + let fs = project::FakeFs::new(cx.executor()); + + // Create test files in the global config directory + let global_config_dir = paths::config_dir(); + fs::create_dir_all(&global_config_dir).unwrap(); + let global_settings_path = global_config_dir.join("settings.json"); + fs::write(&global_settings_path, "{}").unwrap(); + + fs.insert_tree("/project", json!({})).await; + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + + // Test global config paths + let test_cases = vec![ + ( + global_settings_path.to_str().unwrap().to_string(), + true, + "Global settings file should require confirmation", + ), + ( + global_config_dir + .join("keymap.json") + .to_str() + .unwrap() + .to_string(), + true, + "Global keymap file should require confirmation", + ), + ( + "project/normal_file.rs".to_string(), + false, + "Normal project file should not require confirmation", + ), + ]; + + for (path, should_confirm, description) in test_cases { + let input = json!({ + "display_description": "Edit file", + "path": path, + "mode": "edit" + }); + cx.update(|cx| { + assert_eq!( + tool.needs_confirmation(&input, &project, cx), + should_confirm, + "Failed for case: {}", + description + ); + }); + } + } + + #[gpui::test] + async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) { + init_test(cx); + let tool = Arc::new(EditFileTool); + let fs = project::FakeFs::new(cx.executor()); + + // Create multiple worktree directories + fs.insert_tree( + "/workspace/frontend", + json!({ + "src": { + "main.js": "console.log('frontend');" + } + }), + ) + .await; + fs.insert_tree( + "/workspace/backend", + json!({ + "src": { + "main.rs": "fn main() {}" + } + }), + ) + .await; + fs.insert_tree( + "/workspace/shared", + json!({ + ".zed": { + "settings.json": "{}" + } + }), + ) + .await; + + // Create project with multiple worktrees + let project = Project::test( + fs.clone(), + [ + path!("/workspace/frontend").as_ref(), + path!("/workspace/backend").as_ref(), + path!("/workspace/shared").as_ref(), + ], + cx, + ) + .await; + + // Test files in different worktrees + let test_cases = vec![ + ("frontend/src/main.js", false, "File in first worktree"), + ("backend/src/main.rs", false, "File in second worktree"), + ( + "shared/.zed/settings.json", + true, + ".zed file in third worktree", + ), + ("/etc/hosts", true, "Absolute path outside all worktrees"), + ( + "../outside/file.txt", + true, + "Relative path outside worktrees", + ), + ]; + + for (path, should_confirm, description) in test_cases { + let input = json!({ + "display_description": "Edit file", + "path": path, + "mode": "edit" + }); + cx.update(|cx| { + assert_eq!( + tool.needs_confirmation(&input, &project, cx), + should_confirm, + "Failed for case: {} - path: {}", + description, + path + ); + }); + } + } + + #[gpui::test] + async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) { + init_test(cx); + let tool = Arc::new(EditFileTool); + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree( + "/project", + json!({ + ".zed": { + "settings.json": "{}" + }, + "src": { + ".zed": { + "local.json": "{}" + } + } + }), + ) + .await; + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + + // Test edge cases + let test_cases = vec![ + // Empty path - find_project_path returns Some for empty paths + ("", false, "Empty path is treated as project root"), + // Root directory + ("/", true, "Root directory should be outside project"), + // Parent directory references - find_project_path resolves these + ( + "project/../other", + false, + "Path with .. is resolved by find_project_path", + ), + ( + "project/./src/file.rs", + false, + "Path with . should work normally", + ), + // Windows-style paths (if on Windows) + #[cfg(target_os = "windows")] + ("C:\\Windows\\System32\\hosts", true, "Windows system path"), + #[cfg(target_os = "windows")] + ("project\\src\\main.rs", false, "Windows-style project path"), + ]; + + for (path, should_confirm, description) in test_cases { + let input = json!({ + "display_description": "Edit file", + "path": path, + "mode": "edit" + }); + cx.update(|cx| { + assert_eq!( + tool.needs_confirmation(&input, &project, cx), + should_confirm, + "Failed for case: {} - path: {}", + description, + path + ); + }); + } + } + + #[gpui::test] + async fn test_ui_text_with_all_path_types(cx: &mut TestAppContext) { + init_test(cx); + let tool = Arc::new(EditFileTool); + + // Test UI text for various scenarios + let test_cases = vec![ + ( + json!({ + "display_description": "Update config", + "path": ".zed/settings.json", + "mode": "edit" + }), + "Update config (local settings)", + ".zed path should show local settings context", + ), + ( + json!({ + "display_description": "Fix bug", + "path": "src/.zed/local.json", + "mode": "edit" + }), + "Fix bug (local settings)", + "Nested .zed path should show local settings context", + ), + ( + json!({ + "display_description": "Update readme", + "path": "README.md", + "mode": "edit" + }), + "Update readme", + "Normal path should not show additional context", + ), + ( + json!({ + "display_description": "Edit config", + "path": "config.zed", + "mode": "edit" + }), + "Edit config", + ".zed as extension should not show context", + ), + ]; + + for (input, expected_text, description) in test_cases { + cx.update(|_cx| { + let ui_text = tool.ui_text(&input); + assert_eq!(ui_text, expected_text, "Failed for case: {}", description); + }); + } + } + + #[gpui::test] + async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) { + init_test(cx); + let tool = Arc::new(EditFileTool); + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree( + "/project", + json!({ + "existing.txt": "content", + ".zed": { + "settings.json": "{}" + } + }), + ) + .await; + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + + // Test different EditFileMode values + let modes = vec![ + EditFileMode::Edit, + EditFileMode::Create, + EditFileMode::Overwrite, + ]; + + for mode in modes { + // Test .zed path with different modes + let input_zed = json!({ + "display_description": "Edit settings", + "path": "project/.zed/settings.json", + "mode": mode + }); + cx.update(|cx| { + assert!( + tool.needs_confirmation(&input_zed, &project, cx), + ".zed path should require confirmation regardless of mode: {:?}", + mode + ); + }); + + // Test outside path with different modes + let input_outside = json!({ + "display_description": "Edit file", + "path": "/outside/file.txt", + "mode": mode + }); + cx.update(|cx| { + assert!( + tool.needs_confirmation(&input_outside, &project, cx), + "Outside path should require confirmation regardless of mode: {:?}", + mode + ); + }); + + // Test normal path with different modes + let input_normal = json!({ + "display_description": "Edit file", + "path": "project/normal.txt", + "mode": mode + }); + cx.update(|cx| { + assert!( + !tool.needs_confirmation(&input_normal, &project, cx), + "Normal path should not require confirmation regardless of mode: {:?}", + mode + ); + }); + } + } + + #[gpui::test] + async fn test_always_allow_tool_actions_bypasses_all_checks(cx: &mut TestAppContext) { + // Set up with custom directories for deterministic testing + let temp_dir = tempfile::tempdir().unwrap(); + init_test_with_config(cx, temp_dir.path()); + + let tool = Arc::new(EditFileTool); + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree("/project", json!({})).await; + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + + // Enable always_allow_tool_actions + cx.update(|cx| { + let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); + settings.always_allow_tool_actions = true; + agent_settings::AgentSettings::override_global(settings, cx); + }); + + // Test that all paths that normally require confirmation are bypassed + let global_settings_path = paths::config_dir().join("settings.json"); + fs::create_dir_all(paths::config_dir()).unwrap(); + fs::write(&global_settings_path, "{}").unwrap(); + + let test_cases = vec![ + ".zed/settings.json", + "project/.zed/config.toml", + global_settings_path.to_str().unwrap(), + "/etc/hosts", + "/absolute/path/file.txt", + "../outside/project.txt", + ]; + + for path in test_cases { + let input = json!({ + "display_description": "Edit file", + "path": path, + "mode": "edit" + }); + cx.update(|cx| { + assert!( + !tool.needs_confirmation(&input, &project, cx), + "Path {} should not require confirmation when always_allow_tool_actions is true", + path + ); + }); + } + + // Disable always_allow_tool_actions and verify confirmation is required again + cx.update(|cx| { + let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); + settings.always_allow_tool_actions = false; + agent_settings::AgentSettings::override_global(settings, cx); + }); + + // Verify .zed path requires confirmation again + let input = json!({ + "display_description": "Edit file", + "path": ".zed/settings.json", + "mode": "edit" + }); + cx.update(|cx| { + assert!( + tool.needs_confirmation(&input, &project, cx), + ".zed path should require confirmation when always_allow_tool_actions is false" + ); + }); + } +} diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index c8467da7954b19..f48cebaaaa91a0 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 37e6622b0343bc..1e0e5b4c09c910 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::{ repository::repo_path, @@ -3701,6 +3702,7 @@ async fn test_buffer_reloading( path!("/dir/a.txt").as_ref(), &new_contents, LineEnding::Windows, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4481,6 +4483,7 @@ async fn test_reloading_buffer_manually( path!("/a/a.rs").as_ref(), &Rope::from_str_small("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 399f1a663fe727..9106edd483a5f4 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::{ @@ -943,6 +944,7 @@ impl RandomizedTest for ProjectCollaborationTest { &path, &Rope::from_str_small(content.as_str()), text::LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 01a1f8d70b9d2b..047707169d2c7a 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 @@ ui.workspace = true util.workspace = true workspace.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 70b11dd545df5a..dd47679500f5bc 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 53155159980f61..91c1e8799a3483 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 c25b56be56be2d..9cca1551ec8f6d 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 16cbd9ac0c0ef9..6ddd4e7a76ec10 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 50b5169f7ad119..468d25427dda92 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::{ @@ -1506,6 +1508,7 @@ impl ExtensionStore { &index_path, &Rope::from_str(&index_json, &executor), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .context("failed to save extension index") @@ -1678,6 +1681,7 @@ impl ExtensionStore { &tmp_dir.join(EXTENSION_TOML), &Rope::from_str_small(&manifest_toml), language::LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await?; } else { diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 176241a64a253a..17f99feb0646db 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 diff --git a/crates/fs/src/encodings.rs b/crates/fs/src/encodings.rs index b0a1264a144268..8aecbcb7646f25 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 c97b2b671cd3a4..dd7f3d1e049bdb 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -62,6 +62,7 @@ use std::ffi::OsStr; #[cfg(any(test, feature = "test-support"))] pub use fake_git_repo::{LOAD_HEAD_TEXT_TASK, LOAD_INDEX_TEXT_TASK}; use crate::encodings::EncodingWrapper; +use crate::encodings::from_utf8; pub trait Watcher: Send + Sync { fn add(&self, path: &Path) -> Result<()>; @@ -129,7 +130,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; @@ -674,7 +681,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?; @@ -682,7 +695,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(()) @@ -2395,14 +2410,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 486e43fea94f53..4c22b7f4ff07de 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 a99b7f8e2428ca..2494177e9d9089 100644 --- a/crates/git_ui/src/file_diff_view.rs +++ b/crates/git_ui/src/file_diff_view.rs @@ -358,10 +358,12 @@ 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 language::Rope; use project::{FakeFs, Fs, Project}; use settings::SettingsStore; + use project::{FakeFs, Fs, Project, encodings::EncodingWrapper}; use std::path::PathBuf; use unindent::unindent; use util::path; @@ -440,6 +442,7 @@ mod tests { ", )), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -474,6 +477,7 @@ mod tests { ", )), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 269ed731b8f01e..f90c2680324509 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -30,7 +30,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 diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index a1af6990bf4647..709804184dcc18 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -22,7 +22,7 @@ use clock::Lamport; pub use clock::ReplicaId; use collections::HashMap; use encoding::Encoding; -use fs::{Fs, MTime, RealFs}; +use fs::MTime; use futures::channel::oneshot; use gpui::{ App, AppContext as _, BackgroundExecutor, Context, Entity, EventEmitter, HighlightStyle, @@ -1348,7 +1348,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| { @@ -5238,11 +5238,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 d9285a8c24ec51..d598ee7685c995 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 zeroize.workspace = true zlog.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 3fb70251869058..d8dc6f4e0ffba7 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -387,6 +387,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) @@ -395,7 +397,7 @@ impl LocalBufferStore { } let save = worktree.update(cx, |worktree, cx| { - worktree.write_file(path, 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 3743f9769eaaff..50b88e7e850a51 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}, @@ -981,10 +982,12 @@ async fn save_prettier_server_file( executor: &BackgroundExecutor, ) -> 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_str(prettier::PRETTIER_SERVER_JS, executor), 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 3dc918d5a757af..281bfb3b2410ae 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -12,7 +12,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, @@ -1459,10 +1460,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(), &Rope::default(), Default::default(), + encoding_wrapper.clone(), ) .await .unwrap(); @@ -1470,6 +1475,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon path!("/the-stdlib/LICENSE").as_ref(), &Rope::default(), Default::default(), + encoding_wrapper.clone(), ) .await .unwrap(); @@ -1477,6 +1483,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon path!("/the/stdlib/src/string.rs").as_ref(), &Rope::default(), Default::default(), + encoding_wrapper, ) .await .unwrap(); @@ -4068,12 +4075,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(), &Rope::from_str("the first contents", cx.background_executor()), Default::default(), + encoding_wrapper.clone(), ) .await .unwrap(); @@ -4085,6 +4095,7 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) path!("/dir/file1").as_ref(), &Rope::from_str("the second contents", cx.background_executor()), Default::default(), + encoding_wrapper, ) .await .unwrap(); @@ -4123,12 +4134,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(), &Rope::from_str("the first contents", cx.background_executor()), Default::default(), + encoding_wrapper, ) .await .unwrap(); @@ -4803,10 +4817,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(), &Rope::from_str(new_contents.as_str(), cx.background_executor()), LineEnding::Unix, + encoding_wrapper, ) .await .unwrap(); @@ -4834,11 +4852,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(), &Rope::from_str("\n\n\nAAAA\naaa\nBB\nbbbbb\n", cx.background_executor()), LineEnding::Unix, + encoding_wrapper, ) .await .unwrap(); @@ -4885,12 +4906,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(), &Rope::from_str("aaa\nb\nc\n", cx.background_executor()), LineEnding::Windows, + encoding_wrapper, ) .await .unwrap(); diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 5034b24e0661eb..8e9869542125e5 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -28,6 +28,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 c7e09e3f681d77..ed696312f7a2d9 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -6,10 +6,10 @@ use agent::{AgentTool, ReadFileTool, ReadFileToolInput, ToolCallEventStream}; use client::{Client, UserStore}; use clock::FakeSystemClock; use collections::{HashMap, HashSet}; -use language_model::LanguageModelToolResultContent; +use encoding::all::UTF_8; use extension::ExtensionHostProxy; -use fs::{FakeFs, Fs}; +use fs::{FakeFs, Fs, encodings::EncodingWrapper}; use gpui::{AppContext as _, Entity, SemanticVersion, SharedString, TestAppContext}; use http_client::{BlockedHttpClient, FakeHttpClient}; use language::{ @@ -122,6 +122,7 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test path!("/code/project1/src/main.rs").as_ref(), &Rope::from_str_small("fn main() {}"), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -768,6 +769,7 @@ async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppCont &PathBuf::from(path!("/code/project1/src/lib.rs")), &Rope::from_str_small("bangles"), LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -783,6 +785,7 @@ async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppCont &PathBuf::from(path!("/code/project1/src/lib.rs")), &Rope::from_str_small("bloop"), LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 4cea29508f437d..0aa24f40f40da9 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -54,6 +54,7 @@ vim_mode_setting.workspace = true workspace.workspace = true zed_actions.workspace = true + [dev-dependencies] assets.workspace = true command_palette.workspace = true diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index d5d3016ab27043..163e068d1363a8 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 28dabeb5f63c7e..086e621ac7c5e3 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}; @@ -132,7 +134,6 @@ use crate::persistence::{ }; use crate::{item::ItemBufferKind, notifications::NotificationId}; - pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200); static ZED_WINDOW_SIZE: LazyLock>> = LazyLock::new(|| { @@ -7587,8 +7588,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(cx), 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 e16b0292060afa..0dc81b73fd32ad 100644 --- a/crates/worktree/Cargo.toml +++ b/crates/worktree/Cargo.toml @@ -27,6 +27,7 @@ anyhow.workspace = true async-lock.workspace = true clock.workspace = true collections.workspace = true +encoding.workspace = true fs.workspace = true futures.workspace = true fuzzy.workspace = true diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 0fd03f321a6f31..51842bc89d2f7b 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -735,9 +735,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"))) } @@ -1450,15 +1451,21 @@ impl LocalWorktree { text: Rope, line_ending: LineEnding, cx: &Context, + encoding: &'static dyn Encoding, ) -> Task>> { let fs = self.fs.clone(); let is_private = self.is_path_private(&path); let abs_path = self.absolutize(&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 1cce23712ae88f..c6e6aeac87d619 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; @@ -651,6 +652,7 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) { "/root/.gitignore".as_ref(), &Rope::from_str("e", cx.background_executor()), Default::default(), + encoding_wrapper, ) .await .unwrap(); @@ -724,6 +726,7 @@ async fn test_write_file(cx: &mut TestAppContext) { Rope::from_str("hello", cx.background_executor()), Default::default(), cx, + UTF_8, ) }) .await @@ -735,6 +738,7 @@ async fn test_write_file(cx: &mut TestAppContext) { Rope::from_str("world", cx.background_executor()), Default::default(), cx, + UTF_8, ) }) .await @@ -1769,6 +1773,7 @@ fn randomly_mutate_worktree( Rope::default(), Default::default(), cx, + UTF_8, ); cx.background_spawn(async move { task.await?; @@ -1857,10 +1862,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, &Rope::from_str(ignore_contents.as_str(), executor), Default::default(), + encoding_wrapper, ) .await .unwrap(); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 8fa237b7f98cb3..25dc614bfd63c0 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -52,6 +52,7 @@ debugger_ui.workspace = true diagnostics.workspace = true editor.workspace = true zeta2_tools.workspace = true +encoding.workspace = true encodings.workspace = true env_logger.workspace = true extension.workspace = true @@ -166,7 +167,6 @@ zeta.workspace = true zeta2.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 735dce07ece59c..607fb302970fb1 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2176,6 +2176,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, @@ -4381,6 +4383,7 @@ mod tests { "/settings.json".as_ref(), &Rope::from_str_small(r#"{"base_keymap": "Atom"}"#), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4391,6 +4394,7 @@ mod tests { "/keymap.json".as_ref(), &Rope::from_str_small(r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4439,6 +4443,7 @@ mod tests { "/keymap.json".as_ref(), &Rope::from_str_small(r#"[{"bindings": {"backspace": "test_only::ActionB"}}]"#), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4459,6 +4464,7 @@ mod tests { "/settings.json".as_ref(), &Rope::from_str_small(r#"{"base_keymap": "JetBrains"}"#), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4499,6 +4505,7 @@ mod tests { "/settings.json".as_ref(), &Rope::from_str_small(r#"{"base_keymap": "Atom"}"#), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4508,6 +4515,7 @@ mod tests { "/keymap.json".as_ref(), &Rope::from_str_small(r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4551,6 +4559,7 @@ mod tests { "/keymap.json".as_ref(), &Rope::from_str_small(r#"[{"bindings": {"backspace": null}}]"#), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4571,6 +4580,7 @@ mod tests { "/settings.json".as_ref(), &Rope::from_str_small(r#"{"base_keymap": "JetBrains"}"#), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); From 1078f85be7e3ee2af44e2f01414e8a9eb40accac Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Tue, 26 Aug 2025 23:54:57 +0530 Subject: [PATCH 07/43] 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/encodings/Cargo.toml | 13 +++++++------ crates/encodings/src/lib.rs | 13 ++++++++++++- docs/src/configuring-zed.md | 3 ++- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d466c739f2aebf..3b6d9b364d689a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5598,6 +5598,7 @@ dependencies = [ "gpui", "language", "picker", + "settings", "ui", "util", "workspace", diff --git a/assets/settings/default.json b/assets/settings/default.json index f62cc1844732db..973dfd46a0d00c 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1352,7 +1352,9 @@ // Whether to show the cursor position button in the status bar. "cursor_position_button": true, // Whether to show active line endings button in the status bar. - "line_endings_button": false + "line_endings_button": false, + // Whether to show the encoding indicator in the status bar. + "encoding_indicator": true }, // Settings specific to the terminal "terminal": { diff --git a/crates/encodings/Cargo.toml b/crates/encodings/Cargo.toml index dd47679500f5bc..a4c2b959e8343b 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 91c1e8799a3483..9715a48d9469fb 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 054b6b1b5c812b..eddaf1084b03c9 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1541,7 +1541,8 @@ Positive `integer` value between 1 and 32. Values outside of this range will be "status_bar": { "active_language_button": true, "cursor_position_button": true, - "line_endings_button": false + "line_endings_button": false, + "encoding_indicator": true, }, ``` From a61d94d3ae1ff17934913f0a813144690e8e54ec Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Wed, 27 Aug 2025 08:11:19 +0530 Subject: [PATCH 08/43] fix: Fix an issue which caused the focus to not be on `EncodingSelector` after an action was selected from `EncodingSaveOrReopenSelector` --- crates/encodings/src/lib.rs | 2 +- crates/encodings/src/selectors.rs | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/encodings/src/lib.rs b/crates/encodings/src/lib.rs index 9715a48d9469fb..0336d70b37cf8b 100644 --- a/crates/encodings/src/lib.rs +++ b/crates/encodings/src/lib.rs @@ -21,7 +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, + show: bool, // Whether to show the indicator or not, based on whether an editor is active } pub mod selectors; diff --git a/crates/encodings/src/selectors.rs b/crates/encodings/src/selectors.rs index 9cca1551ec8f6d..46fc939aba1281 100644 --- a/crates/encodings/src/selectors.rs +++ b/crates/encodings/src/selectors.rs @@ -120,13 +120,14 @@ pub mod save_or_reopen { workspace.update(cx, |workspace, cx| { workspace.toggle_modal(window, cx, |window, cx| { - EncodingSelector::new( + let selector = EncodingSelector::new( window, cx, Action::Save, buffer.downgrade(), weak_workspace, - ) + ); + selector }) }); } @@ -143,14 +144,15 @@ pub mod save_or_reopen { workspace.update(cx, |workspace, cx| { workspace.toggle_modal(window, cx, |window, cx| { - EncodingSelector::new( + let selector = EncodingSelector::new( window, cx, Action::Reopen, buffer.downgrade(), weak_workspace, - ) - }) + ); + selector + }); }); } } @@ -516,7 +518,7 @@ pub mod encoding { impl Focusable for EncodingSelector { fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle { - cx.focus_handle() + self.picker.focus_handle(cx) } } From cca3bb85a5d1e381024fc87e19fae1b236316efb Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sat, 30 Aug 2025 07:49:53 +0530 Subject: [PATCH 09/43] - Fix cargo clippy errors - Add workspace-hack as a dependency --- Cargo.lock | 1 + crates/encodings/Cargo.toml | 2 ++ crates/fs/src/encodings.rs | 8 ++++---- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b6d9b364d689a..b241569ffb0922 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5602,6 +5602,7 @@ dependencies = [ "ui", "util", "workspace", + "workspace-hack", ] [[package]] diff --git a/crates/encodings/Cargo.toml b/crates/encodings/Cargo.toml index a4c2b959e8343b..19456909425d5b 100644 --- a/crates/encodings/Cargo.toml +++ b/crates/encodings/Cargo.toml @@ -16,6 +16,8 @@ settings.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true +workspace-hack.workspace = true + [lints] workspace = true diff --git a/crates/fs/src/encodings.rs b/crates/fs/src/encodings.rs index 8aecbcb7646f25..efdd6fc187434c 100644 --- a/crates/fs/src/encodings.rs +++ b/crates/fs/src/encodings.rs @@ -86,11 +86,11 @@ impl EncodingWrapper { } /// 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?) +pub async fn to_utf8(input: Vec, encoding: EncodingWrapper) -> Result { + 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?) +pub async fn from_utf8(input: String, target: EncodingWrapper) -> Result> { + target.encode(input).await } From 6e1a5faa756a7135697672b7d7cf9421e4534636 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Mon, 1 Sep 2025 19:34:34 +0530 Subject: [PATCH 10/43] Migrate from `encoding` to `encoding_rs` Co-authored-by: GitHub Copilot --- Cargo.lock | 90 +------ Cargo.toml | 2 +- crates/agent/src/tools/edit_file_tool.rs | 2 +- crates/agent2/Cargo.toml | 2 +- crates/assistant_tools/Cargo.toml | 2 +- crates/assistant_tools/src/edit_file_tool.rs | 2 +- crates/collab/Cargo.toml | 2 +- crates/collab/src/tests/integration_tests.rs | 2 +- .../random_project_collaboration_tests.rs | 2 +- crates/copilot/Cargo.toml | 2 +- crates/copilot/src/copilot.rs | 4 +- crates/encodings/Cargo.toml | 2 +- crates/encodings/src/lib.rs | 234 ++++++++---------- crates/encodings/src/selectors.rs | 68 +++-- crates/extension_host/Cargo.toml | 2 +- crates/extension_host/src/extension_host.rs | 2 +- crates/fs/Cargo.toml | 2 +- crates/fs/src/encodings.rs | 81 ++++-- crates/fs/src/fs.rs | 2 +- crates/git_ui/Cargo.toml | 2 +- crates/git_ui/src/file_diff_view.rs | 2 +- crates/language/Cargo.toml | 2 +- crates/language/src/buffer.rs | 10 +- crates/project/Cargo.toml | 2 +- crates/project/src/prettier_store.rs | 2 +- crates/project/src/project_tests.rs | 2 +- crates/remote_server/Cargo.toml | 2 +- crates/workspace/Cargo.toml | 2 +- crates/workspace/src/workspace.rs | 2 +- crates/worktree/Cargo.toml | 2 +- crates/worktree/src/worktree.rs | 8 +- crates/worktree/src/worktree_tests.rs | 2 +- crates/zed/src/zed.rs | 2 +- 33 files changed, 248 insertions(+), 299 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b241569ffb0922..d2ef2c789a5398 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,7 +153,7 @@ dependencies = [ "db", "derive_more 0.99.20", "editor", - "encoding", + "encoding_rs", "env_logger 0.11.8", "fs", "futures 0.3.31", @@ -3355,7 +3355,7 @@ dependencies = [ "dashmap 6.1.0", "debugger_ui", "editor", - "encoding", + "encoding_rs", "envy", "extension", "file_finder", @@ -3722,7 +3722,7 @@ dependencies = [ "dirs 4.0.0", "edit_prediction", "editor", - "encoding", + "encoding_rs", "fs", "futures 0.3.31", "gpui", @@ -5514,70 +5514,6 @@ 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" @@ -5593,7 +5529,7 @@ version = "0.1.0" dependencies = [ "anyhow", "editor", - "encoding", + "encoding_rs", "fuzzy", "gpui", "language", @@ -5982,7 +5918,7 @@ dependencies = [ "criterion", "ctor", "dap", - "encoding", + "encoding_rs", "extension", "fs", "futures 0.3.31", @@ -6482,7 +6418,7 @@ dependencies = [ "async-trait", "cocoa 0.26.0", "collections", - "encoding", + "encoding_rs", "fsevent", "futures 0.3.31", "git", @@ -7185,7 +7121,7 @@ dependencies = [ "ctor", "db", "editor", - "encoding", + "encoding_rs", "futures 0.3.31", "fuzzy", "git", @@ -8860,7 +8796,7 @@ dependencies = [ "ctor", "diffy", "ec4rs", - "encoding", + "encoding_rs", "fs", "futures 0.3.31", "fuzzy", @@ -13076,7 +13012,7 @@ dependencies = [ "context_server", "dap", "dap_adapters", - "encoding", + "encoding_rs", "extension", "fancy-regex 0.14.0", "fs", @@ -14052,7 +13988,7 @@ dependencies = [ "dap_adapters", "debug_adapter_extension", "editor", - "encoding", + "encoding_rs", "env_logger 0.11.8", "extension", "extension_host", @@ -20817,7 +20753,7 @@ dependencies = [ "component", "dap", "db", - "encoding", + "encoding_rs", "fs", "futures 0.3.31", "gpui", @@ -20860,7 +20796,7 @@ dependencies = [ "async-lock 2.8.0", "clock", "collections", - "encoding", + "encoding_rs", "fs", "futures 0.3.31", "fuzzy", @@ -21270,7 +21206,7 @@ dependencies = [ "diagnostics", "edit_prediction_button", "editor", - "encoding", + "encoding_rs", "encodings", "env_logger 0.11.8", "extension", diff --git a/Cargo.toml b/Cargo.toml index 155099bfb5920f..80c8452838d9db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -498,7 +498,7 @@ documented = "0.9.1" dotenvy = "0.15.0" ec4rs = "1.1" emojis = "0.6.1" -encoding = "0.2.33" +encoding_rs = "0.8" env_logger = "0.11" exec = "0.3.1" fancy-regex = "0.14.0" diff --git a/crates/agent/src/tools/edit_file_tool.rs b/crates/agent/src/tools/edit_file_tool.rs index aea3d64c39ccb5..6764a1759e81d6 100644 --- a/crates/agent/src/tools/edit_file_tool.rs +++ b/crates/agent/src/tools/edit_file_tool.rs @@ -563,7 +563,7 @@ mod tests { use super::*; use crate::{ContextServerRegistry, Templates}; use client::TelemetrySettings; - use encoding::all::UTF_8; + use encoding_rs::UTF_8; use fs::{Fs, encodings::EncodingWrapper}; use gpui::{TestAppContext, UpdateGlobal}; use language_model::fake_provider::FakeLanguageModel; diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index a6b82f29b1e9cd..4135f07710970a 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -32,7 +32,7 @@ cloud_llm_client.workspace = true collections.workspace = true context_server.workspace = true db.workspace = true -encoding.workspace = true +encoding_rs.workspace = true fs.workspace = true futures.workspace = true git.workspace = true diff --git a/crates/assistant_tools/Cargo.toml b/crates/assistant_tools/Cargo.toml index e48dc9a0c5edec..dff6caccef9979 100644 --- a/crates/assistant_tools/Cargo.toml +++ b/crates/assistant_tools/Cargo.toml @@ -28,7 +28,7 @@ component.workspace = true derive_more.workspace = true diffy = "0.4.2" editor.workspace = true -encoding.workspace = true +encoding_rs.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 3fa63e59ef7cdc..4a3e755a35dc79 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -1231,7 +1231,7 @@ mod tests { use super::*; use ::fs::{Fs, encodings::EncodingWrapper}; use client::TelemetrySettings; - use encoding::all::UTF_8; + use encoding_rs::UTF_8; use gpui::{TestAppContext, UpdateGlobal}; use language_model::fake_provider::FakeLanguageModel; use serde_json::json; diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index f48cebaaaa91a0..82a0b7141e3b04 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -31,7 +31,7 @@ chrono.workspace = true clock.workspace = true collections.workspace = true dashmap.workspace = true -encoding.workspace = true +encoding_rs.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 1e0e5b4c09c910..0b65c91625801a 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -12,7 +12,7 @@ use buffer_diff::{DiffHunkSecondaryStatus, DiffHunkStatus, assert_hunks}; use call::{ActiveCall, ParticipantLocation, Room, room}; use client::{RECEIVE_TIMEOUT, User}; use collections::{HashMap, HashSet}; -use encoding::all::UTF_8; +use encoding_rs::UTF_8; use fs::{FakeFs, Fs as _, RemoveOptions, encodings::EncodingWrapper}; use futures::{StreamExt as _, channel::mpsc}; use git::{ diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index 9106edd483a5f4..c7571665574d6c 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -5,7 +5,7 @@ use async_trait::async_trait; use call::ActiveCall; use collections::{BTreeMap, HashMap}; use editor::Bias; -use encoding::all::UTF_8; +use encoding_rs::UTF_8; use fs::{FakeFs, Fs as _, encodings::EncodingWrapper}; use git::status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode}; use gpui::{BackgroundExecutor, Entity, TestAppContext}; diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 047707169d2c7a..129afaaffb13b3 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -30,7 +30,7 @@ client.workspace = true collections.workspace = true command_palette_hooks.workspace = true dirs.workspace = true -encoding.workspace = true +encoding_rs.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 4a3f891f54912d..cf136962b9c004 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1241,7 +1241,7 @@ async fn get_copilot_lsp(fs: Arc, node_runtime: NodeRuntime) -> anyhow:: #[cfg(test)] mod tests { use super::*; - use encoding::Encoding; + use encoding_rs::Encoding; use gpui::TestAppContext; use util::{path, paths::PathStyle, rel_path::rel_path}; @@ -1460,7 +1460,7 @@ mod tests { unimplemented!() } - fn load_with_encoding(&self, _: &App, _: &'static dyn Encoding) -> Task> { + fn load_with_encoding(&self, _: &App, _: &'static Encoding) -> Task> { unimplemented!() } } diff --git a/crates/encodings/Cargo.toml b/crates/encodings/Cargo.toml index 19456909425d5b..a81e28bf4832e0 100644 --- a/crates/encodings/Cargo.toml +++ b/crates/encodings/Cargo.toml @@ -7,7 +7,7 @@ edition.workspace = true [dependencies] anyhow.workspace = true editor.workspace = true -encoding.workspace = true +encoding_rs.workspace = true fuzzy.workspace = true gpui.workspace = true language.workspace = true diff --git a/crates/encodings/src/lib.rs b/crates/encodings/src/lib.rs index 0336d70b37cf8b..e9414cb0bc99ba 100644 --- a/crates/encodings/src/lib.rs +++ b/crates/encodings/src/lib.rs @@ -1,13 +1,6 @@ ///! A crate for handling file encodings in the text 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, - 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 encoding_rs::Encoding; use gpui::{ClickEvent, Entity, Subscription, WeakEntity}; use settings::Settings; use ui::{Button, ButtonCommon, Context, LabelSize, Render, Tooltip, Window, div}; @@ -18,7 +11,7 @@ use crate::selectors::save_or_reopen::EncodingSaveOrReopenSelector; /// A status bar item that shows the current file encoding and allows changing it. pub struct EncodingIndicator { - pub encoding: Option<&'static dyn Encoding>, + pub encoding: Option<&'static Encoding>, pub workspace: WeakEntity, observe: Option, // Subscription to observe changes in the active editor show: bool, // Whether to show the indicator or not, based on whether an editor is active @@ -37,7 +30,7 @@ impl Render for EncodingIndicator { } status_element.child( - Button::new("encoding", encoding_name(self.encoding.unwrap_or(UTF_8))) + Button::new("encoding", encoding_name(self.encoding.unwrap_or(encoding_rs::UTF_8))) .label_size(LabelSize::Small) .tooltip(Tooltip::text("Select Encoding")) .on_click(cx.listener(|indicator, _: &ClickEvent, window, cx| { @@ -54,7 +47,7 @@ impl Render for EncodingIndicator { impl EncodingIndicator { pub fn new( - encoding: Option<&'static dyn encoding::Encoding>, + encoding: Option<&'static Encoding>, workspace: WeakEntity, observe: Option, ) -> EncodingIndicator { @@ -105,140 +98,117 @@ impl StatusItemView for EncodingIndicator { } /// Get a human-readable name for the given encoding. -pub fn encoding_name(encoding: &'static dyn Encoding) -> String { +pub fn encoding_name(encoding: &'static 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", - _ => "", + match name { + "UTF-8" => "UTF-8", + "windows-1252" => "Windows-1252", + "windows-1251" => "Windows-1251", + "windows-1250" => "Windows-1250", + "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-13" => "ISO 8859-13", + "ISO-8859-15" => "ISO 8859-15", + "KOI8-R" => "KOI8-R", + "KOI8-U" => "KOI8-U", + "macintosh" => "MacRoman", + "x-mac-cyrillic" => "Mac Cyrillic", + "windows-874" => "Windows-874", + "windows-1253" => "Windows-1253", + "windows-1254" => "Windows-1254", + "windows-1255" => "Windows-1255", + "windows-1256" => "Windows-1256", + "windows-1257" => "Windows-1257", + "windows-1258" => "Windows-1258", + "EUC-KR" => "Windows-949", + "EUC-JP" => "EUC-JP", + "ISO-2022-JP" => "ISO 2022-JP", + "GBK" => "GBK", + "gb18030" => "GB18030", + "Big5" => "Big5", + _ => name, } .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 { +pub fn encoding_from_index(index: usize) -> &'static 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, + 0 => encoding_rs::UTF_8, + 1 => encoding_rs::WINDOWS_1252, + 2 => encoding_rs::WINDOWS_1251, + 3 => encoding_rs::WINDOWS_1250, + 4 => encoding_rs::ISO_8859_2, + 5 => encoding_rs::ISO_8859_3, + 6 => encoding_rs::ISO_8859_4, + 7 => encoding_rs::ISO_8859_5, + 8 => encoding_rs::ISO_8859_6, + 9 => encoding_rs::ISO_8859_7, + 10 => encoding_rs::ISO_8859_8, + 11 => encoding_rs::ISO_8859_13, + 12 => encoding_rs::ISO_8859_15, + 13 => encoding_rs::KOI8_R, + 14 => encoding_rs::KOI8_U, + 15 => encoding_rs::MACINTOSH, + 16 => encoding_rs::X_MAC_CYRILLIC, + 17 => encoding_rs::WINDOWS_874, + 18 => encoding_rs::WINDOWS_1253, + 19 => encoding_rs::WINDOWS_1254, + 20 => encoding_rs::WINDOWS_1255, + 21 => encoding_rs::WINDOWS_1256, + 22 => encoding_rs::WINDOWS_1257, + 23 => encoding_rs::WINDOWS_1258, + 24 => encoding_rs::EUC_KR, + 25 => encoding_rs::EUC_JP, + 26 => encoding_rs::ISO_2022_JP, + 27 => encoding_rs::GBK, + 28 => encoding_rs::GB18030, + 29 => encoding_rs::BIG5, + _ => encoding_rs::UTF_8, } } /// Get an encoding from its name. -pub fn encoding_from_name(name: &str) -> &'static dyn Encoding { +pub fn encoding_from_name(name: &str) -> &'static 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 + "UTF-8" => encoding_rs::UTF_8, + "Windows-1252" => encoding_rs::WINDOWS_1252, + "Windows-1251" => encoding_rs::WINDOWS_1251, + "Windows-1250" => encoding_rs::WINDOWS_1250, + "ISO 8859-2" => encoding_rs::ISO_8859_2, + "ISO 8859-3" => encoding_rs::ISO_8859_3, + "ISO 8859-4" => encoding_rs::ISO_8859_4, + "ISO 8859-5" => encoding_rs::ISO_8859_5, + "ISO 8859-6" => encoding_rs::ISO_8859_6, + "ISO 8859-7" => encoding_rs::ISO_8859_7, + "ISO 8859-8" => encoding_rs::ISO_8859_8, + "ISO 8859-13" => encoding_rs::ISO_8859_13, + "ISO 8859-15" => encoding_rs::ISO_8859_15, + "KOI8-R" => encoding_rs::KOI8_R, + "KOI8-U" => encoding_rs::KOI8_U, + "MacRoman" => encoding_rs::MACINTOSH, + "Mac Cyrillic" => encoding_rs::X_MAC_CYRILLIC, + "Windows-874" => encoding_rs::WINDOWS_874, + "Windows-1253" => encoding_rs::WINDOWS_1253, + "Windows-1254" => encoding_rs::WINDOWS_1254, + "Windows-1255" => encoding_rs::WINDOWS_1255, + "Windows-1256" => encoding_rs::WINDOWS_1256, + "Windows-1257" => encoding_rs::WINDOWS_1257, + "Windows-1258" => encoding_rs::WINDOWS_1258, + "Windows-949" => encoding_rs::EUC_KR, + "EUC-JP" => encoding_rs::EUC_JP, + "ISO 2022-JP" => encoding_rs::ISO_2022_JP, + "GBK" => encoding_rs::GBK, + "GB18030" => encoding_rs::GB18030, + "Big5" => encoding_rs::BIG5, + "HZ-GB-2312" => encoding_rs::UTF_8, // encoding_rs doesn't support HZ, fallback to UTF-8 + _ => encoding_rs::UTF_8, // Default to UTF-8 for unknown names } } diff --git a/crates/encodings/src/selectors.rs b/crates/encodings/src/selectors.rs index 46fc939aba1281..daa41499a89412 100644 --- a/crates/encodings/src/selectors.rs +++ b/crates/encodings/src/selectors.rs @@ -320,43 +320,35 @@ pub mod encoding { current_selection: 0, encodings: vec![ StringMatchCandidate::new(0, "UTF-8"), - 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, "Windows-949"), - StringMatchCandidate::new(32, "EUC-JP"), - StringMatchCandidate::new(33, "ISO 2022-JP"), - StringMatchCandidate::new(34, "GBK"), - StringMatchCandidate::new(35, "GB18030"), - StringMatchCandidate::new(36, "Big5"), - StringMatchCandidate::new(37, "HZ-GB-2312"), + StringMatchCandidate::new(1, "Windows-1252"), + StringMatchCandidate::new(2, "Windows-1251"), + StringMatchCandidate::new(3, "Windows-1250"), + StringMatchCandidate::new(4, "ISO 8859-2"), + StringMatchCandidate::new(5, "ISO 8859-3"), + StringMatchCandidate::new(6, "ISO 8859-4"), + StringMatchCandidate::new(7, "ISO 8859-5"), + StringMatchCandidate::new(8, "ISO 8859-6"), + StringMatchCandidate::new(9, "ISO 8859-7"), + StringMatchCandidate::new(10, "ISO 8859-8"), + StringMatchCandidate::new(11, "ISO 8859-13"), + StringMatchCandidate::new(12, "ISO 8859-15"), + StringMatchCandidate::new(13, "KOI8-R"), + StringMatchCandidate::new(14, "KOI8-U"), + StringMatchCandidate::new(15, "MacRoman"), + StringMatchCandidate::new(16, "Mac Cyrillic"), + StringMatchCandidate::new(17, "Windows-874"), + StringMatchCandidate::new(18, "Windows-1253"), + StringMatchCandidate::new(19, "Windows-1254"), + StringMatchCandidate::new(20, "Windows-1255"), + StringMatchCandidate::new(21, "Windows-1256"), + StringMatchCandidate::new(22, "Windows-1257"), + StringMatchCandidate::new(23, "Windows-1258"), + StringMatchCandidate::new(24, "Windows-949"), + StringMatchCandidate::new(25, "EUC-JP"), + StringMatchCandidate::new(26, "ISO 2022-JP"), + StringMatchCandidate::new(27, "GBK"), + StringMatchCandidate::new(28, "GB18030"), + StringMatchCandidate::new(29, "Big5"), ], matches: Vec::new(), selector, @@ -414,7 +406,7 @@ pub mod encoding { &query, true, false, - 38, + 30, &AtomicBool::new(false), executor, ) diff --git a/crates/extension_host/Cargo.toml b/crates/extension_host/Cargo.toml index 6ddd4e7a76ec10..4db3672bbd782e 100644 --- a/crates/extension_host/Cargo.toml +++ b/crates/extension_host/Cargo.toml @@ -23,7 +23,7 @@ async-trait.workspace = true client.workspace = true collections.workspace = true dap.workspace = true -encoding.workspace = true +encoding_rs.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 468d25427dda92..f79d4fd00ea0e4 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -12,7 +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; +use encoding_rs::UTF_8; pub use extension::ExtensionManifest; use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; use extension::{ diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 17f99feb0646db..a9e1028f608d07 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -16,7 +16,7 @@ anyhow.workspace = true async-tar.workspace = true async-trait.workspace = true collections.workspace = true -encoding.workspace = true +encoding_rs.workspace = true futures.workspace = true git.workspace = true gpui.workspace = true diff --git a/crates/fs/src/encodings.rs b/crates/fs/src/encodings.rs index efdd6fc187434c..e846ceff538838 100644 --- a/crates/fs/src/encodings.rs +++ b/crates/fs/src/encodings.rs @@ -1,13 +1,13 @@ -//! Encoding and decoding utilities using the `encoding` crate. +//! Encoding and decoding utilities using the `encoding_rs` crate. use std::fmt::Debug; use anyhow::{Error, Result}; -use encoding::Encoding; +use encoding_rs::Encoding; use serde::{Deserialize, de::Visitor}; -/// A wrapper around `encoding::Encoding` to implement `Send` and `Sync`. +/// A wrapper around `encoding_rs::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 struct EncodingWrapper(&'static Encoding); impl Debug for EncodingWrapper { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -28,14 +28,14 @@ impl<'vi> Visitor<'vi> for EncodingWrapperVisitor { fn visit_str(self, encoding: &str) -> Result { Ok(EncodingWrapper( - encoding::label::encoding_from_whatwg_label(encoding) + Encoding::for_label(encoding.as_bytes()) .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) + Encoding::for_label(encoding.as_bytes()) .ok_or_else(|| serde::de::Error::custom("Invalid Encoding"))?, )) } @@ -66,22 +66,24 @@ impl Clone for EncodingWrapper { } impl EncodingWrapper { - pub fn new(encoding: &'static dyn Encoding) -> EncodingWrapper { + pub fn new(encoding: &'static 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())), - } + let (cow, _encoding_used, _had_errors) = self.0.decode(&input); + // encoding_rs handles invalid bytes by replacing them with replacement characters + // in the output string, so we return the result even if there were errors. + // This preserves the original behavior where files with invalid bytes could still be opened. + Ok(cow.into_owned()) } 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())), - } + let (cow, _encoding_used, _had_errors) = self.0.encode(&input); + // encoding_rs handles unencodable characters by replacing them with + // appropriate substitutes in the output, so we return the result even if there were errors. + // This maintains consistency with the decode behavior. + Ok(cow.into_owned()) } } @@ -94,3 +96,52 @@ pub async fn to_utf8(input: Vec, encoding: EncodingWrapper) -> Result Result> { target.encode(input).await } + +#[cfg(test)] +mod tests { + use super::*; + use gpui::BackgroundExecutor; + + #[gpui::test] + async fn test_decode_with_invalid_bytes(_: BackgroundExecutor) { + // Test that files with invalid bytes can still be decoded + // This is a regression test for the issue where files couldn't be opened + // when they contained invalid bytes for the specified encoding + + // Create some invalid UTF-8 bytes + let invalid_bytes = vec![0xFF, 0xFE, 0x00, 0x48]; // Invalid UTF-8 sequence + + let encoding = EncodingWrapper::new(encoding_rs::UTF_8); + let result = encoding.decode(invalid_bytes).await; + + // The decode should succeed, not fail + assert!(result.is_ok(), "Decode should succeed even with invalid bytes"); + + let decoded = result.unwrap(); + // The result should contain replacement characters for invalid sequences + assert!(!decoded.is_empty(), "Decoded string should not be empty"); + + // Test with Windows-1252 and some bytes that might be invalid + let maybe_invalid_bytes = vec![0x81, 0x8D, 0x8F, 0x90, 0x9D]; // Some potentially problematic bytes + let encoding = EncodingWrapper::new(encoding_rs::WINDOWS_1252); + let result = encoding.decode(maybe_invalid_bytes).await; + + // Should still succeed + assert!(result.is_ok(), "Decode should succeed with Windows-1252 even with potentially invalid bytes"); + } + + #[gpui::test] + async fn test_encode_with_unencodable_chars(_: BackgroundExecutor) { + // Test that strings with unencodable characters can still be encoded + let input = "Hello 世界 🌍".to_string(); // Contains Unicode that may not encode to all formats + + let encoding = EncodingWrapper::new(encoding_rs::WINDOWS_1252); + let result = encoding.encode(input).await; + + // The encode should succeed, not fail + assert!(result.is_ok(), "Encode should succeed even with unencodable characters"); + + let encoded = result.unwrap(); + assert!(!encoded.is_empty(), "Encoded bytes should not be empty"); + } +} diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index dd7f3d1e049bdb..fc010c8a2e04be 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -618,7 +618,7 @@ impl Fs for RealFs { async fn load(&self, path: &Path) -> Result { let path = path.to_path_buf(); - let encoding = EncodingWrapper::new(encoding::all::UTF_8); + let encoding = EncodingWrapper::new(encoding_rs::UTF_8); let text = smol::unblock(async || Ok(encodings::to_utf8(std::fs::read(path)?, encoding).await?)) .await diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 4c22b7f4ff07de..9ab0042edd81f1 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -29,7 +29,7 @@ command_palette_hooks.workspace = true component.workspace = true db.workspace = true editor.workspace = true -encoding.workspace = true +encoding_rs.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 2494177e9d9089..a14524a2caff0b 100644 --- a/crates/git_ui/src/file_diff_view.rs +++ b/crates/git_ui/src/file_diff_view.rs @@ -358,7 +358,7 @@ impl Render for FileDiffView { mod tests { use super::*; use editor::test::editor_test_context::assert_state_with_diff; - use encoding::all::UTF_8; + use encoding_rs::UTF_8; use gpui::TestAppContext; use language::Rope; use project::{FakeFs, Fs, Project}; diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index f90c2680324509..42610ce9c59983 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -32,7 +32,7 @@ clock.workspace = true collections.workspace = true diffy = "0.4.2" ec4rs.workspace = true -encoding.workspace = true +encoding_rs.workspace = true fs.workspace = true futures.workspace = true fuzzy.workspace = true diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 709804184dcc18..5db1cc8e9d90a5 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -21,7 +21,7 @@ use anyhow::{Context as _, Result}; use clock::Lamport; pub use clock::ReplicaId; use collections::HashMap; -use encoding::Encoding; +use encoding_rs::Encoding; use fs::MTime; use futures::channel::oneshot; use gpui::{ @@ -127,7 +127,7 @@ pub struct Buffer { has_unsaved_edits: Cell<(clock::Global, bool)>, change_bits: Vec>>, _subscriptions: Vec, - pub encoding: &'static dyn encoding::Encoding, + pub encoding: &'static Encoding, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -420,7 +420,7 @@ pub trait LocalFile: File { 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) + fn load_with_encoding(&self, cx: &App, encoding: &'static Encoding) -> Task>; } @@ -1012,7 +1012,7 @@ impl Buffer { has_conflict: false, change_bits: Default::default(), _subscriptions: Vec::new(), - encoding: encoding::all::UTF_8, + encoding: encoding_rs::UTF_8, } } @@ -5238,7 +5238,7 @@ impl LocalFile for TestFile { unimplemented!() } - fn load_with_encoding(&self, _: &App, _: &'static dyn Encoding) -> Task> { + fn load_with_encoding(&self, _: &App, _: &'static Encoding) -> Task> { unimplemented!() } } diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index d598ee7685c995..d000a036ec7617 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -39,7 +39,7 @@ clock.workspace = true collections.workspace = true context_server.workspace = true dap.workspace = true -encoding.workspace = true +encoding_rs.workspace = true extension.workspace = true fancy-regex.workspace = true fs.workspace = true diff --git a/crates/project/src/prettier_store.rs b/crates/project/src/prettier_store.rs index 50b88e7e850a51..ccdfb9661536a4 100644 --- a/crates/project/src/prettier_store.rs +++ b/crates/project/src/prettier_store.rs @@ -7,7 +7,7 @@ use std::{ use anyhow::{Context as _, Result, anyhow}; use collections::{HashMap, HashSet}; -use encoding::all::UTF_8; +use encoding_rs::UTF_8; use fs::{Fs, encodings::EncodingWrapper}; use futures::{ FutureExt, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 281bfb3b2410ae..ce70464eae1d38 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -12,7 +12,7 @@ use buffer_diff::{ BufferDiffEvent, CALCULATE_DIFF_TASK, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind, assert_hunks, }; -use encoding::all::UTF_8; +use encoding_rs::UTF_8; use fs::{FakeFs, encodings::EncodingWrapper}; use futures::{StreamExt, future}; use git::{ diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 8e9869542125e5..9acb391e9f357d 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -28,7 +28,7 @@ clap.workspace = true client.workspace = true dap_adapters.workspace = true debug_adapter_extension.workspace = true -encoding.workspace = true +encoding_rs.workspace = true env_logger.workspace = true extension.workspace = true extension_host.workspace = true diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 163e068d1363a8..14ee5692d4b4eb 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -35,7 +35,7 @@ clock.workspace = true collections.workspace = true component.workspace = true db.workspace = true -encoding.workspace = true +encoding_rs.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 086e621ac7c5e3..f561e7f30a15f7 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -19,7 +19,7 @@ mod workspace_settings; pub use crate::notifications::NotificationFrame; pub use dock::Panel; -use encoding::all::UTF_8; +use encoding_rs::UTF_8; use fs::encodings::EncodingWrapper; pub use path_list::PathList; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; diff --git a/crates/worktree/Cargo.toml b/crates/worktree/Cargo.toml index 0dc81b73fd32ad..8166825a1c9775 100644 --- a/crates/worktree/Cargo.toml +++ b/crates/worktree/Cargo.toml @@ -27,7 +27,7 @@ anyhow.workspace = true async-lock.workspace = true clock.workspace = true collections.workspace = true -encoding.workspace = true +encoding_rs.workspace = true fs.workspace = true futures.workspace = true fuzzy.workspace = true diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 51842bc89d2f7b..42b086adcbf589 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -7,7 +7,7 @@ use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{Context as _, Result, anyhow}; use clock::ReplicaId; use collections::{HashMap, HashSet, VecDeque}; -use encoding::Encoding; +use encoding_rs::Encoding; use fs::{ Fs, MTime, PathEvent, RemoveOptions, Watcher, copy_recursive, encodings::EncodingWrapper, read_dir_items, @@ -735,7 +735,7 @@ impl Worktree { text: Rope, line_ending: LineEnding, cx: &Context, - encoding: &'static dyn Encoding, + encoding: &'static Encoding, ) -> Task>> { match self { Worktree::Local(this) => this.write_file(path, text, line_ending, cx, encoding), @@ -1451,7 +1451,7 @@ impl LocalWorktree { text: Rope, line_ending: LineEnding, cx: &Context, - encoding: &'static dyn Encoding, + encoding: &'static Encoding, ) -> Task>> { let fs = self.fs.clone(); let is_private = self.is_path_private(&path); @@ -3132,7 +3132,7 @@ impl language::LocalFile for File { fn load_with_encoding( &self, cx: &App, - encoding: &'static dyn Encoding, + encoding: &'static Encoding, ) -> Task> { let worktree = self.worktree.read(cx).as_local().unwrap(); let path = worktree.absolutize(&self.path); diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index c6e6aeac87d619..f69adc540452f4 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -3,7 +3,7 @@ use crate::{ worktree_settings::WorktreeSettings, }; use anyhow::Result; -use encoding::all::UTF_8; +use encoding_rs::UTF_8; use fs::{FakeFs, Fs, RealFs, RemoveOptions, encodings::EncodingWrapper}; use git::GITIGNORE; use gpui::{AppContext as _, BackgroundExecutor, BorrowAppContext, Context, Task, TestAppContext}; diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 607fb302970fb1..60da5c5494cedd 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2176,7 +2176,7 @@ mod tests { use assets::Assets; use collections::HashSet; use editor::{DisplayPoint, Editor, SelectionEffects, display_map::DisplayRow}; - use encoding::all::UTF_8; + use encoding_rs::UTF_8; use fs::encodings::EncodingWrapper; use gpui::{ Action, AnyWindowHandle, App, AssetSource, BorrowAppContext, SemanticVersion, From 93281b2fc97abffed3f2553b11ba9678736150fc Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sun, 31 Aug 2025 23:11:49 +0530 Subject: [PATCH 11/43] Fix cargo clippy errors --- crates/encodings/src/lib.rs | 33 +++++++++++++------------ crates/encodings/src/selectors.rs | 6 ++--- crates/fs/src/encodings.rs | 41 +++++++++++++++++++------------ 3 files changed, 44 insertions(+), 36 deletions(-) diff --git a/crates/encodings/src/lib.rs b/crates/encodings/src/lib.rs index e9414cb0bc99ba..cda2fdcb4cf46a 100644 --- a/crates/encodings/src/lib.rs +++ b/crates/encodings/src/lib.rs @@ -1,4 +1,4 @@ -///! A crate for handling file encodings in the text editor. +//! A crate for handling file encodings in the text editor. use editor::{Editor, EditorSettings}; use encoding_rs::Encoding; use gpui::{ClickEvent, Entity, Subscription, WeakEntity}; @@ -23,24 +23,25 @@ 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) - { + if (!EditorSettings::get_global(cx).status_bar.encoding_indicator) || !self.show { return status_element; } status_element.child( - Button::new("encoding", encoding_name(self.encoding.unwrap_or(encoding_rs::UTF_8))) - .label_size(LabelSize::Small) - .tooltip(Tooltip::text("Select Encoding")) - .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) - }) - } else { - } - })), + Button::new( + "encoding", + encoding_name(self.encoding.unwrap_or(encoding_rs::UTF_8)), + ) + .label_size(LabelSize::Small) + .tooltip(Tooltip::text("Select Encoding")) + .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) + }) + } else { + } + })), ) } } @@ -209,6 +210,6 @@ pub fn encoding_from_name(name: &str) -> &'static Encoding { "GB18030" => encoding_rs::GB18030, "Big5" => encoding_rs::BIG5, "HZ-GB-2312" => encoding_rs::UTF_8, // encoding_rs doesn't support HZ, fallback to UTF-8 - _ => encoding_rs::UTF_8, // Default to UTF-8 for unknown names + _ => encoding_rs::UTF_8, // Default to UTF-8 for unknown names } } diff --git a/crates/encodings/src/selectors.rs b/crates/encodings/src/selectors.rs index daa41499a89412..30e5dd1cd52bed 100644 --- a/crates/encodings/src/selectors.rs +++ b/crates/encodings/src/selectors.rs @@ -31,8 +31,7 @@ pub mod save_or_reopen { cx: &mut Context, workspace: WeakEntity, ) -> Self { - let delegate = - EncodingSaveOrReopenDelegate::new(cx.entity().downgrade(), workspace.clone()); + let delegate = EncodingSaveOrReopenDelegate::new(cx.entity().downgrade(), workspace); let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); @@ -498,8 +497,7 @@ pub mod encoding { buffer: WeakEntity, workspace: WeakEntity, ) -> EncodingSelector { - let delegate = - EncodingSelectorDelegate::new(cx.entity().downgrade(), buffer, action.clone()); + let delegate = EncodingSelectorDelegate::new(cx.entity().downgrade(), buffer, action); let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); EncodingSelector { picker, workspace } diff --git a/crates/fs/src/encodings.rs b/crates/fs/src/encodings.rs index e846ceff538838..d9a5a93828a0ec 100644 --- a/crates/fs/src/encodings.rs +++ b/crates/fs/src/encodings.rs @@ -1,7 +1,7 @@ //! Encoding and decoding utilities using the `encoding_rs` crate. use std::fmt::Debug; -use anyhow::{Error, Result}; +use anyhow::Result; use encoding_rs::Encoding; use serde::{Deserialize, de::Visitor}; @@ -80,7 +80,7 @@ impl EncodingWrapper { pub async fn encode(&self, input: String) -> Result> { let (cow, _encoding_used, _had_errors) = self.0.encode(&input); - // encoding_rs handles unencodable characters by replacing them with + // encoding_rs handles unencodable characters by replacing them with // appropriate substitutes in the output, so we return the result even if there were errors. // This maintains consistency with the decode behavior. Ok(cow.into_owned()) @@ -101,46 +101,55 @@ pub async fn from_utf8(input: String, target: EncodingWrapper) -> Result mod tests { use super::*; use gpui::BackgroundExecutor; - + #[gpui::test] async fn test_decode_with_invalid_bytes(_: BackgroundExecutor) { // Test that files with invalid bytes can still be decoded // This is a regression test for the issue where files couldn't be opened // when they contained invalid bytes for the specified encoding - + // Create some invalid UTF-8 bytes let invalid_bytes = vec![0xFF, 0xFE, 0x00, 0x48]; // Invalid UTF-8 sequence - + let encoding = EncodingWrapper::new(encoding_rs::UTF_8); let result = encoding.decode(invalid_bytes).await; - + // The decode should succeed, not fail - assert!(result.is_ok(), "Decode should succeed even with invalid bytes"); - + assert!( + result.is_ok(), + "Decode should succeed even with invalid bytes" + ); + let decoded = result.unwrap(); // The result should contain replacement characters for invalid sequences assert!(!decoded.is_empty(), "Decoded string should not be empty"); - + // Test with Windows-1252 and some bytes that might be invalid let maybe_invalid_bytes = vec![0x81, 0x8D, 0x8F, 0x90, 0x9D]; // Some potentially problematic bytes let encoding = EncodingWrapper::new(encoding_rs::WINDOWS_1252); let result = encoding.decode(maybe_invalid_bytes).await; - + // Should still succeed - assert!(result.is_ok(), "Decode should succeed with Windows-1252 even with potentially invalid bytes"); + assert!( + result.is_ok(), + "Decode should succeed with Windows-1252 even with potentially invalid bytes" + ); } - + #[gpui::test] async fn test_encode_with_unencodable_chars(_: BackgroundExecutor) { // Test that strings with unencodable characters can still be encoded let input = "Hello 世界 🌍".to_string(); // Contains Unicode that may not encode to all formats - + let encoding = EncodingWrapper::new(encoding_rs::WINDOWS_1252); let result = encoding.encode(input).await; - + // The encode should succeed, not fail - assert!(result.is_ok(), "Encode should succeed even with unencodable characters"); - + assert!( + result.is_ok(), + "Encode should succeed even with unencodable characters" + ); + let encoded = result.unwrap(); assert!(!encoded.is_empty(), "Encoded bytes should not be empty"); } From 3b77f57d461c63322b01b0fcc9194e66990a249b Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Mon, 1 Sep 2025 20:11:01 +0530 Subject: [PATCH 12/43] - Format code properly - Remove unused dependencies --- crates/encodings/Cargo.toml | 1 - crates/language/src/buffer.rs | 3 +-- crates/worktree/src/worktree.rs | 6 +----- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/encodings/Cargo.toml b/crates/encodings/Cargo.toml index a81e28bf4832e0..b7743d438d8cc2 100644 --- a/crates/encodings/Cargo.toml +++ b/crates/encodings/Cargo.toml @@ -5,7 +5,6 @@ publish.workspace = true edition.workspace = true [dependencies] -anyhow.workspace = true editor.workspace = true encoding_rs.workspace = true fuzzy.workspace = true diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 5db1cc8e9d90a5..e22f86be5be371 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -420,8 +420,7 @@ pub trait LocalFile: File { 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 Encoding) - -> Task>; + fn load_with_encoding(&self, cx: &App, encoding: &'static Encoding) -> Task>; } /// The auto-indent behavior associated with an editing operation. diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 42b086adcbf589..a20f4035ec63cc 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3129,11 +3129,7 @@ impl language::LocalFile for File { cx.background_spawn(async move { fs.load_bytes(&abs_path).await }) } - fn load_with_encoding( - &self, - cx: &App, - encoding: &'static Encoding, - ) -> Task> { + fn load_with_encoding(&self, cx: &App, encoding: &'static Encoding) -> Task> { let worktree = self.worktree.read(cx).as_local().unwrap(); let path = worktree.absolutize(&self.path); let fs = worktree.fs.clone(); From 009c4ea319724640e1f3dd9b2f9397b7445b59ff Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Wed, 3 Sep 2025 21:41:52 +0530 Subject: [PATCH 13/43] Fix issues caused by Cargo.lock --- Cargo.lock | 2 -- 1 file changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d2ef2c789a5398..673dc0317985ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5527,7 +5527,6 @@ dependencies = [ name = "encodings" version = "0.1.0" dependencies = [ - "anyhow", "editor", "encoding_rs", "fuzzy", @@ -6432,7 +6431,6 @@ dependencies = [ "paths", "proto", "rope", - "schemars", "serde", "serde_json", "smol", From 81376ddf058f53726394242f435a59699b7501fe Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Thu, 4 Sep 2025 07:10:58 +0530 Subject: [PATCH 14/43] Add a licence symlink --- crates/encodings/LICENSE-GPL | 1 + 1 file changed, 1 insertion(+) create mode 120000 crates/encodings/LICENSE-GPL diff --git a/crates/encodings/LICENSE-GPL b/crates/encodings/LICENSE-GPL new file mode 120000 index 00000000000000..89e542f750cd38 --- /dev/null +++ b/crates/encodings/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file From 0e2eda0a23ee3f19898b474e1d44bab84dd2798a Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Fri, 5 Sep 2025 14:45:14 +0530 Subject: [PATCH 15/43] Add UTF-16 LE/BE support and remove unused serde code - Add UTF-16 LE and UTF-16 BE to encoding lists and mappings - Implement encode/decode for UTF-16LE/BE in `EncodingWrapper` - Remove unused serde deserialisation code and tests --- crates/encodings/src/lib.rs | 64 +++++++------- crates/encodings/src/selectors.rs | 64 +++++++------- crates/fs/src/encodings.rs | 133 ++++++++---------------------- 3 files changed, 100 insertions(+), 161 deletions(-) diff --git a/crates/encodings/src/lib.rs b/crates/encodings/src/lib.rs index cda2fdcb4cf46a..c7072e7552368e 100644 --- a/crates/encodings/src/lib.rs +++ b/crates/encodings/src/lib.rs @@ -104,6 +104,8 @@ pub fn encoding_name(encoding: &'static Encoding) -> String { match name { "UTF-8" => "UTF-8", + "UTF-16LE" => "UTF-16 LE", + "UTF-16BE" => "UTF-16 BE", "windows-1252" => "Windows-1252", "windows-1251" => "Windows-1251", "windows-1250" => "Windows-1250", @@ -143,35 +145,37 @@ pub fn encoding_name(encoding: &'static Encoding) -> String { pub fn encoding_from_index(index: usize) -> &'static Encoding { match index { 0 => encoding_rs::UTF_8, - 1 => encoding_rs::WINDOWS_1252, - 2 => encoding_rs::WINDOWS_1251, - 3 => encoding_rs::WINDOWS_1250, - 4 => encoding_rs::ISO_8859_2, - 5 => encoding_rs::ISO_8859_3, - 6 => encoding_rs::ISO_8859_4, - 7 => encoding_rs::ISO_8859_5, - 8 => encoding_rs::ISO_8859_6, - 9 => encoding_rs::ISO_8859_7, - 10 => encoding_rs::ISO_8859_8, - 11 => encoding_rs::ISO_8859_13, - 12 => encoding_rs::ISO_8859_15, - 13 => encoding_rs::KOI8_R, - 14 => encoding_rs::KOI8_U, - 15 => encoding_rs::MACINTOSH, - 16 => encoding_rs::X_MAC_CYRILLIC, - 17 => encoding_rs::WINDOWS_874, - 18 => encoding_rs::WINDOWS_1253, - 19 => encoding_rs::WINDOWS_1254, - 20 => encoding_rs::WINDOWS_1255, - 21 => encoding_rs::WINDOWS_1256, - 22 => encoding_rs::WINDOWS_1257, - 23 => encoding_rs::WINDOWS_1258, - 24 => encoding_rs::EUC_KR, - 25 => encoding_rs::EUC_JP, - 26 => encoding_rs::ISO_2022_JP, - 27 => encoding_rs::GBK, - 28 => encoding_rs::GB18030, - 29 => encoding_rs::BIG5, + 1 => encoding_rs::UTF_16LE, + 2 => encoding_rs::UTF_16BE, + 3 => encoding_rs::WINDOWS_1252, + 4 => encoding_rs::WINDOWS_1251, + 5 => encoding_rs::WINDOWS_1250, + 6 => encoding_rs::ISO_8859_2, + 7 => encoding_rs::ISO_8859_3, + 8 => encoding_rs::ISO_8859_4, + 9 => encoding_rs::ISO_8859_5, + 10 => encoding_rs::ISO_8859_6, + 11 => encoding_rs::ISO_8859_7, + 12 => encoding_rs::ISO_8859_8, + 13 => encoding_rs::ISO_8859_13, + 14 => encoding_rs::ISO_8859_15, + 15 => encoding_rs::KOI8_R, + 16 => encoding_rs::KOI8_U, + 17 => encoding_rs::MACINTOSH, + 18 => encoding_rs::X_MAC_CYRILLIC, + 19 => encoding_rs::WINDOWS_874, + 20 => encoding_rs::WINDOWS_1253, + 21 => encoding_rs::WINDOWS_1254, + 22 => encoding_rs::WINDOWS_1255, + 23 => encoding_rs::WINDOWS_1256, + 24 => encoding_rs::WINDOWS_1257, + 25 => encoding_rs::WINDOWS_1258, + 26 => encoding_rs::EUC_KR, + 27 => encoding_rs::EUC_JP, + 28 => encoding_rs::ISO_2022_JP, + 29 => encoding_rs::GBK, + 30 => encoding_rs::GB18030, + 31 => encoding_rs::BIG5, _ => encoding_rs::UTF_8, } } @@ -180,6 +184,8 @@ pub fn encoding_from_index(index: usize) -> &'static Encoding { pub fn encoding_from_name(name: &str) -> &'static Encoding { match name { "UTF-8" => encoding_rs::UTF_8, + "UTF-16 LE" => encoding_rs::UTF_16LE, + "UTF-16 BE" => encoding_rs::UTF_16BE, "Windows-1252" => encoding_rs::WINDOWS_1252, "Windows-1251" => encoding_rs::WINDOWS_1251, "Windows-1250" => encoding_rs::WINDOWS_1250, diff --git a/crates/encodings/src/selectors.rs b/crates/encodings/src/selectors.rs index 30e5dd1cd52bed..75c1045059dcd2 100644 --- a/crates/encodings/src/selectors.rs +++ b/crates/encodings/src/selectors.rs @@ -271,10 +271,6 @@ pub mod save_or_reopen { ) } } - - pub fn get_current_encoding() -> &'static str { - "UTF-8" - } } /// This module contains the encoding selector for choosing an encoding to save or reopen a file with. @@ -319,35 +315,37 @@ pub mod encoding { current_selection: 0, encodings: vec![ StringMatchCandidate::new(0, "UTF-8"), - StringMatchCandidate::new(1, "Windows-1252"), - StringMatchCandidate::new(2, "Windows-1251"), - StringMatchCandidate::new(3, "Windows-1250"), - StringMatchCandidate::new(4, "ISO 8859-2"), - StringMatchCandidate::new(5, "ISO 8859-3"), - StringMatchCandidate::new(6, "ISO 8859-4"), - StringMatchCandidate::new(7, "ISO 8859-5"), - StringMatchCandidate::new(8, "ISO 8859-6"), - StringMatchCandidate::new(9, "ISO 8859-7"), - StringMatchCandidate::new(10, "ISO 8859-8"), - StringMatchCandidate::new(11, "ISO 8859-13"), - StringMatchCandidate::new(12, "ISO 8859-15"), - StringMatchCandidate::new(13, "KOI8-R"), - StringMatchCandidate::new(14, "KOI8-U"), - StringMatchCandidate::new(15, "MacRoman"), - StringMatchCandidate::new(16, "Mac Cyrillic"), - StringMatchCandidate::new(17, "Windows-874"), - StringMatchCandidate::new(18, "Windows-1253"), - StringMatchCandidate::new(19, "Windows-1254"), - StringMatchCandidate::new(20, "Windows-1255"), - StringMatchCandidate::new(21, "Windows-1256"), - StringMatchCandidate::new(22, "Windows-1257"), - StringMatchCandidate::new(23, "Windows-1258"), - StringMatchCandidate::new(24, "Windows-949"), - StringMatchCandidate::new(25, "EUC-JP"), - StringMatchCandidate::new(26, "ISO 2022-JP"), - StringMatchCandidate::new(27, "GBK"), - StringMatchCandidate::new(28, "GB18030"), - StringMatchCandidate::new(29, "Big5"), + StringMatchCandidate::new(1, "UTF-16 LE"), + StringMatchCandidate::new(2, "UTF-16 BE"), + StringMatchCandidate::new(3, "Windows-1252"), + StringMatchCandidate::new(4, "Windows-1251"), + StringMatchCandidate::new(5, "Windows-1250"), + StringMatchCandidate::new(6, "ISO 8859-2"), + StringMatchCandidate::new(7, "ISO 8859-3"), + StringMatchCandidate::new(8, "ISO 8859-4"), + StringMatchCandidate::new(9, "ISO 8859-5"), + StringMatchCandidate::new(10, "ISO 8859-6"), + StringMatchCandidate::new(11, "ISO 8859-7"), + StringMatchCandidate::new(12, "ISO 8859-8"), + StringMatchCandidate::new(13, "ISO 8859-13"), + StringMatchCandidate::new(14, "ISO 8859-15"), + StringMatchCandidate::new(15, "KOI8-R"), + StringMatchCandidate::new(16, "KOI8-U"), + StringMatchCandidate::new(17, "MacRoman"), + StringMatchCandidate::new(18, "Mac Cyrillic"), + StringMatchCandidate::new(19, "Windows-874"), + StringMatchCandidate::new(20, "Windows-1253"), + StringMatchCandidate::new(21, "Windows-1254"), + StringMatchCandidate::new(22, "Windows-1255"), + StringMatchCandidate::new(23, "Windows-1256"), + StringMatchCandidate::new(24, "Windows-1257"), + StringMatchCandidate::new(25, "Windows-1258"), + StringMatchCandidate::new(26, "Windows-949"), + StringMatchCandidate::new(27, "EUC-JP"), + StringMatchCandidate::new(28, "ISO 2022-JP"), + StringMatchCandidate::new(29, "GBK"), + StringMatchCandidate::new(30, "GB18030"), + StringMatchCandidate::new(31, "Big5"), ], matches: Vec::new(), selector, diff --git a/crates/fs/src/encodings.rs b/crates/fs/src/encodings.rs index d9a5a93828a0ec..0a37c947c3e1e0 100644 --- a/crates/fs/src/encodings.rs +++ b/crates/fs/src/encodings.rs @@ -3,7 +3,6 @@ use std::fmt::Debug; use anyhow::Result; use encoding_rs::Encoding; -use serde::{Deserialize, de::Visitor}; /// A wrapper around `encoding_rs::Encoding` to implement `Send` and `Sync`. /// Since the reference is static, it is safe to send it across threads. @@ -11,7 +10,7 @@ pub struct EncodingWrapper(&'static Encoding); impl Debug for EncodingWrapper { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("EncodingWrapper") + f.debug_tuple(&format!("EncodingWrapper{:?}", self.0)) .field(&self.0.name()) .finish() } @@ -19,37 +18,6 @@ impl Debug for EncodingWrapper { 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::for_label(encoding.as_bytes()) - .ok_or_else(|| serde::de::Error::custom("Invalid Encoding"))?, - )) - } - - fn visit_string(self, encoding: String) -> Result { - Ok(EncodingWrapper( - Encoding::for_label(encoding.as_bytes()) - .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() @@ -71,19 +39,44 @@ impl EncodingWrapper { } pub async fn decode(&self, input: Vec) -> Result { - let (cow, _encoding_used, _had_errors) = self.0.decode(&input); - // encoding_rs handles invalid bytes by replacing them with replacement characters + let (cow, _had_errors) = self.0.decode_with_bom_removal(&input); + // `encoding_rs` handles invalid bytes by replacing them with replacement characters // in the output string, so we return the result even if there were errors. - // This preserves the original behavior where files with invalid bytes could still be opened. + // This preserves the original behaviour where files with invalid bytes could still be opened. Ok(cow.into_owned()) } pub async fn encode(&self, input: String) -> Result> { - let (cow, _encoding_used, _had_errors) = self.0.encode(&input); - // encoding_rs handles unencodable characters by replacing them with - // appropriate substitutes in the output, so we return the result even if there were errors. - // This maintains consistency with the decode behavior. - Ok(cow.into_owned()) + if self.0 == encoding_rs::UTF_16BE { + let mut data: Vec = vec![]; + let utf = input.encode_utf16().collect::>(); + + for i in utf { + let byte = i.to_be_bytes(); + for b in byte { + data.push(b); + } + } + return Ok(data); + } else if self.0 == encoding_rs::UTF_16LE { + let mut data: Vec = vec![]; + let utf = input.encode_utf16().collect::>(); + + for i in utf { + let byte = i.to_le_bytes(); + for b in byte { + data.push(b); + } + } + return Ok(data); + } else { + let (cow, _encoding_used, _had_errors) = self.0.encode(&input); + println!("Encoding: {:?}", self); + // `encoding_rs` handles unencodable characters by replacing them with + // appropriate substitutes in the output, so we return the result even if there were errors. + // This maintains consistency with the decode behaviour. + Ok(cow.into_owned()) + } } } @@ -96,61 +89,3 @@ pub async fn to_utf8(input: Vec, encoding: EncodingWrapper) -> Result Result> { target.encode(input).await } - -#[cfg(test)] -mod tests { - use super::*; - use gpui::BackgroundExecutor; - - #[gpui::test] - async fn test_decode_with_invalid_bytes(_: BackgroundExecutor) { - // Test that files with invalid bytes can still be decoded - // This is a regression test for the issue where files couldn't be opened - // when they contained invalid bytes for the specified encoding - - // Create some invalid UTF-8 bytes - let invalid_bytes = vec![0xFF, 0xFE, 0x00, 0x48]; // Invalid UTF-8 sequence - - let encoding = EncodingWrapper::new(encoding_rs::UTF_8); - let result = encoding.decode(invalid_bytes).await; - - // The decode should succeed, not fail - assert!( - result.is_ok(), - "Decode should succeed even with invalid bytes" - ); - - let decoded = result.unwrap(); - // The result should contain replacement characters for invalid sequences - assert!(!decoded.is_empty(), "Decoded string should not be empty"); - - // Test with Windows-1252 and some bytes that might be invalid - let maybe_invalid_bytes = vec![0x81, 0x8D, 0x8F, 0x90, 0x9D]; // Some potentially problematic bytes - let encoding = EncodingWrapper::new(encoding_rs::WINDOWS_1252); - let result = encoding.decode(maybe_invalid_bytes).await; - - // Should still succeed - assert!( - result.is_ok(), - "Decode should succeed with Windows-1252 even with potentially invalid bytes" - ); - } - - #[gpui::test] - async fn test_encode_with_unencodable_chars(_: BackgroundExecutor) { - // Test that strings with unencodable characters can still be encoded - let input = "Hello 世界 🌍".to_string(); // Contains Unicode that may not encode to all formats - - let encoding = EncodingWrapper::new(encoding_rs::WINDOWS_1252); - let result = encoding.encode(input).await; - - // The encode should succeed, not fail - assert!( - result.is_ok(), - "Encode should succeed even with unencodable characters" - ); - - let encoded = result.unwrap(); - assert!(!encoded.is_empty(), "Encoded bytes should not be empty"); - } -} From 27376e051e6390fec6c9d9721fbab1aa33cd2a02 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Fri, 5 Sep 2025 18:11:15 +0530 Subject: [PATCH 16/43] Write BOM once for UTF-16 files --- crates/fs/src/encodings.rs | 40 ++++++++++++++++++++------------------ crates/fs/src/fs.rs | 15 +++++++++++++- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/crates/fs/src/encodings.rs b/crates/fs/src/encodings.rs index 0a37c947c3e1e0..db75dcadad8078 100644 --- a/crates/fs/src/encodings.rs +++ b/crates/fs/src/encodings.rs @@ -38,8 +38,13 @@ impl EncodingWrapper { EncodingWrapper(encoding) } + pub fn get_encoding(&self) -> &'static Encoding { + self.0 + } + pub async fn decode(&self, input: Vec) -> Result { let (cow, _had_errors) = self.0.decode_with_bom_removal(&input); + // `encoding_rs` handles invalid bytes by replacing them with replacement characters // in the output string, so we return the result even if there were errors. // This preserves the original behaviour where files with invalid bytes could still be opened. @@ -48,30 +53,27 @@ impl EncodingWrapper { pub async fn encode(&self, input: String) -> Result> { if self.0 == encoding_rs::UTF_16BE { - let mut data: Vec = vec![]; - let utf = input.encode_utf16().collect::>(); - - for i in utf { - let byte = i.to_be_bytes(); - for b in byte { - data.push(b); - } - } + let mut data = Vec::::new(); + data.reserve(input.len() * 2); // Reserve space for UTF-16BE bytes + + // Convert the input string to UTF-16BE bytes + let utf16be_bytes: Vec = + input.encode_utf16().flat_map(|u| u.to_be_bytes()).collect(); + + data.extend(utf16be_bytes); return Ok(data); } else if self.0 == encoding_rs::UTF_16LE { - let mut data: Vec = vec![]; - let utf = input.encode_utf16().collect::>(); - - for i in utf { - let byte = i.to_le_bytes(); - for b in byte { - data.push(b); - } - } + let mut data = Vec::::new(); + data.reserve(input.len() * 2); // Reserve space for UTF-16LE bytes + + // Convert the input string to UTF-16LE bytes + let utf16le_bytes: Vec = + input.encode_utf16().flat_map(|u| u.to_le_bytes()).collect(); + + data.extend(utf16le_bytes); return Ok(data); } else { let (cow, _encoding_used, _had_errors) = self.0.encode(&input); - println!("Encoding: {:?}", self); // `encoding_rs` handles unencodable characters by replacing them with // appropriate substitutes in the output, so we return the result even if there were errors. // This maintains consistency with the decode behaviour. diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index fc010c8a2e04be..0857b049d07235 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -694,11 +694,24 @@ impl Fs for RealFs { } let file = smol::fs::File::create(path).await?; let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file); + + // BOM for UTF-16 is written at the start of the file here because + // if BOM is written in the `encode` function of `fs::encodings`, it would be written + // for every chunk, resulting in multiple BOMs in the file. + if encoding.get_encoding() == encoding_rs::UTF_16BE { + // Write BOM for UTF-16BE + writer.write_all(&[0xFE, 0xFF]).await?; + } else if encoding.get_encoding() == encoding_rs::UTF_16LE { + // Write BOM for UTF-16LE + writer.write_all(&[0xFF, 0xFE]).await?; + } + for chunk in chunks(text, line_ending) { writer .write_all(&from_utf8(chunk.to_string(), encoding.clone()).await?) - .await?; + .await? } + writer.flush().await?; Ok(()) } From 680a72b397c9691ecd0c35c265db22903d1c79ae Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sat, 6 Sep 2025 14:41:00 +0530 Subject: [PATCH 17/43] - Update encoding detection to automatically switch to UTF-16LE/BE if BOM is present - Change `encoding` in `Buffer` to `Arc>` - Observe changes in the `encoding` field of `Buffer` and update the status bar indicator --- crates/copilot/src/copilot.rs | 8 ++++- crates/encodings/src/lib.rs | 49 ++++++++++++++++++++++++------ crates/encodings/src/selectors.rs | 7 +++-- crates/fs/src/encodings.rs | 49 +++++++++++++++++++++++++----- crates/fs/src/fs.rs | 24 ++++++++++++--- crates/git_ui/src/git_panel.rs | 2 +- crates/language/src/buffer.rs | 36 ++++++++++++++++------ crates/project/src/buffer_store.rs | 37 +++++++++++----------- crates/worktree/src/worktree.rs | 13 ++++++-- crates/zed/src/zed.rs | 5 +-- 10 files changed, 170 insertions(+), 60 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index cf136962b9c004..f72e1f392d0a7a 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1460,7 +1460,13 @@ mod tests { unimplemented!() } - fn load_with_encoding(&self, _: &App, _: &'static Encoding) -> Task> { + fn load_with_encoding( + &self, + _: &App, + _: &'static Encoding, + _: bool, + _: Arc>, + ) -> Task> { unimplemented!() } } diff --git a/crates/encodings/src/lib.rs b/crates/encodings/src/lib.rs index c7072e7552368e..318f4660eeb854 100644 --- a/crates/encodings/src/lib.rs +++ b/crates/encodings/src/lib.rs @@ -1,7 +1,9 @@ //! A crate for handling file encodings in the text editor. + use editor::{Editor, EditorSettings}; use encoding_rs::Encoding; use gpui::{ClickEvent, Entity, Subscription, WeakEntity}; +use language::Buffer; use settings::Settings; use ui::{Button, ButtonCommon, Context, LabelSize, Render, Tooltip, Window, div}; use ui::{Clickable, ParentElement}; @@ -13,7 +15,13 @@ use crate::selectors::save_or_reopen::EncodingSaveOrReopenSelector; pub struct EncodingIndicator { pub encoding: Option<&'static Encoding>, pub workspace: WeakEntity, - observe: Option, // Subscription to observe changes in the active editor + + /// Subscription to observe changes in the active editor + observe_editor: Option, + + /// Subscription to observe changes in the `encoding` field of the `Buffer` struct + observe_buffer_encoding: Option, + show: bool, // Whether to show the indicator or not, based on whether an editor is active } @@ -50,17 +58,20 @@ impl EncodingIndicator { pub fn new( encoding: Option<&'static Encoding>, workspace: WeakEntity, - observe: Option, + observe_editor: Option, + observe_buffer_encoding: Option, ) -> EncodingIndicator { EncodingIndicator { encoding, workspace, - observe, + observe_editor, show: true, + observe_buffer_encoding, } } - pub fn update( + /// Update the encoding when the active editor is switched. + pub fn update_when_editor_is_switched( &mut self, editor: Entity, _: &mut Window, @@ -68,12 +79,24 @@ impl EncodingIndicator { ) { let editor = editor.read(cx); if let Some((_, buffer, _)) = editor.active_excerpt(cx) { - let encoding = buffer.read(cx).encoding; - self.encoding = Some(encoding); + let encoding = buffer.read(cx).encoding.clone(); + self.encoding = Some(&*encoding.lock().unwrap()); } cx.notify(); } + + /// Update the encoding when the `encoding` field of the `Buffer` struct changes. + pub fn update_when_buffer_encoding_changes( + &mut self, + buffer: Entity, + _: &mut Window, + cx: &mut Context, + ) { + let encoding = buffer.read(cx).encoding.clone(); + self.encoding = Some(&*encoding.lock().unwrap()); + cx.notify(); + } } impl StatusItemView for EncodingIndicator { @@ -85,13 +108,21 @@ impl StatusItemView for EncodingIndicator { ) { 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); + self.observe_editor = + Some(cx.observe_in(&editor, window, Self::update_when_editor_is_switched)); + if let Some((_, buffer, _)) = &editor.read(cx).active_excerpt(cx) { + self.observe_buffer_encoding = Some(cx.observe_in( + buffer, + window, + Self::update_when_buffer_encoding_changes, + )); + } + self.update_when_editor_is_switched(editor, window, cx); self.show = true; } None => { self.encoding = None; - self.observe = None; + self.observe_editor = None; self.show = false; } } diff --git a/crates/encodings/src/selectors.rs b/crates/encodings/src/selectors.rs index 75c1045059dcd2..bd3ad321a21fe7 100644 --- a/crates/encodings/src/selectors.rs +++ b/crates/encodings/src/selectors.rs @@ -409,7 +409,6 @@ pub mod encoding { ) .await } - picker .update(cx, |picker, cx| { let delegate = &mut picker.delegate; @@ -426,11 +425,13 @@ pub mod encoding { 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 = + let buffer_encoding = buffer.encoding.clone(); + let buffer_encoding = &mut *buffer_encoding.lock().unwrap(); + *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(); + executor.spawn(buffer.reload(cx, true)).detach(); } else if self.action == Action::Save { let executor = cx.background_executor().clone(); diff --git a/crates/fs/src/encodings.rs b/crates/fs/src/encodings.rs index db75dcadad8078..882f6c0adee22f 100644 --- a/crates/fs/src/encodings.rs +++ b/crates/fs/src/encodings.rs @@ -1,5 +1,8 @@ //! Encoding and decoding utilities using the `encoding_rs` crate. -use std::fmt::Debug; +use std::{ + fmt::Debug, + sync::{Arc, Mutex}, +}; use anyhow::Result; use encoding_rs::Encoding; @@ -42,7 +45,34 @@ impl EncodingWrapper { self.0 } - pub async fn decode(&self, input: Vec) -> Result { + pub async fn decode( + &mut self, + input: Vec, + force: bool, + buffer_encoding: Option>>, + ) -> Result { + // Check if the input starts with a BOM for UTF-16 encodings only if not forced to + // use the encoding specified. + if !force { + if (input[0] == 0xFF) & (input[1] == 0xFE) { + self.0 = encoding_rs::UTF_16LE; + + if let Some(v) = buffer_encoding { + if let Ok(mut v) = (*v).lock() { + *v = encoding_rs::UTF_16LE; + } + } + } else if (input[0] == 0xFE) & (input[1] == 0xFF) { + self.0 = encoding_rs::UTF_16BE; + + if let Some(v) = buffer_encoding { + if let Ok(mut v) = (*v).lock() { + *v = encoding_rs::UTF_16BE; + } + } + } + } + let (cow, _had_errors) = self.0.decode_with_bom_removal(&input); // `encoding_rs` handles invalid bytes by replacing them with replacement characters @@ -53,8 +83,7 @@ impl EncodingWrapper { pub async fn encode(&self, input: String) -> Result> { if self.0 == encoding_rs::UTF_16BE { - let mut data = Vec::::new(); - data.reserve(input.len() * 2); // Reserve space for UTF-16BE bytes + let mut data = Vec::::with_capacity(input.len() * 2); // Convert the input string to UTF-16BE bytes let utf16be_bytes: Vec = @@ -63,8 +92,7 @@ impl EncodingWrapper { data.extend(utf16be_bytes); return Ok(data); } else if self.0 == encoding_rs::UTF_16LE { - let mut data = Vec::::new(); - data.reserve(input.len() * 2); // Reserve space for UTF-16LE bytes + let mut data = Vec::::with_capacity(input.len() * 2); // Convert the input string to UTF-16LE bytes let utf16le_bytes: Vec = @@ -83,8 +111,13 @@ impl EncodingWrapper { } /// Convert a byte vector from a specified encoding to a UTF-8 string. -pub async fn to_utf8(input: Vec, encoding: EncodingWrapper) -> Result { - encoding.decode(input).await +pub async fn to_utf8( + input: Vec, + mut encoding: EncodingWrapper, + force: bool, + buffer_encoding: Option>>, +) -> Result { + encoding.decode(input, force, buffer_encoding).await } /// Convert a UTF-8 string to a byte vector in a specified encoding. diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 0857b049d07235..3f0be2fa078988 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -9,6 +9,7 @@ use anyhow::{Context as _, Result, anyhow}; #[cfg(any(target_os = "linux", target_os = "freebsd"))] use ashpd::desktop::trash; use futures::stream::iter; +use encoding_rs::Encoding; use gpui::App; use gpui::BackgroundExecutor; use gpui::Global; @@ -124,8 +125,20 @@ pub trait Fs: Send + Sync { &self, path: PathBuf, encoding: EncodingWrapper, + force: bool, // if true, ignore BOM and use the specified encoding, + + // The current encoding of the buffer. BOM (if it exists) is checked + // to find if encoding is UTF-16, and if so, the encoding is updated to UTF-16 + // regardless of the value of `encoding`. + buffer_encoding: Arc>, ) -> anyhow::Result { - Ok(encodings::to_utf8(self.load_bytes(path.as_path()).await?, encoding).await?) + Ok(encodings::to_utf8( + self.load_bytes(path.as_path()).await?, + encoding, + force, + Some(buffer_encoding.clone()), + ) + .await?) } async fn load_bytes(&self, path: &Path) -> Result>; @@ -619,10 +632,11 @@ impl Fs for RealFs { async fn load(&self, path: &Path) -> Result { let path = path.to_path_buf(); let encoding = EncodingWrapper::new(encoding_rs::UTF_8); - let text = - smol::unblock(async || Ok(encodings::to_utf8(std::fs::read(path)?, encoding).await?)) - .await - .await; + let text = smol::unblock(async || { + Ok(encodings::to_utf8(std::fs::read(path)?, encoding, false, None).await?) + }) + .await + .await; text } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 0c43058c067aa9..77b4abace49e2a 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1056,7 +1056,7 @@ impl GitPanel { .iter() .filter_map(|buffer| { buffer.as_ref().ok()?.update(cx, |buffer, cx| { - buffer.is_dirty().then(|| buffer.reload(cx)) + buffer.is_dirty().then(|| buffer.reload(cx, false)) }) }) .collect() diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index e22f86be5be371..a90fa3411d4b32 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, - pub encoding: &'static Encoding, + pub encoding: Arc>, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -420,7 +420,13 @@ pub trait LocalFile: File { 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 Encoding) -> Task>; + fn load_with_encoding( + &self, + cx: &App, + encoding: &'static Encoding, + force: bool, // whether to force the encoding even if a BOM is present + buffer_encoding: Arc>, + ) -> Task>; } /// The auto-indent behavior associated with an editing operation. @@ -1011,7 +1017,7 @@ impl Buffer { has_conflict: false, change_bits: Default::default(), _subscriptions: Vec::new(), - encoding: encoding_rs::UTF_8, + encoding: Arc::new(std::sync::Mutex::new(encoding_rs::UTF_8)), } } @@ -1345,17 +1351,21 @@ impl Buffer { } /// Reloads the contents of the buffer from disk. - pub fn reload(&mut self, cx: &Context) -> oneshot::Receiver> { + pub fn reload( + &mut self, + cx: &Context, + force: bool, // whether to force the encoding even if a BOM is present + ) -> oneshot::Receiver> { let (tx, rx) = futures::channel::oneshot::channel(); - let encoding = self.encoding; + 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_with_encoding(cx, encoding), - )) + Some((file.disk_state().mtime(), { + file.load_with_encoding(cx, &*encoding.lock().unwrap(), force, encoding.clone()) + })) })? else { return Ok(()); @@ -5237,7 +5247,13 @@ impl LocalFile for TestFile { unimplemented!() } - fn load_with_encoding(&self, _: &App, _: &'static Encoding) -> Task> { + fn load_with_encoding( + &self, + _: &App, + _: &'static Encoding, + _: bool, // whether to force the encoding even if a BOM is present + _: Arc>, + ) -> Task> { unimplemented!() } } diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index d8dc6f4e0ffba7..9ae1ed0310e53d 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -387,7 +387,7 @@ impl LocalBufferStore { let version = buffer.version(); let buffer_id = buffer.remote_id(); let file = buffer.file().cloned(); - let encoding = buffer.encoding; + let encoding = buffer.encoding.clone(); if file .as_ref() @@ -397,7 +397,13 @@ impl LocalBufferStore { } let save = worktree.update(cx, |worktree, cx| { - worktree.write_file(path.as_ref(), text, line_ending, cx, encoding) + worktree.write_file( + path.as_ref(), + text, + line_ending, + cx, + &*encoding.lock().unwrap(), + ) }); cx.spawn(async move |this, cx| { @@ -629,22 +635,13 @@ impl LocalBufferStore { ) -> Task>> { let load_file = worktree.update(cx, |worktree, cx| worktree.load_file(path.as_ref(), cx)); cx.spawn(async move |this, cx| { - let path = path.clone(); - let buffer = match load_file.await.with_context(|| { - format!("Could not open path: {}", path.display(PathStyle::local())) - }) { - Ok(loaded) => { - let reservation = cx.reserve_entity::()?; - let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64()); - let executor = cx.background_executor().clone(); - let text_buffer = cx - .background_spawn(async move { - text::Buffer::new(ReplicaId::LOCAL, buffer_id, loaded.text, &executor) - }) - .await; - cx.insert_entity(reservation, |_| { - Buffer::build(text_buffer, Some(loaded.file), Capability::ReadWrite) - })? + let buffer = match load_buffer.await { + Ok(buffer) => { + // Reload the buffer to trigger UTF-16 detection + buffer + .update(cx, |buffer, cx| buffer.reload(cx, false))? + .await?; + Ok(buffer) } Err(error) if is_not_found_error(&error) => cx.new(|cx| { let buffer_id = BufferId::from(cx.entity_id().as_non_zero_u64()); @@ -723,7 +720,9 @@ impl LocalBufferStore { cx.spawn(async move |_, cx| { let mut project_transaction = ProjectTransaction::default(); for buffer in buffers { - let transaction = buffer.update(cx, |buffer, cx| buffer.reload(cx))?.await?; + let transaction = buffer + .update(cx, |buffer, cx| buffer.reload(cx, false))? + .await?; buffer.update(cx, |buffer, cx| { if let Some(transaction) = transaction { if !push_to_history { diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index a20f4035ec63cc..f087e9b6519268 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3129,13 +3129,22 @@ impl language::LocalFile for File { cx.background_spawn(async move { fs.load_bytes(&abs_path).await }) } - fn load_with_encoding(&self, cx: &App, encoding: &'static Encoding) -> Task> { + fn load_with_encoding( + &self, + cx: &App, + encoding: &'static Encoding, + force: bool, // whether to force the encoding even if there's a BOM + buffer_encoding: Arc>, + ) -> 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 }) + cx.background_spawn(async move { + fs.load_with_encoding(path?, encoding, force, buffer_encoding) + .await + }) } } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 60da5c5494cedd..2bbdb428ee12c7 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -443,8 +443,9 @@ pub fn initialize_workspace( } }); - let encoding_indicator = - cx.new(|_cx| encodings::EncodingIndicator::new(None, workspace.weak_handle(), None)); + let encoding_indicator = cx.new(|_cx| { + encodings::EncodingIndicator::new(None, workspace.weak_handle(), None, None) + }); let cursor_position = cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace)); From 2a193aeb66d64415957bca0eecfd274c512bdebb Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sat, 6 Sep 2025 16:15:13 +0530 Subject: [PATCH 18/43] Make the `EncodingSaveOrReopenSelector` open only when the current buffer is associated with a file --- crates/encodings/src/lib.rs | 51 +++++++++++++++++++++++++++--- crates/fs/src/fs.rs | 2 +- crates/language/src/buffer.rs | 2 +- crates/project/src/buffer_store.rs | 2 +- 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/crates/encodings/src/lib.rs b/crates/encodings/src/lib.rs index 318f4660eeb854..dc8bfe56ba48eb 100644 --- a/crates/encodings/src/lib.rs +++ b/crates/encodings/src/lib.rs @@ -1,5 +1,6 @@ //! A crate for handling file encodings in the text editor. +use crate::selectors::encoding::Action; use editor::{Editor, EditorSettings}; use encoding_rs::Encoding; use gpui::{ClickEvent, Entity, Subscription, WeakEntity}; @@ -9,6 +10,7 @@ use ui::{Button, ButtonCommon, Context, LabelSize, Render, Tooltip, Window, div} use ui::{Clickable, ParentElement}; use workspace::{ItemHandle, StatusItemView, Workspace}; +use crate::selectors::encoding::EncodingSelector; use crate::selectors::save_or_reopen::EncodingSaveOrReopenSelector; /// A status bar item that shows the current file encoding and allows changing it. @@ -22,7 +24,12 @@ pub struct EncodingIndicator { /// Subscription to observe changes in the `encoding` field of the `Buffer` struct observe_buffer_encoding: Option, - show: bool, // Whether to show the indicator or not, based on whether an editor is active + /// Whether to show the indicator or not, based on whether an editor is active + show: bool, + + /// Whether to show `EncodingSaveOrReopenSelector`. It will be shown only when + /// the current buffer is associated with a file. + show_save_or_reopen_selector: bool, } pub mod selectors; @@ -30,6 +37,7 @@ pub mod selectors; impl Render for EncodingIndicator { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { let status_element = div(); + let show_save_or_reopen_selector = self.show_save_or_reopen_selector; if (!EditorSettings::get_global(cx).status_bar.encoding_indicator) || !self.show { return status_element; @@ -42,12 +50,38 @@ impl Render for EncodingIndicator { ) .label_size(LabelSize::Small) .tooltip(Tooltip::text("Select Encoding")) - .on_click(cx.listener(|indicator, _: &ClickEvent, window, cx| { + .on_click(cx.listener(move |indicator, _: &ClickEvent, window, cx| { if let Some(workspace) = indicator.workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - EncodingSaveOrReopenSelector::toggle(workspace, window, cx) + workspace.update(cx, move |workspace, cx| { + // Open the `EncodingSaveOrReopenSelector` if the buffer is associated with a file, + if show_save_or_reopen_selector { + EncodingSaveOrReopenSelector::toggle(workspace, window, cx) + } + // otherwise, open the `EncodingSelector` directly. + else { + let (_, buffer, _) = workspace + .active_item(cx) + .unwrap() + .act_as::(cx) + .unwrap() + .read(cx) + .active_excerpt(cx) + .unwrap(); + + let weak_workspace = workspace.weak_handle(); + + workspace.toggle_modal(window, cx, |window, cx| { + let selector = EncodingSelector::new( + window, + cx, + Action::Save, + buffer.downgrade(), + weak_workspace, + ); + selector + }) + } }) - } else { } })), ) @@ -67,6 +101,7 @@ impl EncodingIndicator { observe_editor, show: true, observe_buffer_encoding, + show_save_or_reopen_selector: false, } } @@ -81,6 +116,12 @@ impl EncodingIndicator { if let Some((_, buffer, _)) = editor.active_excerpt(cx) { let encoding = buffer.read(cx).encoding.clone(); self.encoding = Some(&*encoding.lock().unwrap()); + + if let Some(_) = buffer.read(cx).file() { + self.show_save_or_reopen_selector = true; + } else { + self.show_save_or_reopen_selector = false; + } } cx.notify(); diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 3f0be2fa078988..6b47d507bb3d89 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -633,7 +633,7 @@ impl Fs for RealFs { let path = path.to_path_buf(); let encoding = EncodingWrapper::new(encoding_rs::UTF_8); let text = smol::unblock(async || { - Ok(encodings::to_utf8(std::fs::read(path)?, encoding, false, None).await?) + encodings::to_utf8(std::fs::read(path)?, encoding, false, None).await }) .await .await; diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index a90fa3411d4b32..ea28f37ab1aaa0 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1364,7 +1364,7 @@ impl Buffer { 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_with_encoding(cx, &*encoding.lock().unwrap(), force, encoding.clone()) + file.load_with_encoding(cx, &encoding.lock().unwrap(), force, encoding.clone()) })) })? else { diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 9ae1ed0310e53d..a1fe441abd89a4 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -402,7 +402,7 @@ impl LocalBufferStore { text, line_ending, cx, - &*encoding.lock().unwrap(), + &encoding.lock().unwrap(), ) }); From fb6ec668872e9758268582c8ef4e1a6eadca6d3c Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sat, 6 Sep 2025 17:37:12 +0530 Subject: [PATCH 19/43] Fix an error caused by UTF-16 BOM checking when there are no BOMs --- crates/fs/src/encodings.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/crates/fs/src/encodings.rs b/crates/fs/src/encodings.rs index 882f6c0adee22f..4d11b600f72136 100644 --- a/crates/fs/src/encodings.rs +++ b/crates/fs/src/encodings.rs @@ -54,20 +54,22 @@ impl EncodingWrapper { // Check if the input starts with a BOM for UTF-16 encodings only if not forced to // use the encoding specified. if !force { - if (input[0] == 0xFF) & (input[1] == 0xFE) { - self.0 = encoding_rs::UTF_16LE; - - if let Some(v) = buffer_encoding { - if let Ok(mut v) = (*v).lock() { - *v = encoding_rs::UTF_16LE; + if input.len() >= 2 { + if (input[0] == 0xFF) & (input[1] == 0xFE) { + self.0 = encoding_rs::UTF_16LE; + + if let Some(v) = buffer_encoding { + if let Ok(mut v) = (*v).lock() { + *v = encoding_rs::UTF_16LE; + } } - } - } else if (input[0] == 0xFE) & (input[1] == 0xFF) { - self.0 = encoding_rs::UTF_16BE; + } else if (input.len() >= 2) & (input[0] == 0xFE) & (input[1] == 0xFF) { + self.0 = encoding_rs::UTF_16BE; - if let Some(v) = buffer_encoding { - if let Ok(mut v) = (*v).lock() { - *v = encoding_rs::UTF_16BE; + if let Some(v) = buffer_encoding { + if let Ok(mut v) = (*v).lock() { + *v = encoding_rs::UTF_16BE; + } } } } From dd5ddbfc4311b64b0b51578e53a0838cc84b8d6f Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sun, 7 Sep 2025 11:07:19 +0530 Subject: [PATCH 20/43] - Remove unnecessary calls to `collect` - Simplify UTF-16 BOM detection Co-authored-by: CrazyboyQCD Release Notes: - Add support for opening and saving files in different encodings and a setting to enable or disable the indicator --- crates/fs/src/encodings.rs | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/crates/fs/src/encodings.rs b/crates/fs/src/encodings.rs index 4d11b600f72136..c6c13f64d2e6b2 100644 --- a/crates/fs/src/encodings.rs +++ b/crates/fs/src/encodings.rs @@ -54,22 +54,16 @@ impl EncodingWrapper { // Check if the input starts with a BOM for UTF-16 encodings only if not forced to // use the encoding specified. if !force { - if input.len() >= 2 { - if (input[0] == 0xFF) & (input[1] == 0xFE) { - self.0 = encoding_rs::UTF_16LE; - - if let Some(v) = buffer_encoding { - if let Ok(mut v) = (*v).lock() { - *v = encoding_rs::UTF_16LE; - } - } - } else if (input.len() >= 2) & (input[0] == 0xFE) & (input[1] == 0xFF) { - self.0 = encoding_rs::UTF_16BE; - - if let Some(v) = buffer_encoding { - if let Ok(mut v) = (*v).lock() { - *v = encoding_rs::UTF_16BE; - } + if let Some(encoding) = match input.get(..2) { + Some([0xFF, 0xFE]) => Some(encoding_rs::UTF_16LE), + Some([0xFE, 0xFF]) => Some(encoding_rs::UTF_16BE), + _ => None, + } { + self.0 = encoding; + + if let Some(v) = buffer_encoding { + if let Ok(mut v) = (*v).lock() { + *v = encoding; } } } @@ -88,8 +82,7 @@ impl EncodingWrapper { let mut data = Vec::::with_capacity(input.len() * 2); // Convert the input string to UTF-16BE bytes - let utf16be_bytes: Vec = - input.encode_utf16().flat_map(|u| u.to_be_bytes()).collect(); + let utf16be_bytes = input.encode_utf16().flat_map(|u| u.to_be_bytes()); data.extend(utf16be_bytes); return Ok(data); @@ -97,8 +90,7 @@ impl EncodingWrapper { let mut data = Vec::::with_capacity(input.len() * 2); // Convert the input string to UTF-16LE bytes - let utf16le_bytes: Vec = - input.encode_utf16().flat_map(|u| u.to_le_bytes()).collect(); + let utf16le_bytes = input.encode_utf16().flat_map(|u| u.to_le_bytes()); data.extend(utf16le_bytes); return Ok(data); From 591fe9f4a0fd6c532e542d84dc717b50d42bde27 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Fri, 3 Oct 2025 10:15:12 +0530 Subject: [PATCH 21/43] - Remove `load_with_encoding` from the `Fs` trait and make appropriate changes to the file loading logic - Add a module and an action in `zed_actions` to open `EncodingSelector` from `InvalidBufferView` - Add an `init` function in `encodings` crate to register the action handler Clicking the second button in `InvalidBufferView` will cause the `EncodingSelector` to be displayed. However, clicking on an encoding currently does nothing. --- Cargo.lock | 1 + crates/copilot/src/copilot.rs | 11 ----- crates/editor/src/editor_settings.rs | 12 +++++ crates/encodings/Cargo.toml | 1 + crates/encodings/src/lib.rs | 27 ++++++++---- crates/encodings/src/selectors.rs | 18 ++++---- crates/fs/src/fs.rs | 30 +------------ crates/git_ui/src/git_panel.rs | 2 +- crates/language/src/buffer.rs | 30 +------------ crates/project/src/buffer_store.rs | 8 +--- crates/project/src/invalid_item_view.rs | 44 +++++++++++++------ .../remote_server/src/remote_editing_tests.rs | 3 +- crates/worktree/src/worktree.rs | 18 -------- crates/zed/src/main.rs | 1 + crates/zed_actions/src/lib.rs | 7 +++ 15 files changed, 90 insertions(+), 123 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 673dc0317985ed..0fac9a1f665bd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5538,6 +5538,7 @@ dependencies = [ "util", "workspace", "workspace-hack", + "zed_actions", ] [[package]] diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index f72e1f392d0a7a..41c8a17c2d251e 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1241,7 +1241,6 @@ async fn get_copilot_lsp(fs: Arc, node_runtime: NodeRuntime) -> anyhow:: #[cfg(test)] mod tests { use super::*; - use encoding_rs::Encoding; use gpui::TestAppContext; use util::{path, paths::PathStyle, rel_path::rel_path}; @@ -1459,16 +1458,6 @@ mod tests { fn load_bytes(&self, _cx: &App) -> Task>> { unimplemented!() } - - fn load_with_encoding( - &self, - _: &App, - _: &'static Encoding, - _: bool, - _: Arc>, - ) -> Task> { - unimplemented!() - } } } diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index dc67ab3ed6c8cf..1050eaf88ac966 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -64,6 +64,18 @@ pub struct Jupyter { pub enabled: bool, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct StatusBar { + /// Whether to display the active language button in the status bar. + /// + /// Default: true + pub active_language_button: bool, + /// Whether to show the cursor position button in the status bar. + /// + /// Default: true + pub cursor_position_button: bool, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct Toolbar { pub breadcrumbs: bool, diff --git a/crates/encodings/Cargo.toml b/crates/encodings/Cargo.toml index b7743d438d8cc2..c8012b77567428 100644 --- a/crates/encodings/Cargo.toml +++ b/crates/encodings/Cargo.toml @@ -16,6 +16,7 @@ ui.workspace = true util.workspace = true workspace.workspace = true workspace-hack.workspace = true +zed_actions.workspace = true [lints] diff --git a/crates/encodings/src/lib.rs b/crates/encodings/src/lib.rs index dc8bfe56ba48eb..293091705f38df 100644 --- a/crates/encodings/src/lib.rs +++ b/crates/encodings/src/lib.rs @@ -1,14 +1,13 @@ //! A crate for handling file encodings in the text editor. use crate::selectors::encoding::Action; -use editor::{Editor, EditorSettings}; +use editor::Editor; use encoding_rs::Encoding; use gpui::{ClickEvent, Entity, Subscription, WeakEntity}; use language::Buffer; -use settings::Settings; -use ui::{Button, ButtonCommon, Context, LabelSize, Render, Tooltip, Window, div}; +use ui::{App, Button, ButtonCommon, Context, LabelSize, Render, Tooltip, Window, div}; use ui::{Clickable, ParentElement}; -use workspace::{ItemHandle, StatusItemView, Workspace}; +use workspace::{ItemHandle, StatusItemView, Workspace, with_active_or_new_workspace}; use crate::selectors::encoding::EncodingSelector; use crate::selectors::save_or_reopen::EncodingSaveOrReopenSelector; @@ -39,7 +38,7 @@ impl Render for EncodingIndicator { let status_element = div(); let show_save_or_reopen_selector = self.show_save_or_reopen_selector; - if (!EditorSettings::get_global(cx).status_bar.encoding_indicator) || !self.show { + if !self.show { return status_element; } @@ -75,7 +74,7 @@ impl Render for EncodingIndicator { window, cx, Action::Save, - buffer.downgrade(), + Some(buffer.downgrade()), weak_workspace, ); selector @@ -99,7 +98,7 @@ impl EncodingIndicator { encoding, workspace, observe_editor, - show: true, + show: false, observe_buffer_encoding, show_save_or_reopen_selector: false, } @@ -287,7 +286,17 @@ pub fn encoding_from_name(name: &str) -> &'static Encoding { "GBK" => encoding_rs::GBK, "GB18030" => encoding_rs::GB18030, "Big5" => encoding_rs::BIG5, - "HZ-GB-2312" => encoding_rs::UTF_8, // encoding_rs doesn't support HZ, fallback to UTF-8 - _ => encoding_rs::UTF_8, // Default to UTF-8 for unknown names + _ => encoding_rs::UTF_8, // Default to UTF-8 for unknown names } } + +pub fn init(cx: &mut App) { + cx.on_action(|_: &zed_actions::encodings::Toggle, cx: &mut App| { + with_active_or_new_workspace(cx, |workspace, window, cx| { + let weak_workspace = workspace.weak_handle(); + workspace.toggle_modal(window, cx, |window, cx| { + EncodingSelector::new(window, cx, Action::Reopen, None, weak_workspace) + }); + }); + }); +} diff --git a/crates/encodings/src/selectors.rs b/crates/encodings/src/selectors.rs index bd3ad321a21fe7..010662e8b8bf70 100644 --- a/crates/encodings/src/selectors.rs +++ b/crates/encodings/src/selectors.rs @@ -123,7 +123,7 @@ pub mod save_or_reopen { window, cx, Action::Save, - buffer.downgrade(), + Some(buffer.downgrade()), weak_workspace, ); selector @@ -147,7 +147,7 @@ pub mod save_or_reopen { window, cx, Action::Reopen, - buffer.downgrade(), + Some(buffer.downgrade()), weak_workspace, ); selector @@ -301,14 +301,14 @@ pub mod encoding { encodings: Vec, matches: Vec, selector: WeakEntity, - buffer: WeakEntity, + buffer: Option>, action: Action, } impl EncodingSelectorDelegate { pub fn new( selector: WeakEntity, - buffer: WeakEntity, + buffer: Option>, action: Action, ) -> EncodingSelectorDelegate { EncodingSelectorDelegate { @@ -349,7 +349,7 @@ pub mod encoding { ], matches: Vec::new(), selector, - buffer, + buffer: buffer, action, } } @@ -423,7 +423,9 @@ pub mod encoding { } fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { - if let Some(buffer) = self.buffer.upgrade() { + if let Some(buffer) = &self.buffer + && let Some(buffer) = buffer.upgrade() + { buffer.update(cx, |buffer, cx| { let buffer_encoding = buffer.encoding.clone(); let buffer_encoding = &mut *buffer_encoding.lock().unwrap(); @@ -431,7 +433,7 @@ pub mod 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, true)).detach(); + executor.spawn(buffer.reload(cx)).detach(); } else if self.action == Action::Save { let executor = cx.background_executor().clone(); @@ -493,7 +495,7 @@ pub mod encoding { window: &mut Window, cx: &mut Context, action: Action, - buffer: WeakEntity, + buffer: Option>, workspace: WeakEntity, ) -> EncodingSelector { let delegate = EncodingSelectorDelegate::new(cx.entity().downgrade(), buffer, action); diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 6b47d507bb3d89..591475bd49f19e 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -120,27 +120,6 @@ pub trait Fs: Send + Sync { 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, - force: bool, // if true, ignore BOM and use the specified encoding, - - // The current encoding of the buffer. BOM (if it exists) is checked - // to find if encoding is UTF-16, and if so, the encoding is updated to UTF-16 - // regardless of the value of `encoding`. - buffer_encoding: Arc>, - ) -> anyhow::Result { - Ok(encodings::to_utf8( - self.load_bytes(path.as_path()).await?, - encoding, - force, - Some(buffer_encoding.clone()), - ) - .await?) - } - async fn load_bytes(&self, path: &Path) -> Result>; async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>; async fn save( @@ -631,13 +610,8 @@ impl Fs for RealFs { async fn load(&self, path: &Path) -> Result { let path = path.to_path_buf(); - let encoding = EncodingWrapper::new(encoding_rs::UTF_8); - let text = smol::unblock(async || { - encodings::to_utf8(std::fs::read(path)?, encoding, false, None).await - }) - .await - .await; - text + let text = smol::unblock(|| std::fs::read_to_string(path)).await?; + Ok(text) } async fn load_bytes(&self, path: &Path) -> Result> { diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 77b4abace49e2a..0c43058c067aa9 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1056,7 +1056,7 @@ impl GitPanel { .iter() .filter_map(|buffer| { buffer.as_ref().ok()?.update(cx, |buffer, cx| { - buffer.is_dirty().then(|| buffer.reload(cx, false)) + buffer.is_dirty().then(|| buffer.reload(cx)) }) }) .collect() diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index ea28f37ab1aaa0..9c094530b16efc 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -418,15 +418,6 @@ 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 Encoding, - force: bool, // whether to force the encoding even if a BOM is present - buffer_encoding: Arc>, - ) -> Task>; } /// The auto-indent behavior associated with an editing operation. @@ -1351,21 +1342,14 @@ impl Buffer { } /// Reloads the contents of the buffer from disk. - pub fn reload( - &mut self, - cx: &Context, - force: bool, // whether to force the encoding even if a BOM is present - ) -> oneshot::Receiver> { + 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_with_encoding(cx, &encoding.lock().unwrap(), force, encoding.clone()) - })) + Some((file.disk_state().mtime(), { file.load(cx) })) })? else { return Ok(()); @@ -5246,16 +5230,6 @@ impl LocalFile for TestFile { fn load_bytes(&self, _cx: &App) -> Task>> { unimplemented!() } - - fn load_with_encoding( - &self, - _: &App, - _: &'static Encoding, - _: bool, // whether to force the encoding even if a BOM is present - _: Arc>, - ) -> Task> { - unimplemented!() - } } pub(crate) fn contiguous_ranges( diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index a1fe441abd89a4..2d2adaee3c2032 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -638,9 +638,7 @@ impl LocalBufferStore { let buffer = match load_buffer.await { Ok(buffer) => { // Reload the buffer to trigger UTF-16 detection - buffer - .update(cx, |buffer, cx| buffer.reload(cx, false))? - .await?; + buffer.update(cx, |buffer, cx| buffer.reload(cx))?.await?; Ok(buffer) } Err(error) if is_not_found_error(&error) => cx.new(|cx| { @@ -720,9 +718,7 @@ impl LocalBufferStore { cx.spawn(async move |_, cx| { let mut project_transaction = ProjectTransaction::default(); for buffer in buffers { - let transaction = buffer - .update(cx, |buffer, cx| buffer.reload(cx, false))? - .await?; + let transaction = buffer.update(cx, |buffer, cx| buffer.reload(cx))?.await?; buffer.update(cx, |buffer, cx| { if let Some(transaction) = transaction { if !push_to_history { diff --git a/crates/project/src/invalid_item_view.rs b/crates/project/src/invalid_item_view.rs index fdcdd16a69ce73..fa42e4c2d32479 100644 --- a/crates/project/src/invalid_item_view.rs +++ b/crates/project/src/invalid_item_view.rs @@ -97,20 +97,38 @@ impl Render for InvalidItemView { .child(Label::new(self.error.clone()).size(LabelSize::Small)), ) .when(self.is_local, |contents| { - contents.child( - h_flex().justify_center().child( - Button::new("open-with-system", "Open in Default App") - .on_click(move |_, _, cx| { - cx.open_with_system(&abs_path); - }) + contents + .child( + h_flex().justify_center().child( + Button::new("open-with-system", "Open in Default App") + .on_click(move |_, _, cx| { + cx.open_with_system(&abs_path); + }) + .style(ButtonStyle::Outlined) + .key_binding(KeyBinding::for_action( + &OpenWithSystem, + window, + cx, + )), + ), + ) + .child( + h_flex().justify_center().child( + Button::new( + "open-with-encoding", + "Open With a Different Encoding", + ) .style(ButtonStyle::Outlined) - .key_binding(KeyBinding::for_action( - &OpenWithSystem, - window, - cx, - )), - ), - ) + .on_click( + |_, window, cx| { + window.dispatch_action( + Box::new(zed_actions::encodings::Toggle), + cx, + ) + }, + ), + ), + ) }), ), ) diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index ed696312f7a2d9..4fff49de6af4b2 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -6,7 +6,8 @@ use agent::{AgentTool, ReadFileTool, ReadFileToolInput, ToolCallEventStream}; use client::{Client, UserStore}; use clock::FakeSystemClock; use collections::{HashMap, HashSet}; -use encoding::all::UTF_8; +use encoding_rs::UTF_8; +use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModel}; use extension::ExtensionHostProxy; use fs::{FakeFs, Fs, encodings::EncodingWrapper}; diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index f087e9b6519268..c1061f40ab0ee6 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3128,24 +3128,6 @@ 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 Encoding, - force: bool, // whether to force the encoding even if there's a BOM - buffer_encoding: Arc>, - ) -> 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, force, buffer_encoding) - .await - }) - } } impl File { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b873a58d3b6133..f826399e4599b1 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -630,6 +630,7 @@ pub fn main() { zeta::init(cx); inspector_ui::init(app_state.clone(), cx); json_schema_store::init(cx); + encodings::init(cx); cx.observe_global::({ let http = app_state.client.http_client(); diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 5cb2903fa653fc..ba35639d1b476f 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -299,6 +299,13 @@ pub mod settings_profile_selector { pub struct Toggle; } +pub mod encodings { + use gpui::Action; + + #[derive(PartialEq, Debug, Clone, Action)] + pub struct Toggle; +} + pub mod agent { use gpui::actions; From 9dc21f5a12dfefde81f04de4b2a15d45549a9bca Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sun, 5 Oct 2025 11:09:02 +0530 Subject: [PATCH 22/43] Re-Add `load_with_encoding` method to the `Fs` trait --- crates/fs/src/fs.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 591475bd49f19e..403602bb623572 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -62,6 +62,7 @@ use std::ffi::OsStr; #[cfg(any(test, feature = "test-support"))] pub use fake_git_repo::{LOAD_HEAD_TEXT_TASK, LOAD_INDEX_TEXT_TASK}; +use crate::encodings::to_utf8; use crate::encodings::EncodingWrapper; use crate::encodings::from_utf8; @@ -120,6 +121,15 @@ pub trait Fs: Send + Sync { Ok(String::from_utf8(self.load_bytes(path).await?)?) } + async fn load_with_encoding( + &self, + path: &Path, + encoding: EncodingWrapper, + detect_utf16: bool, + ) -> Result { + Ok(to_utf8(self.load_bytes(path).await?, encoding, detect_utf16, None).await?) + } + async fn load_bytes(&self, path: &Path) -> Result>; async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>; async fn save( From 6561c890daea69cd17a530659cbe5b2e2fe58539 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sun, 5 Oct 2025 11:21:18 +0530 Subject: [PATCH 23/43] - Make `File::load` use `load_with_encoding` - Update the places where `File::load` is called WARNING: The changes were committed with an error --- crates/language/src/buffer.rs | 17 +++++++++++++---- crates/worktree/src/worktree.rs | 12 ++++++++++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 9c094530b16efc..029619a4ad7cc6 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -22,7 +22,7 @@ use clock::Lamport; pub use clock::ReplicaId; use collections::HashMap; use encoding_rs::Encoding; -use fs::MTime; +use fs::{MTime, encodings::EncodingWrapper}; use futures::channel::oneshot; use gpui::{ App, AppContext as _, BackgroundExecutor, Context, Entity, EventEmitter, HighlightStyle, @@ -414,7 +414,8 @@ pub trait LocalFile: File { fn abs_path(&self, cx: &App) -> PathBuf; /// Loads the file contents from disk and returns them as a UTF-8 encoded string. - fn load(&self, cx: &App) -> Task>; + fn load(&self, cx: &App, encoding: EncodingWrapper, detect_utf16: bool) + -> Task>; /// Loads the file's contents from disk. fn load_bytes(&self, cx: &App) -> Task>>; @@ -1344,12 +1345,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 = EncodingWrapper::new(*(self.encoding.lock().unwrap())); 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(cx, encoding, false) + })) })? else { return Ok(()); @@ -5223,7 +5227,12 @@ impl LocalFile for TestFile { .join(self.path.as_std_path()) } - fn load(&self, _cx: &App) -> Task> { + fn load( + &self, + _cx: &App, + _encoding: EncodingWrapper, + _detect_utf16: bool, + ) -> Task> { unimplemented!() } diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index c1061f40ab0ee6..4b69787acf9c04 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3115,11 +3115,19 @@ impl language::LocalFile for File { self.worktree.read(cx).absolutize(&self.path) } - fn load(&self, cx: &App) -> Task> { + fn load( + &self, + cx: &App, + encoding: EncodingWrapper, + detect_utf16: bool, + ) -> Task> { let worktree = self.worktree.read(cx).as_local().unwrap(); let abs_path = worktree.absolutize(&self.path); let fs = worktree.fs.clone(); - cx.background_spawn(async move { fs.load(&abs_path).await }) + cx.background_spawn(async move { + fs.load_with_encoding(&abs_path?, encoding, detect_utf16) + .await + }) } fn load_bytes(&self, cx: &App) -> Task>> { From 80631446e0ea6b1c9eccb93c051105ff1b08e264 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sun, 5 Oct 2025 12:13:20 +0530 Subject: [PATCH 24/43] Update `tests` in `copilot.rs` to match the new `load` method signature --- crates/copilot/src/copilot.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 41c8a17c2d251e..1ca01038e28d3c 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1241,6 +1241,7 @@ async fn get_copilot_lsp(fs: Arc, node_runtime: NodeRuntime) -> anyhow:: #[cfg(test)] mod tests { use super::*; + use fs::encodings::EncodingWrapper; use gpui::TestAppContext; use util::{path, paths::PathStyle, rel_path::rel_path}; @@ -1451,7 +1452,7 @@ mod tests { self.abs_path.clone() } - fn load(&self, _: &App) -> Task> { + fn load(&self, _: &App, _: EncodingWrapper, _: bool) -> Task> { unimplemented!() } From 13ea13b0a4843e1d7af15b4ef13d6bd134f5a3d3 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sun, 5 Oct 2025 12:22:48 +0530 Subject: [PATCH 25/43] Pass file path to `EncodingSelector` via `Toggle` action, if there is one. --- crates/encodings/src/lib.rs | 9 +++++++-- crates/encodings/src/selectors.rs | 12 ++++++++++-- crates/project/src/invalid_item_view.rs | 8 ++++++-- crates/zed_actions/src/lib.rs | 8 ++++++-- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/crates/encodings/src/lib.rs b/crates/encodings/src/lib.rs index 293091705f38df..4d7256db6f3b0b 100644 --- a/crates/encodings/src/lib.rs +++ b/crates/encodings/src/lib.rs @@ -8,6 +8,7 @@ use language::Buffer; use ui::{App, Button, ButtonCommon, Context, LabelSize, Render, Tooltip, Window, div}; use ui::{Clickable, ParentElement}; use workspace::{ItemHandle, StatusItemView, Workspace, with_active_or_new_workspace}; +use zed_actions::encodings::Toggle; use crate::selectors::encoding::EncodingSelector; use crate::selectors::save_or_reopen::EncodingSaveOrReopenSelector; @@ -76,6 +77,7 @@ impl Render for EncodingIndicator { Action::Save, Some(buffer.downgrade()), weak_workspace, + None, ); selector }) @@ -291,11 +293,14 @@ pub fn encoding_from_name(name: &str) -> &'static Encoding { } pub fn init(cx: &mut App) { - cx.on_action(|_: &zed_actions::encodings::Toggle, cx: &mut App| { + cx.on_action(|action: &Toggle, cx: &mut App| { + let Toggle(path) = action.clone(); + let path = path.to_path_buf(); + with_active_or_new_workspace(cx, |workspace, window, cx| { let weak_workspace = workspace.weak_handle(); workspace.toggle_modal(window, cx, |window, cx| { - EncodingSelector::new(window, cx, Action::Reopen, None, weak_workspace) + EncodingSelector::new(window, cx, Action::Reopen, None, weak_workspace, Some(path)) }); }); }); diff --git a/crates/encodings/src/selectors.rs b/crates/encodings/src/selectors.rs index 010662e8b8bf70..f3e26f8bf5842f 100644 --- a/crates/encodings/src/selectors.rs +++ b/crates/encodings/src/selectors.rs @@ -125,6 +125,7 @@ pub mod save_or_reopen { Action::Save, Some(buffer.downgrade()), weak_workspace, + None, ); selector }) @@ -149,6 +150,7 @@ pub mod save_or_reopen { Action::Reopen, Some(buffer.downgrade()), weak_workspace, + None, ); selector }); @@ -275,7 +277,7 @@ 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::sync::atomic::AtomicBool; + use std::{path::PathBuf, sync::atomic::AtomicBool}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, WeakEntity}; @@ -294,6 +296,7 @@ pub mod encoding { pub struct EncodingSelector { picker: Entity>, workspace: WeakEntity, + path: Option, } pub struct EncodingSelectorDelegate { @@ -497,11 +500,16 @@ pub mod encoding { action: Action, buffer: Option>, workspace: WeakEntity, + path: Option, ) -> EncodingSelector { let delegate = EncodingSelectorDelegate::new(cx.entity().downgrade(), buffer, action); let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); - EncodingSelector { picker, workspace } + EncodingSelector { + picker, + workspace, + path, + } } } diff --git a/crates/project/src/invalid_item_view.rs b/crates/project/src/invalid_item_view.rs index fa42e4c2d32479..92952a788b6fd2 100644 --- a/crates/project/src/invalid_item_view.rs +++ b/crates/project/src/invalid_item_view.rs @@ -78,6 +78,8 @@ impl Focusable for InvalidItemView { impl Render for InvalidItemView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { let abs_path = self.abs_path.clone(); + let path = self.abs_path.clone(); + v_flex() .size_full() .track_focus(&self.focus_handle(cx)) @@ -120,9 +122,11 @@ impl Render for InvalidItemView { ) .style(ButtonStyle::Outlined) .on_click( - |_, window, cx| { + move |_, window, cx| { window.dispatch_action( - Box::new(zed_actions::encodings::Toggle), + Box::new(zed_actions::encodings::Toggle( + path.clone(), + )), cx, ) }, diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index ba35639d1b476f..5a4558aa3dcad4 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -300,10 +300,14 @@ pub mod settings_profile_selector { } pub mod encodings { + use std::sync::Arc; + use gpui::Action; + use schemars::JsonSchema; + use serde::Deserialize; - #[derive(PartialEq, Debug, Clone, Action)] - pub struct Toggle; + #[derive(PartialEq, Debug, Clone, Action, JsonSchema, Deserialize)] + pub struct Toggle(pub Arc); } pub mod agent { From 37754b0fd512310bf2cff7ee18d7e8c4d35b441a Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sun, 5 Oct 2025 14:06:57 +0530 Subject: [PATCH 26/43] Add a call to `open_abs_path` to enable opening of files from `InvalidBufferView` --- crates/encodings/src/selectors.rs | 36 +++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/crates/encodings/src/selectors.rs b/crates/encodings/src/selectors.rs index f3e26f8bf5842f..893842723c6076 100644 --- a/crates/encodings/src/selectors.rs +++ b/crates/encodings/src/selectors.rs @@ -426,6 +426,15 @@ pub mod encoding { } fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { + let workspace = self + .selector + .upgrade() + .unwrap() + .read(cx) + .workspace + .upgrade() + .unwrap(); + if let Some(buffer) = &self.buffer && let Some(buffer) = buffer.upgrade() { @@ -440,15 +449,6 @@ pub mod encoding { } 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 @@ -458,6 +458,24 @@ pub mod encoding { .detach(); } }); + } else { + workspace.update(cx, |workspace, cx| { + workspace + .open_abs_path( + self.selector + .upgrade() + .unwrap() + .read(cx) + .path + .as_ref() + .unwrap() + .clone(), + Default::default(), + window, + cx, + ) + .detach(); + }) } self.dismissed(window, cx); } From 44abaed85783a1ea872e70e0064e8eb6649fe93a Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sun, 5 Oct 2025 14:52:00 +0530 Subject: [PATCH 27/43] - Return an error if the file contains invalid bytes for the specified encoding instead of replacing the invalid bytes with replacement characters - Add `encoding` field in `Workspace` --- crates/encodings/src/selectors.rs | 2 ++ crates/fs/src/encodings.rs | 19 +++++++++++++++---- crates/workspace/src/workspace.rs | 4 +++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/crates/encodings/src/selectors.rs b/crates/encodings/src/selectors.rs index 893842723c6076..b88f882e31955c 100644 --- a/crates/encodings/src/selectors.rs +++ b/crates/encodings/src/selectors.rs @@ -460,6 +460,8 @@ pub mod encoding { }); } else { workspace.update(cx, |workspace, cx| { + *workspace.encoding.lock().unwrap() = + encoding_from_name(self.matches[self.current_selection].string.as_str()); workspace .open_abs_path( self.selector diff --git a/crates/fs/src/encodings.rs b/crates/fs/src/encodings.rs index c6c13f64d2e6b2..1759f8917c643c 100644 --- a/crates/fs/src/encodings.rs +++ b/crates/fs/src/encodings.rs @@ -19,6 +19,12 @@ impl Debug for EncodingWrapper { } } +impl Default for EncodingWrapper { + fn default() -> Self { + EncodingWrapper(encoding_rs::UTF_8) + } +} + pub struct EncodingWrapperVisitor; impl PartialEq for EncodingWrapper { @@ -71,10 +77,15 @@ impl EncodingWrapper { let (cow, _had_errors) = self.0.decode_with_bom_removal(&input); - // `encoding_rs` handles invalid bytes by replacing them with replacement characters - // in the output string, so we return the result even if there were errors. - // This preserves the original behaviour where files with invalid bytes could still be opened. - Ok(cow.into_owned()) + if !_had_errors { + Ok(cow.to_string()) + } else { + // If there were decoding errors, return an error. + Err(anyhow::anyhow!( + "The file contains invalid bytes for the specified encoding: {}. This usually menas that the file is not a regular text file, or is encoded in a different encoding. Continuing to open it may result in data loss if saved.", + self.0.name() + )) + } } pub async fn encode(&self, input: String) -> Result> { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f561e7f30a15f7..bf21e72fb38ebe 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -19,6 +19,7 @@ mod workspace_settings; pub use crate::notifications::NotificationFrame; pub use dock::Panel; +use encoding_rs::Encoding; use encoding_rs::UTF_8; use fs::encodings::EncodingWrapper; pub use path_list::PathList; @@ -1181,6 +1182,7 @@ pub struct Workspace { session_id: Option, scheduled_tasks: Vec>, last_open_dock_positions: Vec, + pub encoding: Arc>, } impl EventEmitter for Workspace {} @@ -1521,9 +1523,9 @@ impl Workspace { serializable_items_tx, _items_serializer, session_id: Some(session_id), - scheduled_tasks: Vec::new(), last_open_dock_positions: Vec::new(), + encoding: Arc::new(std::sync::Mutex::new(encoding_rs::UTF_8)), } } From 183bff580e395279b22f513ea9a6edd9707f7de1 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sun, 5 Oct 2025 18:07:19 +0530 Subject: [PATCH 28/43] - Add a field `encoding` in both `Workspace` and `Project` - Pass encoding to `ProjectRegistry::open_path` and set the `encoding` field in `Project` --- crates/fs/src/encodings.rs | 2 +- crates/project/src/project.rs | 5 +++++ crates/workspace/src/workspace.rs | 28 +++++++++++++++++++++------- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/crates/fs/src/encodings.rs b/crates/fs/src/encodings.rs index 1759f8917c643c..780da0cadc5474 100644 --- a/crates/fs/src/encodings.rs +++ b/crates/fs/src/encodings.rs @@ -9,7 +9,7 @@ use encoding_rs::Encoding; /// A wrapper around `encoding_rs::Encoding` to implement `Send` and `Sync`. /// Since the reference is static, it is safe to send it across threads. -pub struct EncodingWrapper(&'static Encoding); +pub struct EncodingWrapper(pub &'static Encoding); impl Debug for EncodingWrapper { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 7c7fe9a4309161..044c1501064bdb 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -26,6 +26,7 @@ mod project_tests; mod environment; use buffer_diff::BufferDiff; use context_server_store::ContextServerStore; +use encoding_rs::Encoding; pub use environment::ProjectEnvironmentEvent; use git::repository::get_git_committer; use git_store::{Repository, RepositoryId}; @@ -215,6 +216,7 @@ pub struct Project { settings_observer: Entity, toolchain_store: Option>, agent_location: Option, + pub encoding: Arc>, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -1225,6 +1227,7 @@ impl Project { toolchain_store: Some(toolchain_store), agent_location: None, + encoding: Arc::new(std::sync::Mutex::new(encoding_rs::UTF_8)), } }) } @@ -1410,6 +1413,7 @@ impl Project { toolchain_store: Some(toolchain_store), agent_location: None, + encoding: Arc::new(std::sync::Mutex::new(encoding_rs::UTF_8)), }; // remote server -> local machine handlers @@ -1663,6 +1667,7 @@ impl Project { remotely_created_models: Arc::new(Mutex::new(RemotelyCreatedModels::default())), toolchain_store: None, agent_location: None, + encoding: Arc::new(std::sync::Mutex::new(encoding_rs::UTF_8)), }; project.set_role(role, cx); for worktree in worktrees { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index bf21e72fb38ebe..d39864abdb1409 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -624,6 +624,7 @@ type BuildProjectItemForPathFn = fn( &Entity, &ProjectPath, + Option, &mut Window, &mut App, ) -> Option, WorkspaceItemBuilder)>>>; @@ -645,8 +646,12 @@ impl ProjectItemRegistry { }, ); self.build_project_item_for_path_fns - .push(|project, project_path, window, cx| { + .push(|project, project_path, encoding, window, cx| { let project_path = project_path.clone(); + let EncodingWrapper(encoding) = encoding.unwrap_or_default(); + + project.update(cx, |project, _| {*project.encoding.lock().unwrap() = encoding}); + let is_file = project .read(cx) .entry_for_path(&project_path, cx) @@ -715,14 +720,17 @@ impl ProjectItemRegistry { &self, project: &Entity, path: &ProjectPath, + encoding: Option, window: &mut Window, cx: &mut App, ) -> Task, WorkspaceItemBuilder)>> { - let Some(open_project_item) = self - .build_project_item_for_path_fns - .iter() - .rev() - .find_map(|open_project_item| open_project_item(project, path, window, cx)) + let Some(open_project_item) = + self.build_project_item_for_path_fns + .iter() + .rev() + .find_map(|open_project_item| { + open_project_item(project, path, encoding.clone(), window, cx) + }) else { return Task::ready(Err(anyhow!("cannot open file {:?}", path.path))); }; @@ -3566,7 +3574,13 @@ impl Workspace { cx: &mut App, ) -> Task, WorkspaceItemBuilder)>> { let registry = cx.default_global::().clone(); - registry.open_path(self.project(), &path, window, cx) + registry.open_path( + self.project(), + &path, + Some(EncodingWrapper::new(*self.encoding.lock().unwrap())), + window, + cx, + ) } pub fn find_project_item( From d515ddd2ec9a098490e58da94f2020e33e519858 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sun, 5 Oct 2025 18:58:06 +0530 Subject: [PATCH 29/43] - Add optional encoding parameter to Worktree::load_file - Remove the parameter from `BufferStore::open_buffer` as it is not needed --- .../src/syntax_index.rs | 3 +- crates/languages/src/json.rs | 2 +- crates/project/src/buffer_store.rs | 21 +++++++++++++- crates/worktree/src/worktree.rs | 28 ++++++++++++++++--- crates/worktree/src/worktree_tests.rs | 4 +-- crates/zeta/src/zeta.rs | 2 +- 6 files changed, 49 insertions(+), 11 deletions(-) diff --git a/crates/edit_prediction_context/src/syntax_index.rs b/crates/edit_prediction_context/src/syntax_index.rs index 76aa10c076d95a..73bc58ec9a50dd 100644 --- a/crates/edit_prediction_context/src/syntax_index.rs +++ b/crates/edit_prediction_context/src/syntax_index.rs @@ -523,8 +523,7 @@ impl SyntaxIndex { }; let snapshot_task = worktree.update(cx, |worktree, cx| { - let load_task = worktree.load_file(&project_path.path, cx); - let worktree_abs_path = worktree.abs_path(); + let load_task = worktree.load_file(&project_path.path, None, cx); cx.spawn(async move |_this, cx| { let loaded_file = load_task.await?; let language = language.await?; diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 45fa2dd75cce05..9e1d76fb1fea6a 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -56,7 +56,7 @@ impl ContextProvider for JsonTaskProvider { cx.spawn(async move |cx| { let contents = file .worktree - .update(cx, |this, cx| this.load_file(&file.path, cx)) + .update(cx, |this, cx| this.load_file(&file.path, None, cx)) .ok()? .await .ok()?; diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 2d2adaee3c2032..04cf75b92e7dca 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -633,7 +633,26 @@ impl LocalBufferStore { worktree: Entity, cx: &mut Context, ) -> Task>> { - let load_file = worktree.update(cx, |worktree, cx| worktree.load_file(path.as_ref(), cx)); + let load_buffer = worktree.update(cx, |worktree, cx| { + let load_file = worktree.load_file(path.as_ref(), None, cx); + let reservation = cx.reserve_entity(); + let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64()); + let path = path.clone(); + cx.spawn(async move |_, cx| { + let loaded = load_file.await.with_context(|| { + format!("Could not open path: {}", path.display(PathStyle::local())) + })?; + let text_buffer = cx + .background_spawn(async move { + text::Buffer::new(ReplicaId::LOCAL, buffer_id, loaded.text) + }) + .await; + cx.insert_entity(reservation, |_| { + Buffer::build(text_buffer, Some(loaded.file), Capability::ReadWrite) + }) + }) + }); + cx.spawn(async move |this, cx| { let buffer = match load_buffer.await { Ok(buffer) => { diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 4b69787acf9c04..b8837950f1c80a 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -707,9 +707,14 @@ impl Worktree { } } - pub fn load_file(&self, path: &RelPath, cx: &Context) -> Task> { + pub fn load_file( + &self, + path: &Path, + encoding: Option>>, + cx: &Context, + ) -> Task> { match self { - Worktree::Local(this) => this.load_file(path, cx), + Worktree::Local(this) => this.load_file(path, encoding, cx), Worktree::Remote(_) => { Task::ready(Err(anyhow!("remote worktrees can't yet load files"))) } @@ -1316,7 +1321,12 @@ impl LocalWorktree { }) } - fn load_file(&self, path: &RelPath, cx: &Context) -> Task> { + fn load_file( + &self, + path: &Path, + encoding: Option>>, + cx: &Context, + ) -> Task> { let path = Arc::from(path); let abs_path = self.absolutize(&path); let fs = self.fs.clone(); @@ -1339,7 +1349,17 @@ impl LocalWorktree { anyhow::bail!("File is too large to load"); } } - let text = fs.load(&abs_path).await?; + let text = fs + .load_with_encoding( + &abs_path, + if let Some(encoding) = encoding { + EncodingWrapper::new(*encoding.lock().unwrap()) + } else { + EncodingWrapper::new(encoding_rs::UTF_8) + }, + false, + ) + .await?; let worktree = this.upgrade().context("worktree was dropped")?; let file = match entry.await? { diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index f69adc540452f4..35217692cba835 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -468,7 +468,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) { let prev_read_dir_count = fs.read_dir_call_count(); let loaded = tree .update(cx, |tree, cx| { - tree.load_file(rel_path("one/node_modules/b/b1.js"), cx) + tree.load_file("one/node_modules/b/b1.js".as_ref(), None, cx) }) .await .unwrap(); @@ -508,7 +508,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) { let prev_read_dir_count = fs.read_dir_call_count(); let loaded = tree .update(cx, |tree, cx| { - tree.load_file(rel_path("one/node_modules/a/a2.js"), cx) + tree.load_file("one/node_modules/a/a2.js".as_ref(), None, cx) }) .await .unwrap(); diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index ca2edd0682e181..a774cf0b162a7f 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -1986,7 +1986,7 @@ mod tests { .worktree_for_root_name("closed_source_worktree", cx) .unwrap(); worktree2.update(cx, |worktree2, cx| { - worktree2.load_file(rel_path("main.rs"), cx) + worktree2.load_file(Path::new("main.rs"), None, cx) }) }) .await From 0d3095a5d1b8e7942cc7518c4eebcb4d70ad0d83 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Mon, 6 Oct 2025 22:33:18 +0530 Subject: [PATCH 30/43] Clicking on `Choose another encoding` and selecting an encoding should now open the file in the chosen encoding if it is valid or show the invalid screen again if not. (UTF-16 files aren't being handled correctly as of now) --- crates/agent_ui/src/acp/message_editor.rs | 91 +++++++++++++++++++ crates/agent_ui/src/context.rs | 2 +- crates/fs/src/encodings.rs | 2 +- crates/project/src/buffer_store.rs | 14 +-- .../project/src/debugger/breakpoint_store.rs | 2 +- crates/project/src/lsp_store.rs | 2 +- .../src/lsp_store/rust_analyzer_ext.rs | 6 +- crates/project/src/project.rs | 7 +- crates/remote_server/src/headless_project.rs | 10 +- crates/workspace/src/workspace.rs | 1 + crates/worktree/src/worktree.rs | 6 +- 11 files changed, 124 insertions(+), 19 deletions(-) diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 856cc4d0d47d1e..c8b8d45c730c09 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -463,6 +463,97 @@ impl MessageEditor { }) } + fn confirm_mention_for_directory( + &mut self, + abs_path: PathBuf, + cx: &mut Context, + ) -> Task> { + fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc, PathBuf)> { + let mut files = Vec::new(); + + for entry in worktree.child_entries(path) { + if entry.is_dir() { + files.extend(collect_files_in_path(worktree, &entry.path)); + } else if entry.is_file() { + files.push((entry.path.clone(), worktree.full_path(&entry.path))); + } + } + + files + } + + let Some(project_path) = self + .project + .read(cx) + .project_path_for_absolute_path(&abs_path, cx) + else { + return Task::ready(Err(anyhow!("project path not found"))); + }; + let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else { + return Task::ready(Err(anyhow!("project entry not found"))); + }; + let directory_path = entry.path.clone(); + let worktree_id = project_path.worktree_id; + let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) else { + return Task::ready(Err(anyhow!("worktree not found"))); + }; + let project = self.project.clone(); + cx.spawn(async move |_, cx| { + let file_paths = worktree.read_with(cx, |worktree, _cx| { + collect_files_in_path(worktree, &directory_path) + })?; + let descendants_future = cx.update(|cx| { + join_all(file_paths.into_iter().map(|(worktree_path, full_path)| { + let rel_path = worktree_path + .strip_prefix(&directory_path) + .log_err() + .map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into()); + + let open_task = project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + let project_path = ProjectPath { + worktree_id, + path: worktree_path, + }; + buffer_store.open_buffer(project_path, cx) + }) + }); + + cx.spawn(async move |cx| { + let buffer = open_task.await.log_err()?; + let buffer_content = outline::get_buffer_content_or_outline( + buffer.clone(), + Some(&full_path), + &cx, + ) + .await + .ok()?; + + Some((rel_path, full_path, buffer_content.text, buffer)) + }) + })) + })?; + + let contents = cx + .background_spawn(async move { + let (contents, tracked_buffers) = descendants_future + .await + .into_iter() + .flatten() + .map(|(rel_path, full_path, rope, buffer)| { + ((rel_path, full_path, rope), buffer) + }) + .unzip(); + Mention::Text { + content: render_directory_contents(contents), + tracked_buffers, + } + }) + .await; + anyhow::Ok(contents) + }) + } + fn confirm_mention_for_fetch( &mut self, url: url::Url, diff --git a/crates/agent_ui/src/context.rs b/crates/agent_ui/src/context.rs index 2a1ff4a1d9d3e0..ff1864fba6d187 100644 --- a/crates/agent_ui/src/context.rs +++ b/crates/agent_ui/src/context.rs @@ -287,7 +287,7 @@ impl DirectoryContextHandle { let open_task = project.update(cx, |project, cx| { project.buffer_store().update(cx, |buffer_store, cx| { let project_path = ProjectPath { worktree_id, path }; - buffer_store.open_buffer(project_path, cx) + buffer_store.open_buffer(project_path, None, cx) }) }); diff --git a/crates/fs/src/encodings.rs b/crates/fs/src/encodings.rs index 780da0cadc5474..cbf3dece65e1fe 100644 --- a/crates/fs/src/encodings.rs +++ b/crates/fs/src/encodings.rs @@ -82,7 +82,7 @@ impl EncodingWrapper { } else { // If there were decoding errors, return an error. Err(anyhow::anyhow!( - "The file contains invalid bytes for the specified encoding: {}. This usually menas that the file is not a regular text file, or is encoded in a different encoding. Continuing to open it may result in data loss if saved.", + "The file contains invalid bytes for the specified encoding: {}.\nThis usually means that the file is not a regular text file, or is encoded in a different encoding.\nContinuing to open it may result in data loss if saved.", self.0.name() )) } diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 04cf75b92e7dca..d1a71fc4bc9ccc 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -9,6 +9,7 @@ use client::Client; use collections::{HashMap, HashSet, hash_map}; use fs::Fs; use futures::{Future, FutureExt as _, StreamExt, channel::oneshot, future::Shared}; +use fs::{Fs, encodings::EncodingWrapper}; use gpui::{ App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, }; @@ -631,11 +632,13 @@ impl LocalBufferStore { &self, path: Arc, worktree: Entity, + encoding: Option, cx: &mut Context, ) -> Task>> { let load_buffer = worktree.update(cx, |worktree, cx| { - let load_file = worktree.load_file(path.as_ref(), None, cx); + let load_file = worktree.load_file(path.as_ref(), encoding, cx); let reservation = cx.reserve_entity(); + let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64()); let path = path.clone(); cx.spawn(async move |_, cx| { @@ -655,11 +658,7 @@ impl LocalBufferStore { cx.spawn(async move |this, cx| { let buffer = match load_buffer.await { - Ok(buffer) => { - // Reload the buffer to trigger UTF-16 detection - buffer.update(cx, |buffer, cx| buffer.reload(cx))?.await?; - Ok(buffer) - } + Ok(buffer) => Ok(buffer), Err(error) if is_not_found_error(&error) => cx.new(|cx| { let buffer_id = BufferId::from(cx.entity_id().as_non_zero_u64()); let text_buffer = text::Buffer::new( @@ -834,6 +833,7 @@ impl BufferStore { pub fn open_buffer( &mut self, project_path: ProjectPath, + encoding: Option, cx: &mut Context, ) -> Task>> { if let Some(buffer) = self.get_by_path(&project_path) { @@ -857,7 +857,7 @@ impl BufferStore { return Task::ready(Err(anyhow!("no such worktree"))); }; let load_buffer = match &self.state { - BufferStoreState::Local(this) => this.open_buffer(path, worktree, cx), + BufferStoreState::Local(this) => this.open_buffer(path, worktree, encoding, cx), BufferStoreState::Remote(this) => this.open_buffer(path, worktree, cx), }; diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index 42663ab9852a5d..2791d6bfe1f322 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -796,7 +796,7 @@ impl BreakpointStore { worktree_id: worktree.read(cx).id(), path: relative_path, }; - this.open_buffer(path, cx) + this.open_buffer(path, None, cx) })? .await; let Ok(buffer) = buffer else { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 762070796f068f..734fac8ce7a569 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -8336,7 +8336,7 @@ impl LspStore { lsp_store .update(cx, |lsp_store, cx| { lsp_store.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(project_path, cx) + buffer_store.open_buffer(project_path, None, cx) }) })? .await diff --git a/crates/project/src/lsp_store/rust_analyzer_ext.rs b/crates/project/src/lsp_store/rust_analyzer_ext.rs index 4d5f134e5f1682..a8a6b06aafe1df 100644 --- a/crates/project/src/lsp_store/rust_analyzer_ext.rs +++ b/crates/project/src/lsp_store/rust_analyzer_ext.rs @@ -91,7 +91,7 @@ pub fn cancel_flycheck( let buffer = buffer_path.map(|buffer_path| { project.update(cx, |project, cx| { project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) + buffer_store.open_buffer(buffer_path, None, cx) }) }) }); @@ -140,7 +140,7 @@ pub fn run_flycheck( let buffer = buffer_path.map(|buffer_path| { project.update(cx, |project, cx| { project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) + buffer_store.open_buffer(buffer_path, None, cx) }) }) }); @@ -198,7 +198,7 @@ pub fn clear_flycheck( let buffer = buffer_path.map(|buffer_path| { project.update(cx, |project, cx| { project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) + buffer_store.open_buffer(buffer_path, None, cx) }) }) }); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 044c1501064bdb..04a3c5680dd9f6 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -28,6 +28,7 @@ use buffer_diff::BufferDiff; use context_server_store::ContextServerStore; use encoding_rs::Encoding; pub use environment::ProjectEnvironmentEvent; +use fs::encodings::EncodingWrapper; use git::repository::get_git_committer; use git_store::{Repository, RepositoryId}; pub mod search_history; @@ -2717,7 +2718,11 @@ impl Project { } self.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.open_buffer(path.into(), cx) + buffer_store.open_buffer( + path.into(), + Some(EncodingWrapper::new(self.encoding.lock().as_ref().unwrap())), + cx, + ) }) } diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 5d50853601b394..da02713a5f4ea1 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -506,7 +506,14 @@ impl HeadlessProject { let (buffer_store, buffer) = this.update(&mut cx, |this, cx| { let buffer_store = this.buffer_store.clone(); let buffer = this.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.open_buffer(ProjectPath { worktree_id, path }, cx) + buffer_store.open_buffer( + ProjectPath { + worktree_id, + path: Arc::::from_proto(message.payload.path), + }, + None, + cx, + ) }); anyhow::Ok((buffer_store, buffer)) })??; @@ -597,6 +604,7 @@ impl HeadlessProject { worktree_id: worktree.read(cx).id(), path: path, }, + None, cx, ) }); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d39864abdb1409..79d446f85c3127 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3416,6 +3416,7 @@ impl Workspace { window: &mut Window, cx: &mut Context, ) -> Task>> { + println!("{:?}", *self.encoding.lock().unwrap()); cx.spawn_in(window, async move |workspace, cx| { let open_paths_task_result = workspace .update_in(cx, |workspace, window, cx| { diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index b8837950f1c80a..7be34dd9a2650f 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -710,7 +710,7 @@ impl Worktree { pub fn load_file( &self, path: &Path, - encoding: Option>>, + encoding: Option, cx: &Context, ) -> Task> { match self { @@ -1324,7 +1324,7 @@ impl LocalWorktree { fn load_file( &self, path: &Path, - encoding: Option>>, + encoding: Option, cx: &Context, ) -> Task> { let path = Arc::from(path); @@ -1353,7 +1353,7 @@ impl LocalWorktree { .load_with_encoding( &abs_path, if let Some(encoding) = encoding { - EncodingWrapper::new(*encoding.lock().unwrap()) + encoding } else { EncodingWrapper::new(encoding_rs::UTF_8) }, From b629b1f9ab34ed3e0f0bcc7c30cb960beb13be5f Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sun, 12 Oct 2025 16:45:30 +0530 Subject: [PATCH 31/43] Enable a file to be opened with an invalid encoding with the invalid bytes replaced with replacement characters - Fix UTF-16 file handling - Introduce a `ForceOpen` action to allow users to open files despite encoding errors - Add `force` and `detect_utf16` flags - Update UI to provide "Accept the Risk and Open" button for invalid encoding files --- Cargo.lock | 2 + crates/agent_ui/src/acp/message_editor.rs | 2 +- crates/agent_ui/src/context.rs | 2 +- crates/copilot/src/copilot.rs | 3 +- .../src/syntax_index.rs | 2 +- crates/encodings/Cargo.toml | 2 + crates/encodings/src/lib.rs | 40 +++++- crates/encodings/src/selectors.rs | 120 ++++++++++++------ crates/fs/src/encodings.rs | 54 +++++--- crates/fs/src/fs.rs | 13 +- crates/language/src/buffer.rs | 30 ++++- crates/languages/src/json.rs | 4 +- crates/project/src/buffer_store.rs | 57 ++++++--- .../project/src/debugger/breakpoint_store.rs | 2 +- crates/project/src/invalid_item_view.rs | 56 +++++--- crates/project/src/lsp_store.rs | 2 +- .../src/lsp_store/rust_analyzer_ext.rs | 6 +- crates/project/src/project.rs | 38 +++++- crates/remote_server/src/headless_project.rs | 4 + crates/workspace/src/workspace.rs | 27 +++- crates/worktree/src/worktree.rs | 18 ++- crates/worktree/src/worktree_tests.rs | 113 ++++++++++++++++- crates/zed_actions/src/lib.rs | 3 + crates/zeta/src/zeta.rs | 2 +- 24 files changed, 477 insertions(+), 125 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0fac9a1f665bd4..119769b58ad3c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5529,6 +5529,8 @@ version = "0.1.0" dependencies = [ "editor", "encoding_rs", + "fs", + "futures 0.3.31", "fuzzy", "gpui", "language", diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index c8b8d45c730c09..50ac5848f71dfd 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -515,7 +515,7 @@ impl MessageEditor { worktree_id, path: worktree_path, }; - buffer_store.open_buffer(project_path, cx) + buffer_store.open_buffer(project_path, None, false, true, cx) }) }); diff --git a/crates/agent_ui/src/context.rs b/crates/agent_ui/src/context.rs index ff1864fba6d187..db98461bea213b 100644 --- a/crates/agent_ui/src/context.rs +++ b/crates/agent_ui/src/context.rs @@ -287,7 +287,7 @@ impl DirectoryContextHandle { let open_task = project.update(cx, |project, cx| { project.buffer_store().update(cx, |buffer_store, cx| { let project_path = ProjectPath { worktree_id, path }; - buffer_store.open_buffer(project_path, None, cx) + buffer_store.open_buffer(project_path, None, false, true, cx) }) }); diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 1ca01038e28d3c..e5dca4d7b48be3 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1242,6 +1242,7 @@ async fn get_copilot_lsp(fs: Arc, node_runtime: NodeRuntime) -> anyhow:: mod tests { use super::*; use fs::encodings::EncodingWrapper; + use encoding_rs::Encoding; use gpui::TestAppContext; use util::{path, paths::PathStyle, rel_path::rel_path}; @@ -1452,7 +1453,7 @@ mod tests { self.abs_path.clone() } - fn load(&self, _: &App, _: EncodingWrapper, _: bool) -> Task> { + fn load(&self, _: &App, _: EncodingWrapper, _: bool, _: bool, _: Option>>) -> Task> { unimplemented!() } diff --git a/crates/edit_prediction_context/src/syntax_index.rs b/crates/edit_prediction_context/src/syntax_index.rs index 73bc58ec9a50dd..1353b0ea289f1a 100644 --- a/crates/edit_prediction_context/src/syntax_index.rs +++ b/crates/edit_prediction_context/src/syntax_index.rs @@ -523,7 +523,7 @@ impl SyntaxIndex { }; let snapshot_task = worktree.update(cx, |worktree, cx| { - let load_task = worktree.load_file(&project_path.path, None, cx); + let load_task = worktree.load_file(&project_path.path, None, false, true, None, cx); cx.spawn(async move |_this, cx| { let loaded_file = load_task.await?; let language = language.await?; diff --git a/crates/encodings/Cargo.toml b/crates/encodings/Cargo.toml index c8012b77567428..341c8cfa8f0782 100644 --- a/crates/encodings/Cargo.toml +++ b/crates/encodings/Cargo.toml @@ -7,6 +7,8 @@ edition.workspace = true [dependencies] editor.workspace = true encoding_rs.workspace = true +fs.workspace = true +futures.workspace = true fuzzy.workspace = true gpui.workspace = true language.workspace = true diff --git a/crates/encodings/src/lib.rs b/crates/encodings/src/lib.rs index 4d7256db6f3b0b..86576212ec47cf 100644 --- a/crates/encodings/src/lib.rs +++ b/crates/encodings/src/lib.rs @@ -7,8 +7,12 @@ use gpui::{ClickEvent, Entity, Subscription, WeakEntity}; use language::Buffer; use ui::{App, Button, ButtonCommon, Context, LabelSize, Render, Tooltip, Window, div}; use ui::{Clickable, ParentElement}; -use workspace::{ItemHandle, StatusItemView, Workspace, with_active_or_new_workspace}; -use zed_actions::encodings::Toggle; +use util::ResultExt; +use workspace::{ + CloseActiveItem, ItemHandle, OpenOptions, StatusItemView, Workspace, + with_active_or_new_workspace, +}; +use zed_actions::encodings::{ForceOpen, Toggle}; use crate::selectors::encoding::EncodingSelector; use crate::selectors::save_or_reopen::EncodingSaveOrReopenSelector; @@ -304,4 +308,36 @@ pub fn init(cx: &mut App) { }); }); }); + + cx.on_action(|action: &ForceOpen, cx: &mut App| { + let ForceOpen(path) = action.clone(); + let path = path.to_path_buf(); + + with_active_or_new_workspace(cx, |workspace, window, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.close_active_item(&CloseActiveItem::default(), window, cx) + .detach(); + }); + + { + let force = workspace.encoding_options.force.get_mut(); + + *force = true; + } + + let open_task = workspace.open_abs_path(path, OpenOptions::default(), window, cx); + let weak_workspace = workspace.weak_handle(); + + cx.spawn(async move |_, cx| { + let workspace = weak_workspace.upgrade().unwrap(); + open_task.await.log_err(); + workspace + .update(cx, |workspace: &mut Workspace, _| { + *workspace.encoding_options.force.get_mut() = false; + }) + .log_err(); + }) + .detach(); + }); + }); } diff --git a/crates/encodings/src/selectors.rs b/crates/encodings/src/selectors.rs index b88f882e31955c..f3345c57299d6a 100644 --- a/crates/encodings/src/selectors.rs +++ b/crates/encodings/src/selectors.rs @@ -1,6 +1,6 @@ /// 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. +/// or reopening with a different encoding. pub mod save_or_reopen { use editor::Editor; use gpui::Styled; @@ -277,10 +277,14 @@ 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 editor::Editor; + use fs::encodings::EncodingWrapper; use std::{path::PathBuf, sync::atomic::AtomicBool}; use fuzzy::{StringMatch, StringMatchCandidate}; - use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, WeakEntity}; + use gpui::{ + AppContext, DismissEvent, Entity, EventEmitter, Focusable, WeakEntity, http_client::anyhow, + }; use language::Buffer; use picker::{Picker, PickerDelegate}; use ui::{ @@ -288,7 +292,7 @@ pub mod encoding { Window, rems, v_flex, }; use util::{ResultExt, TryFutureExt}; - use workspace::{ModalView, Workspace}; + use workspace::{CloseActiveItem, ModalView, OpenOptions, Workspace}; use crate::encoding_from_name; @@ -436,50 +440,84 @@ pub mod encoding { .unwrap(); if let Some(buffer) = &self.buffer - && let Some(buffer) = buffer.upgrade() + && let Some(buffer_entity) = buffer.upgrade() { - buffer.update(cx, |buffer, cx| { + let buffer = buffer_entity.read(cx); + + // Since the encoding will be accessed in `reload`, + // the lock must be released before calling `reload`. + // By limiting the scope, we ensure that it is released + { let buffer_encoding = buffer.encoding.clone(); - let buffer_encoding = &mut *buffer_encoding.lock().unwrap(); - *buffer_encoding = + *buffer_encoding.lock().unwrap() = 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(); - - executor - .spawn(workspace.update(cx, |workspace, cx| { - workspace - .save_active_item(workspace::SaveIntent::Save, window, cx) - .log_err() - })) - .detach(); - } - }); + } + + self.dismissed(window, cx); + + if self.action == Action::Reopen { + buffer_entity.update(cx, |buffer, cx| { + let rec = buffer.reload(cx); + cx.spawn(async move |_, _| rec.await).detach() + }); + } else if self.action == Action::Save { + let task = workspace.update(cx, |workspace, cx| { + workspace + .save_active_item(workspace::SaveIntent::Save, window, cx) + .log_err() + }); + cx.spawn(async |_, _| task).detach(); + } } else { - workspace.update(cx, |workspace, cx| { - *workspace.encoding.lock().unwrap() = + if let Some(path) = self.selector.upgrade().unwrap().read(cx).path.clone() { + workspace.update(cx, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.close_active_item(&CloseActiveItem::default(), window, cx) + .detach(); + }); + }); + + let encoding = encoding_from_name(self.matches[self.current_selection].string.as_str()); - workspace - .open_abs_path( - self.selector - .upgrade() - .unwrap() - .read(cx) - .path - .as_ref() - .unwrap() - .clone(), - Default::default(), - window, - cx, - ) - .detach(); - }) + + let open_task = workspace.update(cx, |workspace, cx| { + *workspace.encoding_options.encoding.lock().unwrap() = + EncodingWrapper::new(encoding); + + workspace.open_abs_path(path, OpenOptions::default(), window, cx) + }); + + cx.spawn(async move |_, cx| { + if let Ok(_) = { + let result = open_task.await; + workspace + .update(cx, |workspace, _| { + *workspace.encoding_options.force.get_mut() = false; + }) + .unwrap(); + + result + } && let Ok(Ok((_, buffer, _))) = + workspace.read_with(cx, |workspace, cx| { + if let Some(active_item) = workspace.active_item(cx) + && let Some(editor) = active_item.act_as::(cx) + { + Ok(editor.read(cx).active_excerpt(cx).unwrap()) + } else { + Err(anyhow!("error")) + } + }) + { + buffer + .read_with(cx, |buffer, _| { + *buffer.encoding.lock().unwrap() = encoding; + }) + .log_err(); + } + }) + .detach(); + } } - self.dismissed(window, cx); } fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { diff --git a/crates/fs/src/encodings.rs b/crates/fs/src/encodings.rs index cbf3dece65e1fe..53dcca407a339f 100644 --- a/crates/fs/src/encodings.rs +++ b/crates/fs/src/encodings.rs @@ -4,6 +4,8 @@ use std::{ sync::{Arc, Mutex}, }; +use std::sync::atomic::AtomicBool; + use anyhow::Result; use encoding_rs::Encoding; @@ -25,8 +27,6 @@ impl Default for EncodingWrapper { } } -pub struct EncodingWrapperVisitor; - impl PartialEq for EncodingWrapper { fn eq(&self, other: &Self) -> bool { self.0.name() == other.0.name() @@ -55,11 +55,13 @@ impl EncodingWrapper { &mut self, input: Vec, force: bool, + detect_utf16: bool, buffer_encoding: Option>>, ) -> Result { - // Check if the input starts with a BOM for UTF-16 encodings only if not forced to - // use the encoding specified. - if !force { + // Check if the input starts with a BOM for UTF-16 encodings only if detect_utf16 is true. + println!("{}", force); + println!("{}", detect_utf16); + if detect_utf16 { if let Some(encoding) = match input.get(..2) { Some([0xFF, 0xFE]) => Some(encoding_rs::UTF_16LE), Some([0xFE, 0xFF]) => Some(encoding_rs::UTF_16BE), @@ -67,20 +69,23 @@ impl EncodingWrapper { } { self.0 = encoding; - if let Some(v) = buffer_encoding { - if let Ok(mut v) = (*v).lock() { - *v = encoding; - } + if let Some(v) = buffer_encoding + && let Ok(mut v) = v.lock() + { + *v = encoding; } } } - let (cow, _had_errors) = self.0.decode_with_bom_removal(&input); + let (cow, had_errors) = self.0.decode_with_bom_removal(&input); + + if force { + return Ok(cow.to_string()); + } - if !_had_errors { + if !had_errors { Ok(cow.to_string()) } else { - // If there were decoding errors, return an error. Err(anyhow::anyhow!( "The file contains invalid bytes for the specified encoding: {}.\nThis usually means that the file is not a regular text file, or is encoded in a different encoding.\nContinuing to open it may result in data loss if saved.", self.0.name() @@ -107,9 +112,7 @@ impl EncodingWrapper { return Ok(data); } else { let (cow, _encoding_used, _had_errors) = self.0.encode(&input); - // `encoding_rs` handles unencodable characters by replacing them with - // appropriate substitutes in the output, so we return the result even if there were errors. - // This maintains consistency with the decode behaviour. + Ok(cow.into_owned()) } } @@ -120,12 +123,31 @@ pub async fn to_utf8( input: Vec, mut encoding: EncodingWrapper, force: bool, + detect_utf16: bool, buffer_encoding: Option>>, ) -> Result { - encoding.decode(input, force, buffer_encoding).await + encoding + .decode(input, force, detect_utf16, buffer_encoding) + .await } /// Convert a UTF-8 string to a byte vector in a specified encoding. pub async fn from_utf8(input: String, target: EncodingWrapper) -> Result> { target.encode(input).await } + +pub struct EncodingOptions { + pub encoding: Arc>, + pub force: AtomicBool, + pub detect_utf16: AtomicBool, +} + +impl Default for EncodingOptions { + fn default() -> Self { + EncodingOptions { + encoding: Arc::new(Mutex::new(EncodingWrapper::default())), + force: AtomicBool::new(false), + detect_utf16: AtomicBool::new(true), + } + } +} diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 403602bb623572..344d9da893b0fb 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -62,9 +62,9 @@ use std::ffi::OsStr; #[cfg(any(test, feature = "test-support"))] pub use fake_git_repo::{LOAD_HEAD_TEXT_TASK, LOAD_INDEX_TEXT_TASK}; -use crate::encodings::to_utf8; use crate::encodings::EncodingWrapper; use crate::encodings::from_utf8; +use crate::encodings::to_utf8; pub trait Watcher: Send + Sync { fn add(&self, path: &Path) -> Result<()>; @@ -125,9 +125,18 @@ pub trait Fs: Send + Sync { &self, path: &Path, encoding: EncodingWrapper, + force: bool, detect_utf16: bool, + buffer_encoding: Option>>, ) -> Result { - Ok(to_utf8(self.load_bytes(path).await?, encoding, detect_utf16, None).await?) + Ok(to_utf8( + self.load_bytes(path).await?, + encoding, + force, + detect_utf16, + buffer_encoding, + ) + .await?) } async fn load_bytes(&self, path: &Path) -> Result>; diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 029619a4ad7cc6..5daac9765f6e9a 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -414,8 +414,14 @@ pub trait LocalFile: File { fn abs_path(&self, cx: &App) -> PathBuf; /// Loads the file contents from disk and returns them as a UTF-8 encoded string. - fn load(&self, cx: &App, encoding: EncodingWrapper, detect_utf16: bool) - -> Task>; + fn load( + &self, + cx: &App, + encoding: EncodingWrapper, + force: bool, + detect_utf16: bool, + buffer_encoding: Option>>, + ) -> Task>; /// Loads the file's contents from disk. fn load_bytes(&self, cx: &App) -> Task>>; @@ -842,6 +848,18 @@ impl Buffer { ) } + /// Replace the text buffer. This function is in contrast to `set_text` in that it does not + /// change the buffer's editing state + pub fn replace_text_buffer(&mut self, new: TextBuffer, cx: &mut Context) { + self.text = new; + self.saved_version = self.version.clone(); + self.has_unsaved_edits.set((self.version.clone(), false)); + + self.was_changed(); + cx.emit(BufferEvent::DirtyChanged); + cx.notify(); + } + /// Create a new buffer with the given base text that has proper line endings and other normalization applied. pub fn local_normalized( base_text_normalized: Rope, @@ -1346,13 +1364,14 @@ impl Buffer { pub fn reload(&mut self, cx: &Context) -> oneshot::Receiver> { let (tx, rx) = futures::channel::oneshot::channel(); let encoding = EncodingWrapper::new(*(self.encoding.lock().unwrap())); + let buffer_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, encoding, false) + file.load(cx, encoding, false, true, Some(buffer_encoding)) })) })? else { @@ -1406,6 +1425,9 @@ impl Buffer { cx.notify(); } + pub fn replace_file(&mut self, new_file: Arc) { + self.file = Some(new_file); + } /// Updates the [`File`] backing this buffer. This should be called when /// the file has changed or has been deleted. pub fn file_updated(&mut self, new_file: Arc, cx: &mut Context) { @@ -5231,7 +5253,9 @@ impl LocalFile for TestFile { &self, _cx: &App, _encoding: EncodingWrapper, + _force: bool, _detect_utf16: bool, + _buffer_encoding: Option>>, ) -> Task> { unimplemented!() } diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 9e1d76fb1fea6a..4d6894d3838e8e 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -56,7 +56,9 @@ impl ContextProvider for JsonTaskProvider { cx.spawn(async move |cx| { let contents = file .worktree - .update(cx, |this, cx| this.load_file(&file.path, None, cx)) + .update(cx, |this, cx| { + this.load_file(&file.path, None, false, true, None, cx) + }) .ok()? .await .ok()?; diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index d1a71fc4bc9ccc..1919f9b0a8efbd 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -633,26 +633,47 @@ impl LocalBufferStore { path: Arc, worktree: Entity, encoding: Option, + force: bool, + detect_utf16: bool, cx: &mut Context, ) -> Task>> { let load_buffer = worktree.update(cx, |worktree, cx| { - let load_file = worktree.load_file(path.as_ref(), encoding, cx); let reservation = cx.reserve_entity(); - let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64()); - let path = path.clone(); - cx.spawn(async move |_, cx| { - let loaded = load_file.await.with_context(|| { - format!("Could not open path: {}", path.display(PathStyle::local())) + + // Create the buffer first + let buffer = cx.insert_entity(reservation, |_| { + Buffer::build( + text::Buffer::new(0, buffer_id, ""), + None, + Capability::ReadWrite, + ) + }); + + let buffer_encoding = buffer.read(cx).encoding.clone(); + + let load_file_task = worktree.load_file( + path.as_ref(), + encoding, + force, + detect_utf16, + Some(buffer_encoding), + cx, + ); + + cx.spawn(async move |_, async_cx| { + let loaded_file = load_file_task.await?; + let mut reload_task = None; + + buffer.update(async_cx, |buffer, cx| { + buffer.replace_file(loaded_file.file); + buffer + .replace_text_buffer(text::Buffer::new(0, buffer_id, loaded_file.text), cx); + + reload_task = Some(buffer.reload(cx)); })?; - let text_buffer = cx - .background_spawn(async move { - text::Buffer::new(ReplicaId::LOCAL, buffer_id, loaded.text) - }) - .await; - cx.insert_entity(reservation, |_| { - Buffer::build(text_buffer, Some(loaded.file), Capability::ReadWrite) - }) + + Ok(buffer) }) }); @@ -834,6 +855,8 @@ impl BufferStore { &mut self, project_path: ProjectPath, encoding: Option, + force: bool, + detect_utf16: bool, cx: &mut Context, ) -> Task>> { if let Some(buffer) = self.get_by_path(&project_path) { @@ -857,7 +880,9 @@ impl BufferStore { return Task::ready(Err(anyhow!("no such worktree"))); }; let load_buffer = match &self.state { - BufferStoreState::Local(this) => this.open_buffer(path, worktree, encoding, cx), + BufferStoreState::Local(this) => { + this.open_buffer(path, worktree, encoding, force, detect_utf16, cx) + } BufferStoreState::Remote(this) => this.open_buffer(path, worktree, cx), }; @@ -1170,7 +1195,7 @@ impl BufferStore { let buffers = this.update(cx, |this, cx| { project_paths .into_iter() - .map(|project_path| this.open_buffer(project_path, cx)) + .map(|project_path| this.open_buffer(project_path, None, cx)) .collect::>() })?; for buffer_task in buffers { diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index 2791d6bfe1f322..aad1edab307124 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -796,7 +796,7 @@ impl BreakpointStore { worktree_id: worktree.read(cx).id(), path: relative_path, }; - this.open_buffer(path, None, cx) + this.open_buffer(path, None, false, true, cx) })? .await; let Ok(buffer) = buffer else { diff --git a/crates/project/src/invalid_item_view.rs b/crates/project/src/invalid_item_view.rs index 92952a788b6fd2..252ea9673244e0 100644 --- a/crates/project/src/invalid_item_view.rs +++ b/crates/project/src/invalid_item_view.rs @@ -4,7 +4,7 @@ use gpui::{EventEmitter, FocusHandle, Focusable}; use ui::{ App, Button, ButtonCommon, ButtonStyle, Clickable, Context, FluentBuilder, InteractiveElement, KeyBinding, Label, LabelCommon, LabelSize, ParentElement, Render, SharedString, Styled as _, - Window, h_flex, v_flex, + TintColor, Window, h_flex, v_flex, }; use zed_actions::workspace::OpenWithSystem; @@ -78,7 +78,8 @@ impl Focusable for InvalidItemView { impl Render for InvalidItemView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { let abs_path = self.abs_path.clone(); - let path = self.abs_path.clone(); + let path0 = self.abs_path.clone(); + let path1 = self.abs_path.clone(); v_flex() .size_full() @@ -115,23 +116,44 @@ impl Render for InvalidItemView { ), ) .child( - h_flex().justify_center().child( - Button::new( - "open-with-encoding", - "Open With a Different Encoding", + h_flex() + .justify_center() + .child( + Button::new( + "open-with-encoding", + "Open With a Different Encoding", + ) + .style(ButtonStyle::Outlined) + .on_click( + move |_, window, cx| { + window.dispatch_action( + Box::new(zed_actions::encodings::Toggle( + path0.clone(), + )), + cx, + ) + }, + ), ) - .style(ButtonStyle::Outlined) - .on_click( - move |_, window, cx| { - window.dispatch_action( - Box::new(zed_actions::encodings::Toggle( - path.clone(), - )), - cx, - ) - }, + .child( + Button::new( + "accept-risk-and-open", + "Accept the Risk and Open", + ) + .style(ButtonStyle::Tinted(TintColor::Warning)) + .on_click( + move |_, window, cx| { + window.dispatch_action( + Box::new( + zed_actions::encodings::ForceOpen( + path1.clone(), + ), + ), + cx, + ); + }, + ), ), - ), ) }), ), diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 734fac8ce7a569..4c70c999b7bfed 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -8336,7 +8336,7 @@ impl LspStore { lsp_store .update(cx, |lsp_store, cx| { lsp_store.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(project_path, None, cx) + buffer_store.open_buffer(project_path, None, false, true,cx) }) })? .await diff --git a/crates/project/src/lsp_store/rust_analyzer_ext.rs b/crates/project/src/lsp_store/rust_analyzer_ext.rs index a8a6b06aafe1df..cdfd9e63cfac36 100644 --- a/crates/project/src/lsp_store/rust_analyzer_ext.rs +++ b/crates/project/src/lsp_store/rust_analyzer_ext.rs @@ -91,7 +91,7 @@ pub fn cancel_flycheck( let buffer = buffer_path.map(|buffer_path| { project.update(cx, |project, cx| { project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, None, cx) + buffer_store.open_buffer(buffer_path, None, false, true, cx) }) }) }); @@ -140,7 +140,7 @@ pub fn run_flycheck( let buffer = buffer_path.map(|buffer_path| { project.update(cx, |project, cx| { project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, None, cx) + buffer_store.open_buffer(buffer_path, None, false, true, cx) }) }) }); @@ -198,7 +198,7 @@ pub fn clear_flycheck( let buffer = buffer_path.map(|buffer_path| { project.update(cx, |project, cx| { project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, None, cx) + buffer_store.open_buffer(buffer_path, None, false, true, cx) }) }) }); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 04a3c5680dd9f6..cf3750462f583c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -28,9 +28,12 @@ use buffer_diff::BufferDiff; use context_server_store::ContextServerStore; use encoding_rs::Encoding; pub use environment::ProjectEnvironmentEvent; +use fs::encodings::EncodingOptions; use fs::encodings::EncodingWrapper; use git::repository::get_git_committer; use git_store::{Repository, RepositoryId}; +use std::sync::atomic::AtomicBool; + pub mod search_history; mod yarn; @@ -108,6 +111,7 @@ use settings::{InvalidSettingsError, Settings, SettingsLocation, SettingsStore}; use smol::channel::Receiver; use snippet::Snippet; use snippet_provider::SnippetProvider; +use std::ops::Deref; use std::{ borrow::Cow, collections::BTreeMap, @@ -217,7 +221,7 @@ pub struct Project { settings_observer: Entity, toolchain_store: Option>, agent_location: Option, - pub encoding: Arc>, + pub encoding_options: EncodingOptions, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -1228,7 +1232,11 @@ impl Project { toolchain_store: Some(toolchain_store), agent_location: None, - encoding: Arc::new(std::sync::Mutex::new(encoding_rs::UTF_8)), + encoding_options: EncodingOptions { + encoding: Arc::new(std::sync::Mutex::new(EncodingWrapper::default())), + force: AtomicBool::new(false), + detect_utf16: AtomicBool::new(true), + }, } }) } @@ -1414,7 +1422,11 @@ impl Project { toolchain_store: Some(toolchain_store), agent_location: None, - encoding: Arc::new(std::sync::Mutex::new(encoding_rs::UTF_8)), + encoding_options: EncodingOptions { + encoding: Arc::new(std::sync::Mutex::new(EncodingWrapper::default())), + force: AtomicBool::new(false), + detect_utf16: AtomicBool::new(false), + }, }; // remote server -> local machine handlers @@ -1668,8 +1680,14 @@ impl Project { remotely_created_models: Arc::new(Mutex::new(RemotelyCreatedModels::default())), toolchain_store: None, agent_location: None, - encoding: Arc::new(std::sync::Mutex::new(encoding_rs::UTF_8)), + encoding_options: EncodingOptions { + encoding: Arc::new(std::sync::Mutex::new(EncodingWrapper::default())), + + force: AtomicBool::new(false), + detect_utf16: AtomicBool::new(false), + }, }; + project.set_role(role, cx); for worktree in worktrees { project.add_worktree(&worktree, cx); @@ -2720,7 +2738,17 @@ impl Project { self.buffer_store.update(cx, |buffer_store, cx| { buffer_store.open_buffer( path.into(), - Some(EncodingWrapper::new(self.encoding.lock().as_ref().unwrap())), + Some( + self.encoding_options + .encoding + .lock() + .as_ref() + .unwrap() + .deref() + .clone(), + ), + *self.encoding_options.force.get_mut(), + *self.encoding_options.detect_utf16.get_mut(), cx, ) }) diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index da02713a5f4ea1..cc9cf0225a13a7 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -512,6 +512,8 @@ impl HeadlessProject { path: Arc::::from_proto(message.payload.path), }, None, + false, + true, cx, ) }); @@ -605,6 +607,8 @@ impl HeadlessProject { path: path, }, None, + false, + true, cx, ) }); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 79d446f85c3127..50683242333a8f 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -19,7 +19,6 @@ mod workspace_settings; pub use crate::notifications::NotificationFrame; pub use dock::Panel; -use encoding_rs::Encoding; use encoding_rs::UTF_8; use fs::encodings::EncodingWrapper; pub use path_list::PathList; @@ -33,6 +32,8 @@ use client::{ }; use collections::{HashMap, HashSet, hash_map}; use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE}; +use fs::encodings::EncodingOptions; + use futures::{ Future, FutureExt, StreamExt, channel::{ @@ -650,7 +651,7 @@ impl ProjectItemRegistry { let project_path = project_path.clone(); let EncodingWrapper(encoding) = encoding.unwrap_or_default(); - project.update(cx, |project, _| {*project.encoding.lock().unwrap() = encoding}); + project.update(cx, |project, _| {*project.encoding_options.encoding.lock().unwrap() = EncodingWrapper::new(encoding)}); let is_file = project .read(cx) @@ -1190,7 +1191,7 @@ pub struct Workspace { session_id: Option, scheduled_tasks: Vec>, last_open_dock_positions: Vec, - pub encoding: Arc>, + pub encoding_options: EncodingOptions, } impl EventEmitter for Workspace {} @@ -1533,7 +1534,7 @@ impl Workspace { session_id: Some(session_id), scheduled_tasks: Vec::new(), last_open_dock_positions: Vec::new(), - encoding: Arc::new(std::sync::Mutex::new(encoding_rs::UTF_8)), + encoding_options: Default::default(), } } @@ -3416,7 +3417,6 @@ impl Workspace { window: &mut Window, cx: &mut Context, ) -> Task>> { - println!("{:?}", *self.encoding.lock().unwrap()); cx.spawn_in(window, async move |workspace, cx| { let open_paths_task_result = workspace .update_in(cx, |workspace, window, cx| { @@ -3574,11 +3574,24 @@ impl Workspace { window: &mut Window, cx: &mut App, ) -> Task, WorkspaceItemBuilder)>> { + let project = self.project(); + + project.update(cx, |project, _| { + project.encoding_options.force.store( + self.encoding_options + .force + .load(std::sync::atomic::Ordering::Relaxed), + std::sync::atomic::Ordering::Relaxed, + ); + }); + let registry = cx.default_global::().clone(); registry.open_path( - self.project(), + project, &path, - Some(EncodingWrapper::new(*self.encoding.lock().unwrap())), + Some(EncodingWrapper::new( + (self.encoding_options.encoding.lock().unwrap()).0, + )), window, cx, ) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 7be34dd9a2650f..e38a960c18ba9d 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -711,10 +711,15 @@ impl Worktree { &self, path: &Path, encoding: Option, + force: bool, + detect_utf16: bool, + buffer_encoding: Option>>, cx: &Context, ) -> Task> { match self { - Worktree::Local(this) => this.load_file(path, encoding, cx), + Worktree::Local(this) => { + this.load_file(path, encoding, force, detect_utf16, buffer_encoding, cx) + } Worktree::Remote(_) => { Task::ready(Err(anyhow!("remote worktrees can't yet load files"))) } @@ -1325,6 +1330,9 @@ impl LocalWorktree { &self, path: &Path, encoding: Option, + force: bool, + detect_utf16: bool, + buffer_encoding: Option>>, cx: &Context, ) -> Task> { let path = Arc::from(path); @@ -1357,7 +1365,9 @@ impl LocalWorktree { } else { EncodingWrapper::new(encoding_rs::UTF_8) }, - false, + force, + detect_utf16, + buffer_encoding, ) .await?; @@ -3139,13 +3149,15 @@ impl language::LocalFile for File { &self, cx: &App, encoding: EncodingWrapper, + force: bool, detect_utf16: bool, + buffer_encoding: Option>>, ) -> Task> { let worktree = self.worktree.read(cx).as_local().unwrap(); let abs_path = worktree.absolutize(&self.path); let fs = worktree.fs.clone(); cx.background_spawn(async move { - fs.load_with_encoding(&abs_path?, encoding, detect_utf16) + fs.load_with_encoding(&abs_path?, encoding, force, detect_utf16, buffer_encoding) .await }) } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 35217692cba835..954729ef9a343a 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -468,7 +468,14 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) { let prev_read_dir_count = fs.read_dir_call_count(); let loaded = tree .update(cx, |tree, cx| { - tree.load_file("one/node_modules/b/b1.js".as_ref(), None, cx) + tree.load_file( + "one/node_modules/b/b1.js".as_ref(), + None, + false, + false, + None, + cx, + ) }) .await .unwrap(); @@ -508,7 +515,14 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) { let prev_read_dir_count = fs.read_dir_call_count(); let loaded = tree .update(cx, |tree, cx| { - tree.load_file("one/node_modules/a/a2.js".as_ref(), None, cx) + tree.load_file( + "one/node_modules/a/a2.js".as_ref(), + None, + false, + false, + None, + cx, + ) }) .await .unwrap(); @@ -1954,6 +1968,101 @@ fn random_filename(rng: &mut impl Rng) -> String { .collect() } +#[gpui::test] +async fn test_rename_file_to_new_directory(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background_executor.clone()); + let expected_contents = "content"; + fs.as_fake() + .insert_tree( + "/root", + json!({ + "test.txt": expected_contents + }), + ) + .await; + let worktree = Worktree::local( + Path::new("/root"), + true, + fs.clone(), + Arc::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete()) + .await; + + let entry_id = worktree.read_with(cx, |worktree, _| { + worktree.entry_for_path("test.txt").unwrap().id + }); + let _result = worktree + .update(cx, |worktree, cx| { + worktree.rename_entry(entry_id, Path::new("dir1/dir2/dir3/test.txt"), cx) + }) + .await + .unwrap(); + worktree.read_with(cx, |worktree, _| { + assert!( + worktree.entry_for_path("test.txt").is_none(), + "Old file should have been removed" + ); + assert!( + worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_some(), + "Whole directory hierarchy and the new file should have been created" + ); + }); + assert_eq!( + worktree + .update(cx, |worktree, cx| { + worktree.load_file("dir1/dir2/dir3/test.txt".as_ref(), None, cx) + }) + .await + .unwrap() + .text, + expected_contents, + "Moved file's contents should be preserved" + ); + + let entry_id = worktree.read_with(cx, |worktree, _| { + worktree + .entry_for_path("dir1/dir2/dir3/test.txt") + .unwrap() + .id + }); + let _result = worktree + .update(cx, |worktree, cx| { + worktree.rename_entry(entry_id, Path::new("dir1/dir2/test.txt"), cx) + }) + .await + .unwrap(); + worktree.read_with(cx, |worktree, _| { + assert!( + worktree.entry_for_path("test.txt").is_none(), + "First file should not reappear" + ); + assert!( + worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_none(), + "Old file should have been removed" + ); + assert!( + worktree.entry_for_path("dir1/dir2/test.txt").is_some(), + "No error should have occurred after moving into existing directory" + ); + }); + assert_eq!( + worktree + .update(cx, |worktree, cx| { + worktree.load_file("dir1/dir2/test.txt".as_ref(), None, cx) + }) + .await + .unwrap() + .text, + expected_contents, + "Moved file's contents should be preserved" + ); +} + #[gpui::test] async fn test_private_single_file_worktree(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 5a4558aa3dcad4..bcf46d5087693b 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -308,6 +308,9 @@ pub mod encodings { #[derive(PartialEq, Debug, Clone, Action, JsonSchema, Deserialize)] pub struct Toggle(pub Arc); + + #[derive(PartialEq, Debug, Clone, Action, JsonSchema, Deserialize)] + pub struct ForceOpen(pub Arc); } pub mod agent { diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index a774cf0b162a7f..02eb5dcae0ac29 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -1986,7 +1986,7 @@ mod tests { .worktree_for_root_name("closed_source_worktree", cx) .unwrap(); worktree2.update(cx, |worktree2, cx| { - worktree2.load_file(Path::new("main.rs"), None, cx) + worktree2.load_file(Path::new("main.rs"), None, false, true, None, cx) }) }) .await From 25c6af48d17508a30cac35035fb6a88bc03a88ac Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sat, 18 Oct 2025 20:23:37 +0530 Subject: [PATCH 32/43] Fix an issue that caused a reopened buffer to use UTF-8 even if the associated file was in a different encoding, rather than showing an error. --- crates/fs/src/encodings.rs | 3 +- crates/language/src/buffer.rs | 17 ++++++++++++ crates/project/src/buffer_store.rs | 11 ++++++++ crates/project/src/image_store.rs | 2 ++ crates/workspace/src/workspace.rs | 4 +++ crates/worktree/src/worktree.rs | 44 +++++++++++++++++++++++++++++- 6 files changed, 78 insertions(+), 3 deletions(-) diff --git a/crates/fs/src/encodings.rs b/crates/fs/src/encodings.rs index 53dcca407a339f..16aab79e77ad92 100644 --- a/crates/fs/src/encodings.rs +++ b/crates/fs/src/encodings.rs @@ -11,6 +11,7 @@ use encoding_rs::Encoding; /// A wrapper around `encoding_rs::Encoding` to implement `Send` and `Sync`. /// Since the reference is static, it is safe to send it across threads. +#[derive(Copy)] pub struct EncodingWrapper(pub &'static Encoding); impl Debug for EncodingWrapper { @@ -59,8 +60,6 @@ impl EncodingWrapper { buffer_encoding: Option>>, ) -> Result { // Check if the input starts with a BOM for UTF-16 encodings only if detect_utf16 is true. - println!("{}", force); - println!("{}", detect_utf16); if detect_utf16 { if let Some(encoding) = match input.get(..2) { Some([0xFF, 0xFE]) => Some(encoding_rs::UTF_16LE), diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 5daac9765f6e9a..2668e4dc75d41b 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -128,6 +128,7 @@ pub struct Buffer { change_bits: Vec>>, _subscriptions: Vec, pub encoding: Arc>, + pub observe_file_encoding: Option, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -373,6 +374,10 @@ pub trait File: Send + Sync + Any { /// Return whether Zed considers this to be a private file. fn is_private(&self) -> bool; + + fn encoding(&self) -> Option>> { + unimplemented!() + } } /// The file's storage status - whether it's stored (`Present`), and if so when it was last @@ -1028,6 +1033,7 @@ impl Buffer { change_bits: Default::default(), _subscriptions: Vec::new(), encoding: Arc::new(std::sync::Mutex::new(encoding_rs::UTF_8)), + observe_file_encoding: None, } } @@ -2928,6 +2934,17 @@ impl Buffer { pub fn preserve_preview(&self) -> bool { !self.has_edits_since(&self.preview_version) } + + /// Update the `encoding` field, whenever the `encoding` field of the file changes + pub fn update_encoding(&mut self) { + if let Some(file) = self.file() { + if let Some(encoding) = file.encoding() { + *self.encoding.lock().unwrap() = *encoding.lock().unwrap(); + } else { + *self.encoding.lock().unwrap() = encoding_rs::UTF_8; + }; + } + } } #[doc(hidden)] diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 1919f9b0a8efbd..08455009fb3605 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -533,6 +533,7 @@ impl LocalBufferStore { path: entry.path.clone(), worktree: worktree.clone(), is_private: entry.is_private, + encoding: None, } } else { File { @@ -542,6 +543,7 @@ impl LocalBufferStore { path: old_file.path.clone(), worktree: worktree.clone(), is_private: old_file.is_private, + encoding: None, } }; @@ -670,6 +672,8 @@ impl LocalBufferStore { buffer .replace_text_buffer(text::Buffer::new(0, buffer_id, loaded_file.text), cx); + buffer.update_encoding(); + reload_task = Some(buffer.reload(cx)); })?; @@ -697,6 +701,13 @@ impl LocalBufferStore { entry_id: None, is_local: true, is_private: false, + encoding: Some(Arc::new(std::sync::Mutex::new( + if let Some(encoding) = encoding { + encoding.0 + } else { + encoding_rs::UTF_8 + }, + ))), })), Capability::ReadWrite, ) diff --git a/crates/project/src/image_store.rs b/crates/project/src/image_store.rs index 8fcf9c8a6172f8..2f24927e043064 100644 --- a/crates/project/src/image_store.rs +++ b/crates/project/src/image_store.rs @@ -605,6 +605,7 @@ impl LocalImageStore { path: entry.path.clone(), worktree: worktree.clone(), is_private: entry.is_private, + encoding: None, } } else { worktree::File { @@ -614,6 +615,7 @@ impl LocalImageStore { path: old_file.path.clone(), worktree: worktree.clone(), is_private: old_file.is_private, + encoding: None, } }; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 50683242333a8f..60111bf47dd498 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1945,6 +1945,10 @@ impl Workspace { window: &mut Window, cx: &mut Context, ) -> Task> { + *self.encoding_options.force.get_mut() = false; + *self.encoding_options.detect_utf16.get_mut() = true; + *self.encoding_options.encoding.lock().unwrap() = EncodingWrapper::new(encoding_rs::UTF_8); + let to_load = if let Some(pane) = pane.upgrade() { pane.update(cx, |pane, cx| { window.focus(&pane.focus_handle(cx)); diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index e38a960c18ba9d..2891a3975770f3 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -1318,6 +1318,7 @@ impl LocalWorktree { }, is_local: true, is_private, + encoding: None, }) } }; @@ -1393,6 +1394,11 @@ impl LocalWorktree { }, is_local: true, is_private, + encoding: if let Some(encoding) = encoding { + Some(Arc::new(std::sync::Mutex::new(encoding.0))) + } else { + None + }, }) } }; @@ -1529,6 +1535,7 @@ impl LocalWorktree { entry_id: None, is_local: true, is_private, + encoding: Some(Arc::new(std::sync::Mutex::new(encoding))), })) } }) @@ -3082,7 +3089,7 @@ impl fmt::Debug for Snapshot { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct File { pub worktree: Entity, pub path: Arc, @@ -3090,8 +3097,35 @@ pub struct File { pub entry_id: Option, pub is_local: bool, pub is_private: bool, + pub encoding: Option>>, } +impl PartialEq for File { + fn eq(&self, other: &Self) -> bool { + if self.worktree == other.worktree + && self.path == other.path + && self.disk_state == other.disk_state + && self.entry_id == other.entry_id + && self.is_local == other.is_local + && self.is_private == other.is_private + && if let Some(encoding) = &self.encoding + && let Some(other_encoding) = &other.encoding + { + if *encoding.lock().unwrap() != *other_encoding.lock().unwrap() { + false + } else { + true + } + } else { + true + } + { + true + } else { + false + } + } +} impl language::File for File { fn as_local(&self) -> Option<&dyn language::LocalFile> { if self.is_local { Some(self) } else { None } @@ -3137,6 +3171,12 @@ impl language::File for File { fn path_style(&self, cx: &App) -> PathStyle { self.worktree.read(cx).path_style() + fn encoding(&self) -> Option>> { + if let Some(encoding) = &self.encoding { + Some(encoding.clone()) + } else { + None + } } } @@ -3183,6 +3223,7 @@ impl File { entry_id: Some(entry.id), is_local: true, is_private: entry.is_private, + encoding: None, }) } @@ -3213,6 +3254,7 @@ impl File { entry_id: proto.entry_id.map(ProjectEntryId::from_proto), is_local: false, is_private: false, + encoding: None, }) } From 0e3870450e51e742936f388750af2c96648f46ae Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sun, 19 Oct 2025 11:33:17 +0530 Subject: [PATCH 33/43] Fix an issue that caused the buffer to be in a modified state after choosing the correct encoding from `InvalidBufferView` --- crates/project/src/buffer_store.rs | 38 +++++++----------------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 08455009fb3605..4a8f5170451288 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -643,38 +643,18 @@ impl LocalBufferStore { let reservation = cx.reserve_entity(); let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64()); - // Create the buffer first - let buffer = cx.insert_entity(reservation, |_| { - Buffer::build( - text::Buffer::new(0, buffer_id, ""), - None, - Capability::ReadWrite, - ) - }); - - let buffer_encoding = buffer.read(cx).encoding.clone(); - - let load_file_task = worktree.load_file( - path.as_ref(), - encoding, - force, - detect_utf16, - Some(buffer_encoding), - cx, - ); + let load_file_task = + worktree.load_file(path.as_ref(), encoding, force, detect_utf16, None, cx); - cx.spawn(async move |_, async_cx| { + cx.spawn(async move |_, cx| { let loaded_file = load_file_task.await?; - let mut reload_task = None; - - buffer.update(async_cx, |buffer, cx| { - buffer.replace_file(loaded_file.file); - buffer - .replace_text_buffer(text::Buffer::new(0, buffer_id, loaded_file.text), cx); - - buffer.update_encoding(); - reload_task = Some(buffer.reload(cx)); + let buffer = cx.insert_entity(reservation, |_| { + Buffer::build( + text::Buffer::new(0, buffer_id, loaded_file.text), + Some(loaded_file.file), + Capability::ReadWrite, + ) })?; Ok(buffer) From 1d95a18a1163c738c2f575b3e85e22a37b319dcd Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sun, 19 Oct 2025 14:48:37 +0530 Subject: [PATCH 34/43] Create a new crate `encodings` that will have all that is not related to UI. The `encodings_ui` crate will only have UI related components in the future. --- Cargo.lock | 10 +- Cargo.toml | 3 + crates/encodings/Cargo.toml | 15 +- crates/encodings/src/lib.rs | 429 +++++------------- crates/encodings_ui/Cargo.toml | 25 + .../{encodings => encodings_ui}/LICENSE-GPL | 0 crates/encodings_ui/src/lib.rs | 343 ++++++++++++++ .../src/selectors.rs | 0 crates/project/src/invalid_item_view.rs | 16 +- crates/zed/Cargo.toml | 2 +- crates/zed/src/main.rs | 2 +- crates/zed/src/zed.rs | 2 +- crates/zed_actions/src/lib.rs | 2 +- 13 files changed, 514 insertions(+), 335 deletions(-) create mode 100644 crates/encodings_ui/Cargo.toml rename crates/{encodings => encodings_ui}/LICENSE-GPL (100%) create mode 100644 crates/encodings_ui/src/lib.rs rename crates/{encodings => encodings_ui}/src/selectors.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 119769b58ad3c6..c4bf18ec7e4b27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5526,6 +5526,14 @@ dependencies = [ [[package]] name = "encodings" version = "0.1.0" +dependencies = [ + "anyhow", + "encoding_rs", +] + +[[package]] +name = "encodings_ui" +version = "0.1.0" dependencies = [ "editor", "encoding_rs", @@ -21208,7 +21216,7 @@ dependencies = [ "edit_prediction_button", "editor", "encoding_rs", - "encodings", + "encodings_ui", "env_logger 0.11.8", "extension", "extension_host", diff --git a/Cargo.toml b/Cargo.toml index 80c8452838d9db..8e5bc5a79cfc17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ members = [ "crates/editor", "crates/eval", "crates/encodings", + "crates/encodings_ui", "crates/explorer_command_injector", "crates/extension", "crates/extension_api", @@ -223,6 +224,7 @@ members = [ "tooling/perf", "tooling/xtask", "crates/encodings", + "crates/encodings_ui", ] default-members = ["crates/zed"] @@ -316,6 +318,7 @@ edit_prediction_button = { path = "crates/edit_prediction_button" } edit_prediction_context = { path = "crates/edit_prediction_context" } zeta2_tools = { path = "crates/zeta2_tools" } encodings = {path = "crates/encodings"} +encodings_ui = {path = "crates/encodings_ui"} inspector_ui = { path = "crates/inspector_ui" } install_cli = { path = "crates/install_cli" } journal = { path = "crates/journal" } diff --git a/crates/encodings/Cargo.toml b/crates/encodings/Cargo.toml index 341c8cfa8f0782..50b932ab8649f2 100644 --- a/crates/encodings/Cargo.toml +++ b/crates/encodings/Cargo.toml @@ -5,21 +5,8 @@ publish.workspace = true edition.workspace = true [dependencies] -editor.workspace = true encoding_rs.workspace = true -fs.workspace = true -futures.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 -workspace-hack.workspace = true -zed_actions.workspace = true - +anyhow.workspace = true [lints] workspace = true diff --git a/crates/encodings/src/lib.rs b/crates/encodings/src/lib.rs index 86576212ec47cf..e3baad90167037 100644 --- a/crates/encodings/src/lib.rs +++ b/crates/encodings/src/lib.rs @@ -1,343 +1,154 @@ -//! A crate for handling file encodings in the text editor. - -use crate::selectors::encoding::Action; -use editor::Editor; -use encoding_rs::Encoding; -use gpui::{ClickEvent, Entity, Subscription, WeakEntity}; -use language::Buffer; -use ui::{App, Button, ButtonCommon, Context, LabelSize, Render, Tooltip, Window, div}; -use ui::{Clickable, ParentElement}; -use util::ResultExt; -use workspace::{ - CloseActiveItem, ItemHandle, OpenOptions, StatusItemView, Workspace, - with_active_or_new_workspace, +use encoding_rs; +use std::{ + fmt::Debug, + sync::{Arc, Mutex, atomic::AtomicBool}, }; -use zed_actions::encodings::{ForceOpen, Toggle}; -use crate::selectors::encoding::EncodingSelector; -use crate::selectors::save_or_reopen::EncodingSaveOrReopenSelector; +pub use encoding_rs::{ + BIG5, EUC_JP, EUC_KR, GB18030, GBK, IBM866, ISO_2022_JP, ISO_8859_2, ISO_8859_3, ISO_8859_4, + ISO_8859_5, ISO_8859_6, ISO_8859_7, ISO_8859_8, ISO_8859_8_I, ISO_8859_10, ISO_8859_13, + ISO_8859_14, ISO_8859_15, ISO_8859_16, KOI8_R, KOI8_U, MACINTOSH, SHIFT_JIS, UTF_8, UTF_16BE, + UTF_16LE, WINDOWS_874, WINDOWS_1250, WINDOWS_1251, WINDOWS_1252, WINDOWS_1253, WINDOWS_1254, + WINDOWS_1255, WINDOWS_1256, WINDOWS_1257, WINDOWS_1258, X_MAC_CYRILLIC, +}; -/// A status bar item that shows the current file encoding and allows changing it. -pub struct EncodingIndicator { - pub encoding: Option<&'static Encoding>, - pub workspace: WeakEntity, +pub struct Encoding(Mutex<&'static encoding_rs::Encoding>); - /// Subscription to observe changes in the active editor - observe_editor: Option, +impl Debug for Encoding { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple(&format!("Encoding{:?}", self.0)) + .field(&self.get().name()) + .finish() + } +} - /// Subscription to observe changes in the `encoding` field of the `Buffer` struct - observe_buffer_encoding: Option, +impl Default for Encoding { + fn default() -> Self { + Encoding(Mutex::new(UTF_8)) + } +} - /// Whether to show the indicator or not, based on whether an editor is active - show: bool, +unsafe impl Send for Encoding {} +unsafe impl Sync for Encoding {} - /// Whether to show `EncodingSaveOrReopenSelector`. It will be shown only when - /// the current buffer is associated with a file. - show_save_or_reopen_selector: bool, -} +impl Encoding { + pub fn new(encoding: &'static encoding_rs::Encoding) -> Self { + Self(Mutex::new(encoding)) + } -pub mod selectors; + pub fn set(&self, encoding: &'static encoding_rs::Encoding) { + *self.0.lock().unwrap() = encoding; + } -impl Render for EncodingIndicator { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { - let status_element = div(); - let show_save_or_reopen_selector = self.show_save_or_reopen_selector; + pub fn get(&self) -> &'static encoding_rs::Encoding { + *self.0.lock().unwrap() + } - if !self.show { - return status_element; + pub async fn decode( + &self, + input: Vec, + force: bool, + detect_utf16: bool, + buffer_encoding: Option>, + ) -> anyhow::Result { + // Check if the input starts with a BOM for UTF-16 encodings only if detect_utf16 is true. + if detect_utf16 { + if let Some(encoding) = match input.get(..2) { + Some([0xFF, 0xFE]) => Some(UTF_16LE), + Some([0xFE, 0xFF]) => Some(UTF_16BE), + _ => None, + } { + self.set(encoding); + + if let Some(v) = buffer_encoding { + v.set(encoding) + } + } } - status_element.child( - Button::new( - "encoding", - encoding_name(self.encoding.unwrap_or(encoding_rs::UTF_8)), - ) - .label_size(LabelSize::Small) - .tooltip(Tooltip::text("Select Encoding")) - .on_click(cx.listener(move |indicator, _: &ClickEvent, window, cx| { - if let Some(workspace) = indicator.workspace.upgrade() { - workspace.update(cx, move |workspace, cx| { - // Open the `EncodingSaveOrReopenSelector` if the buffer is associated with a file, - if show_save_or_reopen_selector { - EncodingSaveOrReopenSelector::toggle(workspace, window, cx) - } - // otherwise, open the `EncodingSelector` directly. - else { - let (_, buffer, _) = workspace - .active_item(cx) - .unwrap() - .act_as::(cx) - .unwrap() - .read(cx) - .active_excerpt(cx) - .unwrap(); - - let weak_workspace = workspace.weak_handle(); + let (cow, had_errors) = self.get().decode_with_bom_removal(&input); - workspace.toggle_modal(window, cx, |window, cx| { - let selector = EncodingSelector::new( - window, - cx, - Action::Save, - Some(buffer.downgrade()), - weak_workspace, - None, - ); - selector - }) - } - }) - } - })), - ) - } -} + if force { + return Ok(cow.to_string()); + } -impl EncodingIndicator { - pub fn new( - encoding: Option<&'static Encoding>, - workspace: WeakEntity, - observe_editor: Option, - observe_buffer_encoding: Option, - ) -> EncodingIndicator { - EncodingIndicator { - encoding, - workspace, - observe_editor, - show: false, - observe_buffer_encoding, - show_save_or_reopen_selector: false, + if !had_errors { + Ok(cow.to_string()) + } else { + Err(anyhow::anyhow!( + "The file contains invalid bytes for the specified encoding: {}.\nThis usually means that the file is not a regular text file, or is encoded in a different encoding.\nContinuing to open it may result in data loss if saved.", + self.get().name() + )) } } - /// Update the encoding when the active editor is switched. - pub fn update_when_editor_is_switched( - &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.clone(); - self.encoding = Some(&*encoding.lock().unwrap()); + pub async fn encode(&self, input: String) -> anyhow::Result> { + if self.get() == UTF_16BE { + let mut data = Vec::::with_capacity(input.len() * 2); - if let Some(_) = buffer.read(cx).file() { - self.show_save_or_reopen_selector = true; - } else { - self.show_save_or_reopen_selector = false; - } - } + // Convert the input string to UTF-16BE bytes + let utf16be_bytes = input.encode_utf16().flat_map(|u| u.to_be_bytes()); - cx.notify(); - } + data.extend(utf16be_bytes); + return Ok(data); + } else if self.get() == UTF_16LE { + let mut data = Vec::::with_capacity(input.len() * 2); - /// Update the encoding when the `encoding` field of the `Buffer` struct changes. - pub fn update_when_buffer_encoding_changes( - &mut self, - buffer: Entity, - _: &mut Window, - cx: &mut Context, - ) { - let encoding = buffer.read(cx).encoding.clone(); - self.encoding = Some(&*encoding.lock().unwrap()); - cx.notify(); - } -} + // Convert the input string to UTF-16LE bytes + let utf16le_bytes = input.encode_utf16().flat_map(|u| u.to_le_bytes()); -impl StatusItemView for EncodingIndicator { - fn set_active_pane_item( - &mut self, - 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_editor = - Some(cx.observe_in(&editor, window, Self::update_when_editor_is_switched)); - if let Some((_, buffer, _)) = &editor.read(cx).active_excerpt(cx) { - self.observe_buffer_encoding = Some(cx.observe_in( - buffer, - window, - Self::update_when_buffer_encoding_changes, - )); - } - self.update_when_editor_is_switched(editor, window, cx); - self.show = true; - } - None => { - self.encoding = None; - self.observe_editor = None; - self.show = false; - } + data.extend(utf16le_bytes); + return Ok(data); + } else { + let (cow, _encoding_used, _had_errors) = self.get().encode(&input); + + Ok(cow.into_owned()) } } -} - -/// Get a human-readable name for the given encoding. -pub fn encoding_name(encoding: &'static Encoding) -> String { - let name = encoding.name(); - match name { - "UTF-8" => "UTF-8", - "UTF-16LE" => "UTF-16 LE", - "UTF-16BE" => "UTF-16 BE", - "windows-1252" => "Windows-1252", - "windows-1251" => "Windows-1251", - "windows-1250" => "Windows-1250", - "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-13" => "ISO 8859-13", - "ISO-8859-15" => "ISO 8859-15", - "KOI8-R" => "KOI8-R", - "KOI8-U" => "KOI8-U", - "macintosh" => "MacRoman", - "x-mac-cyrillic" => "Mac Cyrillic", - "windows-874" => "Windows-874", - "windows-1253" => "Windows-1253", - "windows-1254" => "Windows-1254", - "windows-1255" => "Windows-1255", - "windows-1256" => "Windows-1256", - "windows-1257" => "Windows-1257", - "windows-1258" => "Windows-1258", - "EUC-KR" => "Windows-949", - "EUC-JP" => "EUC-JP", - "ISO-2022-JP" => "ISO 2022-JP", - "GBK" => "GBK", - "gb18030" => "GB18030", - "Big5" => "Big5", - _ => name, + pub fn reset(&self) { + self.set(UTF_8); } - .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 Encoding { - match index { - 0 => encoding_rs::UTF_8, - 1 => encoding_rs::UTF_16LE, - 2 => encoding_rs::UTF_16BE, - 3 => encoding_rs::WINDOWS_1252, - 4 => encoding_rs::WINDOWS_1251, - 5 => encoding_rs::WINDOWS_1250, - 6 => encoding_rs::ISO_8859_2, - 7 => encoding_rs::ISO_8859_3, - 8 => encoding_rs::ISO_8859_4, - 9 => encoding_rs::ISO_8859_5, - 10 => encoding_rs::ISO_8859_6, - 11 => encoding_rs::ISO_8859_7, - 12 => encoding_rs::ISO_8859_8, - 13 => encoding_rs::ISO_8859_13, - 14 => encoding_rs::ISO_8859_15, - 15 => encoding_rs::KOI8_R, - 16 => encoding_rs::KOI8_U, - 17 => encoding_rs::MACINTOSH, - 18 => encoding_rs::X_MAC_CYRILLIC, - 19 => encoding_rs::WINDOWS_874, - 20 => encoding_rs::WINDOWS_1253, - 21 => encoding_rs::WINDOWS_1254, - 22 => encoding_rs::WINDOWS_1255, - 23 => encoding_rs::WINDOWS_1256, - 24 => encoding_rs::WINDOWS_1257, - 25 => encoding_rs::WINDOWS_1258, - 26 => encoding_rs::EUC_KR, - 27 => encoding_rs::EUC_JP, - 28 => encoding_rs::ISO_2022_JP, - 29 => encoding_rs::GBK, - 30 => encoding_rs::GB18030, - 31 => encoding_rs::BIG5, - _ => encoding_rs::UTF_8, - } +/// Convert a byte vector from a specified encoding to a UTF-8 string. +pub async fn to_utf8( + input: Vec, + encoding: Encoding, + force: bool, + detect_utf16: bool, + buffer_encoding: Option>, +) -> anyhow::Result { + encoding + .decode(input, force, detect_utf16, buffer_encoding) + .await } -/// Get an encoding from its name. -pub fn encoding_from_name(name: &str) -> &'static Encoding { - match name { - "UTF-8" => encoding_rs::UTF_8, - "UTF-16 LE" => encoding_rs::UTF_16LE, - "UTF-16 BE" => encoding_rs::UTF_16BE, - "Windows-1252" => encoding_rs::WINDOWS_1252, - "Windows-1251" => encoding_rs::WINDOWS_1251, - "Windows-1250" => encoding_rs::WINDOWS_1250, - "ISO 8859-2" => encoding_rs::ISO_8859_2, - "ISO 8859-3" => encoding_rs::ISO_8859_3, - "ISO 8859-4" => encoding_rs::ISO_8859_4, - "ISO 8859-5" => encoding_rs::ISO_8859_5, - "ISO 8859-6" => encoding_rs::ISO_8859_6, - "ISO 8859-7" => encoding_rs::ISO_8859_7, - "ISO 8859-8" => encoding_rs::ISO_8859_8, - "ISO 8859-13" => encoding_rs::ISO_8859_13, - "ISO 8859-15" => encoding_rs::ISO_8859_15, - "KOI8-R" => encoding_rs::KOI8_R, - "KOI8-U" => encoding_rs::KOI8_U, - "MacRoman" => encoding_rs::MACINTOSH, - "Mac Cyrillic" => encoding_rs::X_MAC_CYRILLIC, - "Windows-874" => encoding_rs::WINDOWS_874, - "Windows-1253" => encoding_rs::WINDOWS_1253, - "Windows-1254" => encoding_rs::WINDOWS_1254, - "Windows-1255" => encoding_rs::WINDOWS_1255, - "Windows-1256" => encoding_rs::WINDOWS_1256, - "Windows-1257" => encoding_rs::WINDOWS_1257, - "Windows-1258" => encoding_rs::WINDOWS_1258, - "Windows-949" => encoding_rs::EUC_KR, - "EUC-JP" => encoding_rs::EUC_JP, - "ISO 2022-JP" => encoding_rs::ISO_2022_JP, - "GBK" => encoding_rs::GBK, - "GB18030" => encoding_rs::GB18030, - "Big5" => encoding_rs::BIG5, - _ => encoding_rs::UTF_8, // Default to UTF-8 for unknown names - } +/// Convert a UTF-8 string to a byte vector in a specified encoding. +pub async fn from_utf8(input: String, target: Encoding) -> anyhow::Result> { + target.encode(input).await } -pub fn init(cx: &mut App) { - cx.on_action(|action: &Toggle, cx: &mut App| { - let Toggle(path) = action.clone(); - let path = path.to_path_buf(); - - with_active_or_new_workspace(cx, |workspace, window, cx| { - let weak_workspace = workspace.weak_handle(); - workspace.toggle_modal(window, cx, |window, cx| { - EncodingSelector::new(window, cx, Action::Reopen, None, weak_workspace, Some(path)) - }); - }); - }); - - cx.on_action(|action: &ForceOpen, cx: &mut App| { - let ForceOpen(path) = action.clone(); - let path = path.to_path_buf(); - - with_active_or_new_workspace(cx, |workspace, window, cx| { - workspace.active_pane().update(cx, |pane, cx| { - pane.close_active_item(&CloseActiveItem::default(), window, cx) - .detach(); - }); - - { - let force = workspace.encoding_options.force.get_mut(); - - *force = true; - } +pub struct EncodingOptions { + pub encoding: Arc>, + pub force: AtomicBool, + pub detect_utf16: AtomicBool, +} - let open_task = workspace.open_abs_path(path, OpenOptions::default(), window, cx); - let weak_workspace = workspace.weak_handle(); +impl EncodingOptions { + pub fn reset(&mut self) { + self.encoding.lock().unwrap().reset(); + *self.force.get_mut() = false; + *self.detect_utf16.get_mut() = true; + } +} - cx.spawn(async move |_, cx| { - let workspace = weak_workspace.upgrade().unwrap(); - open_task.await.log_err(); - workspace - .update(cx, |workspace: &mut Workspace, _| { - *workspace.encoding_options.force.get_mut() = false; - }) - .log_err(); - }) - .detach(); - }); - }); +impl Default for EncodingOptions { + fn default() -> Self { + EncodingOptions { + encoding: Arc::new(Mutex::new(Encoding::default())), + force: AtomicBool::new(false), + detect_utf16: AtomicBool::new(true), + } + } } diff --git a/crates/encodings_ui/Cargo.toml b/crates/encodings_ui/Cargo.toml new file mode 100644 index 00000000000000..bc83885841456f --- /dev/null +++ b/crates/encodings_ui/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "encodings_ui" +version = "0.1.0" +publish.workspace = true +edition.workspace = true + +[dependencies] +editor.workspace = true +encoding_rs.workspace = true +fs.workspace = true +futures.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 +workspace-hack.workspace = true +zed_actions.workspace = true + + +[lints] +workspace = true diff --git a/crates/encodings/LICENSE-GPL b/crates/encodings_ui/LICENSE-GPL similarity index 100% rename from crates/encodings/LICENSE-GPL rename to crates/encodings_ui/LICENSE-GPL diff --git a/crates/encodings_ui/src/lib.rs b/crates/encodings_ui/src/lib.rs new file mode 100644 index 00000000000000..591d6e5349350b --- /dev/null +++ b/crates/encodings_ui/src/lib.rs @@ -0,0 +1,343 @@ +//! A crate for handling file encodings in the text editor. + +use crate::selectors::encoding::Action; +use editor::Editor; +use encoding_rs::Encoding; +use gpui::{ClickEvent, Entity, Subscription, WeakEntity}; +use language::Buffer; +use ui::{App, Button, ButtonCommon, Context, LabelSize, Render, Tooltip, Window, div}; +use ui::{Clickable, ParentElement}; +use util::ResultExt; +use workspace::{ + CloseActiveItem, ItemHandle, OpenOptions, StatusItemView, Workspace, + with_active_or_new_workspace, +}; +use zed_actions::encodings_ui::{ForceOpen, Toggle}; + +use crate::selectors::encoding::EncodingSelector; +use crate::selectors::save_or_reopen::EncodingSaveOrReopenSelector; + +/// A status bar item that shows the current file encoding and allows changing it. +pub struct EncodingIndicator { + pub encoding: Option<&'static Encoding>, + pub workspace: WeakEntity, + + /// Subscription to observe changes in the active editor + observe_editor: Option, + + /// Subscription to observe changes in the `encoding` field of the `Buffer` struct + observe_buffer_encoding: Option, + + /// Whether to show the indicator or not, based on whether an editor is active + show: bool, + + /// Whether to show `EncodingSaveOrReopenSelector`. It will be shown only when + /// the current buffer is associated with a file. + show_save_or_reopen_selector: bool, +} + +pub mod selectors; + +impl Render for EncodingIndicator { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { + let status_element = div(); + let show_save_or_reopen_selector = self.show_save_or_reopen_selector; + + if !self.show { + return status_element; + } + + status_element.child( + Button::new( + "encoding", + encoding_name(self.encoding.unwrap_or(encoding_rs::UTF_8)), + ) + .label_size(LabelSize::Small) + .tooltip(Tooltip::text("Select Encoding")) + .on_click(cx.listener(move |indicator, _: &ClickEvent, window, cx| { + if let Some(workspace) = indicator.workspace.upgrade() { + workspace.update(cx, move |workspace, cx| { + // Open the `EncodingSaveOrReopenSelector` if the buffer is associated with a file, + if show_save_or_reopen_selector { + EncodingSaveOrReopenSelector::toggle(workspace, window, cx) + } + // otherwise, open the `EncodingSelector` directly. + else { + let (_, buffer, _) = workspace + .active_item(cx) + .unwrap() + .act_as::(cx) + .unwrap() + .read(cx) + .active_excerpt(cx) + .unwrap(); + + let weak_workspace = workspace.weak_handle(); + + workspace.toggle_modal(window, cx, |window, cx| { + let selector = EncodingSelector::new( + window, + cx, + Action::Save, + Some(buffer.downgrade()), + weak_workspace, + None, + ); + selector + }) + } + }) + } + })), + ) + } +} + +impl EncodingIndicator { + pub fn new( + encoding: Option<&'static Encoding>, + workspace: WeakEntity, + observe_editor: Option, + observe_buffer_encoding: Option, + ) -> EncodingIndicator { + EncodingIndicator { + encoding, + workspace, + observe_editor, + show: false, + observe_buffer_encoding, + show_save_or_reopen_selector: false, + } + } + + /// Update the encoding when the active editor is switched. + pub fn update_when_editor_is_switched( + &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.clone(); + self.encoding = Some(&*encoding.lock().unwrap()); + + if let Some(_) = buffer.read(cx).file() { + self.show_save_or_reopen_selector = true; + } else { + self.show_save_or_reopen_selector = false; + } + } + + cx.notify(); + } + + /// Update the encoding when the `encoding` field of the `Buffer` struct changes. + pub fn update_when_buffer_encoding_changes( + &mut self, + buffer: Entity, + _: &mut Window, + cx: &mut Context, + ) { + let encoding = buffer.read(cx).encoding.clone(); + self.encoding = Some(&*encoding.lock().unwrap()); + 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, + ) { + match active_pane_item.and_then(|item| item.downcast::()) { + Some(editor) => { + self.observe_editor = + Some(cx.observe_in(&editor, window, Self::update_when_editor_is_switched)); + if let Some((_, buffer, _)) = &editor.read(cx).active_excerpt(cx) { + self.observe_buffer_encoding = Some(cx.observe_in( + buffer, + window, + Self::update_when_buffer_encoding_changes, + )); + } + self.update_when_editor_is_switched(editor, window, cx); + self.show = true; + } + None => { + self.encoding = None; + self.observe_editor = None; + self.show = false; + } + } + } +} + +/// Get a human-readable name for the given encoding. +pub fn encoding_name(encoding: &'static Encoding) -> String { + let name = encoding.name(); + + match name { + "UTF-8" => "UTF-8", + "UTF-16LE" => "UTF-16 LE", + "UTF-16BE" => "UTF-16 BE", + "windows-1252" => "Windows-1252", + "windows-1251" => "Windows-1251", + "windows-1250" => "Windows-1250", + "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-13" => "ISO 8859-13", + "ISO-8859-15" => "ISO 8859-15", + "KOI8-R" => "KOI8-R", + "KOI8-U" => "KOI8-U", + "macintosh" => "MacRoman", + "x-mac-cyrillic" => "Mac Cyrillic", + "windows-874" => "Windows-874", + "windows-1253" => "Windows-1253", + "windows-1254" => "Windows-1254", + "windows-1255" => "Windows-1255", + "windows-1256" => "Windows-1256", + "windows-1257" => "Windows-1257", + "windows-1258" => "Windows-1258", + "EUC-KR" => "Windows-949", + "EUC-JP" => "EUC-JP", + "ISO-2022-JP" => "ISO 2022-JP", + "GBK" => "GBK", + "gb18030" => "GB18030", + "Big5" => "Big5", + _ => name, + } + .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 Encoding { + match index { + 0 => encoding_rs::UTF_8, + 1 => encoding_rs::UTF_16LE, + 2 => encoding_rs::UTF_16BE, + 3 => encoding_rs::WINDOWS_1252, + 4 => encoding_rs::WINDOWS_1251, + 5 => encoding_rs::WINDOWS_1250, + 6 => encoding_rs::ISO_8859_2, + 7 => encoding_rs::ISO_8859_3, + 8 => encoding_rs::ISO_8859_4, + 9 => encoding_rs::ISO_8859_5, + 10 => encoding_rs::ISO_8859_6, + 11 => encoding_rs::ISO_8859_7, + 12 => encoding_rs::ISO_8859_8, + 13 => encoding_rs::ISO_8859_13, + 14 => encoding_rs::ISO_8859_15, + 15 => encoding_rs::KOI8_R, + 16 => encoding_rs::KOI8_U, + 17 => encoding_rs::MACINTOSH, + 18 => encoding_rs::X_MAC_CYRILLIC, + 19 => encoding_rs::WINDOWS_874, + 20 => encoding_rs::WINDOWS_1253, + 21 => encoding_rs::WINDOWS_1254, + 22 => encoding_rs::WINDOWS_1255, + 23 => encoding_rs::WINDOWS_1256, + 24 => encoding_rs::WINDOWS_1257, + 25 => encoding_rs::WINDOWS_1258, + 26 => encoding_rs::EUC_KR, + 27 => encoding_rs::EUC_JP, + 28 => encoding_rs::ISO_2022_JP, + 29 => encoding_rs::GBK, + 30 => encoding_rs::GB18030, + 31 => encoding_rs::BIG5, + _ => encoding_rs::UTF_8, + } +} + +/// Get an encoding from its name. +pub fn encoding_from_name(name: &str) -> &'static Encoding { + match name { + "UTF-8" => encoding_rs::UTF_8, + "UTF-16 LE" => encoding_rs::UTF_16LE, + "UTF-16 BE" => encoding_rs::UTF_16BE, + "Windows-1252" => encoding_rs::WINDOWS_1252, + "Windows-1251" => encoding_rs::WINDOWS_1251, + "Windows-1250" => encoding_rs::WINDOWS_1250, + "ISO 8859-2" => encoding_rs::ISO_8859_2, + "ISO 8859-3" => encoding_rs::ISO_8859_3, + "ISO 8859-4" => encoding_rs::ISO_8859_4, + "ISO 8859-5" => encoding_rs::ISO_8859_5, + "ISO 8859-6" => encoding_rs::ISO_8859_6, + "ISO 8859-7" => encoding_rs::ISO_8859_7, + "ISO 8859-8" => encoding_rs::ISO_8859_8, + "ISO 8859-13" => encoding_rs::ISO_8859_13, + "ISO 8859-15" => encoding_rs::ISO_8859_15, + "KOI8-R" => encoding_rs::KOI8_R, + "KOI8-U" => encoding_rs::KOI8_U, + "MacRoman" => encoding_rs::MACINTOSH, + "Mac Cyrillic" => encoding_rs::X_MAC_CYRILLIC, + "Windows-874" => encoding_rs::WINDOWS_874, + "Windows-1253" => encoding_rs::WINDOWS_1253, + "Windows-1254" => encoding_rs::WINDOWS_1254, + "Windows-1255" => encoding_rs::WINDOWS_1255, + "Windows-1256" => encoding_rs::WINDOWS_1256, + "Windows-1257" => encoding_rs::WINDOWS_1257, + "Windows-1258" => encoding_rs::WINDOWS_1258, + "Windows-949" => encoding_rs::EUC_KR, + "EUC-JP" => encoding_rs::EUC_JP, + "ISO 2022-JP" => encoding_rs::ISO_2022_JP, + "GBK" => encoding_rs::GBK, + "GB18030" => encoding_rs::GB18030, + "Big5" => encoding_rs::BIG5, + _ => encoding_rs::UTF_8, // Default to UTF-8 for unknown names + } +} + +pub fn init(cx: &mut App) { + cx.on_action(|action: &Toggle, cx: &mut App| { + let Toggle(path) = action.clone(); + let path = path.to_path_buf(); + + with_active_or_new_workspace(cx, |workspace, window, cx| { + let weak_workspace = workspace.weak_handle(); + workspace.toggle_modal(window, cx, |window, cx| { + EncodingSelector::new(window, cx, Action::Reopen, None, weak_workspace, Some(path)) + }); + }); + }); + + cx.on_action(|action: &ForceOpen, cx: &mut App| { + let ForceOpen(path) = action.clone(); + let path = path.to_path_buf(); + + with_active_or_new_workspace(cx, |workspace, window, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.close_active_item(&CloseActiveItem::default(), window, cx) + .detach(); + }); + + { + let force = workspace.encoding_options.force.get_mut(); + + *force = true; + } + + let open_task = workspace.open_abs_path(path, OpenOptions::default(), window, cx); + let weak_workspace = workspace.weak_handle(); + + cx.spawn(async move |_, cx| { + let workspace = weak_workspace.upgrade().unwrap(); + open_task.await.log_err(); + workspace + .update(cx, |workspace: &mut Workspace, _| { + *workspace.encoding_options.force.get_mut() = false; + }) + .log_err(); + }) + .detach(); + }); + }); +} diff --git a/crates/encodings/src/selectors.rs b/crates/encodings_ui/src/selectors.rs similarity index 100% rename from crates/encodings/src/selectors.rs rename to crates/encodings_ui/src/selectors.rs diff --git a/crates/project/src/invalid_item_view.rs b/crates/project/src/invalid_item_view.rs index 252ea9673244e0..7436c941d56f25 100644 --- a/crates/project/src/invalid_item_view.rs +++ b/crates/project/src/invalid_item_view.rs @@ -2,9 +2,9 @@ use std::{path::Path, sync::Arc}; use gpui::{EventEmitter, FocusHandle, Focusable}; use ui::{ - App, Button, ButtonCommon, ButtonStyle, Clickable, Context, FluentBuilder, InteractiveElement, - KeyBinding, Label, LabelCommon, LabelSize, ParentElement, Render, SharedString, Styled as _, - TintColor, Window, h_flex, v_flex, + h_flex, v_flex, App, Button, ButtonCommon, ButtonStyle, Clickable, Context, FluentBuilder, + InteractiveElement, KeyBinding, Label, LabelCommon, LabelSize, ParentElement, Render, + SharedString, Styled as _, TintColor, Window, }; use zed_actions::workspace::OpenWithSystem; @@ -127,9 +127,11 @@ impl Render for InvalidItemView { .on_click( move |_, window, cx| { window.dispatch_action( - Box::new(zed_actions::encodings::Toggle( - path0.clone(), - )), + Box::new( + zed_actions::encodings_ui::Toggle( + path0.clone(), + ), + ), cx, ) }, @@ -145,7 +147,7 @@ impl Render for InvalidItemView { move |_, window, cx| { window.dispatch_action( Box::new( - zed_actions::encodings::ForceOpen( + zed_actions::encodings_ui::ForceOpen( path1.clone(), ), ), diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 25dc614bfd63c0..4bf7f2824e9d20 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -53,7 +53,7 @@ diagnostics.workspace = true editor.workspace = true zeta2_tools.workspace = true encoding.workspace = true -encodings.workspace = true +encodings_ui.workspace = true env_logger.workspace = true extension.workspace = true extension_host.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index f826399e4599b1..576e8cd41a587d 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -630,7 +630,7 @@ pub fn main() { zeta::init(cx); inspector_ui::init(app_state.clone(), cx); json_schema_store::init(cx); - encodings::init(cx); + encodings_ui::init(cx); cx.observe_global::({ let http = app_state.client.http_client(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 2bbdb428ee12c7..2c5479f5b90316 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -444,7 +444,7 @@ pub fn initialize_workspace( }); let encoding_indicator = cx.new(|_cx| { - encodings::EncodingIndicator::new(None, workspace.weak_handle(), None, None) + encodings_ui::EncodingIndicator::new(None, workspace.weak_handle(), None, None) }); let cursor_position = diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index bcf46d5087693b..5c60efd96f6d9f 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -299,7 +299,7 @@ pub mod settings_profile_selector { pub struct Toggle; } -pub mod encodings { +pub mod encodings_ui { use std::sync::Arc; use gpui::Action; From 8580683f01b3a156bdbc51a42f717297e001bf71 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sun, 19 Oct 2025 20:35:18 +0530 Subject: [PATCH 35/43] - Move the functionality in `fs::encodings` to a seperate crate `encodings` - `EncodingWrapper` is replaced with `encodings::Encoding` --- Cargo.lock | 24 +-- crates/agent/src/tools/edit_file_tool.rs | 8 +- crates/agent2/Cargo.toml | 2 +- crates/assistant_tools/Cargo.toml | 2 +- crates/assistant_tools/src/edit_file_tool.rs | 9 +- crates/collab/Cargo.toml | 2 +- crates/collab/src/tests/integration_tests.rs | 8 +- .../random_project_collaboration_tests.rs | 6 +- crates/copilot/Cargo.toml | 2 +- crates/copilot/src/copilot.rs | 12 +- crates/encodings/src/lib.rs | 6 + crates/encodings_ui/src/lib.rs | 4 +- crates/encodings_ui/src/selectors.rs | 16 +- crates/extension_host/Cargo.toml | 2 +- crates/extension_host/src/extension_host.rs | 7 +- crates/fs/Cargo.toml | 2 +- crates/fs/src/encodings.rs | 152 ------------------ crates/fs/src/fs.rs | 29 ++-- crates/git_ui/Cargo.toml | 2 +- crates/git_ui/src/file_diff_view.rs | 6 +- crates/language/Cargo.toml | 2 +- crates/language/src/buffer.rs | 27 ++-- crates/project/Cargo.toml | 2 +- crates/project/src/buffer_store.rs | 36 ++--- crates/project/src/prettier_store.rs | 8 +- crates/project/src/project.rs | 25 +-- crates/project/src/project_tests.rs | 34 ++-- crates/remote_server/Cargo.toml | 2 +- .../remote_server/src/remote_editing_tests.rs | 12 +- crates/workspace/Cargo.toml | 2 +- crates/workspace/src/workspace.rs | 25 ++- crates/worktree/Cargo.toml | 2 +- crates/worktree/src/worktree.rs | 54 +++---- crates/worktree/src/worktree_tests.rs | 14 +- crates/zed/Cargo.toml | 3 +- crates/zed/src/zed.rs | 20 +-- 36 files changed, 201 insertions(+), 368 deletions(-) delete mode 100644 crates/fs/src/encodings.rs diff --git a/Cargo.lock b/Cargo.lock index c4bf18ec7e4b27..c4a3210ab2d248 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,7 +153,7 @@ dependencies = [ "db", "derive_more 0.99.20", "editor", - "encoding_rs", + "encodings", "env_logger 0.11.8", "fs", "futures 0.3.31", @@ -3355,7 +3355,7 @@ dependencies = [ "dashmap 6.1.0", "debugger_ui", "editor", - "encoding_rs", + "encodings", "envy", "extension", "file_finder", @@ -3722,7 +3722,7 @@ dependencies = [ "dirs 4.0.0", "edit_prediction", "editor", - "encoding_rs", + "encodings", "fs", "futures 0.3.31", "gpui", @@ -5928,7 +5928,7 @@ dependencies = [ "criterion", "ctor", "dap", - "encoding_rs", + "encodings", "extension", "fs", "futures 0.3.31", @@ -6428,7 +6428,7 @@ dependencies = [ "async-trait", "cocoa 0.26.0", "collections", - "encoding_rs", + "encodings", "fsevent", "futures 0.3.31", "git", @@ -7130,7 +7130,7 @@ dependencies = [ "ctor", "db", "editor", - "encoding_rs", + "encodings", "futures 0.3.31", "fuzzy", "git", @@ -8805,7 +8805,7 @@ dependencies = [ "ctor", "diffy", "ec4rs", - "encoding_rs", + "encodings", "fs", "futures 0.3.31", "fuzzy", @@ -13021,7 +13021,7 @@ dependencies = [ "context_server", "dap", "dap_adapters", - "encoding_rs", + "encodings", "extension", "fancy-regex 0.14.0", "fs", @@ -13997,7 +13997,7 @@ dependencies = [ "dap_adapters", "debug_adapter_extension", "editor", - "encoding_rs", + "encodings", "env_logger 0.11.8", "extension", "extension_host", @@ -20762,7 +20762,7 @@ dependencies = [ "component", "dap", "db", - "encoding_rs", + "encodings", "fs", "futures 0.3.31", "gpui", @@ -20805,7 +20805,7 @@ dependencies = [ "async-lock 2.8.0", "clock", "collections", - "encoding_rs", + "encodings", "fs", "futures 0.3.31", "fuzzy", @@ -21215,7 +21215,7 @@ dependencies = [ "diagnostics", "edit_prediction_button", "editor", - "encoding_rs", + "encodings", "encodings_ui", "env_logger 0.11.8", "extension", diff --git a/crates/agent/src/tools/edit_file_tool.rs b/crates/agent/src/tools/edit_file_tool.rs index 6764a1759e81d6..e7843d3388139f 100644 --- a/crates/agent/src/tools/edit_file_tool.rs +++ b/crates/agent/src/tools/edit_file_tool.rs @@ -563,8 +563,8 @@ mod tests { use super::*; use crate::{ContextServerRegistry, Templates}; use client::TelemetrySettings; - use encoding_rs::UTF_8; - use fs::{Fs, encodings::EncodingWrapper}; + use encodings::{Encoding, UTF_8}; + use fs::Fs; use gpui::{TestAppContext, UpdateGlobal}; use language_model::fake_provider::FakeLanguageModel; use prompt_store::ProjectContext; @@ -745,7 +745,7 @@ mod tests { path!("/root/src/main.rs").as_ref(), &Rope::from_str_small("initial content"), language::LineEnding::Unix, - EncodingWrapper::new(UTF_8), + Encoding::new(UTF_8), ) .await .unwrap(); @@ -913,7 +913,7 @@ mod tests { path!("/root/src/main.rs").as_ref(), &Rope::from_str_small("initial content"), language::LineEnding::Unix, - EncodingWrapper::new(UTF_8), + Encoding::default(), ) .await .unwrap(); diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 4135f07710970a..e481af9c834424 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -32,7 +32,7 @@ cloud_llm_client.workspace = true collections.workspace = true context_server.workspace = true db.workspace = true -encoding_rs.workspace = true +encodings.workspace = true fs.workspace = true futures.workspace = true git.workspace = true diff --git a/crates/assistant_tools/Cargo.toml b/crates/assistant_tools/Cargo.toml index dff6caccef9979..b31561305dec86 100644 --- a/crates/assistant_tools/Cargo.toml +++ b/crates/assistant_tools/Cargo.toml @@ -28,7 +28,7 @@ component.workspace = true derive_more.workspace = true diffy = "0.4.2" editor.workspace = true -encoding_rs.workspace = true +encodings.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 4a3e755a35dc79..eea65b173ff3ca 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -1229,9 +1229,10 @@ async fn build_buffer_diff( #[cfg(test)] mod tests { use super::*; - use ::fs::{Fs, encodings::EncodingWrapper}; + use ::fs::Fs; use client::TelemetrySettings; - use encoding_rs::UTF_8; + use encodings::Encoding; + use encodings::UTF_8; use gpui::{TestAppContext, UpdateGlobal}; use language_model::fake_provider::FakeLanguageModel; use serde_json::json; @@ -1500,7 +1501,7 @@ mod tests { path!("/root/src/main.rs").as_ref(), &"initial content".into(), language::LineEnding::Unix, - EncodingWrapper::new(UTF_8), + Encoding::new(UTF_8), ) .await .unwrap(); @@ -1670,7 +1671,7 @@ mod tests { path!("/root/src/main.rs").as_ref(), &"initial content".into(), language::LineEnding::Unix, - EncodingWrapper::new(UTF_8), + Encoding::default(), ) .await .unwrap(); diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 82a0b7141e3b04..01ef52291fa089 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -31,7 +31,7 @@ chrono.workspace = true clock.workspace = true collections.workspace = true dashmap.workspace = true -encoding_rs.workspace = true +encodings.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 0b65c91625801a..1adb2ae5fb65dc 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -12,8 +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 encoding_rs::UTF_8; -use fs::{FakeFs, Fs as _, RemoveOptions, encodings::EncodingWrapper}; +use encodings::Encoding; +use fs::{FakeFs, Fs as _, RemoveOptions}; use futures::{StreamExt as _, channel::mpsc}; use git::{ repository::repo_path, @@ -3702,7 +3702,7 @@ async fn test_buffer_reloading( path!("/dir/a.txt").as_ref(), &new_contents, LineEnding::Windows, - EncodingWrapper::new(UTF_8), + Encoding::default(), ) .await .unwrap(); @@ -4483,7 +4483,7 @@ async fn test_reloading_buffer_manually( path!("/a/a.rs").as_ref(), &Rope::from_str_small("let seven = 7;"), LineEnding::Unix, - EncodingWrapper::new(UTF_8), + Encoding::default(), ) .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 c7571665574d6c..9f0c47bcd722ef 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -5,8 +5,8 @@ use async_trait::async_trait; use call::ActiveCall; use collections::{BTreeMap, HashMap}; use editor::Bias; -use encoding_rs::UTF_8; -use fs::{FakeFs, Fs as _, encodings::EncodingWrapper}; +use encodings::Encoding; +use fs::{FakeFs, Fs as _}; use git::status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode}; use gpui::{BackgroundExecutor, Entity, TestAppContext}; use language::{ @@ -944,7 +944,7 @@ impl RandomizedTest for ProjectCollaborationTest { &path, &Rope::from_str_small(content.as_str()), text::LineEnding::Unix, - EncodingWrapper::new(UTF_8), + Encoding::default(), ) .await .unwrap(); diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 129afaaffb13b3..ba7209368a4f5a 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -30,7 +30,7 @@ client.workspace = true collections.workspace = true command_palette_hooks.workspace = true dirs.workspace = true -encoding_rs.workspace = true +encodings.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index e5dca4d7b48be3..28828db43170b9 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1241,8 +1241,7 @@ async fn get_copilot_lsp(fs: Arc, node_runtime: NodeRuntime) -> anyhow:: #[cfg(test)] mod tests { use super::*; - use fs::encodings::EncodingWrapper; - use encoding_rs::Encoding; + use encodings::Encoding; use gpui::TestAppContext; use util::{path, paths::PathStyle, rel_path::rel_path}; @@ -1453,7 +1452,14 @@ mod tests { self.abs_path.clone() } - fn load(&self, _: &App, _: EncodingWrapper, _: bool, _: bool, _: Option>>) -> Task> { + fn load( + &self, + _: &App, + _: Encoding, + _: bool, + _: bool, + _: Option>, + ) -> Task> { unimplemented!() } diff --git a/crates/encodings/src/lib.rs b/crates/encodings/src/lib.rs index e3baad90167037..556dfba766336f 100644 --- a/crates/encodings/src/lib.rs +++ b/crates/encodings/src/lib.rs @@ -22,6 +22,12 @@ impl Debug for Encoding { } } +impl Clone for Encoding { + fn clone(&self) -> Self { + Encoding(Mutex::new(self.get())) + } +} + impl Default for Encoding { fn default() -> Self { Encoding(Mutex::new(UTF_8)) diff --git a/crates/encodings_ui/src/lib.rs b/crates/encodings_ui/src/lib.rs index 591d6e5349350b..2e01c41e08cd51 100644 --- a/crates/encodings_ui/src/lib.rs +++ b/crates/encodings_ui/src/lib.rs @@ -120,7 +120,7 @@ impl EncodingIndicator { let editor = editor.read(cx); if let Some((_, buffer, _)) = editor.active_excerpt(cx) { let encoding = buffer.read(cx).encoding.clone(); - self.encoding = Some(&*encoding.lock().unwrap()); + self.encoding = Some(encoding.get()); if let Some(_) = buffer.read(cx).file() { self.show_save_or_reopen_selector = true; @@ -140,7 +140,7 @@ impl EncodingIndicator { cx: &mut Context, ) { let encoding = buffer.read(cx).encoding.clone(); - self.encoding = Some(&*encoding.lock().unwrap()); + self.encoding = Some(encoding.get()); cx.notify(); } } diff --git a/crates/encodings_ui/src/selectors.rs b/crates/encodings_ui/src/selectors.rs index f3345c57299d6a..b049c9583d4693 100644 --- a/crates/encodings_ui/src/selectors.rs +++ b/crates/encodings_ui/src/selectors.rs @@ -278,7 +278,6 @@ 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 editor::Editor; - use fs::encodings::EncodingWrapper; use std::{path::PathBuf, sync::atomic::AtomicBool}; use fuzzy::{StringMatch, StringMatchCandidate}; @@ -449,8 +448,9 @@ pub mod encoding { // By limiting the scope, we ensure that it is released { let buffer_encoding = buffer.encoding.clone(); - *buffer_encoding.lock().unwrap() = - encoding_from_name(self.matches[self.current_selection].string.as_str()); + buffer_encoding.set(encoding_from_name( + self.matches[self.current_selection].string.as_str(), + )); } self.dismissed(window, cx); @@ -481,8 +481,12 @@ pub mod encoding { encoding_from_name(self.matches[self.current_selection].string.as_str()); let open_task = workspace.update(cx, |workspace, cx| { - *workspace.encoding_options.encoding.lock().unwrap() = - EncodingWrapper::new(encoding); + workspace + .encoding_options + .encoding + .lock() + .unwrap() + .set(encoding); workspace.open_abs_path(path, OpenOptions::default(), window, cx) }); @@ -510,7 +514,7 @@ pub mod encoding { { buffer .read_with(cx, |buffer, _| { - *buffer.encoding.lock().unwrap() = encoding; + buffer.encoding.set(encoding); }) .log_err(); } diff --git a/crates/extension_host/Cargo.toml b/crates/extension_host/Cargo.toml index 4db3672bbd782e..229f2e2d185c4b 100644 --- a/crates/extension_host/Cargo.toml +++ b/crates/extension_host/Cargo.toml @@ -23,7 +23,7 @@ async-trait.workspace = true client.workspace = true collections.workspace = true dap.workspace = true -encoding_rs.workspace = true +encodings.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 f79d4fd00ea0e4..c58bce9d51a877 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -12,7 +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_rs::UTF_8; +use encodings::Encoding; pub use extension::ExtensionManifest; use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; use extension::{ @@ -21,7 +21,6 @@ use extension::{ ExtensionLanguageServerProxy, ExtensionSlashCommandProxy, ExtensionSnippetProxy, ExtensionThemeProxy, }; -use fs::encodings::EncodingWrapper; use fs::{Fs, RemoveOptions}; use futures::future::join_all; use futures::{ @@ -1508,7 +1507,7 @@ impl ExtensionStore { &index_path, &Rope::from_str(&index_json, &executor), Default::default(), - EncodingWrapper::new(UTF_8), + Encoding::default(), ) .await .context("failed to save extension index") @@ -1681,7 +1680,7 @@ impl ExtensionStore { &tmp_dir.join(EXTENSION_TOML), &Rope::from_str_small(&manifest_toml), language::LineEnding::Unix, - EncodingWrapper::new(UTF_8), + Encoding::default(), ) .await?; } else { diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index a9e1028f608d07..25867218d57176 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -16,7 +16,7 @@ anyhow.workspace = true async-tar.workspace = true async-trait.workspace = true collections.workspace = true -encoding_rs.workspace = true +encodings.workspace = true futures.workspace = true git.workspace = true gpui.workspace = true diff --git a/crates/fs/src/encodings.rs b/crates/fs/src/encodings.rs deleted file mode 100644 index 16aab79e77ad92..00000000000000 --- a/crates/fs/src/encodings.rs +++ /dev/null @@ -1,152 +0,0 @@ -//! Encoding and decoding utilities using the `encoding_rs` crate. -use std::{ - fmt::Debug, - sync::{Arc, Mutex}, -}; - -use std::sync::atomic::AtomicBool; - -use anyhow::Result; -use encoding_rs::Encoding; - -/// A wrapper around `encoding_rs::Encoding` to implement `Send` and `Sync`. -/// Since the reference is static, it is safe to send it across threads. -#[derive(Copy)] -pub struct EncodingWrapper(pub &'static Encoding); - -impl Debug for EncodingWrapper { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple(&format!("EncodingWrapper{:?}", self.0)) - .field(&self.0.name()) - .finish() - } -} - -impl Default for EncodingWrapper { - fn default() -> Self { - EncodingWrapper(encoding_rs::UTF_8) - } -} - -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 Encoding) -> EncodingWrapper { - EncodingWrapper(encoding) - } - - pub fn get_encoding(&self) -> &'static Encoding { - self.0 - } - - pub async fn decode( - &mut self, - input: Vec, - force: bool, - detect_utf16: bool, - buffer_encoding: Option>>, - ) -> Result { - // Check if the input starts with a BOM for UTF-16 encodings only if detect_utf16 is true. - if detect_utf16 { - if let Some(encoding) = match input.get(..2) { - Some([0xFF, 0xFE]) => Some(encoding_rs::UTF_16LE), - Some([0xFE, 0xFF]) => Some(encoding_rs::UTF_16BE), - _ => None, - } { - self.0 = encoding; - - if let Some(v) = buffer_encoding - && let Ok(mut v) = v.lock() - { - *v = encoding; - } - } - } - - let (cow, had_errors) = self.0.decode_with_bom_removal(&input); - - if force { - return Ok(cow.to_string()); - } - - if !had_errors { - Ok(cow.to_string()) - } else { - Err(anyhow::anyhow!( - "The file contains invalid bytes for the specified encoding: {}.\nThis usually means that the file is not a regular text file, or is encoded in a different encoding.\nContinuing to open it may result in data loss if saved.", - self.0.name() - )) - } - } - - pub async fn encode(&self, input: String) -> Result> { - if self.0 == encoding_rs::UTF_16BE { - let mut data = Vec::::with_capacity(input.len() * 2); - - // Convert the input string to UTF-16BE bytes - let utf16be_bytes = input.encode_utf16().flat_map(|u| u.to_be_bytes()); - - data.extend(utf16be_bytes); - return Ok(data); - } else if self.0 == encoding_rs::UTF_16LE { - let mut data = Vec::::with_capacity(input.len() * 2); - - // Convert the input string to UTF-16LE bytes - let utf16le_bytes = input.encode_utf16().flat_map(|u| u.to_le_bytes()); - - data.extend(utf16le_bytes); - return Ok(data); - } else { - let (cow, _encoding_used, _had_errors) = self.0.encode(&input); - - Ok(cow.into_owned()) - } - } -} - -/// Convert a byte vector from a specified encoding to a UTF-8 string. -pub async fn to_utf8( - input: Vec, - mut encoding: EncodingWrapper, - force: bool, - detect_utf16: bool, - buffer_encoding: Option>>, -) -> Result { - encoding - .decode(input, force, detect_utf16, buffer_encoding) - .await -} - -/// Convert a UTF-8 string to a byte vector in a specified encoding. -pub async fn from_utf8(input: String, target: EncodingWrapper) -> Result> { - target.encode(input).await -} - -pub struct EncodingOptions { - pub encoding: Arc>, - pub force: AtomicBool, - pub detect_utf16: AtomicBool, -} - -impl Default for EncodingOptions { - fn default() -> Self { - EncodingOptions { - encoding: Arc::new(Mutex::new(EncodingWrapper::default())), - force: AtomicBool::new(false), - detect_utf16: AtomicBool::new(true), - } - } -} diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 344d9da893b0fb..00dd220fbaa16f 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -1,7 +1,6 @@ #[cfg(target_os = "macos")] mod mac_watcher; -pub mod encodings; #[cfg(not(target_os = "macos"))] pub mod fs_watcher; @@ -9,7 +8,7 @@ use anyhow::{Context as _, Result, anyhow}; #[cfg(any(target_os = "linux", target_os = "freebsd"))] use ashpd::desktop::trash; use futures::stream::iter; -use encoding_rs::Encoding; +use encodings::Encoding; use gpui::App; use gpui::BackgroundExecutor; use gpui::Global; @@ -62,9 +61,9 @@ use std::ffi::OsStr; #[cfg(any(test, feature = "test-support"))] pub use fake_git_repo::{LOAD_HEAD_TEXT_TASK, LOAD_INDEX_TEXT_TASK}; -use crate::encodings::EncodingWrapper; -use crate::encodings::from_utf8; -use crate::encodings::to_utf8; +use encodings::Encoding; +use encodings::from_utf8; +use encodings::to_utf8; pub trait Watcher: Send + Sync { fn add(&self, path: &Path) -> Result<()>; @@ -124,10 +123,10 @@ pub trait Fs: Send + Sync { async fn load_with_encoding( &self, path: &Path, - encoding: EncodingWrapper, + encoding: Encoding, force: bool, detect_utf16: bool, - buffer_encoding: Option>>, + buffer_encoding: Option>, ) -> Result { Ok(to_utf8( self.load_bytes(path).await?, @@ -146,7 +145,7 @@ pub trait Fs: Send + Sync { path: &Path, text: &Rope, line_ending: LineEnding, - encoding: EncodingWrapper, + encoding: Encoding, ) -> Result<()>; async fn write(&self, path: &Path, content: &[u8]) -> Result<()>; async fn canonicalize(&self, path: &Path) -> Result; @@ -693,7 +692,7 @@ impl Fs for RealFs { path: &Path, text: &Rope, line_ending: LineEnding, - encoding: EncodingWrapper, + encoding: Encoding, ) -> Result<()> { let buffer_size = text.summary().len.min(10 * 1024); if let Some(path) = path.parent() { @@ -704,18 +703,18 @@ impl Fs for RealFs { // BOM for UTF-16 is written at the start of the file here because // if BOM is written in the `encode` function of `fs::encodings`, it would be written - // for every chunk, resulting in multiple BOMs in the file. - if encoding.get_encoding() == encoding_rs::UTF_16BE { + // twice. Hence, it is written only here. + if encoding.get() == encodings::UTF_16BE { // Write BOM for UTF-16BE writer.write_all(&[0xFE, 0xFF]).await?; - } else if encoding.get_encoding() == encoding_rs::UTF_16LE { + } else if encoding.get() == encodings::UTF_16LE { // Write BOM for UTF-16LE writer.write_all(&[0xFF, 0xFE]).await?; } for chunk in chunks(text, line_ending) { writer - .write_all(&from_utf8(chunk.to_string(), encoding.clone()).await?) + .write_all(&from_utf8(chunk.to_string(), Encoding::new(encoding.get())).await?) .await? } @@ -2435,9 +2434,9 @@ impl Fs for FakeFs { path: &Path, text: &Rope, line_ending: LineEnding, - encoding: EncodingWrapper, + encoding: Encoding, ) -> Result<()> { - use crate::encodings::from_utf8; + use encodings::from_utf8; self.simulate_random_delay().await; let path = normalize_path(path); diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 9ab0042edd81f1..6a8d9d3daf10b6 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -29,7 +29,7 @@ command_palette_hooks.workspace = true component.workspace = true db.workspace = true editor.workspace = true -encoding_rs.workspace = true +encodings.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 a14524a2caff0b..7746ac8519250d 100644 --- a/crates/git_ui/src/file_diff_view.rs +++ b/crates/git_ui/src/file_diff_view.rs @@ -358,7 +358,7 @@ impl Render for FileDiffView { mod tests { use super::*; use editor::test::editor_test_context::assert_state_with_diff; - use encoding_rs::UTF_8; + use encodings::Encoding; use gpui::TestAppContext; use language::Rope; use project::{FakeFs, Fs, Project}; @@ -442,7 +442,7 @@ mod tests { ", )), Default::default(), - EncodingWrapper::new(UTF_8), + Encoding::default(), ) .await .unwrap(); @@ -477,7 +477,7 @@ mod tests { ", )), Default::default(), - EncodingWrapper::new(UTF_8), + Encoding::default(), ) .await .unwrap(); diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 42610ce9c59983..2afc02893fd6a7 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -32,7 +32,7 @@ clock.workspace = true collections.workspace = true diffy = "0.4.2" ec4rs.workspace = true -encoding_rs.workspace = true +encodings.workspace = true fs.workspace = true futures.workspace = true fuzzy.workspace = true diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 2668e4dc75d41b..dd5569ce645b8b 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -21,8 +21,8 @@ use anyhow::{Context as _, Result}; use clock::Lamport; pub use clock::ReplicaId; use collections::HashMap; -use encoding_rs::Encoding; -use fs::{MTime, encodings::EncodingWrapper}; +use encodings::Encoding; +use fs::MTime; use futures::channel::oneshot; use gpui::{ App, AppContext as _, BackgroundExecutor, Context, Entity, EventEmitter, HighlightStyle, @@ -127,7 +127,7 @@ pub struct Buffer { has_unsaved_edits: Cell<(clock::Global, bool)>, change_bits: Vec>>, _subscriptions: Vec, - pub encoding: Arc>, + pub encoding: Arc, pub observe_file_encoding: Option, } @@ -375,7 +375,7 @@ pub trait File: Send + Sync + Any { /// Return whether Zed considers this to be a private file. fn is_private(&self) -> bool; - fn encoding(&self) -> Option>> { + fn encoding(&self) -> Option> { unimplemented!() } } @@ -422,10 +422,10 @@ pub trait LocalFile: File { fn load( &self, cx: &App, - encoding: EncodingWrapper, + encoding: Encoding, force: bool, detect_utf16: bool, - buffer_encoding: Option>>, + buffer_encoding: Option>, ) -> Task>; /// Loads the file's contents from disk. @@ -1032,7 +1032,7 @@ impl Buffer { has_conflict: false, change_bits: Default::default(), _subscriptions: Vec::new(), - encoding: Arc::new(std::sync::Mutex::new(encoding_rs::UTF_8)), + encoding: Arc::new(Encoding::new(encodings::UTF_8)), observe_file_encoding: None, } } @@ -1369,7 +1369,8 @@ 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 = EncodingWrapper::new(*(self.encoding.lock().unwrap())); + let encoding = self.encoding.clone(); + let buffer_encoding = self.encoding.clone(); let prev_version = self.text.version(); @@ -1377,7 +1378,7 @@ impl Buffer { 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, encoding, false, true, Some(buffer_encoding)) + file.load(cx, (*encoding).clone(), false, true, Some(buffer_encoding)) })) })? else { @@ -2939,9 +2940,9 @@ impl Buffer { pub fn update_encoding(&mut self) { if let Some(file) = self.file() { if let Some(encoding) = file.encoding() { - *self.encoding.lock().unwrap() = *encoding.lock().unwrap(); + self.encoding.set(encoding.get()); } else { - *self.encoding.lock().unwrap() = encoding_rs::UTF_8; + self.encoding.set(encodings::UTF_8); }; } } @@ -5269,10 +5270,10 @@ impl LocalFile for TestFile { fn load( &self, _cx: &App, - _encoding: EncodingWrapper, + _encoding: Encoding, _force: bool, _detect_utf16: bool, - _buffer_encoding: Option>>, + _buffer_encoding: Option>, ) -> Task> { unimplemented!() } diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index d000a036ec7617..d05cdef0b3d1c7 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -39,7 +39,7 @@ clock.workspace = true collections.workspace = true context_server.workspace = true dap.workspace = true -encoding_rs.workspace = true +encodings.workspace = true extension.workspace = true fancy-regex.workspace = true fs.workspace = true diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 4a8f5170451288..97cd401624b63f 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -7,9 +7,9 @@ use crate::{ use anyhow::{Context as _, Result, anyhow}; use client::Client; use collections::{HashMap, HashSet, hash_map}; +use encodings::Encoding; use fs::Fs; use futures::{Future, FutureExt as _, StreamExt, channel::oneshot, future::Shared}; -use fs::{Fs, encodings::EncodingWrapper}; use gpui::{ App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, }; @@ -398,13 +398,7 @@ impl LocalBufferStore { } let save = worktree.update(cx, |worktree, cx| { - worktree.write_file( - path.as_ref(), - text, - line_ending, - cx, - &encoding.lock().unwrap(), - ) + worktree.write_file(path.as_ref(), text, line_ending, cx, (*encoding).clone()) }); cx.spawn(async move |this, cx| { @@ -634,7 +628,7 @@ impl LocalBufferStore { &self, path: Arc, worktree: Entity, - encoding: Option, + encoding: Option, force: bool, detect_utf16: bool, cx: &mut Context, @@ -643,8 +637,14 @@ impl LocalBufferStore { let reservation = cx.reserve_entity(); let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64()); - let load_file_task = - worktree.load_file(path.as_ref(), encoding, force, detect_utf16, None, cx); + let load_file_task = worktree.load_file( + path.as_ref(), + encoding.clone(), + force, + detect_utf16, + None, + cx, + ); cx.spawn(async move |_, cx| { let loaded_file = load_file_task.await?; @@ -681,13 +681,11 @@ impl LocalBufferStore { entry_id: None, is_local: true, is_private: false, - encoding: Some(Arc::new(std::sync::Mutex::new( - if let Some(encoding) = encoding { - encoding.0 - } else { - encoding_rs::UTF_8 - }, - ))), + encoding: Some(Arc::new(if let Some(encoding) = encoding { + encoding + } else { + Encoding::default() + })), })), Capability::ReadWrite, ) @@ -845,7 +843,7 @@ impl BufferStore { pub fn open_buffer( &mut self, project_path: ProjectPath, - encoding: Option, + encoding: Option, force: bool, detect_utf16: bool, cx: &mut Context, diff --git a/crates/project/src/prettier_store.rs b/crates/project/src/prettier_store.rs index ccdfb9661536a4..9539146ebb4029 100644 --- a/crates/project/src/prettier_store.rs +++ b/crates/project/src/prettier_store.rs @@ -7,8 +7,8 @@ use std::{ use anyhow::{Context as _, Result, anyhow}; use collections::{HashMap, HashSet}; -use encoding_rs::UTF_8; -use fs::{Fs, encodings::EncodingWrapper}; +use encodings::Encoding; +use fs::Fs; use futures::{ FutureExt, future::{self, Shared}, @@ -982,12 +982,12 @@ async fn save_prettier_server_file( executor: &BackgroundExecutor, ) -> anyhow::Result<()> { let prettier_wrapper_path = default_prettier_dir().join(prettier::PRETTIER_SERVER_FILE); - let encoding_wrapper = EncodingWrapper::new(UTF_8); + let encoding = Encoding::default(); fs.save( &prettier_wrapper_path, &text::Rope::from_str(prettier::PRETTIER_SERVER_JS, executor), text::LineEnding::Unix, - encoding_wrapper, + encoding, ) .await .with_context(|| { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index cf3750462f583c..5b3540e4e89c85 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -26,13 +26,11 @@ mod project_tests; mod environment; use buffer_diff::BufferDiff; use context_server_store::ContextServerStore; -use encoding_rs::Encoding; + +use encodings::EncodingOptions; pub use environment::ProjectEnvironmentEvent; -use fs::encodings::EncodingOptions; -use fs::encodings::EncodingWrapper; use git::repository::get_git_committer; use git_store::{Repository, RepositoryId}; -use std::sync::atomic::AtomicBool; pub mod search_history; mod yarn; @@ -1232,11 +1230,7 @@ impl Project { toolchain_store: Some(toolchain_store), agent_location: None, - encoding_options: EncodingOptions { - encoding: Arc::new(std::sync::Mutex::new(EncodingWrapper::default())), - force: AtomicBool::new(false), - detect_utf16: AtomicBool::new(true), - }, + encoding_options: EncodingOptions::default(), } }) } @@ -1422,11 +1416,7 @@ impl Project { toolchain_store: Some(toolchain_store), agent_location: None, - encoding_options: EncodingOptions { - encoding: Arc::new(std::sync::Mutex::new(EncodingWrapper::default())), - force: AtomicBool::new(false), - detect_utf16: AtomicBool::new(false), - }, + encoding_options: EncodingOptions::default(), }; // remote server -> local machine handlers @@ -1680,12 +1670,7 @@ impl Project { remotely_created_models: Arc::new(Mutex::new(RemotelyCreatedModels::default())), toolchain_store: None, agent_location: None, - encoding_options: EncodingOptions { - encoding: Arc::new(std::sync::Mutex::new(EncodingWrapper::default())), - - force: AtomicBool::new(false), - detect_utf16: AtomicBool::new(false), - }, + encoding_options: EncodingOptions::default(), }; project.set_role(role, cx); diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index ce70464eae1d38..8a78c3e0082b28 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -12,8 +12,8 @@ use buffer_diff::{ BufferDiffEvent, CALCULATE_DIFF_TASK, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind, assert_hunks, }; -use encoding_rs::UTF_8; -use fs::{FakeFs, encodings::EncodingWrapper}; +use encodings::{Encoding, UTF_8}; +use fs::FakeFs; use futures::{StreamExt, future}; use git::{ GitHostingProviderRegistry, @@ -1461,13 +1461,13 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon .await .unwrap(); - let encoding_wrapper = EncodingWrapper::new(UTF_8); + let encoding = Encoding::default(); fs.save( path!("/the-root/Cargo.lock").as_ref(), &Rope::default(), Default::default(), - encoding_wrapper.clone(), + encoding.clone(), ) .await .unwrap(); @@ -1475,7 +1475,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon path!("/the-stdlib/LICENSE").as_ref(), &Rope::default(), Default::default(), - encoding_wrapper.clone(), + encoding.clone(), ) .await .unwrap(); @@ -1483,7 +1483,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon path!("/the/stdlib/src/string.rs").as_ref(), &Rope::default(), Default::default(), - encoding_wrapper, + encoding, ) .await .unwrap(); @@ -4075,7 +4075,7 @@ 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); + let encoding = Encoding::default(); // 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. @@ -4083,7 +4083,7 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) path!("/dir/file1").as_ref(), &Rope::from_str("the first contents", cx.background_executor()), Default::default(), - encoding_wrapper.clone(), + encoding.clone(), ) .await .unwrap(); @@ -4095,7 +4095,7 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) path!("/dir/file1").as_ref(), &Rope::from_str("the second contents", cx.background_executor()), Default::default(), - encoding_wrapper, + encoding, ) .await .unwrap(); @@ -4134,7 +4134,7 @@ 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); + let encoding = Encoding::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. @@ -4142,7 +4142,7 @@ async fn test_edit_buffer_while_it_reloads(cx: &mut gpui::TestAppContext) { path!("/dir/file1").as_ref(), &Rope::from_str("the first contents", cx.background_executor()), Default::default(), - encoding_wrapper, + encoding, ) .await .unwrap(); @@ -4818,13 +4818,13 @@ 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); + let encoding = Encoding::new(UTF_8); fs.save( path!("/dir/the-file").as_ref(), &Rope::from_str(new_contents.as_str(), cx.background_executor()), LineEnding::Unix, - encoding_wrapper, + encoding, ) .await .unwrap(); @@ -4852,14 +4852,14 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { assert!(!buffer.has_conflict()); }); - let encoding_wrapper = EncodingWrapper::new(UTF_8); + let encoding = Encoding::new(UTF_8); // Change the file on disk again, adding blank lines to the beginning. fs.save( path!("/dir/the-file").as_ref(), &Rope::from_str("\n\n\nAAAA\naaa\nBB\nbbbbb\n", cx.background_executor()), LineEnding::Unix, - encoding_wrapper, + encoding, ) .await .unwrap(); @@ -4906,7 +4906,7 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { assert_eq!(buffer.line_ending(), LineEnding::Windows); }); - let encoding_wrapper = EncodingWrapper::new(UTF_8); + let encoding = Encoding::new(UTF_8); // Change a file's line endings on disk from unix to windows. The buffer's // state updates correctly. @@ -4914,7 +4914,7 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { path!("/dir/file1").as_ref(), &Rope::from_str("aaa\nb\nc\n", cx.background_executor()), LineEnding::Windows, - encoding_wrapper, + encoding, ) .await .unwrap(); diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 9acb391e9f357d..ac8c680d4e5218 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -28,7 +28,7 @@ clap.workspace = true client.workspace = true dap_adapters.workspace = true debug_adapter_extension.workspace = true -encoding_rs.workspace = true +encodings.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 4fff49de6af4b2..aadf2c2376f360 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -6,12 +6,12 @@ use agent::{AgentTool, ReadFileTool, ReadFileToolInput, ToolCallEventStream}; use client::{Client, UserStore}; use clock::FakeSystemClock; use collections::{HashMap, HashSet}; -use encoding_rs::UTF_8; use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModel}; +use encodings::Encoding; use extension::ExtensionHostProxy; -use fs::{FakeFs, Fs, encodings::EncodingWrapper}; -use gpui::{AppContext as _, Entity, SemanticVersion, SharedString, TestAppContext}; +use fs::{FakeFs, Fs}; +use gpui::{AppContext as _, Entity, SemanticVersion, TestAppContext}; use http_client::{BlockedHttpClient, FakeHttpClient}; use language::{ Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding, Rope, @@ -123,7 +123,7 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test path!("/code/project1/src/main.rs").as_ref(), &Rope::from_str_small("fn main() {}"), Default::default(), - EncodingWrapper::new(UTF_8), + Encoding::default(), ) .await .unwrap(); @@ -770,7 +770,7 @@ async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppCont &PathBuf::from(path!("/code/project1/src/lib.rs")), &Rope::from_str_small("bangles"), LineEnding::Unix, - EncodingWrapper::new(UTF_8), + Encoding::default(), ) .await .unwrap(); @@ -786,7 +786,7 @@ async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppCont &PathBuf::from(path!("/code/project1/src/lib.rs")), &Rope::from_str_small("bloop"), LineEnding::Unix, - EncodingWrapper::new(UTF_8), + Encoding::default(), ) .await .unwrap(); diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 14ee5692d4b4eb..c1d0153fc08441 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -35,7 +35,7 @@ clock.workspace = true collections.workspace = true component.workspace = true db.workspace = true -encoding_rs.workspace = true +encodings.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 60111bf47dd498..68c7d704f757d7 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -19,8 +19,8 @@ mod workspace_settings; pub use crate::notifications::NotificationFrame; pub use dock::Panel; -use encoding_rs::UTF_8; -use fs::encodings::EncodingWrapper; +use encodings::Encoding; + pub use path_list::PathList; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; @@ -32,7 +32,7 @@ use client::{ }; use collections::{HashMap, HashSet, hash_map}; use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE}; -use fs::encodings::EncodingOptions; +use encodings::EncodingOptions; use futures::{ Future, FutureExt, StreamExt, @@ -625,7 +625,7 @@ type BuildProjectItemForPathFn = fn( &Entity, &ProjectPath, - Option, + Option, &mut Window, &mut App, ) -> Option, WorkspaceItemBuilder)>>>; @@ -649,9 +649,9 @@ impl ProjectItemRegistry { self.build_project_item_for_path_fns .push(|project, project_path, encoding, window, cx| { let project_path = project_path.clone(); - let EncodingWrapper(encoding) = encoding.unwrap_or_default(); + let encoding = encoding.unwrap_or_default(); - project.update(cx, |project, _| {*project.encoding_options.encoding.lock().unwrap() = EncodingWrapper::new(encoding)}); + project.update(cx, |project, _| {project.encoding_options.encoding.lock().unwrap().set(encoding.get())}); let is_file = project .read(cx) @@ -721,7 +721,7 @@ impl ProjectItemRegistry { &self, project: &Entity, path: &ProjectPath, - encoding: Option, + encoding: Option, window: &mut Window, cx: &mut App, ) -> Task, WorkspaceItemBuilder)>> { @@ -1945,9 +1945,7 @@ impl Workspace { window: &mut Window, cx: &mut Context, ) -> Task> { - *self.encoding_options.force.get_mut() = false; - *self.encoding_options.detect_utf16.get_mut() = true; - *self.encoding_options.encoding.lock().unwrap() = EncodingWrapper::new(encoding_rs::UTF_8); + self.encoding_options.reset(); let to_load = if let Some(pane) = pane.upgrade() { pane.update(cx, |pane, cx| { @@ -3593,9 +3591,7 @@ impl Workspace { registry.open_path( project, &path, - Some(EncodingWrapper::new( - (self.encoding_options.encoding.lock().unwrap()).0, - )), + Some(self.encoding_options.encoding.lock().unwrap().clone()), window, cx, ) @@ -7622,12 +7618,11 @@ 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?; - let encoding_wrapper = EncodingWrapper::new(UTF_8); fs.save( path, &default_content(), Default::default(), - encoding_wrapper, + Default::default(), ) .await?; } diff --git a/crates/worktree/Cargo.toml b/crates/worktree/Cargo.toml index 8166825a1c9775..46ad794b2886a8 100644 --- a/crates/worktree/Cargo.toml +++ b/crates/worktree/Cargo.toml @@ -27,7 +27,7 @@ anyhow.workspace = true async-lock.workspace = true clock.workspace = true collections.workspace = true -encoding_rs.workspace = true +encodings.workspace = true fs.workspace = true futures.workspace = true fuzzy.workspace = true diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 2891a3975770f3..a36a81f6d95ce5 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -7,11 +7,8 @@ use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{Context as _, Result, anyhow}; use clock::ReplicaId; use collections::{HashMap, HashSet, VecDeque}; -use encoding_rs::Encoding; -use fs::{ - Fs, MTime, PathEvent, RemoveOptions, Watcher, copy_recursive, encodings::EncodingWrapper, - read_dir_items, -}; +use encodings::Encoding; +use fs::{Fs, MTime, PathEvent, RemoveOptions, Watcher, copy_recursive, read_dir_items}; use futures::{ FutureExt as _, Stream, StreamExt, channel::{ @@ -710,10 +707,10 @@ impl Worktree { pub fn load_file( &self, path: &Path, - encoding: Option, + encoding: Option, force: bool, detect_utf16: bool, - buffer_encoding: Option>>, + buffer_encoding: Option>, cx: &Context, ) -> Task> { match self { @@ -745,7 +742,7 @@ impl Worktree { text: Rope, line_ending: LineEnding, cx: &Context, - encoding: &'static Encoding, + encoding: Encoding, ) -> Task>> { match self { Worktree::Local(this) => this.write_file(path, text, line_ending, cx, encoding), @@ -1330,10 +1327,10 @@ impl LocalWorktree { fn load_file( &self, path: &Path, - encoding: Option, + encoding: Option, force: bool, detect_utf16: bool, - buffer_encoding: Option>>, + buffer_encoding: Option>, cx: &Context, ) -> Task> { let path = Arc::from(path); @@ -1361,14 +1358,14 @@ impl LocalWorktree { let text = fs .load_with_encoding( &abs_path, - if let Some(encoding) = encoding { - encoding + if let Some(ref encoding) = encoding { + Encoding::new(encoding.get()) } else { - EncodingWrapper::new(encoding_rs::UTF_8) + Encoding::new(encodings::UTF_8) }, force, detect_utf16, - buffer_encoding, + buffer_encoding.clone(), ) .await?; @@ -1394,11 +1391,7 @@ impl LocalWorktree { }, is_local: true, is_private, - encoding: if let Some(encoding) = encoding { - Some(Arc::new(std::sync::Mutex::new(encoding.0))) - } else { - None - }, + encoding: encoding.map(|e| Arc::new(Encoding::new(e.get()))), }) } }; @@ -1487,20 +1480,18 @@ impl LocalWorktree { text: Rope, line_ending: LineEnding, cx: &Context, - encoding: &'static Encoding, + encoding: Encoding, ) -> Task>> { let fs = self.fs.clone(); let is_private = self.is_path_private(&path); let abs_path = self.absolutize(&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, encoding_wrapper) - .await + { + let encoding = encoding.clone(); + async move { fs.save(&abs_path, &text, line_ending, encoding).await } } }); @@ -1535,7 +1526,7 @@ impl LocalWorktree { entry_id: None, is_local: true, is_private, - encoding: Some(Arc::new(std::sync::Mutex::new(encoding))), + encoding: Some(Arc::new(encoding)), })) } }) @@ -3097,7 +3088,7 @@ pub struct File { pub entry_id: Option, pub is_local: bool, pub is_private: bool, - pub encoding: Option>>, + pub encoding: Option>, } impl PartialEq for File { @@ -3111,7 +3102,7 @@ impl PartialEq for File { && if let Some(encoding) = &self.encoding && let Some(other_encoding) = &other.encoding { - if *encoding.lock().unwrap() != *other_encoding.lock().unwrap() { + if encoding.get() != other_encoding.get() { false } else { true @@ -3171,7 +3162,8 @@ impl language::File for File { fn path_style(&self, cx: &App) -> PathStyle { self.worktree.read(cx).path_style() - fn encoding(&self) -> Option>> { + } + fn encoding(&self) -> Option> { if let Some(encoding) = &self.encoding { Some(encoding.clone()) } else { @@ -3188,10 +3180,10 @@ impl language::LocalFile for File { fn load( &self, cx: &App, - encoding: EncodingWrapper, + encoding: Encoding, force: bool, detect_utf16: bool, - buffer_encoding: Option>>, + buffer_encoding: Option>, ) -> Task> { let worktree = self.worktree.read(cx).as_local().unwrap(); let abs_path = worktree.absolutize(&self.path); diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 954729ef9a343a..69ee98f7bdf3ba 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -3,8 +3,7 @@ use crate::{ worktree_settings::WorktreeSettings, }; use anyhow::Result; -use encoding_rs::UTF_8; -use fs::{FakeFs, Fs, RealFs, RemoveOptions, encodings::EncodingWrapper}; +use fs::{FakeFs, Fs, RealFs, RemoveOptions}; use git::GITIGNORE; use gpui::{AppContext as _, BackgroundExecutor, BorrowAppContext, Context, Task, TestAppContext}; use parking_lot::Mutex; @@ -666,7 +665,7 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) { "/root/.gitignore".as_ref(), &Rope::from_str("e", cx.background_executor()), Default::default(), - encoding_wrapper, + Default::default(), ) .await .unwrap(); @@ -740,7 +739,7 @@ async fn test_write_file(cx: &mut TestAppContext) { Rope::from_str("hello", cx.background_executor()), Default::default(), cx, - UTF_8, + Default::default(), ) }) .await @@ -752,7 +751,7 @@ async fn test_write_file(cx: &mut TestAppContext) { Rope::from_str("world", cx.background_executor()), Default::default(), cx, - UTF_8, + Default::default(), ) }) .await @@ -1787,7 +1786,7 @@ fn randomly_mutate_worktree( Rope::default(), Default::default(), cx, - UTF_8, + Default::default(), ); cx.background_spawn(async move { task.await?; @@ -1876,12 +1875,11 @@ 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, &Rope::from_str(ignore_contents.as_str(), executor), Default::default(), - encoding_wrapper, + Default::default(), ) .await .unwrap(); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 4bf7f2824e9d20..139f1d63ae792a 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -52,7 +52,8 @@ debugger_ui.workspace = true diagnostics.workspace = true editor.workspace = true zeta2_tools.workspace = true -encoding.workspace = true +edit_prediction_tools.workspace = true +encodings.workspace = true encodings_ui.workspace = true env_logger.workspace = true extension.workspace = true diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 2c5479f5b90316..134c0eb452fb52 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2177,8 +2177,8 @@ mod tests { use assets::Assets; use collections::HashSet; use editor::{DisplayPoint, Editor, SelectionEffects, display_map::DisplayRow}; - use encoding_rs::UTF_8; - use fs::encodings::EncodingWrapper; + use encodings::Encoding; + use gpui::{ Action, AnyWindowHandle, App, AssetSource, BorrowAppContext, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext, WindowHandle, actions, @@ -4384,7 +4384,7 @@ mod tests { "/settings.json".as_ref(), &Rope::from_str_small(r#"{"base_keymap": "Atom"}"#), Default::default(), - EncodingWrapper::new(UTF_8), + Encoding::default(), ) .await .unwrap(); @@ -4395,7 +4395,7 @@ mod tests { "/keymap.json".as_ref(), &Rope::from_str_small(r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#), Default::default(), - EncodingWrapper::new(UTF_8), + Encoding::default(), ) .await .unwrap(); @@ -4444,7 +4444,7 @@ mod tests { "/keymap.json".as_ref(), &Rope::from_str_small(r#"[{"bindings": {"backspace": "test_only::ActionB"}}]"#), Default::default(), - EncodingWrapper::new(UTF_8), + Encoding::default(), ) .await .unwrap(); @@ -4465,7 +4465,7 @@ mod tests { "/settings.json".as_ref(), &Rope::from_str_small(r#"{"base_keymap": "JetBrains"}"#), Default::default(), - EncodingWrapper::new(UTF_8), + Encoding::default(), ) .await .unwrap(); @@ -4506,7 +4506,7 @@ mod tests { "/settings.json".as_ref(), &Rope::from_str_small(r#"{"base_keymap": "Atom"}"#), Default::default(), - EncodingWrapper::new(UTF_8), + Encoding::default(), ) .await .unwrap(); @@ -4516,7 +4516,7 @@ mod tests { "/keymap.json".as_ref(), &Rope::from_str_small(r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#), Default::default(), - EncodingWrapper::new(UTF_8), + Encoding::default(), ) .await .unwrap(); @@ -4560,7 +4560,7 @@ mod tests { "/keymap.json".as_ref(), &Rope::from_str_small(r#"[{"bindings": {"backspace": null}}]"#), Default::default(), - EncodingWrapper::new(UTF_8), + Encoding::default(), ) .await .unwrap(); @@ -4581,7 +4581,7 @@ mod tests { "/settings.json".as_ref(), &Rope::from_str_small(r#"{"base_keymap": "JetBrains"}"#), Default::default(), - EncodingWrapper::new(UTF_8), + Encoding::default(), ) .await .unwrap(); From b2187e5f9394a10fea4ed03f75115e6fa22cc3cc Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Tue, 21 Oct 2025 17:42:04 +0530 Subject: [PATCH 36/43] - Fix an issue that caused UTF-8 to be used when a file was closed and re-opened, while retaining the text. - Fix an issue that prevented `InvalidBufferView` from being shown when an incorrect encoding was chosen from the status bar. - Centre the error message in `InvalidBufferView`. --- Cargo.lock | 1 + crates/encodings/src/lib.rs | 16 +-- crates/encodings_ui/Cargo.toml | 1 + crates/encodings_ui/src/lib.rs | 25 +++-- crates/encodings_ui/src/selectors.rs | 130 +++++++++++++++++------- crates/language/src/buffer.rs | 5 +- crates/project/src/buffer_store.rs | 12 ++- crates/project/src/invalid_item_view.rs | 19 ++-- crates/project/src/project.rs | 11 +- crates/workspace/src/workspace.rs | 10 +- 10 files changed, 149 insertions(+), 81 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c4a3210ab2d248..2ce055edf4a714 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5535,6 +5535,7 @@ dependencies = [ name = "encodings_ui" version = "0.1.0" dependencies = [ + "anyhow", "editor", "encoding_rs", "fs", diff --git a/crates/encodings/src/lib.rs b/crates/encodings/src/lib.rs index 556dfba766336f..f5f62394ee7182 100644 --- a/crates/encodings/src/lib.rs +++ b/crates/encodings/src/lib.rs @@ -136,23 +136,27 @@ pub async fn from_utf8(input: String, target: Encoding) -> anyhow::Result>, + pub encoding: Arc, pub force: AtomicBool, pub detect_utf16: AtomicBool, } impl EncodingOptions { - pub fn reset(&mut self) { - self.encoding.lock().unwrap().reset(); - *self.force.get_mut() = false; - *self.detect_utf16.get_mut() = true; + pub fn reset(&self) { + self.encoding.reset(); + + self.force + .store(false, std::sync::atomic::Ordering::Release); + + self.detect_utf16 + .store(true, std::sync::atomic::Ordering::Release); } } impl Default for EncodingOptions { fn default() -> Self { EncodingOptions { - encoding: Arc::new(Mutex::new(Encoding::default())), + encoding: Arc::new(Encoding::default()), force: AtomicBool::new(false), detect_utf16: AtomicBool::new(true), } diff --git a/crates/encodings_ui/Cargo.toml b/crates/encodings_ui/Cargo.toml index bc83885841456f..31a7783d133202 100644 --- a/crates/encodings_ui/Cargo.toml +++ b/crates/encodings_ui/Cargo.toml @@ -5,6 +5,7 @@ publish.workspace = true edition.workspace = true [dependencies] +anyhow.workspace = true editor.workspace = true encoding_rs.workspace = true fs.workspace = true diff --git a/crates/encodings_ui/src/lib.rs b/crates/encodings_ui/src/lib.rs index 2e01c41e08cd51..b77cf991505aaa 100644 --- a/crates/encodings_ui/src/lib.rs +++ b/crates/encodings_ui/src/lib.rs @@ -74,17 +74,20 @@ impl Render for EncodingIndicator { let weak_workspace = workspace.weak_handle(); - workspace.toggle_modal(window, cx, |window, cx| { - let selector = EncodingSelector::new( - window, - cx, - Action::Save, - Some(buffer.downgrade()), - weak_workspace, - None, - ); - selector - }) + if let Some(path) = buffer.read(cx).file() { + let path = path.clone().path().to_path_buf(); + workspace.toggle_modal(window, cx, |window, cx| { + let selector = EncodingSelector::new( + window, + cx, + Action::Save, + Some(buffer.downgrade()), + weak_workspace, + Some(path), + ); + selector + }); + } } }) } diff --git a/crates/encodings_ui/src/selectors.rs b/crates/encodings_ui/src/selectors.rs index b049c9583d4693..f4edecd0428d1f 100644 --- a/crates/encodings_ui/src/selectors.rs +++ b/crates/encodings_ui/src/selectors.rs @@ -117,19 +117,23 @@ pub mod save_or_reopen { let weak_workspace = workspace.read(cx).weak_handle(); - workspace.update(cx, |workspace, cx| { - workspace.toggle_modal(window, cx, |window, cx| { - let selector = EncodingSelector::new( - window, - cx, - Action::Save, - Some(buffer.downgrade()), - weak_workspace, - None, - ); - selector - }) - }); + if let Some(file) = buffer.read(cx).file() { + let path = file.as_local()?.abs_path(cx); + + workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(window, cx, |window, cx| { + let selector = EncodingSelector::new( + window, + cx, + Action::Save, + Some(buffer.downgrade()), + weak_workspace, + Some(path), + ); + selector + }) + }); + } } } else if self.current_selection == 1 { if let Some(workspace) = self.workspace.upgrade() { @@ -142,19 +146,23 @@ pub mod save_or_reopen { let weak_workspace = workspace.read(cx).weak_handle(); - workspace.update(cx, |workspace, cx| { - workspace.toggle_modal(window, cx, |window, cx| { - let selector = EncodingSelector::new( - window, - cx, - Action::Reopen, - Some(buffer.downgrade()), - weak_workspace, - None, - ); - selector + if let Some(file) = buffer.read(cx).file() { + let path = file.as_local()?.abs_path(cx); + + workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(window, cx, |window, cx| { + let selector = EncodingSelector::new( + window, + cx, + Action::Reopen, + Some(buffer.downgrade()), + weak_workspace, + Some(path), + ); + selector + }); }); - }); + } } } @@ -290,7 +298,7 @@ pub mod encoding { Context, HighlightedLabel, ListItem, ListItemSpacing, ParentElement, Render, Styled, Window, rems, v_flex, }; - use util::{ResultExt, TryFutureExt}; + use util::ResultExt; use workspace::{CloseActiveItem, ModalView, OpenOptions, Workspace}; use crate::encoding_from_name; @@ -438,35 +446,81 @@ pub mod encoding { .upgrade() .unwrap(); + let weak_workspace = workspace.read(cx).weak_handle(); + + let current_selection = self.matches[self.current_selection].string.clone(); + if let Some(buffer) = &self.buffer - && let Some(buffer_entity) = buffer.upgrade() + && let Some(buffer) = buffer.upgrade() { - let buffer = buffer_entity.read(cx); - + let path = self + .selector + .upgrade() + .unwrap() + .read(cx) + .path + .clone() + .unwrap(); + + let reload = buffer.update(cx, |buffer, cx| buffer.reload(cx)); // Since the encoding will be accessed in `reload`, // the lock must be released before calling `reload`. // By limiting the scope, we ensure that it is released + { + let buffer = buffer.read(cx); + let buffer_encoding = buffer.encoding.clone(); - buffer_encoding.set(encoding_from_name( - self.matches[self.current_selection].string.as_str(), - )); + buffer_encoding.set(encoding_from_name(¤t_selection.clone())); } self.dismissed(window, cx); if self.action == Action::Reopen { - buffer_entity.update(cx, |buffer, cx| { - let rec = buffer.reload(cx); - cx.spawn(async move |_, _| rec.await).detach() + buffer.update(cx, |_, cx| { + cx.spawn_in(window, async move |_, cx| { + if let Err(_) | Ok(None) = reload.await { + let workspace = weak_workspace.upgrade().unwrap(); + + workspace + .update_in(cx, |workspace, window, cx| { + workspace + .encoding_options + .encoding + .lock() + .unwrap() + .set(encoding_from_name(¤t_selection)); + + *workspace.encoding_options.force.get_mut() = false; + + *workspace.encoding_options.detect_utf16.get_mut() = true; + + workspace + .active_pane() + .update(cx, |pane, cx| { + pane.close_active_item( + &CloseActiveItem::default(), + window, + cx, + ) + }) + .detach(); + + workspace + .open_abs_path(path, OpenOptions::default(), window, cx) + .detach() + }) + .log_err(); + } + }) + .detach() }); } else if self.action == Action::Save { - let task = workspace.update(cx, |workspace, cx| { + workspace.update(cx, |workspace, cx| { workspace .save_active_item(workspace::SaveIntent::Save, window, cx) - .log_err() + .detach(); }); - cx.spawn(async |_, _| task).detach(); } } else { if let Some(path) = self.selector.upgrade().unwrap().read(cx).path.clone() { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index dd5569ce645b8b..1b8420b0f149a0 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1369,7 +1369,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).clone(); let buffer_encoding = self.encoding.clone(); @@ -1377,8 +1377,9 @@ impl Buffer { 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, (*encoding).clone(), false, true, Some(buffer_encoding)) + file.load(cx, encoding, false, true, Some(buffer_encoding)) })) })? else { diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 97cd401624b63f..f68a4229f179db 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -633,6 +633,8 @@ impl LocalBufferStore { detect_utf16: bool, cx: &mut Context, ) -> Task>> { + println!("{:?}", encoding); + let load_buffer = worktree.update(cx, |worktree, cx| { let reservation = cx.reserve_entity(); let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64()); @@ -681,8 +683,8 @@ impl LocalBufferStore { entry_id: None, is_local: true, is_private: false, - encoding: Some(Arc::new(if let Some(encoding) = encoding { - encoding + encoding: Some(Arc::new(if let Some(encoding) = &encoding { + encoding.clone() } else { Encoding::default() })), @@ -713,6 +715,12 @@ impl LocalBufferStore { anyhow::Ok(()) })??; + buffer.update(cx, |buffer, _| { + buffer + .encoding + .set(encoding.unwrap_or(Encoding::default()).get()) + })?; + Ok(buffer) }) } diff --git a/crates/project/src/invalid_item_view.rs b/crates/project/src/invalid_item_view.rs index 7436c941d56f25..208cf05c48d27b 100644 --- a/crates/project/src/invalid_item_view.rs +++ b/crates/project/src/invalid_item_view.rs @@ -2,9 +2,9 @@ use std::{path::Path, sync::Arc}; use gpui::{EventEmitter, FocusHandle, Focusable}; use ui::{ - h_flex, v_flex, App, Button, ButtonCommon, ButtonStyle, Clickable, Context, FluentBuilder, - InteractiveElement, KeyBinding, Label, LabelCommon, LabelSize, ParentElement, Render, - SharedString, Styled as _, TintColor, Window, + App, Button, ButtonCommon, ButtonStyle, Clickable, Context, FluentBuilder, InteractiveElement, + KeyBinding, Label, LabelCommon, LabelSize, ParentElement, Render, SharedString, Styled as _, + TintColor, Window, div, h_flex, v_flex, }; use zed_actions::workspace::OpenWithSystem; @@ -89,15 +89,18 @@ impl Render for InvalidItemView { .overflow_hidden() .key_context("InvalidBuffer") .child( - h_flex().size_full().justify_center().child( + h_flex().size_full().justify_center().items_center().child( v_flex() - .justify_center() .gap_2() + .max_w_96() .child(h_flex().justify_center().child("Could not open file")) .child( - h_flex() - .justify_center() - .child(Label::new(self.error.clone()).size(LabelSize::Small)), + h_flex().justify_center().child( + div() + .whitespace_normal() + .text_center() + .child(Label::new(self.error.clone()).size(LabelSize::Small)), + ), ) .when(self.is_local, |contents| { contents diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5b3540e4e89c85..231349d63eef7c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -109,7 +109,6 @@ use settings::{InvalidSettingsError, Settings, SettingsLocation, SettingsStore}; use smol::channel::Receiver; use snippet::Snippet; use snippet_provider::SnippetProvider; -use std::ops::Deref; use std::{ borrow::Cow, collections::BTreeMap, @@ -2723,15 +2722,7 @@ impl Project { self.buffer_store.update(cx, |buffer_store, cx| { buffer_store.open_buffer( path.into(), - Some( - self.encoding_options - .encoding - .lock() - .as_ref() - .unwrap() - .deref() - .clone(), - ), + Some((*self.encoding_options.encoding).clone()), *self.encoding_options.force.get_mut(), *self.encoding_options.detect_utf16.get_mut(), cx, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 68c7d704f757d7..c1508a74f8603c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -651,7 +651,7 @@ impl ProjectItemRegistry { let project_path = project_path.clone(); let encoding = encoding.unwrap_or_default(); - project.update(cx, |project, _| {project.encoding_options.encoding.lock().unwrap().set(encoding.get())}); + project.update(cx, |project, _| project.encoding_options.encoding.set(encoding.get())); let is_file = project .read(cx) @@ -1945,6 +1945,8 @@ impl Workspace { window: &mut Window, cx: &mut Context, ) -> Task> { + // This is done so that we would get an error when we try to open the file with wrong encoding, + // and not silently use the previously set encoding. self.encoding_options.reset(); let to_load = if let Some(pane) = pane.upgrade() { @@ -3582,8 +3584,8 @@ impl Workspace { project.encoding_options.force.store( self.encoding_options .force - .load(std::sync::atomic::Ordering::Relaxed), - std::sync::atomic::Ordering::Relaxed, + .load(std::sync::atomic::Ordering::Acquire), + std::sync::atomic::Ordering::Release, ); }); @@ -3591,7 +3593,7 @@ impl Workspace { registry.open_path( project, &path, - Some(self.encoding_options.encoding.lock().unwrap().clone()), + Some((*self.encoding_options.encoding).clone()), window, cx, ) From c130110987326c3e4902fb4f40d2657119f4c41b Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Tue, 21 Oct 2025 17:52:39 +0530 Subject: [PATCH 37/43] Remove calls to `lock` and `unwrap` as they are no longer needed --- crates/encodings_ui/src/selectors.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/crates/encodings_ui/src/selectors.rs b/crates/encodings_ui/src/selectors.rs index f4edecd0428d1f..879cadba19de10 100644 --- a/crates/encodings_ui/src/selectors.rs +++ b/crates/encodings_ui/src/selectors.rs @@ -487,8 +487,6 @@ pub mod encoding { workspace .encoding_options .encoding - .lock() - .unwrap() .set(encoding_from_name(¤t_selection)); *workspace.encoding_options.force.get_mut() = false; @@ -535,12 +533,7 @@ pub mod encoding { encoding_from_name(self.matches[self.current_selection].string.as_str()); let open_task = workspace.update(cx, |workspace, cx| { - workspace - .encoding_options - .encoding - .lock() - .unwrap() - .set(encoding); + workspace.encoding_options.encoding.set(encoding); workspace.open_abs_path(path, OpenOptions::default(), window, cx) }); From 0e89634a16709c8d9301a3fbafb62e48e14a0c89 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Wed, 22 Oct 2025 12:24:28 +0530 Subject: [PATCH 38/43] Fix conflicts --- Cargo.lock | 68 +- crates/agent/Cargo.toml | 1 + crates/agent/src/tools/edit_file_tool.rs | 4 +- crates/agent2/Cargo.toml | 104 - crates/agent_ui/src/acp/message_editor.rs | 93 +- crates/assistant_tools/Cargo.toml | 94 - crates/assistant_tools/src/edit_file_tool.rs | 2440 ----------------- .../src/syntax_index.rs | 2 + crates/editor/src/editor_settings.rs | 12 - crates/encodings_ui/Cargo.toml | 1 - crates/encodings_ui/src/lib.rs | 4 +- crates/git_ui/src/file_diff_view.rs | 1 - crates/language/Cargo.toml | 1 - crates/project/src/buffer_store.rs | 11 +- crates/project/src/project.rs | 11 +- crates/project/src/project_search.rs | 754 +++++ crates/project/src/project_tests.rs | 29 +- crates/remote_server/src/headless_project.rs | 11 +- .../remote_server/src/remote_editing_tests.rs | 2 +- crates/worktree/src/worktree.rs | 6 +- crates/worktree/src/worktree_tests.rs | 99 +- crates/zed/Cargo.toml | 1 - crates/zed/src/zed/open_listener.rs | 3 + crates/zeta/src/zeta.rs | 2 +- 24 files changed, 878 insertions(+), 2876 deletions(-) delete mode 100644 crates/agent2/Cargo.toml delete mode 100644 crates/assistant_tools/Cargo.toml delete mode 100644 crates/assistant_tools/src/edit_file_tool.rs create mode 100644 crates/project/src/project_search.rs diff --git a/Cargo.lock b/Cargo.lock index 2ce055edf4a714..247e92d48ad123 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5514,6 +5514,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" @@ -5548,7 +5612,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", "zed_actions", ] @@ -6429,6 +6492,7 @@ dependencies = [ "async-trait", "cocoa 0.26.0", "collections", + "encoding", "encodings", "fsevent", "futures 0.3.31", @@ -8806,6 +8870,7 @@ dependencies = [ "ctor", "diffy", "ec4rs", + "encoding", "encodings", "fs", "futures 0.3.31", @@ -20806,6 +20871,7 @@ dependencies = [ "async-lock 2.8.0", "clock", "collections", + "encoding", "encodings", "fs", "futures 0.3.31", diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index e962c876a38f78..f1c277c3ada94b 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -32,6 +32,7 @@ collections.workspace = true context_server.workspace = true db.workspace = true derive_more.workspace = true +encodings.workspace = true fs.workspace = true futures.workspace = true git.workspace = true diff --git a/crates/agent/src/tools/edit_file_tool.rs b/crates/agent/src/tools/edit_file_tool.rs index e7843d3388139f..2fe299392ea6ae 100644 --- a/crates/agent/src/tools/edit_file_tool.rs +++ b/crates/agent/src/tools/edit_file_tool.rs @@ -563,7 +563,7 @@ mod tests { use super::*; use crate::{ContextServerRegistry, Templates}; use client::TelemetrySettings; - use encodings::{Encoding, UTF_8}; + use encodings::Encoding; use fs::Fs; use gpui::{TestAppContext, UpdateGlobal}; use language_model::fake_provider::FakeLanguageModel; @@ -745,7 +745,7 @@ mod tests { path!("/root/src/main.rs").as_ref(), &Rope::from_str_small("initial content"), language::LineEnding::Unix, - Encoding::new(UTF_8), + Encoding::default(), ) .await .unwrap(); diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml deleted file mode 100644 index e481af9c834424..00000000000000 --- a/crates/agent2/Cargo.toml +++ /dev/null @@ -1,104 +0,0 @@ -[package] -name = "agent2" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lib] -path = "src/agent2.rs" - -[features] -test-support = ["db/test-support"] -e2e = [] - -[lints] -workspace = true - -[dependencies] -acp_thread.workspace = true -action_log.workspace = true -agent.workspace = true -agent-client-protocol.workspace = true -agent_servers.workspace = true -agent_settings.workspace = true -anyhow.workspace = true -assistant_context.workspace = true -assistant_tool.workspace = true -assistant_tools.workspace = true -chrono.workspace = true -client.workspace = true -cloud_llm_client.workspace = true -collections.workspace = true -context_server.workspace = true -db.workspace = true -encodings.workspace = true -fs.workspace = true -futures.workspace = true -git.workspace = true -gpui.workspace = true -handlebars = { workspace = true, features = ["rust-embed"] } -html_to_markdown.workspace = true -http_client.workspace = true -indoc.workspace = true -itertools.workspace = true -language.workspace = true -language_model.workspace = true -language_models.workspace = true -log.workspace = true -open.workspace = true -parking_lot.workspace = true -paths.workspace = true -project.workspace = true -prompt_store.workspace = true -rust-embed.workspace = true -schemars.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smol.workspace = true -sqlez.workspace = true -task.workspace = true -telemetry.workspace = true -terminal.workspace = true -thiserror.workspace = true -text.workspace = true -ui.workspace = true -util.workspace = true -uuid.workspace = true -watch.workspace = true -web_search.workspace = true -workspace-hack.workspace = true -zed_env_vars.workspace = true -zstd.workspace = true - - -[dev-dependencies] -agent = { workspace = true, "features" = ["test-support"] } -agent_servers = { workspace = true, "features" = ["test-support"] } -assistant_context = { workspace = true, "features" = ["test-support"] } -ctor.workspace = true -client = { workspace = true, "features" = ["test-support"] } -clock = { workspace = true, "features" = ["test-support"] } -context_server = { workspace = true, "features" = ["test-support"] } -db = { workspace = true, "features" = ["test-support"] } -editor = { workspace = true, "features" = ["test-support"] } -env_logger.workspace = true -fs = { workspace = true, "features" = ["test-support"] } -git = { workspace = true, "features" = ["test-support"] } -gpui = { workspace = true, "features" = ["test-support"] } -gpui_tokio.workspace = true -language = { workspace = true, "features" = ["test-support"] } -language_model = { workspace = true, "features" = ["test-support"] } -lsp = { workspace = true, "features" = ["test-support"] } -pretty_assertions.workspace = true -project = { workspace = true, "features" = ["test-support"] } -reqwest_client.workspace = true -settings = { workspace = true, "features" = ["test-support"] } -tempfile.workspace = true -terminal = { workspace = true, "features" = ["test-support"] } -theme = { workspace = true, "features" = ["test-support"] } -tree-sitter-rust.workspace = true -unindent = { workspace = true } -worktree = { workspace = true, "features" = ["test-support"] } -zlog.workspace = true diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 50ac5848f71dfd..a2a701659eaae6 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -463,97 +463,6 @@ impl MessageEditor { }) } - fn confirm_mention_for_directory( - &mut self, - abs_path: PathBuf, - cx: &mut Context, - ) -> Task> { - fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc, PathBuf)> { - let mut files = Vec::new(); - - for entry in worktree.child_entries(path) { - if entry.is_dir() { - files.extend(collect_files_in_path(worktree, &entry.path)); - } else if entry.is_file() { - files.push((entry.path.clone(), worktree.full_path(&entry.path))); - } - } - - files - } - - let Some(project_path) = self - .project - .read(cx) - .project_path_for_absolute_path(&abs_path, cx) - else { - return Task::ready(Err(anyhow!("project path not found"))); - }; - let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else { - return Task::ready(Err(anyhow!("project entry not found"))); - }; - let directory_path = entry.path.clone(); - let worktree_id = project_path.worktree_id; - let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) else { - return Task::ready(Err(anyhow!("worktree not found"))); - }; - let project = self.project.clone(); - cx.spawn(async move |_, cx| { - let file_paths = worktree.read_with(cx, |worktree, _cx| { - collect_files_in_path(worktree, &directory_path) - })?; - let descendants_future = cx.update(|cx| { - join_all(file_paths.into_iter().map(|(worktree_path, full_path)| { - let rel_path = worktree_path - .strip_prefix(&directory_path) - .log_err() - .map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into()); - - let open_task = project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - let project_path = ProjectPath { - worktree_id, - path: worktree_path, - }; - buffer_store.open_buffer(project_path, None, false, true, cx) - }) - }); - - cx.spawn(async move |cx| { - let buffer = open_task.await.log_err()?; - let buffer_content = outline::get_buffer_content_or_outline( - buffer.clone(), - Some(&full_path), - &cx, - ) - .await - .ok()?; - - Some((rel_path, full_path, buffer_content.text, buffer)) - }) - })) - })?; - - let contents = cx - .background_spawn(async move { - let (contents, tracked_buffers) = descendants_future - .await - .into_iter() - .flatten() - .map(|(rel_path, full_path, rope, buffer)| { - ((rel_path, full_path, rope), buffer) - }) - .unzip(); - Mention::Text { - content: render_directory_contents(contents), - tracked_buffers, - } - }) - .await; - anyhow::Ok(contents) - }) - } - fn confirm_mention_for_fetch( &mut self, url: url::Url, @@ -1339,7 +1248,7 @@ fn full_mention_for_directory( worktree_id, path: worktree_path, }; - buffer_store.open_buffer(project_path, cx) + buffer_store.open_buffer(project_path, None, false, true, cx) }) }); diff --git a/crates/assistant_tools/Cargo.toml b/crates/assistant_tools/Cargo.toml deleted file mode 100644 index b31561305dec86..00000000000000 --- a/crates/assistant_tools/Cargo.toml +++ /dev/null @@ -1,94 +0,0 @@ -[package] -name = "assistant_tools" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/assistant_tools.rs" - -[features] -eval = [] - -[dependencies] -action_log.workspace = true -agent_settings.workspace = true -anyhow.workspace = true -assistant_tool.workspace = true -buffer_diff.workspace = true -chrono.workspace = true -client.workspace = true -cloud_llm_client.workspace = true -collections.workspace = true -component.workspace = true -derive_more.workspace = true -diffy = "0.4.2" -editor.workspace = true -encodings.workspace = true -feature_flags.workspace = true -futures.workspace = true -gpui.workspace = true -handlebars = { workspace = true, features = ["rust-embed"] } -html_to_markdown.workspace = true -http_client.workspace = true -indoc.workspace = true -itertools.workspace = true -language.workspace = true -language_model.workspace = true -log.workspace = true -lsp.workspace = true -markdown.workspace = true -open.workspace = true -paths.workspace = true -portable-pty.workspace = true -project.workspace = true -prompt_store.workspace = true -regex.workspace = true -rust-embed.workspace = true -schemars.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smallvec.workspace = true -streaming_diff.workspace = true -strsim.workspace = true -task.workspace = true -terminal.workspace = true -terminal_view.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -watch.workspace = true -web_search.workspace = true -which.workspace = true -workspace-hack.workspace = true -workspace.workspace = true - -[dev-dependencies] -lsp = { workspace = true, features = ["test-support"] } -client = { workspace = true, features = ["test-support"] } -clock = { workspace = true, features = ["test-support"] } -collections = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -gpui_tokio.workspace = true -fs = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -language_model = { workspace = true, features = ["test-support"] } -language_models.workspace = true -project = { workspace = true, features = ["test-support"] } -rand.workspace = true -pretty_assertions.workspace = true -reqwest_client.workspace = true -settings = { workspace = true, features = ["test-support"] } -smol.workspace = true -task = { workspace = true, features = ["test-support"]} -tempfile.workspace = true -theme.workspace = true -tree-sitter-rust.workspace = true -workspace = { workspace = true, features = ["test-support"] } -unindent.workspace = true -zlog.workspace = true diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs deleted file mode 100644 index eea65b173ff3ca..00000000000000 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ /dev/null @@ -1,2440 +0,0 @@ -use crate::{ - Templates, - edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat}, - schema::json_schema_for, - ui::{COLLAPSED_LINES, ToolOutputPreview}, -}; -use action_log::ActionLog; -use agent_settings; -use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{ - AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus, -}; -use buffer_diff::{BufferDiff, BufferDiffSnapshot}; -use editor::{ - Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, multibuffer_context_lines, -}; -use futures::StreamExt; -use gpui::{ - Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task, - TextStyleRefinement, WeakEntity, pulsating_between, px, -}; -use indoc::formatdoc; -use language::{ - Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Point, Rope, - TextBuffer, - language_settings::{self, FormatOnSave, SoftWrap}, -}; -use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; -use markdown::{Markdown, MarkdownElement, MarkdownStyle}; -use paths; -use project::{ - Project, ProjectPath, - lsp_store::{FormatTrigger, LspFormatTarget}, -}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::Settings; -use std::{ - cmp::Reverse, - collections::HashSet, - ops::Range, - path::{Path, PathBuf}, - sync::Arc, - time::Duration, -}; -use theme::ThemeSettings; -use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*}; -use util::ResultExt; -use workspace::Workspace; - -pub struct EditFileTool; - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct EditFileToolInput { - /// A one-line, user-friendly markdown description of the edit. This will be - /// shown in the UI and also passed to another model to perform the edit. - /// - /// Be terse, but also descriptive in what you want to achieve with this - /// edit. Avoid generic instructions. - /// - /// NEVER mention the file path in this description. - /// - /// Fix API endpoint URLs - /// Update copyright year in `page_footer` - /// - /// Make sure to include this field before all the others in the input object - /// so that we can display it immediately. - pub display_description: String, - - /// The full path of the file to create or modify in the project. - /// - /// WARNING: When specifying which file path need changing, you MUST - /// start each path with one of the project's root directories. - /// - /// The following examples assume we have two root directories in the project: - /// - /a/b/backend - /// - /c/d/frontend - /// - /// - /// `backend/src/main.rs` - /// - /// Notice how the file path starts with `backend`. Without that, the path - /// would be ambiguous and the call would fail! - /// - /// - /// - /// `frontend/db.js` - /// - pub path: PathBuf, - - /// The mode of operation on the file. Possible values: - /// - 'edit': Make granular edits to an existing file. - /// - 'create': Create a new file if it doesn't exist. - /// - 'overwrite': Replace the entire contents of an existing file. - /// - /// When a file already exists or you just created it, prefer editing - /// it as opposed to recreating it from scratch. - pub mode: EditFileMode, -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "lowercase")] -pub enum EditFileMode { - Edit, - Create, - Overwrite, -} - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct EditFileToolOutput { - pub original_path: PathBuf, - pub new_text: String, - pub old_text: Arc, - pub raw_output: Option, -} - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -struct PartialInput { - #[serde(default)] - path: String, - #[serde(default)] - display_description: String, -} - -const DEFAULT_UI_TEXT: &str = "Editing file"; - -impl Tool for EditFileTool { - fn name(&self) -> String { - "edit_file".into() - } - - fn needs_confirmation( - &self, - input: &serde_json::Value, - project: &Entity, - cx: &App, - ) -> bool { - if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions { - return false; - } - - let Ok(input) = serde_json::from_value::(input.clone()) else { - // If it's not valid JSON, it's going to error and confirming won't do anything. - return false; - }; - - // If any path component matches the local settings folder, then this could affect - // the editor in ways beyond the project source, so prompt. - let local_settings_folder = paths::local_settings_folder_relative_path(); - let path = Path::new(&input.path); - if path - .components() - .any(|component| component.as_os_str() == local_settings_folder.as_os_str()) - { - return true; - } - - // It's also possible that the global config dir is configured to be inside the project, - // so check for that edge case too. - if let Ok(canonical_path) = std::fs::canonicalize(&input.path) - && canonical_path.starts_with(paths::config_dir()) - { - return true; - } - - // Check if path is inside the global config directory - // First check if it's already inside project - if not, try to canonicalize - let project_path = project.read(cx).find_project_path(&input.path, cx); - - // If the path is inside the project, and it's not one of the above edge cases, - // then no confirmation is necessary. Otherwise, confirmation is necessary. - project_path.is_none() - } - - fn may_perform_edits(&self) -> bool { - true - } - - fn description(&self) -> String { - include_str!("edit_file_tool/description.md").to_string() - } - - fn icon(&self) -> IconName { - IconName::ToolPencil - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(input.clone()) { - Ok(input) => { - let path = Path::new(&input.path); - let mut description = input.display_description.clone(); - - // Add context about why confirmation may be needed - let local_settings_folder = paths::local_settings_folder_relative_path(); - if path - .components() - .any(|c| c.as_os_str() == local_settings_folder.as_os_str()) - { - description.push_str(" (local settings)"); - } else if let Ok(canonical_path) = std::fs::canonicalize(&input.path) - && canonical_path.starts_with(paths::config_dir()) - { - description.push_str(" (global settings)"); - } - - description - } - Err(_) => "Editing file".to_string(), - } - } - - fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String { - if let Some(input) = serde_json::from_value::(input.clone()).ok() { - let description = input.display_description.trim(); - if !description.is_empty() { - return description.to_string(); - } - - let path = input.path.trim(); - if !path.is_empty() { - return path.to_string(); - } - } - - DEFAULT_UI_TEXT.to_string() - } - - fn run( - self: Arc, - input: serde_json::Value, - request: Arc, - project: Entity, - action_log: Entity, - model: Arc, - window: Option, - cx: &mut App, - ) -> ToolResult { - let input = match serde_json::from_value::(input) { - Ok(input) => input, - Err(err) => return Task::ready(Err(anyhow!(err))).into(), - }; - - let project_path = match resolve_path(&input, project.clone(), cx) { - Ok(path) => path, - Err(err) => return Task::ready(Err(anyhow!(err))).into(), - }; - - let card = window.and_then(|window| { - window - .update(cx, |_, window, cx| { - cx.new(|cx| { - EditFileToolCard::new(input.path.clone(), project.clone(), window, cx) - }) - }) - .ok() - }); - - let card_clone = card.clone(); - let action_log_clone = action_log.clone(); - let task = cx.spawn(async move |cx: &mut AsyncApp| { - let edit_format = EditFormat::from_model(model.clone())?; - let edit_agent = EditAgent::new( - model, - project.clone(), - action_log_clone, - Templates::new(), - edit_format, - ); - - let buffer = project - .update(cx, |project, cx| { - project.open_buffer(project_path.clone(), cx) - })? - .await?; - - let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; - let old_text = cx - .background_spawn({ - let old_snapshot = old_snapshot.clone(); - async move { Arc::new(old_snapshot.text()) } - }) - .await; - - if let Some(card) = card_clone.as_ref() { - card.update(cx, |card, cx| card.initialize(buffer.clone(), cx))?; - } - - let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) { - edit_agent.edit( - buffer.clone(), - input.display_description.clone(), - &request, - cx, - ) - } else { - edit_agent.overwrite( - buffer.clone(), - input.display_description.clone(), - &request, - cx, - ) - }; - - let mut hallucinated_old_text = false; - let mut ambiguous_ranges = Vec::new(); - while let Some(event) = events.next().await { - match event { - EditAgentOutputEvent::Edited { .. } => { - if let Some(card) = card_clone.as_ref() { - card.update(cx, |card, cx| card.update_diff(cx))?; - } - } - EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true, - EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges, - EditAgentOutputEvent::ResolvingEditRange(range) => { - if let Some(card) = card_clone.as_ref() { - card.update(cx, |card, cx| card.reveal_range(range, cx))?; - } - } - } - } - let agent_output = output.await?; - - // If format_on_save is enabled, format the buffer - let format_on_save_enabled = buffer - .read_with(cx, |buffer, cx| { - let settings = language_settings::language_settings( - buffer.language().map(|l| l.name()), - buffer.file(), - cx, - ); - !matches!(settings.format_on_save, FormatOnSave::Off) - }) - .unwrap_or(false); - - if format_on_save_enabled { - action_log.update(cx, |log, cx| { - log.buffer_edited(buffer.clone(), cx); - })?; - let format_task = project.update(cx, |project, cx| { - project.format( - HashSet::from_iter([buffer.clone()]), - LspFormatTarget::Buffers, - false, // Don't push to history since the tool did it. - FormatTrigger::Save, - cx, - ) - })?; - format_task.await.log_err(); - } - - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))? - .await?; - - // Notify the action log that we've edited the buffer (*after* formatting has completed). - action_log.update(cx, |log, cx| { - log.buffer_edited(buffer.clone(), cx); - })?; - - let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; - let (new_text, diff) = cx - .background_spawn({ - let new_snapshot = new_snapshot.clone(); - let old_text = old_text.clone(); - async move { - let new_text = new_snapshot.text(); - let diff = language::unified_diff(&old_text, &new_text); - - (new_text, diff) - } - }) - .await; - - let output = EditFileToolOutput { - original_path: project_path.path.to_path_buf(), - new_text, - old_text, - raw_output: Some(agent_output), - }; - - if let Some(card) = card_clone { - card.update(cx, |card, cx| { - card.update_diff(cx); - card.finalize(cx) - }) - .log_err(); - } - - let input_path = input.path.display(); - if diff.is_empty() { - anyhow::ensure!( - !hallucinated_old_text, - formatdoc! {" - Some edits were produced but none of them could be applied. - Read the relevant sections of {input_path} again so that - I can perform the requested edits. - "} - ); - anyhow::ensure!( - ambiguous_ranges.is_empty(), - { - let line_numbers = ambiguous_ranges - .iter() - .map(|range| range.start.to_string()) - .collect::>() - .join(", "); - formatdoc! {" - matches more than one position in the file (lines: {line_numbers}). Read the - relevant sections of {input_path} again and extend so - that I can perform the requested edits. - "} - } - ); - Ok(ToolResultOutput { - content: ToolResultContent::Text("No edits were made.".into()), - output: serde_json::to_value(output).ok(), - }) - } else { - Ok(ToolResultOutput { - content: ToolResultContent::Text(format!( - "Edited {}:\n\n```diff\n{}\n```", - input_path, diff - )), - output: serde_json::to_value(output).ok(), - }) - } - }); - - ToolResult { - output: task, - card: card.map(AnyToolCard::from), - } - } - - fn deserialize_card( - self: Arc, - output: serde_json::Value, - project: Entity, - window: &mut Window, - cx: &mut App, - ) -> Option { - let output = match serde_json::from_value::(output) { - Ok(output) => output, - Err(_) => return None, - }; - - let card = cx.new(|cx| { - EditFileToolCard::new(output.original_path.clone(), project.clone(), window, cx) - }); - - cx.spawn({ - let path: Arc = output.original_path.into(); - let language_registry = project.read(cx).languages().clone(); - let card = card.clone(); - async move |cx| { - let buffer = - build_buffer(output.new_text, path.clone(), &language_registry, cx).await?; - let buffer_diff = - build_buffer_diff(output.old_text.clone(), &buffer, &language_registry, cx) - .await?; - card.update(cx, |card, cx| { - card.multibuffer.update(cx, |multibuffer, cx| { - let snapshot = buffer.read(cx).snapshot(); - let diff = buffer_diff.read(cx); - let diff_hunk_ranges = diff - .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx) - .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot)) - .collect::>(); - - multibuffer.set_excerpts_for_path( - PathKey::for_buffer(&buffer, cx), - buffer, - diff_hunk_ranges, - multibuffer_context_lines(cx), - cx, - ); - multibuffer.add_diff(buffer_diff, cx); - let end = multibuffer.len(cx); - card.total_lines = - Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1); - }); - - cx.notify(); - })?; - anyhow::Ok(()) - } - }) - .detach_and_log_err(cx); - - Some(card.into()) - } -} - -/// Validate that the file path is valid, meaning: -/// -/// - For `edit` and `overwrite`, the path must point to an existing file. -/// - For `create`, the file must not already exist, but it's parent dir must exist. -fn resolve_path( - input: &EditFileToolInput, - project: Entity, - cx: &mut App, -) -> Result { - let project = project.read(cx); - - match input.mode { - EditFileMode::Edit | EditFileMode::Overwrite => { - let path = project - .find_project_path(&input.path, cx) - .context("Can't edit file: path not found")?; - - let entry = project - .entry_for_path(&path, cx) - .context("Can't edit file: path not found")?; - - anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory"); - Ok(path) - } - - EditFileMode::Create => { - if let Some(path) = project.find_project_path(&input.path, cx) { - anyhow::ensure!( - project.entry_for_path(&path, cx).is_none(), - "Can't create file: file already exists" - ); - } - - let parent_path = input - .path - .parent() - .context("Can't create file: incorrect path")?; - - let parent_project_path = project.find_project_path(&parent_path, cx); - - let parent_entry = parent_project_path - .as_ref() - .and_then(|path| project.entry_for_path(path, cx)) - .context("Can't create file: parent directory doesn't exist")?; - - anyhow::ensure!( - parent_entry.is_dir(), - "Can't create file: parent is not a directory" - ); - - let file_name = input - .path - .file_name() - .context("Can't create file: invalid filename")?; - - let new_file_path = parent_project_path.map(|parent| ProjectPath { - path: Arc::from(parent.path.join(file_name)), - ..parent - }); - - new_file_path.context("Can't create file") - } - } -} - -pub struct EditFileToolCard { - path: PathBuf, - editor: Entity, - multibuffer: Entity, - project: Entity, - buffer: Option>, - base_text: Option>, - buffer_diff: Option>, - revealed_ranges: Vec>, - diff_task: Option>>, - preview_expanded: bool, - error_expanded: Option>, - full_height_expanded: bool, - total_lines: Option, -} - -impl EditFileToolCard { - pub fn new(path: PathBuf, project: Entity, window: &mut Window, cx: &mut App) -> Self { - let expand_edit_card = agent_settings::AgentSettings::get_global(cx).expand_edit_card; - let multibuffer = cx.new(|_| MultiBuffer::without_headers(Capability::ReadOnly)); - - let editor = cx.new(|cx| { - let mut editor = Editor::new( - EditorMode::Full { - scale_ui_elements_with_buffer_font_size: false, - show_active_line_background: false, - sized_by_content: true, - }, - multibuffer.clone(), - Some(project.clone()), - window, - cx, - ); - editor.set_show_gutter(false, cx); - editor.disable_inline_diagnostics(); - editor.disable_expand_excerpt_buttons(cx); - // Keep horizontal scrollbar so user can scroll horizontally if needed - editor.set_show_vertical_scrollbar(false, cx); - editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx); - editor.set_soft_wrap_mode(SoftWrap::None, cx); - editor.scroll_manager.set_forbid_vertical_scroll(true); - editor.set_show_indent_guides(false, cx); - editor.set_read_only(true); - editor.set_show_breakpoints(false, cx); - editor.set_show_code_actions(false, cx); - editor.set_show_git_diff_gutter(false, cx); - editor.set_expand_all_diff_hunks(cx); - editor - }); - Self { - path, - project, - editor, - multibuffer, - buffer: None, - base_text: None, - buffer_diff: None, - revealed_ranges: Vec::new(), - diff_task: None, - preview_expanded: true, - error_expanded: None, - full_height_expanded: expand_edit_card, - total_lines: None, - } - } - - pub fn initialize(&mut self, buffer: Entity, cx: &mut App) { - let buffer_snapshot = buffer.read(cx).snapshot(); - let base_text = buffer_snapshot.text(); - let language_registry = buffer.read(cx).language_registry(); - let text_snapshot = buffer.read(cx).text_snapshot(); - - // Create a buffer diff with the current text as the base - let buffer_diff = cx.new(|cx| { - let mut diff = BufferDiff::new(&text_snapshot, cx); - let _ = diff.set_base_text( - buffer_snapshot.clone(), - language_registry, - text_snapshot, - cx, - ); - diff - }); - - self.buffer = Some(buffer); - self.base_text = Some(base_text.into()); - self.buffer_diff = Some(buffer_diff.clone()); - - // Add the diff to the multibuffer - self.multibuffer - .update(cx, |multibuffer, cx| multibuffer.add_diff(buffer_diff, cx)); - } - - pub fn is_loading(&self) -> bool { - self.total_lines.is_none() - } - - pub fn update_diff(&mut self, cx: &mut Context) { - let Some(buffer) = self.buffer.as_ref() else { - return; - }; - let Some(buffer_diff) = self.buffer_diff.as_ref() else { - return; - }; - - let buffer = buffer.clone(); - let buffer_diff = buffer_diff.clone(); - let base_text = self.base_text.clone(); - self.diff_task = Some(cx.spawn(async move |this, cx| { - let text_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot())?; - let diff_snapshot = BufferDiff::update_diff( - buffer_diff.clone(), - text_snapshot.clone(), - base_text, - false, - false, - None, - None, - cx, - ) - .await?; - buffer_diff.update(cx, |diff, cx| { - diff.set_snapshot(diff_snapshot, &text_snapshot, cx) - })?; - this.update(cx, |this, cx| this.update_visible_ranges(cx)) - })); - } - - pub fn reveal_range(&mut self, range: Range, cx: &mut Context) { - self.revealed_ranges.push(range); - self.update_visible_ranges(cx); - } - - fn update_visible_ranges(&mut self, cx: &mut Context) { - let Some(buffer) = self.buffer.as_ref() else { - return; - }; - - let ranges = self.excerpt_ranges(cx); - self.total_lines = self.multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - PathKey::for_buffer(buffer, cx), - buffer.clone(), - ranges, - multibuffer_context_lines(cx), - cx, - ); - let end = multibuffer.len(cx); - Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1) - }); - cx.notify(); - } - - fn excerpt_ranges(&self, cx: &App) -> Vec> { - let Some(buffer) = self.buffer.as_ref() else { - return Vec::new(); - }; - let Some(diff) = self.buffer_diff.as_ref() else { - return Vec::new(); - }; - - let buffer = buffer.read(cx); - let diff = diff.read(cx); - let mut ranges = diff - .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx) - .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer)) - .collect::>(); - ranges.extend( - self.revealed_ranges - .iter() - .map(|range| range.to_point(buffer)), - ); - ranges.sort_unstable_by_key(|range| (range.start, Reverse(range.end))); - - // Merge adjacent ranges - let mut ranges = ranges.into_iter().peekable(); - let mut merged_ranges = Vec::new(); - while let Some(mut range) = ranges.next() { - while let Some(next_range) = ranges.peek() { - if range.end >= next_range.start { - range.end = range.end.max(next_range.end); - ranges.next(); - } else { - break; - } - } - - merged_ranges.push(range); - } - merged_ranges - } - - pub fn finalize(&mut self, cx: &mut Context) -> Result<()> { - let ranges = self.excerpt_ranges(cx); - let buffer = self.buffer.take().context("card was already finalized")?; - let base_text = self - .base_text - .take() - .context("card was already finalized")?; - let language_registry = self.project.read(cx).languages().clone(); - - // Replace the buffer in the multibuffer with the snapshot - let buffer = cx.new(|cx| { - let language = buffer.read(cx).language().cloned(); - let buffer = TextBuffer::new_normalized( - 0, - cx.entity_id().as_non_zero_u64().into(), - buffer.read(cx).line_ending(), - buffer.read(cx).as_rope().clone(), - ); - let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite); - buffer.set_language(language, cx); - buffer - }); - - let buffer_diff = cx.spawn({ - let buffer = buffer.clone(); - async move |_this, cx| { - build_buffer_diff(base_text, &buffer, &language_registry, cx).await - } - }); - - cx.spawn(async move |this, cx| { - let buffer_diff = buffer_diff.await?; - this.update(cx, |this, cx| { - this.multibuffer.update(cx, |multibuffer, cx| { - let path_key = PathKey::for_buffer(&buffer, cx); - multibuffer.clear(cx); - multibuffer.set_excerpts_for_path( - path_key, - buffer, - ranges, - multibuffer_context_lines(cx), - cx, - ); - multibuffer.add_diff(buffer_diff.clone(), cx); - }); - - cx.notify(); - }) - }) - .detach_and_log_err(cx); - Ok(()) - } -} - -impl ToolCard for EditFileToolCard { - fn render( - &mut self, - status: &ToolUseStatus, - window: &mut Window, - workspace: WeakEntity, - cx: &mut Context, - ) -> impl IntoElement { - let error_message = match status { - ToolUseStatus::Error(err) => Some(err), - _ => None, - }; - - let running_or_pending = match status { - ToolUseStatus::Running | ToolUseStatus::Pending => Some(()), - _ => None, - }; - - let should_show_loading = running_or_pending.is_some() && !self.full_height_expanded; - - let path_label_button = h_flex() - .id(("edit-tool-path-label-button", self.editor.entity_id())) - .w_full() - .max_w_full() - .px_1() - .gap_0p5() - .cursor_pointer() - .rounded_sm() - .opacity(0.8) - .hover(|label| { - label - .opacity(1.) - .bg(cx.theme().colors().element_hover.opacity(0.5)) - }) - .tooltip(Tooltip::text("Jump to File")) - .child( - h_flex() - .child( - Icon::new(IconName::ToolPencil) - .size(IconSize::Small) - .color(Color::Muted), - ) - .child( - div() - .text_size(rems(0.8125)) - .child(self.path.display().to_string()) - .ml_1p5() - .mr_0p5(), - ) - .child( - Icon::new(IconName::ArrowUpRight) - .size(IconSize::Small) - .color(Color::Ignored), - ), - ) - .on_click({ - let path = self.path.clone(); - move |_, window, cx| { - workspace - .update(cx, { - |workspace, cx| { - let Some(project_path) = - workspace.project().read(cx).find_project_path(&path, cx) - else { - return; - }; - let open_task = - workspace.open_path(project_path, None, true, window, cx); - window - .spawn(cx, async move |cx| { - let item = open_task.await?; - if let Some(active_editor) = item.downcast::() { - active_editor - .update_in(cx, |editor, window, cx| { - let snapshot = - editor.buffer().read(cx).snapshot(cx); - let first_hunk = editor - .diff_hunks_in_ranges( - &[editor::Anchor::min() - ..editor::Anchor::max()], - &snapshot, - ) - .next(); - if let Some(first_hunk) = first_hunk { - let first_hunk_start = - first_hunk.multi_buffer_range().start; - editor.change_selections( - Default::default(), - window, - cx, - |selections| { - selections.select_anchor_ranges([ - first_hunk_start - ..first_hunk_start, - ]); - }, - ) - } - }) - .log_err(); - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - }) - .ok(); - } - }) - .into_any_element(); - - let codeblock_header_bg = cx - .theme() - .colors() - .element_background - .blend(cx.theme().colors().editor_foreground.opacity(0.025)); - - let codeblock_header = h_flex() - .flex_none() - .p_1() - .gap_1() - .justify_between() - .rounded_t_md() - .when(error_message.is_none(), |header| { - header.bg(codeblock_header_bg) - }) - .child(path_label_button) - .when(should_show_loading, |header| { - header.pr_1p5().child( - Icon::new(IconName::ArrowCircle) - .size(IconSize::XSmall) - .color(Color::Info) - .with_rotate_animation(2), - ) - }) - .when_some(error_message, |header, error_message| { - header.child( - h_flex() - .gap_1() - .child( - Icon::new(IconName::Close) - .size(IconSize::Small) - .color(Color::Error), - ) - .child( - Disclosure::new( - ("edit-file-error-disclosure", self.editor.entity_id()), - self.error_expanded.is_some(), - ) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .on_click(cx.listener({ - let error_message = error_message.clone(); - - move |this, _event, _window, cx| { - if this.error_expanded.is_some() { - this.error_expanded.take(); - } else { - this.error_expanded = Some(cx.new(|cx| { - Markdown::new(error_message.clone(), None, None, cx) - })) - } - cx.notify(); - } - })), - ), - ) - }) - .when(error_message.is_none() && !self.is_loading(), |header| { - header.child( - Disclosure::new( - ("edit-file-disclosure", self.editor.entity_id()), - self.preview_expanded, - ) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .on_click(cx.listener( - move |this, _event, _window, _cx| { - this.preview_expanded = !this.preview_expanded; - }, - )), - ) - }); - - let (editor, editor_line_height) = self.editor.update(cx, |editor, cx| { - let line_height = editor - .style() - .map(|style| style.text.line_height_in_pixels(window.rem_size())) - .unwrap_or_default(); - - editor.set_text_style_refinement(TextStyleRefinement { - font_size: Some( - TextSize::Small - .rems(cx) - .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx)) - .into(), - ), - ..TextStyleRefinement::default() - }); - let element = editor.render(window, cx); - (element.into_any_element(), line_height) - }); - - let border_color = cx.theme().colors().border.opacity(0.6); - - let waiting_for_diff = { - let styles = [ - ("w_4_5", (0.1, 0.85), 2000), - ("w_1_4", (0.2, 0.75), 2200), - ("w_2_4", (0.15, 0.64), 1900), - ("w_3_5", (0.25, 0.72), 2300), - ("w_2_5", (0.3, 0.56), 1800), - ]; - - let mut container = v_flex() - .p_3() - .gap_1() - .border_t_1() - .rounded_b_md() - .border_color(border_color) - .bg(cx.theme().colors().editor_background); - - for (width_method, pulse_range, duration_ms) in styles.iter() { - let (min_opacity, max_opacity) = *pulse_range; - let placeholder = match *width_method { - "w_4_5" => div().w_3_4(), - "w_1_4" => div().w_1_4(), - "w_2_4" => div().w_2_4(), - "w_3_5" => div().w_3_5(), - "w_2_5" => div().w_2_5(), - _ => div().w_1_2(), - } - .id("loading_div") - .h_1() - .rounded_full() - .bg(cx.theme().colors().element_active) - .with_animation( - "loading_pulsate", - Animation::new(Duration::from_millis(*duration_ms)) - .repeat() - .with_easing(pulsating_between(min_opacity, max_opacity)), - |label, delta| label.opacity(delta), - ); - - container = container.child(placeholder); - } - - container - }; - - v_flex() - .mb_2() - .border_1() - .when(error_message.is_some(), |card| card.border_dashed()) - .border_color(border_color) - .rounded_md() - .overflow_hidden() - .child(codeblock_header) - .when_some(self.error_expanded.as_ref(), |card, error_markdown| { - card.child( - v_flex() - .p_2() - .gap_1() - .border_t_1() - .border_dashed() - .border_color(border_color) - .bg(cx.theme().colors().editor_background) - .rounded_b_md() - .child( - Label::new("Error") - .size(LabelSize::XSmall) - .color(Color::Error), - ) - .child( - div() - .rounded_md() - .text_ui_sm(cx) - .bg(cx.theme().colors().editor_background) - .child(MarkdownElement::new( - error_markdown.clone(), - markdown_style(window, cx), - )), - ), - ) - }) - .when(self.is_loading() && error_message.is_none(), |card| { - card.child(waiting_for_diff) - }) - .when(self.preview_expanded && !self.is_loading(), |card| { - let editor_view = v_flex() - .relative() - .h_full() - .when(!self.full_height_expanded, |editor_container| { - editor_container.max_h(px(COLLAPSED_LINES as f32 * editor_line_height.0)) - }) - .overflow_hidden() - .border_t_1() - .border_color(border_color) - .bg(cx.theme().colors().editor_background) - .child(editor); - - card.child( - ToolOutputPreview::new(editor_view.into_any_element(), self.editor.entity_id()) - .with_total_lines(self.total_lines.unwrap_or(0) as usize) - .toggle_state(self.full_height_expanded) - .with_collapsed_fade() - .on_toggle({ - let this = cx.entity().downgrade(); - move |is_expanded, _window, cx| { - if let Some(this) = this.upgrade() { - this.update(cx, |this, _cx| { - this.full_height_expanded = is_expanded; - }); - } - } - }), - ) - }) - } -} - -fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle { - let theme_settings = ThemeSettings::get_global(cx); - let ui_font_size = TextSize::Default.rems(cx); - let mut text_style = window.text_style(); - - text_style.refine(&TextStyleRefinement { - font_family: Some(theme_settings.ui_font.family.clone()), - font_fallbacks: theme_settings.ui_font.fallbacks.clone(), - font_features: Some(theme_settings.ui_font.features.clone()), - font_size: Some(ui_font_size.into()), - color: Some(cx.theme().colors().text), - ..Default::default() - }); - - MarkdownStyle { - base_text_style: text_style.clone(), - selection_background_color: cx.theme().colors().element_selection_background, - ..Default::default() - } -} - -async fn build_buffer( - mut text: String, - path: Arc, - language_registry: &Arc, - cx: &mut AsyncApp, -) -> Result> { - let line_ending = LineEnding::detect(&text); - LineEnding::normalize(&mut text); - let text = Rope::from(text); - let language = cx - .update(|_cx| language_registry.language_for_file_path(&path))? - .await - .ok(); - let buffer = cx.new(|cx| { - let buffer = TextBuffer::new_normalized( - 0, - cx.entity_id().as_non_zero_u64().into(), - line_ending, - text, - ); - let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite); - buffer.set_language(language, cx); - buffer - })?; - Ok(buffer) -} - -async fn build_buffer_diff( - old_text: Arc, - buffer: &Entity, - language_registry: &Arc, - cx: &mut AsyncApp, -) -> Result> { - let buffer = cx.update(|cx| buffer.read(cx).snapshot())?; - - let old_text_rope = cx - .background_spawn({ - let old_text = old_text.clone(); - async move { Rope::from(old_text.as_str()) } - }) - .await; - let base_buffer = cx - .update(|cx| { - Buffer::build_snapshot( - old_text_rope, - buffer.language().cloned(), - Some(language_registry.clone()), - cx, - ) - })? - .await; - - let diff_snapshot = cx - .update(|cx| { - BufferDiffSnapshot::new_with_base_buffer( - buffer.text.clone(), - Some(old_text), - base_buffer, - cx, - ) - })? - .await; - - let secondary_diff = cx.new(|cx| { - let mut diff = BufferDiff::new(&buffer, cx); - diff.set_snapshot(diff_snapshot.clone(), &buffer, cx); - diff - })?; - - cx.new(|cx| { - let mut diff = BufferDiff::new(&buffer.text, cx); - diff.set_snapshot(diff_snapshot, &buffer, cx); - diff.set_secondary_diff(secondary_diff); - diff - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use ::fs::Fs; - use client::TelemetrySettings; - use encodings::Encoding; - use encodings::UTF_8; - use gpui::{TestAppContext, UpdateGlobal}; - use language_model::fake_provider::FakeLanguageModel; - use serde_json::json; - use settings::SettingsStore; - use std::fs; - use util::path; - - #[gpui::test] - async fn test_edit_nonexistent_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree("/root", json!({})).await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - let result = cx - .update(|cx| { - let input = serde_json::to_value(EditFileToolInput { - display_description: "Some edit".into(), - path: "root/nonexistent_file.txt".into(), - mode: EditFileMode::Edit, - }) - .unwrap(); - Arc::new(EditFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log, - model, - None, - cx, - ) - .output - }) - .await; - assert_eq!( - result.unwrap_err().to_string(), - "Can't edit file: path not found" - ); - } - - #[gpui::test] - async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) { - let mode = &EditFileMode::Create; - - let result = test_resolve_path(mode, "root/new.txt", cx); - assert_resolved_path_eq(result.await, "new.txt"); - - let result = test_resolve_path(mode, "new.txt", cx); - assert_resolved_path_eq(result.await, "new.txt"); - - let result = test_resolve_path(mode, "dir/new.txt", cx); - assert_resolved_path_eq(result.await, "dir/new.txt"); - - let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx); - assert_eq!( - result.await.unwrap_err().to_string(), - "Can't create file: file already exists" - ); - - let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx); - assert_eq!( - result.await.unwrap_err().to_string(), - "Can't create file: parent directory doesn't exist" - ); - } - - #[gpui::test] - async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) { - let mode = &EditFileMode::Edit; - - let path_with_root = "root/dir/subdir/existing.txt"; - let path_without_root = "dir/subdir/existing.txt"; - let result = test_resolve_path(mode, path_with_root, cx); - assert_resolved_path_eq(result.await, path_without_root); - - let result = test_resolve_path(mode, path_without_root, cx); - assert_resolved_path_eq(result.await, path_without_root); - - let result = test_resolve_path(mode, "root/nonexistent.txt", cx); - assert_eq!( - result.await.unwrap_err().to_string(), - "Can't edit file: path not found" - ); - - let result = test_resolve_path(mode, "root/dir", cx); - assert_eq!( - result.await.unwrap_err().to_string(), - "Can't edit file: path is a directory" - ); - } - - async fn test_resolve_path( - mode: &EditFileMode, - path: &str, - cx: &mut TestAppContext, - ) -> anyhow::Result { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "dir": { - "subdir": { - "existing.txt": "hello" - } - } - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - let input = EditFileToolInput { - display_description: "Some edit".into(), - path: path.into(), - mode: mode.clone(), - }; - - cx.update(|cx| resolve_path(&input, project, cx)) - } - - fn assert_resolved_path_eq(path: anyhow::Result, expected: &str) { - let actual = path - .expect("Should return valid path") - .path - .to_str() - .unwrap() - .replace("\\", "/"); // Naive Windows paths normalization - assert_eq!(actual, expected); - } - - #[test] - fn still_streaming_ui_text_with_path() { - let input = json!({ - "path": "src/main.rs", - "display_description": "", - "old_string": "old code", - "new_string": "new code" - }); - - assert_eq!(EditFileTool.still_streaming_ui_text(&input), "src/main.rs"); - } - - #[test] - fn still_streaming_ui_text_with_description() { - let input = json!({ - "path": "", - "display_description": "Fix error handling", - "old_string": "old code", - "new_string": "new code" - }); - - assert_eq!( - EditFileTool.still_streaming_ui_text(&input), - "Fix error handling", - ); - } - - #[test] - fn still_streaming_ui_text_with_path_and_description() { - let input = json!({ - "path": "src/main.rs", - "display_description": "Fix error handling", - "old_string": "old code", - "new_string": "new code" - }); - - assert_eq!( - EditFileTool.still_streaming_ui_text(&input), - "Fix error handling", - ); - } - - #[test] - fn still_streaming_ui_text_no_path_or_description() { - let input = json!({ - "path": "", - "display_description": "", - "old_string": "old code", - "new_string": "new code" - }); - - assert_eq!( - EditFileTool.still_streaming_ui_text(&input), - DEFAULT_UI_TEXT, - ); - } - - #[test] - fn still_streaming_ui_text_with_null() { - let input = serde_json::Value::Null; - - assert_eq!( - EditFileTool.still_streaming_ui_text(&input), - DEFAULT_UI_TEXT, - ); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - language::init(cx); - TelemetrySettings::register(cx); - agent_settings::AgentSettings::register(cx); - Project::init_settings(cx); - }); - } - - fn init_test_with_config(cx: &mut TestAppContext, data_dir: &Path) { - cx.update(|cx| { - // Set custom data directory (config will be under data_dir/config) - paths::set_custom_data_dir(data_dir.to_str().unwrap()); - - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - language::init(cx); - TelemetrySettings::register(cx); - agent_settings::AgentSettings::register(cx); - Project::init_settings(cx); - }); - } - - #[gpui::test] - async fn test_format_on_save(cx: &mut TestAppContext) { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree("/root", json!({"src": {}})).await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - // Set up a Rust language with LSP formatting support - let rust_language = Arc::new(language::Language::new( - language::LanguageConfig { - name: "Rust".into(), - matcher: language::LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - None, - )); - - // Register the language and fake LSP - let language_registry = project.read_with(cx, |project, _| project.languages().clone()); - language_registry.add(rust_language); - - let mut fake_language_servers = language_registry.register_fake_lsp( - "Rust", - language::FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - document_formatting_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - }, - ); - - // Create the file - fs.save( - path!("/root/src/main.rs").as_ref(), - &"initial content".into(), - language::LineEnding::Unix, - Encoding::new(UTF_8), - ) - .await - .unwrap(); - - // Open the buffer to trigger LSP initialization - let buffer = project - .update(cx, |project, cx| { - project.open_local_buffer(path!("/root/src/main.rs"), cx) - }) - .await - .unwrap(); - - // Register the buffer with language servers - let _handle = project.update(cx, |project, cx| { - project.register_buffer_with_language_servers(&buffer, cx) - }); - - const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\n"; - const FORMATTED_CONTENT: &str = - "This file was formatted by the fake formatter in the test.\n"; - - // Get the fake language server and set up formatting handler - let fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.set_request_handler::({ - |_, _| async move { - Ok(Some(vec![lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 0)), - new_text: FORMATTED_CONTENT.to_string(), - }])) - } - }); - - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - - // First, test with format_on_save enabled - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::( - cx, - |settings| { - settings.defaults.format_on_save = Some(FormatOnSave::On); - settings.defaults.formatter = - Some(language::language_settings::SelectedFormatter::Auto); - }, - ); - }); - }); - - // Have the model stream unformatted content - let edit_result = { - let edit_task = cx.update(|cx| { - let input = serde_json::to_value(EditFileToolInput { - display_description: "Create main function".into(), - path: "root/src/main.rs".into(), - mode: EditFileMode::Overwrite, - }) - .unwrap(); - Arc::new(EditFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }); - - // Stream the unformatted content - cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); - model.end_last_completion_stream(); - - edit_task.await - }; - assert!(edit_result.is_ok()); - - // Wait for any async operations (e.g. formatting) to complete - cx.executor().run_until_parked(); - - // Read the file to verify it was formatted automatically - let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); - assert_eq!( - // Ignore carriage returns on Windows - new_content.replace("\r\n", "\n"), - FORMATTED_CONTENT, - "Code should be formatted when format_on_save is enabled" - ); - - let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count()); - - assert_eq!( - stale_buffer_count, 0, - "BUG: Buffer is incorrectly marked as stale after format-on-save. Found {} stale buffers. \ - This causes the agent to think the file was modified externally when it was just formatted.", - stale_buffer_count - ); - - // Next, test with format_on_save disabled - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::( - cx, - |settings| { - settings.defaults.format_on_save = Some(FormatOnSave::Off); - }, - ); - }); - }); - - // Stream unformatted edits again - let edit_result = { - let edit_task = cx.update(|cx| { - let input = serde_json::to_value(EditFileToolInput { - display_description: "Update main function".into(), - path: "root/src/main.rs".into(), - mode: EditFileMode::Overwrite, - }) - .unwrap(); - Arc::new(EditFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }); - - // Stream the unformatted content - cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); - model.end_last_completion_stream(); - - edit_task.await - }; - assert!(edit_result.is_ok()); - - // Wait for any async operations (e.g. formatting) to complete - cx.executor().run_until_parked(); - - // Verify the file was not formatted - let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); - assert_eq!( - // Ignore carriage returns on Windows - new_content.replace("\r\n", "\n"), - UNFORMATTED_CONTENT, - "Code should not be formatted when format_on_save is disabled" - ); - } - - #[gpui::test] - async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree("/root", json!({"src": {}})).await; - - // Create a simple file with trailing whitespace - fs.save( - path!("/root/src/main.rs").as_ref(), - &"initial content".into(), - language::LineEnding::Unix, - Encoding::default(), - ) - .await - .unwrap(); - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - - // First, test with remove_trailing_whitespace_on_save enabled - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::( - cx, - |settings| { - settings.defaults.remove_trailing_whitespace_on_save = Some(true); - }, - ); - }); - }); - - const CONTENT_WITH_TRAILING_WHITESPACE: &str = - "fn main() { \n println!(\"Hello!\"); \n}\n"; - - // Have the model stream content that contains trailing whitespace - let edit_result = { - let edit_task = cx.update(|cx| { - let input = serde_json::to_value(EditFileToolInput { - display_description: "Create main function".into(), - path: "root/src/main.rs".into(), - mode: EditFileMode::Overwrite, - }) - .unwrap(); - Arc::new(EditFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }); - - // Stream the content with trailing whitespace - cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk( - CONTENT_WITH_TRAILING_WHITESPACE.to_string(), - ); - model.end_last_completion_stream(); - - edit_task.await - }; - assert!(edit_result.is_ok()); - - // Wait for any async operations (e.g. formatting) to complete - cx.executor().run_until_parked(); - - // Read the file to verify trailing whitespace was removed automatically - assert_eq!( - // Ignore carriage returns on Windows - fs.load(path!("/root/src/main.rs").as_ref()) - .await - .unwrap() - .replace("\r\n", "\n"), - "fn main() {\n println!(\"Hello!\");\n}\n", - "Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled" - ); - - // Next, test with remove_trailing_whitespace_on_save disabled - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::( - cx, - |settings| { - settings.defaults.remove_trailing_whitespace_on_save = Some(false); - }, - ); - }); - }); - - // Stream edits again with trailing whitespace - let edit_result = { - let edit_task = cx.update(|cx| { - let input = serde_json::to_value(EditFileToolInput { - display_description: "Update main function".into(), - path: "root/src/main.rs".into(), - mode: EditFileMode::Overwrite, - }) - .unwrap(); - Arc::new(EditFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }); - - // Stream the content with trailing whitespace - cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk( - CONTENT_WITH_TRAILING_WHITESPACE.to_string(), - ); - model.end_last_completion_stream(); - - edit_task.await - }; - assert!(edit_result.is_ok()); - - // Wait for any async operations (e.g. formatting) to complete - cx.executor().run_until_parked(); - - // Verify the file still has trailing whitespace - // Read the file again - it should still have trailing whitespace - let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); - assert_eq!( - // Ignore carriage returns on Windows - final_content.replace("\r\n", "\n"), - CONTENT_WITH_TRAILING_WHITESPACE, - "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled" - ); - } - - #[gpui::test] - async fn test_needs_confirmation(cx: &mut TestAppContext) { - init_test(cx); - let tool = Arc::new(EditFileTool); - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree("/root", json!({})).await; - - // Test 1: Path with .zed component should require confirmation - let input_with_zed = json!({ - "display_description": "Edit settings", - "path": ".zed/settings.json", - "mode": "edit" - }); - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - cx.update(|cx| { - assert!( - tool.needs_confirmation(&input_with_zed, &project, cx), - "Path with .zed component should require confirmation" - ); - }); - - // Test 2: Absolute path should require confirmation - let input_absolute = json!({ - "display_description": "Edit file", - "path": "/etc/hosts", - "mode": "edit" - }); - cx.update(|cx| { - assert!( - tool.needs_confirmation(&input_absolute, &project, cx), - "Absolute path should require confirmation" - ); - }); - - // Test 3: Relative path without .zed should not require confirmation - let input_relative = json!({ - "display_description": "Edit file", - "path": "root/src/main.rs", - "mode": "edit" - }); - cx.update(|cx| { - assert!( - !tool.needs_confirmation(&input_relative, &project, cx), - "Relative path without .zed should not require confirmation" - ); - }); - - // Test 4: Path with .zed in the middle should require confirmation - let input_zed_middle = json!({ - "display_description": "Edit settings", - "path": "root/.zed/tasks.json", - "mode": "edit" - }); - cx.update(|cx| { - assert!( - tool.needs_confirmation(&input_zed_middle, &project, cx), - "Path with .zed in any component should require confirmation" - ); - }); - - // Test 5: When always_allow_tool_actions is enabled, no confirmation needed - cx.update(|cx| { - let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); - settings.always_allow_tool_actions = true; - agent_settings::AgentSettings::override_global(settings, cx); - - assert!( - !tool.needs_confirmation(&input_with_zed, &project, cx), - "When always_allow_tool_actions is true, no confirmation should be needed" - ); - assert!( - !tool.needs_confirmation(&input_absolute, &project, cx), - "When always_allow_tool_actions is true, no confirmation should be needed for absolute paths" - ); - }); - } - - #[gpui::test] - async fn test_ui_text_shows_correct_context(cx: &mut TestAppContext) { - // Set up a custom config directory for testing - let temp_dir = tempfile::tempdir().unwrap(); - init_test_with_config(cx, temp_dir.path()); - - let tool = Arc::new(EditFileTool); - - // Test ui_text shows context for various paths - let test_cases = vec![ - ( - json!({ - "display_description": "Update config", - "path": ".zed/settings.json", - "mode": "edit" - }), - "Update config (local settings)", - ".zed path should show local settings context", - ), - ( - json!({ - "display_description": "Fix bug", - "path": "src/.zed/local.json", - "mode": "edit" - }), - "Fix bug (local settings)", - "Nested .zed path should show local settings context", - ), - ( - json!({ - "display_description": "Update readme", - "path": "README.md", - "mode": "edit" - }), - "Update readme", - "Normal path should not show additional context", - ), - ( - json!({ - "display_description": "Edit config", - "path": "config.zed", - "mode": "edit" - }), - "Edit config", - ".zed as extension should not show context", - ), - ]; - - for (input, expected_text, description) in test_cases { - cx.update(|_cx| { - let ui_text = tool.ui_text(&input); - assert_eq!(ui_text, expected_text, "Failed for case: {}", description); - }); - } - } - - #[gpui::test] - async fn test_needs_confirmation_outside_project(cx: &mut TestAppContext) { - init_test(cx); - let tool = Arc::new(EditFileTool); - let fs = project::FakeFs::new(cx.executor()); - - // Create a project in /project directory - fs.insert_tree("/project", json!({})).await; - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - - // Test file outside project requires confirmation - let input_outside = json!({ - "display_description": "Edit file", - "path": "/outside/file.txt", - "mode": "edit" - }); - cx.update(|cx| { - assert!( - tool.needs_confirmation(&input_outside, &project, cx), - "File outside project should require confirmation" - ); - }); - - // Test file inside project doesn't require confirmation - let input_inside = json!({ - "display_description": "Edit file", - "path": "project/file.txt", - "mode": "edit" - }); - cx.update(|cx| { - assert!( - !tool.needs_confirmation(&input_inside, &project, cx), - "File inside project should not require confirmation" - ); - }); - } - - #[gpui::test] - async fn test_needs_confirmation_config_paths(cx: &mut TestAppContext) { - // Set up a custom data directory for testing - let temp_dir = tempfile::tempdir().unwrap(); - init_test_with_config(cx, temp_dir.path()); - - let tool = Arc::new(EditFileTool); - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree("/home/user/myproject", json!({})).await; - let project = Project::test(fs.clone(), [path!("/home/user/myproject").as_ref()], cx).await; - - // Get the actual local settings folder name - let local_settings_folder = paths::local_settings_folder_relative_path(); - - // Test various config path patterns - let test_cases = vec![ - ( - format!("{}/settings.json", local_settings_folder.display()), - true, - "Top-level local settings file".to_string(), - ), - ( - format!( - "myproject/{}/settings.json", - local_settings_folder.display() - ), - true, - "Local settings in project path".to_string(), - ), - ( - format!("src/{}/config.toml", local_settings_folder.display()), - true, - "Local settings in subdirectory".to_string(), - ), - ( - ".zed.backup/file.txt".to_string(), - true, - ".zed.backup is outside project".to_string(), - ), - ( - "my.zed/file.txt".to_string(), - true, - "my.zed is outside project".to_string(), - ), - ( - "myproject/src/file.zed".to_string(), - false, - ".zed as file extension".to_string(), - ), - ( - "myproject/normal/path/file.rs".to_string(), - false, - "Normal file without config paths".to_string(), - ), - ]; - - for (path, should_confirm, description) in test_cases { - let input = json!({ - "display_description": "Edit file", - "path": path, - "mode": "edit" - }); - cx.update(|cx| { - assert_eq!( - tool.needs_confirmation(&input, &project, cx), - should_confirm, - "Failed for case: {} - path: {}", - description, - path - ); - }); - } - } - - #[gpui::test] - async fn test_needs_confirmation_global_config(cx: &mut TestAppContext) { - // Set up a custom data directory for testing - let temp_dir = tempfile::tempdir().unwrap(); - init_test_with_config(cx, temp_dir.path()); - - let tool = Arc::new(EditFileTool); - let fs = project::FakeFs::new(cx.executor()); - - // Create test files in the global config directory - let global_config_dir = paths::config_dir(); - fs::create_dir_all(&global_config_dir).unwrap(); - let global_settings_path = global_config_dir.join("settings.json"); - fs::write(&global_settings_path, "{}").unwrap(); - - fs.insert_tree("/project", json!({})).await; - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - - // Test global config paths - let test_cases = vec![ - ( - global_settings_path.to_str().unwrap().to_string(), - true, - "Global settings file should require confirmation", - ), - ( - global_config_dir - .join("keymap.json") - .to_str() - .unwrap() - .to_string(), - true, - "Global keymap file should require confirmation", - ), - ( - "project/normal_file.rs".to_string(), - false, - "Normal project file should not require confirmation", - ), - ]; - - for (path, should_confirm, description) in test_cases { - let input = json!({ - "display_description": "Edit file", - "path": path, - "mode": "edit" - }); - cx.update(|cx| { - assert_eq!( - tool.needs_confirmation(&input, &project, cx), - should_confirm, - "Failed for case: {}", - description - ); - }); - } - } - - #[gpui::test] - async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) { - init_test(cx); - let tool = Arc::new(EditFileTool); - let fs = project::FakeFs::new(cx.executor()); - - // Create multiple worktree directories - fs.insert_tree( - "/workspace/frontend", - json!({ - "src": { - "main.js": "console.log('frontend');" - } - }), - ) - .await; - fs.insert_tree( - "/workspace/backend", - json!({ - "src": { - "main.rs": "fn main() {}" - } - }), - ) - .await; - fs.insert_tree( - "/workspace/shared", - json!({ - ".zed": { - "settings.json": "{}" - } - }), - ) - .await; - - // Create project with multiple worktrees - let project = Project::test( - fs.clone(), - [ - path!("/workspace/frontend").as_ref(), - path!("/workspace/backend").as_ref(), - path!("/workspace/shared").as_ref(), - ], - cx, - ) - .await; - - // Test files in different worktrees - let test_cases = vec![ - ("frontend/src/main.js", false, "File in first worktree"), - ("backend/src/main.rs", false, "File in second worktree"), - ( - "shared/.zed/settings.json", - true, - ".zed file in third worktree", - ), - ("/etc/hosts", true, "Absolute path outside all worktrees"), - ( - "../outside/file.txt", - true, - "Relative path outside worktrees", - ), - ]; - - for (path, should_confirm, description) in test_cases { - let input = json!({ - "display_description": "Edit file", - "path": path, - "mode": "edit" - }); - cx.update(|cx| { - assert_eq!( - tool.needs_confirmation(&input, &project, cx), - should_confirm, - "Failed for case: {} - path: {}", - description, - path - ); - }); - } - } - - #[gpui::test] - async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) { - init_test(cx); - let tool = Arc::new(EditFileTool); - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree( - "/project", - json!({ - ".zed": { - "settings.json": "{}" - }, - "src": { - ".zed": { - "local.json": "{}" - } - } - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - - // Test edge cases - let test_cases = vec![ - // Empty path - find_project_path returns Some for empty paths - ("", false, "Empty path is treated as project root"), - // Root directory - ("/", true, "Root directory should be outside project"), - // Parent directory references - find_project_path resolves these - ( - "project/../other", - false, - "Path with .. is resolved by find_project_path", - ), - ( - "project/./src/file.rs", - false, - "Path with . should work normally", - ), - // Windows-style paths (if on Windows) - #[cfg(target_os = "windows")] - ("C:\\Windows\\System32\\hosts", true, "Windows system path"), - #[cfg(target_os = "windows")] - ("project\\src\\main.rs", false, "Windows-style project path"), - ]; - - for (path, should_confirm, description) in test_cases { - let input = json!({ - "display_description": "Edit file", - "path": path, - "mode": "edit" - }); - cx.update(|cx| { - assert_eq!( - tool.needs_confirmation(&input, &project, cx), - should_confirm, - "Failed for case: {} - path: {}", - description, - path - ); - }); - } - } - - #[gpui::test] - async fn test_ui_text_with_all_path_types(cx: &mut TestAppContext) { - init_test(cx); - let tool = Arc::new(EditFileTool); - - // Test UI text for various scenarios - let test_cases = vec![ - ( - json!({ - "display_description": "Update config", - "path": ".zed/settings.json", - "mode": "edit" - }), - "Update config (local settings)", - ".zed path should show local settings context", - ), - ( - json!({ - "display_description": "Fix bug", - "path": "src/.zed/local.json", - "mode": "edit" - }), - "Fix bug (local settings)", - "Nested .zed path should show local settings context", - ), - ( - json!({ - "display_description": "Update readme", - "path": "README.md", - "mode": "edit" - }), - "Update readme", - "Normal path should not show additional context", - ), - ( - json!({ - "display_description": "Edit config", - "path": "config.zed", - "mode": "edit" - }), - "Edit config", - ".zed as extension should not show context", - ), - ]; - - for (input, expected_text, description) in test_cases { - cx.update(|_cx| { - let ui_text = tool.ui_text(&input); - assert_eq!(ui_text, expected_text, "Failed for case: {}", description); - }); - } - } - - #[gpui::test] - async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) { - init_test(cx); - let tool = Arc::new(EditFileTool); - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree( - "/project", - json!({ - "existing.txt": "content", - ".zed": { - "settings.json": "{}" - } - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - - // Test different EditFileMode values - let modes = vec![ - EditFileMode::Edit, - EditFileMode::Create, - EditFileMode::Overwrite, - ]; - - for mode in modes { - // Test .zed path with different modes - let input_zed = json!({ - "display_description": "Edit settings", - "path": "project/.zed/settings.json", - "mode": mode - }); - cx.update(|cx| { - assert!( - tool.needs_confirmation(&input_zed, &project, cx), - ".zed path should require confirmation regardless of mode: {:?}", - mode - ); - }); - - // Test outside path with different modes - let input_outside = json!({ - "display_description": "Edit file", - "path": "/outside/file.txt", - "mode": mode - }); - cx.update(|cx| { - assert!( - tool.needs_confirmation(&input_outside, &project, cx), - "Outside path should require confirmation regardless of mode: {:?}", - mode - ); - }); - - // Test normal path with different modes - let input_normal = json!({ - "display_description": "Edit file", - "path": "project/normal.txt", - "mode": mode - }); - cx.update(|cx| { - assert!( - !tool.needs_confirmation(&input_normal, &project, cx), - "Normal path should not require confirmation regardless of mode: {:?}", - mode - ); - }); - } - } - - #[gpui::test] - async fn test_always_allow_tool_actions_bypasses_all_checks(cx: &mut TestAppContext) { - // Set up with custom directories for deterministic testing - let temp_dir = tempfile::tempdir().unwrap(); - init_test_with_config(cx, temp_dir.path()); - - let tool = Arc::new(EditFileTool); - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree("/project", json!({})).await; - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - - // Enable always_allow_tool_actions - cx.update(|cx| { - let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); - settings.always_allow_tool_actions = true; - agent_settings::AgentSettings::override_global(settings, cx); - }); - - // Test that all paths that normally require confirmation are bypassed - let global_settings_path = paths::config_dir().join("settings.json"); - fs::create_dir_all(paths::config_dir()).unwrap(); - fs::write(&global_settings_path, "{}").unwrap(); - - let test_cases = vec![ - ".zed/settings.json", - "project/.zed/config.toml", - global_settings_path.to_str().unwrap(), - "/etc/hosts", - "/absolute/path/file.txt", - "../outside/project.txt", - ]; - - for path in test_cases { - let input = json!({ - "display_description": "Edit file", - "path": path, - "mode": "edit" - }); - cx.update(|cx| { - assert!( - !tool.needs_confirmation(&input, &project, cx), - "Path {} should not require confirmation when always_allow_tool_actions is true", - path - ); - }); - } - - // Disable always_allow_tool_actions and verify confirmation is required again - cx.update(|cx| { - let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); - settings.always_allow_tool_actions = false; - agent_settings::AgentSettings::override_global(settings, cx); - }); - - // Verify .zed path requires confirmation again - let input = json!({ - "display_description": "Edit file", - "path": ".zed/settings.json", - "mode": "edit" - }); - cx.update(|cx| { - assert!( - tool.needs_confirmation(&input, &project, cx), - ".zed path should require confirmation when always_allow_tool_actions is false" - ); - }); - } -} diff --git a/crates/edit_prediction_context/src/syntax_index.rs b/crates/edit_prediction_context/src/syntax_index.rs index 1353b0ea289f1a..9b2b884e2f1d82 100644 --- a/crates/edit_prediction_context/src/syntax_index.rs +++ b/crates/edit_prediction_context/src/syntax_index.rs @@ -524,6 +524,8 @@ impl SyntaxIndex { let snapshot_task = worktree.update(cx, |worktree, cx| { let load_task = worktree.load_file(&project_path.path, None, false, true, None, cx); + let worktree_abs_path = worktree.abs_path(); + cx.spawn(async move |_this, cx| { let loaded_file = load_task.await?; let language = language.await?; diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 1050eaf88ac966..dc67ab3ed6c8cf 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -64,18 +64,6 @@ pub struct Jupyter { pub enabled: bool, } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct StatusBar { - /// Whether to display the active language button in the status bar. - /// - /// Default: true - pub active_language_button: bool, - /// Whether to show the cursor position button in the status bar. - /// - /// Default: true - pub cursor_position_button: bool, -} - #[derive(Clone, Debug, PartialEq, Eq)] pub struct Toolbar { pub breadcrumbs: bool, diff --git a/crates/encodings_ui/Cargo.toml b/crates/encodings_ui/Cargo.toml index 31a7783d133202..64c5d14161963e 100644 --- a/crates/encodings_ui/Cargo.toml +++ b/crates/encodings_ui/Cargo.toml @@ -18,7 +18,6 @@ settings.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true -workspace-hack.workspace = true zed_actions.workspace = true diff --git a/crates/encodings_ui/src/lib.rs b/crates/encodings_ui/src/lib.rs index b77cf991505aaa..bb8d2d38583bc9 100644 --- a/crates/encodings_ui/src/lib.rs +++ b/crates/encodings_ui/src/lib.rs @@ -75,7 +75,7 @@ impl Render for EncodingIndicator { let weak_workspace = workspace.weak_handle(); if let Some(path) = buffer.read(cx).file() { - let path = path.clone().path().to_path_buf(); + let path = path.clone().path().to_rel_path_buf(); workspace.toggle_modal(window, cx, |window, cx| { let selector = EncodingSelector::new( window, @@ -83,7 +83,7 @@ impl Render for EncodingIndicator { Action::Save, Some(buffer.downgrade()), weak_workspace, - Some(path), + Some(path.as_std_path().to_path_buf()), ); selector }); diff --git a/crates/git_ui/src/file_diff_view.rs b/crates/git_ui/src/file_diff_view.rs index 7746ac8519250d..27811859b1ab95 100644 --- a/crates/git_ui/src/file_diff_view.rs +++ b/crates/git_ui/src/file_diff_view.rs @@ -363,7 +363,6 @@ mod tests { use language::Rope; use project::{FakeFs, Fs, Project}; use settings::SettingsStore; - use project::{FakeFs, Fs, Project, encodings::EncodingWrapper}; use std::path::PathBuf; use unindent::unindent; use util::path; diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 2afc02893fd6a7..d24934ee24d400 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -69,7 +69,6 @@ unicase = "2.6" util.workspace = true watch.workspace = true zlog.workspace = true -diffy = "0.4.2" encoding = "0.2.33" [dev-dependencies] diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index f68a4229f179db..21ad263511d078 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -8,8 +8,7 @@ use anyhow::{Context as _, Result, anyhow}; use client::Client; use collections::{HashMap, HashSet, hash_map}; use encodings::Encoding; -use fs::Fs; -use futures::{Future, FutureExt as _, StreamExt, channel::oneshot, future::Shared}; +use futures::{Future, FutureExt as _, channel::oneshot, future::Shared}; use gpui::{ App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, }; @@ -27,7 +26,7 @@ use rpc::{ use smol::channel::Receiver; use std::{io, pin::pin, sync::Arc, time::Instant}; use text::{BufferId, ReplicaId}; -use util::{ResultExt as _, TryFutureExt, debug_panic, maybe, paths::PathStyle, rel_path::RelPath}; +use util::{ResultExt as _, TryFutureExt, debug_panic, maybe, rel_path::RelPath}; use worktree::{File, PathChange, ProjectEntryId, Worktree, WorktreeId}; /// A set of open buffers. @@ -398,7 +397,7 @@ impl LocalBufferStore { } let save = worktree.update(cx, |worktree, cx| { - worktree.write_file(path.as_ref(), text, line_ending, cx, (*encoding).clone()) + worktree.write_file(path.clone(), text, line_ending, cx, (*encoding).clone()) }); cx.spawn(async move |this, cx| { @@ -633,8 +632,6 @@ impl LocalBufferStore { detect_utf16: bool, cx: &mut Context, ) -> Task>> { - println!("{:?}", encoding); - let load_buffer = worktree.update(cx, |worktree, cx| { let reservation = cx.reserve_entity(); let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64()); @@ -653,7 +650,7 @@ impl LocalBufferStore { let buffer = cx.insert_entity(reservation, |_| { Buffer::build( - text::Buffer::new(0, buffer_id, loaded_file.text), + text::Buffer::new(ReplicaId::LOCAL, buffer_id, loaded_file.text), Some(loaded_file.file), Capability::ReadWrite, ) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 231349d63eef7c..2650ca78e534c5 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -27,7 +27,7 @@ mod environment; use buffer_diff::BufferDiff; use context_server_store::ContextServerStore; -use encodings::EncodingOptions; +use encodings::{Encoding, EncodingOptions}; pub use environment::ProjectEnvironmentEvent; use git::repository::get_git_committer; use git_store::{Repository, RepositoryId}; @@ -5408,7 +5408,9 @@ impl Project { }; cx.spawn(async move |cx| { let file = worktree - .update(cx, |worktree, cx| worktree.load_file(&rel_path, cx))? + .update(cx, |worktree, cx| { + worktree.load_file(&rel_path, None, false, true, None, cx) + })? .await .context("Failed to load settings file")?; @@ -5423,6 +5425,11 @@ impl Project { Rope::from_str(&new_text, cx.background_executor()), line_ending, cx, + Encoding::default(), + new_text.into(), + line_ending, + cx, + Encoding::default(), ) })? .await diff --git a/crates/project/src/project_search.rs b/crates/project/src/project_search.rs new file mode 100644 index 00000000000000..b68632d9ca1e2f --- /dev/null +++ b/crates/project/src/project_search.rs @@ -0,0 +1,754 @@ +use std::{ + io::{BufRead, BufReader}, + path::Path, + pin::pin, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, +}; + +use anyhow::Context; +use collections::HashSet; +use fs::Fs; +use futures::{SinkExt, StreamExt, select_biased, stream::FuturesOrdered}; +use gpui::{App, AppContext, AsyncApp, Entity, Task}; +use language::{Buffer, BufferSnapshot}; +use parking_lot::Mutex; +use postage::oneshot; +use rpc::{AnyProtoClient, proto}; +use smol::{ + channel::{Receiver, Sender, bounded, unbounded}, + future::FutureExt, +}; + +use text::BufferId; +use util::{ResultExt, maybe, paths::compare_rel_paths}; +use worktree::{Entry, ProjectEntryId, Snapshot, Worktree}; + +use crate::{ + Project, ProjectItem, ProjectPath, RemotelyCreatedModels, + buffer_store::BufferStore, + search::{SearchQuery, SearchResult}, + worktree_store::WorktreeStore, +}; + +pub struct Search { + buffer_store: Entity, + worktree_store: Entity, + limit: usize, + kind: SearchKind, +} + +/// Represents search setup, before it is actually kicked off with Search::into_results +enum SearchKind { + /// Search for candidates by inspecting file contents on file system, avoiding loading the buffer unless we know that a given file contains a match. + Local { + fs: Arc, + worktrees: Vec>, + }, + /// Query remote host for candidates. As of writing, the host runs a local search in "buffers with matches only" mode. + Remote { + client: AnyProtoClient, + remote_id: u64, + models: Arc>, + }, + /// Run search against a known set of candidates. Even when working with a remote host, this won't round-trip to host. + OpenBuffersOnly, +} + +/// Represents results of project search and allows one to either obtain match positions OR +/// just the handles to buffers that may match the search. Grabbing the handles is cheaper than obtaining full match positions, because in that case we'll look for +/// at most one match in each file. +#[must_use] +pub struct SearchResultsHandle { + results: Receiver, + matching_buffers: Receiver>, + trigger_search: Box Task<()> + Send + Sync>, +} + +impl SearchResultsHandle { + pub fn results(self, cx: &mut App) -> Receiver { + (self.trigger_search)(cx).detach(); + self.results + } + pub fn matching_buffers(self, cx: &mut App) -> Receiver> { + (self.trigger_search)(cx).detach(); + self.matching_buffers + } +} + +#[derive(Clone)] +enum FindSearchCandidates { + Local { + fs: Arc, + /// Start off with all paths in project and filter them based on: + /// - Include filters + /// - Exclude filters + /// - Only open buffers + /// - Scan ignored files + /// Put another way: filter out files that can't match (without looking at file contents) + input_paths_rx: Receiver, + /// After that, if the buffer is not yet loaded, we'll figure out if it contains at least one match + /// based on disk contents of a buffer. This step is not performed for buffers we already have in memory. + confirm_contents_will_match_tx: Sender, + confirm_contents_will_match_rx: Receiver, + /// Of those that contain at least one match (or are already in memory), look for rest of matches (and figure out their ranges). + /// But wait - first, we need to go back to the main thread to open a buffer (& create an entity for it). + get_buffer_for_full_scan_tx: Sender, + }, + Remote, + OpenBuffersOnly, +} + +impl Search { + pub fn local( + fs: Arc, + buffer_store: Entity, + worktree_store: Entity, + limit: usize, + cx: &mut App, + ) -> Self { + let worktrees = worktree_store.read(cx).visible_worktrees(cx).collect(); + Self { + kind: SearchKind::Local { fs, worktrees }, + buffer_store, + worktree_store, + limit, + } + } + + pub(crate) fn remote( + buffer_store: Entity, + worktree_store: Entity, + limit: usize, + client_state: (AnyProtoClient, u64, Arc>), + ) -> Self { + Self { + kind: SearchKind::Remote { + client: client_state.0, + remote_id: client_state.1, + models: client_state.2, + }, + buffer_store, + worktree_store, + limit, + } + } + pub(crate) fn open_buffers_only( + buffer_store: Entity, + worktree_store: Entity, + limit: usize, + ) -> Self { + Self { + kind: SearchKind::OpenBuffersOnly, + buffer_store, + worktree_store, + limit, + } + } + + pub(crate) const MAX_SEARCH_RESULT_FILES: usize = 5_000; + pub(crate) const MAX_SEARCH_RESULT_RANGES: usize = 10_000; + /// Prepares a project search run. The resulting [`SearchResultsHandle`] has to be used to specify whether you're interested in matching buffers + /// or full search results. + pub fn into_handle(mut self, query: SearchQuery, cx: &mut App) -> SearchResultsHandle { + let mut open_buffers = HashSet::default(); + let mut unnamed_buffers = Vec::new(); + const MAX_CONCURRENT_BUFFER_OPENS: usize = 64; + let buffers = self.buffer_store.read(cx); + for handle in buffers.buffers() { + let buffer = handle.read(cx); + if !buffers.is_searchable(&buffer.remote_id()) { + continue; + } else if let Some(entry_id) = buffer.entry_id(cx) { + open_buffers.insert(entry_id); + } else { + self.limit -= self.limit.saturating_sub(1); + unnamed_buffers.push(handle) + }; + } + let executor = cx.background_executor().clone(); + let (tx, rx) = unbounded(); + let (grab_buffer_snapshot_tx, grab_buffer_snapshot_rx) = unbounded(); + let matching_buffers = grab_buffer_snapshot_rx.clone(); + let trigger_search = Box::new(move |cx: &mut App| { + cx.spawn(async move |cx| { + for buffer in unnamed_buffers { + _ = grab_buffer_snapshot_tx.send(buffer).await; + } + + let (find_all_matches_tx, find_all_matches_rx) = + bounded(MAX_CONCURRENT_BUFFER_OPENS); + + let (candidate_searcher, tasks) = match self.kind { + SearchKind::OpenBuffersOnly => { + let Ok(open_buffers) = cx.update(|cx| self.all_loaded_buffers(&query, cx)) + else { + return; + }; + let fill_requests = cx + .background_spawn(async move { + for buffer in open_buffers { + if let Err(_) = grab_buffer_snapshot_tx.send(buffer).await { + return; + } + } + }) + .boxed_local(); + (FindSearchCandidates::OpenBuffersOnly, vec![fill_requests]) + } + SearchKind::Local { + fs, + ref mut worktrees, + } => { + let (get_buffer_for_full_scan_tx, get_buffer_for_full_scan_rx) = + unbounded(); + let (confirm_contents_will_match_tx, confirm_contents_will_match_rx) = + bounded(64); + let (sorted_search_results_tx, sorted_search_results_rx) = unbounded(); + + let (input_paths_tx, input_paths_rx) = unbounded(); + + let tasks = vec![ + cx.spawn(Self::provide_search_paths( + std::mem::take(worktrees), + query.include_ignored(), + input_paths_tx, + sorted_search_results_tx, + )) + .boxed_local(), + Self::open_buffers( + &self.buffer_store, + get_buffer_for_full_scan_rx, + grab_buffer_snapshot_tx, + cx.clone(), + ) + .boxed_local(), + cx.background_spawn(Self::maintain_sorted_search_results( + sorted_search_results_rx, + get_buffer_for_full_scan_tx.clone(), + self.limit, + )) + .boxed_local(), + ]; + ( + FindSearchCandidates::Local { + fs, + get_buffer_for_full_scan_tx, + confirm_contents_will_match_tx, + confirm_contents_will_match_rx, + input_paths_rx, + }, + tasks, + ) + } + SearchKind::Remote { + client, + remote_id, + models, + } => { + let request = client.request(proto::FindSearchCandidates { + project_id: remote_id, + query: Some(query.to_proto()), + limit: self.limit as _, + }); + let Ok(guard) = cx.update(|cx| { + Project::retain_remotely_created_models_impl( + &models, + &self.buffer_store, + &self.worktree_store, + cx, + ) + }) else { + return; + }; + let buffer_store = self.buffer_store.downgrade(); + let issue_remote_buffers_request = cx + .spawn(async move |cx| { + let _ = maybe!(async move { + let response = request.await?; + + for buffer_id in response.buffer_ids { + let buffer_id = BufferId::new(buffer_id)?; + let buffer = buffer_store + .update(cx, |buffer_store, cx| { + buffer_store.wait_for_remote_buffer(buffer_id, cx) + })? + .await?; + let _ = grab_buffer_snapshot_tx.send(buffer).await; + } + + drop(guard); + anyhow::Ok(()) + }) + .await + .log_err(); + }) + .boxed_local(); + ( + FindSearchCandidates::Remote, + vec![issue_remote_buffers_request], + ) + } + }; + + let matches_count = AtomicUsize::new(0); + let matched_buffer_count = AtomicUsize::new(0); + + let worker_pool = executor.scoped(|scope| { + let num_cpus = executor.num_cpus(); + + assert!(num_cpus > 0); + for _ in 0..executor.num_cpus() - 1 { + let worker = Worker { + query: &query, + open_buffers: &open_buffers, + matched_buffer_count: &matched_buffer_count, + matches_count: &matches_count, + candidates: candidate_searcher.clone(), + find_all_matches_rx: find_all_matches_rx.clone(), + publish_matches: tx.clone(), + }; + scope.spawn(worker.run()); + } + drop(tx); + drop(find_all_matches_rx); + drop(candidate_searcher); + }); + + let buffer_snapshots = Self::grab_buffer_snapshots( + grab_buffer_snapshot_rx, + find_all_matches_tx, + cx.clone(), + ); + futures::future::join_all( + [worker_pool.boxed_local(), buffer_snapshots.boxed_local()] + .into_iter() + .chain(tasks), + ) + .await; + }) + }); + + SearchResultsHandle { + results: rx, + matching_buffers, + trigger_search, + } + } + + fn provide_search_paths( + worktrees: Vec>, + include_ignored: bool, + tx: Sender, + results: Sender>, + ) -> impl AsyncFnOnce(&mut AsyncApp) { + async move |cx| { + _ = maybe!(async move { + for worktree in worktrees { + let (mut snapshot, worktree_settings) = worktree + .read_with(cx, |this, _| { + Some((this.snapshot(), this.as_local()?.settings())) + })? + .context("The worktree is not local")?; + if include_ignored { + // Pre-fetch all of the ignored directories as they're going to be searched. + let mut entries_to_refresh = vec![]; + for entry in snapshot.entries(include_ignored, 0) { + if entry.is_ignored && entry.kind.is_unloaded() { + if !worktree_settings.is_path_excluded(&entry.path) { + entries_to_refresh.push(entry.path.clone()); + } + } + } + let barrier = worktree.update(cx, |this, _| { + let local = this.as_local_mut()?; + let barrier = entries_to_refresh + .into_iter() + .map(|path| local.add_path_prefix_to_scan(path).into_future()) + .collect::>(); + Some(barrier) + })?; + if let Some(barriers) = barrier { + futures::future::join_all(barriers).await; + } + snapshot = worktree.read_with(cx, |this, _| this.snapshot())?; + } + cx.background_executor() + .scoped(|scope| { + scope.spawn(async { + for entry in snapshot.files(include_ignored, 0) { + let (should_scan_tx, should_scan_rx) = oneshot::channel(); + let Ok(_) = tx + .send(InputPath { + entry: entry.clone(), + snapshot: snapshot.clone(), + should_scan_tx, + }) + .await + else { + return; + }; + if results.send(should_scan_rx).await.is_err() { + return; + }; + } + }) + }) + .await; + } + anyhow::Ok(()) + }) + .await; + } + } + + async fn maintain_sorted_search_results( + rx: Receiver>, + paths_for_full_scan: Sender, + limit: usize, + ) { + let mut rx = pin!(rx); + let mut matched = 0; + while let Some(mut next_path_result) = rx.next().await { + let Some(successful_path) = next_path_result.next().await else { + // This math did not produce a match, hence skip it. + continue; + }; + if paths_for_full_scan.send(successful_path).await.is_err() { + return; + }; + matched += 1; + if matched >= limit { + break; + } + } + } + + /// Background workers cannot open buffers by themselves, hence main thread will do it on their behalf. + async fn open_buffers( + buffer_store: &Entity, + rx: Receiver, + find_all_matches_tx: Sender>, + mut cx: AsyncApp, + ) { + let mut rx = pin!(rx.ready_chunks(64)); + _ = maybe!(async move { + while let Some(requested_paths) = rx.next().await { + let mut buffers = buffer_store.update(&mut cx, |this, cx| { + requested_paths + .into_iter() + .map(|path| this.open_buffer(path, None, false, true, cx)) + .collect::>() + })?; + + while let Some(buffer) = buffers.next().await { + if let Some(buffer) = buffer.log_err() { + find_all_matches_tx.send(buffer).await?; + } + } + } + Result::<_, anyhow::Error>::Ok(()) + }) + .await; + } + + async fn grab_buffer_snapshots( + rx: Receiver>, + find_all_matches_tx: Sender<(Entity, BufferSnapshot)>, + mut cx: AsyncApp, + ) { + _ = maybe!(async move { + while let Ok(buffer) = rx.recv().await { + let snapshot = buffer.read_with(&mut cx, |this, _| this.snapshot())?; + find_all_matches_tx.send((buffer, snapshot)).await?; + } + Result::<_, anyhow::Error>::Ok(()) + }) + .await; + } + + fn all_loaded_buffers(&self, search_query: &SearchQuery, cx: &App) -> Vec> { + let worktree_store = self.worktree_store.read(cx); + let mut buffers = search_query + .buffers() + .into_iter() + .flatten() + .filter(|buffer| { + let b = buffer.read(cx); + if let Some(file) = b.file() { + if !search_query.match_path(file.path().as_std_path()) { + return false; + } + if !search_query.include_ignored() + && let Some(entry) = b + .entry_id(cx) + .and_then(|entry_id| worktree_store.entry_for_id(entry_id, cx)) + && entry.is_ignored + { + return false; + } + } + true + }) + .cloned() + .collect::>(); + buffers.sort_by(|a, b| { + let a = a.read(cx); + let b = b.read(cx); + match (a.file(), b.file()) { + (None, None) => a.remote_id().cmp(&b.remote_id()), + (None, Some(_)) => std::cmp::Ordering::Less, + (Some(_), None) => std::cmp::Ordering::Greater, + (Some(a), Some(b)) => compare_rel_paths((a.path(), true), (b.path(), true)), + } + }); + + buffers + } +} + +struct Worker<'search> { + query: &'search SearchQuery, + matched_buffer_count: &'search AtomicUsize, + matches_count: &'search AtomicUsize, + open_buffers: &'search HashSet, + candidates: FindSearchCandidates, + /// Ok, we're back in background: run full scan & find all matches in a given buffer snapshot. + find_all_matches_rx: Receiver<(Entity, BufferSnapshot)>, + /// Cool, we have results; let's share them with the world. + publish_matches: Sender, +} + +impl Worker<'_> { + async fn run(mut self) { + let ( + input_paths_rx, + confirm_contents_will_match_rx, + mut confirm_contents_will_match_tx, + mut get_buffer_for_full_scan_tx, + fs, + ) = match self.candidates { + FindSearchCandidates::Local { + fs, + input_paths_rx, + confirm_contents_will_match_rx, + confirm_contents_will_match_tx, + get_buffer_for_full_scan_tx, + } => ( + input_paths_rx, + confirm_contents_will_match_rx, + confirm_contents_will_match_tx, + get_buffer_for_full_scan_tx, + Some(fs), + ), + FindSearchCandidates::Remote | FindSearchCandidates::OpenBuffersOnly => ( + unbounded().1, + unbounded().1, + unbounded().0, + unbounded().0, + None, + ), + }; + let mut find_all_matches = pin!(self.find_all_matches_rx.fuse()); + let mut find_first_match = pin!(confirm_contents_will_match_rx.fuse()); + let mut scan_path = pin!(input_paths_rx.fuse()); + + loop { + let handler = RequestHandler { + query: self.query, + open_entries: &self.open_buffers, + fs: fs.as_deref(), + matched_buffer_count: self.matched_buffer_count, + matches_count: self.matches_count, + confirm_contents_will_match_tx: &confirm_contents_will_match_tx, + get_buffer_for_full_scan_tx: &get_buffer_for_full_scan_tx, + publish_matches: &self.publish_matches, + }; + // Whenever we notice that some step of a pipeline is closed, we don't want to close subsequent + // steps straight away. Another worker might be about to produce a value that will + // be pushed there, thus we'll replace current worker's pipe with a dummy one. + // That way, we'll only ever close a next-stage channel when ALL workers do so. + select_biased! { + find_all_matches = find_all_matches.next() => { + + if self.publish_matches.is_closed() { + break; + } + let Some(matches) = find_all_matches else { + self.publish_matches = bounded(1).0; + continue; + }; + let result = handler.handle_find_all_matches(matches).await; + if let Some(_should_bail) = result { + + self.publish_matches = bounded(1).0; + continue; + } + }, + find_first_match = find_first_match.next() => { + if let Some(buffer_with_at_least_one_match) = find_first_match { + handler.handle_find_first_match(buffer_with_at_least_one_match).await; + } else { + get_buffer_for_full_scan_tx = bounded(1).0; + } + + }, + scan_path = scan_path.next() => { + if let Some(path_to_scan) = scan_path { + handler.handle_scan_path(path_to_scan).await; + } else { + // If we're the last worker to notice that this is not producing values, close the upstream. + confirm_contents_will_match_tx = bounded(1).0; + } + + } + complete => { + break + }, + + } + } + } +} + +struct RequestHandler<'worker> { + query: &'worker SearchQuery, + fs: Option<&'worker dyn Fs>, + open_entries: &'worker HashSet, + matched_buffer_count: &'worker AtomicUsize, + matches_count: &'worker AtomicUsize, + + confirm_contents_will_match_tx: &'worker Sender, + get_buffer_for_full_scan_tx: &'worker Sender, + publish_matches: &'worker Sender, +} + +struct LimitReached; + +impl RequestHandler<'_> { + async fn handle_find_all_matches( + &self, + (buffer, snapshot): (Entity, BufferSnapshot), + ) -> Option { + let ranges = self + .query + .search(&snapshot, None) + .await + .iter() + .map(|range| snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end)) + .collect::>(); + + let matched_ranges = ranges.len(); + if self.matched_buffer_count.fetch_add(1, Ordering::Release) + > Search::MAX_SEARCH_RESULT_FILES + || self + .matches_count + .fetch_add(matched_ranges, Ordering::Release) + > Search::MAX_SEARCH_RESULT_RANGES + { + _ = self.publish_matches.send(SearchResult::LimitReached).await; + Some(LimitReached) + } else { + _ = self + .publish_matches + .send(SearchResult::Buffer { buffer, ranges }) + .await; + None + } + } + async fn handle_find_first_match(&self, mut entry: MatchingEntry) { + _=maybe!(async move { + let abs_path = entry.worktree_root.join(entry.path.path.as_std_path()); + let Some(file) = self.fs.context("Trying to query filesystem in remote project search")?.open_sync(&abs_path).await.log_err() else { + return anyhow::Ok(()); + }; + + let mut file = BufReader::new(file); + let file_start = file.fill_buf()?; + + if let Err(Some(starting_position)) = + std::str::from_utf8(file_start).map_err(|e| e.error_len()) + { + // Before attempting to match the file content, throw away files that have invalid UTF-8 sequences early on; + // That way we can still match files in a streaming fashion without having look at "obviously binary" files. + log::debug!( + "Invalid UTF-8 sequence in file {abs_path:?} at byte position {starting_position}" + ); + return Ok(()); + } + + if self.query.detect(file).unwrap_or(false) { + // Yes, we should scan the whole file. + entry.should_scan_tx.send(entry.path).await?; + } + Ok(()) + }).await; + } + + async fn handle_scan_path(&self, req: InputPath) { + _ = maybe!(async move { + let InputPath { + entry, + + snapshot, + should_scan_tx, + } = req; + + if entry.is_fifo || !entry.is_file() { + return Ok(()); + } + + if self.query.filters_path() { + let matched_path = if self.query.match_full_paths() { + let mut full_path = snapshot.root_name().as_std_path().to_owned(); + full_path.push(entry.path.as_std_path()); + self.query.match_path(&full_path) + } else { + self.query.match_path(entry.path.as_std_path()) + }; + if !matched_path { + return Ok(()); + } + } + + if self.open_entries.contains(&entry.id) { + // The buffer is already in memory and that's the version we want to scan; + // hence skip the dilly-dally and look for all matches straight away. + self.get_buffer_for_full_scan_tx + .send(ProjectPath { + worktree_id: snapshot.id(), + path: entry.path.clone(), + }) + .await?; + } else { + self.confirm_contents_will_match_tx + .send(MatchingEntry { + should_scan_tx: should_scan_tx, + worktree_root: snapshot.abs_path().clone(), + path: ProjectPath { + worktree_id: snapshot.id(), + path: entry.path.clone(), + }, + }) + .await?; + } + + anyhow::Ok(()) + }) + .await; + } +} + +struct InputPath { + entry: Entry, + snapshot: Snapshot, + should_scan_tx: oneshot::Sender, +} + +struct MatchingEntry { + worktree_root: Arc, + path: ProjectPath, + should_scan_tx: oneshot::Sender, +} diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 8a78c3e0082b28..6671106bffbc44 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -3878,7 +3878,14 @@ async fn test_rename_file_to_new_directory(cx: &mut gpui::TestAppContext) { assert_eq!( worktree .update(cx, |worktree, cx| { - worktree.load_file(rel_path("dir1/dir2/dir3/test.txt"), cx) + worktree.load_file( + rel_path("dir1/dir2/dir3/test.txt"), + None, + false, + true, + None, + cx, + ) }) .await .unwrap() @@ -3925,7 +3932,7 @@ async fn test_rename_file_to_new_directory(cx: &mut gpui::TestAppContext) { assert_eq!( worktree .update(cx, |worktree, cx| { - worktree.load_file(rel_path("dir1/dir2/test.txt"), cx) + worktree.load_file(rel_path("dir1/dir2/test.txt"), None, false, true, None, cx) }) .await .unwrap() @@ -9003,7 +9010,14 @@ async fn test_ignored_dirs_events(cx: &mut gpui::TestAppContext) { let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap()); tree.flush_fs_events(cx).await; tree.update(cx, |tree, cx| { - tree.load_file(rel_path("project/target/debug/important_text.txt"), cx) + tree.load_file( + rel_path("project/target/debug/important_text.txt"), + None, + false, + true, + None, + cx, + ) }) .await .unwrap(); @@ -9164,7 +9178,14 @@ async fn test_odd_events_for_ignored_dirs( let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap()); tree.update(cx, |tree, cx| { - tree.load_file(rel_path("target/debug/foo.txt"), cx) + tree.load_file( + rel_path("target/debug/foo.txt"), + None, + false, + true, + None, + cx, + ) }) .await .unwrap(); diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index cc9cf0225a13a7..9859b27daee0a1 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -506,16 +506,7 @@ impl HeadlessProject { let (buffer_store, buffer) = this.update(&mut cx, |this, cx| { let buffer_store = this.buffer_store.clone(); let buffer = this.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.open_buffer( - ProjectPath { - worktree_id, - path: Arc::::from_proto(message.payload.path), - }, - None, - false, - true, - cx, - ) + buffer_store.open_buffer(ProjectPath { worktree_id, path }, None, false, true, cx) }); anyhow::Ok((buffer_store, buffer)) })??; diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index aadf2c2376f360..fffb6ae2d560af 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -6,7 +6,6 @@ use agent::{AgentTool, ReadFileTool, ReadFileToolInput, ToolCallEventStream}; use client::{Client, UserStore}; use clock::FakeSystemClock; use collections::{HashMap, HashSet}; -use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModel}; use encodings::Encoding; use extension::ExtensionHostProxy; @@ -17,6 +16,7 @@ use language::{ Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding, Rope, language_settings::{AllLanguageSettings, language_settings}, }; +use language_model::LanguageModelToolResultContent; use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind, LanguageServerName}; use node_runtime::NodeRuntime; use project::{ diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index a36a81f6d95ce5..fa40192cb093f1 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -706,7 +706,7 @@ impl Worktree { pub fn load_file( &self, - path: &Path, + path: &RelPath, encoding: Option, force: bool, detect_utf16: bool, @@ -1326,7 +1326,7 @@ impl LocalWorktree { fn load_file( &self, - path: &Path, + path: &RelPath, encoding: Option, force: bool, detect_utf16: bool, @@ -3189,7 +3189,7 @@ impl language::LocalFile for File { let abs_path = worktree.absolutize(&self.path); let fs = worktree.fs.clone(); cx.background_spawn(async move { - fs.load_with_encoding(&abs_path?, encoding, force, detect_utf16, buffer_encoding) + fs.load_with_encoding(&abs_path, encoding, force, detect_utf16, buffer_encoding) .await }) } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 69ee98f7bdf3ba..7f873814c0079f 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -468,7 +468,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) { let loaded = tree .update(cx, |tree, cx| { tree.load_file( - "one/node_modules/b/b1.js".as_ref(), + rel_path("one/node_modules/b/b1.js"), None, false, false, @@ -515,7 +515,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) { let loaded = tree .update(cx, |tree, cx| { tree.load_file( - "one/node_modules/a/a2.js".as_ref(), + rel_path("one/node_modules/a/a2.js"), None, false, false, @@ -1966,101 +1966,6 @@ fn random_filename(rng: &mut impl Rng) -> String { .collect() } -#[gpui::test] -async fn test_rename_file_to_new_directory(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - let expected_contents = "content"; - fs.as_fake() - .insert_tree( - "/root", - json!({ - "test.txt": expected_contents - }), - ) - .await; - let worktree = Worktree::local( - Path::new("/root"), - true, - fs.clone(), - Arc::default(), - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete()) - .await; - - let entry_id = worktree.read_with(cx, |worktree, _| { - worktree.entry_for_path("test.txt").unwrap().id - }); - let _result = worktree - .update(cx, |worktree, cx| { - worktree.rename_entry(entry_id, Path::new("dir1/dir2/dir3/test.txt"), cx) - }) - .await - .unwrap(); - worktree.read_with(cx, |worktree, _| { - assert!( - worktree.entry_for_path("test.txt").is_none(), - "Old file should have been removed" - ); - assert!( - worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_some(), - "Whole directory hierarchy and the new file should have been created" - ); - }); - assert_eq!( - worktree - .update(cx, |worktree, cx| { - worktree.load_file("dir1/dir2/dir3/test.txt".as_ref(), None, cx) - }) - .await - .unwrap() - .text, - expected_contents, - "Moved file's contents should be preserved" - ); - - let entry_id = worktree.read_with(cx, |worktree, _| { - worktree - .entry_for_path("dir1/dir2/dir3/test.txt") - .unwrap() - .id - }); - let _result = worktree - .update(cx, |worktree, cx| { - worktree.rename_entry(entry_id, Path::new("dir1/dir2/test.txt"), cx) - }) - .await - .unwrap(); - worktree.read_with(cx, |worktree, _| { - assert!( - worktree.entry_for_path("test.txt").is_none(), - "First file should not reappear" - ); - assert!( - worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_none(), - "Old file should have been removed" - ); - assert!( - worktree.entry_for_path("dir1/dir2/test.txt").is_some(), - "No error should have occurred after moving into existing directory" - ); - }); - assert_eq!( - worktree - .update(cx, |worktree, cx| { - worktree.load_file("dir1/dir2/test.txt".as_ref(), None, cx) - }) - .await - .unwrap() - .text, - expected_contents, - "Moved file's contents should be preserved" - ); -} - #[gpui::test] async fn test_private_single_file_worktree(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 139f1d63ae792a..73681693e91c59 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -52,7 +52,6 @@ debugger_ui.workspace = true diagnostics.workspace = true editor.workspace = true zeta2_tools.workspace = true -edit_prediction_tools.workspace = true encodings.workspace = true encodings_ui.workspace = true env_logger.workspace = true diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index bc6c25105e69eb..790c3d3ba2b567 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -653,6 +653,7 @@ mod tests { ipc::{self}, }; use editor::Editor; + use encodings::Encoding; use gpui::TestAppContext; use language::LineEnding; use remote::SshConnectionOptions; @@ -863,6 +864,7 @@ mod tests { Path::new(file1_path), &Rope::from_str("content1", cx.background_executor()), LineEnding::Unix, + Encoding::default(), ) .await .unwrap(); @@ -877,6 +879,7 @@ mod tests { Path::new(file2_path), &Rope::from_str("content2", cx.background_executor()), LineEnding::Unix, + Encoding::default(), ) .await .unwrap(); diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 02eb5dcae0ac29..f558c0e782b6f7 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -1986,7 +1986,7 @@ mod tests { .worktree_for_root_name("closed_source_worktree", cx) .unwrap(); worktree2.update(cx, |worktree2, cx| { - worktree2.load_file(Path::new("main.rs"), None, false, true, None, cx) + worktree2.load_file(rel_path("main.rs"), None, false, true, None, cx) }) }) .await From 19b06e508938546bc9cee26f2e717c9ab3b2720d Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Wed, 22 Oct 2025 12:40:42 +0530 Subject: [PATCH 39/43] Fix conflicts --- crates/fs/src/fs.rs | 2 +- crates/project/src/buffer_store.rs | 4 +++- crates/project/src/lsp_store.rs | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 00dd220fbaa16f..2380049da70849 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -7,8 +7,8 @@ pub mod fs_watcher; use anyhow::{Context as _, Result, anyhow}; #[cfg(any(target_os = "linux", target_os = "freebsd"))] use ashpd::desktop::trash; -use futures::stream::iter; use encodings::Encoding; +use futures::stream::iter; use gpui::App; use gpui::BackgroundExecutor; use gpui::Global; diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 21ad263511d078..bd82f3949b9404 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -8,6 +8,8 @@ use anyhow::{Context as _, Result, anyhow}; use client::Client; use collections::{HashMap, HashSet, hash_map}; use encodings::Encoding; +use fs::Fs; +use futures::StreamExt; use futures::{Future, FutureExt as _, channel::oneshot, future::Shared}; use gpui::{ App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, @@ -1189,7 +1191,7 @@ impl BufferStore { let buffers = this.update(cx, |this, cx| { project_paths .into_iter() - .map(|project_path| this.open_buffer(project_path, None, cx)) + .map(|project_path| this.open_buffer(project_path, None, false, true, cx)) .collect::>() })?; for buffer_task in buffers { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 4c70c999b7bfed..446251678365f9 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -8336,7 +8336,7 @@ impl LspStore { lsp_store .update(cx, |lsp_store, cx| { lsp_store.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(project_path, None, false, true,cx) + buffer_store.open_buffer(project_path, None, false, true, cx) }) })? .await From 08032bd1ffdc5d4c52625b7ce2e606f64ececc1c Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Fri, 24 Oct 2025 19:58:38 +0530 Subject: [PATCH 40/43] Move the invalid encoding UI from `project` to `workspace` --- crates/fs/src/fs.rs | 1 - crates/project/src/invalid_item_view.rs | 73 ++++---------------- crates/workspace/src/invalid_item_view.rs | 81 +++++++++++++++++++---- 3 files changed, 81 insertions(+), 74 deletions(-) diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 2380049da70849..19b3ac850efb59 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -7,7 +7,6 @@ pub mod fs_watcher; use anyhow::{Context as _, Result, anyhow}; #[cfg(any(target_os = "linux", target_os = "freebsd"))] use ashpd::desktop::trash; -use encodings::Encoding; use futures::stream::iter; use gpui::App; use gpui::BackgroundExecutor; diff --git a/crates/project/src/invalid_item_view.rs b/crates/project/src/invalid_item_view.rs index 208cf05c48d27b..c5dfd6436f8a0a 100644 --- a/crates/project/src/invalid_item_view.rs +++ b/crates/project/src/invalid_item_view.rs @@ -78,8 +78,6 @@ impl Focusable for InvalidItemView { impl Render for InvalidItemView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { let abs_path = self.abs_path.clone(); - let path0 = self.abs_path.clone(); - let path1 = self.abs_path.clone(); v_flex() .size_full() @@ -103,63 +101,20 @@ impl Render for InvalidItemView { ), ) .when(self.is_local, |contents| { - contents - .child( - h_flex().justify_center().child( - Button::new("open-with-system", "Open in Default App") - .on_click(move |_, _, cx| { - cx.open_with_system(&abs_path); - }) - .style(ButtonStyle::Outlined) - .key_binding(KeyBinding::for_action( - &OpenWithSystem, - window, - cx, - )), - ), - ) - .child( - h_flex() - .justify_center() - .child( - Button::new( - "open-with-encoding", - "Open With a Different Encoding", - ) - .style(ButtonStyle::Outlined) - .on_click( - move |_, window, cx| { - window.dispatch_action( - Box::new( - zed_actions::encodings_ui::Toggle( - path0.clone(), - ), - ), - cx, - ) - }, - ), - ) - .child( - Button::new( - "accept-risk-and-open", - "Accept the Risk and Open", - ) - .style(ButtonStyle::Tinted(TintColor::Warning)) - .on_click( - move |_, window, cx| { - window.dispatch_action( - Box::new( - zed_actions::encodings_ui::ForceOpen( - path1.clone(), - ), - ), - cx, - ); - }, - ), - ), - ) + contents.child( + h_flex().justify_center().child( + Button::new("open-with-system", "Open in Default App") + .on_click(move |_, _, cx| { + cx.open_with_system(&abs_path); + }) + .style(ButtonStyle::Outlined) + .key_binding(KeyBinding::for_action( + &OpenWithSystem, + window, + cx, + )), + ), + ) }), ), ) diff --git a/crates/workspace/src/invalid_item_view.rs b/crates/workspace/src/invalid_item_view.rs index eb6c8f3299838c..75dcf768a16c2d 100644 --- a/crates/workspace/src/invalid_item_view.rs +++ b/crates/workspace/src/invalid_item_view.rs @@ -1,6 +1,7 @@ use std::{path::Path, sync::Arc}; +use ui::TintColor; -use gpui::{EventEmitter, FocusHandle, Focusable}; +use gpui::{EventEmitter, FocusHandle, Focusable, div}; use ui::{ App, Button, ButtonCommon, ButtonStyle, Clickable, Context, FluentBuilder, InteractiveElement, KeyBinding, Label, LabelCommon, LabelSize, ParentElement, Render, SharedString, Styled as _, @@ -77,6 +78,9 @@ impl Focusable for InvalidItemView { impl Render for InvalidItemView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { let abs_path = self.abs_path.clone(); + let path0 = self.abs_path.clone(); + let path1 = self.abs_path.clone(); + v_flex() .size_full() .track_focus(&self.focus_handle(cx)) @@ -91,21 +95,70 @@ impl Render for InvalidItemView { .gap_2() .child(h_flex().justify_center().child("Could not open file")) .child( - h_flex() - .justify_center() - .child(Label::new(self.error.clone()).size(LabelSize::Small)), + h_flex().justify_center().child( + div() + .whitespace_normal() + .text_center() + .child(Label::new(self.error.clone()).size(LabelSize::Small)), + ), ) .when(self.is_local, |contents| { - contents.child( - h_flex().justify_center().child( - Button::new("open-with-system", "Open in Default App") - .on_click(move |_, _, cx| { - cx.open_with_system(&abs_path); - }) - .style(ButtonStyle::Outlined) - .key_binding(KeyBinding::for_action(&OpenWithSystem, cx)), - ), - ) + contents + .child( + h_flex().justify_center().child( + Button::new("open-with-system", "Open in Default App") + .on_click(move |_, _, cx| { + cx.open_with_system(&abs_path); + }) + .style(ButtonStyle::Outlined) + .key_binding(KeyBinding::for_action( + &OpenWithSystem, + cx, + )), + ), + ) + .child( + h_flex() + .justify_center() + .child( + Button::new( + "open-with-encoding", + "Open With a Different Encoding", + ) + .style(ButtonStyle::Outlined) + .on_click( + move |_, window, cx| { + window.dispatch_action( + Box::new( + zed_actions::encodings_ui::Toggle( + path0.clone(), + ), + ), + cx, + ) + }, + ), + ) + .child( + Button::new( + "accept-risk-and-open", + "Accept the Risk and Open", + ) + .style(ButtonStyle::Tinted(TintColor::Warning)) + .on_click( + move |_, window, cx| { + window.dispatch_action( + Box::new( + zed_actions::encodings_ui::ForceOpen( + path1.clone(), + ), + ), + cx, + ); + }, + ), + ), + ) }), ), ) From 0b942fe8a7292032940240f2bb2780549586e2c8 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Tue, 28 Oct 2025 20:54:12 +0530 Subject: [PATCH 41/43] - Use `EncodingOptions` for parameters - Implement `From` for `Encoding` and `Clone` for `EncodingOptions` --- crates/agent_ui/src/acp/message_editor.rs | 2 +- crates/agent_ui/src/context.rs | 2 +- crates/copilot/src/copilot.rs | 6 +-- .../src/syntax_index.rs | 2 +- crates/encodings/src/lib.rs | 34 ++++++++++++-- crates/fs/src/fs.rs | 17 ++----- crates/language/src/buffer.rs | 14 +++--- crates/languages/src/json.rs | 2 +- crates/project/src/buffer_store.rs | 40 +++++----------- .../project/src/debugger/breakpoint_store.rs | 2 +- crates/project/src/lsp_store.rs | 2 +- .../src/lsp_store/rust_analyzer_ext.rs | 6 +-- crates/project/src/project.rs | 10 +--- crates/project/src/project_tests.rs | 19 ++++---- crates/remote_server/src/headless_project.rs | 6 +-- crates/worktree/src/worktree.rs | 47 +++++++++---------- crates/worktree/src/worktree_tests.rs | 8 +--- crates/zeta/src/zeta.rs | 2 +- 18 files changed, 97 insertions(+), 124 deletions(-) diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index a2a701659eaae6..0198fca47fe063 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1248,7 +1248,7 @@ fn full_mention_for_directory( worktree_id, path: worktree_path, }; - buffer_store.open_buffer(project_path, None, false, true, cx) + buffer_store.open_buffer(project_path, &Default::default(), cx) }) }); diff --git a/crates/agent_ui/src/context.rs b/crates/agent_ui/src/context.rs index db98461bea213b..fbe76961967aca 100644 --- a/crates/agent_ui/src/context.rs +++ b/crates/agent_ui/src/context.rs @@ -287,7 +287,7 @@ impl DirectoryContextHandle { let open_task = project.update(cx, |project, cx| { project.buffer_store().update(cx, |buffer_store, cx| { let project_path = ProjectPath { worktree_id, path }; - buffer_store.open_buffer(project_path, None, false, true, cx) + buffer_store.open_buffer(project_path, &Default::default(), cx) }) }); diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 28828db43170b9..c77b49dea38317 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1241,7 +1241,7 @@ async fn get_copilot_lsp(fs: Arc, node_runtime: NodeRuntime) -> anyhow:: #[cfg(test)] mod tests { use super::*; - use encodings::Encoding; + use encodings::{Encoding, EncodingOptions}; use gpui::TestAppContext; use util::{path, paths::PathStyle, rel_path::rel_path}; @@ -1455,9 +1455,7 @@ mod tests { fn load( &self, _: &App, - _: Encoding, - _: bool, - _: bool, + _: &EncodingOptions, _: Option>, ) -> Task> { unimplemented!() diff --git a/crates/edit_prediction_context/src/syntax_index.rs b/crates/edit_prediction_context/src/syntax_index.rs index 9b2b884e2f1d82..dc2a465eee13fa 100644 --- a/crates/edit_prediction_context/src/syntax_index.rs +++ b/crates/edit_prediction_context/src/syntax_index.rs @@ -523,7 +523,7 @@ impl SyntaxIndex { }; let snapshot_task = worktree.update(cx, |worktree, cx| { - let load_task = worktree.load_file(&project_path.path, None, false, true, None, cx); + let load_task = worktree.load_file(&project_path.path, &Default::default(), None, cx); let worktree_abs_path = worktree.abs_path(); cx.spawn(async move |_this, cx| { diff --git a/crates/encodings/src/lib.rs b/crates/encodings/src/lib.rs index f5f62394ee7182..6158084e87464e 100644 --- a/crates/encodings/src/lib.rs +++ b/crates/encodings/src/lib.rs @@ -34,6 +34,12 @@ impl Default for Encoding { } } +impl From<&'static encoding_rs::Encoding> for Encoding { + fn from(encoding: &'static encoding_rs::Encoding) -> Self { + Encoding::new(encoding) + } +} + unsafe impl Send for Encoding {} unsafe impl Sync for Encoding {} @@ -120,13 +126,19 @@ impl Encoding { /// Convert a byte vector from a specified encoding to a UTF-8 string. pub async fn to_utf8( input: Vec, - encoding: Encoding, - force: bool, - detect_utf16: bool, + options: &EncodingOptions, buffer_encoding: Option>, ) -> anyhow::Result { - encoding - .decode(input, force, detect_utf16, buffer_encoding) + options + .encoding + .decode( + input, + options.force.load(std::sync::atomic::Ordering::Acquire), + options + .detect_utf16 + .load(std::sync::atomic::Ordering::Acquire), + buffer_encoding, + ) .await } @@ -162,3 +174,15 @@ impl Default for EncodingOptions { } } } + +impl Clone for EncodingOptions { + fn clone(&self) -> Self { + EncodingOptions { + encoding: Arc::new(self.encoding.get().into()), + force: AtomicBool::new(self.force.load(std::sync::atomic::Ordering::Acquire)), + detect_utf16: AtomicBool::new( + self.detect_utf16.load(std::sync::atomic::Ordering::Acquire), + ), + } + } +} diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 19b3ac850efb59..59719e47af4a44 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -58,11 +58,9 @@ use smol::io::AsyncReadExt; #[cfg(any(test, feature = "test-support"))] use std::ffi::OsStr; +use encodings::{Encoding, EncodingOptions, from_utf8, to_utf8}; #[cfg(any(test, feature = "test-support"))] pub use fake_git_repo::{LOAD_HEAD_TEXT_TASK, LOAD_INDEX_TEXT_TASK}; -use encodings::Encoding; -use encodings::from_utf8; -use encodings::to_utf8; pub trait Watcher: Send + Sync { fn add(&self, path: &Path) -> Result<()>; @@ -122,19 +120,10 @@ pub trait Fs: Send + Sync { async fn load_with_encoding( &self, path: &Path, - encoding: Encoding, - force: bool, - detect_utf16: bool, + options: &EncodingOptions, buffer_encoding: Option>, ) -> Result { - Ok(to_utf8( - self.load_bytes(path).await?, - encoding, - force, - detect_utf16, - buffer_encoding, - ) - .await?) + Ok(to_utf8(self.load_bytes(path).await?, options, buffer_encoding).await?) } async fn load_bytes(&self, path: &Path) -> Result>; diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 1b8420b0f149a0..6df473726c28d2 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -21,7 +21,7 @@ use anyhow::{Context as _, Result}; use clock::Lamport; pub use clock::ReplicaId; use collections::HashMap; -use encodings::Encoding; +use encodings::{Encoding, EncodingOptions}; use fs::MTime; use futures::channel::oneshot; use gpui::{ @@ -422,9 +422,7 @@ pub trait LocalFile: File { fn load( &self, cx: &App, - encoding: Encoding, - force: bool, - detect_utf16: bool, + options: &EncodingOptions, buffer_encoding: Option>, ) -> Task>; @@ -1372,6 +1370,8 @@ impl Buffer { let encoding = (*self.encoding).clone(); let buffer_encoding = self.encoding.clone(); + let options = EncodingOptions::default(); + options.encoding.set(encoding.get()); let prev_version = self.text.version(); self.reload_task = Some(cx.spawn(async move |this, cx| { @@ -1379,7 +1379,7 @@ impl Buffer { let file = this.file.as_ref()?.as_local()?; Some((file.disk_state().mtime(), { - file.load(cx, encoding, false, true, Some(buffer_encoding)) + file.load(cx, &options, Some(buffer_encoding)) })) })? else { @@ -5271,9 +5271,7 @@ impl LocalFile for TestFile { fn load( &self, _cx: &App, - _encoding: Encoding, - _force: bool, - _detect_utf16: bool, + _options: &EncodingOptions, _buffer_encoding: Option>, ) -> Task> { unimplemented!() diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 4d6894d3838e8e..30d3c34f1502c8 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -57,7 +57,7 @@ impl ContextProvider for JsonTaskProvider { let contents = file .worktree .update(cx, |this, cx| { - this.load_file(&file.path, None, false, true, None, cx) + this.load_file(&file.path, &Default::default(), None, cx) }) .ok()? .await diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index bd82f3949b9404..ed74e555467736 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -7,7 +7,7 @@ use crate::{ use anyhow::{Context as _, Result, anyhow}; use client::Client; use collections::{HashMap, HashSet, hash_map}; -use encodings::Encoding; +use encodings::EncodingOptions; use fs::Fs; use futures::StreamExt; use futures::{Future, FutureExt as _, channel::oneshot, future::Shared}; @@ -629,23 +629,17 @@ impl LocalBufferStore { &self, path: Arc, worktree: Entity, - encoding: Option, - force: bool, - detect_utf16: bool, + options: &EncodingOptions, cx: &mut Context, ) -> Task>> { + let options = options.clone(); + let encoding = options.encoding.clone(); + let load_buffer = worktree.update(cx, |worktree, cx| { let reservation = cx.reserve_entity(); let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64()); - let load_file_task = worktree.load_file( - path.as_ref(), - encoding.clone(), - force, - detect_utf16, - None, - cx, - ); + let load_file_task = worktree.load_file(path.as_ref(), &options, None, cx); cx.spawn(async move |_, cx| { let loaded_file = load_file_task.await?; @@ -682,11 +676,7 @@ impl LocalBufferStore { entry_id: None, is_local: true, is_private: false, - encoding: Some(Arc::new(if let Some(encoding) = &encoding { - encoding.clone() - } else { - Encoding::default() - })), + encoding: Some(encoding.clone()), })), Capability::ReadWrite, ) @@ -714,11 +704,7 @@ impl LocalBufferStore { anyhow::Ok(()) })??; - buffer.update(cx, |buffer, _| { - buffer - .encoding - .set(encoding.unwrap_or(Encoding::default()).get()) - })?; + buffer.update(cx, |buffer, _| buffer.encoding.set(encoding.get()))?; Ok(buffer) }) @@ -850,9 +836,7 @@ impl BufferStore { pub fn open_buffer( &mut self, project_path: ProjectPath, - encoding: Option, - force: bool, - detect_utf16: bool, + options: &EncodingOptions, cx: &mut Context, ) -> Task>> { if let Some(buffer) = self.get_by_path(&project_path) { @@ -876,9 +860,7 @@ impl BufferStore { return Task::ready(Err(anyhow!("no such worktree"))); }; let load_buffer = match &self.state { - BufferStoreState::Local(this) => { - this.open_buffer(path, worktree, encoding, force, detect_utf16, cx) - } + BufferStoreState::Local(this) => this.open_buffer(path, worktree, options, cx), BufferStoreState::Remote(this) => this.open_buffer(path, worktree, cx), }; @@ -1191,7 +1173,7 @@ impl BufferStore { let buffers = this.update(cx, |this, cx| { project_paths .into_iter() - .map(|project_path| this.open_buffer(project_path, None, false, true, cx)) + .map(|project_path| this.open_buffer(project_path, &Default::default(), cx)) .collect::>() })?; for buffer_task in buffers { diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index aad1edab307124..fe29bebceae51c 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -796,7 +796,7 @@ impl BreakpointStore { worktree_id: worktree.read(cx).id(), path: relative_path, }; - this.open_buffer(path, None, false, true, cx) + this.open_buffer(path, &Default::default(), cx) })? .await; let Ok(buffer) = buffer else { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 446251678365f9..f803d0bc82532f 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -8336,7 +8336,7 @@ impl LspStore { lsp_store .update(cx, |lsp_store, cx| { lsp_store.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(project_path, None, false, true, cx) + buffer_store.open_buffer(project_path, &Default::default(), cx) }) })? .await diff --git a/crates/project/src/lsp_store/rust_analyzer_ext.rs b/crates/project/src/lsp_store/rust_analyzer_ext.rs index cdfd9e63cfac36..ef74b9e26f072a 100644 --- a/crates/project/src/lsp_store/rust_analyzer_ext.rs +++ b/crates/project/src/lsp_store/rust_analyzer_ext.rs @@ -91,7 +91,7 @@ pub fn cancel_flycheck( let buffer = buffer_path.map(|buffer_path| { project.update(cx, |project, cx| { project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, None, false, true, cx) + buffer_store.open_buffer(buffer_path, &Default::default(), cx) }) }) }); @@ -140,7 +140,7 @@ pub fn run_flycheck( let buffer = buffer_path.map(|buffer_path| { project.update(cx, |project, cx| { project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, None, false, true, cx) + buffer_store.open_buffer(buffer_path, &Default::default(), cx) }) }) }); @@ -198,7 +198,7 @@ pub fn clear_flycheck( let buffer = buffer_path.map(|buffer_path| { project.update(cx, |project, cx| { project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, None, false, true, cx) + buffer_store.open_buffer(buffer_path, &Default::default(), cx) }) }) }); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 2650ca78e534c5..0d481f487ba8f3 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2720,13 +2720,7 @@ impl Project { } self.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.open_buffer( - path.into(), - Some((*self.encoding_options.encoding).clone()), - *self.encoding_options.force.get_mut(), - *self.encoding_options.detect_utf16.get_mut(), - cx, - ) + buffer_store.open_buffer(path.into(), &self.encoding_options, cx) }) } @@ -5409,7 +5403,7 @@ impl Project { cx.spawn(async move |cx| { let file = worktree .update(cx, |worktree, cx| { - worktree.load_file(&rel_path, None, false, true, None, cx) + worktree.load_file(&rel_path, &Default::default(), None, cx) })? .await .context("Failed to load settings file")?; diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 6671106bffbc44..cb5b17fcd0efb4 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -3880,9 +3880,7 @@ async fn test_rename_file_to_new_directory(cx: &mut gpui::TestAppContext) { .update(cx, |worktree, cx| { worktree.load_file( rel_path("dir1/dir2/dir3/test.txt"), - None, - false, - true, + &Default::default(), None, cx, ) @@ -3932,7 +3930,12 @@ async fn test_rename_file_to_new_directory(cx: &mut gpui::TestAppContext) { assert_eq!( worktree .update(cx, |worktree, cx| { - worktree.load_file(rel_path("dir1/dir2/test.txt"), None, false, true, None, cx) + worktree.load_file( + rel_path("dir1/dir2/test.txt"), + &Default::default(), + None, + cx, + ) }) .await .unwrap() @@ -9012,9 +9015,7 @@ async fn test_ignored_dirs_events(cx: &mut gpui::TestAppContext) { tree.update(cx, |tree, cx| { tree.load_file( rel_path("project/target/debug/important_text.txt"), - None, - false, - true, + &Default::default(), None, cx, ) @@ -9180,9 +9181,7 @@ async fn test_odd_events_for_ignored_dirs( tree.update(cx, |tree, cx| { tree.load_file( rel_path("target/debug/foo.txt"), - None, - false, - true, + &Default::default(), None, cx, ) diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 9859b27daee0a1..e6e81ca577865b 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -506,7 +506,7 @@ impl HeadlessProject { let (buffer_store, buffer) = this.update(&mut cx, |this, cx| { let buffer_store = this.buffer_store.clone(); let buffer = this.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.open_buffer(ProjectPath { worktree_id, path }, None, false, true, cx) + buffer_store.open_buffer(ProjectPath { worktree_id, path }, &Default::default(), cx) }); anyhow::Ok((buffer_store, buffer)) })??; @@ -597,9 +597,7 @@ impl HeadlessProject { worktree_id: worktree.read(cx).id(), path: path, }, - None, - false, - true, + &Default::default(), cx, ) }); diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index fa40192cb093f1..0054af86f0ac41 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -7,7 +7,7 @@ use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{Context as _, Result, anyhow}; use clock::ReplicaId; use collections::{HashMap, HashSet, VecDeque}; -use encodings::Encoding; +use encodings::{Encoding, EncodingOptions}; use fs::{Fs, MTime, PathEvent, RemoveOptions, Watcher, copy_recursive, read_dir_items}; use futures::{ FutureExt as _, Stream, StreamExt, @@ -707,16 +707,12 @@ impl Worktree { pub fn load_file( &self, path: &RelPath, - encoding: Option, - force: bool, - detect_utf16: bool, + options: &EncodingOptions, buffer_encoding: Option>, cx: &Context, ) -> Task> { match self { - Worktree::Local(this) => { - this.load_file(path, encoding, force, detect_utf16, buffer_encoding, cx) - } + Worktree::Local(this) => this.load_file(path, options, buffer_encoding, cx), Worktree::Remote(_) => { Task::ready(Err(anyhow!("remote worktrees can't yet load files"))) } @@ -1327,9 +1323,7 @@ impl LocalWorktree { fn load_file( &self, path: &RelPath, - encoding: Option, - force: bool, - detect_utf16: bool, + options: &EncodingOptions, buffer_encoding: Option>, cx: &Context, ) -> Task> { @@ -1338,6 +1332,8 @@ impl LocalWorktree { let fs = self.fs.clone(); let entry = self.refresh_entry(path.clone(), None, cx); let is_private = self.is_path_private(path.as_ref()); + let options = options.clone(); + let encoding = options.encoding.clone(); let this = cx.weak_entity(); cx.background_spawn(async move { @@ -1356,17 +1352,7 @@ impl LocalWorktree { } } let text = fs - .load_with_encoding( - &abs_path, - if let Some(ref encoding) = encoding { - Encoding::new(encoding.get()) - } else { - Encoding::new(encodings::UTF_8) - }, - force, - detect_utf16, - buffer_encoding.clone(), - ) + .load_with_encoding(&abs_path, &options, buffer_encoding.clone()) .await?; let worktree = this.upgrade().context("worktree was dropped")?; @@ -1391,7 +1377,7 @@ impl LocalWorktree { }, is_local: true, is_private, - encoding: encoding.map(|e| Arc::new(Encoding::new(e.get()))), + encoding: Some(encoding), }) } }; @@ -3180,16 +3166,25 @@ impl language::LocalFile for File { fn load( &self, cx: &App, - encoding: Encoding, - force: bool, - detect_utf16: bool, + options: &EncodingOptions, buffer_encoding: Option>, ) -> Task> { let worktree = self.worktree.read(cx).as_local().unwrap(); let abs_path = worktree.absolutize(&self.path); let fs = worktree.fs.clone(); + let options = EncodingOptions { + encoding: options.encoding.clone(), + force: std::sync::atomic::AtomicBool::new( + options.force.load(std::sync::atomic::Ordering::Acquire), + ), + detect_utf16: std::sync::atomic::AtomicBool::new( + options + .detect_utf16 + .load(std::sync::atomic::Ordering::Acquire), + ), + }; cx.background_spawn(async move { - fs.load_with_encoding(&abs_path, encoding, force, detect_utf16, buffer_encoding) + fs.load_with_encoding(&abs_path, &options, buffer_encoding) .await }) } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 7f873814c0079f..cf049793ec9b47 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -469,9 +469,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) { .update(cx, |tree, cx| { tree.load_file( rel_path("one/node_modules/b/b1.js"), - None, - false, - false, + &Default::default(), None, cx, ) @@ -516,9 +514,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) { .update(cx, |tree, cx| { tree.load_file( rel_path("one/node_modules/a/a2.js"), - None, - false, - false, + &Default::default(), None, cx, ) diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index f558c0e782b6f7..d95e62be5e4ceb 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -1986,7 +1986,7 @@ mod tests { .worktree_for_root_name("closed_source_worktree", cx) .unwrap(); worktree2.update(cx, |worktree2, cx| { - worktree2.load_file(rel_path("main.rs"), None, false, true, None, cx) + worktree2.load_file(rel_path("main.rs"), &Default::default(), None, cx) }) }) .await From 2e18a5b6621c9065858d6246852bb8f7df98c10f Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sat, 1 Nov 2025 11:07:05 +0530 Subject: [PATCH 42/43] Use `Buffer::update` and `Buffer::update_encoding` to set the `encoding` field of `Buffer` --- crates/encodings/Cargo.toml | 2 +- crates/encodings_ui/src/selectors.rs | 16 ++++------------ crates/language/src/buffer.rs | 14 +++----------- crates/project/src/buffer_store.rs | 4 +++- crates/worktree/src/worktree.rs | 4 ++-- 5 files changed, 13 insertions(+), 27 deletions(-) diff --git a/crates/encodings/Cargo.toml b/crates/encodings/Cargo.toml index 50b932ab8649f2..0e900d576012e5 100644 --- a/crates/encodings/Cargo.toml +++ b/crates/encodings/Cargo.toml @@ -5,8 +5,8 @@ publish.workspace = true edition.workspace = true [dependencies] -encoding_rs.workspace = true anyhow.workspace = true +encoding_rs.workspace = true [lints] workspace = true diff --git a/crates/encodings_ui/src/selectors.rs b/crates/encodings_ui/src/selectors.rs index 879cadba19de10..fc5a6793a96f2a 100644 --- a/crates/encodings_ui/src/selectors.rs +++ b/crates/encodings_ui/src/selectors.rs @@ -463,16 +463,10 @@ pub mod encoding { .unwrap(); let reload = buffer.update(cx, |buffer, cx| buffer.reload(cx)); - // Since the encoding will be accessed in `reload`, - // the lock must be released before calling `reload`. - // By limiting the scope, we ensure that it is released - { - let buffer = buffer.read(cx); - - let buffer_encoding = buffer.encoding.clone(); - buffer_encoding.set(encoding_from_name(¤t_selection.clone())); - } + buffer.update(cx, |buffer, _| { + buffer.update_encoding(encoding_from_name(¤t_selection).into()) + }); self.dismissed(window, cx); @@ -560,9 +554,7 @@ pub mod encoding { }) { buffer - .read_with(cx, |buffer, _| { - buffer.encoding.set(encoding); - }) + .update(cx, |buffer, _| buffer.update_encoding(encoding.into())) .log_err(); } }) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 6df473726c28d2..a82c49ae546980 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -128,7 +128,6 @@ pub struct Buffer { change_bits: Vec>>, _subscriptions: Vec, pub encoding: Arc, - pub observe_file_encoding: Option, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -1031,7 +1030,6 @@ impl Buffer { change_bits: Default::default(), _subscriptions: Vec::new(), encoding: Arc::new(Encoding::new(encodings::UTF_8)), - observe_file_encoding: None, } } @@ -2937,15 +2935,9 @@ impl Buffer { !self.has_edits_since(&self.preview_version) } - /// Update the `encoding` field, whenever the `encoding` field of the file changes - pub fn update_encoding(&mut self) { - if let Some(file) = self.file() { - if let Some(encoding) = file.encoding() { - self.encoding.set(encoding.get()); - } else { - self.encoding.set(encodings::UTF_8); - }; - } + /// Update the buffer + pub fn update_encoding(&mut self, encoding: Encoding) { + self.encoding.set(encoding.get()); } } diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index ed74e555467736..92159a4596a153 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -704,7 +704,9 @@ impl LocalBufferStore { anyhow::Ok(()) })??; - buffer.update(cx, |buffer, _| buffer.encoding.set(encoding.get()))?; + buffer.update(cx, |buffer, _| { + buffer.update_encoding(encoding.get().into()) + })?; Ok(buffer) }) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 0054af86f0ac41..f8366dacb4dfd1 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3085,7 +3085,7 @@ impl PartialEq for File { && self.entry_id == other.entry_id && self.is_local == other.is_local && self.is_private == other.is_private - && if let Some(encoding) = &self.encoding + && (if let Some(encoding) = &self.encoding && let Some(other_encoding) = &other.encoding { if encoding.get() != other_encoding.get() { @@ -3095,7 +3095,7 @@ impl PartialEq for File { } } else { true - } + }) { true } else { From 4330e5ff1439be5c0b2a88203b456379526abf84 Mon Sep 17 00:00:00 2001 From: R Aadarsh Date: Sat, 1 Nov 2025 14:53:06 +0530 Subject: [PATCH 43/43] - Change the order in which `cx` and `encoding` appear - Add a licence symlink to `encodings` --- crates/encodings/LICENSE-GPL | 1 + crates/encodings_ui/src/lib.rs | 9 ++++----- crates/project/src/buffer_store.rs | 12 +++++++++--- crates/project/src/project.rs | 4 ---- crates/remote_server/src/remote_editing_tests.rs | 2 ++ crates/workspace/src/workspace.rs | 4 +++- crates/worktree/src/worktree.rs | 2 +- crates/worktree/src/worktree_tests.rs | 4 ++-- 8 files changed, 22 insertions(+), 16 deletions(-) create mode 120000 crates/encodings/LICENSE-GPL diff --git a/crates/encodings/LICENSE-GPL b/crates/encodings/LICENSE-GPL new file mode 120000 index 00000000000000..89e542f750cd38 --- /dev/null +++ b/crates/encodings/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/encodings_ui/src/lib.rs b/crates/encodings_ui/src/lib.rs index bb8d2d38583bc9..1cef23b7010e3f 100644 --- a/crates/encodings_ui/src/lib.rs +++ b/crates/encodings_ui/src/lib.rs @@ -322,11 +322,10 @@ pub fn init(cx: &mut App) { .detach(); }); - { - let force = workspace.encoding_options.force.get_mut(); - - *force = true; - } + workspace + .encoding_options + .force + .store(true, std::sync::atomic::Ordering::Release); let open_task = workspace.open_abs_path(path, OpenOptions::default(), window, cx); let weak_workspace = workspace.weak_handle(); diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 92159a4596a153..a27f7a0759f512 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -399,7 +399,7 @@ impl LocalBufferStore { } let save = worktree.update(cx, |worktree, cx| { - worktree.write_file(path.clone(), text, line_ending, cx, (*encoding).clone()) + worktree.write_file(path.clone(), text, line_ending, (*encoding).clone(), cx) }); cx.spawn(async move |this, cx| { @@ -643,10 +643,16 @@ impl LocalBufferStore { cx.spawn(async move |_, cx| { let loaded_file = load_file_task.await?; + let background_executor = cx.background_executor().clone(); let buffer = cx.insert_entity(reservation, |_| { Buffer::build( - text::Buffer::new(ReplicaId::LOCAL, buffer_id, loaded_file.text), + text::Buffer::new( + ReplicaId::LOCAL, + buffer_id, + loaded_file.text, + &background_executor, + ), Some(loaded_file.file), Capability::ReadWrite, ) @@ -658,7 +664,7 @@ impl LocalBufferStore { cx.spawn(async move |this, cx| { let buffer = match load_buffer.await { - Ok(buffer) => Ok(buffer), + Ok(buffer) => buffer, Err(error) if is_not_found_error(&error) => cx.new(|cx| { let buffer_id = BufferId::from(cx.entity_id().as_non_zero_u64()); let text_buffer = text::Buffer::new( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 0d481f487ba8f3..69d201be295855 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5418,12 +5418,8 @@ impl Project { rel_path.clone(), Rope::from_str(&new_text, cx.background_executor()), line_ending, - cx, Encoding::default(), - new_text.into(), - line_ending, cx, - Encoding::default(), ) })? .await diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index fffb6ae2d560af..ae8235b6559e40 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -35,6 +35,8 @@ use std::{ use unindent::Unindent as _; use util::{path, rel_path::rel_path}; +use gpui::SharedString; + #[gpui::test] async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { let fs = FakeFs::new(server_cx.executor()); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c1508a74f8603c..8fc2432429f6b9 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -7622,7 +7622,9 @@ pub fn create_and_open_local_file( fs.create_file(path, Default::default()).await?; fs.save( path, - &default_content(), + &default_content(cx), + + Default::default(), Default::default(), ) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index f8366dacb4dfd1..b4ceceda3f6f32 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -737,8 +737,8 @@ impl Worktree { path: Arc, text: Rope, line_ending: LineEnding, - cx: &Context, encoding: Encoding, + cx: &Context, ) -> Task>> { match self { Worktree::Local(this) => this.write_file(path, text, line_ending, cx, encoding), diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index cf049793ec9b47..673764bbba9ca2 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -734,8 +734,8 @@ async fn test_write_file(cx: &mut TestAppContext) { rel_path("tracked-dir/file.txt").into(), Rope::from_str("hello", cx.background_executor()), Default::default(), - cx, Default::default(), + cx, ) }) .await @@ -746,8 +746,8 @@ async fn test_write_file(cx: &mut TestAppContext) { rel_path("ignored-dir/file.txt").into(), Rope::from_str("world", cx.background_executor()), Default::default(), - cx, Default::default(), + cx, ) }) .await