Merge pull request #757 from zed-industries/restructure-settings

Max Brunsfeld created

Enable language specific tab sizes

Change summary

Cargo.lock                                    |  34 +
crates/breadcrumbs/Cargo.toml                 |   1 
crates/breadcrumbs/src/breadcrumbs.rs         |   3 
crates/chat_panel/Cargo.toml                  |   1 
crates/chat_panel/src/chat_panel.rs           |   2 
crates/contacts_panel/Cargo.toml              |   1 
crates/contacts_panel/src/contacts_panel.rs   |   3 
crates/diagnostics/Cargo.toml                 |   1 
crates/diagnostics/src/diagnostics.rs         |   3 
crates/diagnostics/src/items.rs               |   3 
crates/editor/Cargo.toml                      |   3 
crates/editor/src/display_map.rs              |  97 ++--
crates/editor/src/display_map/block_map.rs    |  17 
crates/editor/src/display_map/fold_map.rs     |   5 
crates/editor/src/display_map/tab_map.rs      |  27 
crates/editor/src/display_map/wrap_map.rs     |   7 
crates/editor/src/editor.rs                   | 434 +++++++++++++++-----
crates/editor/src/element.rs                  |   2 
crates/editor/src/items.rs                    |   5 
crates/editor/src/movement.rs                 |  11 
crates/editor/src/multi_buffer.rs             |  48 +
crates/editor/src/test.rs                     |  25 +
crates/file_finder/Cargo.toml                 |   1 
crates/file_finder/src/file_finder.rs         |   3 
crates/go_to_line/Cargo.toml                  |   1 
crates/go_to_line/src/go_to_line.rs           |   3 
crates/gpui/src/app.rs                        |  15 
crates/language/src/buffer.rs                 |  67 +-
crates/language/src/tests.rs                  |  18 
crates/outline/Cargo.toml                     |   1 
crates/outline/src/outline.rs                 |   3 
crates/project/Cargo.toml                     |   1 
crates/project/src/project.rs                 |   7 
crates/project_panel/Cargo.toml               |   1 
crates/project_panel/src/project_panel.rs     |   3 
crates/project_symbols/Cargo.toml             |   1 
crates/project_symbols/src/project_symbols.rs |   3 
crates/search/Cargo.toml                      |   1 
crates/search/src/buffer_search.rs            |   3 
crates/search/src/project_search.rs           |   5 
crates/server/Cargo.toml                      |   1 
crates/server/src/rpc.rs                      |   3 
crates/settings/Cargo.toml                    |  22 +
crates/settings/src/settings.rs               | 171 ++++++++
crates/theme_selector/Cargo.toml              |   1 
crates/theme_selector/src/theme_selector.rs   |   3 
crates/vim/Cargo.toml                         |   4 
crates/vim/src/vim.rs                         |   3 
crates/workspace/Cargo.toml                   |   3 
crates/workspace/src/lsp_status.rs            |   3 
crates/workspace/src/pane.rs                  |   3 
crates/workspace/src/settings.rs              | 325 ---------------
crates/workspace/src/status_bar.rs            |   3 
crates/workspace/src/toolbar.rs               |   3 
crates/workspace/src/workspace.rs             |   3 
crates/zed/Cargo.toml                         |   2 
crates/zed/src/main.rs                        |  30 +
crates/zed/src/settings_file.rs               | 157 +++++++
crates/zed/src/test.rs                        |   2 
crates/zed/src/zed.rs                         |  12 
styles/src/buildThemes.ts                     |  14 
styles/src/buildTokens.ts                     | 148 +++---
styles/src/styleTree/app.ts                   |  54 +-
styles/src/styleTree/chatPanel.ts             | 194 ++++----
styles/src/styleTree/components.ts            | 106 ++--
styles/src/styleTree/contactsPanel.ts         | 108 ++--
styles/src/styleTree/editor.ts                | 272 ++++++------
styles/src/styleTree/projectPanel.ts          |  56 +-
styles/src/styleTree/search.ts                | 144 +++---
styles/src/styleTree/selectorModal.ts         | 104 ++--
styles/src/styleTree/workspace.ts             | 280 ++++++------
styles/src/themes/dark.ts                     | 394 +++++++++---------
styles/src/themes/light.ts                    | 390 +++++++++---------
styles/src/themes/theme.ts                    | 240 +++++-----
styles/src/tokens.ts                          |   2 
styles/src/utils/color.ts                     |  70 +-
styles/src/utils/snakeCase.ts                 |  36 
77 files changed, 2,320 insertions(+), 1,916 deletions(-)

Detailed changes

Cargo.lock šŸ”—

@@ -729,6 +729,7 @@ dependencies = [
  "language",
  "project",
  "search",
+ "settings",
  "theme",
  "workspace",
 ]
