Move editor into its own crate

Max Brunsfeld and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

Cargo.lock                                |  24 +
crates/buffer/src/syntax_theme.rs         |   1 
crates/editor/Cargo.toml                  |  29 ++
crates/editor/src/display_map.rs          |   2 
crates/editor/src/display_map/fold_map.rs |   2 
crates/editor/src/display_map/tab_map.rs  |   0 
crates/editor/src/display_map/wrap_map.rs |  10 
crates/editor/src/element.rs              |  97 +++---
crates/editor/src/lib.rs                  | 353 +++++++-----------------
crates/editor/src/movement.rs             |   2 
crates/editor/src/test.rs                 |  39 ++
crates/server/src/main.rs                 |   2 
crates/server/src/rpc.rs                  |  21 
crates/zed/Cargo.toml                     |   4 
crates/zed/assets/themes/_base.toml       |   2 
crates/zed/assets/themes/black.toml       |   4 
crates/zed/assets/themes/dark.toml        |   4 
crates/zed/assets/themes/light.toml       |   4 
crates/zed/src/chat_panel.rs              |  14 
crates/zed/src/file_finder.rs             |  18 
crates/zed/src/lib.rs                     |   2 
crates/zed/src/main.rs                    |   2 
crates/zed/src/menus.rs                   |   2 
crates/zed/src/test.rs                    |  41 --
crates/zed/src/theme.rs                   |   5 
crates/zed/src/theme_selector.rs          |  11 
crates/zed/src/workspace.rs               |   8 
crates/zed/src/workspace/items.rs         | 153 ++++++++++
28 files changed, 456 insertions(+), 400 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -1575,6 +1575,29 @@ dependencies = [
  "getrandom 0.2.2",
 ]
 
+[[package]]
+name = "editor"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "buffer",
+ "clock",
+ "gpui",
+ "lazy_static",
+ "log",
+ "parking_lot",
+ "postage",
+ "rand 0.8.3",
+ "serde 1.0.125",
+ "smallvec",
+ "smol",
+ "sum_tree",
+ "tree-sitter",
+ "tree-sitter-rust",
+ "unindent",
+ "util",
+]
+
 [[package]]
 name = "either"
 version = "1.6.1"
