Merge pull request #261 from zed-industries/setting-overrides

Max Brunsfeld created

Override soft-wrap settings for Markdown and Plain Text

Change summary

crates/chat_panel/src/lib.rs              |  1 
crates/editor/src/display_map.rs          |  4 
crates/editor/src/element.rs              |  8 +
crates/editor/src/items.rs                | 14 ++++
crates/editor/src/lib.rs                  | 27 +++++--
crates/file_finder/src/lib.rs             |  1 
crates/go_to_line/src/lib.rs              |  1 
crates/language/src/language.rs           | 80 ++++++++++++++++++------
crates/language/src/lib.rs                | 56 ++++++++++------
crates/language/src/tests.rs              |  2 
crates/project/src/worktree.rs            |  2 
crates/server/src/rpc.rs                  |  2 
crates/theme_selector/src/lib.rs          |  1 
crates/workspace/src/settings.rs          | 60 ++++++++++++------
crates/zed/languages/markdown/config.toml |  8 ++
crates/zed/src/language.rs                | 11 ++
crates/zed/src/lib.rs                     | 10 ++
crates/zed/src/main.rs                    | 24 ++++++-
crates/zed/src/test.rs                    |  2 
19 files changed, 226 insertions(+), 88 deletions(-)

Detailed changes

crates/chat_panel/src/lib.rs 🔗

@@ -61,6 +61,7 @@ impl ChatPanel {
                         EditorSettings {
                             tab_size: settings.tab_size,
                             style: settings.theme.chat_panel.input_editor.as_editor(),
+                            soft_wrap: editor::SoftWrap::EditorWidth,
                         }
                     }
                 },

crates/editor/src/display_map.rs 🔗

@@ -778,7 +778,7 @@ mod tests {
                     path_suffixes: vec![".test".to_string()],
                     ..Default::default()
                 },
