Merge branch 'main' into people-panel

Antonio Scandurra created

Change summary

Dockerfile.migrator                      |   4 
gpui/Cargo.toml                          |   3 
gpui/src/app.rs                          |  10 
gpui/src/elements/container.rs           |  11 
gpui/src/elements/mouse_event_handler.rs |   3 
gpui/src/elements/svg.rs                 |   5 
gpui/src/font_cache.rs                   |  26 
gpui/src/fonts.rs                        |  33 +
gpui/src/presenter.rs                    |   8 
server/src/rpc.rs                        |  11 
zed/Cargo.toml                           |   3 
zed/assets/themes/_base.toml             |  12 
zed/src/chat_panel.rs                    |  13 
zed/src/editor.rs                        | 593 ++++++++++---------------
zed/src/editor/buffer.rs                 |  17 
zed/src/editor/display_map.rs            | 161 ++++--
zed/src/editor/display_map/wrap_map.rs   |  87 +--
zed/src/editor/element.rs                | 338 +++++++++++--
zed/src/editor/movement.rs               |  17 
zed/src/file_finder.rs                   |  12 
zed/src/language.rs                      |  23 
zed/src/rpc.rs                           |  12 
zed/src/theme.rs                         |  56 --
zed/src/theme_selector.rs                |  12 
zed/src/workspace.rs                     |  54 ++
zed/src/workspace/pane.rs                |  70 +-
zed/src/worktree.rs                      |  20 
27 files changed, 912 insertions(+), 702 deletions(-)

Detailed changes

Dockerfile.migrator πŸ”—

@@ -1,12 +1,12 @@
 # syntax = docker/dockerfile:1.2
 
-FROM rust as builder
+FROM rust:1.55-bullseye as builder
 WORKDIR app
 RUN --mount=type=cache,target=/usr/local/cargo/registry \
     --mount=type=cache,target=./target \
     cargo install sqlx-cli --root=/app --target-dir=/app/target --version 0.5.7
 
-FROM debian:buster-slim as runtime
+FROM debian:bullseye-slim as runtime
 RUN apt-get update; \
     apt-get install -y --no-install-recommends libssl1.1
 WORKDIR app

gpui/Cargo.toml πŸ”—

@@ -4,6 +4,9 @@ edition = "2018"
 name = "gpui"
 version = "0.1.0"
 
+[features]
+test-support = []
+
 [dependencies]
 arrayvec = "0.7.1"
 async-task = "4.0.3"

gpui/src/app.rs πŸ”—

@@ -2335,6 +2335,16 @@ impl<V: View> ReadModel for RenderContext<'_, V> {
     }
 }
 
+impl<V: View> UpdateModel for RenderContext<'_, V> {
+    fn update_model<T, F, S>(&mut self, handle: &ModelHandle<T>, update: F) -> S
+    where
+        T: Entity,
+        F: FnOnce(&mut T, &mut ModelContext<T>) -> S,
+    {
+        self.app.update_model(handle, update)
+    }
+}
+
 impl<M> AsRef<AppContext> for ViewContext<'_, M> {
     fn as_ref(&self) -> &AppContext {
         &self.app.cx

gpui/src/elements/container.rs πŸ”—

@@ -348,6 +348,17 @@ enum Spacing {
     },
 }
 