@@ -6030,6 +6053,7 @@ dependencies = [
  "ctor",
  "dirs 3.0.1",
  "easy-parallel",
+ "editor",
  "env_logger",
  "fsevent",
  "futures",

crates/buffer/src/syntax_theme.rs πŸ”—

@@ -4,6 +4,7 @@ use crate::HighlightId;
 use gpui::fonts::HighlightStyle;
 use serde::Deserialize;
 
+#[derive(Default)]
 pub struct SyntaxTheme {
     pub(crate) highlights: Vec<(String, HighlightStyle)>,
 }

crates/editor/Cargo.toml πŸ”—

@@ -0,0 +1,29 @@
+[package]
+name = "editor"
+version = "0.1.0"
+edition = "2021"
+
+[features]
+test-support = ["buffer/test-support"]
+
+[dependencies]
+anyhow = "1.0"
+buffer = { path = "../buffer" }
+clock = { path = "../clock" }
+gpui = { path = "../gpui" }
+lazy_static = "1.4"
+log = "0.4"
+parking_lot = "0.11"
+postage = { version = "0.4", features = ["futures-traits"] }
+serde = { version = "1", features = ["derive", "rc"] }
+smallvec = { version = "1.6", features = ["union"] }
+smol = "1.2"
+sum_tree = { path = "../sum_tree" }
+util = { path = "../util" }
+
+[dev-dependencies]
+rand = "0.8"
+unindent = "0.1.7"
+tree-sitter = "0.19"
+tree-sitter-rust = "0.19"
+buffer = { path = "../buffer", features = ["test-support"] }

crates/zed/src/editor/display_map.rs β†’ crates/editor/src/display_map.rs πŸ”—

@@ -357,7 +357,7 @@ impl ToDisplayPoint for Anchor {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{editor::movement, test::*};
+    use crate::{movement, test::*};
     use buffer::{History, Language, LanguageConfig, RandomCharIter, SelectionGoal, SyntaxTheme};
     use gpui::{color::Color, MutableAppContext};
     use rand::{prelude::StdRng, Rng};

crates/zed/src/editor/display_map/fold_map.rs β†’ crates/editor/src/display_map/fold_map.rs πŸ”—

@@ -1128,7 +1128,7 @@ impl FoldEdit {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{editor::ToPoint, test::sample_text};
+    use crate::{test::sample_text, ToPoint};
     use buffer::RandomCharIter;
     use rand::prelude::*;
     use std::{env, mem};

crates/zed/src/editor/display_map/wrap_map.rs β†’ crates/editor/src/display_map/wrap_map.rs πŸ”—

@@ -2,8 +2,7 @@ use super::{
     fold_map,
     tab_map::{self, Edit as TabEdit, Snapshot as TabSnapshot, TabPoint, TextSummary},
 };
-use crate::editor::Point;
-use buffer::HighlightId;
+use buffer::{HighlightId, Point};
 use gpui::{fonts::FontId, text_layout::LineWrapper, Entity, ModelContext, Task};
 use lazy_static::lazy_static;
 use smol::future::yield_now;
@@ -897,13 +896,10 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for WrapPoint {
 mod tests {
     use super::*;
     use crate::{
-        editor::{
-            display_map::{fold_map::FoldMap, tab_map::TabMap},
-            Buffer,
-        },
+        display_map::{fold_map::FoldMap, tab_map::TabMap},
         test::Observer,
     };
-    use buffer::RandomCharIter;
+    use buffer::{Buffer, RandomCharIter};
     use rand::prelude::*;
     use std::env;
 

crates/zed/src/editor/element.rs β†’ crates/editor/src/element.rs πŸ”—

@@ -1,6 +1,6 @@
 use super::{
-    DisplayPoint, Editor, EditorMode, EditorStyle, Insert, Scroll, Select, SelectPhase, Snapshot,
-    MAX_LINE_LEN,
+    DisplayPoint, Editor, EditorMode, EditorSettings, EditorStyle, Insert, Scroll, Select,
+    SelectPhase, Snapshot, MAX_LINE_LEN,
 };
 use buffer::HighlightId;
 use clock::ReplicaId;
@@ -28,12 +28,12 @@ use std::{
 
 pub struct EditorElement {
     view: WeakViewHandle<Editor>,
-    style: EditorStyle,
+    settings: EditorSettings,
 }
 
 impl EditorElement {
-    pub fn new(view: WeakViewHandle<Editor>, style: EditorStyle) -> Self {
-        Self { view, style }
+    pub fn new(view: WeakViewHandle<Editor>, settings: EditorSettings) -> Self {
+        Self { view, settings }
     }
 
     fn view<'a>(&self, cx: &'a AppContext) -> &'a Editor {
@@ -196,15 +196,16 @@ impl EditorElement {
         let bounds = gutter_bounds.union_rect(text_bounds);
         let scroll_top = layout.snapshot.scroll_position().y() * layout.line_height;
         let editor = self.view(cx.app);
+        let style = &self.settings.style;
         cx.scene.push_quad(Quad {
             bounds: gutter_bounds,
-            background: Some(self.style.gutter_background),
+            background: Some(style.gutter_background),
             border: Border::new(0., Color::transparent_black()),
             corner_radius: 0.,
         });
         cx.scene.push_quad(Quad {
             bounds: text_bounds,
-            background: Some(self.style.background),
+            background: Some(style.background),
             border: Border::new(0., Color::transparent_black()),
             corner_radius: 0.,
         });
@@ -231,7 +232,7 @@ impl EditorElement {
                     );
                     cx.scene.push_quad(Quad {
                         bounds: RectF::new(origin, size),
-                        background: Some(self.style.active_line_background),
+                        background: Some(style.active_line_background),
                         border: Border::default(),
                         corner_radius: 0.,
                     });
@@ -268,8 +269,7 @@ impl EditorElement {
         cx: &mut PaintContext,
     ) {
         let view = self.view(cx.app);
-        let settings = self.view(cx.app).settings.borrow();
-        let theme = &settings.theme.editor;
+        let style = &self.settings.style;
         let local_replica_id = view.replica_id(cx);
         let scroll_position = layout.snapshot.scroll_position();
         let start_row = scroll_position.y() as u32;
@@ -287,11 +287,11 @@ impl EditorElement {
         let content_origin = bounds.origin() + layout.text_offset;
 
         for (replica_id, selections) in &layout.selections {
-            let style_ix = *replica_id as usize % (theme.guest_selections.len() + 1);
+            let style_ix = *replica_id as usize % (style.guest_selections.len() + 1);
             let style = if style_ix == 0 {
-                &theme.selection
+                &style.selection
             } else {
-                &theme.guest_selections[style_ix - 1]
+                &style.guest_selections[style_ix - 1]
             };
 
             for selection in selections {
@@ -383,15 +383,16 @@ impl EditorElement {
 
     fn max_line_number_width(&self, snapshot: &Snapshot, cx: &LayoutContext) -> f32 {
         let digit_count = (snapshot.buffer_row_count() as f32).log10().floor() as usize + 1;
+        let style = &self.settings.style;
 
         cx.text_layout_cache
             .layout_str(
                 "1".repeat(digit_count).as_str(),
-                self.style.text.font_size,
+                style.text.font_size,
                 &[(
                     digit_count,
                     RunStyle {
-                        font_id: self.style.text.font_id,
+                        font_id: style.text.font_id,
                         color: Color::black(),
                         underline: false,
                     },
@@ -407,6 +408,7 @@ impl EditorElement {
         snapshot: &Snapshot,
         cx: &LayoutContext,
     ) -> Vec<Option<text_layout::Line>> {
+        let style = &self.settings.style;
         let mut layouts = Vec::with_capacity(rows.len());
         let mut line_number = String::new();
         for (ix, (buffer_row, soft_wrapped)) in snapshot
@@ -416,9 +418,9 @@ impl EditorElement {
         {
             let display_row = rows.start + ix as u32;
             let color = if active_rows.contains_key(&display_row) {
-                self.style.line_number_active
+                style.line_number_active
             } else {
-                self.style.line_number
+                style.line_number
             };
             if soft_wrapped {
                 layouts.push(None);
@@ -427,11 +429,11 @@ impl EditorElement {
                 write!(&mut line_number, "{}", buffer_row + 1).unwrap();
                 layouts.push(Some(cx.text_layout_cache.layout_str(
                     &line_number,
-                    self.style.text.font_size,
+                    style.text.font_size,
                     &[(
                         line_number.len(),
                         RunStyle {
-                            font_id: self.style.text.font_id,
+                            font_id: style.text.font_id,
                             color,
                             underline: false,
                         },
@@ -456,7 +458,7 @@ impl EditorElement {
 
         // When the editor is empty and unfocused, then show the placeholder.
         if snapshot.is_empty() && !snapshot.is_focused() {
-            let placeholder_style = self.style.placeholder_text();
+            let placeholder_style = self.settings.style.placeholder_text();
             let placeholder_text = snapshot.placeholder_text();
             let placeholder_lines = placeholder_text
                 .as_ref()
@@ -482,10 +484,10 @@ impl EditorElement {
                 .collect();
         }
 
-        let mut prev_font_properties = self.style.text.font_properties.clone();
-        let mut prev_font_id = self.style.text.font_id;
+        let style = &self.settings.style;
+        let mut prev_font_properties = style.text.font_properties.clone();
+        let mut prev_font_id = style.text.font_id;
 
-        let theme = snapshot.theme().clone();
         let mut layouts = Vec::with_capacity(rows.len());
         let mut line = String::new();
         let mut styles = Vec::new();
@@ -498,7 +500,7 @@ impl EditorElement {
                 if ix > 0 {
                     layouts.push(cx.text_layout_cache.layout_str(
                         &line,
-                        self.style.text.font_size,
+                        style.text.font_size,
                         &styles,
                     ));
                     line.clear();
@@ -511,17 +513,20 @@ impl EditorElement {
                 }
 
                 if !line_chunk.is_empty() && !line_exceeded_max_len {
-                    let style = theme
+                    let highlight_style = style
                         .syntax
                         .highlight_style(style_ix)
-                        .unwrap_or(self.style.text.clone().into());
+                        .unwrap_or(style.text.clone().into());
                     // Avoid a lookup if the font properties match the previous ones.
-                    let font_id = if style.font_properties == prev_font_properties {
+                    let font_id = if highlight_style.font_properties == prev_font_properties {
                         prev_font_id
                     } else {
                         cx.font_cache
-                            .select_font(self.style.text.font_family_id, &style.font_properties)
-                            .unwrap_or(self.style.text.font_id)
+                            .select_font(
+                                style.text.font_family_id,
+                                &highlight_style.font_properties,
+                            )
+                            .unwrap_or(style.text.font_id)
                     };
 
                     if line.len() + line_chunk.len() > MAX_LINE_LEN {
@@ -538,12 +543,12 @@ impl EditorElement {
                         line_chunk.len(),
                         RunStyle {
                             font_id,
-                            color: style.color,
-                            underline: style.underline,
+                            color: highlight_style.color,
+                            underline: highlight_style.underline,
                         },
                     ));
                     prev_font_id = font_id;
-                    prev_font_properties = style.font_properties;
+                    prev_font_properties = highlight_style.font_properties;
                 }
             }
         }
@@ -567,12 +572,13 @@ impl Element for EditorElement {
         }
 
         let snapshot = self.snapshot(cx.app);
-        let line_height = self.style.text.line_height(cx.font_cache);
+        let style = self.settings.style.clone();
+        let line_height = style.text.line_height(cx.font_cache);
 
         let gutter_padding;
         let gutter_width;
         if snapshot.mode == EditorMode::Full {
-            gutter_padding = self.style.text.em_width(cx.font_cache);
+            gutter_padding = style.text.em_width(cx.font_cache);
             gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0;
         } else {
             gutter_padding = 0.0;
@@ -580,8 +586,8 @@ impl Element for EditorElement {
         };
 
         let text_width = size.x() - gutter_width;
-        let text_offset = vec2f(-self.style.text.descent(cx.font_cache), 0.);
-        let em_width = self.style.text.em_width(cx.font_cache);
+        let text_offset = vec2f(-style.text.descent(cx.font_cache), 0.);
+        let em_width = style.text.em_width(cx.font_cache);
         let overscroll = vec2f(em_width, 0.);
         let wrap_width = text_width - text_offset.x() - overscroll.x() - em_width;
         let snapshot = self.update_view(cx.app, |view, cx| {
@@ -677,7 +683,7 @@ impl Element for EditorElement {
             overscroll,
             text_offset,
             snapshot,
-            style: self.style.clone(),
+            style: self.settings.style.clone(),
             active_rows,
             line_layouts,
             line_number_layouts,
@@ -689,7 +695,7 @@ impl Element for EditorElement {
 
         let scroll_max = layout.scroll_max(cx.font_cache, cx.text_layout_cache).x();
         let scroll_width = layout.scroll_width(cx.text_layout_cache);
-        let max_glyph_width = self.style.text.em_width(&cx.font_cache);
+        let max_glyph_width = style.text.em_width(&cx.font_cache);
         self.update_view(cx.app, |view, cx| {
             let clamped = view.clamp_scroll_left(scroll_max);
             let autoscrolled;
@@ -1035,30 +1041,27 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: f32) -> f32 {
 mod tests {
     use super::*;
     use crate::{
-        editor::{Buffer, Editor, EditorStyle},
-        settings,
         test::sample_text,
+        {Editor, EditorSettings},
     };
+    use buffer::Buffer;
 
     #[gpui::test]
     fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) {
-        let font_cache = cx.font_cache().clone();
-        let settings = settings::test(&cx).1;
-        let style = EditorStyle::test(&font_cache);
+        let settings = EditorSettings::test(cx);
 
         let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6), cx));
         let (window_id, editor) = cx.add_window(Default::default(), |cx| {
             Editor::for_buffer(
                 buffer,
-                settings.clone(),
                 {
-                    let style = style.clone();
-                    move |_| style.clone()
+                    let settings = settings.clone();
+                    move |_| settings.clone()
                 },
                 cx,
             )
         });
-        let element = EditorElement::new(editor.downgrade(), style);
+        let element = EditorElement::new(editor.downgrade(), settings);
 
         let layouts = editor.update(cx, |editor, cx| {
             let snapshot = editor.snapshot(cx);

crates/zed/src/editor.rs β†’ crates/editor/src/lib.rs πŸ”—

@@ -2,8 +2,11 @@ pub mod display_map;
 mod element;
 pub mod movement;
 
-use crate::{project::ProjectPath, settings::Settings, theme::Theme, workspace};
-use anyhow::Result;
+#[cfg(test)]
+mod test;
+
+// use crate::{project::ProjectPath, settings::Settings, theme::Theme, workspace};
+
 use buffer::*;
 use clock::ReplicaId;
 pub use display_map::DisplayPoint;
@@ -12,9 +15,8 @@ pub use element::*;
 use gpui::{
     action, color::Color, fonts::TextStyle, geometry::vector::Vector2F, keymap::Binding,
     text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle,
-    MutableAppContext, RenderContext, Task, View, ViewContext, WeakViewHandle,
+    MutableAppContext, RenderContext, View, ViewContext, WeakViewHandle,
 };
-use postage::watch;
 use serde::{Deserialize, Serialize};
 use smallvec::SmallVec;
 use smol::Timer;
@@ -23,14 +25,12 @@ use std::{
     cmp::{self, Ordering},
     mem,
     ops::{Range, RangeInclusive},
-    path::Path,
     rc::Rc,
     sync::Arc,
     time::Duration,
 };
 use sum_tree::Bias;
 use util::post_inc;
-use worktree::Worktree;
 
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 const MAX_LINE_LEN: usize = 1024;
@@ -279,6 +279,12 @@ pub enum EditorMode {
     Full,
 }
 
+#[derive(Clone)]
+pub struct EditorSettings {
+    pub tab_size: usize,
+    pub style: EditorStyle,
+}
+
 #[derive(Clone, Deserialize)]
 pub struct EditorStyle {
     pub text: TextStyle,
@@ -291,6 +297,7 @@ pub struct EditorStyle {
     pub line_number: Color,
     pub line_number_active: Color,
     pub guest_selections: Vec<SelectionStyle>,
+    pub syntax: Arc<SyntaxTheme>,
 }
 
 #[derive(Clone, Copy, Default, Deserialize)]
@@ -311,8 +318,7 @@ pub struct Editor {
     scroll_position: Vector2F,
     scroll_top_anchor: Anchor,
     autoscroll_requested: bool,
-    build_style: Rc<RefCell<dyn FnMut(&mut MutableAppContext) -> EditorStyle>>,
-    settings: watch::Receiver<Settings>,
+    build_settings: Rc<RefCell<dyn Fn(&AppContext) -> EditorSettings>>,
     focused: bool,
     show_local_cursors: bool,
     blink_epoch: usize,
@@ -325,7 +331,6 @@ pub struct Snapshot {
     pub mode: EditorMode,
     pub display_snapshot: DisplayMapSnapshot,
     pub placeholder_text: Option<Arc<str>>,
-    pub theme: Arc<Theme>,
     is_focused: bool,
     scroll_position: Vector2F,
     scroll_top_anchor: Anchor,
@@ -344,50 +349,53 @@ struct ClipboardSelection {
 
 impl Editor {
     pub fn single_line(
-        settings: watch::Receiver<Settings>,
-        build_style: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle,
+        build_settings: impl 'static + Fn(&AppContext) -> EditorSettings,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
-        let mut view = Self::for_buffer(buffer, settings, build_style, cx);
+        let mut view = Self::for_buffer(buffer, build_settings, cx);
         view.mode = EditorMode::SingleLine;
         view
     }
 
     pub fn auto_height(
         max_lines: usize,
-        settings: watch::Receiver<Settings>,
-        build_style: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle,
+        build_settings: impl 'static + Fn(&AppContext) -> EditorSettings,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
-        let mut view = Self::for_buffer(buffer, settings, build_style, cx);
+        let mut view = Self::for_buffer(buffer, build_settings, cx);
         view.mode = EditorMode::AutoHeight { max_lines };
         view
     }
 
     pub fn for_buffer(
         buffer: ModelHandle<Buffer>,
-        settings: watch::Receiver<Settings>,
-        build_style: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle,
+        build_settings: impl 'static + Fn(&AppContext) -> EditorSettings,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        Self::new(buffer, settings, Rc::new(RefCell::new(build_style)), cx)
+        Self::new(buffer, Rc::new(RefCell::new(build_settings)), cx)
     }
 
-    fn new(
+    pub fn clone(&self, cx: &mut ViewContext<Self>) -> Self {
+        let mut clone = Self::new(self.buffer.clone(), self.build_settings.clone(), cx);
+        clone.scroll_position = self.scroll_position;
+        clone.scroll_top_anchor = self.scroll_top_anchor.clone();
+        clone
+    }
+
+    pub fn new(
         buffer: ModelHandle<Buffer>,
-        settings: watch::Receiver<Settings>,
-        build_style: Rc<RefCell<dyn FnMut(&mut MutableAppContext) -> EditorStyle>>,
+        build_settings: Rc<RefCell<dyn Fn(&AppContext) -> EditorSettings>>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        let style = build_style.borrow_mut()(cx);
+        let settings = build_settings.borrow_mut()(cx);
         let display_map = cx.add_model(|cx| {
             DisplayMap::new(
                 buffer.clone(),
-                settings.borrow().tab_size,
-                style.text.font_id,
-                style.text.font_size,
+                settings.tab_size,
+                settings.style.text.font_id,
+                settings.style.text.font_size,
                 None,
                 cx,
             )
@@ -419,11 +427,10 @@ impl Editor {
             next_selection_id,
             add_selections_state: None,
             select_larger_syntax_node_stack: Vec::new(),
-            build_style,
+            build_settings,
             scroll_position: Vector2F::zero(),
             scroll_top_anchor: Anchor::min(),
             autoscroll_requested: false,
-            settings,
             focused: false,
             show_local_cursors: false,
             blink_epoch: 0,
@@ -442,14 +449,11 @@ impl Editor {
     }
 
     pub fn snapshot(&mut self, cx: &mut MutableAppContext) -> Snapshot {
-        let settings = self.settings.borrow();
-
         Snapshot {
             mode: self.mode,
             display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
             scroll_position: self.scroll_position,
             scroll_top_anchor: self.scroll_top_anchor.clone(),
-            theme: settings.theme.clone(),
             placeholder_text: self.placeholder_text.clone(),
             is_focused: self
                 .handle
@@ -719,7 +723,11 @@ impl Editor {
     }
 
     #[cfg(test)]
-    fn select_display_ranges<'a, T>(&mut self, ranges: T, cx: &mut ViewContext<Self>) -> Result<()>
+    fn select_display_ranges<'a, T>(
+        &mut self,
+        ranges: T,
+        cx: &mut ViewContext<Self>,
+    ) -> anyhow::Result<()>
     where
         T: IntoIterator<Item = &'a Range<DisplayPoint>>,
     {
@@ -2293,9 +2301,9 @@ impl Editor {
             .text()
     }
 
-    pub fn font_size(&self) -> f32 {
-        self.settings.borrow().buffer_font_size
-    }
+    // pub fn font_size(&self) -> f32 {
+    //     self.settings.font_size
+    // }
 
     pub fn set_wrap_width(&self, width: f32, cx: &mut MutableAppContext) -> bool {
         self.display_map
@@ -2409,10 +2417,6 @@ impl Snapshot {
             .highlighted_chunks_for_rows(display_rows)
     }
 
-    pub fn theme(&self) -> &Arc<Theme> {
-        &self.theme
-    }
-
     pub fn scroll_position(&self) -> Vector2F {
         compute_scroll_position(
             &self.display_snapshot,
@@ -2473,6 +2477,7 @@ impl EditorStyle {
             line_number_active: Default::default(),
             selection: Default::default(),
             guest_selections: Default::default(),
+            syntax: Default::default(),
         }
     }
 
@@ -2481,6 +2486,16 @@ impl EditorStyle {
     }
 }
 
+impl EditorSettings {
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn test(cx: &AppContext) -> Self {
+        Self {
+            tab_size: 4,
+            style: EditorStyle::test(cx.font_cache()),
+        }
+    }
+}
+
 fn compute_scroll_position(
     snapshot: &DisplayMapSnapshot,
     mut scroll_position: Vector2F,
@@ -2517,11 +2532,15 @@ impl Entity for Editor {
 
 impl View for Editor {
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let style = self.build_style.borrow_mut()(cx);
+        let settings = self.build_settings.borrow_mut()(cx);
         self.display_map.update(cx, |map, cx| {
-            map.set_font(style.text.font_id, style.text.font_size, cx)
+            map.set_font(
+                settings.style.text.font_id,
+                settings.style.text.font_size,
+                cx,
+            )
         });
-        EditorElement::new(self.handle.clone(), style).boxed()
+        EditorElement::new(self.handle.clone(), settings).boxed()
     }
 
     fn ui_name() -> &'static str {
@@ -2560,156 +2579,6 @@ impl View for Editor {
     }
 }
 
-impl workspace::Item for Buffer {
-    type View = Editor;
-
-    fn build_view(
-        handle: ModelHandle<Self>,
-        settings: watch::Receiver<Settings>,
-        cx: &mut ViewContext<Self::View>,
-    ) -> Self::View {
-        Editor::for_buffer(
-            handle,
-            settings.clone(),
-            move |cx| {
-                let settings = settings.borrow();
-                let font_cache = cx.font_cache();
-                let font_family_id = settings.buffer_font_family;
-                let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
-                let font_properties = Default::default();
-                let font_id = font_cache
-                    .select_font(font_family_id, &font_properties)
-                    .unwrap();
-                let font_size = settings.buffer_font_size;
-
-                let mut theme = settings.theme.editor.clone();
-                theme.text = TextStyle {
-                    color: theme.text.color,
-                    font_family_name,
-                    font_family_id,
-                    font_id,
-                    font_size,
-                    font_properties,
-                    underline: false,
-                };
-                theme
-            },
-            cx,
-        )
-    }
-
-    fn project_path(&self) -> Option<ProjectPath> {
-        self.file().map(|f| ProjectPath {
-            worktree_id: f.worktree_id(),
-            path: f.path().clone(),
-        })
-    }
-}
-
-impl workspace::ItemView for Editor {
-    fn should_activate_item_on_event(event: &Self::Event) -> bool {
-        matches!(event, Event::Activate)
-    }
-
-    fn should_close_item_on_event(event: &Self::Event) -> bool {
-        matches!(event, Event::Closed)
-    }
-
-    fn should_update_tab_on_event(event: &Self::Event) -> bool {
-        matches!(
-            event,
-            Event::Saved | Event::Dirtied | Event::FileHandleChanged
-        )
-    }
-
-    fn title(&self, cx: &AppContext) -> std::string::String {
-        let filename = self
-            .buffer
-            .read(cx)
-            .file()
-            .and_then(|file| file.file_name(cx));
-        if let Some(name) = filename {
-            name.to_string_lossy().into()
-        } else {
-            "untitled".into()
-        }
-    }
-
-    fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
-        self.buffer.read(cx).file().map(|file| ProjectPath {
-            worktree_id: file.worktree_id(),
-            path: file.path().clone(),
-        })
-    }
-
-    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
-    where
-        Self: Sized,
-    {
-        let mut clone = Editor::new(
-            self.buffer.clone(),
-            self.settings.clone(),
-            self.build_style.clone(),
-            cx,
-        );
-        clone.scroll_position = self.scroll_position;
-        clone.scroll_top_anchor = self.scroll_top_anchor.clone();
-        Some(clone)
-    }
-
-    fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
-        let save = self.buffer.update(cx, |b, cx| b.save(cx))?;
-        Ok(cx.spawn(|_, _| async move {
-            save.await?;
-            Ok(())
-        }))
-    }
-
-    fn save_as(
-        &mut self,
-        worktree: ModelHandle<Worktree>,
-        path: &Path,
-        cx: &mut ViewContext<Self>,
-    ) -> Task<Result<()>> {
-        self.buffer.update(cx, |buffer, cx| {
-            let handle = cx.handle();
-            let text = buffer.as_rope().clone();
-            let version = buffer.version();
-
-            let save_as = worktree.update(cx, |worktree, cx| {
-                worktree
-                    .as_local_mut()
-                    .unwrap()
-                    .save_buffer_as(handle, path, text, cx)
-            });
-
-            cx.spawn(|buffer, mut cx| async move {
-                save_as.await.map(|new_file| {
-                    let language = worktree.read_with(&cx, |worktree, cx| {
-                        worktree
-                            .languages()
-                            .select_language(new_file.full_path(cx))
-                            .cloned()
-                    });
-
-                    buffer.update(&mut cx, |buffer, cx| {
-                        buffer.did_save(version, new_file.mtime, Some(Box::new(new_file)), cx);
-                        buffer.set_language(language, cx);
-                    });
-                })
-            })
-        })
-    }
-
-    fn is_dirty(&self, cx: &AppContext) -> bool {
-        self.buffer.read(cx).is_dirty()
-    }
-
-    fn has_conflict(&self, cx: &AppContext) -> bool {
-        self.buffer.read(cx).has_conflict()
-    }
-}
-
 impl SelectionExt for Selection {
     fn display_range(&self, map: &DisplayMapSnapshot) -> Range<DisplayPoint> {
         let start = self.start.to_display_point(map, Bias::Left);
@@ -2749,18 +2618,14 @@ impl SelectionExt for Selection {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{
-        editor::Point,
-        settings,
-        test::{self, sample_text},
-    };
-    use buffer::History;
+    use crate::test::sample_text;
+    use buffer::{History, Point};
     use unindent::Unindent;
 
     #[gpui::test]
     fn test_selection_with_mouse(cx: &mut gpui::MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx));
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(cx);
         let (_, editor) =
             cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
@@ -2827,7 +2692,7 @@ mod tests {
     #[gpui::test]
     fn test_canceling_pending_selection(cx: &mut gpui::MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx));
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
         view.update(cx, |view, cx| {
@@ -2859,7 +2724,7 @@ mod tests {
     #[gpui::test]
     fn test_cancel(cx: &mut gpui::MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx));
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
         view.update(cx, |view, cx| {
@@ -2922,7 +2787,7 @@ mod tests {
                 cx,
             )
         });
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| {
             build_editor(buffer.clone(), settings, cx)
         });
@@ -2990,7 +2855,7 @@ mod tests {
     #[gpui::test]
     fn test_move_cursor(cx: &mut gpui::MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6), cx));
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| {
             build_editor(buffer.clone(), settings, cx)
         });
@@ -3067,7 +2932,7 @@ mod tests {
     #[gpui::test]
     fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, "ⓐⓑⓒⓓⓔ\nabcde\nαβγδΡ\n", cx));
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| {
             build_editor(buffer.clone(), settings, cx)
         });
@@ -3125,7 +2990,7 @@ mod tests {
     #[gpui::test]
     fn test_move_cursor_different_line_lengths(cx: &mut gpui::MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, "ⓐⓑⓒⓓⓔ\nabcd\nΞ±Ξ²Ξ³\nabcd\nⓐⓑⓒⓓⓔ\n", cx));
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| {
             build_editor(buffer.clone(), settings, cx)
         });
@@ -3156,7 +3021,7 @@ mod tests {
     #[gpui::test]
     fn test_beginning_end_of_line(cx: &mut gpui::MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, "abc\n  def", cx));
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.select_display_ranges(
@@ -3299,7 +3164,7 @@ mod tests {
     fn test_prev_next_word_boundary(cx: &mut gpui::MutableAppContext) {
         let buffer =
             cx.add_model(|cx| Buffer::new(0, "use std::str::{foo, bar}\n\n  {baz.qux()}", cx));
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.select_display_ranges(
@@ -3439,11 +3304,11 @@ mod tests {
     fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut gpui::MutableAppContext) {
         let buffer =
             cx.add_model(|cx| Buffer::new(0, "use one::{\n    two::three::four::five\n};", cx));
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
         view.update(cx, |view, cx| {
-            view.set_wrap_width(130., cx);
+            view.set_wrap_width(140., cx);
             assert_eq!(
                 view.display_text(cx),
                 "use one::{\n    two::three::\n    four::five\n};"
@@ -3493,7 +3358,7 @@ mod tests {
     #[gpui::test]
     fn test_delete_to_word_boundary(cx: &mut gpui::MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, "one two three four", cx));
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| {
             build_editor(buffer.clone(), settings, cx)
         });
@@ -3540,7 +3405,7 @@ mod tests {
                 cx,
             )
         });
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| {
             build_editor(buffer.clone(), settings, cx)
         });
@@ -3576,7 +3441,7 @@ mod tests {
                 cx,
             )
         });
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| {
             build_editor(buffer.clone(), settings, cx)
         });
@@ -3605,7 +3470,7 @@ mod tests {
 
     #[gpui::test]
     fn test_delete_line(cx: &mut gpui::MutableAppContext) {
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(&cx);
         let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx));
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
@@ -3629,7 +3494,7 @@ mod tests {
             );
         });
 
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(&cx);
         let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx));
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
@@ -3646,7 +3511,7 @@ mod tests {
 
     #[gpui::test]
     fn test_duplicate_line(cx: &mut gpui::MutableAppContext) {
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(&cx);
         let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx));
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
@@ -3673,7 +3538,7 @@ mod tests {
             );
         });
 
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(&cx);
         let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx));
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
@@ -3699,7 +3564,7 @@ mod tests {
 
     #[gpui::test]
     fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(&cx);
         let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(10, 5), cx));
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
@@ -3797,7 +3662,7 @@ mod tests {
     #[gpui::test]
     fn test_clipboard(cx: &mut gpui::MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, "oneβœ… two three four five six ", cx));
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(&cx);
         let view = cx
             .add_window(Default::default(), |cx| {
                 build_editor(buffer.clone(), settings, cx)
@@ -3932,7 +3797,7 @@ mod tests {
     #[gpui::test]
     fn test_select_all(cx: &mut gpui::MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, "abc\nde\nfgh", cx));
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.select_all(&SelectAll, cx);
@@ -3945,7 +3810,7 @@ mod tests {
 
     #[gpui::test]
     fn test_select_line(cx: &mut gpui::MutableAppContext) {
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(&cx);
         let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 5), cx));
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
@@ -3991,7 +3856,7 @@ mod tests {
 
     #[gpui::test]
     fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) {
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(&cx);
         let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(9, 5), cx));
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
@@ -4059,7 +3924,7 @@ mod tests {
 
     #[gpui::test]
     fn test_add_selection_above_below(cx: &mut gpui::MutableAppContext) {
-        let settings = settings::test(&cx).1;
+        let settings = EditorSettings::test(&cx);
         let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefghi\n\njk\nlmno\n", cx));
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
@@ -4232,9 +4097,17 @@ mod tests {
 
     #[gpui::test]
     async fn test_select_larger_smaller_syntax_node(mut cx: gpui::TestAppContext) {
-        let app_state = cx.update(test::test_app_state);
+        let settings = cx.read(EditorSettings::test);
+
+        let grammar = tree_sitter_rust::language();
+        let language = Arc::new(Language {
+            config: LanguageConfig::default(),
+            brackets_query: tree_sitter::Query::new(grammar, "").unwrap(),
+            highlight_query: tree_sitter::Query::new(grammar, "").unwrap(),
+            highlight_map: Default::default(),
+            grammar,
+        });
 
-        let lang = app_state.languages.select_language("z.rs");
         let text = r#"
             use mod1::mod2::{mod3, mod4};
 
@@ -4245,9 +4118,9 @@ mod tests {
         .unindent();
         let buffer = cx.add_model(|cx| {
             let history = History::new(text.into());
-            Buffer::from_history(0, history, None, lang.cloned(), cx)
+            Buffer::from_history(0, history, None, Some(language), cx)
         });
-        let (_, view) = cx.add_window(|cx| build_editor(buffer, app_state.settings.clone(), cx));
+        let (_, view) = cx.add_window(|cx| build_editor(buffer, settings, cx));
         view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing())
             .await;
 
@@ -4388,36 +4261,10 @@ mod tests {
 
     fn build_editor(
         buffer: ModelHandle<Buffer>,
-        settings: watch::Receiver<Settings>,
+        settings: EditorSettings,
         cx: &mut ViewContext<Editor>,
     ) -> Editor {
-        let style = {
-            let font_cache = cx.font_cache();
-            let settings = settings.borrow();
-            EditorStyle {
-                text: TextStyle {
-                    color: Default::default(),
-                    font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(),
-                    font_family_id: settings.buffer_font_family,
-                    font_id: font_cache
-                        .select_font(settings.buffer_font_family, &Default::default())
-                        .unwrap(),
-                    font_size: settings.buffer_font_size,
-                    font_properties: Default::default(),
-                    underline: false,
-                },
-                placeholder_text: None,
-                background: Default::default(),
-                selection: Default::default(),
-                gutter_background: Default::default(),
-                active_line_background: Default::default(),
-                line_number: Default::default(),
-                line_number_active: Default::default(),
-                guest_selections: Default::default(),
-            }
-        };
-
-        Editor::for_buffer(buffer, settings, move |_| style.clone(), cx)
+        Editor::for_buffer(buffer, move |_| settings.clone(), cx)
     }
 }
 

crates/zed/src/editor/movement.rs β†’ crates/editor/src/movement.rs πŸ”—

@@ -196,7 +196,7 @@ fn char_kind(c: char) -> CharKind {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::editor::{display_map::DisplayMap, Buffer};
+    use crate::{display_map::DisplayMap, Buffer};
 
     #[gpui::test]
     fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {

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

@@ -0,0 +1,39 @@
+use gpui::{Entity, ModelHandle};
+use smol::channel;
+use std::marker::PhantomData;
+
+pub fn sample_text(rows: usize, cols: usize) -> String {
+    let mut text = String::new();
+    for row in 0..rows {
+        let c: char = ('a' as u32 + row as u32) as u8 as char;
+        let mut line = c.to_string().repeat(cols);
+        if row < rows - 1 {
+            line.push('\n');
+        }
+        text += &line;
+    }
+    text
+}
+
+pub struct Observer<T>(PhantomData<T>);
+
+impl<T: 'static> Entity for Observer<T> {
+    type Event = ();
+}
+
+impl<T: Entity> Observer<T> {
+    pub fn new(
+        handle: &ModelHandle<T>,
+        cx: &mut gpui::TestAppContext,
+    ) -> (ModelHandle<Self>, channel::Receiver<()>) {
+        let (notify_tx, notify_rx) = channel::unbounded();
+        let observer = cx.add_model(|cx| {
+            cx.observe(handle, move |_, _, _| {
+                let _ = notify_tx.try_send(());
+            })
+            .detach();
+            Observer(PhantomData)
+        });
+        (observer, notify_rx)
+    }
+}

crates/server/src/main.rs πŸ”—

@@ -13,6 +13,7 @@ mod rpc;
 mod team;
 
 use self::errors::TideResultExt as _;
+use ::rpc::Peer;
 use anyhow::Result;
 use async_std::net::TcpListener;
 use async_trait::async_trait;
@@ -26,7 +27,6 @@ use std::sync::Arc;
 use surf::http::cookies::SameSite;
 use tide::{log, sessions::SessionMiddleware};
 use tide_compress::CompressMiddleware;
-use rpc::Peer;
 
 type Request = tide::Request<Arc<AppState>>;
 

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

@@ -10,6 +10,10 @@ use async_std::{sync::RwLock, task};
 use async_tungstenite::{tungstenite::protocol::Role, WebSocketStream};
 use futures::{future::BoxFuture, FutureExt};
 use postage::{mpsc, prelude::Sink as _, prelude::Stream as _};
+use rpc::{
+    proto::{self, AnyTypedEnvelope, EnvelopedMessage},
+    Connection, ConnectionId, Peer, TypedEnvelope,
+};
 use sha1::{Digest as _, Sha1};
 use std::{
     any::TypeId,
@@ -27,10 +31,6 @@ use tide::{
     Request, Response,
 };
 use time::OffsetDateTime;
-use rpc::{
-    proto::{self, AnyTypedEnvelope, EnvelopedMessage},
-    Connection, ConnectionId, Peer, TypedEnvelope,
-};
 
 type MessageHandler = Box<
     dyn Send
@@ -960,6 +960,7 @@ mod tests {
         db::{tests::TestDb, UserId},
         github, AppState, Config,
     };
+    use ::rpc::Peer;
     use async_std::{sync::RwLockReadGuard, task};
     use gpui::{ModelHandle, TestAppContext};
     use parking_lot::Mutex;
@@ -977,23 +978,20 @@ mod tests {
     use zed::{
         buffer::LanguageRegistry,
         channel::{Channel, ChannelDetails, ChannelList},
-        editor::{Editor, EditorStyle, Insert},
+        editor::{Editor, EditorSettings, Insert},
         fs::{FakeFs, Fs as _},
         people_panel::JoinWorktree,
         project::ProjectPath,
         rpc::{self, Client, Credentials, EstablishConnectionError},
-        settings,
         test::FakeHttpClient,
         user::UserStore,
         workspace::Workspace,
         worktree::Worktree,
     };
-    use rpc::Peer;
 
     #[gpui::test]
     async fn test_share_worktree(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
         let (window_b, _) = cx_b.add_window(|_| EmptyView);
-        let settings = cx_b.read(settings::test).1;
         let lang_registry = Arc::new(LanguageRegistry::new());
 
         // Connect to a server as 2 clients.
@@ -1063,12 +1061,7 @@ mod tests {
 
         // Create a selection set as client B and see that selection set as client A.
         let editor_b = cx_b.add_view(window_b, |cx| {
-            Editor::for_buffer(
-                buffer_b,
-                settings,
-                |cx| EditorStyle::test(cx.font_cache()),
-                cx,
-            )
+            Editor::for_buffer(buffer_b, |cx| EditorSettings::test(cx), cx)
         });
         buffer_a
             .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1)

crates/zed/Cargo.toml πŸ”—

@@ -33,6 +33,7 @@ clock = { path = "../clock" }
 crossbeam-channel = "0.5.0"
 ctor = "0.1.20"
 dirs = "3.0"
+editor = { path = "../editor" }
 easy-parallel = "3.1.0"
 fsevent = { path = "../fsevent" }
 futures = "0.3"
@@ -70,7 +71,7 @@ tree-sitter = "0.19.5"
 tree-sitter-rust = "0.19.0"
 url = "2.2"
 util = { path = "../util" }
-worktree =  { path = "../worktree" }
+worktree = { path = "../worktree" }
 rpc = { path = "../rpc" }
 
 [dev-dependencies]
@@ -80,6 +81,7 @@ serde_json = { version = "1.0.64", features = ["preserve_order"] }
 tempdir = { version = "0.3.7" }
 unindent = "0.1.7"
 buffer = { path = "../buffer", features = ["test-support"] }
+editor = { path = "../editor", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 rpc_client = { path = "../rpc_client", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }

crates/zed/assets/themes/_base.toml πŸ”—

@@ -208,7 +208,7 @@ padding = { left = 16, right = 16, top = 8, bottom = 4 }
 
 [selector.item]
 text = "$text.1"
-highlight_text = { extends = "$text.base", color = "$syntax.keyword.color", weight = "$syntax.keyword.weight" }
+highlight_text = { extends = "$text.base", color = "$editor.syntax.keyword.color", weight = "$editor.syntax.keyword.weight" }
 padding = { left = 16, right = 16, top = 4, bottom = 4 }
 corner_radius = 6
 

crates/zed/assets/themes/black.toml πŸ”—

@@ -26,7 +26,7 @@ guests = [
   { selection = "#3B874B33", cursor = "#3B874B" },
   { selection = "#BD7CB433", cursor = "#BD7CB4" },
   { selection = "#EE823133", cursor = "#EE8231" },
-  { selection = "#5A2B9233", cursor = "#5A2B92" }
+  { selection = "#5A2B9233", cursor = "#5A2B92" },
 ]
 
 [status]
@@ -39,7 +39,7 @@ bad = "#b7372e"
 active_line = "#00000033"
 hover = "#00000033"
 
-[syntax]
+[editor.syntax]
 keyword = { color = "#0086c0", weight = "bold" }
 function = "#dcdcaa"
 string = "#cb8f77"

crates/zed/assets/themes/dark.toml πŸ”—

@@ -26,7 +26,7 @@ guests = [
   { selection = "#3B874B33", cursor = "#3B874B" },
   { selection = "#BD7CB433", cursor = "#BD7CB4" },
   { selection = "#EE823133", cursor = "#EE8231" },
-  { selection = "#5A2B9233", cursor = "#5A2B92" }
+  { selection = "#5A2B9233", cursor = "#5A2B92" },
 ]
 
 [status]
@@ -39,7 +39,7 @@ bad = "#b7372e"
 active_line = "#00000022"
 hover = "#00000033"
 
-[syntax]
+[editor.syntax]
 keyword = { color = "#0086c0", weight = "bold" }
 function = "#dcdcaa"
 string = "#cb8f77"

crates/zed/assets/themes/light.toml πŸ”—

@@ -26,7 +26,7 @@ guests = [
   { selection = "#3B874B33", cursor = "#3B874B" },
   { selection = "#BD7CB433", cursor = "#BD7CB4" },
   { selection = "#EE823133", cursor = "#EE8231" },
-  { selection = "#5A2B9233", cursor = "#5A2B92" }
+  { selection = "#5A2B9233", cursor = "#5A2B92" },
 ]
 
 [status]
@@ -39,7 +39,7 @@ bad = "#b7372e"
 active_line = "#00000008"
 hover = "#0000000D"
 
-[syntax]
+[editor.syntax]
 keyword = { color = "#0000fa", weight = "bold" }
 function = "#795e26"
 string = "#a82121"

crates/zed/src/chat_panel.rs πŸ”—

@@ -1,10 +1,8 @@
-use std::sync::Arc;
-
 use crate::{
     channel::{Channel, ChannelEvent, ChannelList, ChannelMessage},
-    editor::Editor,
     theme, Settings,
 };
+use editor::{Editor, EditorSettings};
 use gpui::{
     action,
     elements::*,
@@ -16,6 +14,7 @@ use gpui::{
 };
 use postage::{prelude::Stream, watch};
 use rpc_client as rpc;
+use std::sync::Arc;
 use time::{OffsetDateTime, UtcOffset};
 use util::{ResultExt, TryFutureExt};
 
@@ -55,10 +54,15 @@ impl ChatPanel {
         let input_editor = cx.add_view(|cx| {
             Editor::auto_height(
                 4,
-                settings.clone(),
                 {
                     let settings = settings.clone();
-                    move |_| settings.borrow().theme.chat_panel.input_editor.as_editor()
+                    move |_| {
+                        let settings = settings.borrow();
+                        EditorSettings {
+                            tab_size: settings.tab_size,
+                            style: settings.theme.chat_panel.input_editor.as_editor(),
+                        }
+                    }
                 },
                 cx,
             )

crates/zed/src/file_finder.rs πŸ”—

@@ -1,10 +1,10 @@
 use crate::{
-    editor::{self, Editor},
     fuzzy::PathMatch,
     project::{Project, ProjectPath},
     settings::Settings,
     workspace::Workspace,
 };
+use editor::{self, Editor, EditorSettings};
 use gpui::{
     action,
     elements::*,
@@ -271,10 +271,15 @@ impl FileFinder {
 
         let query_editor = cx.add_view(|cx| {
             Editor::single_line(
-                settings.clone(),
                 {
                     let settings = settings.clone();
-                    move |_| settings.borrow().theme.selector.input_editor.as_editor()
+                    move |_| {
+                        let settings = settings.borrow();
+                        EditorSettings {
+                            style: settings.theme.selector.input_editor.as_editor(),
+                            tab_size: settings.tab_size,
+                        }
+                    }
                 },
                 cx,
             )
@@ -420,11 +425,8 @@ impl FileFinder {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{
-        editor::{self, Insert},
-        test::test_app_state,
-        workspace::Workspace,
-    };
+    use crate::{test::test_app_state, workspace::Workspace};
+    use editor::{self, Insert};
     use serde_json::json;
     use std::path::PathBuf;
     use worktree::fs::FakeFs;

crates/zed/src/lib.rs πŸ”—

@@ -1,7 +1,6 @@
 pub mod assets;
 pub mod channel;
 pub mod chat_panel;
-pub mod editor;
 pub mod file_finder;
 mod fuzzy;
 pub mod http;
@@ -21,6 +20,7 @@ pub mod workspace;
 pub use buffer;
 use buffer::LanguageRegistry;
 use channel::ChannelList;
+pub use editor;
 use gpui::{action, keymap::Binding, ModelHandle};
 use parking_lot::Mutex;
 use postage::watch;

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

@@ -33,7 +33,7 @@ fn main() {
     let themes = settings::ThemeRegistry::new(Assets, app.font_cache());
     let (settings_tx, settings) = settings::channel(&app.font_cache(), &themes).unwrap();
     let languages = Arc::new(language::build_language_registry());
-    languages.set_theme(&settings.borrow().theme.syntax);
+    languages.set_theme(&settings.borrow().theme.editor.syntax);
 
     app.run(move |cx| {
         let rpc = rpc::Client::new();

crates/zed/src/menus.rs πŸ”—

@@ -4,8 +4,6 @@ use std::sync::Arc;
 
 #[cfg(target_os = "macos")]
 pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
-    use crate::editor;
-
     vec![
         Menu {
             name: "Zed",

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

@@ -10,11 +10,10 @@ use crate::{
 use anyhow::Result;
 use buffer::LanguageRegistry;
 use futures::{future::BoxFuture, Future};
-use gpui::{Entity, ModelHandle, MutableAppContext};
+use gpui::MutableAppContext;
 use parking_lot::Mutex;
 use rpc_client as rpc;
-use smol::channel;
-use std::{fmt, marker::PhantomData, sync::Arc};
+use std::{fmt, sync::Arc};
 use worktree::fs::FakeFs;
 
 #[cfg(test)]
@@ -23,19 +22,6 @@ fn init_logger() {
     env_logger::init();
 }
 
-pub fn sample_text(rows: usize, cols: usize) -> String {
-    let mut text = String::new();
-    for row in 0..rows {
-        let c: char = ('a' as u32 + row as u32) as u8 as char;
-        let mut line = c.to_string().repeat(cols);
-        if row < rows - 1 {
-            line.push('\n');
-        }
-        text += &line;
-    }
-    text
-}
-
 pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
     let (settings_tx, settings) = settings::test(cx);
     let mut languages = LanguageRegistry::new();
@@ -56,29 +42,6 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
     })
 }
 
-pub struct Observer<T>(PhantomData<T>);
-
-impl<T: 'static> Entity for Observer<T> {
-    type Event = ();
-}
-
-impl<T: Entity> Observer<T> {
-    pub fn new(
-        handle: &ModelHandle<T>,
-        cx: &mut gpui::TestAppContext,
-    ) -> (ModelHandle<Self>, channel::Receiver<()>) {
-        let (notify_tx, notify_rx) = channel::unbounded();
-        let observer = cx.add_model(|cx| {
-            cx.observe(handle, move |_, _, _| {
-                let _ = notify_tx.try_send(());
-            })
-            .detach();
-            Observer(PhantomData)
-        });
-        (observer, notify_rx)
-    }
-}
-
 pub struct FakeHttpClient {
     handler:
         Box<dyn 'static + Send + Sync + Fn(Request) -> BoxFuture<'static, Result<ServerResponse>>>,

crates/zed/src/theme.rs πŸ”—

@@ -1,8 +1,7 @@
 mod resolution;
 mod theme_registry;
 
-use crate::editor::{EditorStyle, SelectionStyle};
-use buffer::SyntaxTheme;
+use editor::{EditorStyle, SelectionStyle};
 use gpui::{
     color::Color,
     elements::{ContainerStyle, ImageStyle, LabelStyle},
@@ -25,7 +24,6 @@ pub struct Theme {
     pub project_panel: ProjectPanel,
     pub selector: Selector,
     pub editor: EditorStyle,
-    pub syntax: SyntaxTheme,
 }
 
 #[derive(Deserialize)]
@@ -228,6 +226,7 @@ impl InputEditorStyle {
             line_number: Default::default(),
             line_number_active: Default::default(),
             guest_selections: Default::default(),
+            syntax: Default::default(),
         }
     }
 }

crates/zed/src/theme_selector.rs πŸ”—

@@ -1,12 +1,12 @@
 use std::{cmp, sync::Arc};
 
 use crate::{
-    editor::{self, Editor},
     fuzzy::{match_strings, StringMatch, StringMatchCandidate},
     settings::ThemeRegistry,
     workspace::Workspace,
     AppState, Settings,
 };
+use editor::{self, Editor, EditorSettings};
 use gpui::{
     action,
     elements::*,
@@ -59,10 +59,15 @@ impl ThemeSelector {
     ) -> Self {
         let query_editor = cx.add_view(|cx| {
             Editor::single_line(
-                settings.clone(),
                 {
                     let settings = settings.clone();
-                    move |_| settings.borrow().theme.selector.input_editor.as_editor()
+                    move |_| {
+                        let settings = settings.borrow();
+                        EditorSettings {
+                            tab_size: settings.tab_size,
+                            style: settings.theme.selector.input_editor.as_editor(),
+                        }
+                    }
                 },
                 cx,
             )

crates/zed/src/workspace.rs πŸ”—

@@ -1,3 +1,4 @@
+mod items;
 pub mod pane;
 pub mod pane_group;
 pub mod sidebar;
@@ -1156,11 +1157,8 @@ impl WorkspaceHandle for ViewHandle<Workspace> {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{
-        editor::{Editor, Insert},
-        fs::FakeFs,
-        test::test_app_state,
-    };
+    use crate::{fs::FakeFs, test::test_app_state};
+    use editor::{Editor, Insert};
     use serde_json::json;
     use std::collections::HashSet;
     use util::test::temp_tree;

crates/zed/src/workspace/items.rs πŸ”—

@@ -0,0 +1,153 @@
+use super::{Item, ItemView};
+use crate::{project::ProjectPath, Settings};
+use anyhow::Result;
+use buffer::{Buffer, File as _};
+use editor::{Editor, EditorSettings, Event};
+use gpui::{fonts::TextStyle, AppContext, ModelHandle, Task, ViewContext};
+use postage::watch;
+use std::path::Path;
+use worktree::Worktree;
+
+impl Item for Buffer {
+    type View = Editor;
+
+    fn build_view(
+        handle: ModelHandle<Self>,
+        settings: watch::Receiver<Settings>,
+        cx: &mut ViewContext<Self::View>,
+    ) -> Self::View {
+        Editor::for_buffer(
+            handle,
+            move |cx| {
+                let settings = settings.borrow();
+                let font_cache = cx.font_cache();
+                let font_family_id = settings.buffer_font_family;
+                let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
+                let font_properties = Default::default();
+                let font_id = font_cache
+                    .select_font(font_family_id, &font_properties)
+                    .unwrap();
+                let font_size = settings.buffer_font_size;
+
+                let mut theme = settings.theme.editor.clone();
+                theme.text = TextStyle {
+                    color: theme.text.color,
+                    font_family_name,
+                    font_family_id,
+                    font_id,
+                    font_size,
+                    font_properties,
+                    underline: false,
+                };
+                EditorSettings {
+                    tab_size: settings.tab_size,
+                    style: theme,
+                }
+            },
+            cx,
+        )
+    }
+
+    fn project_path(&self) -> Option<ProjectPath> {
+        self.file().map(|f| ProjectPath {
+            worktree_id: f.worktree_id(),
+            path: f.path().clone(),
+        })
+    }
+}
+
+impl ItemView for Editor {
+    fn should_activate_item_on_event(event: &Event) -> bool {
+        matches!(event, Event::Activate)
+    }
+
+    fn should_close_item_on_event(event: &Event) -> bool {
+        matches!(event, Event::Closed)
+    }
+
+    fn should_update_tab_on_event(event: &Event) -> bool {
+        matches!(
+            event,
+            Event::Saved | Event::Dirtied | Event::FileHandleChanged
+        )
+    }
+
+    fn title(&self, cx: &AppContext) -> String {
+        let filename = self
+            .buffer()
+            .read(cx)
+            .file()
+            .and_then(|file| file.file_name(cx));
+        if let Some(name) = filename {
+            name.to_string_lossy().into()
+        } else {
+            "untitled".into()
+        }
+    }
+
+    fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
+        self.buffer().read(cx).file().map(|file| ProjectPath {
+            worktree_id: file.worktree_id(),
+            path: file.path().clone(),
+        })
+    }
+
+    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
+    where
+        Self: Sized,
+    {
+        Some(self.clone(cx))
+    }
+
+    fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
+        let save = self.buffer().update(cx, |b, cx| b.save(cx))?;
+        Ok(cx.spawn(|_, _| async move {
+            save.await?;
+            Ok(())
+        }))
+    }
+
+    fn save_as(
+        &mut self,
+        worktree: ModelHandle<Worktree>,
+        path: &Path,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        self.buffer().update(cx, |buffer, cx| {
+            let handle = cx.handle();
+            let text = buffer.as_rope().clone();
+            let version = buffer.version();
+
+            let save_as = worktree.update(cx, |worktree, cx| {
+                worktree
+                    .as_local_mut()
+                    .unwrap()
+                    .save_buffer_as(handle, path, text, cx)
+            });
+
+            cx.spawn(|buffer, mut cx| async move {
+                save_as.await.map(|new_file| {
+                    let language = worktree.read_with(&cx, |worktree, cx| {
+                        worktree
+                            .languages()
+                            .select_language(new_file.full_path(cx))
+                            .cloned()
+                    });
+
+                    buffer.update(&mut cx, |buffer, cx| {
+                        buffer.did_save(version, new_file.mtime, Some(Box::new(new_file)), cx);
+                        buffer.set_language(language, cx);
+                    });
+                })
+            })
+        })
+    }
+
+    fn is_dirty(&self, cx: &AppContext) -> bool {
+        self.buffer().read(cx).is_dirty()
+    }
+
+    fn has_conflict(&self, cx: &AppContext) -> bool {
+        self.buffer().read(cx).has_conflict()
+    }
+}