Merge branch 'main' into project-find

Max Brunsfeld created

Change summary

Cargo.lock                                    |  11 
crates/chat_panel/src/chat_panel.rs           |  23 
crates/diagnostics/src/diagnostics.rs         |  24 
crates/editor/Cargo.toml                      |   2 
crates/editor/src/editor.rs                   | 476 +++++++++-----------
crates/editor/src/element.rs                  |  86 +-
crates/editor/src/items.rs                    |   6 
crates/file_finder/src/file_finder.rs         |  15 
crates/find/src/buffer_find.rs                |  24 
crates/find/src/project_find.rs               |  14 
crates/go_to_line/src/go_to_line.rs           |  16 
crates/outline/src/outline.rs                 |  20 
crates/project_symbols/src/project_symbols.rs |  16 
crates/server/src/rpc.rs                      |  15 
crates/theme/src/theme.rs                     |  87 ---
crates/theme_selector/src/theme_selector.rs   |  15 
crates/workspace/src/settings.rs              |  13 
crates/zed/Cargo.toml                         |   1 
crates/zed/assets/themes/_base.toml           |   6 
crates/zed/languages/c/config.toml            |  13 
crates/zed/languages/c/highlights.scm         | 101 ++++
crates/zed/languages/c/indents.scm            |   7 
crates/zed/languages/c/outline.scm            |  30 +
crates/zed/src/language.rs                    | 146 ++++++
crates/zed/src/test.rs                        |  30 -
crates/zed/src/zed.rs                         |  22 
26 files changed, 675 insertions(+), 544 deletions(-)

Detailed changes

Cargo.lock šŸ”—

@@ -5278,6 +5278,16 @@ dependencies = [
  "regex",
 ]
 
+[[package]]
+name = "tree-sitter-c"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bdc5574c6cbc39c409246caeb1dd4d3c4bd6d30d4e9b399776086c20365fd24"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
 [[package]]
 name = "tree-sitter-markdown"
 version = "0.0.1"
