diff --git a/..gitignore.swp b/..gitignore.swp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Cargo.lock b/Cargo.lock index 42649b137f..d6ce0130ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -228,6 +228,7 @@ dependencies = [ "ctor", "db", "editor", + "encoding", "env_logger 0.11.8", "fs", "futures 0.3.31", @@ -961,6 +962,7 @@ dependencies = [ "derive_more", "diffy", "editor", + "encoding", "feature_flags", "fs", "futures 0.3.31", @@ -3302,6 +3304,7 @@ dependencies = [ "dashmap 6.1.0", "debugger_ui", "editor", + "encoding", "envy", "extension", "file_finder", @@ -3643,6 +3646,7 @@ dependencies = [ "dirs 4.0.0", "edit_prediction", "editor", + "encoding", "fs", "futures 0.3.31", "gpui", @@ -5194,6 +5198,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" @@ -5203,6 +5271,23 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "encodings" +version = "0.1.0" +dependencies = [ + "anyhow", + "editor", + "encoding", + "fuzzy", + "gpui", + "language", + "picker", + "settings", + "ui", + "util", + "workspace", +] + [[package]] name = "endi" version = "1.1.0" @@ -5546,6 +5631,7 @@ dependencies = [ "criterion", "ctor", "dap", + "encoding", "extension", "fs", "futures 0.3.31", @@ -6042,6 +6128,7 @@ dependencies = [ "async-trait", "cocoa 0.26.0", "collections", + "encoding", "fsevent", "futures 0.3.31", "git", @@ -6055,6 +6142,7 @@ dependencies = [ "paths", "proto", "rope", + "schemars", "serde", "serde_json", "smol", @@ -6480,6 +6568,7 @@ dependencies = [ "ctor", "db", "editor", + "encoding", "futures 0.3.31", "fuzzy", "git", @@ -9022,6 +9111,7 @@ dependencies = [ "ctor", "diffy", "ec4rs", + "encoding", "fs", "futures 0.3.31", "fuzzy", @@ -12562,6 +12652,7 @@ dependencies = [ "context_server", "dap", "dap_adapters", + "encoding", "extension", "fancy-regex 0.14.0", "fs", @@ -13485,6 +13576,7 @@ dependencies = [ "dap_adapters", "debug_adapter_extension", "editor", + "encoding", "env_logger 0.11.8", "extension", "extension_host", @@ -19796,6 +19888,7 @@ dependencies = [ "component", "dap", "db", + "encoding", "fs", "futures 0.3.31", "gpui", @@ -20026,6 +20119,7 @@ dependencies = [ "anyhow", "clock", "collections", + "encoding", "fs", "futures 0.3.31", "fuzzy", @@ -20437,6 +20531,8 @@ dependencies = [ "diagnostics", "edit_prediction_button", "editor", + "encoding", + "encodings", "env_logger 0.11.8", "extension", "extension_host", diff --git a/Cargo.toml b/Cargo.toml index 6ec243a9b9..a45f594d31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ members = [ "crates/diagnostics", "crates/docs_preprocessor", "crates/editor", + "crates/encodings", "crates/eval", "crates/explorer_command_injector", "crates/extension", @@ -214,7 +215,7 @@ members = [ # "tooling/workspace-hack", - "tooling/xtask", + "tooling/xtask", "crates/encodings", ] default-members = ["crates/zed"] @@ -237,7 +238,6 @@ activity_indicator = { path = "crates/activity_indicator" } agent_ui = { path = "crates/agent_ui" } agent_settings = { path = "crates/agent_settings" } agent_servers = { path = "crates/agent_servers" } -ai = { path = "crates/ai" } ai_onboarding = { path = "crates/ai_onboarding" } anthropic = { path = "crates/anthropic" } askpass = { path = "crates/askpass" } @@ -249,7 +249,6 @@ assistant_tool = { path = "crates/assistant_tool" } assistant_tools = { path = "crates/assistant_tools" } audio = { path = "crates/audio" } auto_update = { path = "crates/auto_update" } -auto_update_helper = { path = "crates/auto_update_helper" } auto_update_ui = { path = "crates/auto_update_ui" } aws_http_client = { path = "crates/aws_http_client" } bedrock = { path = "crates/bedrock" } @@ -263,7 +262,6 @@ clock = { path = "crates/clock" } cloud_api_client = { path = "crates/cloud_api_client" } cloud_api_types = { path = "crates/cloud_api_types" } cloud_llm_client = { path = "crates/cloud_llm_client" } -collab = { path = "crates/collab" } collab_ui = { path = "crates/collab_ui" } collections = { path = "crates/collections" } command_palette = { path = "crates/command_palette" } @@ -309,6 +307,7 @@ icons = { path = "crates/icons" } image_viewer = { path = "crates/image_viewer" } edit_prediction = { path = "crates/edit_prediction" } edit_prediction_button = { path = "crates/edit_prediction_button" } +encodings = {path = "crates/encodings"} inspector_ui = { path = "crates/inspector_ui" } install_cli = { path = "crates/install_cli" } jj = { path = "crates/jj" } @@ -346,8 +345,6 @@ outline_panel = { path = "crates/outline_panel" } panel = { path = "crates/panel" } paths = { path = "crates/paths" } picker = { path = "crates/picker" } -plugin = { path = "crates/plugin" } -plugin_macros = { path = "crates/plugin_macros" } prettier = { path = "crates/prettier" } settings_profile_selector = { path = "crates/settings_profile_selector" } project = { path = "crates/project" } @@ -368,7 +365,6 @@ rope = { path = "crates/rope" } rpc = { path = "crates/rpc" } rules_library = { path = "crates/rules_library" } search = { path = "crates/search" } -semantic_index = { path = "crates/semantic_index" } semantic_version = { path = "crates/semantic_version" } session = { path = "crates/session" } settings = { path = "crates/settings" } @@ -379,7 +375,6 @@ snippets_ui = { path = "crates/snippets_ui" } sqlez = { path = "crates/sqlez" } sqlez_macros = { path = "crates/sqlez_macros" } story = { path = "crates/story" } -storybook = { path = "crates/storybook" } streaming_diff = { path = "crates/streaming_diff" } sum_tree = { path = "crates/sum_tree" } supermaven = { path = "crates/supermaven" } @@ -395,7 +390,6 @@ terminal_view = { path = "crates/terminal_view" } text = { path = "crates/text" } theme = { path = "crates/theme" } theme_extension = { path = "crates/theme_extension" } -theme_importer = { path = "crates/theme_importer" } theme_selector = { path = "crates/theme_selector" } time_format = { path = "crates/time_format" } title_bar = { path = "crates/title_bar" } @@ -467,7 +461,6 @@ ciborium = "0.2" circular-buffer = "1.0" clap = { version = "4.4", features = ["derive"] } cocoa = "0.26" -cocoa-foundation = "0.2.0" convert_case = "0.8.0" core-foundation = "0.10.0" core-foundation-sys = "0.8.6" @@ -484,6 +477,7 @@ documented = "0.9.1" dotenvy = "0.15.0" ec4rs = "1.1" emojis = "0.6.1" +encoding = "0.2.33" env_logger = "0.11" exec = "0.3.1" fancy-regex = "0.14.0" @@ -543,7 +537,6 @@ pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } -pet-pixi = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } portable-pty = "0.9.0" @@ -666,7 +659,6 @@ wasmtime = { version = "29", default-features = false, features = [ wasmtime-wasi = "29" which = "6.0.0" windows-core = "0.61" -wit-component = "0.221" workspace-hack = "0.1.0" yawc = "0.2.5" zstd = "0.11" @@ -740,11 +732,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 @@ -754,7 +742,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/assets/settings/default.json b/assets/settings/default.json index 804198090f..399eb1892f 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1258,7 +1258,9 @@ // Whether to show the active language button in the status bar. "active_language_button": true, // Whether to show the cursor position button in the status bar. - "cursor_position_button": true + "cursor_position_button": true, + // Whether to show the encoding indicator in the status bar. + "encoding_indicator": true }, // Settings specific to the terminal "terminal": { diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 68246a96b0..2de49d11d1 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -32,6 +32,7 @@ cloud_llm_client.workspace = true collections.workspace = true context_server.workspace = true db.workspace = true +encoding.workspace = true fs.workspace = true futures.workspace = true git.workspace = true @@ -72,6 +73,7 @@ which.workspace = true workspace-hack.workspace = true zstd.workspace = true + [dev-dependencies] agent = { workspace = true, "features" = ["test-support"] } agent_servers = { workspace = true, "features" = ["test-support"] } diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index f86bfd25f7..1f0850304f 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -523,7 +523,8 @@ mod tests { use super::*; use crate::{ContextServerRegistry, Templates}; use client::TelemetrySettings; - use fs::Fs; + use encoding::all::UTF_8; + use fs::{Fs, encodings::EncodingWrapper}; use gpui::{TestAppContext, UpdateGlobal}; use language_model::fake_provider::FakeLanguageModel; use prompt_store::ProjectContext; @@ -705,6 +706,7 @@ mod tests { path!("/root/src/main.rs").as_ref(), &"initial content".into(), language::LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -873,6 +875,7 @@ mod tests { path!("/root/src/main.rs").as_ref(), &"initial content".into(), language::LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); diff --git a/crates/assistant_tools/Cargo.toml b/crates/assistant_tools/Cargo.toml index 5a8ca8a5e9..e48dc9a0c5 100644 --- a/crates/assistant_tools/Cargo.toml +++ b/crates/assistant_tools/Cargo.toml @@ -28,6 +28,7 @@ component.workspace = true derive_more.workspace = true diffy = "0.4.2" editor.workspace = true +encoding.workspace = true feature_flags.workspace = true futures.workspace = true gpui.workspace = true diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 95b01c40eb..6b06ce03c5 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -1231,8 +1231,9 @@ async fn build_buffer_diff( #[cfg(test)] mod tests { use super::*; - use ::fs::Fs; + use ::fs::{Fs, encodings::EncodingWrapper}; use client::TelemetrySettings; + use encoding::all::UTF_8; use gpui::{TestAppContext, UpdateGlobal}; use language_model::fake_provider::FakeLanguageModel; use serde_json::json; @@ -1501,6 +1502,7 @@ mod tests { path!("/root/src/main.rs").as_ref(), &"initial content".into(), language::LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -1670,6 +1672,7 @@ mod tests { path!("/root/src/main.rs").as_ref(), &"initial content".into(), language::LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 4fccd3be7f..13b7932b95 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -31,6 +31,7 @@ chrono.workspace = true clock.workspace = true collections.workspace = true dashmap.workspace = true +encoding.workspace = true envy = "0.4.2" futures.workspace = true gpui.workspace = true diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 5c73253048..bc67f1351a 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -12,7 +12,8 @@ use buffer_diff::{DiffHunkSecondaryStatus, DiffHunkStatus, assert_hunks}; use call::{ActiveCall, ParticipantLocation, Room, room}; use client::{RECEIVE_TIMEOUT, User}; use collections::{HashMap, HashSet}; -use fs::{FakeFs, Fs as _, RemoveOptions}; +use encoding::all::UTF_8; +use fs::{FakeFs, Fs as _, RemoveOptions, encodings::EncodingWrapper}; use futures::{StreamExt as _, channel::mpsc}; use git::status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode}; use gpui::{ @@ -3706,6 +3707,7 @@ async fn test_buffer_reloading( path!("/dir/a.txt").as_ref(), &new_contents, LineEnding::Windows, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4472,6 +4474,7 @@ async fn test_reloading_buffer_manually( path!("/a/a.rs").as_ref(), &Rope::from("let seven = 7;"), LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index ac5c4c54ca..1c36e9f33e 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -5,7 +5,8 @@ use async_trait::async_trait; use call::ActiveCall; use collections::{BTreeMap, HashMap}; use editor::Bias; -use fs::{FakeFs, Fs as _}; +use encoding::all::UTF_8; +use fs::{FakeFs, Fs as _, encodings::EncodingWrapper}; use git::status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode}; use gpui::{BackgroundExecutor, Entity, TestAppContext}; use language::{ @@ -924,7 +925,12 @@ impl RandomizedTest for ProjectCollaborationTest { client .fs() - .save(&path, &content.as_str().into(), text::LineEnding::Unix) + .save( + &path, + &content.as_str().into(), + text::LineEnding::Unix, + EncodingWrapper::new(UTF_8), + ) .await .unwrap(); } diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 0fc119f311..ca8e4ae85d 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -30,6 +30,7 @@ client.workspace = true collections.workspace = true command_palette_hooks.workspace = true dirs.workspace = true +encoding.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true @@ -54,6 +55,7 @@ workspace.workspace = true workspace-hack.workspace = true itertools.workspace = true + [target.'cfg(windows)'.dependencies] async-std = { version = "1.12.0", features = ["unstable"] } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index b7d8423fd7..355df6b41e 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1193,6 +1193,7 @@ async fn get_copilot_lsp(fs: Arc, node_runtime: NodeRuntime) -> anyhow:: #[cfg(test)] mod tests { use super::*; + use encoding::Encoding; use gpui::TestAppContext; use util::path; @@ -1406,6 +1407,10 @@ mod tests { fn load_bytes(&self, _cx: &App) -> Task>> { unimplemented!() } + + fn load_with_encoding(&self, _: &App, _: &'static dyn Encoding) -> Task> { + unimplemented!() + } } } diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 1d7e04cae0..29a5dd43d4 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -136,6 +136,11 @@ pub struct StatusBar { /// /// Default: true pub cursor_position_button: bool, + + /// Whether to show the encoding indicator in the status bar. + /// + /// Default: true + pub encoding_indicator: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -593,6 +598,10 @@ pub struct StatusBarContent { /// /// Default: true pub cursor_position_button: Option, + /// Whether to show the encoding indicator in the status bar. + /// + /// Default: true + pub encoding_indicator: Option, } // Toolbar related settings diff --git a/crates/encodings/Cargo.toml b/crates/encodings/Cargo.toml new file mode 100644 index 0000000000..a4c2b959e8 --- /dev/null +++ b/crates/encodings/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "encodings" +version = "0.1.0" +publish.workspace = true +edition.workspace = true + +[dependencies] +anyhow.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 new file mode 100644 index 0000000000..9715a48d94 --- /dev/null +++ b/crates/encodings/src/lib.rs @@ -0,0 +1,244 @@ +///! 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 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}; + +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 workspace: WeakEntity, + observe: Option, // Subscription to observe changes in the active editor + show: 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(); + + 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) + .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 { + } + })), + ) + } +} + +impl EncodingIndicator { + pub fn new( + encoding: Option<&'static dyn encoding::Encoding>, + workspace: WeakEntity, + observe: Option, + ) -> EncodingIndicator { + EncodingIndicator { + encoding, + workspace, + observe, + show: true, + } + } + + 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, + ) { + 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.show = true; + } + None => { + self.encoding = None; + self.observe = None; + self.show = false; + } + } + } +} + +/// Get a human-readable name for the given encoding. +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() +} + +/// 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, + 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, + } +} + +/// 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 new file mode 100644 index 0000000000..9cca1551ec --- /dev/null +++ b/crates/encodings/src/selectors.rs @@ -0,0 +1,530 @@ +/// 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::sync::atomic::AtomicBool; + use util::ResultExt; + + use fuzzy::{StringMatch, StringMatchCandidate}; + use gpui::{DismissEvent, Entity, EventEmitter, Focusable, WeakEntity}; + + use ui::{Context, HighlightedLabel, ListItem, Render, Window, rems, v_flex}; + use workspace::{ModalView, Workspace}; + + 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, + } + + impl EncodingSaveOrReopenSelector { + 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, + current_selection: 0, + } + } + + /// 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| { + EncodingSaveOrReopenSelector::new(window, cx, weak_workspace) + }); + } + } + + 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 { + selector: WeakEntity, + current_selection: usize, + matches: Vec, + pub actions: Vec, + workspace: WeakEntity, + } + + impl EncodingSaveOrReopenDelegate { + pub fn new( + selector: WeakEntity, + workspace: WeakEntity, + ) -> Self { + Self { + 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) + } + + /// Handle the action selected by the user. + pub fn post_selection( + &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)?; + + 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(), + weak_workspace, + ) + }) + }); + } + } 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)?; + + 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(), + weak_workspace, + ) + }) + }); + } + } + + Some(()) + } + } + + 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; + 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 { + "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.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, _: 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.selector + .update(cx, |_, cx| cx.emit(DismissEvent)) + .log_err(); + } + + fn render_match( + &self, + ix: usize, + _: bool, + _: &mut Window, + _: &mut Context>, + ) -> Option { + Some( + ListItem::new(ix) + .child(HighlightedLabel::new( + &self.matches[ix].string, + self.matches[ix].positions.clone(), + )) + .spacing(ui::ListItemSpacing::Sparse), + ) + } + } + + 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. +pub mod encoding { + use std::sync::atomic::AtomicBool; + + use fuzzy::{StringMatch, StringMatchCandidate}; + use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, WeakEntity}; + use language::Buffer; + use picker::{Picker, PickerDelegate}; + use ui::{ + Context, HighlightedLabel, ListItem, ListItemSpacing, ParentElement, Render, Styled, + Window, rems, v_flex, + }; + use util::{ResultExt, TryFutureExt}; + use workspace::{ModalView, Workspace}; + + 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>, + workspace: WeakEntity, + } + + pub struct EncodingSelectorDelegate { + current_selection: usize, + encodings: Vec, + matches: Vec, + selector: WeakEntity, + buffer: WeakEntity, + action: Action, + } + + impl EncodingSelectorDelegate { + pub fn new( + selector: WeakEntity, + buffer: WeakEntity, + action: Action, + ) -> EncodingSelectorDelegate { + EncodingSelectorDelegate { + 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"), + ], + matches: Vec::new(), + selector, + buffer, + action, + } + } + } + + 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, _: &mut Window, _: &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(); + + 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, + true, + false, + 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(); + }) + } + + 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_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, _: &mut Window, cx: &mut Context>) { + self.selector + .update(cx, |_, cx| cx.emit(DismissEvent)) + .log_err(); + } + + fn render_match( + &self, + ix: usize, + _: bool, + _: &mut Window, + _: &mut Context>, + ) -> Option { + Some( + ListItem::new(ix) + .child(HighlightedLabel::new( + &self.matches[ix].string, + self.matches[ix].positions.clone(), + )) + .spacing(ListItemSpacing::Sparse), + ) + } + } + + /// The action to perform after selecting an encoding. + #[derive(PartialEq, Clone)] + pub enum Action { + Save, + Reopen, + } + + impl EncodingSelector { + pub fn new( + window: &mut Window, + cx: &mut Context, + action: Action, + buffer: WeakEntity, + workspace: WeakEntity, + ) -> EncodingSelector { + let delegate = + EncodingSelectorDelegate::new(cx.entity().downgrade(), buffer, action.clone()); + let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); + + EncodingSelector { picker, workspace } + } + } + + 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/extension_host/Cargo.toml b/crates/extension_host/Cargo.toml index c933d253c6..ef65c2e3d6 100644 --- a/crates/extension_host/Cargo.toml +++ b/crates/extension_host/Cargo.toml @@ -23,6 +23,7 @@ async-trait.workspace = true client.workspace = true collections.workspace = true dap.workspace = true +encoding.workspace = true extension.workspace = true fs.workspace = true futures.workspace = true diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index fde0aeac94..a5411c923f 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -12,6 +12,7 @@ use async_tar::Archive; use client::ExtensionProvides; use client::{Client, ExtensionMetadata, GetExtensionsResponse, proto, telemetry::Telemetry}; use collections::{BTreeMap, BTreeSet, HashMap, HashSet, btree_map}; +use encoding::all::UTF_8; pub use extension::ExtensionManifest; use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; use extension::{ @@ -20,6 +21,7 @@ use extension::{ ExtensionLanguageServerProxy, ExtensionSlashCommandProxy, ExtensionSnippetProxy, ExtensionThemeProxy, }; +use fs::encodings::EncodingWrapper; use fs::{Fs, RemoveOptions}; use futures::future::join_all; use futures::{ @@ -1468,10 +1470,15 @@ impl ExtensionStore { } if let Ok(index_json) = serde_json::to_string_pretty(&index) { - fs.save(&index_path, &index_json.as_str().into(), Default::default()) - .await - .context("failed to save extension index") - .log_err(); + fs.save( + &index_path, + &index_json.as_str().into(), + Default::default(), + EncodingWrapper::new(UTF_8), + ) + .await + .context("failed to save extension index") + .log_err(); } log::info!("rebuilt extension index in {:?}", start_time.elapsed()); @@ -1636,6 +1643,7 @@ impl ExtensionStore { &tmp_dir.join(EXTENSION_TOML), &Rope::from(manifest_toml), language::LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await?; } else { diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 1d4161134e..05654fa2cc 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -16,6 +16,7 @@ anyhow.workspace = true async-tar.workspace = true async-trait.workspace = true collections.workspace = true +encoding.workspace = true futures.workspace = true git.workspace = true gpui.workspace = true @@ -34,6 +35,9 @@ text.workspace = true time.workspace = true util.workspace = true workspace-hack.workspace = true +schemars.workspace = true + + [target.'cfg(target_os = "macos")'.dependencies] fsevent.workspace = true diff --git a/crates/fs/src/encodings.rs b/crates/fs/src/encodings.rs new file mode 100644 index 0000000000..8aecbcb764 --- /dev/null +++ b/crates/fs/src/encodings.rs @@ -0,0 +1,96 @@ +//! 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) + } + + pub async fn decode(&self, input: Vec) -> Result { + match self.0.decode(&input, encoding::DecoderTrap::Replace) { + Ok(v) => Ok(v), + Err(e) => Err(Error::msg(e.to_string())), + } + } + + pub async fn encode(&self, input: String) -> Result> { + match self.0.encode(&input, encoding::EncoderTrap::Replace) { + Ok(v) => Ok(v), + Err(e) => Err(Error::msg(e.to_string())), + } + } +} + +/// Convert a byte vector from a specified encoding to a UTF-8 string. +pub async fn to_utf8<'a>(input: Vec, encoding: EncodingWrapper) -> Result { + Ok(encoding.decode(input).await?) +} + +/// Convert a UTF-8 string to a byte vector in a specified encoding. +pub async fn from_utf8<'a>(input: String, target: EncodingWrapper) -> Result> { + Ok(target.encode(input).await?) +} diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 75312c5c0c..06d897681c 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -1,6 +1,7 @@ #[cfg(target_os = "macos")] mod mac_watcher; +pub mod encodings; #[cfg(not(target_os = "macos"))] pub mod fs_watcher; @@ -54,6 +55,9 @@ use smol::io::AsyncReadExt; #[cfg(any(test, feature = "test-support"))] use std::ffi::OsStr; +use crate::encodings::EncodingWrapper; +use crate::encodings::from_utf8; + pub trait Watcher: Send + Sync { fn add(&self, path: &Path) -> Result<()>; fn remove(&self, path: &Path) -> Result<()>; @@ -108,9 +112,25 @@ 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<()>; + 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; @@ -539,8 +559,12 @@ impl Fs for RealFs { async fn load(&self, path: &Path) -> Result { let path = path.to_path_buf(); - let text = smol::unblock(|| std::fs::read_to_string(path)).await?; - Ok(text) + let encoding = EncodingWrapper::new(encoding::all::UTF_8); + let text = + smol::unblock(async || Ok(encodings::to_utf8(std::fs::read(path)?, encoding).await?)) + .await + .await; + text } async fn load_bytes(&self, path: &Path) -> Result> { let path = path.to_path_buf(); @@ -594,7 +618,13 @@ impl Fs for RealFs { Ok(()) } - async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> { + async fn save( + &self, + path: &Path, + text: &Rope, + line_ending: LineEnding, + encoding: EncodingWrapper, + ) -> Result<()> { let buffer_size = text.summary().len.min(10 * 1024); if let Some(path) = path.parent() { self.create_dir(path).await?; @@ -602,7 +632,9 @@ impl Fs for RealFs { let file = smol::fs::File::create(path).await?; let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file); for chunk in chunks(text, line_ending) { - writer.write_all(chunk.as_bytes()).await?; + writer + .write_all(&from_utf8(chunk.to_string(), encoding.clone()).await?) + .await?; } writer.flush().await?; Ok(()) @@ -2273,14 +2305,22 @@ impl Fs for FakeFs { Ok(()) } - async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> { + async fn save( + &self, + path: &Path, + text: &Rope, + line_ending: LineEnding, + encoding: EncodingWrapper, + ) -> Result<()> { + use crate::encodings::from_utf8; + self.simulate_random_delay().await; let path = normalize_path(path); let content = chunks(text, line_ending).collect::(); if let Some(path) = path.parent() { self.create_dir(path).await?; } - self.write_file_internal(path, content.into_bytes(), false)?; + self.write_file_internal(path, from_utf8(content, encoding).await?, false)?; Ok(()) } diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 35f7a60354..2314279bb8 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -29,6 +29,7 @@ command_palette_hooks.workspace = true component.workspace = true db.workspace = true editor.workspace = true +encoding.workspace = true futures.workspace = true fuzzy.workspace = true git.workspace = true diff --git a/crates/git_ui/src/file_diff_view.rs b/crates/git_ui/src/file_diff_view.rs index a320888b3b..4b1446c071 100644 --- a/crates/git_ui/src/file_diff_view.rs +++ b/crates/git_ui/src/file_diff_view.rs @@ -362,8 +362,9 @@ impl Render for FileDiffView { mod tests { use super::*; use editor::test::editor_test_context::assert_state_with_diff; + use encoding::all::UTF_8; use gpui::TestAppContext; - use project::{FakeFs, Fs, Project}; + use project::{FakeFs, Fs, Project, encodings::EncodingWrapper}; use settings::{Settings, SettingsStore}; use std::path::PathBuf; use unindent::unindent; @@ -444,6 +445,7 @@ mod tests { ) .into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -479,6 +481,7 @@ mod tests { ) .into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 4ab56d6647..89bda420ea 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -31,7 +31,9 @@ anyhow.workspace = true async-trait.workspace = true clock.workspace = true collections.workspace = true +diffy = "0.4.2" ec4rs.workspace = true +encoding.workspace = true fs.workspace = true futures.workspace = true fuzzy.workspace = true @@ -69,7 +71,6 @@ unicase = "2.6" util.workspace = true watch.workspace = true workspace-hack.workspace = true -diffy = "0.4.2" [dev-dependencies] collections = { workspace = true, features = ["test-support"] } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 4ddc2b3018..c3c7f3ab92 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -21,6 +21,7 @@ use anyhow::{Context as _, Result}; pub use clock::ReplicaId; use clock::{AGENT_REPLICA_ID, Lamport}; use collections::HashMap; +use encoding::Encoding; use fs::MTime; use futures::channel::oneshot; use gpui::{ @@ -127,6 +128,7 @@ pub struct Buffer { has_unsaved_edits: Cell<(clock::Global, bool)>, change_bits: Vec>>, _subscriptions: Vec, + pub encoding: &'static dyn encoding::Encoding, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -400,6 +402,10 @@ pub trait LocalFile: File { /// Loads the file's contents from disk. fn load_bytes(&self, cx: &App) -> Task>>; + + /// Loads the file contents from disk, decoding them with the given encoding. + fn load_with_encoding(&self, cx: &App, encoding: &'static dyn Encoding) + -> Task>; } /// The auto-indent behavior associated with an editing operation. @@ -958,6 +964,7 @@ impl Buffer { has_conflict: false, change_bits: Default::default(), _subscriptions: Vec::new(), + encoding: encoding::all::UTF_8, } } @@ -1274,12 +1281,15 @@ impl Buffer { /// Reloads the contents of the buffer from disk. pub fn reload(&mut self, cx: &Context) -> oneshot::Receiver> { let (tx, rx) = futures::channel::oneshot::channel(); + let encoding = self.encoding; 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(()); @@ -4965,6 +4975,10 @@ impl LocalFile for TestFile { fn load_bytes(&self, _cx: &App) -> Task>> { unimplemented!() } + + fn load_with_encoding(&self, _: &App, _: &'static dyn Encoding) -> Task> { + unimplemented!() + } } pub(crate) fn contiguous_ranges( diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 57d6d6ca28..a2793c21e0 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -39,6 +39,7 @@ clock.workspace = true collections.workspace = true context_server.workspace = true dap.workspace = true +encoding.workspace = true extension.workspace = true fancy-regex.workspace = true fs.workspace = true @@ -90,6 +91,7 @@ worktree.workspace = true zlog.workspace = true workspace-hack.workspace = true + [dev-dependencies] client = { workspace = true, features = ["test-support"] } collections = { workspace = true, features = ["test-support"] } diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 295bad6e59..790ef4304d 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -372,6 +372,8 @@ impl LocalBufferStore { let version = buffer.version(); let buffer_id = buffer.remote_id(); let file = buffer.file().cloned(); + let encoding = buffer.encoding; + if file .as_ref() .is_some_and(|file| file.disk_state() == DiskState::New) @@ -380,7 +382,7 @@ impl LocalBufferStore { } let save = worktree.update(cx, |worktree, cx| { - worktree.write_file(path.as_ref(), text, line_ending, cx) + worktree.write_file(path.as_ref(), text, line_ending, cx, encoding) }); cx.spawn(async move |this, cx| { diff --git a/crates/project/src/prettier_store.rs b/crates/project/src/prettier_store.rs index 3ae5dc24ae..3cc0a58adb 100644 --- a/crates/project/src/prettier_store.rs +++ b/crates/project/src/prettier_store.rs @@ -7,7 +7,8 @@ use std::{ use anyhow::{Context as _, Result, anyhow}; use collections::{HashMap, HashSet}; -use fs::Fs; +use encoding::all::UTF_8; +use fs::{Fs, encodings::EncodingWrapper}; use futures::{ FutureExt, future::{self, Shared}, @@ -941,10 +942,12 @@ async fn install_prettier_packages( async fn save_prettier_server_file(fs: &dyn Fs) -> anyhow::Result<()> { let prettier_wrapper_path = default_prettier_dir().join(prettier::PRETTIER_SERVER_FILE); + let encoding_wrapper = EncodingWrapper::new(UTF_8); fs.save( &prettier_wrapper_path, &text::Rope::from(prettier::PRETTIER_SERVER_JS), text::LineEnding::Unix, + encoding_wrapper, ) .await .with_context(|| { diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 6dcd07482e..38b4cfe52d 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -9,7 +9,8 @@ use buffer_diff::{ BufferDiffEvent, CALCULATE_DIFF_TASK, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind, assert_hunks, }; -use fs::FakeFs; +use encoding::all::UTF_8; +use fs::{FakeFs, encodings::EncodingWrapper}; use futures::{StreamExt, future}; use git::{ GitHostingProviderRegistry, @@ -1448,10 +1449,14 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon ) .await .unwrap(); + + let encoding_wrapper = EncodingWrapper::new(UTF_8); + fs.save( path!("/the-root/Cargo.lock").as_ref(), &"".into(), Default::default(), + encoding_wrapper.clone(), ) .await .unwrap(); @@ -1459,6 +1464,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon path!("/the-stdlib/LICENSE").as_ref(), &"".into(), Default::default(), + encoding_wrapper.clone(), ) .await .unwrap(); @@ -1466,6 +1472,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon path!("/the/stdlib/src/string.rs").as_ref(), &"".into(), Default::default(), + encoding_wrapper, ) .await .unwrap(); @@ -3941,12 +3948,15 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) // the next file change occurs. cx.executor().deprioritize(*language::BUFFER_DIFF_TASK); + let encoding_wrapper = EncodingWrapper::new(UTF_8); + // Change the buffer's file on disk, and then wait for the file change // to be detected by the worktree, so that the buffer starts reloading. fs.save( path!("/dir/file1").as_ref(), &"the first contents".into(), Default::default(), + encoding_wrapper.clone(), ) .await .unwrap(); @@ -3958,6 +3968,7 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) path!("/dir/file1").as_ref(), &"the second contents".into(), Default::default(), + encoding_wrapper, ) .await .unwrap(); @@ -3996,12 +4007,15 @@ async fn test_edit_buffer_while_it_reloads(cx: &mut gpui::TestAppContext) { // the next file change occurs. cx.executor().deprioritize(*language::BUFFER_DIFF_TASK); + let encoding_wrapper = EncodingWrapper::new(UTF_8); + // Change the buffer's file on disk, and then wait for the file change // to be detected by the worktree, so that the buffer starts reloading. fs.save( path!("/dir/file1").as_ref(), &"the first contents".into(), Default::default(), + encoding_wrapper, ) .await .unwrap(); @@ -4603,10 +4617,14 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { let (new_contents, new_offsets) = marked_text_offsets("oneˇ\nthree ˇFOURˇ five\nsixtyˇ seven\n"); + + let encoding_wrapper = EncodingWrapper::new(UTF_8); + fs.save( path!("/dir/the-file").as_ref(), &new_contents.as_str().into(), LineEnding::Unix, + encoding_wrapper, ) .await .unwrap(); @@ -4634,11 +4652,14 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { assert!(!buffer.has_conflict()); }); + let encoding_wrapper = EncodingWrapper::new(UTF_8); + // Change the file on disk again, adding blank lines to the beginning. fs.save( path!("/dir/the-file").as_ref(), &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(), LineEnding::Unix, + encoding_wrapper, ) .await .unwrap(); @@ -4685,12 +4706,15 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { assert_eq!(buffer.line_ending(), LineEnding::Windows); }); + let encoding_wrapper = EncodingWrapper::new(UTF_8); + // Change a file's line endings on disk from unix to windows. The buffer's // state updates correctly. fs.save( path!("/dir/file1").as_ref(), &"aaa\nb\nc\n".into(), LineEnding::Windows, + encoding_wrapper, ) .await .unwrap(); diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 5dbb9a2771..a8817eabb6 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -30,6 +30,7 @@ clap.workspace = true client.workspace = true dap_adapters.workspace = true debug_adapter_extension.workspace = true +encoding.workspace = true env_logger.workspace = true extension.workspace = true extension_host.workspace = true diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 69fae7f399..bbf2e07c9c 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -6,10 +6,11 @@ use assistant_tool::{Tool as _, ToolResultContent}; use assistant_tools::{ReadFileTool, ReadFileToolInput}; use client::{Client, UserStore}; use clock::FakeSystemClock; +use encoding::all::UTF_8; use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModel}; use extension::ExtensionHostProxy; -use fs::{FakeFs, Fs}; +use fs::{FakeFs, Fs, encodings::EncodingWrapper}; use gpui::{AppContext as _, Entity, SemanticVersion, TestAppContext}; use http_client::{BlockedHttpClient, FakeHttpClient}; use language::{ @@ -123,6 +124,7 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test path!("/code/project1/src/main.rs").as_ref(), &"fn main() {}".into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -763,6 +765,7 @@ async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppCont &PathBuf::from(path!("/code/project1/src/lib.rs")), &("bangles".to_string().into()), LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -778,6 +781,7 @@ async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppCont &PathBuf::from(path!("/code/project1/src/lib.rs")), &("bloop".to_string().into()), LineEnding::Unix, + EncodingWrapper::new(UTF_8), ) .await .unwrap(); diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 434b14b07c..c9d98a0019 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -52,6 +52,7 @@ workspace.workspace = true zed_actions.workspace = true workspace-hack.workspace = true + [dev-dependencies] assets.workspace = true command_palette.workspace = true diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 869aa5322e..7cc8304989 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -35,6 +35,7 @@ clock.workspace = true collections.workspace = true component.workspace = true db.workspace = true +encoding.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 044601df97..0b59622578 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -19,6 +19,8 @@ mod workspace_settings; pub use crate::notifications::NotificationFrame; pub use dock::Panel; +use encoding::all::UTF_8; +use fs::encodings::EncodingWrapper; pub use path_list::PathList; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; @@ -7232,8 +7234,14 @@ pub fn create_and_open_local_file( let fs = workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?; if !fs.is_file(path).await { fs.create_file(path, Default::default()).await?; - fs.save(path, &default_content(), Default::default()) - .await?; + let encoding_wrapper = EncodingWrapper::new(UTF_8); + fs.save( + path, + &default_content(), + Default::default(), + encoding_wrapper, + ) + .await?; } let mut items = workspace diff --git a/crates/worktree/Cargo.toml b/crates/worktree/Cargo.toml index db264fe3aa..8b6e46ca3e 100644 --- a/crates/worktree/Cargo.toml +++ b/crates/worktree/Cargo.toml @@ -26,6 +26,7 @@ test-support = [ anyhow.workspace = true clock.workspace = true collections.workspace = true +encoding.workspace = true fs.workspace = true futures.workspace = true fuzzy.workspace = true @@ -49,6 +50,7 @@ text.workspace = true util.workspace = true workspace-hack.workspace = true + [dev-dependencies] clock = { workspace = true, features = ["test-support"] } collections = { workspace = true, features = ["test-support"] } diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index cf61ee2669..ce1eb0e818 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::{ @@ -831,9 +835,10 @@ impl Worktree { text: Rope, line_ending: LineEnding, cx: &Context, + encoding: &'static dyn Encoding, ) -> Task>> { match self { - Worktree::Local(this) => this.write_file(path, text, line_ending, cx), + Worktree::Local(this) => this.write_file(path, text, line_ending, cx, encoding), Worktree::Remote(_) => { Task::ready(Err(anyhow!("remote worktree can't yet write files"))) } @@ -1644,6 +1649,7 @@ impl LocalWorktree { text: Rope, line_ending: LineEnding, cx: &Context, + encoding: &'static dyn Encoding, ) -> Task>> { let path = path.into(); let fs = self.fs.clone(); @@ -1652,10 +1658,15 @@ impl LocalWorktree { return Task::ready(Err(anyhow!("invalid path {path:?}"))); }; + let encoding_wrapper = EncodingWrapper::new(encoding); + let write = cx.background_spawn({ let fs = fs.clone(); let abs_path = abs_path.clone(); - async move { fs.save(&abs_path, &text, line_ending).await } + async move { + fs.save(&abs_path, &text, line_ending, encoding_wrapper) + .await + } }); cx.spawn(async move |this, cx| { @@ -3361,6 +3372,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 { diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index c46e14f077..9c6564d7a4 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -3,7 +3,8 @@ use crate::{ worktree_settings::WorktreeSettings, }; use anyhow::Result; -use fs::{FakeFs, Fs, RealFs, RemoveOptions}; +use encoding::all::UTF_8; +use fs::{FakeFs, Fs, RealFs, RemoveOptions, encodings::EncodingWrapper}; use git::GITIGNORE; use gpui::{AppContext as _, BackgroundExecutor, BorrowAppContext, Context, Task, TestAppContext}; use parking_lot::Mutex; @@ -642,9 +643,15 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) { // Update the gitignore so that node_modules is no longer ignored, // but a subdirectory is ignored - fs.save("/root/.gitignore".as_ref(), &"e".into(), Default::default()) - .await - .unwrap(); + let encoding_wrapper = fs::encodings::EncodingWrapper::new(UTF_8); + fs.save( + "/root/.gitignore".as_ref(), + &"e".into(), + Default::default(), + encoding_wrapper, + ) + .await + .unwrap(); cx.executor().run_until_parked(); // All of the directories that are no longer ignored are now loaded. @@ -715,6 +722,7 @@ async fn test_write_file(cx: &mut TestAppContext) { "hello".into(), Default::default(), cx, + UTF_8, ) }) .await @@ -726,6 +734,7 @@ async fn test_write_file(cx: &mut TestAppContext) { "world".into(), Default::default(), cx, + UTF_8, ) }) .await @@ -1746,8 +1755,13 @@ fn randomly_mutate_worktree( }) } else { log::info!("overwriting file {:?} ({})", entry.path, entry.id.0); - let task = - worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx); + let task = worktree.write_file( + entry.path.clone(), + "".into(), + Default::default(), + cx, + UTF_8, + ); cx.background_spawn(async move { task.await?; Ok(()) @@ -1834,10 +1848,12 @@ async fn randomly_mutate_fs( ignore_path.strip_prefix(root_path).unwrap(), ignore_contents ); + let encoding_wrapper = EncodingWrapper::new(UTF_8); fs.save( &ignore_path, &ignore_contents.as_str().into(), Default::default(), + encoding_wrapper, ) .await .unwrap(); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6f4ead9ebb..21fe5798f4 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -55,6 +55,8 @@ debugger_tools.workspace = true debugger_ui.workspace = true diagnostics.workspace = true editor.workspace = true +encoding.workspace = true +encodings.workspace = true env_logger.workspace = true extension.workspace = true extension_host.workspace = true diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 553444ebdb..e11f65c60f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -397,6 +397,9 @@ pub fn initialize_workspace( } }); + 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)); workspace.status_bar().update(cx, |status_bar, cx| { @@ -409,6 +412,7 @@ pub fn initialize_workspace( status_bar.add_right_item(active_toolchain_language, window, cx); status_bar.add_right_item(vim_mode_indicator, window, cx); status_bar.add_right_item(cursor_position, window, cx); + status_bar.add_right_item(encoding_indicator, window, cx); status_bar.add_right_item(image_info, window, cx); }); @@ -1933,6 +1937,8 @@ mod tests { use assets::Assets; use collections::HashSet; use editor::{DisplayPoint, Editor, SelectionEffects, display_map::DisplayRow}; + use encoding::all::UTF_8; + use fs::encodings::EncodingWrapper; use gpui::{ Action, AnyWindowHandle, App, AssetSource, BorrowAppContext, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext, WindowHandle, actions, @@ -4124,6 +4130,7 @@ mod tests { "/settings.json".as_ref(), &r#"{"base_keymap": "Atom"}"#.into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4134,6 +4141,7 @@ mod tests { "/keymap.json".as_ref(), &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4182,6 +4190,7 @@ mod tests { "/keymap.json".as_ref(), &r#"[{"bindings": {"backspace": "test_only::ActionB"}}]"#.into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4202,6 +4211,7 @@ mod tests { "/settings.json".as_ref(), &r#"{"base_keymap": "JetBrains"}"#.into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4242,6 +4252,7 @@ mod tests { "/settings.json".as_ref(), &r#"{"base_keymap": "Atom"}"#.into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4251,6 +4262,7 @@ mod tests { "/keymap.json".as_ref(), &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4294,6 +4306,7 @@ mod tests { "/keymap.json".as_ref(), &r#"[{"bindings": {"backspace": null}}]"#.into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); @@ -4314,6 +4327,7 @@ mod tests { "/settings.json".as_ref(), &r#"{"base_keymap": "JetBrains"}"#.into(), Default::default(), + EncodingWrapper::new(UTF_8), ) .await .unwrap(); diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index a8a4689689..a7ec556ffe 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1278,7 +1278,8 @@ Each option controls displaying of a particular toolbar element. If all elements ```json "status_bar": { "active_language_button": true, - "cursor_position_button": true + "cursor_position_button": true, + "encoding_indicator": true, }, ```