linux: Implement local time zone support (#14610)

apricotbucket28 created

I decided to remove the GPUI APIs since `chrono` already provides this
functionality, and is already been used for this purpose in other parts
of the code (e.g.
[here](https://github.com/zed-industries/zed/blob/80402a684066628793695d9303979ad993803740/crates/zed/src/main.rs#L756)
or
[here](https://github.com/zed-industries/zed/blob/80402a684066628793695d9303979ad993803740/crates/ui/src/utils/format_distance.rs#L258))

These usages end up calling the `time_format` crate, which takes in a
`UtcOffset`. It's probably cleaner to rewrite the crate to take in
`chrono` types, but that would require rewriting most of the code there.

Release Notes:

- linux: Use local time zone in chat and Git blame

Change summary

Cargo.lock                                   |  2 +
Cargo.toml                                   |  1 
crates/collab_ui/Cargo.toml                  |  1 
crates/collab_ui/src/chat_panel.rs           |  3 +
crates/collab_ui/src/notification_panel.rs   |  3 +
crates/editor/Cargo.toml                     |  1 
crates/editor/src/blame_entry_tooltip.rs     | 36 +++++++++------------
crates/editor/src/element.rs                 |  4 +-
crates/gpui/src/app.rs                       |  6 ---
crates/gpui/src/platform.rs                  |  2 -
crates/gpui/src/platform/linux/platform.rs   |  5 ---
crates/gpui/src/platform/mac/platform.rs     |  9 -----
crates/gpui/src/platform/test/platform.rs    |  4 --
crates/gpui/src/platform/windows/platform.rs | 21 ------------
14 files changed, 26 insertions(+), 72 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2564,6 +2564,7 @@ dependencies = [
  "anyhow",
  "call",
  "channel",
+ "chrono",
  "client",
  "collections",
  "db",
@@ -3593,6 +3594,7 @@ dependencies = [
  "aho-corasick",
  "anyhow",
  "assets",
+ "chrono",
  "client",
  "clock",
  "collections",

Cargo.toml 🔗

@@ -457,7 +457,6 @@ features = [
     "Win32_System_SystemInformation",
     "Win32_System_SystemServices",
     "Win32_System_Threading",
-    "Win32_System_Time",
     "Win32_System_WinRT",
     "Win32_UI_Controls",
     "Win32_UI_HiDpi",

crates/collab_ui/Cargo.toml 🔗

@@ -32,6 +32,7 @@ test-support = [
 anyhow.workspace = true
 call.workspace = true
 channel.workspace = true
+chrono.workspace = true
 client.workspace = true
 collections.workspace = true
 db.workspace = true

crates/collab_ui/src/chat_panel.rs 🔗

@@ -111,6 +111,7 @@ impl ChatPanel {
                 this.is_scrolled_to_bottom = !event.is_scrolled;
             }));
 
+            let local_offset = chrono::Local::now().offset().local_minus_utc();
             let mut this = Self {
                 fs,
                 client,
@@ -120,7 +121,7 @@ impl ChatPanel {
                 active_chat: Default::default(),
                 pending_serialization: Task::ready(None),
                 message_editor: input_editor,
-                local_timezone: cx.local_timezone(),
+                local_timezone: UtcOffset::from_whole_seconds(local_offset).unwrap(),
                 subscriptions: Vec::new(),
                 is_scrolled_to_bottom: true,
                 active: false,

crates/collab_ui/src/notification_panel.rs 🔗

@@ -127,11 +127,12 @@ impl NotificationPanel {
                 },
             ));
 
+            let local_offset = chrono::Local::now().offset().local_minus_utc();
             let mut this = Self {
                 fs,
                 client,
                 user_store,
-                local_timezone: cx.local_timezone(),
+                local_timezone: UtcOffset::from_whole_seconds(local_offset).unwrap(),
                 channel_store: ChannelStore::global(cx),
                 notification_store: NotificationStore::global(cx),
                 notification_list,

crates/editor/Cargo.toml 🔗

@@ -31,6 +31,7 @@ test-support = [
 aho-corasick = "1.1"
 anyhow.workspace = true
 assets.workspace = true
+chrono.workspace = true
 client.workspace = true
 clock.workspace = true
 collections.workspace = true

crates/editor/src/blame_entry_tooltip.rs 🔗

@@ -8,6 +8,7 @@ use gpui::{
 use settings::Settings;
 use std::hash::Hash;
 use theme::{ActiveTheme, ThemeSettings};
+use time::UtcOffset;
 use ui::{
     div, h_flex, tooltip_container, v_flex, Avatar, Button, ButtonStyle, Clickable as _, Color,
     FluentBuilder, Icon, IconName, IconPosition, InteractiveElement as _, IntoElement,
@@ -129,7 +130,7 @@ impl Render for BlameEntryTooltip {
         let author_email = self.blame_entry.author_mail.clone();
 
         let short_commit_id = self.blame_entry.sha.display_short();
-        let absolute_timestamp = blame_entry_absolute_timestamp(&self.blame_entry, cx);
+        let absolute_timestamp = blame_entry_absolute_timestamp(&self.blame_entry);
 
         let message = self
             .details
@@ -247,30 +248,25 @@ impl Render for BlameEntryTooltip {
     }
 }
 
-fn blame_entry_timestamp(
-    blame_entry: &BlameEntry,
-    format: time_format::TimestampFormat,
-    cx: &WindowContext,
-) -> String {
+fn blame_entry_timestamp(blame_entry: &BlameEntry, format: time_format::TimestampFormat) -> String {
     match blame_entry.author_offset_date_time() {
-        Ok(timestamp) => time_format::format_localized_timestamp(
-            timestamp,
-            time::OffsetDateTime::now_utc(),
-            cx.local_timezone(),
-            format,
-        ),
+        Ok(timestamp) => {
+            let local = chrono::Local::now().offset().local_minus_utc();
+            time_format::format_localized_timestamp(
+                timestamp,
+                time::OffsetDateTime::now_utc(),
+                UtcOffset::from_whole_seconds(local).unwrap(),
+                format,
+            )
+        }
         Err(_) => "Error parsing date".to_string(),
     }
 }
 
-pub fn blame_entry_relative_timestamp(blame_entry: &BlameEntry, cx: &WindowContext) -> String {
-    blame_entry_timestamp(blame_entry, time_format::TimestampFormat::Relative, cx)
+pub fn blame_entry_relative_timestamp(blame_entry: &BlameEntry) -> String {
+    blame_entry_timestamp(blame_entry, time_format::TimestampFormat::Relative)
 }
 
-fn blame_entry_absolute_timestamp(blame_entry: &BlameEntry, cx: &WindowContext) -> String {
-    blame_entry_timestamp(
-        blame_entry,
-        time_format::TimestampFormat::MediumAbsolute,
-        cx,
-    )
+fn blame_entry_absolute_timestamp(blame_entry: &BlameEntry) -> String {
+    blame_entry_timestamp(blame_entry, time_format::TimestampFormat::MediumAbsolute)
 }

crates/editor/src/element.rs 🔗

@@ -3923,7 +3923,7 @@ fn render_inline_blame_entry(
     workspace: Option<WeakView<Workspace>>,
     cx: &mut WindowContext<'_>,
 ) -> AnyElement {
-    let relative_timestamp = blame_entry_relative_timestamp(&blame_entry, cx);
+    let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
 
     let author = blame_entry.author.as_deref().unwrap_or_default();
     let text = format!("{}, {}", author, relative_timestamp);
@@ -3969,7 +3969,7 @@ fn render_blame_entry(
     };
     last_used_color.replace((sha_color, blame_entry.sha));
 
-    let relative_timestamp = blame_entry_relative_timestamp(&blame_entry, cx);
+    let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
 
     let short_commit_id = blame_entry.sha.display_short();
 

crates/gpui/src/app.rs 🔗

@@ -14,7 +14,6 @@ use derive_more::{Deref, DerefMut};
 use futures::{channel::oneshot, future::LocalBoxFuture, Future};
 use slotmap::SlotMap;
 use smol::future::FutureExt;
-use time::UtcOffset;
 
 pub use async_context::*;
 use collections::{FxHashMap, FxHashSet, VecDeque};
@@ -647,11 +646,6 @@ impl AppContext {
         self.platform.restart(binary_path)
     }
 
-    /// Returns the local timezone at the platform level.
-    pub fn local_timezone(&self) -> UtcOffset {
-        self.platform.local_timezone()
-    }
-
     /// Updates the http client assigned to GPUI
     pub fn update_http_client(&mut self, new_client: Arc<dyn HttpClient>) {
         self.http_client = new_client;

crates/gpui/src/platform.rs 🔗

@@ -60,7 +60,6 @@ pub(crate) use mac::*;
 pub use semantic_version::SemanticVersion;
 #[cfg(any(test, feature = "test-support"))]
 pub(crate) use test::*;
-use time::UtcOffset;
 #[cfg(target_os = "windows")]
 pub(crate) use windows::*;
 
@@ -159,7 +158,6 @@ pub(crate) trait Platform: 'static {
         ""
     }
     fn app_path(&self) -> Result<PathBuf>;
-    fn local_timezone(&self) -> UtcOffset;
     fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
 
     fn set_cursor_style(&self, style: CursorStyle);

crates/gpui/src/platform/linux/platform.rs 🔗

@@ -30,7 +30,6 @@ use filedescriptor::FileDescriptor;
 use flume::{Receiver, Sender};
 use futures::channel::oneshot;
 use parking_lot::Mutex;
-use time::UtcOffset;
 use util::ResultExt;
 use wayland_client::Connection;
 use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
@@ -397,10 +396,6 @@ impl<P: LinuxClient + 'static> Platform for P {
 
     fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap) {}
 
-    fn local_timezone(&self) -> UtcOffset {
-        UtcOffset::UTC
-    }
-
     fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
         Err(anyhow::Error::msg(
             "Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet",

crates/gpui/src/platform/mac/platform.rs 🔗

@@ -49,7 +49,6 @@ use std::{
     slice, str,
     sync::Arc,
 };
-use time::UtcOffset;
 
 use super::renderer;
 
@@ -760,14 +759,6 @@ impl Platform for MacPlatform {
         }
     }
 
-    fn local_timezone(&self) -> UtcOffset {
-        unsafe {
-            let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone];
-            let seconds_from_gmt: NSInteger = msg_send![local_timezone, secondsFromGMT];
-            UtcOffset::from_whole_seconds(seconds_from_gmt.try_into().unwrap()).unwrap()
-        }
-    }
-
     fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
         unsafe {
             let bundle: id = NSBundle::mainBundle();

crates/gpui/src/platform/test/platform.rs 🔗

@@ -257,10 +257,6 @@ impl Platform for TestPlatform {
         unimplemented!()
     }
 
-    fn local_timezone(&self) -> time::UtcOffset {
-        time::UtcOffset::UTC
-    }
-
     fn path_for_auxiliary_executable(&self, _name: &str) -> Result<std::path::PathBuf> {
         unimplemented!()
     }

crates/gpui/src/platform/windows/platform.rs 🔗

@@ -16,7 +16,6 @@ use futures::channel::oneshot::{self, Receiver};
 use itertools::Itertools;
 use parking_lot::RwLock;
 use smallvec::SmallVec;
-use time::UtcOffset;
 use windows::{
     core::*,
     Win32::{
@@ -35,7 +34,6 @@ use windows::{
             Ole::*,
             SystemInformation::*,
             Threading::*,
-            Time::*,
         },
         UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*},
     },
@@ -482,25 +480,6 @@ impl Platform for WindowsPlatform {
         Ok(std::env::current_exe()?)
     }
 
-    fn local_timezone(&self) -> UtcOffset {
-        let mut info = unsafe { std::mem::zeroed() };
-        let ret = unsafe { GetTimeZoneInformation(&mut info) };
-        if ret == TIME_ZONE_ID_INVALID {
-            log::error!(
-                "Unable to get local timezone: {}",
-                std::io::Error::last_os_error()
-            );
-            return UtcOffset::UTC;
-        }
-        // Windows treat offset as:
-        // UTC = localtime + offset
-        // so we add a minus here
-        let hours = -info.Bias / 60;
-        let minutes = -info.Bias % 60;
-
-        UtcOffset::from_hms(hours as _, minutes as _, 0).unwrap()
-    }
-
     // todo(windows)
     fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
         Err(anyhow!("not yet implemented"))