@@ -5887,6 +5897,7 @@ dependencies = [
  "tiny_http",
  "toml",
  "tree-sitter",
+ "tree-sitter-c",
  "tree-sitter-markdown",
  "tree-sitter-rust",
  "unindent",

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

@@ -2,7 +2,7 @@ use client::{
     channel::{Channel, ChannelEvent, ChannelList, ChannelMessage},
     Client,
 };
-use editor::{Editor, EditorSettings};
+use editor::Editor;
 use gpui::{
     action,
     elements::*,
@@ -16,7 +16,7 @@ use postage::{prelude::Stream, watch};
 use std::sync::Arc;
 use time::{OffsetDateTime, UtcOffset};
 use util::{ResultExt, TryFutureExt};
-use workspace::Settings;
+use workspace::{settings::SoftWrap, Settings};
 
 const MESSAGE_LOADING_THRESHOLD: usize = 50;
 
@@ -52,21 +52,14 @@ impl ChatPanel {
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let input_editor = cx.add_view(|cx| {
-            Editor::auto_height(
+            let mut editor = Editor::auto_height(
                 4,
-                {
-                    let settings = settings.clone();
-                    Arc::new(move |_| {
-                        let settings = settings.borrow();
-                        EditorSettings {
-                            tab_size: settings.tab_size,
-                            style: settings.theme.chat_panel.input_editor.as_editor(),
-                            soft_wrap: editor::SoftWrap::EditorWidth,
-                        }
-                    })
-                },
+                settings.clone(),
+                Some(|theme| theme.chat_panel.input_editor.clone()),
                 cx,
-            )
+            );
+            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
+            editor
         });
         let channel_select = cx.add_view(|cx| {
             let channel_list = channel_list.clone();

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

@@ -7,7 +7,7 @@ use editor::{
     display_map::{BlockDisposition, BlockId, BlockProperties, RenderBlock},
     highlight_diagnostic_message,
     items::BufferItemHandle,
-    Autoscroll, BuildSettings, Editor, ExcerptId, MultiBuffer, ToOffset,
+    Autoscroll, Editor, ExcerptId, MultiBuffer, ToOffset,
 };
 use gpui::{
     action, elements::*, fonts::TextStyle, keymap::Binding, AnyViewHandle, AppContext, Entity,
@@ -62,7 +62,6 @@ struct ProjectDiagnosticsEditor {
     excerpts: ModelHandle<MultiBuffer>,
     path_states: Vec<PathState>,
     paths_to_update: BTreeSet<ProjectPath>,
-    build_settings: BuildSettings,
     settings: watch::Receiver<workspace::Settings>,
 }
 
@@ -142,12 +141,11 @@ impl ProjectDiagnosticsEditor {
         .detach();
 
         let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
-        let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
         let editor = cx.add_view(|cx| {
             let mut editor = Editor::for_buffer(
                 excerpts.clone(),
-                build_settings.clone(),
                 Some(project.clone()),
+                settings.clone(),
                 cx,
             );
             editor.set_vertical_scroll_margin(5, cx);
@@ -164,7 +162,6 @@ impl ProjectDiagnosticsEditor {
             workspace,
             excerpts,
             editor,
-            build_settings,
             settings,
             path_states: Default::default(),
             paths_to_update,
@@ -360,7 +357,7 @@ impl ProjectDiagnosticsEditor {
                                     height: 2,
                                     render: diagnostic_header_renderer(
                                         primary,
-                                        self.build_settings.clone(),
+                                        self.settings.clone(),
                                     ),
                                     disposition: BlockDisposition::Above,
                                 });
@@ -382,7 +379,7 @@ impl ProjectDiagnosticsEditor {
                                         render: diagnostic_block_renderer(
                                             diagnostic,
                                             true,
-                                            self.build_settings.clone(),
+                                            self.settings.clone(),
                                         ),
                                         disposition: BlockDisposition::Below,
                                     });
@@ -644,20 +641,21 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
 
 fn diagnostic_header_renderer(
     diagnostic: Diagnostic,
-    build_settings: BuildSettings,
+    settings: watch::Receiver<workspace::Settings>,
 ) -> RenderBlock {
     let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
     Arc::new(move |cx| {
-        let settings = build_settings(cx);
-        let style = &settings.style.diagnostic_header;
-        let font_size = (style.text_scale_factor * settings.style.text.font_size).round();
+        let settings = settings.borrow();
+        let theme = &settings.theme.editor;
+        let style = &theme.diagnostic_header;
+        let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
         let icon_width = cx.em_width * style.icon_width_factor;
         let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
             Svg::new("icons/diagnostic-error-10.svg")
-                .with_color(settings.style.error_diagnostic.message.text.color)
+                .with_color(theme.error_diagnostic.message.text.color)
         } else {
             Svg::new("icons/diagnostic-warning-10.svg")
-                .with_color(settings.style.warning_diagnostic.message.text.color)
+                .with_color(theme.warning_diagnostic.message.text.color)
         };
 
         Flex::row()

crates/editor/Cargo.toml šŸ”—

@@ -14,6 +14,7 @@ test-support = [
     "gpui/test-support",
     "project/test-support",
     "util/test-support",
+    "workspace/test-support",
 ]
 
 [dependencies]
@@ -50,6 +51,7 @@ lsp = { path = "../lsp", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
 ctor = "0.1"
 env_logger = "0.8"
 rand = "0.8"

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

@@ -26,7 +26,7 @@ use gpui::{
     platform::CursorStyle,
     text_layout, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
     ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
-    WeakModelHandle, WeakViewHandle,
+    WeakViewHandle,
 };
 use items::{BufferItemHandle, MultiBufferItemHandle};
 use itertools::Itertools as _;
@@ -57,9 +57,9 @@ use std::{
 };
 pub use sum_tree::Bias;
 use text::rope::TextDimension;
-use theme::{DiagnosticStyle, EditorStyle};
+use theme::DiagnosticStyle;
 use util::{post_inc, ResultExt, TryFutureExt};
-use workspace::{ItemNavHistory, PathOpener, Workspace};
+use workspace::{settings, ItemNavHistory, PathOpener, Settings, Workspace};
 
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 const MAX_LINE_LEN: usize = 1024;
@@ -388,13 +388,6 @@ pub enum EditorMode {
     Full,
 }
 
-#[derive(Clone)]
-pub struct EditorSettings {
-    pub tab_size: usize,
-    pub soft_wrap: SoftWrap,
-    pub style: EditorStyle,
-}
-
 #[derive(Clone)]
 pub enum SoftWrap {
     None,
@@ -402,9 +395,16 @@ pub enum SoftWrap {
     Column(u32),
 }
 
+#[derive(Clone)]
+pub struct EditorStyle {
+    pub text: TextStyle,
+    pub placeholder_text: Option<TextStyle>,
+    pub theme: theme::Editor,
+}
+
 type CompletionId = usize;
 
-pub type BuildSettings = Arc<dyn 'static + Send + Sync + Fn(&AppContext) -> EditorSettings>;
+pub type GetFieldEditorTheme = fn(&theme::Theme) -> theme::FieldEditor;
 
 pub struct Editor {
     handle: WeakViewHandle<Self>,
@@ -425,7 +425,9 @@ pub struct Editor {
     scroll_position: Vector2F,
     scroll_top_anchor: Option<Anchor>,
     autoscroll_request: Option<Autoscroll>,
-    build_settings: BuildSettings,
+    settings: watch::Receiver<Settings>,
+    soft_wrap_mode_override: Option<settings::SoftWrap>,
+    get_field_editor_theme: Option<GetFieldEditorTheme>,
     project: Option<ModelHandle<Project>>,
     focused: bool,
     show_local_cursors: bool,
@@ -531,12 +533,12 @@ impl ContextMenu {
     fn render(
         &self,
         cursor_position: DisplayPoint,
-        build_settings: BuildSettings,
+        style: EditorStyle,
         cx: &AppContext,
     ) -> (DisplayPoint, ElementBox) {
         match self {
-            ContextMenu::Completions(menu) => (cursor_position, menu.render(build_settings, cx)),
-            ContextMenu::CodeActions(menu) => menu.render(cursor_position, build_settings, cx),
+            ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)),
+            ContextMenu::CodeActions(menu) => menu.render(cursor_position, style),
         }
     }
 }
@@ -573,15 +575,14 @@ impl CompletionsMenu {
         !self.matches.is_empty()
     }
 
-    fn render(&self, build_settings: BuildSettings, cx: &AppContext) -> ElementBox {
+    fn render(&self, style: EditorStyle, _: &AppContext) -> ElementBox {
         enum CompletionTag {}
 
-        let settings = build_settings(cx);
         let completions = self.completions.clone();
         let matches = self.matches.clone();
         let selected_item = self.selected_item;
+        let container_style = style.autocomplete.container;
         UniformList::new(self.list.clone(), matches.len(), move |range, items, cx| {
-            let settings = build_settings(cx);
             let start_ix = range.start;
             for (ix, mat) in matches[range].iter().enumerate() {
                 let completion = &completions[mat.candidate_id];
@@ -592,22 +593,22 @@ impl CompletionsMenu {
                         cx,
                         |state, _| {
                             let item_style = if item_ix == selected_item {
-                                settings.style.autocomplete.selected_item
+                                style.autocomplete.selected_item
                             } else if state.hovered {
-                                settings.style.autocomplete.hovered_item
+                                style.autocomplete.hovered_item
                             } else {
-                                settings.style.autocomplete.item
+                                style.autocomplete.item
                             };
 
-                            Text::new(completion.label.text.clone(), settings.style.text.clone())
+                            Text::new(completion.label.text.clone(), style.text.clone())
                                 .with_soft_wrap(false)
                                 .with_highlights(combine_syntax_and_fuzzy_match_highlights(
                                     &completion.label.text,
-                                    settings.style.text.color.into(),
+                                    style.text.color.into(),
                                     styled_runs_for_code_label(
                                         &completion.label,
-                                        settings.style.text.color,
-                                        &settings.style.syntax,
+                                        style.text.color,
+                                        &style.syntax,
                                     ),
                                     &mat.positions,
                                 ))
@@ -638,7 +639,7 @@ impl CompletionsMenu {
                 .map(|(ix, _)| ix),
         )
         .contained()
-        .with_style(settings.style.autocomplete.container)
+        .with_style(container_style)
         .boxed()
     }
 
@@ -714,31 +715,29 @@ impl CodeActionsMenu {
     fn render(
         &self,
         mut cursor_position: DisplayPoint,
-        build_settings: BuildSettings,
-        cx: &AppContext,
+        style: EditorStyle,
     ) -> (DisplayPoint, ElementBox) {
         enum ActionTag {}
 
-        let settings = build_settings(cx);
+        let container_style = style.autocomplete.container;
         let actions = self.actions.clone();
         let selected_item = self.selected_item;
         let element =
             UniformList::new(self.list.clone(), actions.len(), move |range, items, cx| {
-                let settings = build_settings(cx);
                 let start_ix = range.start;
                 for (ix, action) in actions[range].iter().enumerate() {
                     let item_ix = start_ix + ix;
                     items.push(
                         MouseEventHandler::new::<ActionTag, _, _>(item_ix, cx, |state, _| {
                             let item_style = if item_ix == selected_item {
-                                settings.style.autocomplete.selected_item
+                                style.autocomplete.selected_item
                             } else if state.hovered {
-                                settings.style.autocomplete.hovered_item
+                                style.autocomplete.hovered_item
                             } else {
-                                settings.style.autocomplete.item
+                                style.autocomplete.item
                             };
 
-                            Text::new(action.lsp_action.title.clone(), settings.style.text.clone())
+                            Text::new(action.lsp_action.title.clone(), style.text.clone())
                                 .with_soft_wrap(false)
                                 .contained()
                                 .with_style(item_style)
@@ -760,7 +759,7 @@ impl CodeActionsMenu {
                     .map(|(ix, _)| ix),
             )
             .contained()
-            .with_style(settings.style.autocomplete.container)
+            .with_style(container_style)
             .boxed();
 
         if self.deployed_from_indicator {
@@ -791,40 +790,57 @@ pub struct NavigationData {
 }
 
 impl Editor {
-    pub fn single_line(build_settings: BuildSettings, cx: &mut ViewContext<Self>) -> Self {
+    pub fn single_line(
+        settings: watch::Receiver<Settings>,
+        field_editor_style: Option<GetFieldEditorTheme>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
         let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-        let mut view = Self::for_buffer(buffer, build_settings, None, cx);
-        view.mode = EditorMode::SingleLine;
-        view
+        Self::new(
+            EditorMode::SingleLine,
+            buffer,
+            None,
+            settings,
+            field_editor_style,
+            cx,
+        )
     }
 
     pub fn auto_height(
         max_lines: usize,
-        build_settings: BuildSettings,
+        settings: watch::Receiver<Settings>,
+        field_editor_style: Option<GetFieldEditorTheme>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-        let mut view = Self::for_buffer(buffer, build_settings, None, cx);
-        view.mode = EditorMode::AutoHeight { max_lines };
-        view
+        Self::new(
+            EditorMode::AutoHeight { max_lines },
+            buffer,
+            None,
+            settings,
+            field_editor_style,
+            cx,
+        )
     }
 
     pub fn for_buffer(
         buffer: ModelHandle<MultiBuffer>,
-        build_settings: BuildSettings,
         project: Option<ModelHandle<Project>>,
+        settings: watch::Receiver<Settings>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        Self::new(buffer, build_settings, project, cx)
+        Self::new(EditorMode::Full, buffer, project, settings, None, cx)
     }
 
     pub fn clone(&self, cx: &mut ViewContext<Self>) -> Self {
         let mut clone = Self::new(
+            self.mode,
             self.buffer.clone(),
-            self.build_settings.clone(),
             self.project.clone(),
+            self.settings.clone(),
+            self.get_field_editor_theme,
             cx,
         );
         clone.scroll_position = self.scroll_position;
@@ -836,19 +852,22 @@ impl Editor {
         clone
     }
 
-    pub fn new(
+    fn new(
+        mode: EditorMode,
         buffer: ModelHandle<MultiBuffer>,
-        build_settings: BuildSettings,
         project: Option<ModelHandle<Project>>,
+        settings: watch::Receiver<Settings>,
+        get_field_editor_theme: Option<GetFieldEditorTheme>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        let settings = build_settings(cx);
         let display_map = cx.add_model(|cx| {
+            let settings = settings.borrow();
+            let style = build_style(&*settings, get_field_editor_theme, cx);
             DisplayMap::new(
                 buffer.clone(),
                 settings.tab_size,
-                settings.style.text.font_id,
-                settings.style.text.font_size,
+                style.text.font_id,
+                style.text.font_size,
                 None,
                 2,
                 1,
@@ -884,7 +903,9 @@ impl Editor {
             snippet_stack: Default::default(),
             select_larger_syntax_node_stack: Vec::new(),
             active_diagnostics: None,
-            build_settings,
+            settings,
+            soft_wrap_mode_override: None,
+            get_field_editor_theme,
             project,
             scroll_position: Vector2F::zero(),
             scroll_top_anchor: None,
@@ -893,7 +914,7 @@ impl Editor {
             show_local_cursors: false,
             blink_epoch: 0,
             blinking_paused: false,
-            mode: EditorMode::Full,
+            mode,
             vertical_scroll_margin: 3.0,
             placeholder_text: None,
             highlighted_rows: None,
@@ -951,6 +972,10 @@ impl Editor {
         self.buffer.read(cx).language(cx)
     }
 
+    fn style(&self, cx: &AppContext) -> EditorStyle {
+        build_style(&*self.settings.borrow(), self.get_field_editor_theme, cx)
+    }
+
     pub fn set_placeholder_text(
         &mut self,
         placeholder_text: impl Into<Arc<str>>,
@@ -2253,12 +2278,8 @@ impl Editor {
             let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx);
             if let Some(editor) = editor.act_as::<Self>(cx) {
                 editor.update(cx, |editor, cx| {
-                    let settings = (editor.build_settings)(cx);
-                    editor.highlight_ranges::<Self>(
-                        ranges_to_highlight,
-                        settings.style.highlighted_line_background,
-                        cx,
-                    );
+                    let color = editor.style(cx).highlighted_line_background;
+                    editor.highlight_ranges::<Self>(ranges_to_highlight, color, cx);
                 });
             }
         });
@@ -2322,7 +2343,9 @@ impl Editor {
                 this.update(&mut cx, |this, cx| {
                     let buffer_id = cursor_position.buffer_id;
                     let excerpt_id = cursor_position.excerpt_id.clone();
-                    let settings = (this.build_settings)(cx);
+                    let style = this.style(cx);
+                    let read_background = style.document_highlight_read_background;
+                    let write_background = style.document_highlight_write_background;
                     let buffer = this.buffer.read(cx);
                     if !buffer
                         .text_anchor_for_position(cursor_position, cx)
@@ -2352,12 +2375,12 @@ impl Editor {
 
                     this.highlight_ranges::<DocumentHighlightRead>(
                         read_ranges,
-                        settings.style.document_highlight_read_background,
+                        read_background,
                         cx,
                     );
                     this.highlight_ranges::<DocumentHighlightWrite>(
                         write_ranges,
-                        settings.style.document_highlight_write_background,
+                        write_background,
                         cx,
                     );
                     cx.notify();
@@ -2367,10 +2390,13 @@ impl Editor {
         None
     }
 
-    pub fn render_code_actions_indicator(&self, cx: &mut ViewContext<Self>) -> Option<ElementBox> {
+    pub fn render_code_actions_indicator(
+        &self,
+        style: &EditorStyle,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<ElementBox> {
         if self.available_code_actions.is_some() {
             enum Tag {}
-            let style = (self.build_settings)(cx).style;
             Some(
                 MouseEventHandler::new::<Tag, _, _>(0, cx, |_, _| {
                     Svg::new("icons/zap.svg")
@@ -2398,11 +2424,12 @@ impl Editor {
     pub fn render_context_menu(
         &self,
         cursor_position: DisplayPoint,
+        style: EditorStyle,
         cx: &AppContext,
     ) -> Option<(DisplayPoint, ElementBox)> {
         self.context_menu
             .as_ref()
-            .map(|menu| menu.render(cursor_position, self.build_settings.clone(), cx))
+            .map(|menu| menu.render(cursor_position, style, cx))
     }
 
     fn show_context_menu(&mut self, menu: ContextMenu, cx: &mut ViewContext<Self>) {
@@ -2578,7 +2605,7 @@ impl Editor {
         }
 
         self.start_transaction(cx);
-        let tab_size = (self.build_settings)(cx).tab_size;
+        let tab_size = self.settings.borrow().tab_size;
         let mut selections = self.local_selections::<Point>(cx);
         let mut last_indent = None;
         self.buffer.update(cx, |buffer, cx| {
@@ -2655,7 +2682,7 @@ impl Editor {
         }
 
         self.start_transaction(cx);
-        let tab_size = (self.build_settings)(cx).tab_size;
+        let tab_size = self.settings.borrow().tab_size;
         let selections = self.local_selections::<Point>(cx);
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut deletion_ranges = Vec::new();
@@ -4234,12 +4261,8 @@ impl Editor {
                 let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx);
                 if let Some(editor) = editor.act_as::<Self>(cx) {
                     editor.update(cx, |editor, cx| {
-                        let settings = (editor.build_settings)(cx);
-                        editor.highlight_ranges::<Self>(
-                            ranges_to_highlight,
-                            settings.style.highlighted_line_background,
-                            cx,
-                        );
+                        let color = editor.style(cx).highlighted_line_background;
+                        editor.highlight_ranges::<Self>(ranges_to_highlight, color, cx);
                     });
                 }
             });
@@ -4282,7 +4305,7 @@ impl Editor {
 
                 this.update(&mut cx, |this, cx| {
                     this.take_rename(cx);
-                    let settings = (this.build_settings)(cx);
+                    let style = this.style(cx);
                     let buffer = this.buffer.read(cx).read(cx);
                     let cursor_offset = selection.head().to_offset(&buffer);
                     let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range);
@@ -4295,7 +4318,7 @@ impl Editor {
 
                     // Position the selection in the rename editor so that it matches the current selection.
                     let rename_editor = cx.add_view(|cx| {
-                        let mut editor = Editor::single_line(this.build_settings.clone(), cx);
+                        let mut editor = Editor::single_line(this.settings.clone(), None, cx);
                         editor
                             .buffer
                             .update(cx, |buffer, cx| buffer.edit([0..0], &old_name, cx));
@@ -4306,14 +4329,14 @@ impl Editor {
                         );
                         editor.highlight_ranges::<Rename>(
                             vec![Anchor::min()..Anchor::max()],
-                            settings.style.diff_background_inserted,
+                            style.diff_background_inserted,
                             cx,
                         );
                         editor
                     });
                     this.highlight_ranges::<Rename>(
                         vec![range.clone()],
-                        settings.style.diff_background_deleted,
+                        style.diff_background_deleted,
                         cx,
                     );
                     this.update_selections(
@@ -4483,7 +4506,7 @@ impl Editor {
                         diagnostic_block_renderer(
                             diagnostic.clone(),
                             is_valid,
-                            self.build_settings.clone(),
+                            self.settings.clone(),
                         ),
                     );
                 }
@@ -4522,14 +4545,16 @@ impl Editor {
             let blocks = display_map
                 .insert_blocks(
                     diagnostic_group.iter().map(|entry| {
-                        let build_settings = self.build_settings.clone();
                         let diagnostic = entry.diagnostic.clone();
                         let message_height = diagnostic.message.lines().count() as u8;
-
                         BlockProperties {
                             position: buffer.anchor_after(entry.range.start),
                             height: message_height,
-                            render: diagnostic_block_renderer(diagnostic, true, build_settings),
+                            render: diagnostic_block_renderer(
+                                diagnostic,
+                                true,
+                                self.settings.clone(),
+                            ),
                             disposition: BlockDisposition::Below,
                         }
                     }),
@@ -5126,6 +5151,26 @@ impl Editor {
             .text()
     }
 
+    pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap {
+        let language = self.language(cx);
+        let settings = self.settings.borrow();
+        let mode = self
+            .soft_wrap_mode_override
+            .unwrap_or_else(|| settings.soft_wrap(language));
+        match mode {
+            settings::SoftWrap::None => SoftWrap::None,
+            settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth,
+            settings::SoftWrap::PreferredLineLength => {
+                SoftWrap::Column(settings.preferred_line_length(language))
+            }
+        }
+    }
+
+    pub fn set_soft_wrap_mode(&mut self, mode: settings::SoftWrap, cx: &mut ViewContext<Self>) {
+        self.soft_wrap_mode_override = Some(mode);
+        cx.notify();
+    }
+
     pub fn set_wrap_width(&self, width: Option<f32>, cx: &mut MutableAppContext) -> bool {
         self.display_map
             .update(cx, |map, cx| map.set_wrap_width(width, cx))
@@ -5327,94 +5372,6 @@ impl Deref for EditorSnapshot {
     }
 }
 
-impl EditorSettings {
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn test(cx: &AppContext) -> Self {
-        use theme::{ContainedLabel, ContainedText, DiagnosticHeader, DiagnosticPathHeader};
-
-        Self {
-            tab_size: 4,
-            soft_wrap: SoftWrap::None,
-            style: {
-                let font_cache: &gpui::FontCache = cx.font_cache();
-                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();
-                let text = gpui::fonts::TextStyle {
-                    font_family_name,
-                    font_family_id,
-                    font_id,
-                    font_size: 14.,
-                    color: gpui::color::Color::from_u32(0xff0000ff),
-                    font_properties,
-                    underline: None,
-                };
-                let default_diagnostic_style = DiagnosticStyle {
-                    message: text.clone().into(),
-                    header: Default::default(),
-                    text_scale_factor: 1.,
-                };
-                EditorStyle {
-                    text: text.clone(),
-                    placeholder_text: None,
-                    background: Default::default(),
-                    gutter_background: Default::default(),
-                    gutter_padding_factor: 2.,
-                    active_line_background: Default::default(),
-                    highlighted_line_background: Default::default(),
-                    diff_background_deleted: Default::default(),
-                    diff_background_inserted: Default::default(),
-                    document_highlight_read_background: Default::default(),
-                    document_highlight_write_background: Default::default(),
-                    line_number: Default::default(),
-                    line_number_active: Default::default(),
-                    selection: Default::default(),
-                    guest_selections: Default::default(),
-                    syntax: Default::default(),
-                    diagnostic_path_header: DiagnosticPathHeader {
-                        container: Default::default(),
-                        filename: ContainedText {
-                            container: Default::default(),
-                            text: text.clone(),
-                        },
-                        path: ContainedText {
-                            container: Default::default(),
-                            text: text.clone(),
-                        },
-                        text_scale_factor: 1.,
-                    },
-                    diagnostic_header: DiagnosticHeader {
-                        container: Default::default(),
-                        message: ContainedLabel {
-                            container: Default::default(),
-                            label: text.clone().into(),
-                        },
-                        code: ContainedText {
-                            container: Default::default(),
-                            text: text.clone(),
-                        },
-                        icon_width_factor: 1.,
-                        text_scale_factor: 1.,
-                    },
-                    error_diagnostic: default_diagnostic_style.clone(),
-                    invalid_error_diagnostic: default_diagnostic_style.clone(),
-                    warning_diagnostic: default_diagnostic_style.clone(),
-                    invalid_warning_diagnostic: default_diagnostic_style.clone(),
-                    information_diagnostic: default_diagnostic_style.clone(),
-                    invalid_information_diagnostic: default_diagnostic_style.clone(),
-                    hint_diagnostic: default_diagnostic_style.clone(),
-                    invalid_hint_diagnostic: default_diagnostic_style.clone(),
-                    autocomplete: Default::default(),
-                    code_actions_indicator: Default::default(),
-                }
-            },
-        }
-    }
-}
-
 fn compute_scroll_position(
     snapshot: &DisplaySnapshot,
     mut scroll_position: Vector2F,
@@ -5447,15 +5404,11 @@ impl Entity for Editor {
 
 impl View for Editor {
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let settings = (self.build_settings)(cx);
+        let style = self.style(cx);
         self.display_map.update(cx, |map, cx| {
-            map.set_font(
-                settings.style.text.font_id,
-                settings.style.text.font_size,
-                cx,
-            )
+            map.set_font(style.text.font_id, style.text.font_size, cx)
         });
-        EditorElement::new(self.handle.clone(), settings).boxed()
+        EditorElement::new(self.handle.clone(), style.clone()).boxed()
     }
 
     fn ui_name() -> &'static str {
@@ -5505,6 +5458,44 @@ impl View for Editor {
     }
 }
 
+fn build_style(
+    settings: &Settings,
+    get_field_editor_theme: Option<GetFieldEditorTheme>,
+    cx: &AppContext,
+) -> EditorStyle {
+    let theme = settings.theme.editor.clone();
+    if let Some(get_field_editor_theme) = get_field_editor_theme {
+        let field_editor_theme = get_field_editor_theme(&settings.theme);
+        EditorStyle {
+            text: field_editor_theme.text,
+            placeholder_text: field_editor_theme.placeholder_text,
+            theme,
+        }
+    } else {
+        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;
+        EditorStyle {
+            text: TextStyle {
+                color: settings.theme.editor.text_color,
+                font_family_name,
+                font_family_id,
+                font_id,
+                font_size,
+                font_properties,
+                underline: None,
+            },
+            placeholder_text: None,
+            theme,
+        }
+    }
+}
+
 impl<T: ToPoint + ToOffset> SelectionExt for Selection<T> {
     fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range<Point> {
         let start = self.start.to_point(buffer);
@@ -5619,10 +5610,18 @@ impl InvalidationRegion for SnippetState {
     }
 }
 
+impl Deref for EditorStyle {
+    type Target = theme::Editor;
+
+    fn deref(&self) -> &Self::Target {
+        &self.theme
+    }
+}
+
 pub fn diagnostic_block_renderer(
     diagnostic: Diagnostic,
     is_valid: bool,
-    build_settings: BuildSettings,
+    settings: watch::Receiver<Settings>,
 ) -> RenderBlock {
     let mut highlighted_lines = Vec::new();
     for line in diagnostic.message.lines() {
@@ -5630,9 +5629,10 @@ pub fn diagnostic_block_renderer(
     }
 
     Arc::new(move |cx: &BlockContext| {
-        let settings = build_settings(cx);
-        let style = diagnostic_style(diagnostic.severity, is_valid, &settings.style);
-        let font_size = (style.text_scale_factor * settings.style.text.font_size).round();
+        let settings = settings.borrow();
+        let theme = &settings.theme.editor;
+        let style = diagnostic_style(diagnostic.severity, is_valid, theme);
+        let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
         Flex::column()
             .with_children(highlighted_lines.iter().map(|(line, highlights)| {
                 Label::new(
@@ -5675,67 +5675,21 @@ pub fn highlight_diagnostic_message(message: &str) -> (String, Vec<usize>) {
 pub fn diagnostic_style(
     severity: DiagnosticSeverity,
     valid: bool,
-    style: &EditorStyle,
+    theme: &theme::Editor,
 ) -> DiagnosticStyle {
     match (severity, valid) {
-        (DiagnosticSeverity::ERROR, true) => style.error_diagnostic.clone(),
-        (DiagnosticSeverity::ERROR, false) => style.invalid_error_diagnostic.clone(),
-        (DiagnosticSeverity::WARNING, true) => style.warning_diagnostic.clone(),
-        (DiagnosticSeverity::WARNING, false) => style.invalid_warning_diagnostic.clone(),
-        (DiagnosticSeverity::INFORMATION, true) => style.information_diagnostic.clone(),
-        (DiagnosticSeverity::INFORMATION, false) => style.invalid_information_diagnostic.clone(),
-        (DiagnosticSeverity::HINT, true) => style.hint_diagnostic.clone(),
-        (DiagnosticSeverity::HINT, false) => style.invalid_hint_diagnostic.clone(),
-        _ => DiagnosticStyle {
-            message: style.text.clone().into(),
-            header: Default::default(),
-            text_scale_factor: 1.,
-        },
+        (DiagnosticSeverity::ERROR, true) => theme.error_diagnostic.clone(),
+        (DiagnosticSeverity::ERROR, false) => theme.invalid_error_diagnostic.clone(),
+        (DiagnosticSeverity::WARNING, true) => theme.warning_diagnostic.clone(),
+        (DiagnosticSeverity::WARNING, false) => theme.invalid_warning_diagnostic.clone(),
+        (DiagnosticSeverity::INFORMATION, true) => theme.information_diagnostic.clone(),
+        (DiagnosticSeverity::INFORMATION, false) => theme.invalid_information_diagnostic.clone(),
+        (DiagnosticSeverity::HINT, true) => theme.hint_diagnostic.clone(),
+        (DiagnosticSeverity::HINT, false) => theme.invalid_hint_diagnostic.clone(),
+        _ => theme.invalid_hint_diagnostic.clone(),
     }
 }
 
-pub fn settings_builder(
-    buffer: WeakModelHandle<MultiBuffer>,
-    settings: watch::Receiver<workspace::Settings>,
-) -> BuildSettings {
-    Arc::new(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: None,
-        };
-        let language = buffer.upgrade(cx).and_then(|buf| buf.read(cx).language(cx));
-        let soft_wrap = match settings.soft_wrap(language) {
-            workspace::settings::SoftWrap::None => SoftWrap::None,
-            workspace::settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth,
-            workspace::settings::SoftWrap::PreferredLineLength => {
-                SoftWrap::Column(settings.preferred_line_length(language).saturating_sub(1))
-            }
-        };
-
-        EditorSettings {
-            tab_size: settings.tab_size,
-            soft_wrap,
-            style: theme,
-        }
-    })
-}
-
 pub fn combine_syntax_and_fuzzy_match_highlights(
     text: &str,
     default_style: HighlightStyle,
@@ -5874,7 +5828,7 @@ mod tests {
         let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx));
         let group_interval = buffer.read(cx).transaction_group_interval();
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-        let settings = EditorSettings::test(cx);
+        let settings = Settings::test(cx);
         let (_, editor) = cx.add_window(Default::default(), |cx| {
             build_editor(buffer.clone(), settings, cx)
         });
@@ -5942,7 +5896,7 @@ mod tests {
     #[gpui::test]
     fn test_selection_with_mouse(cx: &mut gpui::MutableAppContext) {
         let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
-        let settings = EditorSettings::test(cx);
+        let settings = Settings::test(cx);
         let (_, editor) =
             cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
@@ -6009,7 +5963,7 @@ mod tests {
     #[gpui::test]
     fn test_canceling_pending_selection(cx: &mut gpui::MutableAppContext) {
         let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
-        let settings = EditorSettings::test(cx);
+        let settings = Settings::test(cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
         view.update(cx, |view, cx| {
@@ -6043,7 +5997,7 @@ mod tests {
         cx.add_window(Default::default(), |cx| {
             use workspace::ItemView;
             let nav_history = Rc::new(RefCell::new(workspace::NavHistory::default()));
-            let settings = EditorSettings::test(&cx);
+            let settings = Settings::test(&cx);
             let buffer = MultiBuffer::build_simple(&sample_text(30, 5, 'a'), cx);
             let mut editor = build_editor(buffer.clone(), settings, cx);
             editor.nav_history = Some(ItemNavHistory::new(nav_history.clone(), &cx.handle()));
@@ -6098,7 +6052,7 @@ mod tests {
     #[gpui::test]
     fn test_cancel(cx: &mut gpui::MutableAppContext) {
         let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
-        let settings = EditorSettings::test(cx);
+        let settings = Settings::test(cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
         view.update(cx, |view, cx| {
@@ -6158,7 +6112,7 @@ mod tests {
             .unindent(),
             cx,
         );
-        let settings = EditorSettings::test(&cx);
+        let settings = Settings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| {
             build_editor(buffer.clone(), settings, cx)
         });
@@ -6225,7 +6179,7 @@ mod tests {
     #[gpui::test]
     fn test_move_cursor(cx: &mut gpui::MutableAppContext) {
         let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
-        let settings = EditorSettings::test(&cx);
+        let settings = Settings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| {
             build_editor(buffer.clone(), settings, cx)
         });
@@ -6301,7 +6255,7 @@ mod tests {
     #[gpui::test]
     fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) {
         let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγΓε\n", cx);
-        let settings = EditorSettings::test(&cx);
+        let settings = Settings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| {
             build_editor(buffer.clone(), settings, cx)
         });
@@ -6404,7 +6358,7 @@ mod tests {
     #[gpui::test]
     fn test_move_cursor_different_line_lengths(cx: &mut gpui::MutableAppContext) {
         let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
-        let settings = EditorSettings::test(&cx);
+        let settings = Settings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| {
             build_editor(buffer.clone(), settings, cx)
         });
@@ -6451,7 +6405,7 @@ mod tests {
     #[gpui::test]
     fn test_beginning_end_of_line(cx: &mut gpui::MutableAppContext) {
         let buffer = MultiBuffer::build_simple("abc\n  def", cx);
-        let settings = EditorSettings::test(&cx);
+        let settings = Settings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.select_display_ranges(
@@ -6592,7 +6546,7 @@ mod tests {
     #[gpui::test]
     fn test_prev_next_word_boundary(cx: &mut gpui::MutableAppContext) {
         let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n  {baz.qux()}", cx);
-        let settings = EditorSettings::test(&cx);
+        let settings = Settings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.select_display_ranges(
@@ -6730,7 +6684,7 @@ mod tests {
     #[gpui::test]
     fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut gpui::MutableAppContext) {
         let buffer = MultiBuffer::build_simple("use one::{\n    two::three::four::five\n};", cx);
-        let settings = EditorSettings::test(&cx);
+        let settings = Settings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
         view.update(cx, |view, cx| {
@@ -6783,7 +6737,7 @@ mod tests {
     #[gpui::test]
     fn test_delete_to_word_boundary(cx: &mut gpui::MutableAppContext) {
         let buffer = MultiBuffer::build_simple("one two three four", cx);
-        let settings = EditorSettings::test(&cx);
+        let settings = Settings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| {
             build_editor(buffer.clone(), settings, cx)
         });
@@ -6822,7 +6776,7 @@ mod tests {
     #[gpui::test]
     fn test_newline(cx: &mut gpui::MutableAppContext) {
         let buffer = MultiBuffer::build_simple("aaaa\n    bbbb\n", cx);
-        let settings = EditorSettings::test(&cx);
+        let settings = Settings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| {
             build_editor(buffer.clone(), settings, cx)
         });
@@ -6859,7 +6813,7 @@ mod tests {
             cx,
         );
 
-        let settings = EditorSettings::test(&cx);
+        let settings = Settings::test(&cx);
         let (_, editor) = cx.add_window(Default::default(), |cx| {
             let mut editor = build_editor(buffer.clone(), settings, cx);
             editor.select_ranges(
@@ -6931,7 +6885,7 @@ mod tests {
     fn test_insert_with_old_selections(cx: &mut gpui::MutableAppContext) {
         let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
 
-        let settings = EditorSettings::test(&cx);
+        let settings = Settings::test(&cx);
         let (_, editor) = cx.add_window(Default::default(), |cx| {
             let mut editor = build_editor(buffer.clone(), settings, cx);
             editor.select_ranges([3..4, 11..12, 19..20], None, cx);
@@ -6958,7 +6912,7 @@ mod tests {
     #[gpui::test]
     fn test_indent_outdent(cx: &mut gpui::MutableAppContext) {
         let buffer = MultiBuffer::build_simple("  one two\nthree\n four", cx);
-        let settings = EditorSettings::test(&cx);
+        let settings = Settings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| {
             build_editor(buffer.clone(), settings, cx)
         });
@@ -7035,7 +6989,7 @@ mod tests {
     fn test_backspace(cx: &mut gpui::MutableAppContext) {
         let buffer =
             MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx);
-        let settings = EditorSettings::test(&cx);
+        let settings = Settings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| {
             build_editor(buffer.clone(), settings, cx)
         });
@@ -7065,7 +7019,7 @@ mod tests {
     fn test_delete(cx: &mut gpui::MutableAppContext) {
         let buffer =
             MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx);
-        let settings = EditorSettings::test(&cx);
+        let settings = Settings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| {
             build_editor(buffer.clone(), settings, cx)
         });
@@ -7093,7 +7047,7 @@ mod tests {
 
     #[gpui::test]
     fn test_delete_line(cx: &mut gpui::MutableAppContext) {
-        let settings = EditorSettings::test(&cx);
+        let settings = Settings::test(&cx);
         let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
@@ -7116,7 +7070,7 @@ mod tests {
             );
         });
 
-        let settings = EditorSettings::test(&cx);
+        let settings = Settings::test(&cx);
         let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
@@ -7132,7 +7086,7 @@ mod tests {
 
     #[gpui::test]
     fn test_duplicate_line(cx: &mut gpui::MutableAppContext) {
-        let settings = EditorSettings::test(&cx);
+        let settings = Settings::test(&cx);
         let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
@@ -7158,7 +7112,7 @@ mod tests {
             );
         });
 
-        let settings = EditorSettings::test(&cx);
+        let settings = Settings::test(&cx);
         let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
@@ -7183,7 +7137,7 @@ mod tests {
 
     #[gpui::test]
     fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
-        let settings = EditorSettings::test(&cx);
+        let settings = Settings::test(&cx);
         let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
@@ -7279,7 +7233,7 @@ mod tests {
 
     #[gpui::test]
     fn test_move_line_up_down_with_blocks(cx: &mut gpui::MutableAppContext) {
-        let settings = EditorSettings::test(&cx);
+        let settings = Settings::test(&cx);
         let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
         let snapshot = buffer.read(cx).snapshot(cx);
         let (_, editor) =

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

@@ -1,9 +1,9 @@
 use super::{
     display_map::{BlockContext, ToDisplayPoint},
-    Anchor, DisplayPoint, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, Input,
-    Scroll, Select, SelectPhase, SoftWrap, ToPoint, MAX_LINE_LEN,
+    Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Input, Scroll, Select, SelectPhase,
+    SoftWrap, ToPoint, MAX_LINE_LEN,
 };
-use crate::display_map::TransformBlock;
+use crate::{display_map::TransformBlock, EditorStyle};
 use clock::ReplicaId;
 use collections::{BTreeMap, HashMap};
 use gpui::{
@@ -31,12 +31,12 @@ use std::{
 
 pub struct EditorElement {
     view: WeakViewHandle<Editor>,
-    settings: EditorSettings,
+    style: EditorStyle,
 }
 
 impl EditorElement {
-    pub fn new(view: WeakViewHandle<Editor>, settings: EditorSettings) -> Self {
-        Self { view, settings }
+    pub fn new(view: WeakViewHandle<Editor>, style: EditorStyle) -> Self {
+        Self { view, style }
     }
 
     fn view<'a>(&self, cx: &'a AppContext) -> &'a Editor {
@@ -209,16 +209,15 @@ impl EditorElement {
         let bounds = gutter_bounds.union_rect(text_bounds);
         let scroll_top = layout.snapshot.scroll_position().y() * layout.line_height;
         let editor = self.view(cx.app);
-        let style = &self.settings.style;
         cx.scene.push_quad(Quad {
             bounds: gutter_bounds,
-            background: Some(style.gutter_background),
+            background: Some(self.style.gutter_background),
             border: Border::new(0., Color::transparent_black()),
             corner_radius: 0.,
         });
         cx.scene.push_quad(Quad {
             bounds: text_bounds,
-            background: Some(style.background),
+            background: Some(self.style.background),
             border: Border::new(0., Color::transparent_black()),
             corner_radius: 0.,
         });
@@ -245,7 +244,7 @@ impl EditorElement {
                     );
                     cx.scene.push_quad(Quad {
                         bounds: RectF::new(origin, size),
-                        background: Some(style.active_line_background),
+                        background: Some(self.style.active_line_background),
                         border: Border::default(),
                         corner_radius: 0.,
                     });
@@ -264,7 +263,7 @@ impl EditorElement {
                 );
                 cx.scene.push_quad(Quad {
                     bounds: RectF::new(origin, size),
-                    background: Some(style.highlighted_line_background),
+                    background: Some(self.style.highlighted_line_background),
                     border: Border::default(),
                     corner_radius: 0.,
                 });
@@ -308,7 +307,7 @@ impl EditorElement {
         cx: &mut PaintContext,
     ) {
         let view = self.view(cx.app);
-        let style = &self.settings.style;
+        let style = &self.style;
         let local_replica_id = view.replica_id(cx);
         let scroll_position = layout.snapshot.scroll_position();
         let start_row = scroll_position.y() as u32;
@@ -498,7 +497,7 @@ impl EditorElement {
 
     fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &LayoutContext) -> f32 {
         let digit_count = (snapshot.max_buffer_row() as f32).log10().floor() as usize + 1;
-        let style = &self.settings.style;
+        let style = &self.style;
 
         cx.text_layout_cache
             .layout_str(
@@ -523,7 +522,7 @@ impl EditorElement {
         snapshot: &EditorSnapshot,
         cx: &LayoutContext,
     ) -> Vec<Option<text_layout::Line>> {
-        let style = &self.settings.style;
+        let style = &self.style;
         let include_line_numbers = snapshot.mode == EditorMode::Full;
         let mut line_number_layouts = Vec::with_capacity(rows.len());
         let mut line_number = String::new();
@@ -576,7 +575,11 @@ impl EditorElement {
 
         // When the editor is empty and unfocused, then show the placeholder.
         if snapshot.is_empty() && !snapshot.is_focused() {
-            let placeholder_style = self.settings.style.placeholder_text();
+            let placeholder_style = self
+                .style
+                .placeholder_text
+                .as_ref()
+                .unwrap_or_else(|| &self.style.text);
             let placeholder_text = snapshot.placeholder_text();
             let placeholder_lines = placeholder_text
                 .as_ref()
@@ -601,7 +604,7 @@ impl EditorElement {
                 })
                 .collect();
         } else {
-            let style = &self.settings.style;
+            let style = &self.style;
             let chunks = snapshot.chunks(rows.clone(), true).map(|chunk| {
                 let highlight_style = chunk
                     .highlight_id
@@ -688,10 +691,9 @@ impl EditorElement {
                         ..
                     } => {
                         if *starts_new_buffer {
-                            let style = &self.settings.style.diagnostic_path_header;
-                            let font_size = (style.text_scale_factor
-                                * self.settings.style.text.font_size)
-                                .round();
+                            let style = &self.style.diagnostic_path_header;
+                            let font_size =
+                                (style.text_scale_factor * self.style.text.font_size).round();
 
                             let mut filename = None;
                             let mut parent_path = None;
@@ -729,7 +731,7 @@ impl EditorElement {
                                 .expanded()
                                 .named("path header block")
                         } else {
-                            let text_style = self.settings.style.text.clone();
+                            let text_style = self.style.text.clone();
                             Label::new("…".to_string(), text_style)
                                 .contained()
                                 .with_padding_left(gutter_padding + scroll_x * em_width)
@@ -766,7 +768,7 @@ impl Element for EditorElement {
         }
 
         let snapshot = self.snapshot(cx.app);
-        let style = self.settings.style.clone();
+        let style = self.style.clone();
         let line_height = style.text.line_height(cx.font_cache);
 
         let gutter_padding;
@@ -786,12 +788,15 @@ impl Element for EditorElement {
         let em_width = style.text.em_width(cx.font_cache);
         let em_advance = style.text.em_advance(cx.font_cache);
         let overscroll = vec2f(em_width, 0.);
-        let wrap_width = match self.settings.soft_wrap {
-            SoftWrap::None => None,
-            SoftWrap::EditorWidth => Some(text_width - gutter_margin - overscroll.x() - em_width),
-            SoftWrap::Column(column) => Some(column as f32 * em_advance),
-        };
         let snapshot = self.update_view(cx.app, |view, cx| {
+            let wrap_width = match view.soft_wrap_mode(cx) {
+                SoftWrap::None => None,
+                SoftWrap::EditorWidth => {
+                    Some(text_width - gutter_margin - overscroll.x() - em_width)
+                }
+                SoftWrap::Column(column) => Some(column as f32 * em_advance),
+            };
+
             if view.set_wrap_width(wrap_width, cx) {
                 view.snapshot(cx)
             } else {
@@ -907,7 +912,7 @@ impl Element for EditorElement {
             }
         }
 
-        let style = self.settings.style.clone();
+        let style = self.style.clone();
         let longest_line_width = layout_line(
             snapshot.longest_row(),
             &snapshot,
@@ -951,12 +956,14 @@ impl Element for EditorElement {
                 .to_display_point(&snapshot);
 
             if (start_row..end_row).contains(&newest_selection_head.row()) {
+                let style = view.style(cx);
                 if view.context_menu_visible() {
-                    context_menu = view.render_context_menu(newest_selection_head, cx);
+                    context_menu =
+                        view.render_context_menu(newest_selection_head, style.clone(), cx);
                 }
 
                 code_actions_indicator = view
-                    .render_code_actions_indicator(cx)
+                    .render_code_actions_indicator(&style, cx)
                     .map(|indicator| (newest_selection_head.row(), indicator));
             }
         });
@@ -1370,26 +1377,19 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: f32) -> f32 {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{Editor, EditorSettings, MultiBuffer};
-    use std::sync::Arc;
+    use crate::{Editor, MultiBuffer};
+    use postage::watch;
     use util::test::sample_text;
+    use workspace::Settings;
 
     #[gpui::test]
     fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) {
-        let settings = EditorSettings::test(cx);
+        let settings = watch::channel_with(Settings::test(cx));
         let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
         let (window_id, editor) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(
-                buffer,
-                {
-                    let settings = settings.clone();
-                    Arc::new(move |_| settings.clone())
-                },
-                None,
-                cx,
-            )
+            Editor::new(EditorMode::Full, buffer, None, settings.1, None, cx)
         });
-        let element = EditorElement::new(editor.downgrade(), settings);
+        let element = EditorElement::new(editor.downgrade(), editor.read(cx).style(cx));
 
         let layouts = editor.update(cx, |editor, cx| {
             let snapshot = editor.snapshot(cx);

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

@@ -56,12 +56,11 @@ impl ItemHandle for BufferItemHandle {
         cx: &mut MutableAppContext,
     ) -> Box<dyn ItemViewHandle> {
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(self.0.clone(), cx));
-        let weak_buffer = buffer.downgrade();
         Box::new(cx.add_view(window_id, |cx| {
             let mut editor = Editor::for_buffer(
                 buffer,
-                crate::settings_builder(weak_buffer, workspace.settings()),
                 Some(workspace.project().clone()),
+                workspace.settings(),
                 cx,
             );
             editor.nav_history = Some(ItemNavHistory::new(nav_history, &cx.handle()));
@@ -101,12 +100,11 @@ impl ItemHandle for MultiBufferItemHandle {
         nav_history: Rc<RefCell<NavHistory>>,
         cx: &mut MutableAppContext,
     ) -> Box<dyn ItemViewHandle> {
-        let weak_buffer = self.0.downgrade();
         Box::new(cx.add_view(window_id, |cx| {
             let mut editor = Editor::for_buffer(
                 self.0.clone(),
-                crate::settings_builder(weak_buffer, workspace.settings()),
                 Some(workspace.project().clone()),
+                workspace.settings(),
                 cx,
             );
             editor.nav_history = Some(ItemNavHistory::new(nav_history, &cx.handle()));

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

@@ -1,4 +1,4 @@
-use editor::{Editor, EditorSettings};
+use editor::Editor;
 use fuzzy::PathMatch;
 use gpui::{
     action,
@@ -266,17 +266,8 @@ impl FileFinder {
 
         let query_editor = cx.add_view(|cx| {
             Editor::single_line(
-                {
-                    let settings = settings.clone();
-                    Arc::new(move |_| {
-                        let settings = settings.borrow();
-                        EditorSettings {
-                            style: settings.theme.selector.input_editor.as_editor(),
-                            tab_size: settings.tab_size,
-                            soft_wrap: editor::SoftWrap::None,
-                        }
-                    })
-                },
+                settings.clone(),
+                Some(|theme| theme.selector.input_editor.clone()),
                 cx,
             )
         });

crates/find/src/buffer_find.rs šŸ”—

@@ -3,8 +3,7 @@ use aho_corasick::AhoCorasickBuilder;
 use anyhow::Result;
 use collections::HashMap;
 use editor::{
-    char_kind, display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor, EditorSettings,
-    MultiBufferSnapshot,
+    char_kind, display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor, MultiBufferSnapshot,
 };
 use gpui::{
     action, elements::*, keymap::Binding, platform::CursorStyle, Entity, MutableAppContext,
@@ -16,7 +15,6 @@ use smol::future::yield_now;
 use std::{
     cmp::{self, Ordering},
     ops::Range,
-    sync::Arc,
 };
 use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace};
 
@@ -173,17 +171,8 @@ impl FindBar {
         let query_editor = cx.add_view(|cx| {
             Editor::auto_height(
                 2,
-                {
-                    let settings = settings.clone();
-                    Arc::new(move |_| {
-                        let settings = settings.borrow();
-                        EditorSettings {
-                            style: settings.theme.find.editor.input.as_editor(),
-                            tab_size: settings.tab_size,
-                            soft_wrap: editor::SoftWrap::None,
-                        }
-                    })
-                },
+                settings.clone(),
+                Some(|theme| theme.find.editor.input.clone()),
                 cx,
             )
         });
@@ -639,7 +628,7 @@ async fn regex_search(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use editor::{DisplayPoint, Editor, EditorSettings, MultiBuffer};
+    use editor::{DisplayPoint, Editor, MultiBuffer};
     use gpui::{color::Color, TestAppContext};
     use std::sync::Arc;
     use unindent::Unindent as _;
@@ -650,6 +639,7 @@ mod tests {
         let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
         theme.find.match_background = Color::red();
         let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap();
+        let settings = watch::channel_with(settings).1;
 
         let buffer = cx.update(|cx| {
             MultiBuffer::build_simple(
@@ -664,11 +654,11 @@ mod tests {
             )
         });
         let editor = cx.add_view(Default::default(), |cx| {
-            Editor::new(buffer.clone(), Arc::new(EditorSettings::test), None, cx)
+            Editor::for_buffer(buffer.clone(), None, settings.clone(), cx)
         });
 
         let find_bar = cx.add_view(Default::default(), |cx| {
-            let mut find_bar = FindBar::new(watch::channel_with(settings).1, cx);
+            let mut find_bar = FindBar::new(settings, cx);
             find_bar.active_item_changed(Some(Box::new(editor.clone())), cx);
             find_bar
         });

crates/find/src/project_find.rs šŸ”—

@@ -1,11 +1,10 @@
 use anyhow::Result;
 use editor::{Editor, MultiBuffer};
 use gpui::{
-    action, elements::*, keymap::Binding, ElementBox, Entity, Handle, ModelContext, ModelHandle,
+    action, elements::*, keymap::Binding, ElementBox, Entity, ModelContext, ModelHandle,
     MutableAppContext, Task, View, ViewContext, ViewHandle,
 };
 use project::Project;
-use std::{borrow::Borrow, sync::Arc};
 use workspace::Workspace;
 
 action!(Deploy);
@@ -75,15 +74,20 @@ impl workspace::Item for ProjectFind {
     ) -> Self::View {
         let settings = workspace.settings();
         let excerpts = model.read(cx).excerpts.clone();
-        let build_settings = editor::settings_builder(excerpts.downgrade(), workspace.settings());
         ProjectFindView {
             model,
-            query_editor: cx.add_view(|cx| Editor::single_line(build_settings.clone(), cx)),
+            query_editor: cx.add_view(|cx| {
+                Editor::single_line(
+                    settings.clone(),
+                    Some(|theme| theme.find.editor.input.clone()),
+                    cx,
+                )
+            }),
             results_editor: cx.add_view(|cx| {
                 Editor::for_buffer(
                     excerpts,
-                    build_settings,
                     Some(workspace.project().clone()),
+                    settings.clone(),
                     cx,
                 )
             }),

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

@@ -1,10 +1,9 @@
-use editor::{display_map::ToDisplayPoint, Autoscroll, DisplayPoint, Editor, EditorSettings};
+use editor::{display_map::ToDisplayPoint, Autoscroll, DisplayPoint, Editor};
 use gpui::{
     action, elements::*, geometry::vector::Vector2F, keymap::Binding, Axis, Entity,
     MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
 };
 use postage::watch;
-use std::sync::Arc;
 use text::{Bias, Point};
 use workspace::{Settings, Workspace};
 
@@ -42,17 +41,8 @@ impl GoToLine {
     ) -> Self {
         let line_editor = cx.add_view(|cx| {
             Editor::single_line(
-                {
-                    let settings = settings.clone();
-                    Arc::new(move |_| {
-                        let settings = settings.borrow();
-                        EditorSettings {
-                            tab_size: settings.tab_size,
-                            style: settings.theme.selector.input_editor.as_editor(),
-                            soft_wrap: editor::SoftWrap::None,
-                        }
-                    })
-                },
+                settings.clone(),
+                Some(|theme| theme.selector.input_editor.clone()),
                 cx,
             )
         });

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

@@ -1,6 +1,6 @@
 use editor::{
     combine_syntax_and_fuzzy_match_highlights, display_map::ToDisplayPoint, Anchor, AnchorRangeExt,
-    Autoscroll, DisplayPoint, Editor, EditorSettings, ToPoint,
+    Autoscroll, DisplayPoint, Editor, ToPoint,
 };
 use fuzzy::StringMatch;
 use gpui::{
@@ -14,10 +14,7 @@ use gpui::{
 use language::Outline;
 use ordered_float::OrderedFloat;
 use postage::watch;
-use std::{
-    cmp::{self, Reverse},
-    sync::Arc,
-};
+use std::cmp::{self, Reverse};
 use workspace::{
     menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
     Settings, Workspace,
@@ -107,17 +104,8 @@ impl OutlineView {
     ) -> Self {
         let query_editor = cx.add_view(|cx| {
             Editor::single_line(
-                {
-                    let settings = settings.clone();
-                    Arc::new(move |_| {
-                        let settings = settings.borrow();
-                        EditorSettings {
-                            style: settings.theme.selector.input_editor.as_editor(),
-                            tab_size: settings.tab_size,
-                            soft_wrap: editor::SoftWrap::None,
-                        }
-                    })
-                },
+                settings.clone(),
+                Some(|theme| theme.selector.input_editor.clone()),
                 cx,
             )
         });

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

@@ -1,6 +1,6 @@
 use editor::{
     combine_syntax_and_fuzzy_match_highlights, items::BufferItemHandle, styled_runs_for_code_label,
-    Autoscroll, Bias, Editor, EditorSettings,
+    Autoscroll, Bias, Editor,
 };
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
@@ -16,7 +16,6 @@ use project::{Project, Symbol};
 use std::{
     borrow::Cow,
     cmp::{self, Reverse},
-    sync::Arc,
 };
 use util::ResultExt;
 use workspace::{
@@ -105,17 +104,8 @@ impl ProjectSymbolsView {
     ) -> Self {
         let query_editor = cx.add_view(|cx| {
             Editor::single_line(
-                {
-                    let settings = settings.clone();
-                    Arc::new(move |_| {
-                        let settings = settings.borrow();
-                        EditorSettings {
-                            style: settings.theme.selector.input_editor.as_editor(),
-                            tab_size: settings.tab_size,
-                            soft_wrap: editor::SoftWrap::None,
-                        }
-                    })
-                },
+                settings.clone(),
+                Some(|theme| theme.selector.input_editor.clone()),
                 cx,
             )
         });

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

@@ -1177,8 +1177,8 @@ mod tests {
             EstablishConnectionError, UserStore,
         },
         editor::{
-            self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, EditorSettings,
-            Input, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions, Undo,
+            self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, MultiBuffer,
+            Redo, Rename, ToOffset, ToggleCodeActions, Undo,
         },
         fs::{FakeFs, Fs as _},
         language::{
@@ -1187,7 +1187,7 @@ mod tests {
         },
         lsp,
         project::{DiagnosticSummary, Project, ProjectPath},
-        workspace::{Workspace, WorkspaceParams},
+        workspace::{Settings, Workspace, WorkspaceParams},
     };
 
     #[cfg(test)]
@@ -1298,7 +1298,12 @@ mod tests {
             .unwrap();
 
         let editor_b = cx_b.add_view(window_b, |cx| {
-            Editor::for_buffer(buffer_b, Arc::new(|cx| EditorSettings::test(cx)), None, cx)
+            Editor::for_buffer(
+                buffer_b,
+                None,
+                watch::channel_with(Settings::test(cx)).1,
+                cx,
+            )
         });
 
         // TODO
@@ -2330,8 +2335,8 @@ mod tests {
         let editor_b = cx_b.add_view(window_b, |cx| {
             Editor::for_buffer(
                 cx.add_model(|cx| MultiBuffer::singleton(buffer_b.clone(), cx)),
-                Arc::new(|cx| EditorSettings::test(cx)),
                 Some(project_b.clone()),
+                watch::channel_with(Settings::test(cx)).1,
                 cx,
             )
         });

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

@@ -23,7 +23,7 @@ pub struct Theme {
     pub contacts_panel: ContactsPanel,
     pub project_panel: ProjectPanel,
     pub selector: Selector,
-    pub editor: EditorStyle,
+    pub editor: Editor,
     pub find: Find,
     pub project_diagnostics: ProjectDiagnostics,
 }
@@ -112,7 +112,7 @@ pub struct Find {
 #[derive(Clone, Deserialize, Default)]
 pub struct FindEditor {
     #[serde(flatten)]
-    pub input: InputEditorStyle,
+    pub input: FieldEditor,
     pub max_width: f32,
 }
 
@@ -151,7 +151,7 @@ pub struct ChatPanel {
     pub message: ChatMessage,
     pub pending_message: ChatMessage,
     pub channel_select: ChannelSelect,
-    pub input_editor: InputEditorStyle,
+    pub input_editor: FieldEditor,
     pub sign_in_prompt: TextStyle,
     pub hovered_sign_in_prompt: TextStyle,
 }
@@ -236,7 +236,7 @@ pub struct Selector {
     #[serde(flatten)]
     pub container: ContainerStyle,
     pub empty: ContainedLabel,
-    pub input_editor: InputEditorStyle,
+    pub input_editor: FieldEditor,
     pub item: ContainedLabel,
     pub active_item: ContainedLabel,
 }
@@ -269,10 +269,9 @@ pub struct ProjectDiagnostics {
 }
 
 #[derive(Clone, Deserialize, Default)]
-pub struct EditorStyle {
-    pub text: TextStyle,
+pub struct Editor {
+    pub text_color: Color,
     #[serde(default)]
-    pub placeholder_text: Option<TextStyle>,
     pub background: Color,
     pub selection: SelectionStyle,
     pub gutter_background: Color,
@@ -345,7 +344,7 @@ pub struct SelectionStyle {
 }
 
 #[derive(Clone, Deserialize, Default)]
-pub struct InputEditorStyle {
+pub struct FieldEditor {
     #[serde(flatten)]
     pub container: ContainerStyle,
     pub text: TextStyle,
@@ -354,11 +353,7 @@ pub struct InputEditorStyle {
     pub selection: SelectionStyle,
 }
 
-impl EditorStyle {
-    pub fn placeholder_text(&self) -> &TextStyle {
-        self.placeholder_text.as_ref().unwrap_or(&self.text)
-    }
-
+impl Editor {
     pub fn replica_selection_style(&self, replica_id: u16) -> &SelectionStyle {
         let style_ix = replica_id as usize % (self.guest_selections.len() + 1);
         if style_ix == 0 {
@@ -369,72 +364,6 @@ impl EditorStyle {
     }
 }
 
-impl InputEditorStyle {
-    pub fn as_editor(&self) -> EditorStyle {
-        let default_diagnostic_style = DiagnosticStyle {
-            message: self.text.clone().into(),
-            header: Default::default(),
-            text_scale_factor: 1.,
-        };
-        EditorStyle {
-            text: self.text.clone(),
-            placeholder_text: self.placeholder_text.clone(),
-            background: self
-                .container
-                .background_color
-                .unwrap_or(Color::transparent_black()),
-            selection: self.selection,
-            gutter_background: Default::default(),
-            gutter_padding_factor: Default::default(),
-            active_line_background: Default::default(),
-            highlighted_line_background: Default::default(),
-            document_highlight_read_background: Default::default(),
-            document_highlight_write_background: Default::default(),
-            diff_background_deleted: Default::default(),
-            diff_background_inserted: Default::default(),
-            line_number: Default::default(),
-            line_number_active: Default::default(),
-            guest_selections: Default::default(),
-            syntax: Default::default(),
-            diagnostic_path_header: DiagnosticPathHeader {
-                container: Default::default(),
-                filename: ContainedText {
-                    container: Default::default(),
-                    text: self.text.clone(),
-                },
-                path: ContainedText {
-                    container: Default::default(),
-                    text: self.text.clone(),
-                },
-                text_scale_factor: 1.,
-            },
-            diagnostic_header: DiagnosticHeader {
-                container: Default::default(),
-                message: ContainedLabel {
-                    container: Default::default(),
-                    label: self.text.clone().into(),
-                },
-                code: ContainedText {
-                    container: Default::default(),
-                    text: self.text.clone(),
-                },
-                icon_width_factor: Default::default(),
-                text_scale_factor: 1.,
-            },
-            error_diagnostic: default_diagnostic_style.clone(),
-            invalid_error_diagnostic: default_diagnostic_style.clone(),
-            warning_diagnostic: default_diagnostic_style.clone(),
-            invalid_warning_diagnostic: default_diagnostic_style.clone(),
-            information_diagnostic: default_diagnostic_style.clone(),
-            invalid_information_diagnostic: default_diagnostic_style.clone(),
-            hint_diagnostic: default_diagnostic_style.clone(),
-            invalid_hint_diagnostic: default_diagnostic_style.clone(),
-            autocomplete: Default::default(),
-            code_actions_indicator: Default::default(),
-        }
-    }
-}
-
 #[derive(Default)]
 pub struct SyntaxTheme {
     pub highlights: Vec<(String, HighlightStyle)>,

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

@@ -1,4 +1,4 @@
-use editor::{Editor, EditorSettings};
+use editor::Editor;
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
 use gpui::{
     action,
@@ -63,17 +63,8 @@ impl ThemeSelector {
     ) -> Self {
         let query_editor = cx.add_view(|cx| {
             Editor::single_line(
-                {
-                    let settings = settings.clone();
-                    Arc::new(move |_| {
-                        let settings = settings.borrow();
-                        EditorSettings {
-                            tab_size: settings.tab_size,
-                            style: settings.theme.selector.input_editor.as_editor(),
-                            soft_wrap: editor::SoftWrap::None,
-                        }
-                    })
-                },
+                settings.clone(),
+                Some(|theme| theme.selector.input_editor.clone()),
                 cx,
             )
         });

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

@@ -72,4 +72,17 @@ impl Settings {
             .and_then(|settings| settings.preferred_line_length)
             .unwrap_or(self.preferred_line_length)
     }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn test(cx: &gpui::AppContext) -> Settings {
+        Settings {
+            buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
+            buffer_font_size: 14.,
+            tab_size: 4,
+            soft_wrap: SoftWrap::None,
+            preferred_line_length: 80,
+            overrides: Default::default(),
+            theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
+        }
+    }
 }

crates/zed/Cargo.toml šŸ”—

@@ -92,6 +92,7 @@ time = "0.3"
 tiny_http = "0.8"
 toml = "0.5"
 tree-sitter = "0.20.4"
+tree-sitter-c = "0.20.1"
 tree-sitter-rust = "0.20.1"
 tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
 url = "2.2"

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

@@ -243,7 +243,7 @@ background = "$state.hover"
 text = "$text.0"
 
 [editor]
-text = "$text.1"
+text_color = "$text.1.color"
 background = "$surface.1"
 gutter_background = "$surface.1"
 gutter_padding_factor = 2.5
@@ -282,8 +282,8 @@ header.border = { width = 1, top = true, color = "$border.0" }
 text_scale_factor = 0.857
 
 [editor.error_diagnostic.message]
-text = { extends = "$editor.text", size = 14, color = "$status.bad" }
-highlight_text = { extends = "$editor.text", size = 14, color = "$status.bad", weight = "bold" }
+text = { extends = "$text.1", size = 14, color = "$status.bad" }
+highlight_text = { extends = "$text.1", size = 14, color = "$status.bad", weight = "bold" }
 
 [editor.warning_diagnostic]
 extends = "$editor.error_diagnostic"

crates/zed/languages/c/config.toml šŸ”—

@@ -0,0 +1,13 @@
+name = "C"
+path_suffixes = ["c", "h"]
+line_comment = "// "
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false },
+    { start = "/*", end = " */", close = true, newline = false },
+]
+
+[language_server]
+disk_based_diagnostic_sources = []

crates/zed/languages/c/highlights.scm šŸ”—

@@ -0,0 +1,101 @@
+[
+  "break"
+  "case"
+  "const"
+  "continue"
+  "default"
+  "do"
+  "else"
+  "enum"
+  "extern"
+  "for"
+  "if"
+  "inline"
+  "return"
+  "sizeof"
+  "static"
+  "struct"
+  "switch"
+  "typedef"
+  "union"
+  "volatile"
+  "while"
+] @keyword
+
+[
+  "#define"
+  "#elif"
+  "#else"
+  "#endif"
+  "#if"
+  "#ifdef"
+  "#ifndef"
+  "#include"
+  (preproc_directive)
+] @keyword
+
+[
+  "--"
+  "-"
+  "-="
+  "->"
+  "="
+  "!="
+  "*"
+  "&"
+  "&&"
+  "+"
+  "++"
+  "+="
+  "<"
+  "=="
+  ">"
+  "||"
+] @operator
+
+[
+  "."
+  ";"
+] @delimiter
+
+[
+  (string_literal)
+  (system_lib_string)
+  (char_literal)
+] @string
+
+(comment) @comment
+
+(null) @constant
+(number_literal) @number
+
+[
+  (true)
+  (false)
+  (null)
+] @constant
+
+(identifier) @variable
+
+((identifier) @constant
+ (#match? @constant "^[A-Z][A-Z\\d_]*$"))
+
+(call_expression
+  function: (identifier) @function)
+(call_expression
+  function: (field_expression
+    field: (field_identifier) @function))
+(function_declarator
+  declarator: (identifier) @function)
+(preproc_function_def
+  name: (identifier) @function.special)
+
+(field_identifier) @property
+(statement_identifier) @label
+
+[
+  (type_identifier)
+  (primitive_type)
+  (sized_type_specifier)
+] @type
+

crates/zed/languages/c/outline.scm šŸ”—

@@ -0,0 +1,30 @@
+(preproc_def
+    "#define" @context
+    name: (_) @name) @item
+
+(preproc_function_def
+    "#define" @context
+    name: (_) @name
+    parameters: (preproc_params
+        "(" @context
+        ")" @context)) @item
+
+(type_definition
+    "typedef" @context
+    declarator: (_) @name) @item
+
+(declaration
+    type: (_) @context
+    declarator: (function_declarator
+        declarator: (_) @name
+        parameters: (parameter_list
+            "(" @context
+            ")" @context))) @item
+
+(function_definition
+    type: (_) @context
+    declarator: (function_declarator
+        declarator: (_) @name
+        parameters: (parameter_list
+            "(" @context
+            ")" @context))) @item

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

@@ -1,4 +1,4 @@
-use anyhow::{anyhow, Result};
+use anyhow::{anyhow, Context, Result};
 use async_compression::futures::bufread::GzipDecoder;
 use client::http::{self, HttpClient, Method};
 use futures::{future::BoxFuture, FutureExt, StreamExt};
@@ -22,6 +22,7 @@ use util::{ResultExt, TryFutureExt};
 struct LanguageDir;
 
 struct RustLsp;
+struct CLsp;
 
 #[derive(Deserialize)]
 struct GithubRelease {
@@ -291,6 +292,135 @@ impl LspExt for RustLsp {
     }
 }
 
+impl LspExt for CLsp {
+    fn fetch_latest_server_version(
+        &self,
+        http: Arc<dyn HttpClient>,
+    ) -> BoxFuture<'static, Result<LspBinaryVersion>> {
+        async move {
+            let release = http
+                .send(
+                    surf::RequestBuilder::new(
+                        Method::Get,
+                        http::Url::parse(
+                            "https://api.github.com/repos/clangd/clangd/releases/latest",
+                        )
+                        .unwrap(),
+                    )
+                    .middleware(surf::middleware::Redirect::default())
+                    .build(),
+                )
+                .await
+                .map_err(|err| anyhow!("error fetching latest release: {}", err))?
+                .body_json::<GithubRelease>()
+                .await
+                .map_err(|err| anyhow!("error parsing latest release: {}", err))?;
+            let asset_name = format!("clangd-mac-{}.zip", release.name);
+            let asset = release
+                .assets
+                .iter()
+                .find(|asset| asset.name == asset_name)
+                .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?;
+            Ok(LspBinaryVersion {
+                name: release.name,
+                url: asset.browser_download_url.clone(),
+            })
+        }
+        .boxed()
+    }
+
+    fn fetch_server_binary(
+        &self,
+        version: LspBinaryVersion,
+        http: Arc<dyn HttpClient>,
+        download_dir: Arc<Path>,
+    ) -> BoxFuture<'static, Result<PathBuf>> {
+        async move {
+            let container_dir = download_dir.join("clangd");
+            fs::create_dir_all(&container_dir)
+                .await
+                .context("failed to create container directory")?;
+
+            let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
+            let version_dir = container_dir.join(format!("clangd_{}", version.name));
+            let binary_path = version_dir.join("bin/clangd");
+
+            if fs::metadata(&binary_path).await.is_err() {
+                let response = http
+                    .send(
+                        surf::RequestBuilder::new(Method::Get, version.url)
+                            .middleware(surf::middleware::Redirect::default())
+                            .build(),
+                    )
+                    .await
+                    .map_err(|err| anyhow!("error downloading release: {}", err))?;
+                let mut file = File::create(&zip_path).await?;
+                if !response.status().is_success() {
+                    Err(anyhow!(
+                        "download failed with status {}",
+                        response.status().to_string()
+                    ))?;
+                }
+                futures::io::copy(response, &mut file).await?;
+
+                let unzip_status = smol::process::Command::new("unzip")
+                    .current_dir(&container_dir)
+                    .arg(&zip_path)
+                    .output()
+                    .await?
+                    .status;
+                if !unzip_status.success() {
+                    Err(anyhow!("failed to unzip clangd archive"))?;
+                }
+
+                if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
+                    while let Some(entry) = entries.next().await {
+                        if let Some(entry) = entry.log_err() {
+                            let entry_path = entry.path();
+                            if entry_path.as_path() != version_dir {
+                                fs::remove_dir_all(&entry_path).await.log_err();
+                            }
+                        }
+                    }
+                }
+            }
+
+            Ok(binary_path)
+        }
+        .boxed()
+    }
+
+    fn cached_server_binary(&self, download_dir: Arc<Path>) -> BoxFuture<'static, Option<PathBuf>> {
+        async move {
+            let destination_dir_path = download_dir.join("clangd");
+            fs::create_dir_all(&destination_dir_path).await?;
+
+            let mut last_clangd_dir = None;
+            let mut entries = fs::read_dir(&destination_dir_path).await?;
+            while let Some(entry) = entries.next().await {
+                let entry = entry?;
+                if entry.file_type().await?.is_dir() {
+                    last_clangd_dir = Some(entry.path());
+                }
+            }
+            let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+            let clangd_bin = clangd_dir.join("bin/clangd");
+            if clangd_bin.exists() {
+                Ok(clangd_bin)
+            } else {
+                Err(anyhow!(
+                    "missing clangd binary in directory {:?}",
+                    clangd_dir
+                ))
+            }
+        }
+        .log_err()
+        .boxed()
+    }
+
+    fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
+}
+
 pub fn build_language_registry() -> LanguageRegistry {
     let mut languages = LanguageRegistry::new();
     languages.set_language_server_download_dir(
@@ -298,6 +428,7 @@ pub fn build_language_registry() -> LanguageRegistry {
             .expect("failed to determine home directory")
             .join(".zed"),
     );
+    languages.add(Arc::new(c()));
     languages.add(Arc::new(rust()));
     languages.add(Arc::new(markdown()));
     languages
@@ -318,6 +449,19 @@ fn rust() -> Language {
         .with_lsp_ext(RustLsp)
 }
 
+fn c() -> Language {
+    let grammar = tree_sitter_c::language();
+    let config = toml::from_slice(&LanguageDir::get("c/config.toml").unwrap().data).unwrap();
+    Language::new(config, Some(grammar))
+        .with_highlights_query(load_query("c/highlights.scm").as_ref())
+        .unwrap()
+        .with_indents_query(load_query("c/indents.scm").as_ref())
+        .unwrap()
+        .with_outline_query(load_query("c/outline.scm").as_ref())
+        .unwrap()
+        .with_lsp_ext(CLsp)
+}
+
 fn markdown() -> Language {
     let grammar = tree_sitter_markdown::language();
     let config = toml::from_slice(&LanguageDir::get("markdown/config.toml").unwrap().data).unwrap();

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

@@ -1,12 +1,12 @@
 use crate::{assets::Assets, build_window_options, build_workspace, AppState};
 use client::{test::FakeHttpClient, ChannelList, Client, UserStore};
-use gpui::{AssetSource, MutableAppContext};
+use gpui::MutableAppContext;
 use language::LanguageRegistry;
 use parking_lot::Mutex;
 use postage::watch;
 use project::fs::FakeFs;
 use std::sync::Arc;
-use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME};
+use theme::ThemeRegistry;
 use workspace::Settings;
 
 #[cfg(test)]
@@ -20,7 +20,7 @@ fn init_logger() {
 pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
     let mut path_openers = Vec::new();
     editor::init(cx, &mut path_openers);
-    let (settings_tx, settings) = watch::channel_with(build_settings(cx));
+    let (settings_tx, settings) = watch::channel_with(Settings::test(cx));
     let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
     let http = FakeHttpClient::with_404_response();
     let client = Client::new(http.clone());
@@ -48,27 +48,3 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
         build_workspace: &build_workspace,
     })
 }
-
-fn build_settings(cx: &gpui::AppContext) -> Settings {
-    lazy_static::lazy_static! {
-        static ref DEFAULT_THEME: parking_lot::Mutex<Option<Arc<Theme>>> = Default::default();
-        static ref FONTS: Vec<Arc<Vec<u8>>> = vec![
-            Assets.load("fonts/zed-sans/zed-sans-regular.ttf").unwrap().to_vec().into()
-        ];
-    }
-
-    cx.platform().fonts().add_fonts(&FONTS).unwrap();
-
-    let mut theme_guard = DEFAULT_THEME.lock();
-    let theme = if let Some(theme) = theme_guard.as_ref() {
-        theme.clone()
-    } else {
-        let theme = ThemeRegistry::new(Assets, cx.font_cache().clone())
-            .get(DEFAULT_THEME_NAME)
-            .expect("failed to load default theme in tests");
-        *theme_guard = Some(theme.clone());
-        theme
-    };
-
-    Settings::new("Zed Sans", cx.font_cache(), theme).unwrap()
-}

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

@@ -133,9 +133,11 @@ fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
 
 #[cfg(test)]
 mod tests {
+    use crate::assets::Assets;
+
     use super::*;
     use editor::{DisplayPoint, Editor};
-    use gpui::{MutableAppContext, TestAppContext, ViewHandle};
+    use gpui::{AssetSource, MutableAppContext, TestAppContext, ViewHandle};
     use project::{Fs, ProjectPath};
     use serde_json::json;
     use std::{
@@ -143,7 +145,7 @@ mod tests {
         path::{Path, PathBuf},
     };
     use test::test_app_state;
-    use theme::DEFAULT_THEME_NAME;
+    use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME};
     use util::test::temp_tree;
     use workspace::{
         open_paths, pane, ItemView, ItemViewHandle, OpenNew, Pane, SplitDirection, WorkspaceHandle,
@@ -862,10 +864,20 @@ mod tests {
 
     #[gpui::test]
     fn test_bundled_themes(cx: &mut MutableAppContext) {
-        let app_state = test_app_state(cx);
+        let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
+
+        lazy_static::lazy_static! {
+            static ref DEFAULT_THEME: parking_lot::Mutex<Option<Arc<Theme>>> = Default::default();
+            static ref FONTS: Vec<Arc<Vec<u8>>> = vec![
+                Assets.load("fonts/zed-sans/zed-sans-regular.ttf").unwrap().to_vec().into()
+            ];
+        }
+
+        cx.platform().fonts().add_fonts(&FONTS).unwrap();
+
         let mut has_default_theme = false;
-        for theme_name in app_state.themes.list() {
-            let theme = app_state.themes.get(&theme_name).unwrap();
+        for theme_name in themes.list() {
+            let theme = themes.get(&theme_name).unwrap();
             if theme.name == DEFAULT_THEME_NAME {
                 has_default_theme = true;
             }