From 7e22d054098135bb06e42cc7115f99cf1903aba9 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 3 Nov 2025 23:11:59 -0700 Subject: [PATCH] Cleanup * Remove the mutexes and have methods return the detected encoding. * Try to handle the BOM safely... * Clean up a bunch of code to make it more Zeddy --- Cargo.lock | 2 +- crates/copilot/src/copilot.rs | 9 +- .../src/syntax_index.rs | 2 +- crates/encodings/Cargo.toml | 5 + crates/encodings/src/encodings.rs | 214 ++++ crates/encodings/src/lib.rs | 188 ---- crates/encodings_ui/Cargo.toml | 5 +- crates/encodings_ui/src/encodings_ui.rs | 108 ++ crates/encodings_ui/src/selectors.rs | 945 +++++++----------- crates/fs/src/fs.rs | 32 +- crates/language/src/buffer.rs | 47 +- crates/languages/src/json.rs | 2 +- crates/multi_buffer/src/multi_buffer.rs | 4 +- crates/project/src/buffer_store.rs | 22 +- crates/project/src/image_store.rs | 2 - crates/project/src/invalid_item_view.rs | 122 --- crates/project/src/project.rs | 10 +- crates/project/src/project_tests.rs | 46 +- crates/workspace/src/invalid_item_view.rs | 55 +- crates/workspace/src/workspace.rs | 48 +- crates/worktree/src/worktree.rs | 84 +- crates/worktree/src/worktree_tests.rs | 4 +- crates/zed/src/zed.rs | 5 +- crates/zed_actions/src/lib.rs | 2 +- crates/zeta/src/zeta.rs | 2 +- 25 files changed, 799 insertions(+), 1166 deletions(-) create mode 100644 crates/encodings/src/encodings.rs delete mode 100644 crates/encodings/src/lib.rs create mode 100644 crates/encodings_ui/src/encodings_ui.rs delete mode 100644 crates/project/src/invalid_item_view.rs diff --git a/Cargo.lock b/Cargo.lock index 247e92d48ad12334a1670fc96a37718aab36def0..596de68b7c3982c69839cb932ec2c81357966df8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5601,7 +5601,7 @@ version = "0.1.0" dependencies = [ "anyhow", "editor", - "encoding_rs", + "encodings", "fs", "futures 0.3.31", "fuzzy", diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index c77b49dea383173eeecdbeab56d50d00c53ede68..9f952919bda80658805c7c31b00985a9ebc06b1a 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, EncodingOptions}; + use encodings::Encoding; use gpui::TestAppContext; use util::{path, paths::PathStyle, rel_path::rel_path}; @@ -1452,12 +1452,7 @@ mod tests { self.abs_path.clone() } - fn load( - &self, - _: &App, - _: &EncodingOptions, - _: Option>, - ) -> Task> { + fn load(&self, _: &App, _: Encoding) -> Task> { unimplemented!() } diff --git a/crates/edit_prediction_context/src/syntax_index.rs b/crates/edit_prediction_context/src/syntax_index.rs index dc2a465eee13fadd86b2721294032244331821d1..7aecdc09bd67e4fb7bceb5b5e313c2bb8d8a5cb1 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, &Default::default(), None, cx); + let load_task = worktree.load_file(&project_path.path, &Default::default(), cx); let worktree_abs_path = worktree.abs_path(); cx.spawn(async move |_this, cx| { diff --git a/crates/encodings/Cargo.toml b/crates/encodings/Cargo.toml index 0e900d576012e592f952332b0262acc69ef7ef40..f9e241d2f7102b75fd4fc3d6d8a2a31c3eac6f33 100644 --- a/crates/encodings/Cargo.toml +++ b/crates/encodings/Cargo.toml @@ -4,6 +4,11 @@ version = "0.1.0" publish.workspace = true edition.workspace = true + +[lib] +path = "src/encodings.rs" +doctest = false + [dependencies] anyhow.workspace = true encoding_rs.workspace = true diff --git a/crates/encodings/src/encodings.rs b/crates/encodings/src/encodings.rs new file mode 100644 index 0000000000000000000000000000000000000000..afcc515fc958d9f71fc8b1a85a5f828fcd14a6d6 --- /dev/null +++ b/crates/encodings/src/encodings.rs @@ -0,0 +1,214 @@ +use encoding_rs; +use std::{borrow::Cow, fmt::Debug}; + +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, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Encoding { + pub encoding: &'static encoding_rs::Encoding, + pub with_bom: bool, +} + +impl Default for Encoding { + fn default() -> Self { + Encoding { + encoding: UTF_8, + with_bom: false, + } + } +} + +impl Encoding { + pub fn decode(&self, input: Vec) -> anyhow::Result { + if self.encoding == UTF_8 && !self.with_bom { + return Ok(String::from_utf8(input)?); + } + let Some(result) = self + .encoding + .decode_without_bom_handling_and_without_replacement(&input) + else { + return Err(anyhow::anyhow!( + "input is not valid {}", + self.encoding.name() + )); + }; + + if self.with_bom && result.starts_with("\u{FEFF}") { + Ok(result[3..].to_string()) + } else { + Ok(result.into_owned()) + } + } + + pub fn bom(&self) -> Option<&'static [u8]> { + if !self.with_bom { + return None; + } + if self.encoding == UTF_8 { + Some(&[0xEF, 0xBB, 0xBF]) + } else if self.encoding == UTF_16BE { + Some(&[0xFE, 0xFF]) + } else if self.encoding == UTF_16LE { + Some(&[0xFF, 0xFE]) + } else { + None + } + } + + pub fn encode_chunk<'a>(&self, input: &'a str) -> anyhow::Result> { + if self.encoding == UTF_8 { + Ok(Cow::Borrowed(input.as_bytes())) + } else if self.encoding == 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); + Ok(Cow::Owned(data)) + } else if self.encoding == 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); + Ok(Cow::Owned(data)) + } else { + // todo: should we error on invalid content when encoding? + let (cow, _encoding_used, _had_errors) = self.encoding.encode(&input); + + Ok(cow) + } + } + + pub fn name(&self) -> &'static str { + let name = self.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 from_name(name: &str) -> Self { + let 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 + }; + + Encoding { + encoding, + with_bom: false, + } + } +} + +#[derive(Default, Clone)] +pub struct EncodingOptions { + pub expected: Encoding, + pub auto_detect: bool, +} + +impl EncodingOptions { + pub fn process(&self, bytes: Vec) -> anyhow::Result<(Encoding, String)> { + let encoding = if self.auto_detect + && let Some(encoding) = Self::detect(&bytes) + { + encoding + } else { + self.expected + }; + + Ok((encoding, encoding.decode(bytes)?)) + } + + fn detect(bytes: &[u8]) -> Option { + if bytes.starts_with(&[0xFE, 0xFF]) { + Some(Encoding { + encoding: UTF_8, + with_bom: true, + }) + } else if bytes.starts_with(&[0xFF, 0xFE]) { + Some(Encoding { + encoding: UTF_16LE, + with_bom: true, + }) + } else if bytes.starts_with(&[0xEF, 0xBB, 0xBF]) { + Some(Encoding { + encoding: UTF_8, + with_bom: true, + }) + } else { + None + } + } +} diff --git a/crates/encodings/src/lib.rs b/crates/encodings/src/lib.rs deleted file mode 100644 index 6158084e87464ec999bba56db094dff32a7fc26c..0000000000000000000000000000000000000000 --- a/crates/encodings/src/lib.rs +++ /dev/null @@ -1,188 +0,0 @@ -use encoding_rs; -use std::{ - fmt::Debug, - sync::{Arc, Mutex, atomic::AtomicBool}, -}; - -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, -}; - -pub struct Encoding(Mutex<&'static encoding_rs::Encoding>); - -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() - } -} - -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)) - } -} - -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 {} - -impl Encoding { - pub fn new(encoding: &'static encoding_rs::Encoding) -> Self { - Self(Mutex::new(encoding)) - } - - pub fn set(&self, encoding: &'static encoding_rs::Encoding) { - *self.0.lock().unwrap() = encoding; - } - - pub fn get(&self) -> &'static encoding_rs::Encoding { - *self.0.lock().unwrap() - } - - 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) - } - } - } - - let (cow, had_errors) = self.get().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.get().name() - )) - } - } - - pub async fn encode(&self, input: String) -> anyhow::Result> { - if self.get() == 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.get() == 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.get().encode(&input); - - Ok(cow.into_owned()) - } - } - - pub fn reset(&self) { - self.set(UTF_8); - } -} - -/// Convert a byte vector from a specified encoding to a UTF-8 string. -pub async fn to_utf8( - input: Vec, - options: &EncodingOptions, - buffer_encoding: Option>, -) -> anyhow::Result { - options - .encoding - .decode( - input, - options.force.load(std::sync::atomic::Ordering::Acquire), - options - .detect_utf16 - .load(std::sync::atomic::Ordering::Acquire), - buffer_encoding, - ) - .await -} - -/// 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 struct EncodingOptions { - pub encoding: Arc, - pub force: AtomicBool, - pub detect_utf16: AtomicBool, -} - -impl EncodingOptions { - 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(Encoding::default()), - force: AtomicBool::new(false), - detect_utf16: AtomicBool::new(true), - } - } -} - -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/encodings_ui/Cargo.toml b/crates/encodings_ui/Cargo.toml index 64c5d14161963e536434971d9be3071337c37c75..658fb02bb7486ca841729d307af63ba9c3c224ca 100644 --- a/crates/encodings_ui/Cargo.toml +++ b/crates/encodings_ui/Cargo.toml @@ -7,7 +7,7 @@ edition.workspace = true [dependencies] anyhow.workspace = true editor.workspace = true -encoding_rs.workspace = true +encodings.workspace = true fs.workspace = true futures.workspace = true fuzzy.workspace = true @@ -20,6 +20,9 @@ util.workspace = true workspace.workspace = true zed_actions.workspace = true +[lib] +path = "src/encodings_ui.rs" +doctest = false [lints] workspace = true diff --git a/crates/encodings_ui/src/encodings_ui.rs b/crates/encodings_ui/src/encodings_ui.rs new file mode 100644 index 0000000000000000000000000000000000000000..62a6eff1a3b8997c7f576f3886790adcbb04e92e --- /dev/null +++ b/crates/encodings_ui/src/encodings_ui.rs @@ -0,0 +1,108 @@ +//! A crate for handling file encodings in the text editor. + +use editor::Editor; +use gpui::{Entity, Subscription, WeakEntity}; +use language::{Buffer, BufferEvent}; +use ui::{ + App, Button, ButtonCommon, Context, IntoElement, LabelSize, Render, Tooltip, Window, div, +}; +use ui::{Clickable, ParentElement}; +use workspace::notifications::NotifyTaskExt; +use workspace::{ItemHandle, StatusItemView, Workspace}; +use zed_actions::encodings_ui::OpenWithEncoding; +// use zed_actions::encodings_ui::Toggle; + +/// A status bar item that shows the current file encoding and allows changing it. +pub struct EncodingIndicator { + pub buffer: Option>, + pub workspace: WeakEntity, + observe_buffer: Option, +} + +pub mod selectors; + +impl Render for EncodingIndicator { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { + let Some(buffer) = self.buffer() else { + return gpui::Empty.into_any_element(); + }; + + div() + .child( + Button::new("encoding", buffer.read(cx).encoding().name()) + .label_size(LabelSize::Small) + .tooltip(Tooltip::text("Select Encoding")) + .on_click(cx.listener(move |this, _, window, cx| { + let Some(buffer) = this.buffer() else { + return; + }; + this.workspace + .update(cx, move |workspace, cx| { + if buffer.read(cx).file().is_some() { + selectors::save_or_reopen(buffer, workspace, window, cx) + } else { + // todo!() + } + }) + .ok(); + })), + ) + .into_any_element() + } +} + +impl EncodingIndicator { + pub fn new(workspace: WeakEntity) -> EncodingIndicator { + EncodingIndicator { + workspace, + buffer: None, + observe_buffer: None, + } + } + + fn buffer(&self) -> Option> { + self.buffer.as_ref().and_then(|b| b.upgrade()) + } + + /// Update the encoding when the `encoding` field of the `Buffer` struct changes. + pub fn on_buffer_event( + &mut self, + _: Entity, + e: &BufferEvent, + cx: &mut Context, + ) { + if matches!(e, BufferEvent::EncodingChanged) { + 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, + ) { + if let Some(editor) = active_pane_item.and_then(|item| item.act_as::(cx)) + && let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() + { + self.buffer = Some(buffer.downgrade()); + self.observe_buffer = Some(cx.subscribe(&buffer, Self::on_buffer_event)); + } else { + self.buffer = None; + self.observe_buffer = None; + } + cx.notify(); + } +} + +pub fn init(cx: &mut App) { + cx.observe_new(|workspace: &mut Workspace, _, _| { + workspace.register_action(|workspace, action: &OpenWithEncoding, window, cx| { + selectors::open_with_encoding(action.0.clone(), workspace, window, cx) + .detach_and_notify_err(window, cx); + }); + }) + .detach(); +} diff --git a/crates/encodings_ui/src/selectors.rs b/crates/encodings_ui/src/selectors.rs index fc5a6793a96f2a53e048143e666deaef89fb9018..37f476df5cc629710eb2bc0ba6074d908c1ef61c 100644 --- a/crates/encodings_ui/src/selectors.rs +++ b/crates/encodings_ui/src/selectors.rs @@ -1,632 +1,409 @@ -/// 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. -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); - - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); +use anyhow::Result; +use editor::Editor; +use encodings::Encoding; +use encodings::EncodingOptions; +use futures::channel::oneshot; +use gpui::ParentElement; +use gpui::Task; +use language::Buffer; +use picker::Picker; +use picker::PickerDelegate; +use std::path::Path; +use std::sync::Arc; +use std::sync::atomic::AtomicBool; +use ui::Label; +use ui::ListItemSpacing; +use ui::rems; +use util::ResultExt; + +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{DismissEvent, Entity, WeakEntity}; + +use ui::{Context, HighlightedLabel, ListItem, Window}; +use workspace::Workspace; + +pub fn save_or_reopen( + buffer: Entity, + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, +) { + let weak_workspace = cx.weak_entity(); + workspace.toggle_modal(window, cx, |window, cx| { + let delegate = EncodingSaveOrReopenDelegate::new(buffer, weak_workspace); + Picker::nonsearchable_uniform_list(delegate, window, cx) + .modal(true) + .width(rems(34.0)) + }) +} - Self { - picker, - current_selection: 0, - } - } +pub fn open_with_encoding( + path: Arc, + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, +) -> Task> { + let (tx, rx) = oneshot::channel(); + workspace.toggle_modal(window, cx, |window, cx| { + let delegate = EncodingSelectorDelegate::new(None, tx); + Picker::uniform_list(delegate, window, cx) + }); + let project = workspace.project().clone(); + cx.spawn_in(window, async move |workspace, cx| { + let encoding = rx.await.unwrap(); + + let (worktree, rel_path) = project + .update(cx, |project, cx| { + project.find_or_create_worktree(path, false, cx) + })? + .await?; + + let project_path = (worktree.update(cx, |worktree, _| worktree.id())?, rel_path).into(); + + let buffer = project + .update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer( + project_path, + &EncodingOptions { + expected: encoding, + auto_detect: true, + }, + cx, + ) + }) + })? + .await?; + workspace.update_in(cx, |workspace, window, cx| { + workspace.open_project_item::( + workspace.active_pane().clone(), + buffer, + true, + true, + window, + cx, + ) + })?; - /// 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) - }); - } - } + Ok(()) + }) +} - impl Focusable for EncodingSaveOrReopenSelector { - fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle { - self.picker.focus_handle(cx) +pub fn reopen_with_encoding( + buffer: Entity, + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, +) { + let encoding = buffer.read(cx).encoding(); + let (tx, rx) = oneshot::channel(); + workspace.toggle_modal(window, cx, |window, cx| { + let delegate = EncodingSelectorDelegate::new(Some(encoding), tx); + Picker::uniform_list(delegate, window, cx) + }); + cx.spawn(async move |_, cx| { + let encoding = rx.await.unwrap(); + + let (task, prev) = buffer.update(cx, |buffer, cx| { + let prev = buffer.encoding(); + buffer.set_encoding(encoding, cx); + (buffer.reload(cx), prev) + })?; + + if task.await.is_err() { + buffer.update(cx, |buffer, cx| { + buffer.set_encoding(prev, 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()) - } - } + anyhow::Ok(()) + }) + .detach(); +} - impl ModalView for EncodingSaveOrReopenSelector {} +pub fn save_with_encoding( + buffer: Entity, + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, +) { + let encoding = buffer.read(cx).encoding(); + let (tx, rx) = oneshot::channel(); + workspace.toggle_modal(window, cx, |window, cx| { + let delegate = EncodingSelectorDelegate::new(Some(encoding), tx); + Picker::uniform_list(delegate, window, cx) + }); + cx.spawn(async move |workspace, cx| { + let encoding = rx.await.unwrap(); + workspace + .update(cx, |workspace, cx| { + buffer.update(cx, |buffer, cx| { + buffer.set_encoding(encoding, cx); + }); + workspace + .project() + .update(cx, |project, cx| project.save_buffer(buffer, cx)) + }) + .ok(); + }) + .detach(); +} - impl EventEmitter for EncodingSaveOrReopenSelector {} +pub enum SaveOrReopen { + Save, + Reopen, +} - pub struct EncodingSaveOrReopenDelegate { - selector: WeakEntity, - current_selection: usize, - matches: Vec, - pub actions: Vec, - workspace: WeakEntity, - } +pub struct EncodingSaveOrReopenDelegate { + current_selection: usize, + actions: Vec, + workspace: WeakEntity, + buffer: Entity, +} - 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, - } +impl EncodingSaveOrReopenDelegate { + pub fn new(buffer: Entity, workspace: WeakEntity) -> Self { + Self { + current_selection: 0, + actions: vec![SaveOrReopen::Save, SaveOrReopen::Reopen], + workspace, + buffer, } + } +} - pub fn get_actions(&self) -> (&str, &str) { - (&self.actions[0].string, &self.actions[1].string) - } +impl PickerDelegate for EncodingSaveOrReopenDelegate { + type ListItem = ListItem; - /// 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(); - - 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() { - let (_, buffer, _) = workspace - .read(cx) - .active_item(cx)? - .act_as::(cx)? - .read(cx) - .active_excerpt(cx)?; - - let weak_workspace = workspace.read(cx).weak_handle(); - - 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 - }); - }); - } - } - } + fn match_count(&self) -> usize { + self.actions.len() + } - Some(()) - } + fn selected_index(&self) -> usize { + self.current_selection } - impl PickerDelegate for EncodingSaveOrReopenDelegate { - type ListItem = ListItem; + fn set_selected_index( + &mut self, + ix: usize, + _window: &mut Window, + _cx: &mut Context>, + ) { + self.current_selection = ix; + } - fn match_count(&self) -> usize { - self.matches.len() - } + fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc { + "Select an action...".into() + } - fn selected_index(&self) -> usize { - self.current_selection - } + fn update_matches( + &mut self, + _query: String, + _window: &mut Window, + _cx: &mut Context>, + ) -> Task<()> { + return Task::ready(()); + } - 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; + fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { + self.dismissed(window, cx); + cx.defer_in(window, |this, window, cx| { + let this = &this.delegate; + this.workspace + .update(cx, |workspace, cx| { + match this.actions[this.current_selection] { + SaveOrReopen::Reopen => { + reopen_with_encoding(this.buffer.clone(), workspace, window, cx); + } + SaveOrReopen::Save => { + save_with_encoding(this.buffer.clone(), workspace, window, cx); + } + } }) - .log_err(); - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc { - "Select an action...".into() - } + .ok(); + }) + } - 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 - }; + fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { + cx.emit(DismissEvent) + } - 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(); + fn render_match( + &self, + ix: usize, + _: bool, + _: &mut Window, + _: &mut Context>, + ) -> Option { + Some( + ListItem::new(ix) + .child(match self.actions[ix] { + SaveOrReopen::Save => Label::new("Save with encoding"), + SaveOrReopen::Reopen => Label::new("Reopen with encoding"), }) - .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); - } - } + .spacing(ui::ListItemSpacing::Sparse), + ) + } +} - fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { - self.selector - .update(cx, |_, cx| cx.emit(DismissEvent)) - .log_err(); - } +pub struct EncodingSelectorDelegate { + current_selection: usize, + encodings: Vec, + matches: Vec, + tx: Option>, +} - 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), - ) +impl EncodingSelectorDelegate { + pub fn new( + encoding: Option, + tx: oneshot::Sender, + ) -> EncodingSelectorDelegate { + let encodings = vec![ + StringMatchCandidate::new(0, "UTF-8"), + 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"), + ]; + let current_selection = if let Some(encoding) = encoding { + encodings + .iter() + .position(|e| encoding.name() == e.string) + .unwrap_or_default() + } else { + 0 + }; + + EncodingSelectorDelegate { + current_selection, + encodings, + matches: Vec::new(), + tx: Some(tx), } } } -/// This module contains the encoding selector for choosing an encoding to save or reopen a file with. -pub mod encoding { - use editor::Editor; - use std::{path::PathBuf, sync::atomic::AtomicBool}; - - use fuzzy::{StringMatch, StringMatchCandidate}; - use gpui::{ - AppContext, DismissEvent, Entity, EventEmitter, Focusable, WeakEntity, http_client::anyhow, - }; - use language::Buffer; - use picker::{Picker, PickerDelegate}; - use ui::{ - Context, HighlightedLabel, ListItem, ListItemSpacing, ParentElement, Render, Styled, - Window, rems, v_flex, - }; - use util::ResultExt; - use workspace::{CloseActiveItem, ModalView, OpenOptions, 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, - path: Option, - } +impl PickerDelegate for EncodingSelectorDelegate { + type ListItem = ListItem; - pub struct EncodingSelectorDelegate { - current_selection: usize, - encodings: Vec, - matches: Vec, - selector: WeakEntity, - buffer: Option>, - action: Action, + fn match_count(&self) -> usize { + self.matches.len() } - impl EncodingSelectorDelegate { - pub fn new( - selector: WeakEntity, - buffer: Option>, - 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, "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, - buffer: buffer, - action, - } - } + fn selected_index(&self) -> usize { + self.current_selection } - 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 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 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, - 30, - &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(); + fn update_matches( + &mut self, + query: String, + window: &mut Window, + cx: &mut Context>, + ) -> 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, }) - .log_err(); - }) - } - - fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { - let workspace = self - .selector - .upgrade() - .unwrap() - .read(cx) - .workspace - .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) = buffer.upgrade() - { - let path = self - .selector - .upgrade() - .unwrap() - .read(cx) - .path - .clone() - .unwrap(); - - let reload = buffer.update(cx, |buffer, cx| buffer.reload(cx)); - - buffer.update(cx, |buffer, _| { - buffer.update_encoding(encoding_from_name(¤t_selection).into()) - }); - - self.dismissed(window, cx); - - if self.action == Action::Reopen { - 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 - .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 { - workspace.update(cx, |workspace, cx| { - workspace - .save_active_item(workspace::SaveIntent::Save, window, cx) - .detach(); - }); - } + .collect(); } else { - 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()); - - let open_task = workspace.update(cx, |workspace, cx| { - workspace.encoding_options.encoding.set(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 - .update(cx, |buffer, _| buffer.update_encoding(encoding.into())) - .log_err(); - } - }) - .detach(); - } + matches = fuzzy::match_strings( + &encodings, + &query, + true, + false, + 30, + &AtomicBool::new(false), + executor, + ) + .await } - } - - fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { - self.selector - .update(cx, |_, cx| cx.emit(DismissEvent)) + 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 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: 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, - path, - } + fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { + let current_selection = self.matches[self.current_selection].string.clone(); + let encoding = Encoding::from_name(¤t_selection); + if let Some(tx) = self.tx.take() { + tx.send(encoding).log_err(); } + self.dismissed(window, cx); } - impl EventEmitter for EncodingSelector {} - - impl Focusable for EncodingSelector { - fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle { - self.picker.focus_handle(cx) - } + fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { + cx.emit(DismissEvent); } - 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()) - } + 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), + ) } } diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 59719e47af4a4433437d3050ddbc960bdab392ed..2ef9a91f156d938fc7e72a1d56c793a3d6f14020 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -58,7 +58,7 @@ use smol::io::AsyncReadExt; #[cfg(any(test, feature = "test-support"))] use std::ffi::OsStr; -use encodings::{Encoding, EncodingOptions, from_utf8, to_utf8}; +use encodings::{Encoding, EncodingOptions}; #[cfg(any(test, feature = "test-support"))] pub use fake_git_repo::{LOAD_HEAD_TEXT_TASK, LOAD_INDEX_TEXT_TASK}; @@ -121,9 +121,9 @@ pub trait Fs: Send + Sync { &self, path: &Path, options: &EncodingOptions, - buffer_encoding: Option>, - ) -> Result { - Ok(to_utf8(self.load_bytes(path).await?, options, buffer_encoding).await?) + ) -> Result<(Encoding, String)> { + let bytes = self.load_bytes(path).await?; + options.process(bytes) } async fn load_bytes(&self, path: &Path) -> Result>; @@ -689,21 +689,12 @@ 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 - // 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() == encodings::UTF_16LE { - // Write BOM for UTF-16LE - writer.write_all(&[0xFF, 0xFE]).await?; + if let Some(bom) = encoding.bom() { + writer.write_all(bom).await?; } for chunk in chunks(text, line_ending) { - writer - .write_all(&from_utf8(chunk.to_string(), Encoding::new(encoding.get())).await?) - .await? + writer.write_all(&encoding.encode_chunk(chunk)?).await? } writer.flush().await?; @@ -2424,15 +2415,18 @@ impl Fs for FakeFs { line_ending: LineEnding, encoding: Encoding, ) -> Result<()> { - use 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, from_utf8(content, encoding).await?, false)?; + let mut bytes = Vec::new(); + if let Some(bom) = encoding.bom() { + bytes.extend_from_slice(bom); + } + bytes.extend_from_slice(&encoding.encode_chunk(&content)?); + self.write_file_internal(path, bytes, false)?; Ok(()) } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index a82c49ae546980a42f4ec823b26fda357bfe40a7..845c32cff6693c8cb2673776b332bb10538c7c7c 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: Arc, + encoding: Encoding, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -332,6 +332,8 @@ pub enum BufferEvent { DiagnosticsUpdated, /// The buffer gained or lost editing capabilities. CapabilityChanged, + /// The buffer's encoding was changed. + EncodingChanged, } /// The file associated with a buffer. @@ -418,12 +420,7 @@ 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, - options: &EncodingOptions, - buffer_encoding: Option>, - ) -> Task>; + fn load(&self, cx: &App, options: EncodingOptions) -> Task>; /// Loads the file's contents from disk. fn load_bytes(&self, cx: &App) -> Task>>; @@ -1029,7 +1026,7 @@ impl Buffer { has_conflict: false, change_bits: Default::default(), _subscriptions: Vec::new(), - encoding: Arc::new(Encoding::new(encodings::UTF_8)), + encoding: Encoding::default(), } } @@ -1365,11 +1362,6 @@ 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 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| { @@ -1377,14 +1369,20 @@ impl Buffer { let file = this.file.as_ref()?.as_local()?; Some((file.disk_state().mtime(), { - file.load(cx, &options, Some(buffer_encoding)) + file.load( + cx, + EncodingOptions { + expected: this.encoding, + auto_detect: false, + }, + ) })) })? else { return Ok(()); }; - let new_text = new_text.await?; + let (new_encoding, new_text) = new_text.await?; let diff = this .update(cx, |this, cx| this.diff(new_text.clone(), cx))? .await; @@ -1394,6 +1392,9 @@ impl Buffer { this.apply_diff(diff, cx); tx.send(this.finalize_last_transaction().cloned()).ok(); this.has_conflict = false; + if new_encoding != this.encoding { + this.set_encoding(new_encoding, cx); + } this.did_reload(this.version(), this.line_ending(), new_mtime, cx); } else { if !diff.edits.is_empty() @@ -2935,9 +2936,14 @@ impl Buffer { !self.has_edits_since(&self.preview_version) } + pub fn encoding(&self) -> Encoding { + self.encoding + } + /// Update the buffer - pub fn update_encoding(&mut self, encoding: Encoding) { - self.encoding.set(encoding.get()); + pub fn set_encoding(&mut self, encoding: Encoding, cx: &mut Context) { + self.encoding = encoding; + cx.emit(BufferEvent::EncodingChanged); } } @@ -5260,12 +5266,7 @@ impl LocalFile for TestFile { .join(self.path.as_std_path()) } - fn load( - &self, - _cx: &App, - _options: &EncodingOptions, - _buffer_encoding: Option>, - ) -> Task> { + fn load(&self, _cx: &App, _options: EncodingOptions) -> Task> { unimplemented!() } diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 30d3c34f1502c8ce1f25e775d41a6f2c55fad115..c939d62fddeec6712fa1c2b76c197653ef60de1b 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, &Default::default(), None, cx) + this.load_file(&file.path, &Default::default(), cx) }) .ok()? .await diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index bdfc7a7606a4d6e4a77a74ebb3c42a41449f002e..822f35c399a6ee711fcfcde1c15551cf063aadb9 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1730,7 +1730,9 @@ impl MultiBuffer { self.capability = buffer.read(cx).capability(); return; } - BufferEvent::Operation { .. } | BufferEvent::ReloadNeeded => return, + BufferEvent::Operation { .. } + | BufferEvent::ReloadNeeded + | BufferEvent::EncodingChanged => return, }); } diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index a27f7a0759f5126eac9344481a1d284252c44855..6e975686c66439caf567f83cda2d07fffb5fd582 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -389,7 +389,7 @@ impl LocalBufferStore { let version = buffer.version(); let buffer_id = buffer.remote_id(); let file = buffer.file().cloned(); - let encoding = buffer.encoding.clone(); + let encoding = buffer.encoding().clone(); if file .as_ref() @@ -399,7 +399,7 @@ impl LocalBufferStore { } let save = worktree.update(cx, |worktree, cx| { - worktree.write_file(path.clone(), text, line_ending, (*encoding).clone(), cx) + worktree.write_file(path.clone(), text, line_ending, encoding, cx) }); cx.spawn(async move |this, cx| { @@ -528,7 +528,6 @@ impl LocalBufferStore { path: entry.path.clone(), worktree: worktree.clone(), is_private: entry.is_private, - encoding: None, } } else { File { @@ -538,7 +537,6 @@ impl LocalBufferStore { path: old_file.path.clone(), worktree: worktree.clone(), is_private: old_file.is_private, - encoding: None, } }; @@ -633,20 +631,19 @@ impl LocalBufferStore { 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(), &options, None, cx); + let load_file_task = worktree.load_file(path.as_ref(), &options, cx); 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( + let buffer = cx.insert_entity(reservation, |cx| { + let mut buffer = Buffer::build( text::Buffer::new( ReplicaId::LOCAL, buffer_id, @@ -655,7 +652,9 @@ impl LocalBufferStore { ), Some(loaded_file.file), Capability::ReadWrite, - ) + ); + buffer.set_encoding(loaded_file.encoding, cx); + buffer })?; Ok(buffer) @@ -682,7 +681,6 @@ impl LocalBufferStore { entry_id: None, is_local: true, is_private: false, - encoding: Some(encoding.clone()), })), Capability::ReadWrite, ) @@ -710,10 +708,6 @@ impl LocalBufferStore { anyhow::Ok(()) })??; - buffer.update(cx, |buffer, _| { - buffer.update_encoding(encoding.get().into()) - })?; - Ok(buffer) }) } diff --git a/crates/project/src/image_store.rs b/crates/project/src/image_store.rs index 2f24927e04306407de9ebb92a88d427f58dda756..8fcf9c8a6172f866d819e34cbf3b0b4810a8fc8d 100644 --- a/crates/project/src/image_store.rs +++ b/crates/project/src/image_store.rs @@ -605,7 +605,6 @@ impl LocalImageStore { path: entry.path.clone(), worktree: worktree.clone(), is_private: entry.is_private, - encoding: None, } } else { worktree::File { @@ -615,7 +614,6 @@ impl LocalImageStore { path: old_file.path.clone(), worktree: worktree.clone(), is_private: old_file.is_private, - encoding: None, } }; diff --git a/crates/project/src/invalid_item_view.rs b/crates/project/src/invalid_item_view.rs deleted file mode 100644 index c5dfd6436f8a0a669db18024eba7ae2ef4b7cc21..0000000000000000000000000000000000000000 --- a/crates/project/src/invalid_item_view.rs +++ /dev/null @@ -1,122 +0,0 @@ -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, div, h_flex, v_flex, -}; -use zed_actions::workspace::OpenWithSystem; - -use crate::Item; - -/// A view to display when a certain buffer fails to open. -#[derive(Debug)] -pub struct InvalidItemView { - /// Which path was attempted to open. - pub abs_path: Arc, - /// An error message, happened when opening the buffer. - pub error: SharedString, - is_local: bool, - focus_handle: FocusHandle, -} - -impl InvalidItemView { - pub fn new( - abs_path: &Path, - is_local: bool, - e: &anyhow::Error, - _: &mut Window, - cx: &mut App, - ) -> Self { - Self { - is_local, - abs_path: Arc::from(abs_path), - error: format!("{}", e.root_cause()).into(), - focus_handle: cx.focus_handle(), - } - } -} - -impl Item for InvalidItemView { - type Event = (); - - fn tab_content_text(&self, mut detail: usize, _: &App) -> SharedString { - // Ensure we always render at least the filename. - detail += 1; - - let path = self.abs_path.as_ref(); - - let mut prefix = path; - while detail > 0 { - if let Some(parent) = prefix.parent() { - prefix = parent; - detail -= 1; - } else { - break; - } - } - - let path = if detail > 0 { - path - } else { - path.strip_prefix(prefix).unwrap_or(path) - }; - - SharedString::new(path.to_string_lossy()) - } -} - -impl EventEmitter<()> for InvalidItemView {} - -impl Focusable for InvalidItemView { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for InvalidItemView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { - let abs_path = self.abs_path.clone(); - - v_flex() - .size_full() - .track_focus(&self.focus_handle(cx)) - .flex_none() - .justify_center() - .overflow_hidden() - .key_context("InvalidBuffer") - .child( - h_flex().size_full().justify_center().items_center().child( - v_flex() - .gap_2() - .max_w_96() - .child(h_flex().justify_center().child("Could not open file")) - .child( - 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, - window, - cx, - )), - ), - ) - }), - ), - ) - } -} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 69d201be295855c916915789270e695b240632fe..880d55c401eb45555948d4586151259b49805e7d 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::{Encoding, EncodingOptions}; +use encodings::Encoding; pub use environment::ProjectEnvironmentEvent; use git::repository::get_git_committer; use git_store::{Repository, RepositoryId}; @@ -218,7 +218,6 @@ pub struct Project { settings_observer: Entity, toolchain_store: Option>, agent_location: Option, - pub encoding_options: EncodingOptions, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -1229,7 +1228,6 @@ impl Project { toolchain_store: Some(toolchain_store), agent_location: None, - encoding_options: EncodingOptions::default(), } }) } @@ -1415,7 +1413,6 @@ impl Project { toolchain_store: Some(toolchain_store), agent_location: None, - encoding_options: EncodingOptions::default(), }; // remote server -> local machine handlers @@ -1669,7 +1666,6 @@ impl Project { remotely_created_models: Arc::new(Mutex::new(RemotelyCreatedModels::default())), toolchain_store: None, agent_location: None, - encoding_options: EncodingOptions::default(), }; project.set_role(role, cx); @@ -2720,7 +2716,7 @@ impl Project { } self.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.open_buffer(path.into(), &self.encoding_options, cx) + buffer_store.open_buffer(path.into(), &Default::default(), cx) }) } @@ -5403,7 +5399,7 @@ impl Project { cx.spawn(async move |cx| { let file = worktree .update(cx, |worktree, cx| { - worktree.load_file(&rel_path, &Default::default(), None, cx) + worktree.load_file(&rel_path, &Default::default(), 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 cb5b17fcd0efb4bb93edd3e015dda2d34052baca..02c1faf7625f4d64bf7e97bd8e4066c05c853fc0 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 encodings::{Encoding, UTF_8}; +use encodings::Encoding; use fs::FakeFs; use futures::{StreamExt, future}; use git::{ @@ -3878,12 +3878,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/dir3/test.txt"), - &Default::default(), - None, - cx, - ) + worktree.load_file(rel_path("dir1/dir2/dir3/test.txt"), &Default::default(), cx) }) .await .unwrap() @@ -3930,12 +3925,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"), - &Default::default(), - None, - cx, - ) + worktree.load_file(rel_path("dir1/dir2/test.txt"), &Default::default(), cx) }) .await .unwrap() @@ -4085,15 +4075,13 @@ 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 = 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. fs.save( path!("/dir/file1").as_ref(), &Rope::from_str("the first contents", cx.background_executor()), Default::default(), - encoding.clone(), + Default::default(), ) .await .unwrap(); @@ -4105,7 +4093,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, + Default::default(), ) .await .unwrap(); @@ -4144,15 +4132,13 @@ 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 = 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. fs.save( path!("/dir/file1").as_ref(), &Rope::from_str("the first contents", cx.background_executor()), Default::default(), - encoding, + Default::default(), ) .await .unwrap(); @@ -4828,13 +4814,11 @@ 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 = 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, + Default::default(), ) .await .unwrap(); @@ -4862,14 +4846,12 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { assert!(!buffer.has_conflict()); }); - 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, + Default::default(), ) .await .unwrap(); @@ -4916,15 +4898,13 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { assert_eq!(buffer.line_ending(), LineEnding::Windows); }); - let encoding = Encoding::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, + Default::default(), ) .await .unwrap(); @@ -9016,7 +8996,6 @@ async fn test_ignored_dirs_events(cx: &mut gpui::TestAppContext) { tree.load_file( rel_path("project/target/debug/important_text.txt"), &Default::default(), - None, cx, ) }) @@ -9179,12 +9158,7 @@ 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"), - &Default::default(), - None, - cx, - ) + tree.load_file(rel_path("target/debug/foo.txt"), &Default::default(), cx) }) .await .unwrap(); diff --git a/crates/workspace/src/invalid_item_view.rs b/crates/workspace/src/invalid_item_view.rs index 75dcf768a16c2d00c8239a95997421b4bab00e90..0609512a17c848c0d9a2583c985c4d0d1c6aa51f 100644 --- a/crates/workspace/src/invalid_item_view.rs +++ b/crates/workspace/src/invalid_item_view.rs @@ -1,5 +1,4 @@ use std::{path::Path, sync::Arc}; -use ui::TintColor; use gpui::{EventEmitter, FocusHandle, Focusable, div}; use ui::{ @@ -79,7 +78,6 @@ 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() @@ -118,46 +116,25 @@ impl Render for InvalidItemView { ), ) .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, - ) - }, - ), + h_flex().justify_center().child( + Button::new( + "open-with-encoding", + "Try a Different Encoding", ) - .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(), - ), + .style(ButtonStyle::Outlined) + .on_click( + move |_, window, cx| { + window.dispatch_action( + Box::new( + zed_actions::encodings_ui::OpenWithEncoding( + path0.clone(), ), - cx, - ); - }, - ), + ), + cx, + ) + }, ), + ), ) }), ), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8fc2432429f6b9f08ede6453a8772f87431b6de8..b05c69799434a428ae69c5003b76150e0b1f0947 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 encodings::Encoding; pub use path_list::PathList; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; @@ -32,7 +31,6 @@ use client::{ }; use collections::{HashMap, HashSet, hash_map}; use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE}; -use encodings::EncodingOptions; use futures::{ Future, FutureExt, StreamExt, @@ -625,7 +623,6 @@ type BuildProjectItemForPathFn = fn( &Entity, &ProjectPath, - Option, &mut Window, &mut App, ) -> Option, WorkspaceItemBuilder)>>>; @@ -647,11 +644,8 @@ impl ProjectItemRegistry { }, ); self.build_project_item_for_path_fns - .push(|project, project_path, encoding, window, cx| { + .push(|project, project_path, window, cx| { let project_path = project_path.clone(); - let encoding = encoding.unwrap_or_default(); - - project.update(cx, |project, _| project.encoding_options.encoding.set(encoding.get())); let is_file = project .read(cx) @@ -721,17 +715,14 @@ 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, encoding.clone(), 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, window, cx)) else { return Task::ready(Err(anyhow!("cannot open file {:?}", path.path))); }; @@ -1191,7 +1182,6 @@ pub struct Workspace { session_id: Option, scheduled_tasks: Vec>, last_open_dock_positions: Vec, - pub encoding_options: EncodingOptions, } impl EventEmitter for Workspace {} @@ -1534,7 +1524,6 @@ impl Workspace { session_id: Some(session_id), scheduled_tasks: Vec::new(), last_open_dock_positions: Vec::new(), - encoding_options: Default::default(), } } @@ -1945,10 +1934,6 @@ 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() { pane.update(cx, |pane, cx| { window.focus(&pane.focus_handle(cx)); @@ -3578,25 +3563,8 @@ 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::Acquire), - std::sync::atomic::Ordering::Release, - ); - }); - let registry = cx.default_global::().clone(); - registry.open_path( - project, - &path, - Some((*self.encoding_options.encoding).clone()), - window, - cx, - ) + registry.open_path(&self.project, &path, window, cx) } pub fn find_project_item( @@ -7623,8 +7591,6 @@ pub fn create_and_open_local_file( fs.save( path, &default_content(cx), - - Default::default(), Default::default(), ) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index b4ceceda3f6f32849e414511adcfca2d3dd6c20c..66878f116d0e18b1a1516cd777036b08784d03dd 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -100,6 +100,7 @@ pub enum CreatedEntry { pub struct LoadedFile { pub file: Arc, + pub encoding: Encoding, pub text: String, } @@ -708,11 +709,10 @@ impl Worktree { &self, path: &RelPath, options: &EncodingOptions, - buffer_encoding: Option>, cx: &Context, ) -> Task> { match self { - Worktree::Local(this) => this.load_file(path, options, buffer_encoding, cx), + Worktree::Local(this) => this.load_file(path, options, cx), Worktree::Remote(_) => { Task::ready(Err(anyhow!("remote worktrees can't yet load files"))) } @@ -741,7 +741,7 @@ impl Worktree { cx: &Context, ) -> Task>> { match self { - Worktree::Local(this) => this.write_file(path, text, line_ending, cx, encoding), + Worktree::Local(this) => this.write_file(path, text, line_ending, encoding, cx), Worktree::Remote(_) => { Task::ready(Err(anyhow!("remote worktree can't yet write files"))) } @@ -1311,7 +1311,6 @@ impl LocalWorktree { }, is_local: true, is_private, - encoding: None, }) } }; @@ -1324,7 +1323,6 @@ impl LocalWorktree { &self, path: &RelPath, options: &EncodingOptions, - buffer_encoding: Option>, cx: &Context, ) -> Task> { let path = Arc::from(path); @@ -1333,7 +1331,6 @@ impl LocalWorktree { 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 { @@ -1351,9 +1348,7 @@ impl LocalWorktree { anyhow::bail!("File is too large to load"); } } - let text = fs - .load_with_encoding(&abs_path, &options, buffer_encoding.clone()) - .await?; + let (encoding, text) = fs.load_with_encoding(&abs_path, &options).await?; let worktree = this.upgrade().context("worktree was dropped")?; let file = match entry.await? { @@ -1377,12 +1372,15 @@ impl LocalWorktree { }, is_local: true, is_private, - encoding: Some(encoding), }) } }; - Ok(LoadedFile { file, text }) + Ok(LoadedFile { + file, + encoding, + text, + }) }) } @@ -1465,8 +1463,8 @@ impl LocalWorktree { path: Arc, text: Rope, line_ending: LineEnding, - cx: &Context, encoding: Encoding, + cx: &Context, ) -> Task>> { let fs = self.fs.clone(); let is_private = self.is_path_private(&path); @@ -1512,7 +1510,6 @@ impl LocalWorktree { entry_id: None, is_local: true, is_private, - encoding: Some(Arc::new(encoding)), })) } }) @@ -3066,7 +3063,7 @@ impl fmt::Debug for Snapshot { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct File { pub worktree: Entity, pub path: Arc, @@ -3074,35 +3071,8 @@ 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.get() != other_encoding.get() { - 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 } @@ -3149,13 +3119,6 @@ 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 - } - } } impl language::LocalFile for File { @@ -3163,30 +3126,11 @@ impl language::LocalFile for File { self.worktree.read(cx).absolutize(&self.path) } - fn load( - &self, - cx: &App, - options: &EncodingOptions, - buffer_encoding: Option>, - ) -> Task> { + fn load(&self, cx: &App, encoding: EncodingOptions) -> 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, &options, buffer_encoding) - .await - }) + cx.background_spawn(async move { fs.load_with_encoding(&abs_path, &encoding).await }) } fn load_bytes(&self, cx: &App) -> Task>> { @@ -3210,7 +3154,6 @@ impl File { entry_id: Some(entry.id), is_local: true, is_private: entry.is_private, - encoding: None, }) } @@ -3241,7 +3184,6 @@ impl File { entry_id: proto.entry_id.map(ProjectEntryId::from_proto), is_local: false, is_private: false, - encoding: None, }) } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 673764bbba9ca2fbca70655c96d7fe426fc83111..7ec4f9dfe3a33c1b921641340590e550aff641c0 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -470,7 +470,6 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) { tree.load_file( rel_path("one/node_modules/b/b1.js"), &Default::default(), - None, cx, ) }) @@ -515,7 +514,6 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) { tree.load_file( rel_path("one/node_modules/a/a2.js"), &Default::default(), - None, cx, ) }) @@ -1781,8 +1779,8 @@ fn randomly_mutate_worktree( entry.path.clone(), Rope::default(), Default::default(), - cx, Default::default(), + cx, ); cx.background_spawn(async move { task.await?; diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 134c0eb452fb52c8c2aa5b8ce18ebc6fc85560a8..b564dc697b0cbc4b9c2cbc3a980ba7fea646fb27 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -443,9 +443,8 @@ pub fn initialize_workspace( } }); - let encoding_indicator = cx.new(|_cx| { - encodings_ui::EncodingIndicator::new(None, workspace.weak_handle(), None, None) - }); + let encoding_indicator = + cx.new(|_cx| encodings_ui::EncodingIndicator::new(workspace.weak_handle())); let cursor_position = cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace)); diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 5c60efd96f6d9f335c29d399fcfbea07db5a2028..7cad32815177b959564ba5da93edc52e9aa86a62 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -307,7 +307,7 @@ pub mod encodings_ui { use serde::Deserialize; #[derive(PartialEq, Debug, Clone, Action, JsonSchema, Deserialize)] - pub struct Toggle(pub Arc); + pub struct OpenWithEncoding(pub Arc); #[derive(PartialEq, Debug, Clone, Action, JsonSchema, Deserialize)] pub struct ForceOpen(pub Arc); diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index d95e62be5e4ceb42b68c43994229f33d50eac809..2deefdd0025263057df8df1ae2041dc2a1987c9b 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"), &Default::default(), None, cx) + worktree2.load_file(rel_path("main.rs"), &Default::default(), cx) }) }) .await