-                tree_sitter_rust::language(),
+                Some(tree_sitter_rust::language()),
             )
             .with_highlights_query(
                 r#"
@@ -865,7 +865,7 @@ mod tests {
                     path_suffixes: vec![".test".to_string()],
                     ..Default::default()
                 },
-                tree_sitter_rust::language(),
+                Some(tree_sitter_rust::language()),
             )
             .with_highlights_query(
                 r#"

crates/editor/src/element.rs 🔗

@@ -1,6 +1,6 @@
 use super::{
     DisplayPoint, DisplayRow, Editor, EditorMode, EditorSettings, EditorStyle, Input, Scroll,
-    Select, SelectPhase, Snapshot, MAX_LINE_LEN,
+    Select, SelectPhase, Snapshot, SoftWrap, MAX_LINE_LEN,
 };
 use clock::ReplicaId;
 use gpui::{
@@ -703,7 +703,11 @@ 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 = text_width - text_offset.x() - overscroll.x() - em_width;
+        let wrap_width = match self.settings.soft_wrap {
+            SoftWrap::None => None,
+            SoftWrap::EditorWidth => Some(text_width - text_offset.x() - overscroll.x() - em_width),
+            SoftWrap::Column(column) => Some(column as f32 * em_advance),
+        };
         let snapshot = self.update_view(cx.app, |view, cx| {
             if view.set_wrap_width(wrap_width, cx) {
                 view.snapshot(cx)

crates/editor/src/items.rs 🔗

@@ -12,7 +12,8 @@ use project::{ProjectPath, Worktree};
 use std::fmt::Write;
 use std::path::Path;
 use workspace::{
-    EntryOpener, ItemHandle, ItemView, ItemViewHandle, Settings, StatusItemView, WeakItemHandle,
+    settings, EntryOpener, ItemHandle, ItemView, ItemViewHandle, Settings, StatusItemView,
+    WeakItemHandle,
 };
 
 pub struct BufferOpener;
@@ -47,6 +48,7 @@ impl ItemHandle for BufferItemHandle {
         settings: watch::Receiver<Settings>,
         cx: &mut MutableAppContext,
     ) -> Box<dyn ItemViewHandle> {
+        let buffer = self.0.downgrade();
         Box::new(cx.add_view(window_id, |cx| {
             Editor::for_buffer(
                 self.0.clone(),
@@ -71,8 +73,18 @@ impl ItemHandle for BufferItemHandle {
                         font_properties,
                         underline: None,
                     };
+                    let language = buffer.upgrade(cx).and_then(|buf| buf.read(cx).language());
+                    let soft_wrap = match settings.soft_wrap(language) {
+                        settings::SoftWrap::None => crate::SoftWrap::None,
+                        settings::SoftWrap::EditorWidth => crate::SoftWrap::EditorWidth,
+                        settings::SoftWrap::PreferredLineLength => crate::SoftWrap::Column(
+                            settings.preferred_line_length(language).saturating_sub(1),
+                        ),
+                    };
+
                     EditorSettings {
                         tab_size: settings.tab_size,
+                        soft_wrap,
                         style: theme,
                     }
                 },

crates/editor/src/lib.rs 🔗

@@ -333,9 +333,17 @@ pub enum EditorMode {
 #[derive(Clone)]
 pub struct EditorSettings {
     pub tab_size: usize,
+    pub soft_wrap: SoftWrap,
     pub style: EditorStyle,
 }
 
+#[derive(Clone)]
+pub enum SoftWrap {
+    None,
+    EditorWidth,
+    Column(u32),
+}
+
 pub struct Editor {
     handle: WeakViewHandle<Self>,
     buffer: ModelHandle<Buffer>,
@@ -510,7 +518,9 @@ impl Editor {
         _: &workspace::OpenNew,
         cx: &mut ViewContext<Workspace>,
     ) {
-        let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
+        let buffer = cx.add_model(|cx| {
+            Buffer::new(0, "", cx).with_language(Some(language::PLAIN_TEXT.clone()), None, cx)
+        });
         workspace.add_item(BufferItemHandle(buffer), cx);
     }
 
@@ -3379,9 +3389,9 @@ impl Editor {
             .text()
     }
 
-    pub fn set_wrap_width(&self, width: f32, cx: &mut MutableAppContext) -> bool {
+    pub fn set_wrap_width(&self, width: Option<f32>, cx: &mut MutableAppContext) -> bool {
         self.display_map
-            .update(cx, |map, cx| map.set_wrap_width(Some(width), cx))
+            .update(cx, |map, cx| map.set_wrap_width(width, cx))
     }
 
     pub fn set_highlighted_row(&mut self, row: Option<u32>) {
@@ -3539,6 +3549,7 @@ impl EditorSettings {
     pub fn test(cx: &AppContext) -> Self {
         Self {
             tab_size: 4,
+            soft_wrap: SoftWrap::None,
             style: {
                 let font_cache: &gpui::FontCache = cx.font_cache();
                 let font_family_name = Arc::from("Monaco");
@@ -4409,7 +4420,7 @@ mod tests {
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
         view.update(cx, |view, cx| {
-            view.set_wrap_width(140., cx);
+            view.set_wrap_width(Some(140.), cx);
             assert_eq!(
                 view.display_text(cx),
                 "use one::{\n    two::three::\n    four::five\n};"
@@ -5286,7 +5297,7 @@ mod tests {
         let settings = cx.read(EditorSettings::test);
         let language = Some(Arc::new(Language::new(
             LanguageConfig::default(),
-            tree_sitter_rust::language(),
+            Some(tree_sitter_rust::language()),
         )));
 
         let text = r#"
@@ -5443,7 +5454,7 @@ mod tests {
                 ],
                 ..Default::default()
             },
-            tree_sitter_rust::language(),
+            Some(tree_sitter_rust::language()),
         )));
 
         let text = r#"
@@ -5542,7 +5553,7 @@ mod tests {
                 line_comment: Some("// ".to_string()),
                 ..Default::default()
             },
-            tree_sitter_rust::language(),
+            Some(tree_sitter_rust::language()),
         )));
 
         let text = "
@@ -5640,7 +5651,7 @@ mod tests {
                 ],
                 ..Default::default()
             },
-            tree_sitter_rust::language(),
+            Some(tree_sitter_rust::language()),
         )));
 
         let text = concat!(

crates/file_finder/src/lib.rs 🔗

@@ -275,6 +275,7 @@ impl FileFinder {
                         EditorSettings {
                             style: settings.theme.selector.input_editor.as_editor(),
                             tab_size: settings.tab_size,
+                            soft_wrap: editor::SoftWrap::None,
                         }
                     }
                 },

crates/go_to_line/src/lib.rs 🔗

@@ -54,6 +54,7 @@ impl GoToLine {
                         EditorSettings {
                             tab_size: settings.tab_size,
                             style: settings.theme.selector.input_editor.as_editor(),
+                            soft_wrap: editor::SoftWrap::None,
                         }
                     }
                 },

crates/language/src/language.rs 🔗

@@ -1,14 +1,28 @@
 use crate::HighlightMap;
-use anyhow::Result;
+use anyhow::{anyhow, Result};
 use gpui::{executor::Background, AppContext};
+use lazy_static::lazy_static;
 use lsp::LanguageServer;
 use parking_lot::Mutex;
 use serde::Deserialize;
 use std::{collections::HashSet, path::Path, str, sync::Arc};
 use theme::SyntaxTheme;
-use tree_sitter::{Language as Grammar, Query};
+use tree_sitter::{self, Query};
 pub use tree_sitter::{Parser, Tree};
 
+lazy_static! {
+    pub static ref PLAIN_TEXT: Arc<Language> = Arc::new(Language::new(
+        LanguageConfig {
+            name: "Plain Text".to_string(),
+            path_suffixes: Default::default(),
+            brackets: Default::default(),
+            line_comment: None,
+            language_server: None,
+        },
+        None,
+    ));
+}
+
 #[derive(Default, Deserialize)]
 pub struct LanguageConfig {
     pub name: String,
@@ -37,7 +51,11 @@ pub struct BracketPair {
 
 pub struct Language {
     pub(crate) config: LanguageConfig,
-    pub(crate) grammar: Grammar,
+    pub(crate) grammar: Option<Arc<Grammar>>,
+}
+
+pub struct Grammar {
+    pub(crate) ts_language: tree_sitter::Language,
     pub(crate) highlights_query: Query,
     pub(crate) brackets_query: Query,
     pub(crate) indents_query: Query,
@@ -86,29 +104,48 @@ impl LanguageRegistry {
 }
 
 impl Language {
-    pub fn new(config: LanguageConfig, grammar: Grammar) -> Self {
+    pub fn new(config: LanguageConfig, ts_language: Option<tree_sitter::Language>) -> Self {
         Self {
             config,
-            brackets_query: Query::new(grammar, "").unwrap(),
-            highlights_query: Query::new(grammar, "").unwrap(),
-            indents_query: Query::new(grammar, "").unwrap(),
-            grammar,
-            highlight_map: Default::default(),
+            grammar: ts_language.map(|ts_language| {
+                Arc::new(Grammar {
+                    brackets_query: Query::new(ts_language, "").unwrap(),
+                    highlights_query: Query::new(ts_language, "").unwrap(),
+                    indents_query: Query::new(ts_language, "").unwrap(),
+                    ts_language,
+                    highlight_map: Default::default(),
+                })
+            }),
         }
     }
 
     pub fn with_highlights_query(mut self, source: &str) -> Result<Self> {
-        self.highlights_query = Query::new(self.grammar, source)?;
+        let grammar = self
+            .grammar
+            .as_mut()
+            .and_then(Arc::get_mut)
+            .ok_or_else(|| anyhow!("grammar does not exist or is already being used"))?;
+        grammar.highlights_query = Query::new(grammar.ts_language, source)?;
         Ok(self)
     }
 
     pub fn with_brackets_query(mut self, source: &str) -> Result<Self> {
-        self.brackets_query = Query::new(self.grammar, source)?;
+        let grammar = self
+            .grammar
+            .as_mut()
+            .and_then(Arc::get_mut)
+            .ok_or_else(|| anyhow!("grammar does not exist or is already being used"))?;
+        grammar.brackets_query = Query::new(grammar.ts_language, source)?;
         Ok(self)
     }
 
     pub fn with_indents_query(mut self, source: &str) -> Result<Self> {
-        self.indents_query = Query::new(self.grammar, source)?;
+        let grammar = self
+            .grammar
+            .as_mut()
+            .and_then(Arc::get_mut)
+            .ok_or_else(|| anyhow!("grammar does not exist or is already being used"))?;
+        grammar.indents_query = Query::new(grammar.ts_language, source)?;
         Ok(self)
     }
 
@@ -156,13 +193,17 @@ impl Language {
         &self.config.brackets
     }
 
-    pub fn highlight_map(&self) -> HighlightMap {
-        self.highlight_map.lock().clone()
+    pub fn set_theme(&self, theme: &SyntaxTheme) {
+        if let Some(grammar) = self.grammar.as_ref() {
+            *grammar.highlight_map.lock() =
+                HighlightMap::new(grammar.highlights_query.capture_names(), theme);
+        }
     }
+}
 
-    pub fn set_theme(&self, theme: &SyntaxTheme) {
-        *self.highlight_map.lock() =
-            HighlightMap::new(self.highlights_query.capture_names(), theme);
+impl Grammar {
+    pub fn highlight_map(&self) -> HighlightMap {
+        self.highlight_map.lock().clone()
     }
 }
 
@@ -189,7 +230,6 @@ mod tests {
 
     #[test]
     fn test_select_language() {
-        let grammar = tree_sitter_rust::language();
         let registry = LanguageRegistry {
             languages: vec![
                 Arc::new(Language::new(
@@ -198,7 +238,7 @@ mod tests {
                         path_suffixes: vec!["rs".to_string()],
                         ..Default::default()
                     },
-                    grammar,
+                    Some(tree_sitter_rust::language()),
                 )),
                 Arc::new(Language::new(
                     LanguageConfig {
@@ -206,7 +246,7 @@ mod tests {
                         path_suffixes: vec!["Makefile".to_string(), "mk".to_string()],
                         ..Default::default()
                     },
-                    grammar,
+                    Some(tree_sitter_rust::language()),
                 )),
             ],
         };

crates/language/src/lib.rs 🔗

@@ -6,7 +6,10 @@ mod tests;
 
 pub use self::{
     highlight_map::{HighlightId, HighlightMap},
-    language::{BracketPair, Language, LanguageConfig, LanguageRegistry, LanguageServerConfig},
+    language::{
+        BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, LanguageServerConfig,
+        PLAIN_TEXT,
+    },
 };
 use anyhow::{anyhow, Result};
 pub use buffer::{Buffer as TextBuffer, Operation as _, *};
@@ -594,13 +597,13 @@ impl Buffer {
             return false;
         }
 
-        if let Some(language) = self.language.clone() {
+        if let Some(grammar) = self.grammar().cloned() {
             let old_tree = self.syntax_tree();
             let text = self.as_rope().clone();
             let parsed_version = self.version();
             let parse_task = cx.background().spawn({
-                let language = language.clone();
-                async move { Self::parse_text(&text, old_tree, &language) }
+                let grammar = grammar.clone();
+                async move { Self::parse_text(&text, old_tree, &grammar) }
             });
 
             match cx
@@ -616,11 +619,10 @@ impl Buffer {
                     cx.spawn(move |this, mut cx| async move {
                         let new_tree = parse_task.await;
                         this.update(&mut cx, move |this, cx| {
-                            let language_changed =
-                                this.language.as_ref().map_or(true, |curr_language| {
-                                    !Arc::ptr_eq(curr_language, &language)
-                                });
-                            let parse_again = this.version.gt(&parsed_version) || language_changed;
+                            let grammar_changed = this
+                                .grammar()
+                                .map_or(true, |curr_grammar| !Arc::ptr_eq(&grammar, curr_grammar));
+                            let parse_again = this.version.gt(&parsed_version) || grammar_changed;
                             this.parsing_in_background = false;
                             this.did_finish_parsing(new_tree, parsed_version, cx);
 
@@ -636,11 +638,11 @@ impl Buffer {
         false
     }
 
-    fn parse_text(text: &Rope, old_tree: Option<Tree>, language: &Language) -> Tree {
+    fn parse_text(text: &Rope, old_tree: Option<Tree>, grammar: &Grammar) -> Tree {
         PARSER.with(|parser| {
             let mut parser = parser.borrow_mut();
             parser
-                .set_language(language.grammar)
+                .set_language(grammar.ts_language)
                 .expect("incompatible grammar");
             let mut chunks = text.chunks_in_range(0..text.len());
             let tree = parser
@@ -1069,15 +1071,15 @@ impl Buffer {
         &self,
         range: Range<T>,
     ) -> Option<(Range<usize>, Range<usize>)> {
-        let (lang, tree) = self.language.as_ref().zip(self.syntax_tree())?;
-        let open_capture_ix = lang.brackets_query.capture_index_for_name("open")?;
-        let close_capture_ix = lang.brackets_query.capture_index_for_name("close")?;
+        let (grammar, tree) = self.grammar().zip(self.syntax_tree())?;
+        let open_capture_ix = grammar.brackets_query.capture_index_for_name("open")?;
+        let close_capture_ix = grammar.brackets_query.capture_index_for_name("close")?;
 
         // Find bracket pairs that *inclusively* contain the given range.
         let range = range.start.to_offset(self).saturating_sub(1)..range.end.to_offset(self) + 1;
         let mut cursor = QueryCursorHandle::new();
         let matches = cursor.set_byte_range(range).matches(
-            &lang.brackets_query,
+            &grammar.brackets_query,
             tree.root_node(),
             TextProvider(self.as_rope()),
         );
@@ -1342,6 +1344,10 @@ impl Buffer {
         cx.notify();
     }
 
+    fn grammar(&self) -> Option<&Arc<Grammar>> {
+        self.language.as_ref().and_then(|l| l.grammar.as_ref())
+    }
+
     pub fn add_selection_set<T: ToOffset>(
         &mut self,
         selections: &[Selection<T>],
@@ -1550,19 +1556,19 @@ impl Snapshot {
         row_range: Range<u32>,
     ) -> Option<impl Iterator<Item = IndentSuggestion> + 'a> {
         let mut query_cursor = QueryCursorHandle::new();
-        if let Some((language, tree)) = self.language.as_ref().zip(self.tree.as_ref()) {
+        if let Some((grammar, tree)) = self.grammar().zip(self.tree.as_ref()) {
             let prev_non_blank_row = self.prev_non_blank_row(row_range.start);
 
             // Get the "indentation ranges" that intersect this row range.
-            let indent_capture_ix = language.indents_query.capture_index_for_name("indent");
-            let end_capture_ix = language.indents_query.capture_index_for_name("end");
+            let indent_capture_ix = grammar.indents_query.capture_index_for_name("indent");
+            let end_capture_ix = grammar.indents_query.capture_index_for_name("end");
             query_cursor.set_point_range(
                 Point::new(prev_non_blank_row.unwrap_or(row_range.start), 0).to_ts_point()
                     ..Point::new(row_range.end, 0).to_ts_point(),
             );
             let mut indentation_ranges = Vec::<(Range<Point>, &'static str)>::new();
             for mat in query_cursor.matches(
-                &language.indents_query,
+                &grammar.indents_query,
                 tree.root_node(),
                 TextProvider(self.as_rope()),
             ) {
@@ -1682,7 +1688,7 @@ impl Snapshot {
             diagnostic_endpoints
                 .sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start));
 
-            if let Some((language, tree)) = self.language.as_ref().zip(self.tree.as_ref()) {
+            if let Some((grammar, tree)) = self.grammar().zip(self.tree.as_ref()) {
                 let mut query_cursor = QueryCursorHandle::new();
 
                 // TODO - add a Tree-sitter API to remove the need for this.
@@ -1690,7 +1696,7 @@ impl Snapshot {
                     std::mem::transmute::<_, &'static mut QueryCursor>(query_cursor.deref_mut())
                 };
                 let captures = cursor.set_byte_range(range.clone()).captures(
-                    &language.highlights_query,
+                    &grammar.highlights_query,
                     tree.root_node(),
                     TextProvider(self.text.as_rope()),
                 );
@@ -1698,7 +1704,7 @@ impl Snapshot {
                     captures,
                     next_capture: None,
                     stack: Default::default(),
-                    highlight_map: language.highlight_map(),
+                    highlight_map: grammar.highlight_map(),
                     _query_cursor: query_cursor,
                     theme,
                 })
@@ -1719,6 +1725,12 @@ impl Snapshot {
             highlights,
         }
     }
+
+    fn grammar(&self) -> Option<&Arc<Grammar>> {
+        self.language
+            .as_ref()
+            .and_then(|language| language.grammar.as_ref())
+    }
 }
 
 impl Clone for Snapshot {

crates/language/src/tests.rs 🔗

@@ -970,7 +970,7 @@ fn rust_lang() -> Language {
             language_server: None,
             ..Default::default()
         },
-        tree_sitter_rust::language(),
+        Some(tree_sitter_rust::language()),
     )
     .with_indents_query(
         r#"

crates/project/src/worktree.rs 🔗

@@ -3671,7 +3671,7 @@ mod tests {
                 language_server: Some(language_server_config),
                 ..Default::default()
             },
-            tree_sitter_rust::language(),
+            Some(tree_sitter_rust::language()),
         )));
 
         let dir = temp_tree(json!({

crates/server/src/rpc.rs 🔗

@@ -1616,7 +1616,7 @@ mod tests {
                 language_server: Some(language_server_config),
                 ..Default::default()
             },
-            tree_sitter_rust::language(),
+            Some(tree_sitter_rust::language()),
         )));
 
         let lang_registry = Arc::new(lang_registry);

crates/theme_selector/src/lib.rs 🔗

@@ -69,6 +69,7 @@ impl ThemeSelector {
                         EditorSettings {
                             tab_size: settings.tab_size,
                             style: settings.theme.selector.input_editor.as_editor(),
+                            soft_wrap: editor::SoftWrap::None,
                         }
                     }
                 },

crates/workspace/src/settings.rs 🔗

@@ -1,17 +1,33 @@
 use anyhow::Result;
 use gpui::font_cache::{FamilyId, FontCache};
-use postage::watch;
-use std::sync::Arc;
-use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME};
+use language::Language;
+use std::{collections::HashMap, sync::Arc};
+use theme::Theme;
 
 #[derive(Clone)]
 pub struct Settings {
     pub buffer_font_family: FamilyId,
     pub buffer_font_size: f32,
     pub tab_size: usize,
+    soft_wrap: SoftWrap,
+    preferred_line_length: u32,
+    overrides: HashMap<String, Override>,
     pub theme: Arc<Theme>,
 }
 
+#[derive(Clone, Default)]
+pub struct Override {
+    pub soft_wrap: Option<SoftWrap>,
+    pub preferred_line_length: Option<u32>,
+}
+
+#[derive(Copy, Clone)]
+pub enum SoftWrap {
+    None,
+    EditorWidth,
+    PreferredLineLength,
+}
+
 impl Settings {
     pub fn new(
         buffer_font_family: &str,
@@ -22,6 +38,9 @@ impl Settings {
             buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
             buffer_font_size: 16.,
             tab_size: 4,
+            soft_wrap: SoftWrap::None,
+            preferred_line_length: 80,
+            overrides: Default::default(),
             theme,
         })
     }
@@ -30,22 +49,23 @@ impl Settings {
         self.tab_size = tab_size;
         self
     }
-}
 
-pub fn channel(
-    buffer_font_family: &str,
-    font_cache: &FontCache,
-    themes: &ThemeRegistry,
-) -> Result<(watch::Sender<Settings>, watch::Receiver<Settings>)> {
-    let theme = match themes.get(DEFAULT_THEME_NAME) {
-        Ok(theme) => theme,
-        Err(err) => {
-            panic!("failed to deserialize default theme: {:?}", err)
-        }
-    };
-    Ok(watch::channel_with(Settings::new(
-        buffer_font_family,
-        font_cache,
-        theme,
-    )?))
+    pub fn with_overrides(mut self, language_name: impl Into<String>, overrides: Override) -> Self {
+        self.overrides.insert(language_name.into(), overrides);
+        self
+    }
+
+    pub fn soft_wrap(&self, language: Option<&Arc<Language>>) -> SoftWrap {
+        language
+            .and_then(|language| self.overrides.get(language.name()))
+            .and_then(|settings| settings.soft_wrap)
+            .unwrap_or(self.soft_wrap)
+    }
+
+    pub fn preferred_line_length(&self, language: Option<&Arc<Language>>) -> u32 {
+        language
+            .and_then(|language| self.overrides.get(language.name()))
+            .and_then(|settings| settings.preferred_line_length)
+            .unwrap_or(self.preferred_line_length)
+    }
 }

crates/zed/languages/markdown/config.toml 🔗

@@ -0,0 +1,8 @@
+name = "Markdown"
+path_suffixes = ["md"]
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "<", end = ">", close = true, newline = true },
+]

crates/zed/src/language.rs 🔗

@@ -10,14 +10,14 @@ struct LanguageDir;
 pub fn build_language_registry() -> LanguageRegistry {
     let mut languages = LanguageRegistry::default();
     languages.add(Arc::new(rust()));
+    languages.add(Arc::new(markdown()));
     languages
 }
 
 fn rust() -> Language {
     let grammar = tree_sitter_rust::language();
-    let rust_config =
-        toml::from_slice(&LanguageDir::get("rust/config.toml").unwrap().data).unwrap();
-    Language::new(rust_config, grammar)
+    let config = toml::from_slice(&LanguageDir::get("rust/config.toml").unwrap().data).unwrap();
+    Language::new(config, Some(grammar))
         .with_highlights_query(load_query("rust/highlights.scm").as_ref())
         .unwrap()
         .with_brackets_query(load_query("rust/brackets.scm").as_ref())
@@ -26,6 +26,11 @@ fn rust() -> Language {
         .unwrap()
 }
 
+fn markdown() -> Language {
+    let config = toml::from_slice(&LanguageDir::get("markdown/config.toml").unwrap().data).unwrap();
+    Language::new(config, None)
+}
+
 fn load_query(path: &str) -> Cow<'static, str> {
     match LanguageDir::get(path).unwrap().data {
         Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),

crates/zed/src/lib.rs 🔗

@@ -589,7 +589,10 @@ mod tests {
         editor.update(&mut cx, |editor, cx| {
             assert!(!editor.is_dirty(cx.as_ref()));
             assert_eq!(editor.title(cx.as_ref()), "untitled");
-            assert!(editor.language(cx).is_none());
+            assert!(Arc::ptr_eq(
+                editor.language(cx).unwrap(),
+                &language::PLAIN_TEXT
+            ));
             editor.handle_input(&editor::Input("hi".into()), cx);
             assert!(editor.is_dirty(cx.as_ref()));
         });
@@ -683,7 +686,10 @@ mod tests {
         });
 
         editor.update(&mut cx, |editor, cx| {
-            assert!(editor.language(cx).is_none());
+            assert!(Arc::ptr_eq(
+                editor.language(cx).unwrap(),
+                &language::PLAIN_TEXT
+            ));
             editor.handle_input(&editor::Input("hi".into()), cx);
             assert!(editor.is_dirty(cx.as_ref()));
         });

crates/zed/src/main.rs 🔗

@@ -8,8 +8,8 @@ use log::LevelFilter;
 use parking_lot::Mutex;
 use simplelog::SimpleLogger;
 use std::{fs, path::PathBuf, sync::Arc};
-use theme::ThemeRegistry;
-use workspace::{self, settings, OpenNew};
+use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
+use workspace::{self, settings, OpenNew, Settings};
 use zed::{self, assets::Assets, fs::RealFs, language, menus, AppState, OpenParams, OpenPaths};
 
 fn main() {
@@ -24,8 +24,24 @@ fn main() {
     app.platform().fonts().add_fonts(&embedded_fonts).unwrap();
 
     let themes = ThemeRegistry::new(Assets, app.font_cache());
-    let (settings_tx, settings) =
-        settings::channel("Inconsolata", &app.font_cache(), &themes).unwrap();
+    let theme = themes.get(DEFAULT_THEME_NAME).unwrap();
+    let settings = Settings::new("Inconsolata", &app.font_cache(), theme)
+        .unwrap()
+        .with_overrides(
+            language::PLAIN_TEXT.name(),
+            settings::Override {
+                soft_wrap: Some(settings::SoftWrap::PreferredLineLength),
+                ..Default::default()
+            },
+        )
+        .with_overrides(
+            "Markdown",
+            settings::Override {
+                soft_wrap: Some(settings::SoftWrap::PreferredLineLength),
+                ..Default::default()
+            },
+        );
+    let (settings_tx, settings) = postage::watch::channel_with(settings);
     let languages = Arc::new(language::build_language_registry());
     languages.set_theme(&settings.borrow().theme.editor.syntax);
 

crates/zed/src/test.rs 🔗

@@ -30,7 +30,7 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
             path_suffixes: vec!["rs".to_string()],
             ..Default::default()
         },
-        tree_sitter_rust::language(),
+        Some(tree_sitter_rust::language()),
     )));
     Arc::new(AppState {
         settings_tx: Arc::new(Mutex::new(settings_tx)),