@@ -881,6 +882,7 @@ dependencies = [
  "editor",
  "gpui",
  "postage",
+ "settings",
  "theme",
  "time 0.3.7",
  "util",
@@ -1131,6 +1133,7 @@ dependencies = [
  "client",
  "gpui",
  "postage",
+ "settings",
  "theme",
  "workspace",
 ]
@@ -1480,6 +1483,7 @@ dependencies = [
  "postage",
  "project",
  "serde_json",
+ "settings",
  "theme",
  "unindent",
  "util",
@@ -1634,6 +1638,7 @@ dependencies = [
  "futures",
  "fuzzy",
  "gpui",
+ "indoc",
  "itertools",
  "language",
  "lazy_static",
@@ -1646,6 +1651,7 @@ dependencies = [
  "rand 0.8.3",
  "rpc",
  "serde",
+ "settings",
  "smallvec",
  "smol",
  "snippet",
@@ -1806,6 +1812,7 @@ dependencies = [
  "postage",
  "project",
  "serde_json",
+ "settings",
  "theme",
  "util",
  "workspace",
@@ -2206,6 +2213,7 @@ dependencies = [
  "editor",
  "gpui",
  "postage",
+ "settings",
  "text",
  "workspace",
 ]
@@ -3255,6 +3263,7 @@ dependencies = [
  "language",
  "ordered-float",
  "postage",
+ "settings",
  "smol",
  "text",
  "workspace",
@@ -3624,6 +3633,7 @@ dependencies = [
  "rpc",
  "serde",
  "serde_json",
+ "settings",
  "sha2 0.10.2",
  "similar",
  "smol",
@@ -3643,6 +3653,7 @@ dependencies = [
  "postage",
  "project",
  "serde_json",
+ "settings",
  "theme",
  "util",
  "workspace",
@@ -3659,6 +3670,7 @@ dependencies = [
  "ordered-float",
  "postage",
  "project",
+ "settings",
  "smol",
  "text",
  "util",
@@ -4258,6 +4270,7 @@ dependencies = [
  "postage",
  "project",
  "serde_json",
+ "settings",
  "theme",
  "unindent",
  "util",
@@ -4406,6 +4419,21 @@ dependencies = [
  "pkg-config",
 ]
 
+[[package]]
+name = "settings"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "gpui",
+ "schemars",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "theme",
+ "toml",
+ "util",
+]
+
 [[package]]
 name = "sha-1"
 version = "0.8.2"
@@ -5142,6 +5170,7 @@ dependencies = [
  "log",
  "parking_lot",
  "postage",
+ "settings",
  "smol",
  "theme",
  "workspace",
@@ -5719,6 +5748,7 @@ dependencies = [
  "language",
  "log",
  "project",
+ "settings",
  "util",
  "workspace",
 ]
@@ -5948,9 +5978,9 @@ dependencies = [
  "parking_lot",
  "postage",
  "project",
- "schemars",
  "serde",
  "serde_json",
+ "settings",
  "smallvec",
  "theme",
  "util",
@@ -6034,6 +6064,7 @@ dependencies = [
  "serde",
  "serde_json",
  "serde_path_to_error",
+ "settings",
  "simplelog",
  "smallvec",
  "smol",
@@ -6099,6 +6130,7 @@ dependencies = [
  "scrypt",
  "serde",
  "serde_json",
+ "settings",
  "sha-1 0.9.6",
  "sqlx 0.5.5",
  "surf",

crates/breadcrumbs/Cargo.toml šŸ”—

@@ -14,6 +14,7 @@ gpui = { path = "../gpui" }
 language = { path = "../language" }
 project = { path = "../project" }
 search = { path = "../search" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }
 

crates/breadcrumbs/src/breadcrumbs.rs šŸ”—

@@ -6,8 +6,9 @@ use gpui::{
 use language::{Buffer, OutlineItem};
 use project::Project;
 use search::ProjectSearchView;
+use settings::Settings;
 use theme::SyntaxTheme;
-use workspace::{ItemHandle, Settings, ToolbarItemLocation, ToolbarItemView};
+use workspace::{ItemHandle, ToolbarItemLocation, ToolbarItemView};
 
 pub enum Event {
     UpdateLocation,

crates/chat_panel/Cargo.toml šŸ”—

@@ -11,6 +11,7 @@ doctest = false
 client = { path = "../client" }
 editor = { path = "../editor" }
 gpui = { path = "../gpui" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }

crates/chat_panel/src/chat_panel.rs šŸ”—

@@ -13,10 +13,10 @@ use gpui::{
     ViewContext, ViewHandle,
 };
 use postage::prelude::Stream;
+use settings::{Settings, SoftWrap};
 use std::sync::Arc;
 use time::{OffsetDateTime, UtcOffset};
 use util::{ResultExt, TryFutureExt};
-use workspace::{settings::SoftWrap, Settings};
 
 const MESSAGE_LOADING_THRESHOLD: usize = 50;
 

crates/contacts_panel/Cargo.toml šŸ”—

@@ -10,6 +10,7 @@ doctest = false
 [dependencies]
 client = { path = "../client" }
 gpui = { path = "../gpui" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }
 postage = { version = "0.4.1", features = ["futures-traits"] }

crates/contacts_panel/src/contacts_panel.rs šŸ”—

@@ -8,7 +8,8 @@ use gpui::{
     Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, View,
     ViewContext,
 };
-use workspace::{AppState, JoinProject, JoinProjectParams, Settings};
+use workspace::{AppState, JoinProject, JoinProjectParams};
+use settings::Settings;
 
 pub struct ContactsPanel {
     contacts: ListState,

crates/diagnostics/Cargo.toml šŸ”—

@@ -14,6 +14,7 @@ editor = { path = "../editor" }
 language = { path = "../language" }
 gpui = { path = "../gpui" }
 project = { path = "../project" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }

crates/diagnostics/src/diagnostics.rs šŸ”—

@@ -25,7 +25,8 @@ use std::{
     sync::Arc,
 };
 use util::TryFutureExt;
-use workspace::{ItemHandle as _, ItemNavHistory, Settings, Workspace};
+use workspace::{ItemHandle as _, ItemNavHistory, Workspace};
+use settings::Settings;
 
 action!(Deploy);
 

crates/diagnostics/src/items.rs šŸ”—

@@ -3,7 +3,8 @@ use gpui::{
     elements::*, platform::CursorStyle, Entity, ModelHandle, RenderContext, View, ViewContext,
 };
 use project::Project;
-use workspace::{Settings, StatusItemView};
+use workspace::{StatusItemView};
+use settings::Settings;
 
 pub struct DiagnosticSummary {
     summary: project::DiagnosticSummary,

crates/editor/Cargo.toml šŸ”—

@@ -28,6 +28,7 @@ language = { path = "../language" }
 lsp = { path = "../lsp" }
 project = { path = "../project" }
 rpc = { path = "../rpc" }
+settings = { path = "../settings" }
 snippet = { path = "../snippet" }
 sum_tree = { path = "../sum_tree" }
 theme = { path = "../theme" }
@@ -36,6 +37,7 @@ workspace = { path = "../workspace" }
 aho-corasick = "0.7"
 anyhow = "1.0"
 futures = "0.3"
+indoc = "1.0.4"
 itertools = "0.10"
 lazy_static = "1.4"
 log = "0.4"
@@ -54,6 +56,7 @@ lsp = { path = "../lsp", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }
 ctor = "0.1"
 env_logger = "0.8"

crates/editor/src/display_map.rs šŸ”—

@@ -12,6 +12,7 @@ use gpui::{
     Entity, ModelContext, ModelHandle,
 };
 use language::{Point, Subscription as BufferSubscription};
+use settings::Settings;
 use std::{any::TypeId, fmt::Debug, ops::Range, sync::Arc};
 use sum_tree::{Bias, TreeMap};
 use tab_map::TabMap;
@@ -46,7 +47,6 @@ impl Entity for DisplayMap {
 impl DisplayMap {
     pub fn new(
         buffer: ModelHandle<MultiBuffer>,
-        tab_size: usize,
         font_id: FontId,
         font_size: f32,
         wrap_width: Option<f32>,
@@ -55,6 +55,8 @@ impl DisplayMap {
         cx: &mut ModelContext<Self>,
     ) -> Self {
         let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
+
+        let tab_size = Self::tab_size(&buffer, cx);
         let (fold_map, snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
         let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
         let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx);
@@ -76,7 +78,9 @@ impl DisplayMap {
         let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
         let edits = self.buffer_subscription.consume().into_inner();
         let (folds_snapshot, edits) = self.fold_map.read(buffer_snapshot, edits);
-        let (tabs_snapshot, edits) = self.tab_map.sync(folds_snapshot.clone(), edits);
+
+        let tab_size = Self::tab_size(&self.buffer, cx);
+        let (tabs_snapshot, edits) = self.tab_map.sync(folds_snapshot.clone(), edits, tab_size);
         let (wraps_snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(tabs_snapshot.clone(), edits, cx));
@@ -100,14 +104,15 @@ impl DisplayMap {
     ) {
         let snapshot = self.buffer.read(cx).snapshot(cx);
         let edits = self.buffer_subscription.consume().into_inner();
+        let tab_size = Self::tab_size(&self.buffer, cx);
         let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
-        let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
         self.block_map.read(snapshot, edits);
         let (snapshot, edits) = fold_map.fold(ranges);
-        let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -122,14 +127,15 @@ impl DisplayMap {
     ) {
         let snapshot = self.buffer.read(cx).snapshot(cx);
         let edits = self.buffer_subscription.consume().into_inner();
+        let tab_size = Self::tab_size(&self.buffer, cx);
         let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
-        let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
         self.block_map.read(snapshot, edits);
         let (snapshot, edits) = fold_map.unfold(ranges, inclusive);
-        let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -143,8 +149,9 @@ impl DisplayMap {
     ) -> Vec<BlockId> {
         let snapshot = self.buffer.read(cx).snapshot(cx);
         let edits = self.buffer_subscription.consume().into_inner();
+        let tab_size = Self::tab_size(&self.buffer, cx);
         let (snapshot, edits) = self.fold_map.read(snapshot, edits);
-        let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -159,8 +166,9 @@ impl DisplayMap {
     pub fn remove_blocks(&mut self, ids: HashSet<BlockId>, cx: &mut ModelContext<Self>) {
         let snapshot = self.buffer.read(cx).snapshot(cx);
         let edits = self.buffer_subscription.consume().into_inner();
+        let tab_size = Self::tab_size(&self.buffer, cx);
         let (snapshot, edits) = self.fold_map.read(snapshot, edits);
-        let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -195,6 +203,16 @@ impl DisplayMap {
             .update(cx, |map, cx| map.set_wrap_width(width, cx))
     }
 
+    fn tab_size(buffer: &ModelHandle<MultiBuffer>, cx: &mut ModelContext<Self>) -> u32 {
+        let language_name = buffer
+            .read(cx)
+            .as_singleton()
+            .and_then(|buffer| buffer.read(cx).language())
+            .map(|language| language.name());
+
+        cx.global::<Settings>().tab_size(language_name.as_deref())
+    }
+
     #[cfg(test)]
     pub fn is_rewrapping(&self, cx: &gpui::AppContext) -> bool {
         self.wrap_map.read(cx).is_rewrapping()
@@ -536,6 +554,8 @@ pub mod tests {
         log::info!("tab size: {}", tab_size);
         log::info!("wrap width: {:?}", wrap_width);
 
+        cx.update(|cx| cx.set_global(Settings::test(cx)));
+
         let buffer = cx.update(|cx| {
             if rng.gen() {
                 let len = rng.gen_range(0..10);
@@ -549,7 +569,6 @@ pub mod tests {
         let map = cx.add_model(|cx| {
             DisplayMap::new(
                 buffer.clone(),
-                tab_size,
                 font_id,
                 font_size,
                 wrap_width,
@@ -759,27 +778,18 @@ pub mod tests {
 
         let font_cache = cx.font_cache();
 
-        let tab_size = 4;
         let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
         let font_size = 12.0;
         let wrap_width = Some(64.);
+        cx.set_global(Settings::test(cx));
 
         let text = "one two three four five\nsix seven eight";
         let buffer = MultiBuffer::build_simple(text, cx);
         let map = cx.add_model(|cx| {
-            DisplayMap::new(
-                buffer.clone(),
-                tab_size,
-                font_id,
-                font_size,
-                wrap_width,
-                1,
-                1,
-                cx,
-            )
+            DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx)
         });
 
         let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
@@ -847,18 +857,17 @@ pub mod tests {
 
     #[gpui::test]
     fn test_text_chunks(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         let text = sample_text(6, 6, 'a');
         let buffer = MultiBuffer::build_simple(&text, cx);
-        let tab_size = 4;
         let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
         let font_id = cx
             .font_cache()
             .select_font(family_id, &Default::default())
             .unwrap();
         let font_size = 14.0;
-        let map = cx.add_model(|cx| {
-            DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx)
-        });
+        let map =
+            cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
         buffer.update(cx, |buffer, cx| {
             buffer.edit(
                 vec![
@@ -923,12 +932,17 @@ pub mod tests {
             .unwrap(),
         );
         language.set_theme(&theme);
+        cx.update(|cx| {
+            cx.set_global(Settings {
+                tab_size: 2,
+                ..Settings::test(cx)
+            })
+        });
 
         let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
         buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
 
-        let tab_size = 2;
         let font_cache = cx.font_cache();
         let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
         let font_id = font_cache
@@ -936,8 +950,7 @@ pub mod tests {
             .unwrap();
         let font_size = 14.0;
 
-        let map = cx
-            .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
+        let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
         assert_eq!(
             cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
             vec![
@@ -1011,22 +1024,22 @@ pub mod tests {
         );
         language.set_theme(&theme);
 
+        cx.update(|cx| cx.set_global(Settings::test(cx)));
+
         let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
         buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
 
         let font_cache = cx.font_cache();
 
-        let tab_size = 4;
         let family_id = font_cache.load_family(&["Courier"]).unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
         let font_size = 16.0;
 
-        let map = cx.add_model(|cx| {
-            DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), 1, 1, cx)
-        });
+        let map =
+            cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, Some(40.0), 1, 1, cx));
         assert_eq!(
             cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
             [
@@ -1058,6 +1071,7 @@ pub mod tests {
     async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
         cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
 
+        cx.update(|cx| cx.set_global(Settings::test(cx)));
         let theme = SyntaxTheme::new(vec![
             ("operator".to_string(), Color::red().into()),
             ("string".to_string(), Color::green().into()),
@@ -1090,14 +1104,12 @@ pub mod tests {
         let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
 
         let font_cache = cx.font_cache();
-        let tab_size = 4;
         let family_id = font_cache.load_family(&["Courier"]).unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
         let font_size = 16.0;
-        let map = cx
-            .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
+        let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
 
         enum MyType {}
 
@@ -1136,6 +1148,7 @@ pub mod tests {
 
     #[gpui::test]
     fn test_clip_point(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::MutableAppContext) {
             let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx);
 
@@ -1187,6 +1200,8 @@ pub mod tests {
 
     #[gpui::test]
     fn test_clip_at_line_ends(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
+
         fn assert(text: &str, cx: &mut gpui::MutableAppContext) {
             let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx);
             unmarked_snapshot.clip_at_line_ends = true;
@@ -1204,9 +1219,9 @@ pub mod tests {
 
     #[gpui::test]
     fn test_tabs_with_multibyte_chars(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         let text = "āœ…\t\tα\nβ\t\nšŸ€Ī²\t\tγ";
         let buffer = MultiBuffer::build_simple(text, cx);
-        let tab_size = 4;
         let font_cache = cx.font_cache();
         let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
         let font_id = font_cache
@@ -1214,9 +1229,8 @@ pub mod tests {
             .unwrap();
         let font_size = 14.0;
 
-        let map = cx.add_model(|cx| {
-            DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx)
-        });
+        let map =
+            cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
         let map = map.update(cx, |map, cx| map.snapshot(cx));
         assert_eq!(map.text(), "āœ…       α\nβ   \nšŸ€Ī²      γ");
         assert_eq!(
@@ -1264,17 +1278,16 @@ pub mod tests {
 
     #[gpui::test]
     fn test_max_point(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
-        let tab_size = 4;
         let font_cache = cx.font_cache();
         let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
         let font_size = 14.0;
-        let map = cx.add_model(|cx| {
-            DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx)
-        });
+        let map =
+            cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
         assert_eq!(
             map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
             DisplayPoint::new(1, 11)

crates/editor/src/display_map/block_map.rs šŸ”—

@@ -969,6 +969,7 @@ mod tests {
     use crate::multi_buffer::MultiBuffer;
     use gpui::{elements::Empty, Element};
     use rand::prelude::*;
+    use settings::Settings;
     use std::env;
     use text::RandomCharIter;
 
@@ -988,6 +989,8 @@ mod tests {
 
     #[gpui::test]
     fn test_basic_blocks(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
+
         let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
         let font_id = cx
             .font_cache()
@@ -1157,7 +1160,7 @@ mod tests {
 
         let (folds_snapshot, fold_edits) =
             fold_map.read(buffer_snapshot, subscription.consume().into_inner());
-        let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+        let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits, 4);
         let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
             wrap_map.sync(tabs_snapshot, tab_edits, cx)
         });
@@ -1167,6 +1170,8 @@ mod tests {
 
     #[gpui::test]
     fn test_blocks_on_wrapped_lines(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
+
         let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
         let font_id = cx
             .font_cache()
@@ -1209,6 +1214,8 @@ mod tests {
 
     #[gpui::test(iterations = 100)]
     fn test_random_blocks(cx: &mut gpui::MutableAppContext, mut rng: StdRng) {
+        cx.set_global(Settings::test(cx));
+
         let operations = env::var("OPERATIONS")
             .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
             .unwrap_or(10);
@@ -1296,7 +1303,8 @@ mod tests {
 
                     let (folds_snapshot, fold_edits) =
                         fold_map.read(buffer_snapshot.clone(), vec![]);
-                    let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+                    let (tabs_snapshot, tab_edits) =
+                        tab_map.sync(folds_snapshot, fold_edits, tab_size);
                     let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
                         wrap_map.sync(tabs_snapshot, tab_edits, cx)
                     });
@@ -1318,7 +1326,8 @@ mod tests {
 
                     let (folds_snapshot, fold_edits) =
                         fold_map.read(buffer_snapshot.clone(), vec![]);
-                    let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+                    let (tabs_snapshot, tab_edits) =
+                        tab_map.sync(folds_snapshot, fold_edits, tab_size);
                     let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
                         wrap_map.sync(tabs_snapshot, tab_edits, cx)
                     });
@@ -1338,7 +1347,7 @@ mod tests {
             }
 
             let (folds_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
-            let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+            let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits, tab_size);
             let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
                 wrap_map.sync(tabs_snapshot, tab_edits, cx)
             });

crates/editor/src/display_map/fold_map.rs šŸ”—

@@ -1210,6 +1210,7 @@ mod tests {
     use super::*;
     use crate::{MultiBuffer, ToPoint};
     use rand::prelude::*;
+    use settings::Settings;
     use std::{cmp::Reverse, env, mem, sync::Arc};
     use sum_tree::TreeMap;
     use text::RandomCharIter;
@@ -1218,6 +1219,7 @@ mod tests {
 
     #[gpui::test]
     fn test_basic_folds(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
         let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
@@ -1291,6 +1293,7 @@ mod tests {
 
     #[gpui::test]
     fn test_adjacent_folds(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("abcdefghijkl", cx);
         let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
@@ -1354,6 +1357,7 @@ mod tests {
 
     #[gpui::test]
     fn test_merging_folds_via_edit(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
         let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
@@ -1404,6 +1408,7 @@ mod tests {
 
     #[gpui::test(iterations = 100)]
     fn test_random_folds(cx: &mut gpui::MutableAppContext, mut rng: StdRng) {
+        cx.set_global(Settings::test(cx));
         let operations = env::var("OPERATIONS")
             .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
             .unwrap_or(10);

crates/editor/src/display_map/tab_map.rs šŸ”—

@@ -12,7 +12,7 @@ use text::Point;
 pub struct TabMap(Mutex<TabSnapshot>);
 
 impl TabMap {
-    pub fn new(input: FoldSnapshot, tab_size: usize) -> (Self, TabSnapshot) {
+    pub fn new(input: FoldSnapshot, tab_size: u32) -> (Self, TabSnapshot) {
         let snapshot = TabSnapshot {
             fold_snapshot: input,
             tab_size,
@@ -24,12 +24,13 @@ impl TabMap {
         &self,
         fold_snapshot: FoldSnapshot,
         mut fold_edits: Vec<FoldEdit>,
+        tab_size: u32,
     ) -> (TabSnapshot, Vec<TabEdit>) {
         let mut old_snapshot = self.0.lock();
         let max_offset = old_snapshot.fold_snapshot.len();
         let new_snapshot = TabSnapshot {
             fold_snapshot,
-            tab_size: old_snapshot.tab_size,
+            tab_size,
         };
 
         let mut tab_edits = Vec::with_capacity(fold_edits.len());
@@ -87,7 +88,7 @@ impl TabMap {
 #[derive(Clone)]
 pub struct TabSnapshot {
     pub fold_snapshot: FoldSnapshot,
-    pub tab_size: usize,
+    pub tab_size: u32,
 }
 
 impl TabSnapshot {
@@ -234,7 +235,7 @@ impl TabSnapshot {
             .to_buffer_point(&self.fold_snapshot)
     }
 
-    fn expand_tabs(chars: impl Iterator<Item = char>, column: usize, tab_size: usize) -> usize {
+    fn expand_tabs(chars: impl Iterator<Item = char>, column: usize, tab_size: u32) -> usize {
         let mut expanded_chars = 0;
         let mut expanded_bytes = 0;
         let mut collapsed_bytes = 0;
@@ -243,7 +244,7 @@ impl TabSnapshot {
                 break;
             }
             if c == '\t' {
-                let tab_len = tab_size - expanded_chars % tab_size;
+                let tab_len = tab_size as usize - expanded_chars % tab_size as usize;
                 expanded_bytes += tab_len;
                 expanded_chars += tab_len;
             } else {
@@ -259,7 +260,7 @@ impl TabSnapshot {
         mut chars: impl Iterator<Item = char>,
         column: usize,
         bias: Bias,
-        tab_size: usize,
+        tab_size: u32,
     ) -> (usize, usize, usize) {
         let mut expanded_bytes = 0;
         let mut expanded_chars = 0;
@@ -270,7 +271,7 @@ impl TabSnapshot {
             }
 
             if c == '\t' {
-                let tab_len = tab_size - (expanded_chars % tab_size);
+                let tab_len = tab_size as usize - (expanded_chars % tab_size as usize);
                 expanded_chars += tab_len;
                 expanded_bytes += tab_len;
                 if expanded_bytes > column {
@@ -383,7 +384,7 @@ pub struct TabChunks<'a> {
     column: usize,
     output_position: Point,
     max_output_position: Point,
-    tab_size: usize,
+    tab_size: u32,
     skip_leading_tab: bool,
 }
 
@@ -415,16 +416,16 @@ impl<'a> Iterator for TabChunks<'a> {
                         });
                     } else {
                         self.chunk.text = &self.chunk.text[1..];
-                        let mut len = self.tab_size - self.column % self.tab_size;
+                        let mut len = self.tab_size - self.column as u32 % self.tab_size;
                         let next_output_position = cmp::min(
-                            self.output_position + Point::new(0, len as u32),
+                            self.output_position + Point::new(0, len),
                             self.max_output_position,
                         );
-                        len = (next_output_position.column - self.output_position.column) as usize;
-                        self.column += len;
+                        len = next_output_position.column - self.output_position.column;
+                        self.column += len as usize;
                         self.output_position = next_output_position;
                         return Some(Chunk {
-                            text: &SPACES[0..len],
+                            text: &SPACES[0..len as usize],
                             ..self.chunk
                         });
                     }

crates/editor/src/display_map/wrap_map.rs šŸ”—

@@ -1014,12 +1014,14 @@ mod tests {
     use gpui::test::observe;
     use language::RandomCharIter;
     use rand::prelude::*;
+    use settings::Settings;
     use smol::stream::StreamExt;
     use std::{cmp, env};
     use text::Rope;
 
     #[gpui::test(iterations = 100)]
     async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
+        cx.update(|cx| cx.set_global(Settings::test(cx)));
         cx.foreground().set_block_on_ticks(0..=50);
         cx.foreground().forbid_parking();
         let operations = env::var("OPERATIONS")
@@ -1104,7 +1106,8 @@ mod tests {
                 }
                 20..=39 => {
                     for (folds_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
-                        let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+                        let (tabs_snapshot, tab_edits) =
+                            tab_map.sync(folds_snapshot, fold_edits, tab_size);
                         let (mut snapshot, wrap_edits) =
                             wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
                         snapshot.check_invariants();
@@ -1129,7 +1132,7 @@ mod tests {
                 "Unwrapped text (unexpanded tabs): {:?}",
                 folds_snapshot.text()
             );
-            let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+            let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits, tab_size);
             log::info!("Unwrapped text (expanded tabs): {:?}", tabs_snapshot.text());
 
             let unwrapped_text = tabs_snapshot.text();

crates/editor/src/editor.rs šŸ”—

@@ -41,6 +41,7 @@ pub use multi_buffer::{
 use ordered_float::OrderedFloat;
 use project::{Project, ProjectTransaction};
 use serde::{Deserialize, Serialize};
+use settings::Settings;
 use smallvec::SmallVec;
 use smol::Timer;
 use snippet::Snippet;
@@ -57,7 +58,7 @@ pub use sum_tree::Bias;
 use text::rope::TextDimension;
 use theme::DiagnosticStyle;
 use util::{post_inc, ResultExt, TryFutureExt};
-use workspace::{settings, ItemNavHistory, Settings, Workspace};
+use workspace::{ItemNavHistory, Workspace};
 
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 const MAX_LINE_LEN: usize = 1024;
@@ -1008,7 +1009,6 @@ impl Editor {
             let style = build_style(&*settings, get_field_editor_theme, None, cx);
             DisplayMap::new(
                 buffer.clone(),
-                settings.tab_size,
                 style.text.font_id,
                 style.text.font_size,
                 None,
@@ -1130,8 +1130,12 @@ impl Editor {
         }
     }
 
-    pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc<Language>> {
-        self.buffer.read(cx).language(cx)
+    pub fn language_at<'a, T: ToOffset>(
+        &self,
+        point: T,
+        cx: &'a AppContext,
+    ) -> Option<&'a Arc<Language>> {
+        self.buffer.read(cx).language_at(point, cx)
     }
 
     fn style(&self, cx: &AppContext) -> EditorStyle {
@@ -2945,8 +2949,9 @@ impl Editor {
                     .buffer_line_for_row(old_head.row)
                 {
                     let indent_column = buffer.indent_column_for_line(line_buffer_range.start.row);
+                    let language_name = buffer.language().map(|language| language.name());
+                    let indent = cx.global::<Settings>().tab_size(language_name.as_deref());
                     if old_head.column <= indent_column && old_head.column > 0 {
-                        let indent = buffer.indent_size();
                         new_head = cmp::min(
                             new_head,
                             Point::new(old_head.row, ((old_head.column - 1) / indent) * indent),
@@ -2991,12 +2996,15 @@ impl Editor {
                     return;
                 }
 
-                let tab_size = cx.global::<Settings>().tab_size;
                 let mut selections = self.local_selections::<Point>(cx);
                 if selections.iter().all(|s| s.is_empty()) {
                     self.transact(cx, |this, cx| {
                         this.buffer.update(cx, |buffer, cx| {
                             for selection in &mut selections {
+                                let language_name =
+                                    buffer.language_at(selection.start, cx).map(|l| l.name());
+                                let tab_size =
+                                    cx.global::<Settings>().tab_size(language_name.as_deref());
                                 let char_column = buffer
                                     .read(cx)
                                     .text_for_range(
@@ -3004,13 +3012,14 @@ impl Editor {
                                     )
                                     .flat_map(str::chars)
                                     .count();
-                                let chars_to_next_tab_stop = tab_size - (char_column % tab_size);
+                                let chars_to_next_tab_stop =
+                                    tab_size - (char_column as u32 % tab_size);
                                 buffer.edit(
                                     [selection.start..selection.start],
-                                    " ".repeat(chars_to_next_tab_stop),
+                                    " ".repeat(chars_to_next_tab_stop as usize),
                                     cx,
                                 );
-                                selection.start.column += chars_to_next_tab_stop as u32;
+                                selection.start.column += chars_to_next_tab_stop;
                                 selection.end = selection.start;
                             }
                         });
@@ -3024,12 +3033,14 @@ impl Editor {
     }
 
     pub fn indent(&mut self, _: &Indent, cx: &mut ViewContext<Self>) {
-        let tab_size = cx.global::<Settings>().tab_size;
         let mut selections = self.local_selections::<Point>(cx);
         self.transact(cx, |this, cx| {
             let mut last_indent = None;
             this.buffer.update(cx, |buffer, cx| {
+                let snapshot = buffer.snapshot(cx);
                 for selection in &mut selections {
+                    let language_name = buffer.language_at(selection.start, cx).map(|l| l.name());
+                    let tab_size = cx.global::<Settings>().tab_size(language_name.as_deref());
                     let mut start_row = selection.start.row;
                     let mut end_row = selection.end.row + 1;
 
@@ -3053,12 +3064,12 @@ impl Editor {
                     }
 
                     for row in start_row..end_row {
-                        let indent_column = buffer.read(cx).indent_column_for_line(row) as usize;
+                        let indent_column = snapshot.indent_column_for_line(row);
                         let columns_to_next_tab_stop = tab_size - (indent_column % tab_size);
                         let row_start = Point::new(row, 0);
                         buffer.edit(
                             [row_start..row_start],
-                            " ".repeat(columns_to_next_tab_stop),
+                            " ".repeat(columns_to_next_tab_stop as usize),
                             cx,
                         );
 
@@ -3080,14 +3091,16 @@ impl Editor {
     }
 
     pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext<Self>) {
-        let tab_size = cx.global::<Settings>().tab_size;
         let selections = self.local_selections::<Point>(cx);
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut deletion_ranges = Vec::new();
         let mut last_outdent = None;
         {
-            let buffer = self.buffer.read(cx).read(cx);
+            let buffer = self.buffer.read(cx);
+            let snapshot = buffer.snapshot(cx);
             for selection in &selections {
+                let language_name = buffer.language_at(selection.start, cx).map(|l| l.name());
+                let tab_size = cx.global::<Settings>().tab_size(language_name.as_deref());
                 let mut rows = selection.spanned_rows(false, &display_map);
 
                 // Avoid re-outdenting a row that has already been outdented by a
@@ -3099,11 +3112,11 @@ impl Editor {
                 }
 
                 for row in rows {
-                    let column = buffer.indent_column_for_line(row) as usize;
+                    let column = snapshot.indent_column_for_line(row);
                     if column > 0 {
-                        let mut deletion_len = (column % tab_size) as u32;
+                        let mut deletion_len = column % tab_size;
                         if deletion_len == 0 {
-                            deletion_len = tab_size as u32;
+                            deletion_len = tab_size;
                         }
                         deletion_ranges.push(Point::new(row, 0)..Point::new(row, deletion_len));
                         last_outdent = Some(row);
@@ -4243,24 +4256,26 @@ impl Editor {
     }
 
     pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext<Self>) {
-        // Get the line comment prefix. Split its trailing whitespace into a separate string,
-        // as that portion won't be used for detecting if a line is a comment.
-        let full_comment_prefix =
-            if let Some(prefix) = self.language(cx).and_then(|l| l.line_comment_prefix()) {
-                prefix.to_string()
-            } else {
-                return;
-            };
-        let comment_prefix = full_comment_prefix.trim_end_matches(' ');
-        let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..];
-
         self.transact(cx, |this, cx| {
             let mut selections = this.local_selections::<Point>(cx);
             let mut all_selection_lines_are_comments = true;
             let mut edit_ranges = Vec::new();
             let mut last_toggled_row = None;
             this.buffer.update(cx, |buffer, cx| {
+                // TODO: Handle selections that cross excerpts
                 for selection in &mut selections {
+                    // Get the line comment prefix. Split its trailing whitespace into a separate string,
+                    // as that portion won't be used for detecting if a line is a comment.
+                    let full_comment_prefix = if let Some(prefix) = buffer
+                        .language_at(selection.start, cx)
+                        .and_then(|l| l.line_comment_prefix())
+                    {
+                        prefix.to_string()
+                    } else {
+                        return;
+                    };
+                    let comment_prefix = full_comment_prefix.trim_end_matches(' ');
+                    let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..];
                     edit_ranges.clear();
                     let snapshot = buffer.snapshot(cx);
 
@@ -5668,16 +5683,22 @@ impl Editor {
     }
 
     pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap {
-        let language = self.language(cx);
+        let language_name = self
+            .buffer
+            .read(cx)
+            .as_singleton()
+            .and_then(|singleton_buffer| singleton_buffer.read(cx).language())
+            .map(|l| l.name());
+
         let settings = cx.global::<Settings>();
         let mode = self
             .soft_wrap_mode_override
-            .unwrap_or_else(|| settings.soft_wrap(language));
+            .unwrap_or_else(|| settings.soft_wrap(language_name.as_deref()));
         match mode {
             settings::SoftWrap::None => SoftWrap::None,
             settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth,
             settings::SoftWrap::PreferredLineLength => {
-                SoftWrap::Column(settings.preferred_line_length(language))
+                SoftWrap::Column(settings.preferred_line_length(language_name.as_deref()))
             }
         }
     }
@@ -6461,14 +6482,18 @@ pub fn styled_runs_for_code_label<'a>(
 
 #[cfg(test)]
 mod tests {
+    use crate::test::{assert_text_with_selections, select_ranges};
+
     use super::*;
     use gpui::{
         geometry::rect::RectF,
         platform::{WindowBounds, WindowOptions},
     };
+    use indoc::indoc;
     use language::{FakeLspAdapter, LanguageConfig};
     use lsp::FakeLanguageServer;
     use project::FakeFs;
+    use settings::LanguageOverride;
     use smol::stream::StreamExt;
     use std::{cell::RefCell, rc::Rc, time::Instant};
     use text::Point;
@@ -6478,7 +6503,7 @@ mod tests {
 
     #[gpui::test]
     fn test_edit_events(cx: &mut MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx));
 
         let events = Rc::new(RefCell::new(Vec::new()));
@@ -6586,7 +6611,7 @@ mod tests {
 
     #[gpui::test]
     fn test_undo_redo_with_selection_restoration(cx: &mut MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let mut now = Instant::now();
         let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx));
         let group_interval = buffer.read(cx).transaction_group_interval();
@@ -6655,7 +6680,7 @@ mod tests {
 
     #[gpui::test]
     fn test_selection_with_mouse(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
 
         let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
         let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
@@ -6720,7 +6745,7 @@ mod tests {
 
     #[gpui::test]
     fn test_canceling_pending_selection(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
 
@@ -6752,7 +6777,7 @@ mod tests {
 
     #[gpui::test]
     fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         use workspace::Item;
         let nav_history = Rc::new(RefCell::new(workspace::NavHistory::default()));
         let buffer = MultiBuffer::build_simple(&sample_text(30, 5, 'a'), cx);
@@ -6812,7 +6837,7 @@ mod tests {
 
     #[gpui::test]
     fn test_cancel(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
 
@@ -6852,7 +6877,7 @@ mod tests {
 
     #[gpui::test]
     fn test_fold(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple(
             &"
                 impl Foo {
@@ -6937,7 +6962,7 @@ mod tests {
 
     #[gpui::test]
     fn test_move_cursor(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
 
@@ -7011,7 +7036,7 @@ mod tests {
 
     #[gpui::test]
     fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγΓε\n", cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
 
@@ -7112,7 +7137,7 @@ mod tests {
 
     #[gpui::test]
     fn test_move_cursor_different_line_lengths(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
         view.update(cx, |view, cx| {
@@ -7157,7 +7182,7 @@ mod tests {
 
     #[gpui::test]
     fn test_beginning_end_of_line(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("abc\n  def", cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
         view.update(cx, |view, cx| {
@@ -7298,7 +7323,7 @@ mod tests {
 
     #[gpui::test]
     fn test_prev_next_word_boundary(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n  {baz.qux()}", cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
         view.update(cx, |view, cx| {
@@ -7403,7 +7428,7 @@ mod tests {
 
     #[gpui::test]
     fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("use one::{\n    two::three::four::five\n};", cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
 
@@ -7456,7 +7481,7 @@ mod tests {
 
     #[gpui::test]
     fn test_delete_to_word_boundary(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("one two three four", cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
 
@@ -7493,7 +7518,7 @@ mod tests {
 
     #[gpui::test]
     fn test_newline(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("aaaa\n    bbbb\n", cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
 
@@ -7514,7 +7539,7 @@ mod tests {
 
     #[gpui::test]
     fn test_newline_with_old_selections(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple(
             "
                 a
@@ -7599,7 +7624,7 @@ mod tests {
 
     #[gpui::test]
     fn test_insert_with_old_selections(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
         let (_, editor) = cx.add_window(Default::default(), |cx| {
             let mut editor = build_editor(buffer.clone(), cx);
@@ -7626,81 +7651,226 @@ mod tests {
 
     #[gpui::test]
     fn test_indent_outdent(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
-        let buffer = MultiBuffer::build_simple("  one two\nthree\n four", cx);
+        cx.set_global(Settings::test(cx));
+        let buffer = MultiBuffer::build_simple(
+            indoc! {"
+                  one two
+                three
+                 four"},
+            cx,
+        );
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
 
         view.update(cx, |view, cx| {
             // two selections on the same line
-            view.select_display_ranges(
-                &[
-                    DisplayPoint::new(0, 2)..DisplayPoint::new(0, 5),
-                    DisplayPoint::new(0, 6)..DisplayPoint::new(0, 9),
-                ],
+            select_ranges(
+                view,
+                indoc! {"
+                      [one] [two]
+                    three
+                     four"},
                 cx,
             );
 
             // indent from mid-tabstop to full tabstop
             view.tab(&Tab(Direction::Next), cx);
-            assert_eq!(view.text(cx), "    one two\nthree\n four");
-            assert_eq!(
-                view.selected_display_ranges(cx),
-                &[
-                    DisplayPoint::new(0, 4)..DisplayPoint::new(0, 7),
-                    DisplayPoint::new(0, 8)..DisplayPoint::new(0, 11),
-                ]
+            assert_text_with_selections(
+                view,
+                indoc! {"
+                        [one] [two]
+                    three
+                     four"},
+                cx,
             );
 
             // outdent from 1 tabstop to 0 tabstops
             view.tab(&Tab(Direction::Prev), cx);
-            assert_eq!(view.text(cx), "one two\nthree\n four");
-            assert_eq!(
-                view.selected_display_ranges(cx),
-                &[
-                    DisplayPoint::new(0, 0)..DisplayPoint::new(0, 3),
-                    DisplayPoint::new(0, 4)..DisplayPoint::new(0, 7),
-                ]
+            assert_text_with_selections(
+                view,
+                indoc! {"
+                    [one] [two]
+                    three
+                     four"},
+                cx,
             );
 
             // select across line ending
-            view.select_display_ranges(&[DisplayPoint::new(1, 1)..DisplayPoint::new(2, 0)], cx);
+            select_ranges(
+                view,
+                indoc! {"
+                    one two
+                    t[hree
+                    ] four"},
+                cx,
+            );
 
             // indent and outdent affect only the preceding line
             view.tab(&Tab(Direction::Next), cx);
-            assert_eq!(view.text(cx), "one two\n    three\n four");
-            assert_eq!(
-                view.selected_display_ranges(cx),
-                &[DisplayPoint::new(1, 5)..DisplayPoint::new(2, 0)]
+            assert_text_with_selections(
+                view,
+                indoc! {"
+                    one two
+                        t[hree
+                    ] four"},
+                cx,
             );
             view.tab(&Tab(Direction::Prev), cx);
-            assert_eq!(view.text(cx), "one two\nthree\n four");
-            assert_eq!(
-                view.selected_display_ranges(cx),
-                &[DisplayPoint::new(1, 1)..DisplayPoint::new(2, 0)]
+            assert_text_with_selections(
+                view,
+                indoc! {"
+                    one two
+                    t[hree
+                    ] four"},
+                cx,
             );
 
             // Ensure that indenting/outdenting works when the cursor is at column 0.
-            view.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx);
+            select_ranges(
+                view,
+                indoc! {"
+                    one two
+                    []three
+                     four"},
+                cx,
+            );
             view.tab(&Tab(Direction::Next), cx);
-            assert_eq!(view.text(cx), "one two\n    three\n four");
-            assert_eq!(
-                view.selected_display_ranges(cx),
-                &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)]
+            assert_text_with_selections(
+                view,
+                indoc! {"
+                    one two
+                        []three
+                     four"},
+                cx,
             );
 
-            view.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx);
+            select_ranges(
+                view,
+                indoc! {"
+                    one two
+                    []    three
+                     four"},
+                cx,
+            );
             view.tab(&Tab(Direction::Prev), cx);
-            assert_eq!(view.text(cx), "one two\nthree\n four");
+            assert_text_with_selections(
+                view,
+                indoc! {"
+                    one two
+                    []three
+                     four"},
+                cx,
+            );
+        });
+    }
+
+    #[gpui::test]
+    fn test_indent_outdent_with_excerpts(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(
+            Settings::test(cx)
+                .with_overrides(
+                    "TOML",
+                    LanguageOverride {
+                        tab_size: Some(2),
+                        ..Default::default()
+                    },
+                )
+                .with_overrides(
+                    "Rust",
+                    LanguageOverride {
+                        tab_size: Some(4),
+                        ..Default::default()
+                    },
+                ),
+        );
+        let toml_language = Arc::new(Language::new(
+            LanguageConfig {
+                name: "TOML".into(),
+                ..Default::default()
+            },
+            None,
+        ));
+        let rust_language = Arc::new(Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                ..Default::default()
+            },
+            None,
+        ));
+
+        let toml_buffer = cx
+            .add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx).with_language(toml_language, cx));
+        let rust_buffer = cx.add_model(|cx| {
+            Buffer::new(0, "const c: usize = 3;\n", cx).with_language(rust_language, cx)
+        });
+        let multibuffer = cx.add_model(|cx| {
+            let mut multibuffer = MultiBuffer::new(0);
+            multibuffer.push_excerpts(
+                toml_buffer.clone(),
+                [Point::new(0, 0)..Point::new(2, 0)],
+                cx,
+            );
+            multibuffer.push_excerpts(
+                rust_buffer.clone(),
+                [Point::new(0, 0)..Point::new(1, 0)],
+                cx,
+            );
+            multibuffer
+        });
+
+        cx.add_window(Default::default(), |cx| {
+            let mut editor = build_editor(multibuffer, cx);
+
             assert_eq!(
-                view.selected_display_ranges(cx),
-                &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]
+                editor.text(cx),
+                indoc! {"
+                    a = 1
+                    b = 2
+
+                    const c: usize = 3;
+                "}
             );
+
+            select_ranges(
+                &mut editor,
+                indoc! {"
+                    [a] = 1
+                    b = 2
+
+                    [const c:] usize = 3;
+                "},
+                cx,
+            );
+
+            editor.tab(&Tab(Direction::Next), cx);
+            assert_text_with_selections(
+                &mut editor,
+                indoc! {"
+                      [a] = 1
+                    b = 2
+
+                        [const c:] usize = 3;
+                "},
+                cx,
+            );
+            editor.tab(&Tab(Direction::Prev), cx);
+            assert_text_with_selections(
+                &mut editor,
+                indoc! {"
+                    [a] = 1
+                    b = 2
+
+                    [const c:] usize = 3;
+                "},
+                cx,
+            );
+
+            editor
         });
     }
 
     #[gpui::test]
     fn test_backspace(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let (_, view) = cx.add_window(Default::default(), |cx| {
             build_editor(MultiBuffer::build_simple("", cx), cx)
         });
@@ -7745,7 +7915,7 @@ mod tests {
 
     #[gpui::test]
     fn test_delete(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer =
             MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
@@ -7773,7 +7943,7 @@ mod tests {
 
     #[gpui::test]
     fn test_delete_line(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
         view.update(cx, |view, cx| {
@@ -7796,7 +7966,7 @@ mod tests {
             );
         });
 
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
         view.update(cx, |view, cx| {
@@ -7812,7 +7982,7 @@ mod tests {
 
     #[gpui::test]
     fn test_duplicate_line(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
         view.update(cx, |view, cx| {
@@ -7862,7 +8032,7 @@ mod tests {
 
     #[gpui::test]
     fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
         view.update(cx, |view, cx| {
@@ -7958,7 +8128,7 @@ mod tests {
 
     #[gpui::test]
     fn test_move_line_up_down_with_blocks(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
         let snapshot = buffer.read(cx).snapshot(cx);
         let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
@@ -7979,7 +8149,7 @@ mod tests {
 
     #[gpui::test]
     fn test_clipboard(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("oneāœ… two three four five six ", cx);
         let view = cx
             .add_window(Default::default(), |cx| build_editor(buffer.clone(), cx))
@@ -8108,7 +8278,7 @@ mod tests {
 
     #[gpui::test]
     fn test_select_all(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
         view.update(cx, |view, cx| {
@@ -8122,7 +8292,7 @@ mod tests {
 
     #[gpui::test]
     fn test_select_line(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
         view.update(cx, |view, cx| {
@@ -8167,7 +8337,7 @@ mod tests {
 
     #[gpui::test]
     fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
         view.update(cx, |view, cx| {
@@ -8233,7 +8403,7 @@ mod tests {
 
     #[gpui::test]
     fn test_add_selection_above_below(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
 
@@ -8417,7 +8587,7 @@ mod tests {
 
     #[gpui::test]
     fn test_select_next(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
 
         let (text, ranges) = marked_text_ranges("[abc]\n[abc] [abc]\ndefabc\n[abc]");
         let buffer = MultiBuffer::build_simple(&text, cx);
@@ -8447,7 +8617,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
-        cx.update(populate_settings);
+        cx.update(|cx| cx.set_global(Settings::test(cx)));
         let language = Arc::new(Language::new(
             LanguageConfig::default(),
             Some(tree_sitter_rust::language()),
@@ -8588,7 +8758,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
-        cx.update(populate_settings);
+        cx.update(|cx| cx.set_global(Settings::test(cx)));
         let language = Arc::new(
             Language::new(
                 LanguageConfig {
@@ -8645,7 +8815,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
-        cx.update(populate_settings);
+        cx.update(|cx| cx.set_global(Settings::test(cx)));
         let language = Arc::new(Language::new(
             LanguageConfig {
                 brackets: vec![
@@ -8792,7 +8962,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_snippets(cx: &mut gpui::TestAppContext) {
-        cx.update(populate_settings);
+        cx.update(|cx| cx.set_global(Settings::test(cx)));
 
         let text = "
             a. b
@@ -8900,7 +9070,7 @@ mod tests {
     #[gpui::test]
     async fn test_format_during_save(cx: &mut gpui::TestAppContext) {
         cx.foreground().forbid_parking();
-        cx.update(populate_settings);
+        cx.update(|cx| cx.set_global(Settings::test(cx)));
 
         let mut language = Language::new(
             LanguageConfig {
@@ -8952,6 +9122,7 @@ mod tests {
                     params.text_document.uri,
                     lsp::Url::from_file_path("/file.rs").unwrap()
                 );
+                assert_eq!(params.options.tab_size, 4);
                 Ok(Some(vec![lsp::TextEdit::new(
                     lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
                     ", ".to_string(),
@@ -8988,11 +9159,39 @@ mod tests {
             "one\ntwo\nthree\n"
         );
         assert!(!cx.read(|cx| editor.is_dirty(cx)));
+
+        // Set rust language override and assert overriden tabsize is sent to language server
+        cx.update(|cx| {
+            cx.update_global::<Settings, _, _>(|settings, _| {
+                settings.language_overrides.insert(
+                    "Rust".into(),
+                    LanguageOverride {
+                        tab_size: Some(8),
+                        ..Default::default()
+                    },
+                );
+            })
+        });
+
+        let save = cx.update(|cx| editor.save(project.clone(), cx));
+        fake_server
+            .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
+                assert_eq!(
+                    params.text_document.uri,
+                    lsp::Url::from_file_path("/file.rs").unwrap()
+                );
+                assert_eq!(params.options.tab_size, 8);
+                Ok(Some(vec![]))
+            })
+            .next()
+            .await;
+        cx.foreground().start_waiting();
+        save.await.unwrap();
     }
 
     #[gpui::test]
     async fn test_completion(cx: &mut gpui::TestAppContext) {
-        cx.update(populate_settings);
+        cx.update(|cx| cx.set_global(Settings::test(cx)));
 
         let mut language = Language::new(
             LanguageConfig {
@@ -9233,7 +9432,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
-        cx.update(populate_settings);
+        cx.update(|cx| cx.set_global(Settings::test(cx)));
         let language = Arc::new(Language::new(
             LanguageConfig {
                 line_comment: Some("// ".to_string()),
@@ -9313,7 +9512,7 @@ mod tests {
 
     #[gpui::test]
     fn test_editing_disjoint_excerpts(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx));
         let multibuffer = cx.add_model(|cx| {
             let mut multibuffer = MultiBuffer::new(0);
@@ -9356,7 +9555,7 @@ mod tests {
 
     #[gpui::test]
     fn test_editing_overlapping_excerpts(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx));
         let multibuffer = cx.add_model(|cx| {
             let mut multibuffer = MultiBuffer::new(0);
@@ -9411,7 +9610,7 @@ mod tests {
 
     #[gpui::test]
     fn test_refresh_selections(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx));
         let mut excerpt1_id = None;
         let multibuffer = cx.add_model(|cx| {
@@ -9489,7 +9688,7 @@ mod tests {
 
     #[gpui::test]
     fn test_refresh_selections_while_selecting_with_mouse(cx: &mut gpui::MutableAppContext) {
-        populate_settings(cx);
+        cx.set_global(Settings::test(cx));
         let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx));
         let mut excerpt1_id = None;
         let multibuffer = cx.add_model(|cx| {
@@ -9543,7 +9742,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
-        cx.update(populate_settings);
+        cx.update(|cx| cx.set_global(Settings::test(cx)));
         let language = Arc::new(Language::new(
             LanguageConfig {
                 brackets: vec![
@@ -9611,7 +9810,8 @@ mod tests {
     #[gpui::test]
     fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) {
         let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
-        populate_settings(cx);
+
+        cx.set_global(Settings::test(cx));
         let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
 
         editor.update(cx, |editor, cx| {
@@ -9690,7 +9890,8 @@ mod tests {
     #[gpui::test]
     fn test_following(cx: &mut gpui::MutableAppContext) {
         let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
-        populate_settings(cx);
+
+        cx.set_global(Settings::test(cx));
 
         let (_, leader) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
         let (_, follower) = cx.add_window(
@@ -9857,11 +10058,6 @@ mod tests {
         Editor::new(EditorMode::Full, buffer, None, None, cx)
     }
 
-    fn populate_settings(cx: &mut gpui::MutableAppContext) {
-        let settings = Settings::test(cx);
-        cx.set_global(settings);
-    }
-
     fn assert_selection_ranges(
         marked_text: &str,
         selection_marker_pairs: Vec<(char, char)>,

crates/editor/src/element.rs šŸ”—

@@ -1494,8 +1494,8 @@ mod tests {
         display_map::{BlockDisposition, BlockProperties},
         Editor, MultiBuffer,
     };
+    use settings::Settings;
     use util::test::sample_text;
-    use workspace::Settings;
 
     #[gpui::test]
     fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) {

crates/editor/src/items.rs šŸ”—

@@ -8,12 +8,11 @@ use gpui::{
 use language::{Bias, Buffer, Diagnostic, File as _, SelectionGoal};
 use project::{File, Project, ProjectEntryId, ProjectPath};
 use rpc::proto::{self, update_view};
+use settings::Settings;
 use std::{fmt::Write, path::PathBuf, time::Duration};
 use text::{Point, Selection};
 use util::TryFutureExt;
-use workspace::{
-    FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView,
-};
+use workspace::{FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, StatusItemView};
 
 pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
 

crates/editor/src/movement.rs šŸ”—

@@ -268,9 +268,11 @@ mod tests {
     use super::*;
     use crate::{test::marked_display_snapshot, Buffer, DisplayMap, MultiBuffer};
     use language::Point;
+    use settings::Settings;
 
     #[gpui::test]
     fn test_previous_word_start(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
             assert_eq!(
@@ -297,6 +299,7 @@ mod tests {
 
     #[gpui::test]
     fn test_previous_subword_start(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
             assert_eq!(
@@ -330,6 +333,7 @@ mod tests {
 
     #[gpui::test]
     fn test_find_preceding_boundary(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         fn assert(
             marked_text: &str,
             cx: &mut gpui::MutableAppContext,
@@ -361,6 +365,7 @@ mod tests {
 
     #[gpui::test]
     fn test_next_word_end(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
             assert_eq!(
@@ -384,6 +389,7 @@ mod tests {
 
     #[gpui::test]
     fn test_next_subword_end(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
             assert_eq!(
@@ -416,6 +422,7 @@ mod tests {
 
     #[gpui::test]
     fn test_find_boundary(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         fn assert(
             marked_text: &str,
             cx: &mut gpui::MutableAppContext,
@@ -447,6 +454,7 @@ mod tests {
 
     #[gpui::test]
     fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
             assert_eq!(
@@ -467,6 +475,7 @@ mod tests {
 
     #[gpui::test]
     fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
         let font_id = cx
             .font_cache()
@@ -487,7 +496,7 @@ mod tests {
             multibuffer
         });
         let display_map =
-            cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, 2, 2, cx));
+            cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
         let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
 
         assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");

crates/editor/src/multi_buffer.rs šŸ”—

@@ -11,6 +11,7 @@ use language::{
     Language, OffsetRangeExt, Outline, OutlineItem, Selection, ToOffset as _, ToPoint as _,
     ToPointUtf16 as _, TransactionId,
 };
+use settings::Settings;
 use std::{
     cell::{Ref, RefCell},
     cmp, fmt, io,
@@ -297,8 +298,10 @@ impl MultiBuffer {
                 .into_iter()
                 .map(|range| range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot));
             return buffer.update(cx, |buffer, cx| {
+                let language_name = buffer.language().map(|language| language.name());
+                let indent_size = cx.global::<Settings>().tab_size(language_name.as_deref());
                 if autoindent {
-                    buffer.edit_with_autoindent(ranges, new_text, cx);
+                    buffer.edit_with_autoindent(ranges, new_text, indent_size, cx);
                 } else {
                     buffer.edit(ranges, new_text, cx);
                 }
@@ -392,10 +395,12 @@ impl MultiBuffer {
                             );
                         }
                     }
+                    let language_name = buffer.language().map(|l| l.name());
+                    let indent_size = cx.global::<Settings>().tab_size(language_name.as_deref());
 
                     if autoindent {
-                        buffer.edit_with_autoindent(deletions, "", cx);
-                        buffer.edit_with_autoindent(insertions, new_text.clone(), cx);
+                        buffer.edit_with_autoindent(deletions, "", indent_size, cx);
+                        buffer.edit_with_autoindent(insertions, new_text.clone(), indent_size, cx);
                     } else {
                         buffer.edit(deletions, "", cx);
                         buffer.edit(insertions, new_text.clone(), cx);
@@ -861,6 +866,29 @@ impl MultiBuffer {
         })
     }
 
+    // If point is at the end of the buffer, the last excerpt is returned
+    pub fn point_to_buffer_offset<'a, T: ToOffset>(
+        &'a self,
+        point: T,
+        cx: &AppContext,
+    ) -> Option<(ModelHandle<Buffer>, usize)> {
+        let snapshot = self.read(cx);
+        let offset = point.to_offset(&snapshot);
+        let mut cursor = snapshot.excerpts.cursor::<usize>();
+        cursor.seek(&offset, Bias::Right, &());
+        if cursor.item().is_none() {
+            cursor.prev(&());
+        }
+
+        cursor.item().map(|excerpt| {
+            let excerpt_start = excerpt.range.start.to_offset(&excerpt.buffer);
+            let buffer_point = excerpt_start + offset - *cursor.start();
+            let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone();
+
+            (buffer, buffer_point)
+        })
+    }
+
     pub fn range_to_buffer_ranges<'a, T: ToOffset>(
         &'a self,
         range: Range<T>,
@@ -1057,12 +1085,13 @@ impl MultiBuffer {
             .unwrap_or(false)
     }
 
-    pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc<Language>> {
-        self.buffers
-            .borrow()
-            .values()
-            .next()
-            .and_then(|state| state.buffer.read(cx).language())
+    pub fn language_at<'a, T: ToOffset>(
+        &self,
+        point: T,
+        cx: &'a AppContext,
+    ) -> Option<&'a Arc<Language>> {
+        self.point_to_buffer_offset(point, cx)
+            .and_then(|(buffer, _)| buffer.read(cx).language())
     }
 
     pub fn file<'a>(&self, cx: &'a AppContext) -> Option<&'a dyn File> {
@@ -3760,6 +3789,7 @@ mod tests {
 
     #[gpui::test]
     fn test_history(cx: &mut MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         let buffer_1 = cx.add_model(|cx| Buffer::new(0, "1234", cx));
         let buffer_2 = cx.add_model(|cx| Buffer::new(0, "5678", cx));
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));

crates/editor/src/test.rs šŸ”—

@@ -1,8 +1,9 @@
-use util::test::marked_text;
+use gpui::ViewContext;
+use util::test::{marked_text, marked_text_ranges};
 
 use crate::{
     display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
-    DisplayPoint, MultiBuffer,
+    DisplayPoint, Editor, MultiBuffer,
 };
 
 #[cfg(test)]
@@ -20,7 +21,6 @@ pub fn marked_display_snapshot(
 ) -> (DisplaySnapshot, Vec<DisplayPoint>) {
     let (unmarked_text, markers) = marked_text(text);
 
-    let tab_size = 4;
     let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
     let font_id = cx
         .font_cache()
@@ -30,7 +30,7 @@ pub fn marked_display_snapshot(
 
     let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
     let display_map =
-        cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
+        cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
     let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
     let markers = markers
         .into_iter()
@@ -39,3 +39,20 @@ pub fn marked_display_snapshot(
 
     (snapshot, markers)
 }
+
+pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext<Editor>) {
+    let (umarked_text, text_ranges) = marked_text_ranges(marked_text);
+    assert_eq!(editor.text(cx), umarked_text);
+    editor.select_ranges(text_ranges, None, cx);
+}
+
+pub fn assert_text_with_selections(
+    editor: &mut Editor,
+    marked_text: &str,
+    cx: &mut ViewContext<Editor>,
+) {
+    let (unmarked_text, text_ranges) = marked_text_ranges(marked_text);
+
+    assert_eq!(editor.text(cx), unmarked_text);
+    assert_eq!(editor.selected_ranges(cx), text_ranges);
+}

crates/file_finder/Cargo.toml šŸ”—

@@ -12,6 +12,7 @@ editor = { path = "../editor" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 project = { path = "../project" }
+settings = { path = "../settings" }
 util = { path = "../util" }
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }

crates/file_finder/src/file_finder.rs šŸ”—

@@ -8,6 +8,7 @@ use gpui::{
     ViewContext, ViewHandle, WeakViewHandle,
 };
 use project::{Project, ProjectPath, WorktreeId};
+use settings::Settings;
 use std::{
     cmp,
     path::Path,
@@ -19,7 +20,7 @@ use std::{
 use util::post_inc;
 use workspace::{
     menu::{Confirm, SelectNext, SelectPrev},
-    Settings, Workspace,
+    Workspace,
 };
 
 pub struct FileFinder {

crates/go_to_line/Cargo.toml šŸ”—

@@ -11,5 +11,6 @@ doctest = false
 text = { path = "../text" }
 editor = { path = "../editor" }
 gpui = { path = "../gpui" }
+settings = { path = "../settings" }
 workspace = { path = "../workspace" }
 postage = { version = "0.4", features = ["futures-traits"] }

crates/go_to_line/src/go_to_line.rs šŸ”—

@@ -3,8 +3,9 @@ use gpui::{
     action, elements::*, geometry::vector::Vector2F, keymap::Binding, Axis, Entity,
     MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
 };
+use settings::Settings;
 use text::{Bias, Point};
-use workspace::{Settings, Workspace};
+use workspace::Workspace;
 
 action!(Toggle);
 action!(Confirm);

crates/gpui/src/app.rs šŸ”—

@@ -1019,7 +1019,10 @@ impl MutableAppContext {
             .insert(TypeId::of::<A>(), handler)
             .is_some()
         {
-            panic!("registered multiple global handlers for the same action type");
+            panic!(
+                "registered multiple global handlers for {}",
+                type_name::<A>()
+            );
         }
     }
 
@@ -2355,11 +2358,11 @@ impl AppContext {
     }
 
     pub fn global<T: 'static>(&self) -> &T {
-        self.globals
-            .get(&TypeId::of::<T>())
-            .expect("no app state has been added for this type")
-            .downcast_ref()
-            .unwrap()
+        if let Some(global) = self.globals.get(&TypeId::of::<T>()) {
+            global.downcast_ref().unwrap()
+        } else {
+            panic!("no global has been added for {}", type_name::<T>());
+        }
     }
 }
 

crates/language/src/buffer.rs šŸ”—

@@ -66,7 +66,6 @@ pub struct Buffer {
     file_update_count: usize,
     completion_triggers: Vec<String>,
     deferred_ops: OperationQueue<Operation>,
-    indent_size: u32,
 }
 
 pub struct BufferSnapshot {
@@ -80,7 +79,6 @@ pub struct BufferSnapshot {
     selections_update_count: usize,
     language: Option<Arc<Language>>,
     parse_count: usize,
-    indent_size: u32,
 }
 
 #[derive(Clone, Debug)]
@@ -214,6 +212,7 @@ struct AutoindentRequest {
     before_edit: BufferSnapshot,
     edited: Vec<Anchor>,
     inserted: Option<Vec<Range<Anchor>>>,
+    indent_size: u32,
 }
 
 #[derive(Debug)]
@@ -427,8 +426,6 @@ impl Buffer {
             file_update_count: 0,
             completion_triggers: Default::default(),
             deferred_ops: OperationQueue::new(),
-            // TODO: make this configurable
-            indent_size: 4,
         }
     }
 
@@ -444,7 +441,6 @@ impl Buffer {
             language: self.language.clone(),
             parse_count: self.parse_count,
             selections_update_count: self.selections_update_count,
-            indent_size: self.indent_size,
         }
     }
 
@@ -786,7 +782,7 @@ impl Buffer {
                                     .indent_column_for_line(suggestion.basis_row)
                             });
                         let delta = if suggestion.indent {
-                            snapshot.indent_size
+                            request.indent_size
                         } else {
                             0
                         };
@@ -809,7 +805,7 @@ impl Buffer {
                         .flatten();
                     for (new_row, suggestion) in new_edited_row_range.zip(suggestions) {
                         let delta = if suggestion.indent {
-                            snapshot.indent_size
+                            request.indent_size
                         } else {
                             0
                         };
@@ -845,7 +841,7 @@ impl Buffer {
                             .flatten();
                         for (row, suggestion) in inserted_row_range.zip(suggestions) {
                             let delta = if suggestion.indent {
-                                snapshot.indent_size
+                                request.indent_size
                             } else {
                                 0
                             };
@@ -1055,7 +1051,7 @@ impl Buffer {
     where
         T: Into<String>,
     {
-        self.edit_internal([0..self.len()], text, false, cx)
+        self.edit_internal([0..self.len()], text, None, cx)
     }
 
     pub fn edit<I, S, T>(
@@ -1069,13 +1065,14 @@ impl Buffer {
         S: ToOffset,
         T: Into<String>,
     {
-        self.edit_internal(ranges_iter, new_text, false, cx)
+        self.edit_internal(ranges_iter, new_text, None, cx)
     }
 
     pub fn edit_with_autoindent<I, S, T>(
         &mut self,
         ranges_iter: I,
         new_text: T,
+        indent_size: u32,
         cx: &mut ModelContext<Self>,
     ) -> Option<clock::Local>
     where
@@ -1083,14 +1080,14 @@ impl Buffer {
         S: ToOffset,
         T: Into<String>,
     {
-        self.edit_internal(ranges_iter, new_text, true, cx)
+        self.edit_internal(ranges_iter, new_text, Some(indent_size), cx)
     }
 
     pub fn edit_internal<I, S, T>(
         &mut self,
         ranges_iter: I,
         new_text: T,
-        autoindent: bool,
+        autoindent_size: Option<u32>,
         cx: &mut ModelContext<Self>,
     ) -> Option<clock::Local>
     where
@@ -1122,23 +1119,27 @@ impl Buffer {
 
         self.start_transaction();
         self.pending_autoindent.take();
-        let autoindent_request = if autoindent && self.language.is_some() {
-            let before_edit = self.snapshot();
-            let edited = ranges
-                .iter()
-                .filter_map(|range| {
-                    let start = range.start.to_point(self);
-                    if new_text.starts_with('\n') && start.column == self.line_len(start.row) {
-                        None
-                    } else {
-                        Some(self.anchor_before(range.start))
-                    }
-                })
-                .collect();
-            Some((before_edit, edited))
-        } else {
-            None
-        };
+        let autoindent_request =
+            self.language
+                .as_ref()
+                .and_then(|_| autoindent_size)
+                .map(|autoindent_size| {
+                    let before_edit = self.snapshot();
+                    let edited = ranges
+                        .iter()
+                        .filter_map(|range| {
+                            let start = range.start.to_point(self);
+                            if new_text.starts_with('\n')
+                                && start.column == self.line_len(start.row)
+                            {
+                                None
+                            } else {
+                                Some(self.anchor_before(range.start))
+                            }
+                        })
+                        .collect();
+                    (before_edit, edited, autoindent_size)
+                });
 
         let first_newline_ix = new_text.find('\n');
         let new_text_len = new_text.len();
@@ -1146,7 +1147,7 @@ impl Buffer {
         let edit = self.text.edit(ranges.iter().cloned(), new_text);
         let edit_id = edit.local_timestamp();
 
-        if let Some((before_edit, edited)) = autoindent_request {
+        if let Some((before_edit, edited, size)) = autoindent_request {
             let mut inserted = None;
             if let Some(first_newline_ix) = first_newline_ix {
                 let mut delta = 0isize;
@@ -1169,6 +1170,7 @@ impl Buffer {
                 before_edit,
                 edited,
                 inserted,
+                indent_size: size,
             }));
         }
 
@@ -1925,10 +1927,6 @@ impl BufferSnapshot {
     pub fn file_update_count(&self) -> usize {
         self.file_update_count
     }
-
-    pub fn indent_size(&self) -> u32 {
-        self.indent_size
-    }
 }
 
 impl Clone for BufferSnapshot {
@@ -1944,7 +1942,6 @@ impl Clone for BufferSnapshot {
             file_update_count: self.file_update_count,
             language: self.language.clone(),
             parse_count: self.parse_count,
-            indent_size: self.indent_size,
         }
     }
 }

crates/language/src/tests.rs šŸ”—

@@ -576,13 +576,13 @@ fn test_edit_with_autoindent(cx: &mut MutableAppContext) {
         let text = "fn a() {}";
         let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
 
-        buffer.edit_with_autoindent([8..8], "\n\n", cx);
+        buffer.edit_with_autoindent([8..8], "\n\n", 4, cx);
         assert_eq!(buffer.text(), "fn a() {\n    \n}");
 
-        buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 4)], "b()\n", cx);
+        buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 4)], "b()\n", 4, cx);
         assert_eq!(buffer.text(), "fn a() {\n    b()\n    \n}");
 
-        buffer.edit_with_autoindent([Point::new(2, 4)..Point::new(2, 4)], ".c", cx);
+        buffer.edit_with_autoindent([Point::new(2, 4)..Point::new(2, 4)], ".c", 4, cx);
         assert_eq!(buffer.text(), "fn a() {\n    b()\n        .c\n}");
 
         buffer
@@ -604,7 +604,12 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta
 
         // Lines 2 and 3 don't match the indentation suggestion. When editing these lines,
         // their indentation is not adjusted.
-        buffer.edit_with_autoindent([empty(Point::new(1, 1)), empty(Point::new(2, 1))], "()", cx);
+        buffer.edit_with_autoindent(
+            [empty(Point::new(1, 1)), empty(Point::new(2, 1))],
+            "()",
+            4,
+            cx,
+        );
         assert_eq!(
             buffer.text(),
             "
@@ -621,6 +626,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta
         buffer.edit_with_autoindent(
             [empty(Point::new(1, 1)), empty(Point::new(2, 1))],
             "\n.f\n.g",
+            4,
             cx,
         );
         assert_eq!(
@@ -651,7 +657,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte
 
         let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
 
-        buffer.edit_with_autoindent([5..5], "\nb", cx);
+        buffer.edit_with_autoindent([5..5], "\nb", 4, cx);
         assert_eq!(
             buffer.text(),
             "
@@ -663,7 +669,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte
 
         // The indentation suggestion changed because `@end` node (a close paren)
         // is now at the beginning of the line.
-        buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 5)], "", cx);
+        buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 5)], "", 4, cx);
         assert_eq!(
             buffer.text(),
             "

crates/outline/Cargo.toml šŸ”—

@@ -12,6 +12,7 @@ editor = { path = "../editor" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
+settings = { path = "../settings" }
 text = { path = "../text" }
 workspace = { path = "../workspace" }
 ordered-float = "2.1.1"

crates/outline/src/outline.rs šŸ”—

@@ -13,10 +13,11 @@ use gpui::{
 };
 use language::Outline;
 use ordered_float::OrderedFloat;
+use settings::Settings;
 use std::cmp::{self, Reverse};
 use workspace::{
     menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
-    Settings, Workspace,
+    Workspace,
 };
 
 action!(Toggle);

crates/project/Cargo.toml šŸ”—

@@ -25,6 +25,7 @@ gpui = { path = "../gpui" }
 language = { path = "../language" }
 lsp = { path = "../lsp" }
 rpc = { path = "../rpc" }
+settings = { path = "../settings" }
 sum_tree = { path = "../sum_tree" }
 util = { path = "../util" }
 aho-corasick = "0.7"

crates/project/src/project.rs šŸ”—

@@ -28,6 +28,7 @@ use parking_lot::Mutex;
 use postage::watch;
 use rand::prelude::*;
 use search::SearchQuery;
+use settings::Settings;
 use sha2::{Digest, Sha256};
 use similar::{ChangeTag, TextDiff};
 use std::{
@@ -2173,6 +2174,10 @@ impl Project {
                     lsp::Url::from_file_path(&buffer_abs_path).unwrap(),
                 );
                 let capabilities = &language_server.capabilities();
+                let tab_size = cx.update(|cx| {
+                    let language_name = buffer.read(cx).language().map(|language| language.name());
+                    cx.global::<Settings>().tab_size(language_name.as_deref())
+                });
                 let lsp_edits = if capabilities
                     .document_formatting_provider
                     .as_ref()
@@ -2182,7 +2187,7 @@ impl Project {
                         .request::<lsp::request::Formatting>(lsp::DocumentFormattingParams {
                             text_document,
                             options: lsp::FormattingOptions {
-                                tab_size: 4,
+                                tab_size,
                                 insert_spaces: true,
                                 insert_final_newline: Some(true),
                                 ..Default::default()

crates/project_panel/Cargo.toml šŸ”—

@@ -10,6 +10,7 @@ doctest = false
 [dependencies]
 gpui = { path = "../gpui" }
 project = { path = "../project" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }

crates/project_panel/src/project_panel.rs šŸ”—

@@ -10,6 +10,7 @@ use gpui::{
     ViewHandle, WeakViewHandle,
 };
 use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
+use settings::Settings;
 use std::{
     collections::{hash_map, HashMap},
     ffi::OsStr,
@@ -17,7 +18,7 @@ use std::{
 };
 use workspace::{
     menu::{SelectNext, SelectPrev},
-    Settings, Workspace,
+    Workspace,
 };
 
 pub struct ProjectPanel {

crates/project_symbols/Cargo.toml šŸ”—

@@ -13,6 +13,7 @@ fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 project = { path = "../project" }
 text = { path = "../text" }
+settings = { path = "../settings" }
 workspace = { path = "../workspace" }
 util = { path = "../util" }
 anyhow = "1.0.38"

crates/project_symbols/src/project_symbols.rs šŸ”—

@@ -11,6 +11,7 @@ use gpui::{
 };
 use ordered_float::OrderedFloat;
 use project::{Project, Symbol};
+use settings::Settings;
 use std::{
     borrow::Cow,
     cmp::{self, Reverse},
@@ -18,7 +19,7 @@ use std::{
 use util::ResultExt;
 use workspace::{
     menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
-    Settings, Workspace,
+    Workspace,
 };
 
 action!(Toggle);

crates/search/Cargo.toml šŸ”—

@@ -13,6 +13,7 @@ editor = { path = "../editor" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
 project = { path = "../project" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }

crates/search/src/buffer_search.rs šŸ”—

@@ -8,8 +8,9 @@ use gpui::{
 };
 use language::OffsetRangeExt;
 use project::search::SearchQuery;
+use settings::Settings;
 use std::ops::Range;
-use workspace::{ItemHandle, Pane, Settings, ToolbarItemLocation, ToolbarItemView};
+use workspace::{ItemHandle, Pane, ToolbarItemLocation, ToolbarItemView};
 
 action!(Deploy, bool);
 action!(Dismiss);

crates/search/src/project_search.rs šŸ”—

@@ -10,15 +10,14 @@ use gpui::{
     ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
 };
 use project::{search::SearchQuery, Project};
+use settings::Settings;
 use std::{
     any::{Any, TypeId},
     ops::Range,
     path::PathBuf,
 };
 use util::ResultExt as _;
-use workspace::{
-    Item, ItemNavHistory, Pane, Settings, ToolbarItemLocation, ToolbarItemView, Workspace,
-};
+use workspace::{Item, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace};
 
 action!(Deploy);
 action!(Search);

crates/server/Cargo.toml šŸ”—

@@ -14,6 +14,7 @@ required-features = ["seed-support"]
 
 [dependencies]
 collections = { path = "../collections" }
+settings = { path = "../settings" }
 rpc = { path = "../rpc" }
 anyhow = "1.0.40"
 async-io = "1.3"

crates/server/src/rpc.rs šŸ”—

@@ -1104,6 +1104,7 @@ mod tests {
     use rand::prelude::*;
     use rpc::PeerId;
     use serde_json::json;
+    use settings::Settings;
     use sqlx::types::time::OffsetDateTime;
     use std::{
         cell::Cell,
@@ -1117,7 +1118,7 @@ mod tests {
         },
         time::Duration,
     };
-    use workspace::{Item, Settings, SplitDirection, Workspace, WorkspaceParams};
+    use workspace::{Item, SplitDirection, Workspace, WorkspaceParams};
 
     #[cfg(test)]
     #[ctor::ctor]

crates/settings/Cargo.toml šŸ”—

@@ -0,0 +1,22 @@
+[package]
+name = "settings"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/settings.rs"
+doctest = false
+
+[features]
+test-support = []
+
+[dependencies]
+gpui = { path = "../gpui" }
+theme = { path = "../theme" }
+util = { path = "../util" }
+anyhow = "1.0.38"
+schemars = "0.8"
+serde = { version = "1", features = ["derive", "rc"] }
+serde_json = { version = "1.0.64", features = ["preserve_order"] }
+serde_path_to_error = "0.1.4"
+toml = "0.5"

crates/settings/src/settings.rs šŸ”—

@@ -0,0 +1,171 @@
+use anyhow::Result;
+use gpui::font_cache::{FamilyId, FontCache};
+use schemars::{schema_for, JsonSchema};
+use serde::Deserialize;
+use std::{collections::HashMap, sync::Arc};
+use theme::{Theme, ThemeRegistry};
+use util::ResultExt as _;
+
+#[derive(Clone)]
+pub struct Settings {
+    pub buffer_font_family: FamilyId,
+    pub buffer_font_size: f32,
+    pub vim_mode: bool,
+    pub tab_size: u32,
+    pub soft_wrap: SoftWrap,
+    pub preferred_line_length: u32,
+    pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
+    pub theme: Arc<Theme>,
+}
+
+#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
+pub struct LanguageOverride {
+    pub tab_size: Option<u32>,
+    pub soft_wrap: Option<SoftWrap>,
+    pub preferred_line_length: Option<u32>,
+}
+
+#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum SoftWrap {
+    None,
+    EditorWidth,
+    PreferredLineLength,
+}
+
+#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
+pub struct SettingsFileContent {
+    #[serde(default)]
+    pub buffer_font_family: Option<String>,
+    #[serde(default)]
+    pub buffer_font_size: Option<f32>,
+    #[serde(default)]
+    pub vim_mode: Option<bool>,
+    #[serde(flatten)]
+    pub editor: LanguageOverride,
+    #[serde(default)]
+    pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
+    #[serde(default)]
+    pub theme: Option<String>,
+}
+
+impl Settings {
+    pub fn new(
+        buffer_font_family: &str,
+        font_cache: &FontCache,
+        theme: Arc<Theme>,
+    ) -> Result<Self> {
+        Ok(Self {
+            buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
+            buffer_font_size: 15.,
+            vim_mode: false,
+            tab_size: 4,
+            soft_wrap: SoftWrap::None,
+            preferred_line_length: 80,
+            language_overrides: Default::default(),
+            theme,
+        })
+    }
+
+    pub fn file_json_schema() -> serde_json::Value {
+        serde_json::to_value(schema_for!(SettingsFileContent)).unwrap()
+    }
+
+    pub fn with_overrides(
+        mut self,
+        language_name: impl Into<Arc<str>>,
+        overrides: LanguageOverride,
+    ) -> Self {
+        self.language_overrides
+            .insert(language_name.into(), overrides);
+        self
+    }
+
+    pub fn tab_size(&self, language: Option<&str>) -> u32 {
+        language
+            .and_then(|language| self.language_overrides.get(language))
+            .and_then(|settings| settings.tab_size)
+            .unwrap_or(self.tab_size)
+    }
+
+    pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap {
+        language
+            .and_then(|language| self.language_overrides.get(language))
+            .and_then(|settings| settings.soft_wrap)
+            .unwrap_or(self.soft_wrap)
+    }
+
+    pub fn preferred_line_length(&self, language: Option<&str>) -> u32 {
+        language
+            .and_then(|language| self.language_overrides.get(language))
+            .and_then(|settings| settings.preferred_line_length)
+            .unwrap_or(self.preferred_line_length)
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn test(cx: &gpui::AppContext) -> Settings {
+        Settings {
+            buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
+            buffer_font_size: 14.,
+            vim_mode: false,
+            tab_size: 4,
+            soft_wrap: SoftWrap::None,
+            preferred_line_length: 80,
+            language_overrides: Default::default(),
+            theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
+        }
+    }
+
+    pub fn merge(
+        &mut self,
+        data: &SettingsFileContent,
+        theme_registry: &ThemeRegistry,
+        font_cache: &FontCache,
+    ) {
+        if let Some(value) = &data.buffer_font_family {
+            if let Some(id) = font_cache.load_family(&[value]).log_err() {
+                self.buffer_font_family = id;
+            }
+        }
+        if let Some(value) = &data.theme {
+            if let Some(theme) = theme_registry.get(value).log_err() {
+                self.theme = theme;
+            }
+        }
+
+        merge(&mut self.buffer_font_size, data.buffer_font_size);
+        merge(&mut self.vim_mode, data.vim_mode);
+        merge(&mut self.soft_wrap, data.editor.soft_wrap);
+        merge(&mut self.tab_size, data.editor.tab_size);
+        merge(
+            &mut self.preferred_line_length,
+            data.editor.preferred_line_length,
+        );
+
+        for (language_name, settings) in &data.language_overrides {
+            let target = self
+                .language_overrides
+                .entry(language_name.clone())
+                .or_default();
+
+            merge_option(&mut target.tab_size, settings.tab_size);
+            merge_option(&mut target.soft_wrap, settings.soft_wrap);
+            merge_option(
+                &mut target.preferred_line_length,
+                settings.preferred_line_length,
+            );
+        }
+    }
+}
+
+fn merge<T: Copy>(target: &mut T, value: Option<T>) {
+    if let Some(value) = value {
+        *target = value;
+    }
+}
+
+fn merge_option<T: Copy>(target: &mut Option<T>, value: Option<T>) {
+    if value.is_some() {
+        *target = value;
+    }
+}

crates/theme_selector/Cargo.toml šŸ”—

@@ -12,6 +12,7 @@ editor = { path = "../editor" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 theme = { path = "../theme" }
+settings = { path = "../settings" }
 workspace = { path = "../workspace" }
 log = "0.4"
 parking_lot = "0.11.1"

crates/theme_selector/src/theme_selector.rs šŸ”—

@@ -9,9 +9,10 @@ use gpui::{
 };
 use std::{cmp, sync::Arc};
 use theme::{Theme, ThemeRegistry};
+use settings::Settings;
 use workspace::{
     menu::{Confirm, SelectNext, SelectPrev},
-    Settings, Workspace,
+    Workspace,
 };
 
 pub struct ThemeSelector {

crates/vim/Cargo.toml šŸ”—

@@ -12,6 +12,7 @@ collections = { path = "../collections" }
 editor = { path = "../editor" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
+settings = { path = "../settings" }
 workspace = { path = "../workspace" }
 log = "0.4"
 
@@ -19,7 +20,8 @@ log = "0.4"
 indoc = "1.0.4"
 editor = { path = "../editor", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
-project = { path = "../project", features = ["test-support"] }
 language = { path = "../language", features = ["test-support"] }
+project = { path = "../project", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
+settings = { path = "../settings" }
 workspace = { path = "../workspace", features = ["test-support"] }

crates/vim/src/vim.rs šŸ”—

@@ -10,7 +10,8 @@ use editor::{CursorShape, Editor};
 use gpui::{action, MutableAppContext, ViewContext, WeakViewHandle};
 
 use mode::Mode;
-use workspace::{self, Settings, Workspace};
+use settings::Settings;
+use workspace::{self, Workspace};
 
 action!(SwitchMode, Mode);
 

crates/workspace/Cargo.toml šŸ”—

@@ -17,6 +17,7 @@ collections = { path = "../collections" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
 project = { path = "../project" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 anyhow = "1.0.38"
@@ -24,7 +25,6 @@ futures = "0.3"
 log = "0.4"
 parking_lot = "0.11.1"
 postage = { version = "0.4.1", features = ["futures-traits"] }
-schemars = "0.8"
 serde = { version = "1", features = ["derive", "rc"] }
 serde_json = { version = "1", features = ["preserve_order"] }
 smallvec = { version = "1.6", features = ["union"] }
@@ -33,3 +33,4 @@ smallvec = { version = "1.6", features = ["union"] }
 client = { path = "../client", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }

crates/workspace/src/lsp_status.rs šŸ”—

@@ -1,4 +1,4 @@
-use crate::{ItemHandle, Settings, StatusItemView};
+use crate::{ItemHandle, StatusItemView};
 use futures::StreamExt;
 use gpui::AppContext;
 use gpui::{
@@ -7,6 +7,7 @@ use gpui::{
 };
 use language::{LanguageRegistry, LanguageServerBinaryStatus};
 use project::{LanguageServerProgress, Project};
+use settings::Settings;
 use smallvec::SmallVec;
 use std::cmp::Reverse;
 use std::fmt::Write;

crates/workspace/src/pane.rs šŸ”—

@@ -1,5 +1,5 @@
 use super::{ItemHandle, SplitDirection};
-use crate::{toolbar::Toolbar, Item, Settings, WeakItemHandle, Workspace};
+use crate::{toolbar::Toolbar, Item, WeakItemHandle, Workspace};
 use anyhow::Result;
 use collections::{HashMap, VecDeque};
 use futures::StreamExt;
@@ -13,6 +13,7 @@ use gpui::{
     ViewContext, ViewHandle, WeakViewHandle,
 };
 use project::{ProjectEntryId, ProjectPath};
+use settings::Settings;
 use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
 use util::ResultExt;
 

crates/workspace/src/settings.rs šŸ”—

@@ -1,325 +0,0 @@
-use anyhow::Result;
-use futures::{stream, SinkExt, StreamExt as _};
-use gpui::{
-    executor,
-    font_cache::{FamilyId, FontCache},
-};
-use language::Language;
-use postage::{prelude::Stream, watch};
-use project::Fs;
-use schemars::{schema_for, JsonSchema};
-use serde::Deserialize;
-use std::{collections::HashMap, path::Path, sync::Arc, time::Duration};
-use theme::{Theme, ThemeRegistry};
-use util::ResultExt;
-
-#[derive(Clone)]
-pub struct Settings {
-    pub buffer_font_family: FamilyId,
-    pub buffer_font_size: f32,
-    pub vim_mode: bool,
-    pub tab_size: usize,
-    pub soft_wrap: SoftWrap,
-    pub preferred_line_length: u32,
-    pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
-    pub theme: Arc<Theme>,
-}
-
-#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
-pub struct LanguageOverride {
-    pub tab_size: Option<usize>,
-    pub soft_wrap: Option<SoftWrap>,
-    pub preferred_line_length: Option<u32>,
-}
-
-#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum SoftWrap {
-    None,
-    EditorWidth,
-    PreferredLineLength,
-}
-
-#[derive(Clone)]
-pub struct SettingsFile(watch::Receiver<SettingsFileContent>);
-
-#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
-struct SettingsFileContent {
-    #[serde(default)]
-    buffer_font_family: Option<String>,
-    #[serde(default)]
-    buffer_font_size: Option<f32>,
-    #[serde(default)]
-    vim_mode: Option<bool>,
-    #[serde(flatten)]
-    editor: LanguageOverride,
-    #[serde(default)]
-    language_overrides: HashMap<Arc<str>, LanguageOverride>,
-    #[serde(default)]
-    theme: Option<String>,
-}
-
-impl SettingsFile {
-    pub async fn new(
-        fs: Arc<dyn Fs>,
-        executor: &executor::Background,
-        path: impl Into<Arc<Path>>,
-    ) -> Self {
-        let path = path.into();
-        let settings = Self::load(fs.clone(), &path).await.unwrap_or_default();
-        let mut events = fs.watch(&path, Duration::from_millis(500)).await;
-        let (mut tx, rx) = watch::channel_with(settings);
-        executor
-            .spawn(async move {
-                while events.next().await.is_some() {
-                    if let Some(settings) = Self::load(fs.clone(), &path).await {
-                        if tx.send(settings).await.is_err() {
-                            break;
-                        }
-                    }
-                }
-            })
-            .detach();
-        Self(rx)
-    }
-
-    async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<SettingsFileContent> {
-        if fs.is_file(&path).await {
-            fs.load(&path)
-                .await
-                .log_err()
-                .and_then(|data| serde_json::from_str(&data).log_err())
-        } else {
-            Some(SettingsFileContent::default())
-        }
-    }
-}
-
-impl Settings {
-    pub fn file_json_schema() -> serde_json::Value {
-        serde_json::to_value(schema_for!(SettingsFileContent)).unwrap()
-    }
-
-    pub fn from_files(
-        defaults: Self,
-        sources: Vec<SettingsFile>,
-        theme_registry: Arc<ThemeRegistry>,
-        font_cache: Arc<FontCache>,
-    ) -> impl futures::stream::Stream<Item = Self> {
-        stream::select_all(sources.iter().enumerate().map(|(i, source)| {
-            let mut rx = source.0.clone();
-            // Consume the initial item from all of the constituent file watches but one.
-            // This way, the stream will yield exactly one item for the files' initial
-            // state, and won't return any more items until the files change.
-            if i > 0 {
-                rx.try_recv().ok();
-            }
-            rx
-        }))
-        .map(move |_| {
-            let mut settings = defaults.clone();
-            for source in &sources {
-                settings.merge(&*source.0.borrow(), &theme_registry, &font_cache);
-            }
-            settings
-        })
-    }
-
-    pub fn new(
-        buffer_font_family: &str,
-        font_cache: &FontCache,
-        theme: Arc<Theme>,
-    ) -> Result<Self> {
-        Ok(Self {
-            buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
-            buffer_font_size: 15.,
-            vim_mode: false,
-            tab_size: 4,
-            soft_wrap: SoftWrap::None,
-            preferred_line_length: 80,
-            language_overrides: Default::default(),
-            theme,
-        })
-    }
-
-    pub fn with_overrides(
-        mut self,
-        language_name: impl Into<Arc<str>>,
-        overrides: LanguageOverride,
-    ) -> Self {
-        self.language_overrides
-            .insert(language_name.into(), overrides);
-        self
-    }
-
-    pub fn tab_size(&self, language: Option<&Arc<Language>>) -> usize {
-        language
-            .and_then(|language| self.language_overrides.get(language.name().as_ref()))
-            .and_then(|settings| settings.tab_size)
-            .unwrap_or(self.tab_size)
-    }
-
-    pub fn soft_wrap(&self, language: Option<&Arc<Language>>) -> SoftWrap {
-        language
-            .and_then(|language| self.language_overrides.get(language.name().as_ref()))
-            .and_then(|settings| settings.soft_wrap)
-            .unwrap_or(self.soft_wrap)
-    }
-
-    pub fn preferred_line_length(&self, language: Option<&Arc<Language>>) -> u32 {
-        language
-            .and_then(|language| self.language_overrides.get(language.name().as_ref()))
-            .and_then(|settings| settings.preferred_line_length)
-            .unwrap_or(self.preferred_line_length)
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn test(cx: &gpui::AppContext) -> Settings {
-        Settings {
-            buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
-            buffer_font_size: 14.,
-            vim_mode: false,
-            tab_size: 4,
-            soft_wrap: SoftWrap::None,
-            preferred_line_length: 80,
-            language_overrides: Default::default(),
-            theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
-        }
-    }
-
-    fn merge(
-        &mut self,
-        data: &SettingsFileContent,
-        theme_registry: &ThemeRegistry,
-        font_cache: &FontCache,
-    ) {
-        if let Some(value) = &data.buffer_font_family {
-            if let Some(id) = font_cache.load_family(&[value]).log_err() {
-                self.buffer_font_family = id;
-            }
-        }
-        if let Some(value) = &data.theme {
-            if let Some(theme) = theme_registry.get(value).log_err() {
-                self.theme = theme;
-            }
-        }
-
-        merge(&mut self.buffer_font_size, data.buffer_font_size);
-        merge(&mut self.vim_mode, data.vim_mode);
-        merge(&mut self.soft_wrap, data.editor.soft_wrap);
-        merge(&mut self.tab_size, data.editor.tab_size);
-        merge(
-            &mut self.preferred_line_length,
-            data.editor.preferred_line_length,
-        );
-
-        for (language_name, settings) in &data.language_overrides {
-            let target = self
-                .language_overrides
-                .entry(language_name.clone())
-                .or_default();
-
-            merge_option(&mut target.tab_size, settings.tab_size);
-            merge_option(&mut target.soft_wrap, settings.soft_wrap);
-            merge_option(
-                &mut target.preferred_line_length,
-                settings.preferred_line_length,
-            );
-        }
-    }
-}
-
-fn merge<T: Copy>(target: &mut T, value: Option<T>) {
-    if let Some(value) = value {
-        *target = value;
-    }
-}
-
-fn merge_option<T: Copy>(target: &mut Option<T>, value: Option<T>) {
-    if value.is_some() {
-        *target = value;
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use project::FakeFs;
-
-    #[gpui::test]
-    async fn test_settings_from_files(cx: &mut gpui::TestAppContext) {
-        let executor = cx.background();
-        let fs = FakeFs::new(executor.clone());
-
-        fs.save(
-            "/settings1.json".as_ref(),
-            &r#"
-            {
-                "buffer_font_size": 24,
-                "soft_wrap": "editor_width",
-                "language_overrides": {
-                    "Markdown": {
-                        "preferred_line_length": 100,
-                        "soft_wrap": "preferred_line_length"
-                    }
-                }
-            }
-            "#
-            .into(),
-        )
-        .await
-        .unwrap();
-
-        let source1 = SettingsFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await;
-        let source2 = SettingsFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await;
-        let source3 = SettingsFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await;
-
-        let mut settings_rx = Settings::from_files(
-            cx.read(Settings::test),
-            vec![source1, source2, source3],
-            ThemeRegistry::new((), cx.font_cache()),
-            cx.font_cache(),
-        );
-
-        let settings = settings_rx.next().await.unwrap();
-        let md_settings = settings.language_overrides.get("Markdown").unwrap();
-        assert_eq!(settings.soft_wrap, SoftWrap::EditorWidth);
-        assert_eq!(settings.buffer_font_size, 24.0);
-        assert_eq!(settings.tab_size, 4);
-        assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
-        assert_eq!(md_settings.preferred_line_length, Some(100));
-
-        fs.save(
-            "/settings2.json".as_ref(),
-            &r#"
-            {
-                "tab_size": 2,
-                "soft_wrap": "none",
-                "language_overrides": {
-                    "Markdown": {
-                        "preferred_line_length": 120
-                    }
-                }
-            }
-            "#
-            .into(),
-        )
-        .await
-        .unwrap();
-
-        let settings = settings_rx.next().await.unwrap();
-        let md_settings = settings.language_overrides.get("Markdown").unwrap();
-        assert_eq!(settings.soft_wrap, SoftWrap::None);
-        assert_eq!(settings.buffer_font_size, 24.0);
-        assert_eq!(settings.tab_size, 2);
-        assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
-        assert_eq!(md_settings.preferred_line_length, Some(120));
-
-        fs.remove_file("/settings2.json".as_ref(), Default::default())
-            .await
-            .unwrap();
-
-        let settings = settings_rx.next().await.unwrap();
-        assert_eq!(settings.tab_size, 4);
-    }
-}

crates/workspace/src/status_bar.rs šŸ”—

@@ -1,4 +1,5 @@
-use crate::{ItemHandle, Pane, Settings};
+use crate::{ItemHandle, Pane};
+use settings::Settings;
 use gpui::{
     elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, Subscription,
     View, ViewContext, ViewHandle,

crates/workspace/src/toolbar.rs šŸ”—

@@ -1,8 +1,9 @@
-use crate::{ItemHandle, Settings};
+use crate::ItemHandle;
 use gpui::{
     elements::*, AnyViewHandle, AppContext, ElementBox, Entity, MutableAppContext, RenderContext,
     View, ViewContext, ViewHandle,
 };
+use settings::Settings;
 
 pub trait ToolbarItemView: View {
     fn set_active_pane_item(

crates/workspace/src/workspace.rs šŸ”—

@@ -2,7 +2,6 @@ pub mod lsp_status;
 pub mod menu;
 pub mod pane;
 pub mod pane_group;
-pub mod settings;
 pub mod sidebar;
 mod status_bar;
 mod toolbar;
@@ -31,7 +30,7 @@ pub use pane::*;
 pub use pane_group::*;
 use postage::prelude::Stream;
 use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree};
-pub use settings::Settings;
+use settings::Settings;
 use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus};
 use status_bar::StatusBar;
 pub use status_bar::StatusItemView;

crates/zed/Cargo.toml šŸ”—

@@ -51,6 +51,7 @@ project = { path = "../project" }
 project_panel = { path = "../project_panel" }
 project_symbols = { path = "../project_symbols" }
 rpc = { path = "../rpc" }
+settings = { path = "../settings" }
 sum_tree = { path = "../sum_tree" }
 text = { path = "../text" }
 theme = { path = "../theme" }
@@ -111,6 +112,7 @@ lsp = { path = "../lsp", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
 rpc = { path = "../rpc", features = ["test-support"] }
 client = { path = "../client", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }
 env_logger = "0.8"

crates/zed/src/main.rs šŸ”—

@@ -9,17 +9,19 @@ use gpui::{App, AssetSource, Task};
 use log::LevelFilter;
 use parking_lot::Mutex;
 use project::Fs;
+use settings::{self, Settings};
 use smol::process::Command;
 use std::{env, fs, path::PathBuf, sync::Arc};
 use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
 use util::ResultExt;
-use workspace::{
-    self,
-    settings::{self, SettingsFile},
-    AppState, OpenNew, OpenParams, OpenPaths, Settings,
-};
+use workspace::{self, AppState, OpenNew, OpenParams, OpenPaths};
 use zed::{
-    self, assets::Assets, build_window_options, build_workspace, fs::RealFs, languages, menus,
+    self,
+    assets::Assets,
+    build_window_options, build_workspace,
+    fs::RealFs,
+    languages, menus,
+    settings_file::{settings_from_files, SettingsFile},
 };
 
 fn main() {
@@ -46,6 +48,20 @@ fn main() {
                 soft_wrap: Some(settings::SoftWrap::PreferredLineLength),
                 ..Default::default()
             },
+        )
+        .with_overrides(
+            "Rust",
+            settings::LanguageOverride {
+                tab_size: Some(4),
+                ..Default::default()
+            },
+        )
+        .with_overrides(
+            "TypeScript",
+            settings::LanguageOverride {
+                tab_size: Some(2),
+                ..Default::default()
+            },
         );
     let settings_file = load_settings_file(&app, fs.clone());
 
@@ -97,7 +113,7 @@ fn main() {
         .detach_and_log_err(cx);
 
         let settings_file = cx.background().block(settings_file).unwrap();
-        let mut settings_rx = Settings::from_files(
+        let mut settings_rx = settings_from_files(
             default_settings,
             vec![settings_file],
             themes.clone(),

crates/zed/src/settings_file.rs šŸ”—

@@ -0,0 +1,157 @@
+use futures::{stream, StreamExt};
+use gpui::{executor, FontCache};
+use postage::sink::Sink as _;
+use postage::{prelude::Stream, watch};
+use project::Fs;
+use settings::{Settings, SettingsFileContent};
+use std::{path::Path, sync::Arc, time::Duration};
+use theme::ThemeRegistry;
+use util::ResultExt;
+
+#[derive(Clone)]
+pub struct SettingsFile(watch::Receiver<SettingsFileContent>);
+
+impl SettingsFile {
+    pub async fn new(
+        fs: Arc<dyn Fs>,
+        executor: &executor::Background,
+        path: impl Into<Arc<Path>>,
+    ) -> Self {
+        let path = path.into();
+        let settings = Self::load(fs.clone(), &path).await.unwrap_or_default();
+        let mut events = fs.watch(&path, Duration::from_millis(500)).await;
+        let (mut tx, rx) = watch::channel_with(settings);
+        executor
+            .spawn(async move {
+                while events.next().await.is_some() {
+                    if let Some(settings) = Self::load(fs.clone(), &path).await {
+                        if tx.send(settings).await.is_err() {
+                            break;
+                        }
+                    }
+                }
+            })
+            .detach();
+        Self(rx)
+    }
+
+    async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<SettingsFileContent> {
+        if fs.is_file(&path).await {
+            fs.load(&path)
+                .await
+                .log_err()
+                .and_then(|data| serde_json::from_str(&data).log_err())
+        } else {
+            Some(SettingsFileContent::default())
+        }
+    }
+}
+
+pub fn settings_from_files(
+    defaults: Settings,
+    sources: Vec<SettingsFile>,
+    theme_registry: Arc<ThemeRegistry>,
+    font_cache: Arc<FontCache>,
+) -> impl futures::stream::Stream<Item = Settings> {
+    stream::select_all(sources.iter().enumerate().map(|(i, source)| {
+        let mut rx = source.0.clone();
+        // Consume the initial item from all of the constituent file watches but one.
+        // This way, the stream will yield exactly one item for the files' initial
+        // state, and won't return any more items until the files change.
+        if i > 0 {
+            rx.try_recv().ok();
+        }
+        rx
+    }))
+    .map(move |_| {
+        let mut settings = defaults.clone();
+        for source in &sources {
+            settings.merge(&*source.0.borrow(), &theme_registry, &font_cache);
+        }
+        settings
+    })
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use project::FakeFs;
+    use settings::SoftWrap;
+
+    #[gpui::test]
+    async fn test_settings_from_files(cx: &mut gpui::TestAppContext) {
+        let executor = cx.background();
+        let fs = FakeFs::new(executor.clone());
+
+        fs.save(
+            "/settings1.json".as_ref(),
+            &r#"
+            {
+                "buffer_font_size": 24,
+                "soft_wrap": "editor_width",
+                "language_overrides": {
+                    "Markdown": {
+                        "preferred_line_length": 100,
+                        "soft_wrap": "preferred_line_length"
+                    }
+                }
+            }
+            "#
+            .into(),
+        )
+        .await
+        .unwrap();
+
+        let source1 = SettingsFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await;
+        let source2 = SettingsFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await;
+        let source3 = SettingsFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await;
+
+        let mut settings_rx = settings_from_files(
+            cx.read(Settings::test),
+            vec![source1, source2, source3],
+            ThemeRegistry::new((), cx.font_cache()),
+            cx.font_cache(),
+        );
+
+        let settings = settings_rx.next().await.unwrap();
+        let md_settings = settings.language_overrides.get("Markdown").unwrap();
+        assert_eq!(settings.soft_wrap, SoftWrap::EditorWidth);
+        assert_eq!(settings.buffer_font_size, 24.0);
+        assert_eq!(settings.tab_size, 4);
+        assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
+        assert_eq!(md_settings.preferred_line_length, Some(100));
+
+        fs.save(
+            "/settings2.json".as_ref(),
+            &r#"
+            {
+                "tab_size": 2,
+                "soft_wrap": "none",
+                "language_overrides": {
+                    "Markdown": {
+                        "preferred_line_length": 120
+                    }
+                }
+            }
+            "#
+            .into(),
+        )
+        .await
+        .unwrap();
+
+        let settings = settings_rx.next().await.unwrap();
+        let md_settings = settings.language_overrides.get("Markdown").unwrap();
+        assert_eq!(settings.soft_wrap, SoftWrap::None);
+        assert_eq!(settings.buffer_font_size, 24.0);
+        assert_eq!(settings.tab_size, 2);
+        assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
+        assert_eq!(md_settings.preferred_line_length, Some(120));
+
+        fs.remove_file("/settings2.json".as_ref(), Default::default())
+            .await
+            .unwrap();
+
+        let settings = settings_rx.next().await.unwrap();
+        assert_eq!(settings.tab_size, 4);
+    }
+}

crates/zed/src/test.rs šŸ”—

@@ -3,9 +3,9 @@ use client::{test::FakeHttpClient, ChannelList, Client, UserStore};
 use gpui::MutableAppContext;
 use language::LanguageRegistry;
 use project::fs::FakeFs;
+use settings::Settings;
 use std::sync::Arc;
 use theme::ThemeRegistry;
-use workspace::Settings;
 
 #[cfg(test)]
 #[ctor::ctor]

crates/zed/src/zed.rs šŸ”—

@@ -1,6 +1,7 @@
 pub mod assets;
 pub mod languages;
 pub mod menus;
+pub mod settings_file;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
 
@@ -23,9 +24,10 @@ use project::Project;
 pub use project::{self, fs};
 use project_panel::ProjectPanel;
 use search::{BufferSearchBar, ProjectSearchBar};
+use settings::Settings;
 use std::{path::PathBuf, sync::Arc};
 pub use workspace;
-use workspace::{AppState, Settings, Workspace, WorkspaceParams};
+use workspace::{AppState, Workspace, WorkspaceParams};
 
 action!(About);
 action!(Quit);
@@ -576,7 +578,7 @@ mod tests {
             assert!(!editor.is_dirty(cx));
             assert_eq!(editor.title(cx), "untitled");
             assert!(Arc::ptr_eq(
-                editor.language(cx).unwrap(),
+                editor.language_at(0, cx).unwrap(),
                 &languages::PLAIN_TEXT
             ));
             editor.handle_input(&editor::Input("hi".into()), cx);
@@ -600,7 +602,7 @@ mod tests {
         editor.read_with(cx, |editor, cx| {
             assert!(!editor.is_dirty(cx));
             assert_eq!(editor.title(cx), "the-new-name.rs");
-            assert_eq!(editor.language(cx).unwrap().name().as_ref(), "Rust");
+            assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust");
         });
 
         // Edit the file and save it again. This time, there is no filename prompt.
@@ -666,7 +668,7 @@ mod tests {
 
         editor.update(cx, |editor, cx| {
             assert!(Arc::ptr_eq(
-                editor.language(cx).unwrap(),
+                editor.language_at(0, cx).unwrap(),
                 &languages::PLAIN_TEXT
             ));
             editor.handle_input(&editor::Input("hi".into()), cx);
@@ -680,7 +682,7 @@ mod tests {
         // The buffer is not dirty anymore and the language is assigned based on the path.
         editor.read_with(cx, |editor, cx| {
             assert!(!editor.is_dirty(cx));
-            assert_eq!(editor.language(cx).unwrap().name().as_ref(), "Rust")
+            assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust")
         });
     }
 

styles/src/buildThemes.ts šŸ”—

@@ -7,11 +7,11 @@ import snakeCase from "./utils/snakeCase";
 
 const themes = [dark, light];
 for (let theme of themes) {
-    let styleTree = snakeCase(app(theme));
-    let styleTreeJSON = JSON.stringify(styleTree, null, 2);
-    let outPath = path.resolve(
-        `${__dirname}/../../crates/zed/assets/themes/${theme.name}.json`
-    );
-    fs.writeFileSync(outPath, styleTreeJSON);
-    console.log(`- ${outPath} created`);
+  let styleTree = snakeCase(app(theme));
+  let styleTreeJSON = JSON.stringify(styleTree, null, 2);
+  let outPath = path.resolve(
+    `${__dirname}/../../crates/zed/assets/themes/${theme.name}.json`
+  );
+  fs.writeFileSync(outPath, styleTreeJSON);
+  console.log(`- ${outPath} created`);
 }

styles/src/buildTokens.ts šŸ”—

@@ -7,80 +7,80 @@ import { colors, fontFamilies, fontSizes, fontWeights } from "./tokens";
 
 // Organize theme tokens
 function themeTokens(theme: Theme) {
-    return {
-        meta: {
-            themeName: theme.name,
-        },
-        text: theme.textColor,
-        icon: theme.iconColor,
-        background: theme.backgroundColor,
-        border: theme.borderColor,
-        editor: theme.editor,
-        syntax: {
-            primary: {
-                value: theme.syntax.primary.color.value,
-                type: "color",
-            },
-            comment: {
-                value: theme.syntax.comment.color.value,
-                type: "color",
-            },
-            keyword: {
-                value: theme.syntax.keyword.color.value,
-                type: "color",
-            },
-            function: {
-                value: theme.syntax.function.color.value,
-                type: "color",
-            },
-            type: {
-                value: theme.syntax.type.color.value,
-                type: "color",
-            },
-            variant: {
-                value: theme.syntax.variant.color.value,
-                type: "color",
-            },
-            property: {
-                value: theme.syntax.property.color.value,
-                type: "color",
-            },
-            enum: {
-                value: theme.syntax.enum.color.value,
-                type: "color",
-            },
-            operator: {
-                value: theme.syntax.operator.color.value,
-                type: "color",
-            },
-            string: {
-                value: theme.syntax.string.color.value,
-                type: "color",
-            },
-            number: {
-                value: theme.syntax.number.color.value,
-                type: "color",
-            },
-            boolean: {
-                value: theme.syntax.boolean.color.value,
-                type: "color",
-            },
-        },
-        player: theme.player,
-        shadowAlpha: theme.shadowAlpha,
-    };
+  return {
+    meta: {
+      themeName: theme.name,
+    },
+    text: theme.textColor,
+    icon: theme.iconColor,
+    background: theme.backgroundColor,
+    border: theme.borderColor,
+    editor: theme.editor,
+    syntax: {
+      primary: {
+        value: theme.syntax.primary.color.value,
+        type: "color",
+      },
+      comment: {
+        value: theme.syntax.comment.color.value,
+        type: "color",
+      },
+      keyword: {
+        value: theme.syntax.keyword.color.value,
+        type: "color",
+      },
+      function: {
+        value: theme.syntax.function.color.value,
+        type: "color",
+      },
+      type: {
+        value: theme.syntax.type.color.value,
+        type: "color",
+      },
+      variant: {
+        value: theme.syntax.variant.color.value,
+        type: "color",
+      },
+      property: {
+        value: theme.syntax.property.color.value,
+        type: "color",
+      },
+      enum: {
+        value: theme.syntax.enum.color.value,
+        type: "color",
+      },
+      operator: {
+        value: theme.syntax.operator.color.value,
+        type: "color",
+      },
+      string: {
+        value: theme.syntax.string.color.value,
+        type: "color",
+      },
+      number: {
+        value: theme.syntax.number.color.value,
+        type: "color",
+      },
+      boolean: {
+        value: theme.syntax.boolean.color.value,
+        type: "color",
+      },
+    },
+    player: theme.player,
+    shadowAlpha: theme.shadowAlpha,
+  };
 }
 
 // Organize core tokens
 const coreTokens = {
-    color: {
-        ...colors,
-    },
-    text: {
-        family: fontFamilies,
-        weight: fontWeights,
-    },
-    size: fontSizes,
+  color: {
+    ...colors,
+  },
+  text: {
+    family: fontFamilies,
+    weight: fontWeights,
+  },
+  size: fontSizes,
 };
 
 const combinedTokens: any = {};
@@ -98,10 +98,10 @@ combinedTokens.core = coreTokens;
 // We write `${theme}.json` as a separate file for the design team's convenience, but it isn't consumed by Figma Tokens directly.
 let themes = [dark, light];
 themes.forEach((theme) => {
-    const themePath = `${distPath}/${theme.name}.json`
-    fs.writeFileSync(themePath, JSON.stringify(themeTokens(theme), null, 2));
-    console.log(`- ${themePath} created`);
-    combinedTokens[theme.name] = themeTokens(theme);
+  const themePath = `${distPath}/${theme.name}.json`
+  fs.writeFileSync(themePath, JSON.stringify(themeTokens(theme), null, 2));
+  console.log(`- ${themePath} created`);
+  combinedTokens[theme.name] = themeTokens(theme);
 });
 
 // Write combined tokens to `tokens.json`. This file is consumed by the Figma Tokens plugin to keep our designs consistent with the app.

styles/src/styleTree/app.ts šŸ”—

@@ -9,35 +9,35 @@ import selectorModal from "./selectorModal";
 import workspace from "./workspace";
 
 export const panel = {
-    padding: { top: 12, left: 12, bottom: 12, right: 12 },
+  padding: { top: 12, left: 12, bottom: 12, right: 12 },
 };
 
 export default function app(theme: Theme): Object {
-    return {
-        selector: selectorModal(theme),
-        workspace: workspace(theme),
-        editor: editor(theme),
-        projectDiagnostics: {
-            tabIconSpacing: 4,
-            tabIconWidth: 13,
-            tabSummarySpacing: 10,
-            emptyMessage: text(theme, "sans", "primary", { size: "lg" }),
-            statusBarItem: {
-                ...text(theme, "sans", "muted"),
-                margin: {
-                    right: 10,
-                },
-            },
+  return {
+    selector: selectorModal(theme),
+    workspace: workspace(theme),
+    editor: editor(theme),
+    projectDiagnostics: {
+      tabIconSpacing: 4,
+      tabIconWidth: 13,
+      tabSummarySpacing: 10,
+      emptyMessage: text(theme, "sans", "primary", { size: "lg" }),
+      statusBarItem: {
+        ...text(theme, "sans", "muted"),
+        margin: {
+          right: 10,
         },
-        projectPanel: projectPanel(theme),
-        chatPanel: chatPanel(theme),
-        contactsPanel: contactsPanel(theme),
-        search: search(theme),
-        breadcrumbs: {
-            ...text(theme, "sans", "primary"),
-            padding: {
-                left: 6,
-            },
-        }
-    };
+      },
+    },
+    projectPanel: projectPanel(theme),
+    chatPanel: chatPanel(theme),
+    contactsPanel: contactsPanel(theme),
+    search: search(theme),
+    breadcrumbs: {
+      ...text(theme, "sans", "primary"),
+      padding: {
+        left: 6,
+      },
+    }
+  };
 }

styles/src/styleTree/chatPanel.ts šŸ”—

@@ -1,108 +1,108 @@
 import Theme from "../themes/theme";
 import { panel } from "./app";
 import {
-    backgroundColor,
-    border,
-    player,
-    shadow,
-    text,
-    TextColor
+  backgroundColor,
+  border,
+  player,
+  shadow,
+  text,
+  TextColor
 } from "./components";
 
 export default function chatPanel(theme: Theme) {
-    function channelSelectItem(
-        theme: Theme,
-        textColor: TextColor,
-        hovered: boolean
-    ) {
-        return {
-            name: text(theme, "sans", textColor),
-            padding: 4,
-            hash: {
-                ...text(theme, "sans", "muted"),
-                margin: {
-                    right: 8,
-                },
-            },
-            background: hovered ? backgroundColor(theme, 300, "hovered") : undefined,
-            cornerRadius: hovered ? 6 : 0,
-        };
-    }
-
-    const message = {
-        body: text(theme, "sans", "secondary"),
-        timestamp: text(theme, "sans", "muted", { size: "sm" }),
-        padding: {
-            bottom: 6,
-        },
-        sender: {
-            ...text(theme, "sans", "primary", { weight: "bold" }),
-            margin: {
-                right: 8,
-            },
+  function channelSelectItem(
+    theme: Theme,
+    textColor: TextColor,
+    hovered: boolean
+  ) {
+    return {
+      name: text(theme, "sans", textColor),
+      padding: 4,
+      hash: {
+        ...text(theme, "sans", "muted"),
+        margin: {
+          right: 8,
         },
+      },
+      background: hovered ? backgroundColor(theme, 300, "hovered") : undefined,
+      cornerRadius: hovered ? 6 : 0,
     };
+  }
 
-    return {
-        ...panel,
-        channelName: text(theme, "sans", "primary", { weight: "bold" }),
-        channelNameHash: {
-            ...text(theme, "sans", "muted"),
-            padding: {
-                right: 8,
-            },
-        },
-        channelSelect: {
-            header: {
-                ...channelSelectItem(theme, "primary", false),
-                padding: {
-                    bottom: 4,
-                    left: 0,
-                },
-            },
-            item: channelSelectItem(theme, "secondary", false),
-            hoveredItem: channelSelectItem(theme, "secondary", true),
-            activeItem: channelSelectItem(theme, "primary", false),
-            hoveredActiveItem: channelSelectItem(theme, "primary", true),
-            menu: {
-                background: backgroundColor(theme, 500),
-                cornerRadius: 6,
-                padding: 4,
-                border: border(theme, "primary"),
-                shadow: shadow(theme),
-            },
-        },
-        signInPrompt: text(theme, "sans", "secondary", { underline: true }),
-        hoveredSignInPrompt: text(theme, "sans", "primary", { underline: true }),
-        message,
-        pendingMessage: {
-            ...message,
-            body: {
-                ...message.body,
-                color: theme.textColor.muted.value,
-            },
-            sender: {
-                ...message.sender,
-                color: theme.textColor.muted.value,
-            },
-            timestamp: {
-                ...message.timestamp,
-                color: theme.textColor.muted.value,
-            },
-        },
-        inputEditor: {
-            background: backgroundColor(theme, 500),
-            cornerRadius: 6,
-            text: text(theme, "mono", "primary"),
-            placeholderText: text(theme, "mono", "placeholder", { size: "sm" }),
-            selection: player(theme, 1).selection,
-            border: border(theme, "secondary"),
-            padding: {
-                bottom: 7,
-                left: 8,
-                right: 8,
-                top: 7,
-            },
+  const message = {
+    body: text(theme, "sans", "secondary"),
+    timestamp: text(theme, "sans", "muted", { size: "sm" }),
+    padding: {
+      bottom: 6,
+    },
+    sender: {
+      ...text(theme, "sans", "primary", { weight: "bold" }),
+      margin: {
+        right: 8,
+      },
+    },
+  };
+
+  return {
+    ...panel,
+    channelName: text(theme, "sans", "primary", { weight: "bold" }),
+    channelNameHash: {
+      ...text(theme, "sans", "muted"),
+      padding: {
+        right: 8,
+      },
+    },
+    channelSelect: {
+      header: {
+        ...channelSelectItem(theme, "primary", false),
+        padding: {
+          bottom: 4,
+          left: 0,
         },
-    };
+      },
+      item: channelSelectItem(theme, "secondary", false),
+      hoveredItem: channelSelectItem(theme, "secondary", true),
+      activeItem: channelSelectItem(theme, "primary", false),
+      hoveredActiveItem: channelSelectItem(theme, "primary", true),
+      menu: {
+        background: backgroundColor(theme, 500),
+        cornerRadius: 6,
+        padding: 4,
+        border: border(theme, "primary"),
+        shadow: shadow(theme),
+      },
+    },
+    signInPrompt: text(theme, "sans", "secondary", { underline: true }),
+    hoveredSignInPrompt: text(theme, "sans", "primary", { underline: true }),
+    message,
+    pendingMessage: {
+      ...message,
+      body: {
+        ...message.body,
+        color: theme.textColor.muted.value,
+      },
+      sender: {
+        ...message.sender,
+        color: theme.textColor.muted.value,
+      },
+      timestamp: {
+        ...message.timestamp,
+        color: theme.textColor.muted.value,
+      },
+    },
+    inputEditor: {
+      background: backgroundColor(theme, 500),
+      cornerRadius: 6,
+      text: text(theme, "mono", "primary"),
+      placeholderText: text(theme, "mono", "placeholder", { size: "sm" }),
+      selection: player(theme, 1).selection,
+      border: border(theme, "secondary"),
+      padding: {
+        bottom: 7,
+        left: 8,
+        right: 8,
+        top: 7,
+      },
+    },
+  };
 }

styles/src/styleTree/components.ts šŸ”—

@@ -5,89 +5,89 @@ import { Color } from "../utils/color";
 
 export type TextColor = keyof Theme["textColor"];
 export function text(
-    theme: Theme,
-    fontFamily: keyof typeof fontFamilies,
-    color: TextColor,
-    properties?: {
-        size?: keyof typeof fontSizes;
-        weight?: FontWeight;
-        underline?: boolean;
-    }
+  theme: Theme,
+  fontFamily: keyof typeof fontFamilies,
+  color: TextColor,
+  properties?: {
+    size?: keyof typeof fontSizes;
+    weight?: FontWeight;
+    underline?: boolean;
+  }
 ) {
-    let size = fontSizes[properties?.size || "sm"].value;
-    return {
-        family: fontFamilies[fontFamily].value,
-        color: theme.textColor[color].value,
-        ...properties,
-        size,
-    };
+  let size = fontSizes[properties?.size || "sm"].value;
+  return {
+    family: fontFamilies[fontFamily].value,
+    color: theme.textColor[color].value,
+    ...properties,
+    size,
+  };
 }
 export function textColor(theme: Theme, color: TextColor) {
-    return theme.textColor[color].value;
+  return theme.textColor[color].value;
 }
 
 export type BorderColor = keyof Theme["borderColor"];
 export interface BorderOptions {
-    width?: number;
-    top?: boolean;
-    bottom?: boolean;
-    left?: boolean;
-    right?: boolean;
-    overlay?: boolean;
+  width?: number;
+  top?: boolean;
+  bottom?: boolean;
+  left?: boolean;
+  right?: boolean;
+  overlay?: boolean;
 }
 export function border(
-    theme: Theme,
-    color: BorderColor,
-    options?: BorderOptions
+  theme: Theme,
+  color: BorderColor,
+  options?: BorderOptions
 ) {
-    return {
-        color: borderColor(theme, color),
-        width: 1,
-        ...options,
-    };
+  return {
+    color: borderColor(theme, color),
+    width: 1,
+    ...options,
+  };
 }
 export function borderColor(theme: Theme, color: BorderColor) {
-    return theme.borderColor[color].value;
+  return theme.borderColor[color].value;
 }
 
 export type IconColor = keyof Theme["iconColor"];
 export function iconColor(theme: Theme, color: IconColor) {
-    return theme.iconColor[color].value;
+  return theme.iconColor[color].value;
 }
 
 export type PlayerIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
 export interface Player {
-    selection: {
-        cursor: Color;
-        selection: Color;
-    };
+  selection: {
+    cursor: Color;
+    selection: Color;
+  };
 }
 export function player(
-    theme: Theme,
-    playerNumber: PlayerIndex,
+  theme: Theme,
+  playerNumber: PlayerIndex,
 ): Player {
-    return {
-        selection: {
-            cursor: theme.player[playerNumber].cursorColor.value,
-            selection: theme.player[playerNumber].selectionColor.value,
-        },
-    };
+  return {
+    selection: {
+      cursor: theme.player[playerNumber].cursorColor.value,
+      selection: theme.player[playerNumber].selectionColor.value,
+    },
+  };
 }
 
 export type BackgroundColor = keyof Theme["backgroundColor"];
 export type BackgroundState = keyof BackgroundColorSet;
 export function backgroundColor(
-    theme: Theme,
-    name: BackgroundColor,
-    state?: BackgroundState,
+  theme: Theme,
+  name: BackgroundColor,
+  state?: BackgroundState,
 ): Color {
-    return theme.backgroundColor[name][state || "base"].value;
+  return theme.backgroundColor[name][state || "base"].value;
 }
 
 export function shadow(theme: Theme) {
-    return {
-        blur: 16,
-        color: chroma("black").alpha(theme.shadowAlpha.value).hex(),
-        offset: [0, 2],
-    };
+  return {
+    blur: 16,
+    color: chroma("black").alpha(theme.shadowAlpha.value).hex(),
+    offset: [0, 2],
+  };
 }

styles/src/styleTree/contactsPanel.ts šŸ”—

@@ -3,60 +3,60 @@ import { panel } from "./app";
 import { backgroundColor, borderColor, text } from "./components";
 
 export default function(theme: Theme) {
-    const project = {
-        guestAvatarSpacing: 4,
-        height: 24,
-        guestAvatar: {
-            cornerRadius: 8,
-            width: 14,
-        },
-        name: {
-            ...text(theme, "mono", "placeholder", { size: "sm" }),
-            margin: {
-                right: 6,
-            },
-        },
-        padding: {
-            left: 8,
-        },
-    };
+  const project = {
+    guestAvatarSpacing: 4,
+    height: 24,
+    guestAvatar: {
+      cornerRadius: 8,
+      width: 14,
+    },
+    name: {
+      ...text(theme, "mono", "placeholder", { size: "sm" }),
+      margin: {
+        right: 6,
+      },
+    },
+    padding: {
+      left: 8,
+    },
+  };
 
-    const sharedProject = {
-        ...project,
-        background: backgroundColor(theme, 300),
-        cornerRadius: 6,
-        name: {
-            ...project.name,
-            ...text(theme, "mono", "secondary", { size: "sm" }),
-        },
-    };
+  const sharedProject = {
+    ...project,
+    background: backgroundColor(theme, 300),
+    cornerRadius: 6,
+    name: {
+      ...project.name,
+      ...text(theme, "mono", "secondary", { size: "sm" }),
+    },
+  };
 
-    return {
-        ...panel,
-        hostRowHeight: 28,
-        treeBranchColor: borderColor(theme, "muted"),
-        treeBranchWidth: 1,
-        hostAvatar: {
-            cornerRadius: 10,
-            width: 18,
-        },
-        hostUsername: {
-            ...text(theme, "mono", "primary", { size: "sm" }),
-            padding: {
-                left: 8,
-            },
-        },
-        project,
-        sharedProject,
-        hoveredSharedProject: {
-            ...sharedProject,
-            background: backgroundColor(theme, 300, "hovered"),
-            cornerRadius: 6,
-        },
-        unsharedProject: project,
-        hoveredUnsharedProject: {
-            ...project,
-            cornerRadius: 6,
-        },
-    }
+  return {
+    ...panel,
+    hostRowHeight: 28,
+    treeBranchColor: borderColor(theme, "muted"),
+    treeBranchWidth: 1,
+    hostAvatar: {
+      cornerRadius: 10,
+      width: 18,
+    },
+    hostUsername: {
+      ...text(theme, "mono", "primary", { size: "sm" }),
+      padding: {
+        left: 8,
+      },
+    },
+    project,
+    sharedProject,
+    hoveredSharedProject: {
+      ...sharedProject,
+      background: backgroundColor(theme, 300, "hovered"),
+      cornerRadius: 6,
+    },
+    unsharedProject: project,
+    hoveredUnsharedProject: {
+      ...project,
+      cornerRadius: 6,
+    },
+  }
 }

styles/src/styleTree/editor.ts šŸ”—

@@ -1,146 +1,146 @@
 import Theme from "../themes/theme";
 import {
-    backgroundColor,
-    border,
-    iconColor,
-    player,
-    text,
-    TextColor
+  backgroundColor,
+  border,
+  iconColor,
+  player,
+  text,
+  TextColor
 } from "./components";
 
 export default function editor(theme: Theme) {
-    const autocompleteItem = {
-        cornerRadius: 6,
-        padding: {
-            bottom: 2,
-            left: 6,
-            right: 6,
-            top: 2,
-        },
-    };
-
-    function diagnostic(theme: Theme, color: TextColor) {
-        return {
-            textScaleFactor: 0.857,
-            header: {
-                border: border(theme, "primary", {
-                    top: true,
-                }),
-            },
-            message: {
-                text: text(theme, "sans", color, { size: "sm" }),
-                highlightText: text(theme, "sans", color, {
-                    size: "sm",
-                    weight: "bold",
-                }),
-            },
-        };
-    }
+  const autocompleteItem = {
+    cornerRadius: 6,
+    padding: {
+      bottom: 2,
+      left: 6,
+      right: 6,
+      top: 2,
+    },
+  };
 
+  function diagnostic(theme: Theme, color: TextColor) {
     return {
-        // textColor: theme.syntax.primary.color,
-        textColor: theme.syntax.primary.color.value,
-        background: backgroundColor(theme, 500),
-        activeLineBackground: theme.editor.line.active.value,
-        codeActionsIndicator: iconColor(theme, "muted"),
-        diffBackgroundDeleted: backgroundColor(theme, "error"),
-        diffBackgroundInserted: backgroundColor(theme, "ok"),
-        documentHighlightReadBackground: theme.editor.highlight.occurrence.value,
-        documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence.value,
-        errorColor: theme.textColor.error.value,
-        gutterBackground: backgroundColor(theme, 500),
-        gutterPaddingFactor: 3.5,
-        highlightedLineBackground: theme.editor.line.highlighted.value,
-        lineNumber: theme.editor.gutter.primary.value,
-        lineNumberActive: theme.editor.gutter.active.value,
-        renameFade: 0.6,
-        unnecessaryCodeFade: 0.5,
-        selection: player(theme, 1).selection,
-        guestSelections: [
-            player(theme, 2).selection,
-            player(theme, 3).selection,
-            player(theme, 4).selection,
-            player(theme, 5).selection,
-            player(theme, 6).selection,
-            player(theme, 7).selection,
-            player(theme, 8).selection,
-        ],
-        autocomplete: {
-            background: backgroundColor(theme, 500),
-            cornerRadius: 6,
-            padding: 6,
-            border: border(theme, "secondary"),
-            item: autocompleteItem,
-            hoveredItem: {
-                ...autocompleteItem,
-                background: backgroundColor(theme, 500, "hovered"),
-            },
-            margin: {
-                left: -14,
-            },
-            matchHighlight: text(theme, "mono", "feature"),
-            selectedItem: {
-                ...autocompleteItem,
-                background: backgroundColor(theme, 500, "active"),
-            },
-        },
-        diagnosticHeader: {
-            background: backgroundColor(theme, 300),
-            iconWidthFactor: 1.5,
-            textScaleFactor: 0.857, // NateQ: Will we need dynamic sizing for text? If so let's create tokens for these.
-            border: border(theme, "secondary", {
-                bottom: true,
-                top: true,
-            }),
-            code: {
-                ...text(theme, "mono", "muted", { size: "sm" }),
-                margin: {
-                    left: 10,
-                },
-            },
-            message: {
-                highlightText: text(theme, "sans", "primary", {
-                    size: "sm",
-                    weight: "bold",
-                }),
-                text: text(theme, "sans", "secondary", { size: "sm" }),
-            },
-        },
-        diagnosticPathHeader: {
-            background: theme.editor.line.active.value,
-            textScaleFactor: 0.857,
-            filename: text(theme, "mono", "primary", { size: "sm" }),
-            path: {
-                ...text(theme, "mono", "muted", { size: "sm" }),
-                margin: {
-                    left: 12,
-                },
-            },
+      textScaleFactor: 0.857,
+      header: {
+        border: border(theme, "primary", {
+          top: true,
+        }),
+      },
+      message: {
+        text: text(theme, "sans", color, { size: "sm" }),
+        highlightText: text(theme, "sans", color, {
+          size: "sm",
+          weight: "bold",
+        }),
+      },
+    };
+  }
+
+  return {
+    // textColor: theme.syntax.primary.color,
+    textColor: theme.syntax.primary.color.value,
+    background: backgroundColor(theme, 500),
+    activeLineBackground: theme.editor.line.active.value,
+    codeActionsIndicator: iconColor(theme, "muted"),
+    diffBackgroundDeleted: backgroundColor(theme, "error"),
+    diffBackgroundInserted: backgroundColor(theme, "ok"),
+    documentHighlightReadBackground: theme.editor.highlight.occurrence.value,
+    documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence.value,
+    errorColor: theme.textColor.error.value,
+    gutterBackground: backgroundColor(theme, 500),
+    gutterPaddingFactor: 3.5,
+    highlightedLineBackground: theme.editor.line.highlighted.value,
+    lineNumber: theme.editor.gutter.primary.value,
+    lineNumberActive: theme.editor.gutter.active.value,
+    renameFade: 0.6,
+    unnecessaryCodeFade: 0.5,
+    selection: player(theme, 1).selection,
+    guestSelections: [
+      player(theme, 2).selection,
+      player(theme, 3).selection,
+      player(theme, 4).selection,
+      player(theme, 5).selection,
+      player(theme, 6).selection,
+      player(theme, 7).selection,
+      player(theme, 8).selection,
+    ],
+    autocomplete: {
+      background: backgroundColor(theme, 500),
+      cornerRadius: 6,
+      padding: 6,
+      border: border(theme, "secondary"),
+      item: autocompleteItem,
+      hoveredItem: {
+        ...autocompleteItem,
+        background: backgroundColor(theme, 500, "hovered"),
+      },
+      margin: {
+        left: -14,
+      },
+      matchHighlight: text(theme, "mono", "feature"),
+      selectedItem: {
+        ...autocompleteItem,
+        background: backgroundColor(theme, 500, "active"),
+      },
+    },
+    diagnosticHeader: {
+      background: backgroundColor(theme, 300),
+      iconWidthFactor: 1.5,
+      textScaleFactor: 0.857, // NateQ: Will we need dynamic sizing for text? If so let's create tokens for these.
+      border: border(theme, "secondary", {
+        bottom: true,
+        top: true,
+      }),
+      code: {
+        ...text(theme, "mono", "muted", { size: "sm" }),
+        margin: {
+          left: 10,
         },
-        errorDiagnostic: diagnostic(theme, "error"),
-        warningDiagnostic: diagnostic(theme, "warning"),
-        informationDiagnostic: diagnostic(theme, "info"),
-        hintDiagnostic: diagnostic(theme, "info"),
-        invalidErrorDiagnostic: diagnostic(theme, "muted"),
-        invalidHintDiagnostic: diagnostic(theme, "muted"),
-        invalidInformationDiagnostic: diagnostic(theme, "muted"),
-        invalidWarningDiagnostic: diagnostic(theme, "muted"),
-        syntax: {
-            keyword: theme.syntax.keyword.color.value,
-            function: theme.syntax.function.color.value,
-            string: theme.syntax.string.color.value,
-            type: theme.syntax.type.color.value,
-            number: theme.syntax.number.color.value,
-            comment: theme.syntax.comment.color.value,
-            property: theme.syntax.property.color.value,
-            variant: theme.syntax.variant.color.value,
-            constant: theme.syntax.constant.color.value,
-            title: { color: theme.syntax.title.color.value, weight: "bold" },
-            emphasis: theme.textColor.feature.value,
-            "emphasis.strong": { color: theme.textColor.feature.value, weight: "bold" },
-            link_uri: { color: theme.syntax.linkUrl.color.value, underline: true },
-            link_text: { color: theme.syntax.linkText.color.value, italic: true },
-            list_marker: theme.syntax.punctuation.color.value,
+      },
+      message: {
+        highlightText: text(theme, "sans", "primary", {
+          size: "sm",
+          weight: "bold",
+        }),
+        text: text(theme, "sans", "secondary", { size: "sm" }),
+      },
+    },
+    diagnosticPathHeader: {
+      background: theme.editor.line.active.value,
+      textScaleFactor: 0.857,
+      filename: text(theme, "mono", "primary", { size: "sm" }),
+      path: {
+        ...text(theme, "mono", "muted", { size: "sm" }),
+        margin: {
+          left: 12,
         },
-    };
+      },
+    },
+    errorDiagnostic: diagnostic(theme, "error"),
+    warningDiagnostic: diagnostic(theme, "warning"),
+    informationDiagnostic: diagnostic(theme, "info"),
+    hintDiagnostic: diagnostic(theme, "info"),
+    invalidErrorDiagnostic: diagnostic(theme, "muted"),
+    invalidHintDiagnostic: diagnostic(theme, "muted"),
+    invalidInformationDiagnostic: diagnostic(theme, "muted"),
+    invalidWarningDiagnostic: diagnostic(theme, "muted"),
+    syntax: {
+      keyword: theme.syntax.keyword.color.value,
+      function: theme.syntax.function.color.value,
+      string: theme.syntax.string.color.value,
+      type: theme.syntax.type.color.value,
+      number: theme.syntax.number.color.value,
+      comment: theme.syntax.comment.color.value,
+      property: theme.syntax.property.color.value,
+      variant: theme.syntax.variant.color.value,
+      constant: theme.syntax.constant.color.value,
+      title: { color: theme.syntax.title.color.value, weight: "bold" },
+      emphasis: theme.textColor.feature.value,
+      "emphasis.strong": { color: theme.textColor.feature.value, weight: "bold" },
+      link_uri: { color: theme.syntax.linkUrl.color.value, underline: true },
+      link_text: { color: theme.syntax.linkText.color.value, italic: true },
+      list_marker: theme.syntax.punctuation.color.value,
+    },
+  };
 }

styles/src/styleTree/projectPanel.ts šŸ”—

@@ -4,34 +4,34 @@ import { panel } from "./app";
 import { backgroundColor, iconColor, text, TextColor } from "./components";
 
 export default function projectPanel(theme: Theme) {
-    function entry(theme: Theme, textColor: TextColor, background?: Color) {
-        return {
-            height: 22,
-            background,
-            iconColor: iconColor(theme, "muted"),
-            iconSize: 8,
-            iconSpacing: 8,
-            text: text(theme, "mono", textColor, { size: "sm" }),
-        };
-    }
-
+  function entry(theme: Theme, textColor: TextColor, background?: Color) {
     return {
-        ...panel,
-        entry: entry(theme, "secondary"),
-        hoveredEntry: entry(
-            theme,
-            "secondary",
-            backgroundColor(theme, 300, "hovered")
-        ),
-        selectedEntry: entry(theme, "primary"),
-        hoveredSelectedEntry: entry(
-            theme,
-            "primary",
-            backgroundColor(theme, 300, "hovered")
-        ),
-        padding: {
-            top: 6,
-            left: 12,
-        },
+      height: 22,
+      background,
+      iconColor: iconColor(theme, "muted"),
+      iconSize: 8,
+      iconSpacing: 8,
+      text: text(theme, "mono", textColor, { size: "sm" }),
     };
+  }
+
+  return {
+    ...panel,
+    entry: entry(theme, "secondary"),
+    hoveredEntry: entry(
+      theme,
+      "secondary",
+      backgroundColor(theme, 300, "hovered")
+    ),
+    selectedEntry: entry(theme, "primary"),
+    hoveredSelectedEntry: entry(
+      theme,
+      "primary",
+      backgroundColor(theme, 300, "hovered")
+    ),
+    padding: {
+      top: 6,
+      left: 12,
+    },
+  };
 }

styles/src/styleTree/search.ts šŸ”—

@@ -2,78 +2,78 @@ import Theme from "../themes/theme";
 import { backgroundColor, border, player, text } from "./components";
 
 export default function search(theme: Theme) {
-    const optionButton = {
-        ...text(theme, "mono", "secondary"),
-        background: backgroundColor(theme, 300),
-        cornerRadius: 6,
-        border: border(theme, "primary"),
-        margin: {
-            left: 1,
-            right: 1,
-        },
-        padding: {
-            bottom: 1,
-            left: 6,
-            right: 6,
-            top: 1,
-        },
-    };
+  const optionButton = {
+    ...text(theme, "mono", "secondary"),
+    background: backgroundColor(theme, 300),
+    cornerRadius: 6,
+    border: border(theme, "primary"),
+    margin: {
+      left: 1,
+      right: 1,
+    },
+    padding: {
+      bottom: 1,
+      left: 6,
+      right: 6,
+      top: 1,
+    },
+  };
 
-    const editor = {
-        background: backgroundColor(theme, 500),
-        cornerRadius: 6,
-        minWidth: 200,
-        maxWidth: 500,
-        placeholderText: text(theme, "mono", "placeholder"),
-        selection: player(theme, 1).selection,
-        text: text(theme, "mono", "primary"),
-        border: border(theme, "secondary"),
-        margin: {
-            right: 5,
-        },
-        padding: {
-            top: 3,
-            bottom: 3,
-            left: 14,
-            right: 14,
-        },
-    };
+  const editor = {
+    background: backgroundColor(theme, 500),
+    cornerRadius: 6,
+    minWidth: 200,
+    maxWidth: 500,
+    placeholderText: text(theme, "mono", "placeholder"),
+    selection: player(theme, 1).selection,
+    text: text(theme, "mono", "primary"),
+    border: border(theme, "secondary"),
+    margin: {
+      right: 5,
+    },
+    padding: {
+      top: 3,
+      bottom: 3,
+      left: 14,
+      right: 14,
+    },
+  };
 
-    return {
-        matchBackground: theme.editor.highlight.match.value,
-        tabIconSpacing: 4,
-        tabIconWidth: 14,
-        activeHoveredOptionButton: {
-            ...optionButton,
-            background: backgroundColor(theme, 100),
-        },
-        activeOptionButton: {
-            ...optionButton,
-            background: backgroundColor(theme, 100),
-        },
-        editor,
-        hoveredOptionButton: {
-            ...optionButton,
-            background: backgroundColor(theme, 100),
-        },
-        invalidEditor: {
-            ...editor,
-            border: border(theme, "error"),
-        },
-        matchIndex: {
-            ...text(theme, "mono", "muted"),
-            padding: 6,
-        },
-        optionButton,
-        optionButtonGroup: {
-            padding: {
-                left: 2,
-                right: 2,
-            },
-        },
-        resultsStatus: {
-            ...text(theme, "mono", "primary"),
-            size: 18,
-        },
-    };
+  return {
+    matchBackground: theme.editor.highlight.match.value,
+    tabIconSpacing: 4,
+    tabIconWidth: 14,
+    activeHoveredOptionButton: {
+      ...optionButton,
+      background: backgroundColor(theme, 100),
+    },
+    activeOptionButton: {
+      ...optionButton,
+      background: backgroundColor(theme, 100),
+    },
+    editor,
+    hoveredOptionButton: {
+      ...optionButton,
+      background: backgroundColor(theme, 100),
+    },
+    invalidEditor: {
+      ...editor,
+      border: border(theme, "error"),
+    },
+    matchIndex: {
+      ...text(theme, "mono", "muted"),
+      padding: 6,
+    },
+    optionButton,
+    optionButtonGroup: {
+      padding: {
+        left: 2,
+        right: 2,
+      },
+    },
+    resultsStatus: {
+      ...text(theme, "mono", "primary"),
+      size: 18,
+    },
+  };
 }

styles/src/styleTree/selectorModal.ts šŸ”—

@@ -2,58 +2,58 @@ import Theme from "../themes/theme";
 import { backgroundColor, border, player, shadow, text } from "./components";
 
 export default function selectorModal(theme: Theme): Object {
-    const item = {
-        padding: {
-            bottom: 4,
-            left: 16,
-            right: 16,
-            top: 4,
-        },
-        cornerRadius: 6,
-        text: text(theme, "sans", "secondary"),
-        highlightText: text(theme, "sans", "feature", { weight: "bold" }),
-    };
+  const item = {
+    padding: {
+      bottom: 4,
+      left: 16,
+      right: 16,
+      top: 4,
+    },
+    cornerRadius: 6,
+    text: text(theme, "sans", "secondary"),
+    highlightText: text(theme, "sans", "feature", { weight: "bold" }),
+  };
 
-    const activeItem = {
-        ...item,
-        background: backgroundColor(theme, 300, "active"),
-        text: text(theme, "sans", "primary"),
-    };
+  const activeItem = {
+    ...item,
+    background: backgroundColor(theme, 300, "active"),
+    text: text(theme, "sans", "primary"),
+  };
 
-    return {
-        background: backgroundColor(theme, 300),
-        cornerRadius: 6,
-        padding: 8,
-        item,
-        activeItem,
-        border: border(theme, "primary"),
-        empty: {
-            text: text(theme, "sans", "placeholder"),
-            padding: {
-                bottom: 4,
-                left: 16,
-                right: 16,
-                top: 8,
-            },
-        },
-        inputEditor: {
-            background: backgroundColor(theme, 500),
-            corner_radius: 6,
-            placeholderText: text(theme, "sans", "placeholder"),
-            selection: player(theme, 1).selection,
-            text: text(theme, "mono", "primary"),
-            border: border(theme, "secondary"),
-            padding: {
-                bottom: 7,
-                left: 16,
-                right: 16,
-                top: 7,
-            },
-        },
-        margin: {
-            bottom: 52,
-            top: 52,
-        },
-        shadow: shadow(theme),
-    };
+  return {
+    background: backgroundColor(theme, 300),
+    cornerRadius: 6,
+    padding: 8,
+    item,
+    activeItem,
+    border: border(theme, "primary"),
+    empty: {
+      text: text(theme, "sans", "placeholder"),
+      padding: {
+        bottom: 4,
+        left: 16,
+        right: 16,
+        top: 8,
+      },
+    },
+    inputEditor: {
+      background: backgroundColor(theme, 500),
+      corner_radius: 6,
+      placeholderText: text(theme, "sans", "placeholder"),
+      selection: player(theme, 1).selection,
+      text: text(theme, "mono", "primary"),
+      border: border(theme, "secondary"),
+      padding: {
+        bottom: 7,
+        left: 16,
+        right: 16,
+        top: 7,
+      },
+    },
+    margin: {
+      bottom: 52,
+      top: 52,
+    },
+    shadow: shadow(theme),
+  };
 }

styles/src/styleTree/workspace.ts šŸ”—

@@ -1,150 +1,150 @@
 import Theme from "../themes/theme";
-import { backgroundColor, border, borderColor, iconColor, text } from "./components";
+import { backgroundColor, border, iconColor, text } from "./components";
 
 export default function workspace(theme: Theme) {
-    const signInPrompt = {
-        ...text(theme, "sans", "secondary", { size: "xs" }),
-        underline: true,
-        padding: {
-            right: 8,
-        },
-    };
+  const signInPrompt = {
+    ...text(theme, "sans", "secondary", { size: "xs" }),
+    underline: true,
+    padding: {
+      right: 8,
+    },
+  };
 
-    const tab = {
-        height: 32,
-        background: backgroundColor(theme, 300),
-        iconClose: iconColor(theme, "muted"),
-        iconCloseActive: iconColor(theme, "active"),
-        iconConflict: iconColor(theme, "warning"),
-        iconDirty: iconColor(theme, "info"),
-        iconWidth: 8,
-        spacing: 10,
-        text: text(theme, "mono", "secondary", { size: "sm" }),
-        border: border(theme, "primary", {
-            left: true,
-            bottom: true,
-            overlay: true,
-        }),
-        padding: {
-            left: 12,
-            right: 12,
-        },
-    };
+  const tab = {
+    height: 32,
+    background: backgroundColor(theme, 300),
+    iconClose: iconColor(theme, "muted"),
+    iconCloseActive: iconColor(theme, "active"),
+    iconConflict: iconColor(theme, "warning"),
+    iconDirty: iconColor(theme, "info"),
+    iconWidth: 8,
+    spacing: 10,
+    text: text(theme, "mono", "secondary", { size: "sm" }),
+    border: border(theme, "primary", {
+      left: true,
+      bottom: true,
+      overlay: true,
+    }),
+    padding: {
+      left: 12,
+      right: 12,
+    },
+  };
 
-    const activeTab = {
-        ...tab,
-        background: backgroundColor(theme, 500),
-        text: text(theme, "mono", "active", { size: "sm" }),
-        border: {
-            ...tab.border,
-            bottom: false,
-        },
-    };
+  const activeTab = {
+    ...tab,
+    background: backgroundColor(theme, 500),
+    text: text(theme, "mono", "active", { size: "sm" }),
+    border: {
+      ...tab.border,
+      bottom: false,
+    },
+  };
 
-    const sidebarItem = {
-        height: 32,
-        iconColor: iconColor(theme, "secondary"),
-        iconSize: 18,
-    };
-    const sidebar = {
-        width: 30,
-        background: backgroundColor(theme, 300),
-        border: border(theme, "primary", { right: true }),
-        item: sidebarItem,
-        activeItem: {
-            ...sidebarItem,
-            iconColor: iconColor(theme, "active"),
-        },
-        resizeHandle: {
-            background: border(theme, "primary").color,
-            padding: {
-                left: 1,
-            },
-        },
-    };
+  const sidebarItem = {
+    height: 32,
+    iconColor: iconColor(theme, "secondary"),
+    iconSize: 18,
+  };
+  const sidebar = {
+    width: 30,
+    background: backgroundColor(theme, 300),
+    border: border(theme, "primary", { right: true }),
+    item: sidebarItem,
+    activeItem: {
+      ...sidebarItem,
+      iconColor: iconColor(theme, "active"),
+    },
+    resizeHandle: {
+      background: border(theme, "primary").color,
+      padding: {
+        left: 1,
+      },
+    },
+  };
 
-    return {
-        background: backgroundColor(theme, 300),
-        leaderBorderOpacity: 0.7,
-        leaderBorderWidth: 2.0,
-        tab,
-        activeTab,
-        leftSidebar: {
-            ...sidebar,
-            border: border(theme, "primary", { right: true }),
-        },
-        rightSidebar: {
-            ...sidebar,
-            border: border(theme, "primary", { left: true }),
-        },
-        paneDivider: {
-            color: border(theme, "secondary").color,
-            width: 1,
-        },
-        status_bar: {
-            height: 24,
-            itemSpacing: 8,
-            padding: {
-                left: 6,
-                right: 6,
-            },
-            border: border(theme, "primary", { top: true, overlay: true }),
-            cursorPosition: text(theme, "sans", "muted"),
-            diagnosticMessage: text(theme, "sans", "muted"),
-            lspMessage: text(theme, "sans", "muted"),
-        },
-        titlebar: {
-            avatarWidth: 18,
-            height: 32,
-            background: backgroundColor(theme, 100),
-            shareIconColor: iconColor(theme, "secondary"),
-            shareIconActiveColor: iconColor(theme, "feature"),
-            title: text(theme, "sans", "primary"),
-            avatar: {
-                cornerRadius: 10,
-                border: {
-                    color: "#00000088",
-                    width: 1,
-                },
-            },
-            avatarRibbon: {
-                height: 3,
-                width: 12,
-                // TODO: The background for this ideally should be 
-                // set with a token, not hardcoded in rust
-            },
-            border: border(theme, "primary", { bottom: true }),
-            signInPrompt,
-            hoveredSignInPrompt: {
-                ...signInPrompt,
-                ...text(theme, "sans", "active", { size: "xs" }),
-            },
-            offlineIcon: {
-                color: iconColor(theme, "secondary"),
-                width: 16,
-                padding: {
-                    right: 4,
-                },
-            },
-            outdatedWarning: {
-                ...text(theme, "sans", "warning"),
-                size: 13,
-            },
-        },
-        toolbar: {
-            height: 34,
-            background: backgroundColor(theme, 500),
-            border: border(theme, "secondary", { bottom: true }),
-            itemSpacing: 8,
-            padding: { left: 16, right: 8, top: 4, bottom: 4 },
-        },
-        breadcrumbs: {
-            ...text(theme, "mono", "secondary"),
-            padding: { left: 6 },
+  return {
+    background: backgroundColor(theme, 300),
+    leaderBorderOpacity: 0.7,
+    leaderBorderWidth: 2.0,
+    tab,
+    activeTab,
+    leftSidebar: {
+      ...sidebar,
+      border: border(theme, "primary", { right: true }),
+    },
+    rightSidebar: {
+      ...sidebar,
+      border: border(theme, "primary", { left: true }),
+    },
+    paneDivider: {
+      color: border(theme, "secondary").color,
+      width: 1,
+    },
+    status_bar: {
+      height: 24,
+      itemSpacing: 8,
+      padding: {
+        left: 6,
+        right: 6,
+      },
+      border: border(theme, "primary", { top: true, overlay: true }),
+      cursorPosition: text(theme, "sans", "muted"),
+      diagnosticMessage: text(theme, "sans", "muted"),
+      lspMessage: text(theme, "sans", "muted"),
+    },
+    titlebar: {
+      avatarWidth: 18,
+      height: 32,
+      background: backgroundColor(theme, 100),
+      shareIconColor: iconColor(theme, "secondary"),
+      shareIconActiveColor: iconColor(theme, "feature"),
+      title: text(theme, "sans", "primary"),
+      avatar: {
+        cornerRadius: 10,
+        border: {
+          color: "#00000088",
+          width: 1,
         },
-        disconnectedOverlay: {
-            ...text(theme, "sans", "active"),
-            background: "#000000aa",
+      },
+      avatarRibbon: {
+        height: 3,
+        width: 12,
+        // TODO: The background for this ideally should be 
+        // set with a token, not hardcoded in rust
+      },
+      border: border(theme, "primary", { bottom: true }),
+      signInPrompt,
+      hoveredSignInPrompt: {
+        ...signInPrompt,
+        ...text(theme, "sans", "active", { size: "xs" }),
+      },
+      offlineIcon: {
+        color: iconColor(theme, "secondary"),
+        width: 16,
+        padding: {
+          right: 4,
         },
-    };
+      },
+      outdatedWarning: {
+        ...text(theme, "sans", "warning"),
+        size: 13,
+      },
+    },
+    toolbar: {
+      height: 34,
+      background: backgroundColor(theme, 500),
+      border: border(theme, "secondary", { bottom: true }),
+      itemSpacing: 8,
+      padding: { left: 16, right: 8, top: 4, bottom: 4 },
+    },
+    breadcrumbs: {
+      ...text(theme, "mono", "secondary"),
+      padding: { left: 6 },
+    },
+    disconnectedOverlay: {
+      ...text(theme, "sans", "active"),
+      background: "#000000aa",
+    },
+  };
 }

styles/src/themes/dark.ts šŸ”—

@@ -3,227 +3,227 @@ import { withOpacity } from "../utils/color";
 import Theme, { buildPlayer, Syntax } from "./theme";
 
 const backgroundColor = {
-    100: {
-        base: colors.neutral[750],
-        hovered: colors.neutral[725],
-        active: colors.neutral[800],
-        focused: colors.neutral[675],
-    },
-    300: {
-        base: colors.neutral[800],
-        hovered: colors.neutral[775],
-        active: colors.neutral[750],
-        focused: colors.neutral[775],
-    },
-    500: {
-        base: colors.neutral[900],
-        hovered: withOpacity(colors.neutral[0], 0.08),
-        active: withOpacity(colors.neutral[0], 0.12),
-        focused: colors.neutral[825],
-    },
-    ok: {
-        base: colors.green[600],
-        hovered: colors.green[600],
-        active: colors.green[600],
-        focused: colors.green[600],
-    },
-    error: {
-        base: colors.red[400],
-        hovered: colors.red[400],
-        active: colors.red[400],
-        focused: colors.red[400],
-    },
-    warning: {
-        base: colors.amber[300],
-        hovered: colors.amber[300],
-        active: colors.amber[300],
-        focused: colors.amber[300],
-    },
-    info: {
-        base: colors.blue[500],
-        hovered: colors.blue[500],
-        active: colors.blue[500],
-        focused: colors.blue[500],
-    },
+  100: {
+    base: colors.neutral[750],
+    hovered: colors.neutral[725],
+    active: colors.neutral[800],
+    focused: colors.neutral[675],
+  },
+  300: {
+    base: colors.neutral[800],
+    hovered: colors.neutral[775],
+    active: colors.neutral[750],
+    focused: colors.neutral[775],
+  },
+  500: {
+    base: colors.neutral[900],
+    hovered: withOpacity(colors.neutral[0], 0.08),
+    active: withOpacity(colors.neutral[0], 0.12),
+    focused: colors.neutral[825],
+  },
+  ok: {
+    base: colors.green[600],
+    hovered: colors.green[600],
+    active: colors.green[600],
+    focused: colors.green[600],
+  },
+  error: {
+    base: colors.red[400],
+    hovered: colors.red[400],
+    active: colors.red[400],
+    focused: colors.red[400],
+  },
+  warning: {
+    base: colors.amber[300],
+    hovered: colors.amber[300],
+    active: colors.amber[300],
+    focused: colors.amber[300],
+  },
+  info: {
+    base: colors.blue[500],
+    hovered: colors.blue[500],
+    active: colors.blue[500],
+    focused: colors.blue[500],
+  },
 };
 
 const borderColor = {
-    primary: colors.neutral[875],
-    secondary: colors.neutral[775],
-    muted: colors.neutral[675],
-    focused: colors.neutral[500],
-    active: colors.neutral[900],
-    ok: colors.green[500],
-    error: colors.red[500],
-    warning: colors.amber[500],
-    info: colors.blue[500],
+  primary: colors.neutral[875],
+  secondary: colors.neutral[775],
+  muted: colors.neutral[675],
+  focused: colors.neutral[500],
+  active: colors.neutral[900],
+  ok: colors.green[500],
+  error: colors.red[500],
+  warning: colors.amber[500],
+  info: colors.blue[500],
 };
 
 const textColor = {
-    primary: colors.neutral[50],
-    secondary: colors.neutral[350],
-    muted: colors.neutral[450],
-    placeholder: colors.neutral[650],
-    active: colors.neutral[0],
-    //TODO: (design) define feature and it's correct value
-    feature: colors.sky[500],
-    ok: colors.green[600],
-    error: colors.red[400],
-    warning: colors.amber[300],
-    info: colors.blue[500],
+  primary: colors.neutral[50],
+  secondary: colors.neutral[350],
+  muted: colors.neutral[450],
+  placeholder: colors.neutral[650],
+  active: colors.neutral[0],
+  //TODO: (design) define feature and it's correct value
+  feature: colors.sky[500],
+  ok: colors.green[600],
+  error: colors.red[400],
+  warning: colors.amber[300],
+  info: colors.blue[500],
 };
 
 const iconColor = {
-    primary: colors.neutral[200],
-    secondary: colors.neutral[350],
-    muted: colors.neutral[600],
-    placeholder: colors.neutral[700],
-    active: colors.neutral[0],
-    //TODO: (design) define feature and it's correct value
-    feature: colors.blue[500],
-    ok: colors.green[600],
-    error: colors.red[500],
-    warning: colors.amber[400],
-    info: colors.blue[600],
+  primary: colors.neutral[200],
+  secondary: colors.neutral[350],
+  muted: colors.neutral[600],
+  placeholder: colors.neutral[700],
+  active: colors.neutral[0],
+  //TODO: (design) define feature and it's correct value
+  feature: colors.blue[500],
+  ok: colors.green[600],
+  error: colors.red[500],
+  warning: colors.amber[400],
+  info: colors.blue[600],
 };
 
 const player = {
-    1: buildPlayer(colors.blue[500]),
-    2: buildPlayer(colors.lime[500]),
-    3: buildPlayer(colors.fuschia[500]),
-    4: buildPlayer(colors.orange[500]),
-    5: buildPlayer(colors.purple[500]),
-    6: buildPlayer(colors.teal[400]),
-    7: buildPlayer(colors.pink[400]),
-    8: buildPlayer(colors.yellow[400]),
+  1: buildPlayer(colors.blue[500]),
+  2: buildPlayer(colors.lime[500]),
+  3: buildPlayer(colors.fuschia[500]),
+  4: buildPlayer(colors.orange[500]),
+  5: buildPlayer(colors.purple[500]),
+  6: buildPlayer(colors.teal[400]),
+  7: buildPlayer(colors.pink[400]),
+  8: buildPlayer(colors.yellow[400]),
 };
 
 const editor = {
-    background: backgroundColor[500].base,
-    indent_guide: borderColor.muted,
-    indent_guide_active: borderColor.secondary,
-    line: {
-        active: withOpacity(colors.neutral[0], 0.07),
-        highlighted: withOpacity(colors.neutral[0], 0.12),
-        inserted: backgroundColor.ok.active,
-        deleted: backgroundColor.error.active,
-        modified: backgroundColor.info.active,
-    },
-    highlight: {
-        selection: player[1].selectionColor,
-        occurrence: withOpacity(colors.neutral[0], 0.12),
-        activeOccurrence: withOpacity(colors.neutral[0], 0.16), // TODO: This is not correctly hooked up to occurences on the rust side
-        matchingBracket: backgroundColor[500].active,
-        match: withOpacity(colors.sky[500], 0.16),
-        activeMatch: withOpacity(colors.sky[800], 0.32),
-        related: backgroundColor[500].focused,
-    },
-    gutter: {
-        primary: textColor.placeholder,
-        active: textColor.active,
-    },
+  background: backgroundColor[500].base,
+  indent_guide: borderColor.muted,
+  indent_guide_active: borderColor.secondary,
+  line: {
+    active: withOpacity(colors.neutral[0], 0.07),
+    highlighted: withOpacity(colors.neutral[0], 0.12),
+    inserted: backgroundColor.ok.active,
+    deleted: backgroundColor.error.active,
+    modified: backgroundColor.info.active,
+  },
+  highlight: {
+    selection: player[1].selectionColor,
+    occurrence: withOpacity(colors.neutral[0], 0.12),
+    activeOccurrence: withOpacity(colors.neutral[0], 0.16), // TODO: This is not correctly hooked up to occurences on the rust side
+    matchingBracket: backgroundColor[500].active,
+    match: withOpacity(colors.sky[500], 0.16),
+    activeMatch: withOpacity(colors.sky[800], 0.32),
+    related: backgroundColor[500].focused,
+  },
+  gutter: {
+    primary: textColor.placeholder,
+    active: textColor.active,
+  },
 };
 
 const syntax: Syntax = {
-    primary: {
-        color: colors.neutral[150],
-        weight: fontWeights.normal,
-    },
-    comment: {
-        color: colors.neutral[300],
-        weight: fontWeights.normal,
-    },
-    punctuation: {
-        color: colors.neutral[200],
-        weight: fontWeights.normal,
-    },
-    constant: {
-        color: colors.neutral[150],
-        weight: fontWeights.normal,
-    },
-    keyword: {
-        color: colors.blue[400],
-        weight: fontWeights.normal,
-    },
-    function: {
-        color: colors.yellow[200],
-        weight: fontWeights.normal,
-    },
-    type: {
-        color: colors.teal[300],
-        weight: fontWeights.normal,
-    },
-    variant: {
-        color: colors.sky[300],
-        weight: fontWeights.normal,
-    },
-    property: {
-        color: colors.blue[400],
-        weight: fontWeights.normal,
-    },
-    enum: {
-        color: colors.orange[500],
-        weight: fontWeights.normal,
-    },
-    operator: {
-        color: colors.orange[500],
-        weight: fontWeights.normal,
-    },
-    string: {
-        color: colors.orange[300],
-        weight: fontWeights.normal,
-    },
-    number: {
-        color: colors.lime[300],
-        weight: fontWeights.normal,
-    },
-    boolean: {
-        color: colors.lime[300],
-        weight: fontWeights.normal,
-    },
-    predictive: {
-        color: textColor.muted,
-        weight: fontWeights.normal,
-    },
-    title: {
-        color: colors.amber[500],
-        weight: fontWeights.bold,
-    },
-    emphasis: {
-        color: textColor.active,
-        weight: fontWeights.normal,
-    },
-    emphasisStrong: {
-        color: textColor.active,
-        weight: fontWeights.bold,
-    },
-    linkUrl: {
-        color: colors.lime[500],
-        weight: fontWeights.normal,
-        // TODO: add underline
-    },
-    linkText: {
-        color: colors.orange[500],
-        weight: fontWeights.normal,
-        // TODO: add italic
-    },
+  primary: {
+    color: colors.neutral[150],
+    weight: fontWeights.normal,
+  },
+  comment: {
+    color: colors.neutral[300],
+    weight: fontWeights.normal,
+  },
+  punctuation: {
+    color: colors.neutral[200],
+    weight: fontWeights.normal,
+  },
+  constant: {
+    color: colors.neutral[150],
+    weight: fontWeights.normal,
+  },
+  keyword: {
+    color: colors.blue[400],
+    weight: fontWeights.normal,
+  },
+  function: {
+    color: colors.yellow[200],
+    weight: fontWeights.normal,
+  },
+  type: {
+    color: colors.teal[300],
+    weight: fontWeights.normal,
+  },
+  variant: {
+    color: colors.sky[300],
+    weight: fontWeights.normal,
+  },
+  property: {
+    color: colors.blue[400],
+    weight: fontWeights.normal,
+  },
+  enum: {
+    color: colors.orange[500],
+    weight: fontWeights.normal,
+  },
+  operator: {
+    color: colors.orange[500],
+    weight: fontWeights.normal,
+  },
+  string: {
+    color: colors.orange[300],
+    weight: fontWeights.normal,
+  },
+  number: {
+    color: colors.lime[300],
+    weight: fontWeights.normal,
+  },
+  boolean: {
+    color: colors.lime[300],
+    weight: fontWeights.normal,
+  },
+  predictive: {
+    color: textColor.muted,
+    weight: fontWeights.normal,
+  },
+  title: {
+    color: colors.amber[500],
+    weight: fontWeights.bold,
+  },
+  emphasis: {
+    color: textColor.active,
+    weight: fontWeights.normal,
+  },
+  emphasisStrong: {
+    color: textColor.active,
+    weight: fontWeights.bold,
+  },
+  linkUrl: {
+    color: colors.lime[500],
+    weight: fontWeights.normal,
+    // TODO: add underline
+  },
+  linkText: {
+    color: colors.orange[500],
+    weight: fontWeights.normal,
+    // TODO: add italic
+  },
 };
 
 const shadowAlpha: NumberToken = {
-    value: 0.32,
-    type: "number",
+  value: 0.32,
+  type: "number",
 };
 
 const theme: Theme = {
-    name: "dark",
-    backgroundColor,
-    borderColor,
-    textColor,
-    iconColor,
-    editor,
-    syntax,
-    player,
-    shadowAlpha,
+  name: "dark",
+  backgroundColor,
+  borderColor,
+  textColor,
+  iconColor,
+  editor,
+  syntax,
+  player,
+  shadowAlpha,
 };
 
 export default theme;

styles/src/themes/light.ts šŸ”—

@@ -3,225 +3,225 @@ import { withOpacity } from "../utils/color";
 import Theme, { buildPlayer, Syntax } from "./theme";
 
 const backgroundColor = {
-    100: {
-        base: colors.neutral[75],
-        hovered: colors.neutral[100],
-        active: colors.neutral[150],
-        focused: colors.neutral[100],
-    },
-    300: {
-        base: colors.neutral[25],
-        hovered: colors.neutral[75],
-        active: colors.neutral[125],
-        focused: colors.neutral[75],
-    },
-    500: {
-        base: colors.neutral[0],
-        hovered: withOpacity(colors.neutral[900], 0.03),
-        active: withOpacity(colors.neutral[900], 0.06),
-        focused: colors.neutral[50],
-    },
-    ok: {
-        base: colors.green[100],
-        hovered: colors.green[100],
-        active: colors.green[100],
-        focused: colors.green[100],
-    },
-    error: {
-        base: colors.red[100],
-        hovered: colors.red[100],
-        active: colors.red[100],
-        focused: colors.red[100],
-    },
-    warning: {
-        base: colors.yellow[100],
-        hovered: colors.yellow[100],
-        active: colors.yellow[100],
-        focused: colors.yellow[100],
-    },
-    info: {
-        base: colors.blue[100],
-        hovered: colors.blue[100],
-        active: colors.blue[100],
-        focused: colors.blue[100],
-    },
+  100: {
+    base: colors.neutral[75],
+    hovered: colors.neutral[100],
+    active: colors.neutral[150],
+    focused: colors.neutral[100],
+  },
+  300: {
+    base: colors.neutral[25],
+    hovered: colors.neutral[75],
+    active: colors.neutral[125],
+    focused: colors.neutral[75],
+  },
+  500: {
+    base: colors.neutral[0],
+    hovered: withOpacity(colors.neutral[900], 0.03),
+    active: withOpacity(colors.neutral[900], 0.06),
+    focused: colors.neutral[50],
+  },
+  ok: {
+    base: colors.green[100],
+    hovered: colors.green[100],
+    active: colors.green[100],
+    focused: colors.green[100],
+  },
+  error: {
+    base: colors.red[100],
+    hovered: colors.red[100],
+    active: colors.red[100],
+    focused: colors.red[100],
+  },
+  warning: {
+    base: colors.yellow[100],
+    hovered: colors.yellow[100],
+    active: colors.yellow[100],
+    focused: colors.yellow[100],
+  },
+  info: {
+    base: colors.blue[100],
+    hovered: colors.blue[100],
+    active: colors.blue[100],
+    focused: colors.blue[100],
+  },
 };
 
 const borderColor = {
-    primary: colors.neutral[150],
-    secondary: colors.neutral[150],
-    muted: colors.neutral[100],
-    focused: colors.neutral[100],
-    active: colors.neutral[250],
-    ok: colors.green[200],
-    error: colors.red[200],
-    warning: colors.yellow[200],
-    info: colors.blue[200],
+  primary: colors.neutral[150],
+  secondary: colors.neutral[150],
+  muted: colors.neutral[100],
+  focused: colors.neutral[100],
+  active: colors.neutral[250],
+  ok: colors.green[200],
+  error: colors.red[200],
+  warning: colors.yellow[200],
+  info: colors.blue[200],
 };
 
 const textColor = {
-    primary: colors.neutral[750],
-    secondary: colors.neutral[650],
-    muted: colors.neutral[550],
-    placeholder: colors.neutral[450],
-    active: colors.neutral[900],
-    feature: colors.indigo[600],
-    ok: colors.green[500],
-    error: colors.red[500],
-    warning: colors.yellow[500],
-    info: colors.blue[500],
+  primary: colors.neutral[750],
+  secondary: colors.neutral[650],
+  muted: colors.neutral[550],
+  placeholder: colors.neutral[450],
+  active: colors.neutral[900],
+  feature: colors.indigo[600],
+  ok: colors.green[500],
+  error: colors.red[500],
+  warning: colors.yellow[500],
+  info: colors.blue[500],
 };
 
 const iconColor = {
-    primary: colors.neutral[700],
-    secondary: colors.neutral[500],
-    muted: colors.neutral[350],
-    placeholder: colors.neutral[300],
-    active: colors.neutral[900],
-    feature: colors.indigo[500],
-    ok: colors.green[600],
-    error: colors.red[600],
-    warning: colors.yellow[400],
-    info: colors.blue[600],
+  primary: colors.neutral[700],
+  secondary: colors.neutral[500],
+  muted: colors.neutral[350],
+  placeholder: colors.neutral[300],
+  active: colors.neutral[900],
+  feature: colors.indigo[500],
+  ok: colors.green[600],
+  error: colors.red[600],
+  warning: colors.yellow[400],
+  info: colors.blue[600],
 };
 
 const player = {
-    1: buildPlayer(colors.blue[500]),
-    2: buildPlayer(colors.emerald[400]),
-    3: buildPlayer(colors.fuschia[400]),
-    4: buildPlayer(colors.orange[400]),
-    5: buildPlayer(colors.purple[400]),
-    6: buildPlayer(colors.teal[400]),
-    7: buildPlayer(colors.pink[400]),
-    8: buildPlayer(colors.yellow[400]),
+  1: buildPlayer(colors.blue[500]),
+  2: buildPlayer(colors.emerald[400]),
+  3: buildPlayer(colors.fuschia[400]),
+  4: buildPlayer(colors.orange[400]),
+  5: buildPlayer(colors.purple[400]),
+  6: buildPlayer(colors.teal[400]),
+  7: buildPlayer(colors.pink[400]),
+  8: buildPlayer(colors.yellow[400]),
 };
 
 const editor = {
-    background: backgroundColor[500].base,
-    indent_guide: borderColor.muted,
-    indent_guide_active: borderColor.secondary,
-    line: {
-        active: withOpacity(colors.neutral[900], 0.06),
-        highlighted: withOpacity(colors.neutral[900], 0.12),
-        inserted: backgroundColor.ok.active,
-        deleted: backgroundColor.error.active,
-        modified: backgroundColor.info.active,
-    },
-    highlight: {
-        selection: player[1].selectionColor,
-        occurrence: withOpacity(colors.neutral[900], 0.06),
-        activeOccurrence: withOpacity(colors.neutral[900], 0.16), // TODO: This is not hooked up to occurences on the rust side
-        matchingBracket: colors.neutral[0],
-        match: withOpacity(colors.red[500], 0.2),
-        activeMatch: withOpacity(colors.indigo[400], 0.36), // TODO: This is not hooked up to occurences on the rust side
-        related: colors.neutral[0],
-    },
-    gutter: {
-        primary: colors.neutral[300],
-        active: textColor.active,
-    },
+  background: backgroundColor[500].base,
+  indent_guide: borderColor.muted,
+  indent_guide_active: borderColor.secondary,
+  line: {
+    active: withOpacity(colors.neutral[900], 0.06),
+    highlighted: withOpacity(colors.neutral[900], 0.12),
+    inserted: backgroundColor.ok.active,
+    deleted: backgroundColor.error.active,
+    modified: backgroundColor.info.active,
+  },
+  highlight: {
+    selection: player[1].selectionColor,
+    occurrence: withOpacity(colors.neutral[900], 0.06),
+    activeOccurrence: withOpacity(colors.neutral[900], 0.16), // TODO: This is not hooked up to occurences on the rust side
+    matchingBracket: colors.neutral[0],
+    match: withOpacity(colors.red[500], 0.2),
+    activeMatch: withOpacity(colors.indigo[400], 0.36), // TODO: This is not hooked up to occurences on the rust side
+    related: colors.neutral[0],
+  },
+  gutter: {
+    primary: colors.neutral[300],
+    active: textColor.active,
+  },
 };
 
 const syntax: Syntax = {
-    primary: {
-        color: colors.neutral[800],
-        weight: fontWeights.normal,
-    },
-    comment: {
-        color: colors.neutral[500],
-        weight: fontWeights.normal,
-    },
-    punctuation: {
-        color: colors.neutral[600],
-        weight: fontWeights.normal,
-    },
-    constant: {
-        color: colors.neutral[800],
-        weight: fontWeights.normal,
-    },
-    keyword: {
-        color: colors.indigo[700],
-        weight: fontWeights.normal,
-    },
-    function: {
-        color: colors.orange[600],
-        weight: fontWeights.normal,
-    },
-    type: {
-        color: colors.yellow[600],
-        weight: fontWeights.normal,
-    },
-    variant: {
-        color: colors.rose[700],
-        weight: fontWeights.normal,
-    },
-    property: {
-        color: colors.emerald[700],
-        weight: fontWeights.normal,
-    },
-    enum: {
-        color: colors.red[500],
-        weight: fontWeights.normal,
-    },
-    operator: {
-        color: colors.red[500],
-        weight: fontWeights.normal,
-    },
-    string: {
-        color: colors.red[500],
-        weight: fontWeights.normal,
-    },
-    number: {
-        color: colors.indigo[500],
-        weight: fontWeights.normal,
-    },
-    boolean: {
-        color: colors.red[500],
-        weight: fontWeights.normal,
-    },
-    predictive: {
-        color: textColor.placeholder,
-        weight: fontWeights.normal,
-    },
-    title: {
-        color: colors.sky[500],
-        weight: fontWeights.bold,
-    },
-    emphasis: {
-        color: textColor.active,
-        weight: fontWeights.normal,
-    },
-    emphasisStrong: {
-        color: textColor.active,
-        weight: fontWeights.bold,
-    },
-    linkUrl: {
-        color: colors.lime[500],
-        weight: fontWeights.normal,
-        // TODO: add underline
-    },
-    linkText: {
-        color: colors.red[500],
-        weight: fontWeights.normal,
-        // TODO: add italic
-    },
+  primary: {
+    color: colors.neutral[800],
+    weight: fontWeights.normal,
+  },
+  comment: {
+    color: colors.neutral[500],
+    weight: fontWeights.normal,
+  },
+  punctuation: {
+    color: colors.neutral[600],
+    weight: fontWeights.normal,
+  },
+  constant: {
+    color: colors.neutral[800],
+    weight: fontWeights.normal,
+  },
+  keyword: {
+    color: colors.indigo[700],
+    weight: fontWeights.normal,
+  },
+  function: {
+    color: colors.orange[600],
+    weight: fontWeights.normal,
+  },
+  type: {
+    color: colors.yellow[600],
+    weight: fontWeights.normal,
+  },
+  variant: {
+    color: colors.rose[700],
+    weight: fontWeights.normal,
+  },
+  property: {
+    color: colors.emerald[700],
+    weight: fontWeights.normal,
+  },
+  enum: {
+    color: colors.red[500],
+    weight: fontWeights.normal,
+  },
+  operator: {
+    color: colors.red[500],
+    weight: fontWeights.normal,
+  },
+  string: {
+    color: colors.red[500],
+    weight: fontWeights.normal,
+  },
+  number: {
+    color: colors.indigo[500],
+    weight: fontWeights.normal,
+  },
+  boolean: {
+    color: colors.red[500],
+    weight: fontWeights.normal,
+  },
+  predictive: {
+    color: textColor.placeholder,
+    weight: fontWeights.normal,
+  },
+  title: {
+    color: colors.sky[500],
+    weight: fontWeights.bold,
+  },
+  emphasis: {
+    color: textColor.active,
+    weight: fontWeights.normal,
+  },
+  emphasisStrong: {
+    color: textColor.active,
+    weight: fontWeights.bold,
+  },
+  linkUrl: {
+    color: colors.lime[500],
+    weight: fontWeights.normal,
+    // TODO: add underline
+  },
+  linkText: {
+    color: colors.red[500],
+    weight: fontWeights.normal,
+    // TODO: add italic
+  },
 };
 
 const shadowAlpha: NumberToken = {
-    value: 0.12,
-    type: "number",
+  value: 0.12,
+  type: "number",
 };
 
 const theme: Theme = {
-    name: "light",
-    backgroundColor,
-    borderColor,
-    textColor,
-    iconColor,
-    editor,
-    syntax,
-    player,
-    shadowAlpha,
+  name: "light",
+  backgroundColor,
+  borderColor,
+  textColor,
+  iconColor,
+  editor,
+  syntax,
+  player,
+  shadowAlpha,
 };
 
 export default theme;

styles/src/themes/theme.ts šŸ”—

@@ -2,144 +2,144 @@ import { ColorToken, FontWeightToken, NumberToken } from "../tokens";
 import { withOpacity } from "../utils/color";
 
 export interface SyntaxHighlightStyle {
-    color: ColorToken;
-    weight: FontWeightToken;
+  color: ColorToken;
+  weight: FontWeightToken;
 }
 
 export interface Player {
-    baseColor: ColorToken;
-    cursorColor: ColorToken;
-    selectionColor: ColorToken;
-    borderColor: ColorToken;
+  baseColor: ColorToken;
+  cursorColor: ColorToken;
+  selectionColor: ColorToken;
+  borderColor: ColorToken;
 }
 export function buildPlayer(
-    color: ColorToken,
-    cursorOpacity?: number,
-    selectionOpacity?: number,
-    borderOpacity?: number
+  color: ColorToken,
+  cursorOpacity?: number,
+  selectionOpacity?: number,
+  borderOpacity?: number
 ) {
-    return {
-        baseColor: color,
-        cursorColor: withOpacity(color, cursorOpacity || 1.0),
-        selectionColor: withOpacity(color, selectionOpacity || 0.24),
-        borderColor: withOpacity(color, borderOpacity || 0.8),
-    }
+  return {
+    baseColor: color,
+    cursorColor: withOpacity(color, cursorOpacity || 1.0),
+    selectionColor: withOpacity(color, selectionOpacity || 0.24),
+    borderColor: withOpacity(color, borderOpacity || 0.8),
+  }
 }
 
 export interface BackgroundColorSet {
-    base: ColorToken;
-    hovered: ColorToken;
-    active: ColorToken;
-    focused: ColorToken;
+  base: ColorToken;
+  hovered: ColorToken;
+  active: ColorToken;
+  focused: ColorToken;
 }
 
 export interface Syntax {
-    primary: SyntaxHighlightStyle;
-    comment: SyntaxHighlightStyle;
-    punctuation: SyntaxHighlightStyle;
-    constant: SyntaxHighlightStyle;
-    keyword: SyntaxHighlightStyle;
-    function: SyntaxHighlightStyle;
-    type: SyntaxHighlightStyle;
-    variant: SyntaxHighlightStyle;
-    property: SyntaxHighlightStyle;
-    enum: SyntaxHighlightStyle;
-    operator: SyntaxHighlightStyle;
-    string: SyntaxHighlightStyle;
-    number: SyntaxHighlightStyle;
-    boolean: SyntaxHighlightStyle;
-    predictive: SyntaxHighlightStyle;
-    // TODO: Either move the following or rename
-    title: SyntaxHighlightStyle;
-    emphasis: SyntaxHighlightStyle;
-    emphasisStrong: SyntaxHighlightStyle;
-    linkUrl: SyntaxHighlightStyle;
-    linkText: SyntaxHighlightStyle;
+  primary: SyntaxHighlightStyle;
+  comment: SyntaxHighlightStyle;
+  punctuation: SyntaxHighlightStyle;
+  constant: SyntaxHighlightStyle;
+  keyword: SyntaxHighlightStyle;
+  function: SyntaxHighlightStyle;
+  type: SyntaxHighlightStyle;
+  variant: SyntaxHighlightStyle;
+  property: SyntaxHighlightStyle;
+  enum: SyntaxHighlightStyle;
+  operator: SyntaxHighlightStyle;
+  string: SyntaxHighlightStyle;
+  number: SyntaxHighlightStyle;
+  boolean: SyntaxHighlightStyle;
+  predictive: SyntaxHighlightStyle;
+  // TODO: Either move the following or rename
+  title: SyntaxHighlightStyle;
+  emphasis: SyntaxHighlightStyle;
+  emphasisStrong: SyntaxHighlightStyle;
+  linkUrl: SyntaxHighlightStyle;
+  linkText: SyntaxHighlightStyle;
 };
 
 export default interface Theme {
-    name: string;
-    backgroundColor: {
-        100: BackgroundColorSet;
-        300: BackgroundColorSet;
-        500: BackgroundColorSet;
-        ok: BackgroundColorSet;
-        error: BackgroundColorSet;
-        warning: BackgroundColorSet;
-        info: BackgroundColorSet;
-    };
-    borderColor: {
-        primary: ColorToken;
-        secondary: ColorToken;
-        muted: ColorToken;
-        focused: ColorToken;
-        active: ColorToken;
-        ok: ColorToken;
-        error: ColorToken;
-        warning: ColorToken;
-        info: ColorToken;
-    };
-    textColor: {
-        primary: ColorToken;
-        secondary: ColorToken;
-        muted: ColorToken;
-        placeholder: ColorToken;
-        active: ColorToken;
-        feature: ColorToken;
-        ok: ColorToken;
-        error: ColorToken;
-        warning: ColorToken;
-        info: ColorToken;
+  name: string;
+  backgroundColor: {
+    100: BackgroundColorSet;
+    300: BackgroundColorSet;
+    500: BackgroundColorSet;
+    ok: BackgroundColorSet;
+    error: BackgroundColorSet;
+    warning: BackgroundColorSet;
+    info: BackgroundColorSet;
+  };
+  borderColor: {
+    primary: ColorToken;
+    secondary: ColorToken;
+    muted: ColorToken;
+    focused: ColorToken;
+    active: ColorToken;
+    ok: ColorToken;
+    error: ColorToken;
+    warning: ColorToken;
+    info: ColorToken;
+  };
+  textColor: {
+    primary: ColorToken;
+    secondary: ColorToken;
+    muted: ColorToken;
+    placeholder: ColorToken;
+    active: ColorToken;
+    feature: ColorToken;
+    ok: ColorToken;
+    error: ColorToken;
+    warning: ColorToken;
+    info: ColorToken;
+  };
+  iconColor: {
+    primary: ColorToken;
+    secondary: ColorToken;
+    muted: ColorToken;
+    placeholder: ColorToken;
+    active: ColorToken;
+    feature: ColorToken;
+    ok: ColorToken;
+    error: ColorToken;
+    warning: ColorToken;
+    info: ColorToken;
+  };
+  editor: {
+    background: ColorToken;
+    indent_guide: ColorToken;
+    indent_guide_active: ColorToken;
+    line: {
+      active: ColorToken;
+      highlighted: ColorToken;
+      inserted: ColorToken;
+      deleted: ColorToken;
+      modified: ColorToken;
     };
-    iconColor: {
-        primary: ColorToken;
-        secondary: ColorToken;
-        muted: ColorToken;
-        placeholder: ColorToken;
-        active: ColorToken;
-        feature: ColorToken;
-        ok: ColorToken;
-        error: ColorToken;
-        warning: ColorToken;
-        info: ColorToken;
+    highlight: {
+      selection: ColorToken;
+      occurrence: ColorToken;
+      activeOccurrence: ColorToken;
+      matchingBracket: ColorToken;
+      match: ColorToken;
+      activeMatch: ColorToken;
+      related: ColorToken;
     };
-    editor: {
-        background: ColorToken;
-        indent_guide: ColorToken;
-        indent_guide_active: ColorToken;
-        line: {
-            active: ColorToken;
-            highlighted: ColorToken;
-            inserted: ColorToken;
-            deleted: ColorToken;
-            modified: ColorToken;
-        };
-        highlight: {
-            selection: ColorToken;
-            occurrence: ColorToken;
-            activeOccurrence: ColorToken;
-            matchingBracket: ColorToken;
-            match: ColorToken;
-            activeMatch: ColorToken;
-            related: ColorToken;
-        };
-        gutter: {
-            primary: ColorToken;
-            active: ColorToken;
-        };
+    gutter: {
+      primary: ColorToken;
+      active: ColorToken;
     };
+  };
 
-    syntax: Syntax,
+  syntax: Syntax,
 
-    player: {
-        1: Player;
-        2: Player;
-        3: Player;
-        4: Player;
-        5: Player;
-        6: Player;
-        7: Player;
-        8: Player;
-    };
-    shadowAlpha: NumberToken;
+  player: {
+    1: Player;
+    2: Player;
+    3: Player;
+    4: Player;
+    5: Player;
+    6: Player;
+    7: Player;
+    8: Player;
+  };
+  shadowAlpha: NumberToken;
 }

styles/src/tokens.ts šŸ”—

@@ -36,7 +36,7 @@ export const fontSizes = {
   xl: fontSize(20),
 };
 
-export type FontWeight = 
+export type FontWeight =
   | "thin"
   | "extra_light"
   | "light"

styles/src/utils/color.ts šŸ”—

@@ -4,49 +4,49 @@ import { ColorToken } from "../tokens";
 export type Color = string;
 export type ColorRampStep = { value: Color; type: "color"; step: number };
 export type ColorRamp = {
-    [index: number]: ColorRampStep;
+  [index: number]: ColorRampStep;
 };
 
 export function colorRamp(
-    color: Color | [Color, Color],
-    options?: { steps?: number; increment?: number; }
+  color: Color | [Color, Color],
+  options?: { steps?: number; increment?: number; }
 ): ColorRamp {
-    let scale: Scale;
-    if (Array.isArray(color)) {
-        const [startColor, endColor] = color;
-        scale = chroma.scale([startColor, endColor]);
-    } else {
-        let hue = Math.round(chroma(color).hsl()[0]);
-        let startColor = chroma.hsl(hue, 0.88, 0.96);
-        let endColor = chroma.hsl(hue, 0.68, 0.12);
-        scale = chroma
-            .scale([startColor, color, endColor])
-            .domain([0, 0.5, 1])
-            .mode("hsl")
-            .gamma(1)
-            // .correctLightness(true)
-            .padding([0, 0]);
-    }
+  let scale: Scale;
+  if (Array.isArray(color)) {
+    const [startColor, endColor] = color;
+    scale = chroma.scale([startColor, endColor]);
+  } else {
+    let hue = Math.round(chroma(color).hsl()[0]);
+    let startColor = chroma.hsl(hue, 0.88, 0.96);
+    let endColor = chroma.hsl(hue, 0.68, 0.12);
+    scale = chroma
+      .scale([startColor, color, endColor])
+      .domain([0, 0.5, 1])
+      .mode("hsl")
+      .gamma(1)
+      // .correctLightness(true)
+      .padding([0, 0]);
+  }
 
-    const ramp: ColorRamp = {};
-    const steps = options?.steps || 10;
-    const increment = options?.increment || 100;
+  const ramp: ColorRamp = {};
+  const steps = options?.steps || 10;
+  const increment = options?.increment || 100;
 
-    scale.colors(steps, "hex").forEach((color, ix) => {
-        const step = ix * increment;
-        ramp[step] = {
-            value: color,
-            step,
-            type: "color",
-        };
-    });
+  scale.colors(steps, "hex").forEach((color, ix) => {
+    const step = ix * increment;
+    ramp[step] = {
+      value: color,
+      step,
+      type: "color",
+    };
+  });
 
-    return ramp;
+  return ramp;
 }
 
 export function withOpacity(color: ColorToken, opacity: number): ColorToken {
-    return {
-        ...color,
-        value: chroma(color.value).alpha(opacity).hex()
-    };
+  return {
+    ...color,
+    value: chroma(color.value).alpha(opacity).hex()
+  };
 }

styles/src/utils/snakeCase.ts šŸ”—

@@ -4,32 +4,32 @@ import { snakeCase } from "case-anything";
 
 // Typescript magic to convert any string from camelCase to snake_case at compile time
 type SnakeCase<S> =
-    S extends string ?
-    S extends `${infer T}${infer U}` ?
-    `${T extends Capitalize<T> ? "_" : ""}${Lowercase<T>}${SnakeCase<U>}` :
-    S :
-    S;
+  S extends string ?
+  S extends `${infer T}${infer U}` ?
+  `${T extends Capitalize<T> ? "_" : ""}${Lowercase<T>}${SnakeCase<U>}` :
+  S :
+  S;
 
 type SnakeCased<Type> = {
-    [Property in keyof Type as SnakeCase<Property>]: SnakeCased<Type[Property]>
+  [Property in keyof Type as SnakeCase<Property>]: SnakeCased<Type[Property]>
 }
 
 export default function snakeCaseTree<T>(object: T): SnakeCased<T> {
-    const snakeObject: any = {};
-    for (const key in object) {
-        snakeObject[snakeCase(key)] = snakeCaseValue(object[key]);
-    }
-    return snakeObject;
+  const snakeObject: any = {};
+  for (const key in object) {
+    snakeObject[snakeCase(key)] = snakeCaseValue(object[key]);
+  }
+  return snakeObject;
 }
 
 function snakeCaseValue(value: any): any {
-    if (typeof value === "object") {
-        if (Array.isArray(value)) {
-            return value.map(snakeCaseValue);
-        } else {
-            return snakeCaseTree(value);
-        }
+  if (typeof value === "object") {
+    if (Array.isArray(value)) {
+      return value.map(snakeCaseValue);
     } else {
-        return value;
+      return snakeCaseTree(value);
     }
+  } else {
+    return value;
+  }
 }