+impl Padding {
+    pub fn uniform(padding: f32) -> Self {
+        Self {
+            top: padding,
+            left: padding,
+            bottom: padding,
+            right: padding,
+        }
+    }
+}
+
 impl ToJson for Padding {
     fn to_json(&self) -> serde_json::Value {
         let mut value = json!({});

gpui/src/elements/mouse_event_handler.rs πŸ”—

@@ -116,7 +116,8 @@ impl Element for MouseEventHandler {
         let hit_bounds = RectF::from_points(
             bounds.origin() - vec2f(self.padding.left, self.padding.top),
             bounds.lower_right() + vec2f(self.padding.right, self.padding.bottom),
-        );
+        )
+        .round_out();
 
         self.state.update(cx, |state, cx| match event {
             Event::MouseMoved {

gpui/src/elements/svg.rs πŸ”—

@@ -47,8 +47,9 @@ impl Element for Svg {
                 );
                 (size, Some(tree))
             }
-            Err(error) => {
-                log::error!("{}", error);
+            Err(_error) => {
+                #[cfg(not(any(test, feature = "test-support")))]
+                log::error!("{}", _error);
                 (constraint.min, None)
             }
         }

gpui/src/font_cache.rs πŸ”—

@@ -17,7 +17,7 @@ use std::{
 pub struct FamilyId(usize);
 
 struct Family {
-    name: String,
+    name: Arc<str>,
     font_ids: Vec<FontId>,
 }
 
@@ -49,7 +49,7 @@ impl FontCache {
         }))
     }
 
-    pub fn family_name(&self, family_id: FamilyId) -> Result<String> {
+    pub fn family_name(&self, family_id: FamilyId) -> Result<Arc<str>> {
         self.0
             .read()
             .families
@@ -62,7 +62,7 @@ impl FontCache {
         for name in names {
             let state = self.0.upgradable_read();
 
-            if let Some(ix) = state.families.iter().position(|f| f.name == *name) {
+            if let Some(ix) = state.families.iter().position(|f| f.name.as_ref() == *name) {
                 return Ok(FamilyId(ix));
             }
 
@@ -81,7 +81,7 @@ impl FontCache {
                 }
 
                 state.families.push(Family {
-                    name: String::from(*name),
+                    name: Arc::from(*name),
                     font_ids,
                 });
                 return Ok(family_id);
@@ -141,8 +141,8 @@ impl FontCache {
 
     pub fn bounding_box(&self, font_id: FontId, font_size: f32) -> Vector2F {
         let bounding_box = self.metric(font_id, |m| m.bounding_box);
-        let width = self.scale_metric(bounding_box.width(), font_id, font_size);
-        let height = self.scale_metric(bounding_box.height(), font_id, font_size);
+        let width = bounding_box.width() * self.em_scale(font_id, font_size);
+        let height = bounding_box.height() * self.em_scale(font_id, font_size);
         vec2f(width, height)
     }
 
@@ -154,28 +154,28 @@ impl FontCache {
             glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap();
             bounds = state.fonts.typographic_bounds(font_id, glyph_id).unwrap();
         }
-        self.scale_metric(bounds.width(), font_id, font_size)
+        bounds.width() * self.em_scale(font_id, font_size)
     }
 
     pub fn line_height(&self, font_id: FontId, font_size: f32) -> f32 {
         let height = self.metric(font_id, |m| m.bounding_box.height());
-        self.scale_metric(height, font_id, font_size)
+        (height * self.em_scale(font_id, font_size)).ceil()
     }
 
     pub fn cap_height(&self, font_id: FontId, font_size: f32) -> f32 {
-        self.scale_metric(self.metric(font_id, |m| m.cap_height), font_id, font_size)
+        self.metric(font_id, |m| m.cap_height) * self.em_scale(font_id, font_size)
     }
 
     pub fn ascent(&self, font_id: FontId, font_size: f32) -> f32 {
-        self.scale_metric(self.metric(font_id, |m| m.ascent), font_id, font_size)
+        self.metric(font_id, |m| m.ascent) * self.em_scale(font_id, font_size)
     }
 
     pub fn descent(&self, font_id: FontId, font_size: f32) -> f32 {
-        self.scale_metric(self.metric(font_id, |m| -m.descent), font_id, font_size)
+        self.metric(font_id, |m| -m.descent) * self.em_scale(font_id, font_size)
     }
 
-    pub fn scale_metric(&self, metric: f32, font_id: FontId, font_size: f32) -> f32 {
-        metric * font_size / self.metric(font_id, |m| m.units_per_em as f32)
+    pub fn em_scale(&self, font_id: FontId, font_size: f32) -> f32 {
+        font_size / self.metric(font_id, |m| m.units_per_em as f32)
     }
 
     pub fn line_wrapper(self: &Arc<Self>, font_id: FontId, font_size: f32) -> LineWrapperHandle {

gpui/src/fonts.rs πŸ”—

@@ -1,5 +1,6 @@
 use crate::{
     color::Color,
+    font_cache::FamilyId,
     json::{json, ToJson},
     text_layout::RunStyle,
     FontCache,
@@ -22,6 +23,7 @@ pub type GlyphId = u32;
 pub struct TextStyle {
     pub color: Color,
     pub font_family_name: Arc<str>,
+    pub font_family_id: FamilyId,
     pub font_id: FontId,
     pub font_size: f32,
     pub font_properties: Properties,
@@ -85,11 +87,12 @@ impl TextStyle {
         font_cache: &FontCache,
     ) -> anyhow::Result<Self> {
         let font_family_name = font_family_name.into();
-        let family_id = font_cache.load_family(&[&font_family_name])?;
-        let font_id = font_cache.select_font(family_id, &font_properties)?;
+        let font_family_id = font_cache.load_family(&[&font_family_name])?;
+        let font_id = font_cache.select_font(font_family_id, &font_properties)?;
         Ok(Self {
             color,
             font_family_name,
+            font_family_id,
             font_id,
             font_size,
             font_properties,
@@ -124,6 +127,32 @@ impl TextStyle {
             }
         })
     }
+
+    pub fn line_height(&self, font_cache: &FontCache) -> f32 {
+        font_cache.line_height(self.font_id, self.font_size)
+    }
+
+    pub fn em_width(&self, font_cache: &FontCache) -> f32 {
+        font_cache.em_width(self.font_id, self.font_size)
+    }
+
+    pub fn descent(&self, font_cache: &FontCache) -> f32 {
+        font_cache.metric(self.font_id, |m| m.descent) * self.em_scale(font_cache)
+    }
+
+    fn em_scale(&self, font_cache: &FontCache) -> f32 {
+        font_cache.em_scale(self.font_id, self.font_size)
+    }
+}
+
+impl From<TextStyle> for HighlightStyle {
+    fn from(other: TextStyle) -> Self {
+        Self {
+            color: other.color,
+            font_properties: other.font_properties,
+            underline: other.underline,
+        }
+    }
 }
 
 impl HighlightStyle {

gpui/src/presenter.rs πŸ”—

@@ -286,6 +286,14 @@ impl<'a> PaintContext<'a> {
     }
 }
 
+impl<'a> Deref for PaintContext<'a> {
+    type Target = AppContext;
+
+    fn deref(&self) -> &Self::Target {
+        self.app
+    }
+}
+
 pub struct EventContext<'a> {
     rendered_views: &'a mut HashMap<usize, ElementBox>,
     dispatched_actions: Vec<DispatchDirective>,

server/src/rpc.rs πŸ”—

@@ -965,7 +965,7 @@ mod tests {
     };
     use zed::{
         channel::{Channel, ChannelDetails, ChannelList},
-        editor::{Editor, Insert},
+        editor::{Editor, EditorStyle, Insert},
         fs::{FakeFs, Fs as _},
         language::LanguageRegistry,
         rpc::{self, Client, Credentials, EstablishConnectionError},
@@ -1048,7 +1048,14 @@ mod tests {
             .unwrap();
 
         // 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));
+        let editor_b = cx_b.add_view(window_b, |cx| {
+            Editor::for_buffer(
+                buffer_b,
+                settings,
+                |cx| EditorStyle::test(cx.font_cache()),
+                cx,
+            )
+        });
         buffer_a
             .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1)
             .await;

zed/Cargo.toml πŸ”—

@@ -14,7 +14,7 @@ name = "Zed"
 path = "src/main.rs"
 
 [features]
-test-support = ["tempdir", "zrpc/test-support"]
+test-support = ["tempdir", "zrpc/test-support", "gpui/test-support"]
 
 [dependencies]
 anyhow = "1.0.38"
@@ -69,6 +69,7 @@ serde_json = { version = "1.0.64", features = ["preserve_order"] }
 tempdir = { version = "0.3.7" }
 unindent = "0.1.7"
 zrpc = { path = "../zrpc", features = ["test-support"] }
+gpui = { path = "../gpui", features = ["test-support"] }
 
 [package.metadata.bundle]
 icon = ["app-icon@2x.png", "app-icon.png"]

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

@@ -18,7 +18,7 @@ width = 16
 
 [workspace.tab]
 text = "$text.2"
-padding = { left = 10, right = 10 }
+padding = { left = 12, right = 12 }
 icon_width = 8
 spacing = 10
 icon_close = "$text.2.color"
@@ -107,8 +107,8 @@ shadow = { offset = [0, 2], blur = 16, color = "$shadow.0" }
 background = "$surface.1"
 corner_radius = 6
 padding = { left = 8, right = 8, top = 7, bottom = 7 }
-text = "$text.0.color"
-placeholder_text = "$text.2.color"
+text = "$text.0"
+placeholder_text = "$text.2"
 selection = "$selection.host"
 border = { width = 1, color = "$border.0" }
 
@@ -140,8 +140,8 @@ border = { width = 1, color = "$border.0" }
 background = "$surface.1"
 corner_radius = 6
 padding = { left = 16, right = 16, top = 7, bottom = 7 }
-text = "$text.0.color"
-placeholder_text = "$text.2.color"
+text = "$text.0"
+placeholder_text = "$text.2"
 selection = "$selection.host"
 border = { width = 1, color = "$border.0" }
 
@@ -161,7 +161,7 @@ background = "$state.hover"
 text = "$text.0"
 
 [editor]
-text = "$text.1.color"
+text = "$text.1"
 background = "$surface.1"
 gutter_background = "$surface.1"
 active_line_background = "$state.active_line"

zed/src/chat_panel.rs πŸ”—

@@ -54,10 +54,15 @@ impl ChatPanel {
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let input_editor = cx.add_view(|cx| {
-            Editor::auto_height(4, settings.clone(), cx).with_style({
-                let settings = settings.clone();
-                move |_| settings.borrow().theme.chat_panel.input_editor.as_editor()
-            })
+            Editor::auto_height(
+                4,
+                settings.clone(),
+                {
+                    let settings = settings.clone();
+                    move |_| settings.borrow().theme.chat_panel.input_editor.as_editor()
+                },
+                cx,
+            )
         });
         let channel_select = cx.add_view(|cx| {
             let channel_list = channel_list.clone();

zed/src/editor.rs πŸ”—

@@ -4,8 +4,9 @@ mod element;
 pub mod movement;
 
 use crate::{
-    settings::{HighlightId, Settings},
-    theme::{EditorStyle, Theme},
+    language::Language,
+    settings::Settings,
+    theme::Theme,
     time::ReplicaId,
     util::{post_inc, Bias},
     workspace,
@@ -17,15 +18,9 @@ pub use display_map::DisplayPoint;
 use display_map::*;
 pub use element::*;
 use gpui::{
-    action,
-    color::Color,
-    font_cache::FamilyId,
-    fonts::Properties as FontProperties,
-    geometry::vector::Vector2F,
-    keymap::Binding,
-    text_layout::{self, RunStyle},
-    AppContext, ClipboardItem, Element, ElementBox, Entity, FontCache, ModelHandle,
-    MutableAppContext, RenderContext, Task, TextLayoutCache, View, ViewContext, WeakViewHandle,
+    action, color::Color, fonts::TextStyle, geometry::vector::Vector2F, keymap::Binding,
+    text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle,
+    MutableAppContext, RenderContext, Task, View, ViewContext, WeakViewHandle,
 };
 use postage::watch;
 use serde::{Deserialize, Serialize};
@@ -34,8 +29,6 @@ use smol::Timer;
 use std::{
     cell::RefCell,
     cmp::{self, Ordering},
-    collections::BTreeMap,
-    fmt::Write,
     iter::FromIterator,
     mem,
     ops::{Range, RangeInclusive},
@@ -278,6 +271,26 @@ pub enum EditorMode {
     Full,
 }
 
+#[derive(Clone, Deserialize)]
+pub struct EditorStyle {
+    pub text: TextStyle,
+    #[serde(default)]
+    pub placeholder_text: Option<TextStyle>,
+    pub background: Color,
+    pub selection: SelectionStyle,
+    pub gutter_background: Color,
+    pub active_line_background: Color,
+    pub line_number: Color,
+    pub line_number_active: Color,
+    pub guest_selections: Vec<SelectionStyle>,
+}
+
+#[derive(Clone, Copy, Default, Deserialize)]
+pub struct SelectionStyle {
+    pub cursor: Color,
+    pub selection: Color,
+}
+
 pub struct Editor {
     handle: WeakViewHandle<Self>,
     buffer: ModelHandle<Buffer>,
@@ -290,10 +303,10 @@ pub struct Editor {
     scroll_position: Vector2F,
     scroll_top_anchor: Anchor,
     autoscroll_requested: bool,
-    build_style: Option<Rc<RefCell<dyn FnMut(&mut MutableAppContext) -> EditorStyle>>>,
+    build_style: Rc<RefCell<dyn FnMut(&mut MutableAppContext) -> EditorStyle>>,
     settings: watch::Receiver<Settings>,
     focused: bool,
-    cursors_visible: bool,
+    show_local_cursors: bool,
     blink_epoch: usize,
     blinking_paused: bool,
     mode: EditorMode,
@@ -305,8 +318,6 @@ pub struct Snapshot {
     pub display_snapshot: DisplayMapSnapshot,
     pub placeholder_text: Option<Arc<str>>,
     pub theme: Arc<Theme>,
-    pub font_family: FamilyId,
-    pub font_size: f32,
     is_focused: bool,
     scroll_position: Vector2F,
     scroll_top_anchor: Anchor,
@@ -324,9 +335,13 @@ struct ClipboardSelection {
 }
 
 impl Editor {
-    pub fn single_line(settings: watch::Receiver<Settings>, cx: &mut ViewContext<Self>) -> Self {
+    pub fn single_line(
+        settings: watch::Receiver<Settings>,
+        build_style: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle,
+        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, cx);
+        let mut view = Self::for_buffer(buffer, settings, build_style, cx);
         view.mode = EditorMode::SingleLine;
         view
     }
@@ -334,10 +349,11 @@ impl Editor {
     pub fn auto_height(
         max_lines: usize,
         settings: watch::Receiver<Settings>,
+        build_style: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle,
         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, cx);
+        let mut view = Self::for_buffer(buffer, settings, build_style, cx);
         view.mode = EditorMode::AutoHeight { max_lines };
         view
     }
@@ -345,10 +361,29 @@ impl Editor {
     pub fn for_buffer(
         buffer: ModelHandle<Buffer>,
         settings: watch::Receiver<Settings>,
+        build_style: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        let display_map =
-            cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.clone(), None, cx));
+        Self::new(buffer, settings, Rc::new(RefCell::new(build_style)), cx)
+    }
+
+    fn new(
+        buffer: ModelHandle<Buffer>,
+        settings: watch::Receiver<Settings>,
+        build_style: Rc<RefCell<dyn FnMut(&mut MutableAppContext) -> EditorStyle>>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let style = build_style.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,
+                None,
+                cx,
+            )
+        });
         cx.observe(&buffer, Self::on_buffer_changed).detach();
         cx.subscribe(&buffer, Self::on_buffer_event).detach();
         cx.observe(&display_map, Self::on_display_map_changed)
@@ -376,13 +411,13 @@ impl Editor {
             next_selection_id,
             add_selections_state: None,
             select_larger_syntax_node_stack: Vec::new(),
-            build_style: None,
+            build_style,
             scroll_position: Vector2F::zero(),
             scroll_top_anchor: Anchor::min(),
             autoscroll_requested: false,
             settings,
             focused: false,
-            cursors_visible: false,
+            show_local_cursors: false,
             blink_epoch: 0,
             blinking_paused: false,
             mode: EditorMode::Full,
@@ -390,14 +425,6 @@ impl Editor {
         }
     }
 
-    pub fn with_style(
-        mut self,
-        f: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle,
-    ) -> Self {
-        self.build_style = Some(Rc::new(RefCell::new(f)));
-        self
-    }
-
     pub fn replica_id(&self, cx: &AppContext) -> ReplicaId {
         self.buffer.read(cx).replica_id()
     }
@@ -416,8 +443,6 @@ impl Editor {
             scroll_top_anchor: self.scroll_top_anchor.clone(),
             theme: settings.theme.clone(),
             placeholder_text: self.placeholder_text.clone(),
-            font_family: settings.buffer_font_family,
-            font_size: settings.buffer_font_size,
             is_focused: self
                 .handle
                 .upgrade(cx)
@@ -425,6 +450,10 @@ impl Editor {
         }
     }
 
+    pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc<Language>> {
+        self.buffer.read(cx).language()
+    }
+
     pub fn set_placeholder_text(
         &mut self,
         placeholder_text: impl Into<Arc<str>>,
@@ -2229,7 +2258,7 @@ impl Editor {
     }
 
     fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
-        self.cursors_visible = true;
+        self.show_local_cursors = true;
         cx.notify();
 
         let epoch = self.next_blink_epoch();
@@ -2254,7 +2283,7 @@ impl Editor {
 
     fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
         if epoch == self.blink_epoch && self.focused && !self.blinking_paused {
-            self.cursors_visible = !self.cursors_visible;
+            self.show_local_cursors = !self.show_local_cursors;
             cx.notify();
 
             let epoch = self.next_blink_epoch();
@@ -2271,8 +2300,8 @@ impl Editor {
         }
     }
 
-    pub fn cursors_visible(&self) -> bool {
-        self.cursors_visible
+    pub fn show_local_cursors(&self) -> bool {
+        self.show_local_cursors
     }
 
     fn on_buffer_changed(&mut self, _: ModelHandle<Buffer>, cx: &mut ViewContext<Self>) {
@@ -2301,263 +2330,60 @@ impl Editor {
 }
 
 impl Snapshot {
-    pub fn scroll_position(&self) -> Vector2F {
-        compute_scroll_position(
-            &self.display_snapshot,
-            self.scroll_position,
-            &self.scroll_top_anchor,
-        )
+    pub fn is_empty(&self) -> bool {
+        self.display_snapshot.is_empty()
     }
 
-    pub fn max_point(&self) -> DisplayPoint {
-        self.display_snapshot.max_point()
+    pub fn is_focused(&self) -> bool {
+        self.is_focused
     }
 
-    pub fn longest_row(&self) -> u32 {
-        self.display_snapshot.longest_row()
+    pub fn placeholder_text(&self) -> Option<&Arc<str>> {
+        self.placeholder_text.as_ref()
     }
 
-    pub fn line_len(&self, display_row: u32) -> u32 {
-        self.display_snapshot.line_len(display_row)
+    pub fn buffer_row_count(&self) -> u32 {
+        self.display_snapshot.buffer_row_count()
     }
 
-    pub fn font_ascent(&self, font_cache: &FontCache) -> f32 {
-        let font_id = font_cache.default_font(self.font_family);
-        let ascent = font_cache.metric(font_id, |m| m.ascent);
-        font_cache.scale_metric(ascent, font_id, self.font_size)
+    pub fn buffer_rows(&self, start_row: u32) -> BufferRows {
+        self.display_snapshot.buffer_rows(start_row)
     }
 
-    pub fn font_descent(&self, font_cache: &FontCache) -> f32 {
-        let font_id = font_cache.default_font(self.font_family);
-        let descent = font_cache.metric(font_id, |m| m.descent);
-        font_cache.scale_metric(descent, font_id, self.font_size)
+    pub fn highlighted_chunks_for_rows(
+        &mut self,
+        display_rows: Range<u32>,
+    ) -> display_map::HighlightedChunks {
+        self.display_snapshot
+            .highlighted_chunks_for_rows(display_rows)
     }
 
-    pub fn line_height(&self, font_cache: &FontCache) -> f32 {
-        let font_id = font_cache.default_font(self.font_family);
-        font_cache.line_height(font_id, self.font_size).ceil()
+    pub fn theme(&self) -> &Arc<Theme> {
+        &self.theme
     }
 
-    pub fn em_width(&self, font_cache: &FontCache) -> f32 {
-        let font_id = font_cache.default_font(self.font_family);
-        font_cache.em_width(font_id, self.font_size)
+    pub fn scroll_position(&self) -> Vector2F {
+        compute_scroll_position(
+            &self.display_snapshot,
+            self.scroll_position,
+            &self.scroll_top_anchor,
+        )
     }
 
-    // TODO: Can we make this not return a result?
-    pub fn max_line_number_width(
-        &self,
-        font_cache: &FontCache,
-        layout_cache: &TextLayoutCache,
-    ) -> Result<f32> {
-        let font_size = self.font_size;
-        let font_id = font_cache.select_font(self.font_family, &FontProperties::new())?;
-        let digit_count = (self.display_snapshot.buffer_row_count() as f32)
-            .log10()
-            .floor() as usize
-            + 1;
-
-        Ok(layout_cache
-            .layout_str(
-                "1".repeat(digit_count).as_str(),
-                font_size,
-                &[(
-                    digit_count,
-                    RunStyle {
-                        font_id,
-                        color: Color::black(),
-                        underline: false,
-                    },
-                )],
-            )
-            .width())
+    pub fn max_point(&self) -> DisplayPoint {
+        self.display_snapshot.max_point()
     }
 
-    pub fn layout_line_numbers(
-        &self,
-        rows: Range<u32>,
-        active_rows: &BTreeMap<u32, bool>,
-        font_cache: &FontCache,
-        layout_cache: &TextLayoutCache,
-        theme: &Theme,
-    ) -> Result<Vec<Option<text_layout::Line>>> {
-        let font_id = font_cache.select_font(self.font_family, &FontProperties::new())?;
-
-        let mut layouts = Vec::with_capacity(rows.len());
-        let mut line_number = String::new();
-        for (ix, (buffer_row, soft_wrapped)) in self
-            .display_snapshot
-            .buffer_rows(rows.start)
-            .take((rows.end - rows.start) as usize)
-            .enumerate()
-        {
-            let display_row = rows.start + ix as u32;
-            let color = if active_rows.contains_key(&display_row) {
-                theme.editor.line_number_active
-            } else {
-                theme.editor.line_number
-            };
-            if soft_wrapped {
-                layouts.push(None);
-            } else {
-                line_number.clear();
-                write!(&mut line_number, "{}", buffer_row + 1).unwrap();
-                layouts.push(Some(layout_cache.layout_str(
-                    &line_number,
-                    self.font_size,
-                    &[(
-                        line_number.len(),
-                        RunStyle {
-                            font_id,
-                            color,
-                            underline: false,
-                        },
-                    )],
-                )));
-            }
-        }
-
-        Ok(layouts)
+    pub fn longest_row(&self) -> u32 {
+        self.display_snapshot.longest_row()
     }
 
-    pub fn layout_lines(
-        &mut self,
-        mut rows: Range<u32>,
-        style: &EditorStyle,
-        font_cache: &FontCache,
-        layout_cache: &TextLayoutCache,
-    ) -> Result<Vec<text_layout::Line>> {
-        rows.end = cmp::min(rows.end, self.display_snapshot.max_point().row() + 1);
-        if rows.start >= rows.end {
-            return Ok(Vec::new());
-        }
-
-        // When the editor is empty and unfocused, then show the placeholder.
-        if self.display_snapshot.is_empty() && !self.is_focused {
-            let placeholder_lines = self
-                .placeholder_text
-                .as_ref()
-                .map_or("", AsRef::as_ref)
-                .split('\n')
-                .skip(rows.start as usize)
-                .take(rows.len());
-            let font_id = font_cache
-                .select_font(self.font_family, &style.placeholder_text.font_properties)?;
-            return Ok(placeholder_lines
-                .into_iter()
-                .map(|line| {
-                    layout_cache.layout_str(
-                        line,
-                        self.font_size,
-                        &[(
-                            line.len(),
-                            RunStyle {
-                                font_id,
-                                color: style.placeholder_text.color,
-                                underline: false,
-                            },
-                        )],
-                    )
-                })
-                .collect());
-        }
-
-        let mut prev_font_properties = FontProperties::new();
-        let mut prev_font_id = font_cache
-            .select_font(self.font_family, &prev_font_properties)
-            .unwrap();
-
-        let mut layouts = Vec::with_capacity(rows.len());
-        let mut line = String::new();
-        let mut styles = Vec::new();
-        let mut row = rows.start;
-        let mut line_exceeded_max_len = false;
-        let chunks = self
-            .display_snapshot
-            .highlighted_chunks_for_rows(rows.clone());
-
-        'outer: for (chunk, style_ix) in chunks.chain(Some(("\n", HighlightId::default()))) {
-            for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
-                if ix > 0 {
-                    layouts.push(layout_cache.layout_str(&line, self.font_size, &styles));
-                    line.clear();
-                    styles.clear();
-                    row += 1;
-                    line_exceeded_max_len = false;
-                    if row == rows.end {
-                        break 'outer;
-                    }
-                }
-
-                if !line_chunk.is_empty() && !line_exceeded_max_len {
-                    let style = self
-                        .theme
-                        .syntax
-                        .highlight_style(style_ix)
-                        .unwrap_or(style.text.clone());
-                    // Avoid a lookup if the font properties match the previous ones.
-                    let font_id = if style.font_properties == prev_font_properties {
-                        prev_font_id
-                    } else {
-                        font_cache.select_font(self.font_family, &style.font_properties)?
-                    };
-
-                    if line.len() + line_chunk.len() > MAX_LINE_LEN {
-                        let mut chunk_len = MAX_LINE_LEN - line.len();
-                        while !line_chunk.is_char_boundary(chunk_len) {
-                            chunk_len -= 1;
-                        }
-                        line_chunk = &line_chunk[..chunk_len];
-                        line_exceeded_max_len = true;
-                    }
-
-                    line.push_str(line_chunk);
-                    styles.push((
-                        line_chunk.len(),
-                        RunStyle {
-                            font_id,
-                            color: style.color,
-                            underline: style.underline,
-                        },
-                    ));
-                    prev_font_id = font_id;
-                    prev_font_properties = style.font_properties;
-                }
-            }
-        }
-
-        Ok(layouts)
+    pub fn line_len(&self, display_row: u32) -> u32 {
+        self.display_snapshot.line_len(display_row)
     }
 
-    pub fn layout_line(
-        &self,
-        row: u32,
-        font_cache: &FontCache,
-        layout_cache: &TextLayoutCache,
-    ) -> Result<text_layout::Line> {
-        let font_id = font_cache.select_font(self.font_family, &FontProperties::new())?;
-
-        let mut line = self.display_snapshot.line(row);
-
-        if line.len() > MAX_LINE_LEN {
-            let mut len = MAX_LINE_LEN;
-            while !line.is_char_boundary(len) {
-                len -= 1;
-            }
-            line.truncate(len);
-        }
-
-        Ok(layout_cache.layout_str(
-            &line,
-            self.font_size,
-            &[(
-                self.display_snapshot.line_len(row) as usize,
-                RunStyle {
-                    font_id,
-                    color: Color::black(),
-                    underline: false,
-                },
-            )],
-        ))
+    pub fn line(&self, display_row: u32) -> String {
+        self.display_snapshot.line(display_row)
     }
 
     pub fn prev_row_boundary(&self, point: DisplayPoint) -> (DisplayPoint, Point) {
@@ -2569,6 +2395,41 @@ impl Snapshot {
     }
 }
 
+impl EditorStyle {
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn test(font_cache: &gpui::FontCache) -> Self {
+        let font_family_name = Arc::from("Monaco");
+        let font_properties = Default::default();
+        let font_family_id = font_cache.load_family(&[&font_family_name]).unwrap();
+        let font_id = font_cache
+            .select_font(font_family_id, &font_properties)
+            .unwrap();
+        Self {
+            text: TextStyle {
+                font_family_name,
+                font_family_id,
+                font_id,
+                font_size: 14.,
+                color: Color::from_u32(0xff0000ff),
+                font_properties,
+                underline: false,
+            },
+            placeholder_text: None,
+            background: Default::default(),
+            gutter_background: Default::default(),
+            active_line_background: Default::default(),
+            line_number: Default::default(),
+            line_number_active: Default::default(),
+            selection: Default::default(),
+            guest_selections: Default::default(),
+        }
+    }
+
+    fn placeholder_text(&self) -> &TextStyle {
+        self.placeholder_text.as_ref().unwrap_or(&self.text)
+    }
+}
+
 fn compute_scroll_position(
     snapshot: &DisplayMapSnapshot,
     mut scroll_position: Vector2F,
@@ -2604,10 +2465,10 @@ impl Entity for Editor {
 
 impl View for Editor {
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let style = self
-            .build_style
-            .as_ref()
-            .map_or(Default::default(), |build| (build.borrow_mut())(cx));
+        let style = self.build_style.borrow_mut()(cx);
+        self.display_map.update(cx, |map, cx| {
+            map.set_font(style.text.font_id, style.text.font_size, cx)
+        });
         EditorElement::new(self.handle.clone(), style).boxed()
     }
 
@@ -2627,7 +2488,7 @@ impl View for Editor {
 
     fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
         self.focused = false;
-        self.cursors_visible = false;
+        self.show_local_cursors = false;
         self.buffer.update(cx, |buffer, cx| {
             buffer.set_active_selection_set(None, cx).unwrap();
         });
@@ -2659,8 +2520,34 @@ impl workspace::Item for Buffer {
         settings: watch::Receiver<Settings>,
         cx: &mut ViewContext<Self::View>,
     ) -> Self::View {
-        Editor::for_buffer(handle, settings.clone(), cx)
-            .with_style(move |_| settings.borrow().theme.editor.clone())
+        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,
+        )
     }
 }
 
@@ -2697,10 +2584,14 @@ impl workspace::ItemView for Editor {
     where
         Self: Sized,
     {
-        let mut clone = Editor::for_buffer(self.buffer.clone(), self.settings.clone(), cx);
+        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();
-        clone.build_style = self.build_style.clone();
         Some(clone)
     }
 
@@ -2742,9 +2633,8 @@ mod tests {
     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 (_, editor) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, editor) =
+            cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
         editor.update(cx, |view, cx| {
             view.begin_selection(DisplayPoint::new(2, 2), false, cx);
@@ -2810,9 +2700,7 @@ mod tests {
     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 (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
         view.update(cx, |view, cx| {
             view.begin_selection(DisplayPoint::new(2, 2), false, cx);
@@ -2844,9 +2732,7 @@ mod tests {
     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 (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
         view.update(cx, |view, cx| {
             view.begin_selection(DisplayPoint::new(3, 4), false, cx);
@@ -2882,33 +2768,6 @@ mod tests {
         });
     }
 
-    #[gpui::test]
-    fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) {
-        let layout_cache = TextLayoutCache::new(cx.platform().fonts());
-        let font_cache = cx.font_cache().clone();
-
-        let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6), cx));
-
-        let settings = settings::test(&cx).1;
-        let (_, editor) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer.clone(), settings.clone(), cx)
-        });
-
-        let layouts = editor.update(cx, |editor, cx| {
-            editor
-                .snapshot(cx)
-                .layout_line_numbers(
-                    0..6,
-                    &Default::default(),
-                    &font_cache,
-                    &layout_cache,
-                    &settings.borrow().theme,
-                )
-                .unwrap()
-        });
-        assert_eq!(layouts.len(), 6);
-    }
-
     #[gpui::test]
     fn test_fold(cx: &mut gpui::MutableAppContext) {
         let buffer = cx.add_model(|cx| {
@@ -2937,7 +2796,7 @@ mod tests {
         });
         let settings = settings::test(&cx).1;
         let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer.clone(), settings, cx)
+            build_editor(buffer.clone(), settings, cx)
         });
 
         view.update(cx, |view, cx| {
@@ -3005,7 +2864,7 @@ mod tests {
         let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6), cx));
         let settings = settings::test(&cx).1;
         let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer.clone(), settings, cx)
+            build_editor(buffer.clone(), settings, cx)
         });
 
         buffer.update(cx, |buffer, cx| {
@@ -3082,7 +2941,7 @@ mod tests {
         let buffer = cx.add_model(|cx| Buffer::new(0, "ⓐⓑⓒⓓⓔ\nabcde\nαβγδΡ\n", cx));
         let settings = settings::test(&cx).1;
         let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer.clone(), settings, cx)
+            build_editor(buffer.clone(), settings, cx)
         });
 
         assert_eq!('ⓐ'.len_utf8(), 3);
@@ -3140,7 +2999,7 @@ mod tests {
         let buffer = cx.add_model(|cx| Buffer::new(0, "ⓐⓑⓒⓓⓔ\nabcd\nΞ±Ξ²Ξ³\nabcd\nⓐⓑⓒⓓⓔ\n", cx));
         let settings = settings::test(&cx).1;
         let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer.clone(), settings, cx)
+            build_editor(buffer.clone(), settings, cx)
         });
         view.update(cx, |view, cx| {
             view.select_display_ranges(&[empty_range(0, "ⓐⓑⓒⓓⓔ".len())], cx)
@@ -3170,9 +3029,7 @@ mod tests {
     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 (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.select_display_ranges(
                 &[
@@ -3315,9 +3172,7 @@ mod tests {
         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 (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.select_display_ranges(
                 &[
@@ -3509,9 +3364,7 @@ mod tests {
         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 (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, 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);
@@ -3572,7 +3425,7 @@ mod tests {
         });
         let settings = settings::test(&cx).1;
         let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer.clone(), settings, cx)
+            build_editor(buffer.clone(), settings, cx)
         });
 
         view.update(cx, |view, cx| {
@@ -3608,7 +3461,7 @@ mod tests {
         });
         let settings = settings::test(&cx).1;
         let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer.clone(), settings, cx)
+            build_editor(buffer.clone(), settings, cx)
         });
 
         view.update(cx, |view, cx| {
@@ -3637,9 +3490,7 @@ mod tests {
     fn test_delete_line(cx: &mut gpui::MutableAppContext) {
         let settings = settings::test(&cx).1;
         let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx));
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.select_display_ranges(
                 &[
@@ -3663,9 +3514,7 @@ mod tests {
 
         let settings = settings::test(&cx).1;
         let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx));
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.select_display_ranges(&[DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)], cx)
                 .unwrap();
@@ -3682,9 +3531,7 @@ mod tests {
     fn test_duplicate_line(cx: &mut gpui::MutableAppContext) {
         let settings = settings::test(&cx).1;
         let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx));
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.select_display_ranges(
                 &[
@@ -3711,9 +3558,7 @@ mod tests {
 
         let settings = settings::test(&cx).1;
         let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx));
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.select_display_ranges(
                 &[
@@ -3739,9 +3584,7 @@ mod tests {
     fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
         let settings = settings::test(&cx).1;
         let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(10, 5), cx));
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.fold_ranges(
                 vec![
@@ -3840,7 +3683,7 @@ mod tests {
         let settings = settings::test(&cx).1;
         let view = cx
             .add_window(Default::default(), |cx| {
-                Editor::for_buffer(buffer.clone(), settings, cx)
+                build_editor(buffer.clone(), settings, cx)
             })
             .1;
 
@@ -3973,9 +3816,7 @@ mod tests {
     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 (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.select_all(&SelectAll, cx);
             assert_eq!(
@@ -3989,9 +3830,7 @@ mod tests {
     fn test_select_line(cx: &mut gpui::MutableAppContext) {
         let settings = settings::test(&cx).1;
         let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 5), cx));
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.select_display_ranges(
                 &[
@@ -4037,9 +3876,7 @@ mod tests {
     fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) {
         let settings = settings::test(&cx).1;
         let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(9, 5), cx));
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.fold_ranges(
                 vec![
@@ -4107,9 +3944,7 @@ mod tests {
     fn test_add_selection_above_below(cx: &mut gpui::MutableAppContext) {
         let settings = settings::test(&cx).1;
         let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefghi\n\njk\nlmno\n", cx));
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
         view.update(cx, |view, cx| {
             view.select_display_ranges(&[DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)], cx)
@@ -4295,7 +4130,7 @@ mod tests {
             let history = History::new(text.into());
             Buffer::from_history(0, history, None, lang.cloned(), cx)
         });
-        let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer, settings.clone(), cx));
+        let (_, view) = cx.add_window(|cx| build_editor(buffer, settings.clone(), cx));
         view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing())
             .await;
 
@@ -4433,6 +4268,40 @@ mod tests {
         let point = DisplayPoint::new(row as u32, column as u32);
         point..point
     }
+
+    fn build_editor(
+        buffer: ModelHandle<Buffer>,
+        settings: watch::Receiver<Settings>,
+        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)
+    }
 }
 
 trait RangeExt<T> {

zed/src/editor/buffer.rs πŸ”—

@@ -714,9 +714,11 @@ impl Buffer {
         path: impl Into<Arc<Path>>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
+        let path = path.into();
         let handle = cx.handle();
         let text = self.visible_text.clone();
         let version = self.version.clone();
+
         let save_as = worktree.update(cx, |worktree, cx| {
             worktree
                 .as_local_mut()
@@ -727,6 +729,11 @@ impl Buffer {
         cx.spawn(|this, mut cx| async move {
             save_as.await.map(|new_file| {
                 this.update(&mut cx, |this, cx| {
+                    if let Some(language) = new_file.select_language(cx) {
+                        this.language = Some(language);
+                        this.reparse(cx);
+                    }
+
                     let mtime = new_file.mtime;
                     this.file = Some(new_file);
                     this.did_save(version, mtime, cx);
@@ -794,6 +801,10 @@ impl Buffer {
         cx.emit(Event::FileHandleChanged);
     }
 
+    pub fn language(&self) -> Option<&Arc<Language>> {
+        self.language.as_ref()
+    }
+
     pub fn parse_count(&self) -> usize {
         self.parse_count
     }
@@ -871,7 +882,11 @@ impl Buffer {
                     cx.spawn(move |this, mut cx| async move {
                         let new_tree = parse_task.await;
                         this.update(&mut cx, move |this, cx| {
-                            let parse_again = this.version > parsed_version;
+                            let language_changed =
+                                this.language.as_ref().map_or(true, |curr_language| {
+                                    !Arc::ptr_eq(curr_language, &language)
+                                });
+                            let parse_again = this.version > parsed_version || language_changed;
                             *this.syntax_tree.lock() = Some(SyntaxTree {
                                 tree: new_tree,
                                 dirty: false,

zed/src/editor/display_map.rs πŸ”—

@@ -2,14 +2,13 @@ mod fold_map;
 mod tab_map;
 mod wrap_map;
 
-use super::{buffer, Anchor, Bias, Buffer, Point, Settings, ToOffset, ToPoint};
+use super::{buffer, Anchor, Bias, Buffer, Point, ToOffset, ToPoint};
 use fold_map::FoldMap;
-use gpui::{Entity, ModelContext, ModelHandle};
-use postage::watch;
+use gpui::{fonts::FontId, Entity, ModelContext, ModelHandle};
 use std::ops::Range;
 use tab_map::TabMap;
-pub use wrap_map::BufferRows;
 use wrap_map::WrapMap;
+pub use wrap_map::{BufferRows, HighlightedChunks};
 
 pub struct DisplayMap {
     buffer: ModelHandle<Buffer>,
@@ -25,13 +24,16 @@ impl Entity for DisplayMap {
 impl DisplayMap {
     pub fn new(
         buffer: ModelHandle<Buffer>,
-        settings: watch::Receiver<Settings>,
+        tab_size: usize,
+        font_id: FontId,
+        font_size: f32,
         wrap_width: Option<f32>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
         let (fold_map, snapshot) = FoldMap::new(buffer.clone(), cx);
-        let (tab_map, snapshot) = TabMap::new(snapshot, settings.borrow().tab_size);
-        let wrap_map = cx.add_model(|cx| WrapMap::new(snapshot, settings, wrap_width, cx));
+        let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
+        let wrap_map =
+            cx.add_model(|cx| WrapMap::new(snapshot, font_id, font_size, wrap_width, cx));
         cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
         DisplayMap {
             buffer,
@@ -85,6 +87,11 @@ impl DisplayMap {
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
     }
 
+    pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) {
+        self.wrap_map
+            .update(cx, |map, cx| map.set_font(font_id, font_size, cx));
+    }
+
     pub fn set_wrap_width(&self, width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
         self.wrap_map
             .update(cx, |map, cx| map.set_wrap_width(width, cx))
@@ -367,12 +374,12 @@ mod tests {
             .unwrap_or(10);
 
         let font_cache = cx.font_cache().clone();
-        let settings = Settings {
-            tab_size: rng.gen_range(1..=4),
-            buffer_font_family: font_cache.load_family(&["Helvetica"]).unwrap(),
-            buffer_font_size: 14.0,
-            ..cx.read(Settings::test)
-        };
+        let tab_size = rng.gen_range(1..=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 = 14.0;
         let max_wrap_width = 300.0;
         let mut wrap_width = if rng.gen_bool(0.1) {
             None
@@ -380,7 +387,7 @@ mod tests {
             Some(rng.gen_range(0.0..=max_wrap_width))
         };
 
-        log::info!("tab size: {}", settings.tab_size);
+        log::info!("tab size: {}", tab_size);
         log::info!("wrap width: {:?}", wrap_width);
 
         let buffer = cx.add_model(|cx| {
@@ -388,9 +395,10 @@ mod tests {
             let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
             Buffer::new(0, text, cx)
         });
-        let settings = watch::channel_with(settings).1;
 
-        let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings, wrap_width, cx));
+        let map = cx.add_model(|cx| {
+            DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, wrap_width, cx)
+        });
         let (_observer, notifications) = Observer::new(&map, &mut cx);
         let mut fold_count = 0;
 
@@ -529,26 +537,27 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_soft_wraps(mut cx: gpui::TestAppContext) {
+    fn test_soft_wraps(cx: &mut MutableAppContext) {
         cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
         cx.foreground().forbid_parking();
 
         let font_cache = cx.font_cache();
 
-        let settings = Settings {
-            buffer_font_family: font_cache.load_family(&["Helvetica"]).unwrap(),
-            buffer_font_size: 12.0,
-            tab_size: 4,
-            ..cx.read(Settings::test)
-        };
+        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.);
 
         let text = "one two three four five\nsix seven eight";
         let buffer = cx.add_model(|cx| Buffer::new(0, text.to_string(), cx));
-        let (mut settings_tx, settings_rx) = watch::channel_with(settings);
-        let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings_rx, wrap_width, cx));
+        let map = cx.add_model(|cx| {
+            DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, wrap_width, cx)
+        });
 
-        let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));
+        let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
         assert_eq!(
             snapshot.chunks_at(0).collect::<String>(),
             "one two \nthree four \nfive\nsix seven \neight"
@@ -592,23 +601,21 @@ mod tests {
             (DisplayPoint::new(2, 4), SelectionGoal::Column(10))
         );
 
-        buffer.update(&mut cx, |buffer, cx| {
+        buffer.update(cx, |buffer, cx| {
             let ix = buffer.text().find("seven").unwrap();
             buffer.edit(vec![ix..ix], "and ", cx);
         });
 
-        let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));
+        let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
         assert_eq!(
             snapshot.chunks_at(1).collect::<String>(),
             "three four \nfive\nsix and \nseven eight"
         );
 
         // Re-wrap on font size changes
-        settings_tx.borrow_mut().buffer_font_size += 3.;
-
-        map.next_notification(&mut cx).await;
+        map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx));
 
-        let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));
+        let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
         assert_eq!(
             snapshot.chunks_at(1).collect::<String>(),
             "three \nfour five\nsix and \nseven \neight"
@@ -619,11 +626,16 @@ mod tests {
     fn test_chunks_at(cx: &mut gpui::MutableAppContext) {
         let text = sample_text(6, 6);
         let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
-        let settings = watch::channel_with(Settings {
-            tab_size: 4,
-            ..Settings::test(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, cx)
         });
-        let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.1, None, cx));
         buffer.update(cx, |buffer, cx| {
             buffer.edit(
                 vec![
@@ -695,13 +707,16 @@ mod tests {
         });
         buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
 
-        let settings = cx.update(|cx| {
-            watch::channel_with(Settings {
-                tab_size: 2,
-                ..Settings::test(cx)
-            })
-        });
-        let map = cx.add_model(|cx| DisplayMap::new(buffer, settings.1, None, 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
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
+
+        let map =
+            cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
         assert_eq!(
             cx.update(|cx| highlighted_chunks(0..5, &map, &theme, cx)),
             vec![
@@ -782,15 +797,16 @@ mod tests {
         buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
 
         let font_cache = cx.font_cache();
-        let settings = cx.update(|cx| {
-            watch::channel_with(Settings {
-                tab_size: 4,
-                buffer_font_family: font_cache.load_family(&["Courier"]).unwrap(),
-                buffer_font_size: 16.0,
-                ..Settings::test(cx)
-            })
-        });
-        let map = cx.add_model(|cx| DisplayMap::new(buffer, settings.1, Some(40.0), cx));
+
+        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), cx));
         assert_eq!(
             cx.update(|cx| highlighted_chunks(0..5, &map, &theme, cx)),
             [
@@ -825,11 +841,17 @@ mod tests {
         let text = "\n'a', 'Ξ±',\t'βœ‹',\t'❎', '🍐'\n";
         let display_text = "\n'a', 'Ξ±',   'βœ‹',    '❎', '🍐'\n";
         let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
-        let settings = watch::channel_with(Settings {
-            tab_size: 4,
-            ..Settings::test(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, cx)
         });
-        let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.1, None, cx));
         let map = map.update(cx, |map, cx| map.snapshot(cx));
 
         assert_eq!(map.text(), display_text);
@@ -863,11 +885,17 @@ mod tests {
     fn test_tabs_with_multibyte_chars(cx: &mut gpui::MutableAppContext) {
         let text = "βœ…\t\tΞ±\nΞ²\t\nπŸ€Ξ²\t\tΞ³";
         let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
-        let settings = watch::channel_with(Settings {
-            tab_size: 4,
-            ..Settings::test(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, cx)
         });
-        let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.1, None, cx));
         let map = map.update(cx, |map, cx| map.snapshot(cx));
         assert_eq!(map.text(), "βœ…       Ξ±\nΞ²   \nπŸ€Ξ²      Ξ³");
         assert_eq!(
@@ -924,11 +952,16 @@ mod tests {
     #[gpui::test]
     fn test_max_point(cx: &mut gpui::MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, "aaa\n\t\tbbb", cx));
-        let settings = watch::channel_with(Settings {
-            tab_size: 4,
-            ..Settings::test(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, cx)
         });
-        let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.1, None, cx));
         assert_eq!(
             map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
             DisplayPoint::new(1, 11)

zed/src/editor/display_map/wrap_map.rs πŸ”—

@@ -2,14 +2,14 @@ use super::{
     fold_map,
     tab_map::{self, Edit as TabEdit, Snapshot as TabSnapshot, TabPoint, TextSummary},
 };
-use crate::{editor::Point, settings::HighlightId, util::Bias, Settings};
+use crate::{editor::Point, settings::HighlightId, util::Bias};
 use gpui::{
+    fonts::FontId,
     sum_tree::{self, Cursor, SumTree},
     text_layout::LineWrapper,
     Entity, ModelContext, Task,
 };
 use lazy_static::lazy_static;
-use postage::{prelude::Stream, watch};
 use smol::future::yield_now;
 use std::{collections::VecDeque, ops::Range, time::Duration};
 
@@ -18,8 +18,7 @@ pub struct WrapMap {
     pending_edits: VecDeque<(TabSnapshot, Vec<TabEdit>)>,
     wrap_width: Option<f32>,
     background_task: Option<Task<()>>,
-    _watch_settings: Task<()>,
-    settings: watch::Receiver<Settings>,
+    font: (FontId, f32),
 }
 
 impl Entity for WrapMap {
@@ -76,36 +75,17 @@ pub struct BufferRows<'a> {
 impl WrapMap {
     pub fn new(
         tab_snapshot: TabSnapshot,
-        settings: watch::Receiver<Settings>,
+        font_id: FontId,
+        font_size: f32,
         wrap_width: Option<f32>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
-        let _watch_settings = cx.spawn_weak({
-            let mut prev_font = (
-                settings.borrow().buffer_font_size,
-                settings.borrow().buffer_font_family,
-            );
-            let mut settings = settings.clone();
-            move |this, mut cx| async move {
-                while let Some(settings) = settings.recv().await {
-                    if let Some(this) = this.upgrade(&cx) {
-                        let font = (settings.buffer_font_size, settings.buffer_font_family);
-                        if font != prev_font {
-                            prev_font = font;
-                            this.update(&mut cx, |this, cx| this.rewrap(cx));
-                        }
-                    }
-                }
-            }
-        });
-
         let mut this = Self {
+            font: (font_id, font_size),
             wrap_width: None,
             pending_edits: Default::default(),
             snapshot: Snapshot::new(tab_snapshot),
-            settings,
             background_task: None,
-            _watch_settings,
         };
         this.set_wrap_width(wrap_width, cx);
 
@@ -128,6 +108,13 @@ impl WrapMap {
         self.snapshot.clone()
     }
 
+    pub fn set_font(&mut self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) {
+        if (font_id, font_size) != self.font {
+            self.font = (font_id, font_size);
+            self.rewrap(cx)
+        }
+    }
+
     pub fn set_wrap_width(&mut self, wrap_width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
         if wrap_width == self.wrap_width {
             return false;
@@ -144,15 +131,9 @@ impl WrapMap {
         if let Some(wrap_width) = self.wrap_width {
             let mut new_snapshot = self.snapshot.clone();
             let font_cache = cx.font_cache().clone();
-            let settings = self.settings.clone();
+            let (font_id, font_size) = self.font;
             let task = cx.background().spawn(async move {
-                let mut line_wrapper = {
-                    let settings = settings.borrow();
-                    let font_id = font_cache
-                        .select_font(settings.buffer_font_family, &Default::default())
-                        .unwrap();
-                    font_cache.line_wrapper(font_id, settings.buffer_font_size)
-                };
+                let mut line_wrapper = font_cache.line_wrapper(font_id, font_size);
                 let tab_snapshot = new_snapshot.tab_snapshot.clone();
                 let range = TabPoint::zero()..tab_snapshot.max_point();
                 new_snapshot
@@ -222,15 +203,9 @@ impl WrapMap {
                 let pending_edits = self.pending_edits.clone();
                 let mut snapshot = self.snapshot.clone();
                 let font_cache = cx.font_cache().clone();
-                let settings = self.settings.clone();
+                let (font_id, font_size) = self.font;
                 let update_task = cx.background().spawn(async move {
-                    let mut line_wrapper = {
-                        let settings = settings.borrow();
-                        let font_id = font_cache
-                            .select_font(settings.buffer_font_family, &Default::default())
-                            .unwrap();
-                        font_cache.line_wrapper(font_id, settings.buffer_font_size)
-                    };
+                    let mut line_wrapper = font_cache.line_wrapper(font_id, font_size);
 
                     for (tab_snapshot, edits) in pending_edits {
                         snapshot
@@ -950,13 +925,14 @@ mod tests {
         } else {
             Some(rng.gen_range(0.0..=1000.0))
         };
-        let settings = Settings {
-            tab_size: rng.gen_range(1..=4),
-            buffer_font_family: font_cache.load_family(&["Helvetica"]).unwrap(),
-            buffer_font_size: 14.0,
-            ..cx.read(Settings::test)
-        };
-        log::info!("Tab size: {}", settings.tab_size);
+        let tab_size = rng.gen_range(1..=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 = 14.0;
+
+        log::info!("Tab size: {}", tab_size);
         log::info!("Wrap width: {:?}", wrap_width);
 
         let buffer = cx.add_model(|cx| {
@@ -965,7 +941,7 @@ mod tests {
             Buffer::new(0, text, cx)
         });
         let (mut fold_map, folds_snapshot) = cx.read(|cx| FoldMap::new(buffer.clone(), cx));
-        let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), settings.tab_size);
+        let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
         log::info!(
             "Unwrapped text (no folds): {:?}",
             buffer.read_with(&cx, |buf, _| buf.text())
@@ -976,16 +952,13 @@ mod tests {
         );
         log::info!("Unwrapped text (expanded tabs): {:?}", tabs_snapshot.text());
 
-        let font_id = font_cache
-            .select_font(settings.buffer_font_family, &Default::default())
-            .unwrap();
-        let mut line_wrapper = LineWrapper::new(font_id, settings.buffer_font_size, font_system);
+        let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system);
         let unwrapped_text = tabs_snapshot.text();
         let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
 
-        let settings = watch::channel_with(settings).1;
-        let wrap_map = cx
-            .add_model(|cx| WrapMap::new(tabs_snapshot.clone(), settings.clone(), wrap_width, cx));
+        let wrap_map = cx.add_model(|cx| {
+            WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx)
+        });
         let (_observer, notifications) = Observer::new(&wrap_map, &mut cx);
 
         if wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) {

zed/src/editor/element.rs πŸ”—

@@ -1,5 +1,8 @@
-use super::{DisplayPoint, Editor, EditorMode, Insert, Scroll, Select, SelectPhase, Snapshot};
-use crate::{theme::EditorStyle, time::ReplicaId};
+use super::{
+    DisplayPoint, Editor, EditorMode, EditorStyle, Insert, Scroll, Select, SelectPhase, Snapshot,
+    MAX_LINE_LEN,
+};
+use crate::{theme::HighlightId, time::ReplicaId};
 use gpui::{
     color::Color,
     geometry::{
@@ -9,7 +12,7 @@ use gpui::{
     },
     json::{self, ToJson},
     keymap::Keystroke,
-    text_layout::{self, TextLayoutCache},
+    text_layout::{self, RunStyle, TextLayoutCache},
     AppContext, Axis, Border, Element, Event, EventContext, FontCache, LayoutContext,
     MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
 };
@@ -18,6 +21,7 @@ use smallvec::SmallVec;
 use std::{
     cmp::{self, Ordering},
     collections::{BTreeMap, HashMap},
+    fmt::Write,
     ops::Range,
 };
 
@@ -265,6 +269,7 @@ impl EditorElement {
         let view = self.view(cx.app);
         let settings = self.view(cx.app).settings.borrow();
         let theme = &settings.theme.editor;
+        let local_replica_id = view.replica_id(cx);
         let scroll_position = layout.snapshot.scroll_position();
         let start_row = scroll_position.y() as u32;
         let scroll_top = scroll_position.y() * layout.line_height;
@@ -334,7 +339,7 @@ impl EditorElement {
                     selection.paint(bounds, cx.scene);
                 }
 
-                if view.cursors_visible() {
+                if view.show_local_cursors() || *replica_id != local_replica_id {
                     let cursor_position = selection.end;
                     if (start_row..end_row).contains(&cursor_position.row()) {
                         let cursor_row_layout =
@@ -374,6 +379,176 @@ impl EditorElement {
 
         cx.scene.pop_layer();
     }
+
+    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;
+
+        cx.text_layout_cache
+            .layout_str(
+                "1".repeat(digit_count).as_str(),
+                self.style.text.font_size,
+                &[(
+                    digit_count,
+                    RunStyle {
+                        font_id: self.style.text.font_id,
+                        color: Color::black(),
+                        underline: false,
+                    },
+                )],
+            )
+            .width()
+    }
+
+    fn layout_line_numbers(
+        &self,
+        rows: Range<u32>,
+        active_rows: &BTreeMap<u32, bool>,
+        snapshot: &Snapshot,
+        cx: &LayoutContext,
+    ) -> Vec<Option<text_layout::Line>> {
+        let mut layouts = Vec::with_capacity(rows.len());
+        let mut line_number = String::new();
+        for (ix, (buffer_row, soft_wrapped)) in snapshot
+            .buffer_rows(rows.start)
+            .take((rows.end - rows.start) as usize)
+            .enumerate()
+        {
+            let display_row = rows.start + ix as u32;
+            let color = if active_rows.contains_key(&display_row) {
+                self.style.line_number_active
+            } else {
+                self.style.line_number
+            };
+            if soft_wrapped {
+                layouts.push(None);
+            } else {
+                line_number.clear();
+                write!(&mut line_number, "{}", buffer_row + 1).unwrap();
+                layouts.push(Some(cx.text_layout_cache.layout_str(
+                    &line_number,
+                    self.style.text.font_size,
+                    &[(
+                        line_number.len(),
+                        RunStyle {
+                            font_id: self.style.text.font_id,
+                            color,
+                            underline: false,
+                        },
+                    )],
+                )));
+            }
+        }
+
+        layouts
+    }
+
+    fn layout_lines(
+        &mut self,
+        mut rows: Range<u32>,
+        snapshot: &mut Snapshot,
+        cx: &LayoutContext,
+    ) -> Vec<text_layout::Line> {
+        rows.end = cmp::min(rows.end, snapshot.max_point().row() + 1);
+        if rows.start >= rows.end {
+            return Vec::new();
+        }
+
+        // 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_text = snapshot.placeholder_text();
+            let placeholder_lines = placeholder_text
+                .as_ref()
+                .map_or("", AsRef::as_ref)
+                .split('\n')
+                .skip(rows.start as usize)
+                .take(rows.len());
+            return placeholder_lines
+                .map(|line| {
+                    cx.text_layout_cache.layout_str(
+                        line,
+                        placeholder_style.font_size,
+                        &[(
+                            line.len(),
+                            RunStyle {
+                                font_id: placeholder_style.font_id,
+                                color: placeholder_style.color,
+                                underline: false,
+                            },
+                        )],
+                    )
+                })
+                .collect();
+        }
+
+        let mut prev_font_properties = self.style.text.font_properties.clone();
+        let mut prev_font_id = self.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();
+        let mut row = rows.start;
+        let mut line_exceeded_max_len = false;
+        let chunks = snapshot.highlighted_chunks_for_rows(rows.clone());
+
+        'outer: for (chunk, style_ix) in chunks.chain(Some(("\n", HighlightId::default()))) {
+            for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
+                if ix > 0 {
+                    layouts.push(cx.text_layout_cache.layout_str(
+                        &line,
+                        self.style.text.font_size,
+                        &styles,
+                    ));
+                    line.clear();
+                    styles.clear();
+                    row += 1;
+                    line_exceeded_max_len = false;
+                    if row == rows.end {
+                        break 'outer;
+                    }
+                }
+
+                if !line_chunk.is_empty() && !line_exceeded_max_len {
+                    let style = theme
+                        .syntax
+                        .highlight_style(style_ix)
+                        .unwrap_or(self.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 {
+                        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)
+                    };
+
+                    if line.len() + line_chunk.len() > MAX_LINE_LEN {
+                        let mut chunk_len = MAX_LINE_LEN - line.len();
+                        while !line_chunk.is_char_boundary(chunk_len) {
+                            chunk_len -= 1;
+                        }
+                        line_chunk = &line_chunk[..chunk_len];
+                        line_exceeded_max_len = true;
+                    }
+
+                    line.push_str(line_chunk);
+                    styles.push((
+                        line_chunk.len(),
+                        RunStyle {
+                            font_id,
+                            color: style.color,
+                            underline: style.underline,
+                        },
+                    ));
+                    prev_font_id = font_id;
+                    prev_font_properties = style.font_properties;
+                }
+            }
+        }
+
+        layouts
+    }
 }
 
 impl Element for EditorElement {
@@ -390,30 +565,22 @@ impl Element for EditorElement {
             unimplemented!("we don't yet handle an infinite width constraint on buffer elements");
         }
 
-        let font_cache = &cx.font_cache;
-        let layout_cache = &cx.text_layout_cache;
         let snapshot = self.snapshot(cx.app);
-        let line_height = snapshot.line_height(font_cache);
+        let line_height = self.style.text.line_height(cx.font_cache);
 
         let gutter_padding;
         let gutter_width;
         if snapshot.mode == EditorMode::Full {
-            gutter_padding = snapshot.em_width(cx.font_cache);
-            match snapshot.max_line_number_width(cx.font_cache, cx.text_layout_cache) {
-                Err(error) => {
-                    log::error!("error computing max line number width: {}", error);
-                    return (size, None);
-                }
-                Ok(width) => gutter_width = width + gutter_padding * 2.0,
-            }
+            gutter_padding = self.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;
             gutter_width = 0.0
         };
 
         let text_width = size.x() - gutter_width;
-        let text_offset = vec2f(-snapshot.font_descent(cx.font_cache), 0.);
-        let em_width = snapshot.em_width(font_cache);
+        let text_offset = vec2f(-self.style.text.descent(cx.font_cache), 0.);
+        let em_width = self.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| {
@@ -488,51 +655,18 @@ impl Element for EditorElement {
         });
 
         let line_number_layouts = if snapshot.mode == EditorMode::Full {
-            let settings = self
-                .view
-                .upgrade(cx.app)
-                .unwrap()
-                .read(cx.app)
-                .settings
-                .borrow();
-            match snapshot.layout_line_numbers(
-                start_row..end_row,
-                &active_rows,
-                cx.font_cache,
-                cx.text_layout_cache,
-                &settings.theme,
-            ) {
-                Err(error) => {
-                    log::error!("error laying out line numbers: {}", error);
-                    return (size, None);
-                }
-                Ok(layouts) => layouts,
-            }
+            self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx)
         } else {
             Vec::new()
         };
 
         let mut max_visible_line_width = 0.0;
-        let line_layouts = match snapshot.layout_lines(
-            start_row..end_row,
-            &self.style,
-            font_cache,
-            layout_cache,
-        ) {
-            Err(error) => {
-                log::error!("error laying out lines: {}", error);
-                return (size, None);
-            }
-            Ok(layouts) => {
-                for line in &layouts {
-                    if line.width() > max_visible_line_width {
-                        max_visible_line_width = line.width();
-                    }
-                }
-
-                layouts
+        let line_layouts = self.layout_lines(start_row..end_row, &mut snapshot, cx);
+        for line in &line_layouts {
+            if line.width() > max_visible_line_width {
+                max_visible_line_width = line.width();
             }
-        };
+        }
 
         let mut layout = LayoutState {
             size,
@@ -542,6 +676,7 @@ impl Element for EditorElement {
             overscroll,
             text_offset,
             snapshot,
+            style: self.style.clone(),
             active_rows,
             line_layouts,
             line_number_layouts,
@@ -551,15 +686,18 @@ impl Element for EditorElement {
             max_visible_line_width,
         };
 
+        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);
         self.update_view(cx.app, |view, cx| {
-            let clamped = view.clamp_scroll_left(layout.scroll_max(font_cache, layout_cache).x());
+            let clamped = view.clamp_scroll_left(scroll_max);
             let autoscrolled;
             if autoscroll_horizontally {
                 autoscrolled = view.autoscroll_horizontally(
                     start_row,
                     layout.text_size.x(),
-                    layout.scroll_width(font_cache, layout_cache),
-                    layout.snapshot.em_width(font_cache),
+                    scroll_width,
+                    max_glyph_width,
                     &layout.line_layouts,
                     cx,
                 );
@@ -659,6 +797,7 @@ pub struct LayoutState {
     gutter_size: Vector2F,
     gutter_padding: f32,
     text_size: Vector2F,
+    style: EditorStyle,
     snapshot: Snapshot,
     active_rows: BTreeMap<u32, bool>,
     line_layouts: Vec<text_layout::Line>,
@@ -672,20 +811,16 @@ pub struct LayoutState {
 }
 
 impl LayoutState {
-    fn scroll_width(&self, font_cache: &FontCache, layout_cache: &TextLayoutCache) -> f32 {
+    fn scroll_width(&self, layout_cache: &TextLayoutCache) -> f32 {
         let row = self.snapshot.longest_row();
-        let longest_line_width = self
-            .snapshot
-            .layout_line(row, font_cache, layout_cache)
-            .unwrap()
-            .width();
+        let longest_line_width = self.layout_line(row, &self.snapshot, layout_cache).width();
         longest_line_width.max(self.max_visible_line_width) + self.overscroll.x()
     }
 
     fn scroll_max(&self, font_cache: &FontCache, layout_cache: &TextLayoutCache) -> Vector2F {
         let text_width = self.text_size.x();
-        let scroll_width = self.scroll_width(font_cache, layout_cache);
-        let em_width = self.snapshot.em_width(font_cache);
+        let scroll_width = self.scroll_width(layout_cache);
+        let em_width = self.style.text.em_width(font_cache);
         let max_row = self.snapshot.max_point().row();
 
         vec2f(
@@ -693,6 +828,36 @@ impl LayoutState {
             max_row.saturating_sub(1) as f32,
         )
     }
+
+    pub fn layout_line(
+        &self,
+        row: u32,
+        snapshot: &Snapshot,
+        layout_cache: &TextLayoutCache,
+    ) -> text_layout::Line {
+        let mut line = snapshot.line(row);
+
+        if line.len() > MAX_LINE_LEN {
+            let mut len = MAX_LINE_LEN;
+            while !line.is_char_boundary(len) {
+                len -= 1;
+            }
+            line.truncate(len);
+        }
+
+        layout_cache.layout_str(
+            &line,
+            self.style.text.font_size,
+            &[(
+                snapshot.line_len(row) as usize,
+                RunStyle {
+                    font_id: self.style.text.font_id,
+                    color: Color::black(),
+                    underline: false,
+                },
+            )],
+        )
+    }
 }
 
 pub struct PaintState {
@@ -864,3 +1029,42 @@ fn scale_vertical_mouse_autoscroll_delta(delta: f32) -> f32 {
 fn scale_horizontal_mouse_autoscroll_delta(delta: f32) -> f32 {
     delta.powf(1.2) / 300.0
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        editor::{Buffer, Editor, EditorStyle},
+        settings,
+        test::sample_text,
+    };
+
+    #[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 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()
+                },
+                cx,
+            )
+        });
+        let element = EditorElement::new(editor.downgrade(), style);
+
+        let layouts = editor.update(cx, |editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let mut presenter = cx.build_presenter(window_id, 30.);
+            let mut layout_cx = presenter.build_layout_context(false, cx);
+            element.layout_line_numbers(0..6, &Default::default(), &snapshot, &mut layout_cx)
+        });
+        assert_eq!(layouts.len(), 6);
+    }
+}

zed/src/editor/movement.rs πŸ”—

@@ -180,16 +180,21 @@ fn char_kind(c: char) -> CharKind {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{
-        editor::{display_map::DisplayMap, Buffer},
-        test::test_app_state,
-    };
+    use crate::editor::{display_map::DisplayMap, Buffer};
 
     #[gpui::test]
     fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
-        let settings = test_app_state(cx).settings.clone();
+        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 buffer = cx.add_model(|cx| Buffer::new(0, "a bcΞ” defΞ³", cx));
-        let display_map = cx.add_model(|cx| DisplayMap::new(buffer, settings, None, cx));
+        let display_map =
+            cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
         let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
         assert_eq!(
             prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)).unwrap(),

zed/src/file_finder.rs πŸ”—

@@ -275,10 +275,14 @@ impl FileFinder {
         cx.observe(&workspace, Self::workspace_updated).detach();
 
         let query_editor = cx.add_view(|cx| {
-            Editor::single_line(settings.clone(), cx).with_style({
-                let settings = settings.clone();
-                move |_| settings.borrow().theme.selector.input_editor.as_editor()
-            })
+            Editor::single_line(
+                settings.clone(),
+                {
+                    let settings = settings.clone();
+                    move |_| settings.borrow().theme.selector.input_editor.as_editor()
+                },
+                cx,
+            )
         });
         cx.subscribe(&query_editor, Self::on_query_editor_event)
             .detach();

zed/src/language.rs πŸ”—

@@ -35,6 +35,10 @@ pub struct LanguageRegistry {
 }
 
 impl Language {
+    pub fn name(&self) -> &str {
+        self.config.name.as_str()
+    }
+
     pub fn highlight_map(&self) -> HighlightMap {
         self.highlight_map.lock().clone()
     }
@@ -133,27 +137,26 @@ mod tests {
 
         // matching file extension
         assert_eq!(
-            registry.select_language("zed/lib.rs").map(get_name),
+            registry.select_language("zed/lib.rs").map(|l| l.name()),
             Some("Rust")
         );
         assert_eq!(
-            registry.select_language("zed/lib.mk").map(get_name),
+            registry.select_language("zed/lib.mk").map(|l| l.name()),
             Some("Make")
         );
 
         // matching filename
         assert_eq!(
-            registry.select_language("zed/Makefile").map(get_name),
+            registry.select_language("zed/Makefile").map(|l| l.name()),
             Some("Make")
         );
 
         // matching suffix that is not the full file extension or filename
-        assert_eq!(registry.select_language("zed/cars").map(get_name), None);
-        assert_eq!(registry.select_language("zed/a.cars").map(get_name), None);
-        assert_eq!(registry.select_language("zed/sumk").map(get_name), None);
-
-        fn get_name(language: &Arc<Language>) -> &str {
-            language.config.name.as_str()
-        }
+        assert_eq!(registry.select_language("zed/cars").map(|l| l.name()), None);
+        assert_eq!(
+            registry.select_language("zed/a.cars").map(|l| l.name()),
+            None
+        );
+        assert_eq!(registry.select_language("zed/sumk").map(|l| l.name()), None);
     }
 }

zed/src/rpc.rs πŸ”—

@@ -417,11 +417,11 @@ impl Client {
                         if let Some(extract_entity_id) =
                             state.entity_id_extractors.get(&message.payload_type_id())
                         {
+                            let payload_type_id = message.payload_type_id();
                             let entity_id = (extract_entity_id)(message.as_ref());
-                            if let Some(handler) = state
-                                .model_handlers
-                                .get_mut(&(message.payload_type_id(), entity_id))
-                            {
+                            let handler_key = (payload_type_id, entity_id);
+                            if let Some(mut handler) = state.model_handlers.remove(&handler_key) {
+                                drop(state); // Avoid deadlocks if the handler interacts with rpc::Client
                                 let start_time = Instant::now();
                                 log::info!("RPC client message {}", message.payload_type_name());
                                 (handler)(message, &mut cx);
@@ -429,6 +429,10 @@ impl Client {
                                     "RPC message handled. duration:{:?}",
                                     start_time.elapsed()
                                 );
+                                this.state
+                                    .write()
+                                    .model_handlers
+                                    .insert(handler_key, handler);
                             } else {
                                 log::info!("unhandled message {}", message.payload_type_name());
                             }

zed/src/theme.rs πŸ”—

@@ -2,6 +2,7 @@ mod highlight_map;
 mod resolution;
 mod theme_registry;
 
+use crate::editor::{EditorStyle, SelectionStyle};
 use anyhow::Result;
 use gpui::{
     color::Color,
@@ -169,35 +170,16 @@ pub struct ContainedLabel {
     pub label: LabelStyle,
 }
 
-#[derive(Clone, Deserialize)]
-pub struct EditorStyle {
-    pub text: HighlightStyle,
-    #[serde(default)]
-    pub placeholder_text: HighlightStyle,
-    pub background: Color,
-    pub selection: SelectionStyle,
-    pub gutter_background: Color,
-    pub active_line_background: Color,
-    pub line_number: Color,
-    pub line_number_active: Color,
-    pub guest_selections: Vec<SelectionStyle>,
-}
-
 #[derive(Clone, Deserialize)]
 pub struct InputEditorStyle {
     #[serde(flatten)]
     pub container: ContainerStyle,
-    pub text: HighlightStyle,
-    pub placeholder_text: HighlightStyle,
+    pub text: TextStyle,
+    #[serde(default)]
+    pub placeholder_text: Option<TextStyle>,
     pub selection: SelectionStyle,
 }
 
-#[derive(Clone, Copy, Default, Deserialize)]
-pub struct SelectionStyle {
-    pub cursor: Color,
-    pub selection: Color,
-}
-
 impl SyntaxTheme {
     pub fn new(highlights: Vec<(String, HighlightStyle)>) -> Self {
         Self { highlights }
@@ -215,30 +197,6 @@ impl SyntaxTheme {
     }
 }
 
-impl Default for EditorStyle {
-    fn default() -> Self {
-        Self {
-            text: HighlightStyle {
-                color: Color::from_u32(0xff0000ff),
-                font_properties: Default::default(),
-                underline: false,
-            },
-            placeholder_text: HighlightStyle {
-                color: Color::from_u32(0x00ff00ff),
-                font_properties: Default::default(),
-                underline: false,
-            },
-            background: Default::default(),
-            gutter_background: Default::default(),
-            active_line_background: Default::default(),
-            line_number: Default::default(),
-            line_number_active: Default::default(),
-            selection: Default::default(),
-            guest_selections: Default::default(),
-        }
-    }
-}
-
 impl InputEditorStyle {
     pub fn as_editor(&self) -> EditorStyle {
         EditorStyle {
@@ -249,7 +207,11 @@ impl InputEditorStyle {
                 .background_color
                 .unwrap_or(Color::transparent_black()),
             selection: self.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(),
         }
     }
 }

zed/src/theme_selector.rs πŸ”—

@@ -58,10 +58,14 @@ impl ThemeSelector {
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let query_editor = cx.add_view(|cx| {
-            Editor::single_line(settings.clone(), cx).with_style({
-                let settings = settings.clone();
-                move |_| settings.borrow().theme.selector.input_editor.as_editor()
-            })
+            Editor::single_line(
+                settings.clone(),
+                {
+                    let settings = settings.clone();
+                    move |_| settings.borrow().theme.selector.input_editor.as_editor()
+                },
+                cx,
+            )
         });
 
         cx.subscribe(&query_editor, Self::on_query_editor_event)

zed/src/workspace.rs πŸ”—

@@ -1445,6 +1445,7 @@ mod tests {
         editor.update(&mut cx, |editor, cx| {
             assert!(!editor.is_dirty(cx.as_ref()));
             assert_eq!(editor.title(cx.as_ref()), "untitled");
+            assert!(editor.language(cx).is_none());
             editor.insert(&Insert("hi".into()), cx);
             assert!(editor.is_dirty(cx.as_ref()));
         });
@@ -1455,7 +1456,7 @@ mod tests {
         });
         cx.simulate_new_path_selection(|parent_dir| {
             assert_eq!(parent_dir, dir.path());
-            Some(parent_dir.join("the-new-name"))
+            Some(parent_dir.join("the-new-name.rs"))
         });
         cx.read(|cx| {
             assert!(editor.is_dirty(cx));
@@ -1468,7 +1469,11 @@ mod tests {
             .await;
         cx.read(|cx| {
             assert!(!editor.is_dirty(cx));
-            assert_eq!(editor.title(cx), "the-new-name");
+            assert_eq!(editor.title(cx), "the-new-name.rs");
+        });
+        // The language is assigned based on the path
+        editor.read_with(&cx, |editor, cx| {
+            assert_eq!(editor.language(cx).unwrap().name(), "Rust")
         });
 
         // Edit the file and save it again. This time, there is no filename prompt.
@@ -1483,7 +1488,7 @@ mod tests {
         editor
             .condition(&cx, |editor, cx| !editor.is_dirty(cx))
             .await;
-        cx.read(|cx| assert_eq!(editor.title(cx), "the-new-name"));
+        cx.read(|cx| assert_eq!(editor.title(cx), "the-new-name.rs"));
 
         // Open the same newly-created file in another pane item. The new editor should reuse
         // the same buffer.
@@ -1491,7 +1496,7 @@ mod tests {
             workspace.open_new_file(&OpenNew(app_state.clone()), cx);
             workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
             assert!(workspace
-                .open_entry((tree.id(), Path::new("the-new-name").into()), cx)
+                .open_entry((tree.id(), Path::new("the-new-name.rs").into()), cx)
                 .is_none());
         });
         let editor2 = workspace.update(&mut cx, |workspace, cx| {
@@ -1507,6 +1512,47 @@ mod tests {
         })
     }
 
+    #[gpui::test]
+    async fn test_setting_language_when_saving_as_single_file_worktree(
+        mut cx: gpui::TestAppContext,
+    ) {
+        let dir = TempDir::new("test-new-file").unwrap();
+        let app_state = cx.update(test_app_state);
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
+
+        // Create a new untitled buffer
+        let editor = workspace.update(&mut cx, |workspace, cx| {
+            workspace.open_new_file(&OpenNew(app_state.clone()), cx);
+            workspace
+                .active_item(cx)
+                .unwrap()
+                .to_any()
+                .downcast::<Editor>()
+                .unwrap()
+        });
+
+        editor.update(&mut cx, |editor, cx| {
+            assert!(editor.language(cx).is_none());
+            editor.insert(&Insert("hi".into()), cx);
+            assert!(editor.is_dirty(cx.as_ref()));
+        });
+
+        // Save the buffer. This prompts for a filename.
+        workspace.update(&mut cx, |workspace, cx| {
+            workspace.save_active_item(&Save, cx)
+        });
+        cx.simulate_new_path_selection(|_| Some(dir.path().join("the-new-name.rs")));
+
+        editor
+            .condition(&cx, |editor, cx| !editor.is_dirty(cx))
+            .await;
+
+        // The language is assigned based on the path
+        editor.read_with(&cx, |editor, cx| {
+            assert_eq!(editor.language(cx).unwrap().name(), "Rust")
+        });
+    }
+
     #[gpui::test]
     async fn test_new_empty_workspace(mut cx: gpui::TestAppContext) {
         cx.update(init);

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

@@ -185,13 +185,13 @@ impl Pane {
             theme.workspace.tab.label.text.font_size,
         );
 
-        let mut row = Flex::row();
-        for (ix, item) in self.items.iter().enumerate() {
-            let is_active = ix == self.active_item;
+        enum Tabs {}
+        let tabs = MouseEventHandler::new::<Tabs, _, _, _>(0, cx, |mouse_state, cx| {
+            let mut row = Flex::row();
+            for (ix, item) in self.items.iter().enumerate() {
+                let is_active = ix == self.active_item;
 
-            enum Tab {}
-            row.add_child(
-                MouseEventHandler::new::<Tab, _, _, _>(item.id(), cx, |mouse_state, cx| {
+                row.add_child({
                     let mut title = item.title(cx);
                     if title.len() > MAX_TAB_TITLE_LEN {
                         let mut truncated_len = MAX_TAB_TITLE_LEN;
@@ -276,7 +276,7 @@ impl Pane {
                                 )
                                 .with_child(
                                     Align::new(
-                                        ConstrainedBox::new(if is_active || mouse_state.hovered {
+                                        ConstrainedBox::new(if mouse_state.hovered {
                                             let item_id = item.id();
                                             enum TabCloseButton {}
                                             let icon = Svg::new("icons/x.svg");
@@ -292,6 +292,7 @@ impl Pane {
                                                     }
                                                 },
                                             )
+                                            .with_padding(Padding::uniform(4.))
                                             .with_cursor_style(CursorStyle::PointingHand)
                                             .on_click(move |cx| {
                                                 cx.dispatch_action(CloseItem(item_id))
@@ -316,36 +317,37 @@ impl Pane {
                     })
                     .boxed()
                 })
-                .boxed(),
-            )
-        }
+            }
 
-        // Ensure there's always a minimum amount of space after the last tab,
-        // so that the tab's border doesn't abut the window's border.
-        let mut border = Border::bottom(1.0, Color::default());
-        border.color = theme.workspace.tab.container.border.color;
-
-        row.add_child(
-            ConstrainedBox::new(
-                Container::new(Empty::new().boxed())
-                    .with_border(border)
-                    .boxed(),
-            )
-            .with_min_width(20.)
-            .named("fixed-filler"),
-        );
+            // Ensure there's always a minimum amount of space after the last tab,
+            // so that the tab's border doesn't abut the window's border.
+            let mut border = Border::bottom(1.0, Color::default());
+            border.color = theme.workspace.tab.container.border.color;
 
-        row.add_child(
-            Expanded::new(
-                0.0,
-                Container::new(Empty::new().boxed())
-                    .with_border(border)
-                    .boxed(),
-            )
-            .named("filler"),
-        );
+            row.add_child(
+                ConstrainedBox::new(
+                    Container::new(Empty::new().boxed())
+                        .with_border(border)
+                        .boxed(),
+                )
+                .with_min_width(20.)
+                .named("fixed-filler"),
+            );
+
+            row.add_child(
+                Expanded::new(
+                    0.0,
+                    Container::new(Empty::new().boxed())
+                        .with_border(border)
+                        .boxed(),
+                )
+                .named("filler"),
+            );
+
+            row.boxed()
+        });
 
-        ConstrainedBox::new(row.boxed())
+        ConstrainedBox::new(tabs.boxed())
             .with_height(line_height + 16.)
             .named("tabs")
     }

zed/src/worktree.rs πŸ”—

@@ -6,7 +6,7 @@ use crate::{
     fs::{self, Fs},
     fuzzy,
     fuzzy::CharBag,
-    language::LanguageRegistry,
+    language::{Language, LanguageRegistry},
     rpc::{self, proto, Status},
     time::{self, ReplicaId},
     util::{Bias, TryFutureExt},
@@ -832,7 +832,6 @@ impl LocalWorktree {
             }
         });
 
-        let languages = self.languages.clone();
         let path = Arc::from(path);
         cx.spawn(|this, mut cx| async move {
             if let Some(existing_buffer) = existing_buffer {
@@ -841,8 +840,8 @@ impl LocalWorktree {
                 let (file, contents) = this
                     .update(&mut cx, |this, cx| this.as_local().unwrap().load(&path, cx))
                     .await?;
-                let language = languages.select_language(&path).cloned();
                 let buffer = cx.add_model(|cx| {
+                    let language = file.select_language(cx);
                     Buffer::from_history(0, History::new(contents.into()), Some(file), language, cx)
                 });
                 this.update(&mut cx, |this, _| {
@@ -1192,7 +1191,6 @@ impl RemoteWorktree {
         });
 
         let rpc = self.rpc.clone();
-        let languages = self.languages.clone();
         let replica_id = self.replica_id;
         let remote_worktree_id = self.remote_id;
         let path = path.to_string_lossy().to_string();
@@ -1204,7 +1202,7 @@ impl RemoteWorktree {
                     .read_with(&cx, |tree, _| tree.entry_for_path(&path).cloned())
                     .ok_or_else(|| anyhow!("file does not exist"))?;
                 let file = File::new(entry.id, handle, entry.path, entry.mtime);
-                let language = languages.select_language(&path).cloned();
+                let language = cx.read(|cx| file.select_language(cx));
                 let response = rpc
                     .request(proto::OpenBuffer {
                         worktree_id: remote_worktree_id as u64,
@@ -1681,6 +1679,18 @@ impl File {
         self.worktree.read(cx).abs_path.join(&self.path)
     }
 
+    pub fn select_language(&self, cx: &AppContext) -> Option<Arc<Language>> {
+        let worktree = self.worktree.read(cx);
+        let mut full_path = PathBuf::new();
+        full_path.push(worktree.root_name());
+        full_path.push(&self.path);
+        let languages = match self.worktree.read(cx) {
+            Worktree::Local(worktree) => &worktree.languages,
+            Worktree::Remote(worktree) => &worktree.languages,
+        };
+        languages.select_language(&full_path).cloned()
+    }
+
     /// Returns the last component of this handle's absolute path. If this handle refers to the root
     /// of its worktree, then this method will return the name of the worktree itself.
     pub fn file_name<'a>(&'a self, cx: &'a AppContext) -> Option<OsString> {