From 83f98dde52035aec9f2b497ec1fc9e3c2b78217a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 9 Mar 2022 16:13:16 -0800 Subject: [PATCH 01/14] Start work on loading settings from a file Co-Authored-By: Keith Simmons --- Cargo.lock | 1 + crates/workspace/Cargo.toml | 3 +- crates/workspace/src/settings.rs | 274 ++++++++++++++++++++++++++++--- crates/zed/src/main.rs | 4 +- 4 files changed, 259 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7aafce3bbb00774249f6ae29e8bbeed1fda559bb..2a50f76502942b8af6cc539bba0bc0b61a2cbcc7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5851,6 +5851,7 @@ dependencies = [ "parking_lot", "postage", "project", + "serde", "serde_json", "theme", "util", diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index d83cbf29d4f8f64c8666e19323b8fd305bc1cae8..b987856450d6475119cc533671a68a30fe3ebaa0 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -24,9 +24,10 @@ futures = "0.3" log = "0.4" parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } +serde = { version = "1", features = ["derive", "rc"] } +serde_json = { version = "1", features = ["preserve_order"] } [dev-dependencies] client = { path = "../client", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } -serde_json = { version = "1.0.64", features = ["preserve_order"] } diff --git a/crates/workspace/src/settings.rs b/crates/workspace/src/settings.rs index 242ecbbce2a765670e02df12aec166dcbdc1a5b8..62a6d4af8c910e832033245d3920aa8d12c24cf5 100644 --- a/crates/workspace/src/settings.rs +++ b/crates/workspace/src/settings.rs @@ -1,34 +1,128 @@ use anyhow::Result; -use gpui::font_cache::{FamilyId, FontCache}; +use futures::{stream, SinkExt, StreamExt as _}; +use gpui::{ + executor, + font_cache::{FamilyId, FontCache}, +}; use language::Language; -use std::{collections::HashMap, sync::Arc}; -use theme::Theme; +use parking_lot::Mutex; +use postage::{prelude::Stream, watch}; +use project::Fs; +use serde::Deserialize; +use std::{collections::HashMap, path::Path, sync::Arc, time::Duration}; +use theme::{Theme, ThemeRegistry}; +use util::ResultExt; #[derive(Clone)] pub struct Settings { pub buffer_font_family: FamilyId, pub buffer_font_size: f32, pub tab_size: usize, - soft_wrap: SoftWrap, - preferred_line_length: u32, - overrides: HashMap, Override>, + pub soft_wrap: SoftWrap, + pub preferred_line_length: u32, + pub language_overrides: HashMap, LanguageOverride>, pub theme: Arc, } -#[derive(Clone, Default)] -pub struct Override { +#[derive(Clone, Debug, Default, Deserialize)] +pub struct LanguageOverride { + pub tab_size: Option, pub soft_wrap: Option, pub preferred_line_length: Option, } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] pub enum SoftWrap { None, EditorWidth, PreferredLineLength, } +#[derive(Clone)] +pub struct SettingsFile(watch::Receiver); + +#[derive(Clone, Debug, Default, Deserialize)] +struct SettingsFileContent { + #[serde(default)] + buffer_font_family: Option, + #[serde(default)] + buffer_font_size: Option, + #[serde(flatten)] + editor: LanguageOverride, + #[serde(default)] + language_overrides: HashMap, LanguageOverride>, + #[serde(default)] + theme: Option, +} + +impl SettingsFile { + pub async fn new( + fs: Arc, + executor: &executor::Background, + path: impl Into>, + ) -> Self { + let path = path.into(); + let settings = Self::load(fs.clone(), &path).await.unwrap_or_default(); + let mut events = fs.watch(&path, Duration::from_millis(500)).await; + let (mut tx, mut rx) = watch::channel_with(settings); + rx.recv().await; + executor + .spawn(async move { + while events.next().await.is_some() { + if let Some(settings) = Self::load(fs.clone(), &path).await { + if tx.send(settings).await.is_err() { + break; + } + } + } + }) + .detach(); + Self(rx) + } + + async fn load(fs: Arc, path: &Path) -> Option { + if fs.is_file(&path).await { + fs.load(&path) + .await + .log_err() + .and_then(|data| serde_json::from_str(&data).log_err()) + } else { + Some(SettingsFileContent::default()) + } + } +} + impl Settings { + pub fn from_files( + defaults: Self, + sources: Vec, + executor: Arc, + theme_registry: Arc, + font_cache: Arc, + ) -> (Arc>>, watch::Receiver) { + let (tx, mut rx) = watch::channel_with(defaults.clone()); + let tx = Arc::new(Mutex::new(tx)); + executor + .spawn({ + let tx = tx.clone(); + async move { + let mut stream = + stream::select_all(sources.iter().map(|source| source.0.clone())); + while stream.next().await.is_some() { + let mut settings = defaults.clone(); + for source in &sources { + settings.merge(&*source.0.borrow(), &theme_registry, &font_cache); + } + *tx.lock().borrow_mut() = settings; + } + } + }) + .detach(); + rx.try_recv().ok(); + (tx, rx) + } + pub fn new( buffer_font_family: &str, font_cache: &FontCache, @@ -40,35 +134,38 @@ impl Settings { tab_size: 4, soft_wrap: SoftWrap::None, preferred_line_length: 80, - overrides: Default::default(), + language_overrides: Default::default(), theme, }) } - pub fn with_tab_size(mut self, tab_size: usize) -> Self { - self.tab_size = tab_size; - self - } - pub fn with_overrides( mut self, language_name: impl Into>, - overrides: Override, + overrides: LanguageOverride, ) -> Self { - self.overrides.insert(language_name.into(), overrides); + self.language_overrides + .insert(language_name.into(), overrides); self } + pub fn tab_size(&self, language: Option<&Arc>) -> usize { + language + .and_then(|language| self.language_overrides.get(language.name().as_ref())) + .and_then(|settings| settings.tab_size) + .unwrap_or(self.tab_size) + } + pub fn soft_wrap(&self, language: Option<&Arc>) -> SoftWrap { language - .and_then(|language| self.overrides.get(language.name().as_ref())) + .and_then(|language| self.language_overrides.get(language.name().as_ref())) .and_then(|settings| settings.soft_wrap) .unwrap_or(self.soft_wrap) } pub fn preferred_line_length(&self, language: Option<&Arc>) -> u32 { language - .and_then(|language| self.overrides.get(language.name().as_ref())) + .and_then(|language| self.language_overrides.get(language.name().as_ref())) .and_then(|settings| settings.preferred_line_length) .unwrap_or(self.preferred_line_length) } @@ -81,8 +178,145 @@ impl Settings { tab_size: 4, soft_wrap: SoftWrap::None, preferred_line_length: 80, - overrides: Default::default(), + language_overrides: Default::default(), theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()), } } + + fn merge( + &mut self, + data: &SettingsFileContent, + theme_registry: &ThemeRegistry, + font_cache: &FontCache, + ) { + if let Some(value) = &data.buffer_font_family { + if let Some(id) = font_cache.load_family(&[value]).log_err() { + self.buffer_font_family = id; + } + } + if let Some(value) = &data.theme { + if let Some(theme) = theme_registry.get(value).log_err() { + self.theme = theme; + } + } + + merge(&mut self.buffer_font_size, data.buffer_font_size); + merge(&mut self.soft_wrap, data.editor.soft_wrap); + merge(&mut self.tab_size, data.editor.tab_size); + merge( + &mut self.preferred_line_length, + data.editor.preferred_line_length, + ); + + for (language_name, settings) in &data.language_overrides { + let target = self + .language_overrides + .entry(language_name.clone()) + .or_default(); + + merge_option(&mut target.tab_size, settings.tab_size); + merge_option(&mut target.soft_wrap, settings.soft_wrap); + merge_option( + &mut target.preferred_line_length, + settings.preferred_line_length, + ); + } + } +} + +fn merge(target: &mut T, value: Option) { + if let Some(value) = value { + *target = value; + } +} + +fn merge_option(target: &mut Option, value: Option) { + if value.is_some() { + *target = value; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use postage::prelude::Stream; + use project::FakeFs; + + #[gpui::test] + async fn test_settings_from_files(cx: &mut gpui::TestAppContext) { + let executor = cx.background(); + let fs = FakeFs::new(executor.clone()); + + fs.save( + "/settings1.json".as_ref(), + &r#" + { + "buffer_font_size": 24, + "soft_wrap": "editor_width", + "language_overrides": { + "Markdown": { + "preferred_line_length": 100, + "soft_wrap": "preferred_line_length" + } + } + } + "# + .into(), + ) + .await + .unwrap(); + + let source1 = SettingsFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await; + let source2 = SettingsFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await; + let source3 = SettingsFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await; + + let (_, mut settings_rx) = Settings::from_files( + cx.read(Settings::test), + vec![source1, source2, source3], + cx.background(), + ThemeRegistry::new((), cx.font_cache()), + cx.font_cache(), + ); + + let settings = settings_rx.recv().await.unwrap(); + let md_settings = settings.language_overrides.get("Markdown").unwrap(); + assert_eq!(settings.soft_wrap, SoftWrap::EditorWidth); + assert_eq!(settings.buffer_font_size, 24.0); + assert_eq!(settings.tab_size, 4); + assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength)); + assert_eq!(md_settings.preferred_line_length, Some(100)); + + fs.save( + "/settings2.json".as_ref(), + &r#" + { + "tab_size": 2, + "soft_wrap": "none", + "language_overrides": { + "Markdown": { + "preferred_line_length": 120 + } + } + } + "# + .into(), + ) + .await + .unwrap(); + + let settings = settings_rx.recv().await.unwrap(); + let md_settings = settings.language_overrides.get("Markdown").unwrap(); + assert_eq!(settings.soft_wrap, SoftWrap::None); + assert_eq!(settings.buffer_font_size, 24.0); + assert_eq!(settings.tab_size, 2); + assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength)); + assert_eq!(md_settings.preferred_line_length, Some(120)); + + fs.remove_file("/settings2.json".as_ref(), Default::default()) + .await + .unwrap(); + + let settings = settings_rx.recv().await.unwrap(); + assert_eq!(settings.tab_size, 4); + } } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 0a3ca281337d8136cef1cfe9330a1879c60124fa..f2635fa7feaf5194de1a6926c867de6709bf98cb 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -29,14 +29,14 @@ fn main() { .unwrap() .with_overrides( language::PLAIN_TEXT.name(), - settings::Override { + settings::LanguageOverride { soft_wrap: Some(settings::SoftWrap::PreferredLineLength), ..Default::default() }, ) .with_overrides( "Markdown", - settings::Override { + settings::LanguageOverride { soft_wrap: Some(settings::SoftWrap::PreferredLineLength), ..Default::default() }, From a9713063814648facdf3743a505259cdb88e9789 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 9 Mar 2022 18:00:09 -0800 Subject: [PATCH 02/14] Reload the app settings whenever ~/.zed/settings.json changes --- crates/zed/src/language.rs | 7 +------ crates/zed/src/main.rs | 42 ++++++++++++++++++++++++++++++-------- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index ca73b1ddab49ac5a13477a49de7982250aa7d042..0d69ebee690a02a7dff73593c538dd978f996835 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -533,12 +533,7 @@ impl LspAdapter for JsonLspAdapter { } pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegistry { - let mut languages = LanguageRegistry::new(login_shell_env_loaded); - languages.set_language_server_download_dir( - dirs::home_dir() - .expect("failed to determine home directory") - .join(".zed"), - ); + let languages = LanguageRegistry::new(login_shell_env_loaded); languages.add(Arc::new(c())); languages.add(Arc::new(json())); languages.add(Arc::new(rust())); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index f2635fa7feaf5194de1a6926c867de6709bf98cb..67a6b1a73eb1d55329ce1dac2f55e8bc752fa4bb 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -12,7 +12,11 @@ use smol::process::Command; use std::{env, fs, path::PathBuf, sync::Arc}; use theme::{ThemeRegistry, DEFAULT_THEME_NAME}; use util::ResultExt; -use workspace::{self, settings, AppState, OpenNew, OpenParams, OpenPaths, Settings}; +use workspace::{ + self, + settings::{self, SettingsFile}, + AppState, OpenNew, OpenParams, OpenPaths, Settings, +}; use zed::{ self, assets::Assets, build_window_options, build_workspace, fs::RealFs, language, menus, }; @@ -23,9 +27,13 @@ fn main() { let app = gpui::App::new(Assets).unwrap(); load_embedded_fonts(&app); + let zed_dir = dirs::home_dir() + .expect("failed to determine home directory") + .join(".zed"); + let themes = ThemeRegistry::new(Assets, app.font_cache()); let theme = themes.get(DEFAULT_THEME_NAME).unwrap(); - let settings = Settings::new("Zed Mono", &app.font_cache(), theme) + let default_settings = Settings::new("Zed Mono", &app.font_cache(), theme) .unwrap() .with_overrides( language::PLAIN_TEXT.name(), @@ -41,7 +49,6 @@ fn main() { ..Default::default() }, ); - let (settings_tx, settings) = postage::watch::channel_with(settings); let login_shell_env_loaded = if stdout_is_a_pty() { Task::ready(()) @@ -51,10 +58,27 @@ fn main() { }) }; - let languages = Arc::new(language::build_language_registry(login_shell_env_loaded)); - languages.set_theme(&settings.borrow().theme.editor.syntax); - app.run(move |cx| { + let fs = Arc::new(RealFs); + let user_settings_file = cx.background().block(SettingsFile::new( + fs.clone(), + cx.background(), + zed_dir.join("settings.json"), + )); + + let (settings_tx, settings) = Settings::from_files( + default_settings, + vec![user_settings_file], + cx.background().clone(), + themes.clone(), + cx.font_cache().clone(), + ); + + let mut languages = language::build_language_registry(login_shell_env_loaded); + languages.set_language_server_download_dir(zed_dir); + languages.set_theme(&settings.borrow().theme.editor.syntax); + let languages = Arc::new(languages); + let http = http::client(); let client = client::Client::new(http.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); @@ -85,15 +109,15 @@ fn main() { .detach_and_log_err(cx); let app_state = Arc::new(AppState { - languages: languages.clone(), - settings_tx: Arc::new(Mutex::new(settings_tx)), + languages, + settings_tx, settings, themes, channel_list: cx .add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)), client, user_store, - fs: Arc::new(RealFs), + fs, path_openers: Arc::from(path_openers), build_window_options: &build_window_options, build_workspace: &build_workspace, From 00056fbe88450738c02dddc71e9c7c10bf637ec0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 10 Mar 2022 15:25:22 -0800 Subject: [PATCH 03/14] Load the settings file on a background thread during startup --- crates/zed/src/main.rs | 64 +++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 67a6b1a73eb1d55329ce1dac2f55e8bc752fa4bb..501a8842a78b4d220fd69d2237e48dc998e06b39 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -4,9 +4,11 @@ use anyhow::{anyhow, Context, Result}; use client::{self, http, ChannelList, UserStore}; use fs::OpenOptions; +use futures::channel::oneshot; use gpui::{App, AssetSource, Task}; use log::LevelFilter; use parking_lot::Mutex; +use project::Fs; use simplelog::SimpleLogger; use smol::process::Command; use std::{env, fs, path::PathBuf, sync::Arc}; @@ -30,7 +32,7 @@ fn main() { let zed_dir = dirs::home_dir() .expect("failed to determine home directory") .join(".zed"); - + let fs = Arc::new(RealFs); let themes = ThemeRegistry::new(Assets, app.font_cache()); let theme = themes.get(DEFAULT_THEME_NAME).unwrap(); let default_settings = Settings::new("Zed Mono", &app.font_cache(), theme) @@ -49,6 +51,7 @@ fn main() { ..Default::default() }, ); + let settings_file = load_settings_file(&app, fs.clone(), zed_dir.join("settings.json")); let login_shell_env_loaded = if stdout_is_a_pty() { Task::ready(()) @@ -59,30 +62,13 @@ fn main() { }; app.run(move |cx| { - let fs = Arc::new(RealFs); - let user_settings_file = cx.background().block(SettingsFile::new( - fs.clone(), - cx.background(), - zed_dir.join("settings.json"), - )); - - let (settings_tx, settings) = Settings::from_files( - default_settings, - vec![user_settings_file], - cx.background().clone(), - themes.clone(), - cx.font_cache().clone(), - ); - - let mut languages = language::build_language_registry(login_shell_env_loaded); - languages.set_language_server_download_dir(zed_dir); - languages.set_theme(&settings.borrow().theme.editor.syntax); - let languages = Arc::new(languages); - let http = http::client(); let client = client::Client::new(http.clone()); - let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); let mut path_openers = Vec::new(); + let mut languages = language::build_language_registry(login_shell_env_loaded); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); + let channel_list = + cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)); project::Project::init(&client); client::Channel::init(&client); @@ -108,13 +94,24 @@ fn main() { }) .detach_and_log_err(cx); + let settings_file = cx.background().block(settings_file).unwrap(); + let (settings_tx, settings) = Settings::from_files( + default_settings, + vec![settings_file], + cx.background().clone(), + themes.clone(), + cx.font_cache().clone(), + ); + + languages.set_language_server_download_dir(zed_dir); + languages.set_theme(&settings.borrow().theme.editor.syntax); + let app_state = Arc::new(AppState { - languages, + languages: Arc::new(languages), settings_tx, settings, themes, - channel_list: cx - .add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)), + channel_list, client, user_store, fs, @@ -232,3 +229,20 @@ fn load_embedded_fonts(app: &App) { .add_fonts(&embedded_fonts.into_inner()) .unwrap(); } + +fn load_settings_file( + app: &App, + fs: Arc, + settings_path: PathBuf, +) -> oneshot::Receiver { + let executor = app.background(); + let (tx, rx) = oneshot::channel(); + executor + .clone() + .spawn(async move { + let file = SettingsFile::new(fs, &executor, settings_path).await; + tx.send(file).ok() + }) + .detach(); + rx +} From f32107eb8e60e16778985defc51daf45e25afc21 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 10 Mar 2022 15:34:04 -0800 Subject: [PATCH 04/14] Always refresh the windows when the settings change --- crates/theme_selector/src/theme_selector.rs | 22 +++++++++------------ crates/zed/src/main.rs | 18 ++++++++++++++++- crates/zed/src/zed.rs | 4 +--- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 8483fa4e8a25ae2d6c53b8872b45d04ae0b4b4e3..bbc4df50b550d33dc74864d2f7f3a9caf52f897f 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -110,13 +110,12 @@ impl ThemeSelector { }); } - fn reload(_: &mut Workspace, action: &Reload, cx: &mut ViewContext) { + fn reload(_: &mut Workspace, action: &Reload, _: &mut ViewContext) { let current_theme_name = action.0.settings.borrow().theme.name.clone(); action.0.themes.clear(); match action.0.themes.get(¤t_theme_name) { Ok(theme) => { action.0.settings_tx.lock().borrow_mut().theme = theme; - cx.refresh_windows(); log::info!("reloaded theme {}", current_theme_name); } Err(error) => { @@ -137,7 +136,7 @@ impl ThemeSelector { self.list_state .scroll_to(ScrollTarget::Show(self.selected_index)); - self.show_selected_theme(cx); + self.show_selected_theme(); cx.notify(); } @@ -148,16 +147,14 @@ impl ThemeSelector { self.list_state .scroll_to(ScrollTarget::Show(self.selected_index)); - self.show_selected_theme(cx); + self.show_selected_theme(); cx.notify(); } - fn show_selected_theme(&mut self, cx: &mut MutableAppContext) { + fn show_selected_theme(&mut self) { if let Some(mat) = self.matches.get(self.selected_index) { match self.themes.get(&mat.string) { - Ok(theme) => { - self.set_theme(theme, cx); - } + Ok(theme) => self.set_theme(theme), Err(error) => { log::error!("error loading theme {}: {}", mat.string, error) } @@ -177,9 +174,8 @@ impl ThemeSelector { self.settings_tx.lock().borrow().theme.clone() } - fn set_theme(&self, theme: Arc, cx: &mut MutableAppContext) { + fn set_theme(&self, theme: Arc) { self.settings_tx.lock().borrow_mut().theme = theme; - cx.refresh_windows(); } fn update_matches(&mut self, cx: &mut ViewContext) { @@ -248,7 +244,7 @@ impl ThemeSelector { editor::Event::Edited => { self.update_matches(cx); self.select_if_matching(&self.current_theme().name); - self.show_selected_theme(cx); + self.show_selected_theme(); } editor::Event::Blurred => cx.emit(Event::Dismissed), _ => {} @@ -322,9 +318,9 @@ impl ThemeSelector { impl Entity for ThemeSelector { type Event = Event; - fn release(&mut self, cx: &mut MutableAppContext) { + fn release(&mut self, _: &mut MutableAppContext) { if !self.selection_completed { - self.set_theme(self.original_theme.clone(), cx); + self.set_theme(self.original_theme.clone()); } } } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 501a8842a78b4d220fd69d2237e48dc998e06b39..8306571a3dffd1c2e67534d2f1a8300c76993dc6 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -4,10 +4,11 @@ use anyhow::{anyhow, Context, Result}; use client::{self, http, ChannelList, UserStore}; use fs::OpenOptions; -use futures::channel::oneshot; +use futures::{channel::oneshot, StreamExt}; use gpui::{App, AssetSource, Task}; use log::LevelFilter; use parking_lot::Mutex; +use postage::{prelude::Stream, watch}; use project::Fs; use simplelog::SimpleLogger; use smol::process::Command; @@ -103,6 +104,8 @@ fn main() { cx.font_cache().clone(), ); + refresh_window_on_settings_change(settings.clone(), cx); + languages.set_language_server_download_dir(zed_dir); languages.set_theme(&settings.borrow().theme.editor.syntax); @@ -246,3 +249,16 @@ fn load_settings_file( .detach(); rx } + +fn refresh_window_on_settings_change( + mut settings_rx: watch::Receiver, + cx: &mut gpui::MutableAppContext, +) { + settings_rx.try_recv().ok(); + cx.spawn(|mut cx| async move { + while settings_rx.next().await.is_some() { + cx.update(|cx| cx.refresh_windows()); + } + }) + .detach(); +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index c9a14bf2364441e002e5208af7b53aa6e2420830..db3781287dc1a56f9afbf55ccb44368252fcc322 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -34,12 +34,10 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { cx.add_global_action(quit); cx.add_global_action({ let settings_tx = app_state.settings_tx.clone(); - - move |action: &AdjustBufferFontSize, cx| { + move |action: &AdjustBufferFontSize, _| { let mut settings_tx = settings_tx.lock(); let new_size = (settings_tx.borrow().buffer_font_size + action.0).max(MIN_FONT_SIZE); settings_tx.borrow_mut().buffer_font_size = new_size; - cx.refresh_windows(); } }); From 44a68b723cedd62ae47bb528c3780d274306e72c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 10 Mar 2022 15:51:57 -0800 Subject: [PATCH 05/14] Add cmd-, as a keybinding for opening settings --- crates/zed/src/main.rs | 15 ++++----------- crates/zed/src/zed.rs | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 8306571a3dffd1c2e67534d2f1a8300c76993dc6..da1186d5bfaeb5357d297342fdfac2ae8a989de2 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -30,9 +30,6 @@ fn main() { let app = gpui::App::new(Assets).unwrap(); load_embedded_fonts(&app); - let zed_dir = dirs::home_dir() - .expect("failed to determine home directory") - .join(".zed"); let fs = Arc::new(RealFs); let themes = ThemeRegistry::new(Assets, app.font_cache()); let theme = themes.get(DEFAULT_THEME_NAME).unwrap(); @@ -52,7 +49,7 @@ fn main() { ..Default::default() }, ); - let settings_file = load_settings_file(&app, fs.clone(), zed_dir.join("settings.json")); + let settings_file = load_settings_file(&app, fs.clone()); let login_shell_env_loaded = if stdout_is_a_pty() { Task::ready(()) @@ -106,7 +103,7 @@ fn main() { refresh_window_on_settings_change(settings.clone(), cx); - languages.set_language_server_download_dir(zed_dir); + languages.set_language_server_download_dir(zed::ROOT_PATH.clone()); languages.set_theme(&settings.borrow().theme.editor.syntax); let app_state = Arc::new(AppState { @@ -233,17 +230,13 @@ fn load_embedded_fonts(app: &App) { .unwrap(); } -fn load_settings_file( - app: &App, - fs: Arc, - settings_path: PathBuf, -) -> oneshot::Receiver { +fn load_settings_file(app: &App, fs: Arc) -> oneshot::Receiver { let executor = app.background(); let (tx, rx) = oneshot::channel(); executor .clone() .spawn(async move { - let file = SettingsFile::new(fs, &executor, settings_path).await; + let file = SettingsFile::new(fs, &executor, zed::SETTINGS_PATH.clone()).await; tx.send(file).ok() }) .detach(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index db3781287dc1a56f9afbf55ccb44368252fcc322..43d37b6d9829aa57358e21d4107dba8dc629c0f3 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -16,20 +16,29 @@ use gpui::{ platform::{WindowBounds, WindowOptions}, ModelHandle, ViewContext, }; +use lazy_static::lazy_static; pub use lsp; use project::Project; pub use project::{self, fs}; use project_panel::ProjectPanel; -use std::sync::Arc; +use std::{path::PathBuf, sync::Arc}; pub use workspace; use workspace::{AppState, Workspace, WorkspaceParams}; action!(About); action!(Quit); +action!(OpenSettings); action!(AdjustBufferFontSize, f32); const MIN_FONT_SIZE: f32 = 6.0; +lazy_static! { + pub static ref ROOT_PATH: PathBuf = dirs::home_dir() + .expect("failed to determine home directory") + .join(".zed"); + pub static ref SETTINGS_PATH: PathBuf = ROOT_PATH.join("settings.json"); +} + pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { cx.add_global_action(quit); cx.add_global_action({ @@ -40,15 +49,21 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { settings_tx.borrow_mut().buffer_font_size = new_size; } }); + cx.add_action(open_settings); workspace::lsp_status::init(cx); cx.add_bindings(vec![ Binding::new("cmd-=", AdjustBufferFontSize(1.), None), Binding::new("cmd--", AdjustBufferFontSize(-1.), None), + Binding::new("cmd-,", OpenSettings, None), ]) } +fn open_settings(workspace: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext) { + workspace.open_paths(&[SETTINGS_PATH.clone()], cx).detach(); +} + pub fn build_workspace( project: ModelHandle, app_state: &Arc, From 9a6819b89912b3367f2c1ffe82f481f5e6616c36 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 10 Mar 2022 16:06:12 -0800 Subject: [PATCH 06/14] For single-file worktrees start LSP with parent dir as CWD --- crates/lsp/src/lsp.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index d9024975e4fe635051c05656f3050a8547ac9a07..a8b715f0206151894153a78799308c9369e086f4 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -109,8 +109,13 @@ impl LanguageServer { options: Option, background: Arc, ) -> Result { + let working_dir = if root_path.is_dir() { + root_path + } else { + root_path.parent().unwrap_or(Path::new("/")) + }; let mut server = Command::new(binary_path) - .current_dir(root_path) + .current_dir(working_dir) .args(args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) From bdb39f6247a4d2b8d96b95aed9617e5ba7602793 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 10 Mar 2022 17:27:46 -0800 Subject: [PATCH 07/14] Create the settings file if needed when opening it via command --- crates/zed/src/zed.rs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 43d37b6d9829aa57358e21d4107dba8dc629c0f3..c093cfd2a7473b55746fc36048aa628dd5acb512 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -49,7 +49,26 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { settings_tx.borrow_mut().buffer_font_size = new_size; } }); - cx.add_action(open_settings); + + cx.add_action({ + let fs = app_state.fs.clone(); + move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { + let fs = fs.clone(); + cx.spawn(move |workspace, mut cx| async move { + if !fs.is_file(&SETTINGS_PATH).await { + fs.create_dir(&ROOT_PATH).await?; + fs.create_file(&SETTINGS_PATH, Default::default()).await?; + } + workspace + .update(&mut cx, |workspace, cx| { + workspace.open_paths(&[SETTINGS_PATH.clone()], cx) + }) + .await; + Ok::<_, anyhow::Error>(()) + }) + .detach_and_log_err(cx); + } + }); workspace::lsp_status::init(cx); @@ -60,10 +79,6 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { ]) } -fn open_settings(workspace: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext) { - workspace.open_paths(&[SETTINGS_PATH.clone()], cx).detach(); -} - pub fn build_workspace( project: ModelHandle, app_state: &Arc, From 022bb28a5935af6028cde2c4e46433f4dfb62cd6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 11 Mar 2022 09:57:43 -0800 Subject: [PATCH 08/14] Handle escaped characters in snippets --- crates/snippet/src/snippet.rs | 50 +++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/crates/snippet/src/snippet.rs b/crates/snippet/src/snippet.rs index 82ec12f5ff881546e46f269f786c76217cbc38f8..ecd343652797fea1c14f6637696b18171fbe5c76 100644 --- a/crates/snippet/src/snippet.rs +++ b/crates/snippet/src/snippet.rs @@ -17,13 +17,20 @@ impl Snippet { parse_snippet(source, false, &mut text, &mut tabstops) .context("failed to parse snippet")?; - let last_tabstop = tabstops - .remove(&0) - .unwrap_or_else(|| SmallVec::from_iter([text.len() as isize..text.len() as isize])); - Ok(Snippet { - text, - tabstops: tabstops.into_values().chain(Some(last_tabstop)).collect(), - }) + let len = text.len() as isize; + let final_tabstop = tabstops.remove(&0); + let mut tabstops = tabstops.into_values().collect::>(); + + if let Some(final_tabstop) = final_tabstop { + tabstops.push(final_tabstop); + } else { + let end_tabstop = [len..len].into_iter().collect(); + if !tabstops.last().map_or(false, |t| *t == end_tabstop) { + tabstops.push(end_tabstop); + } + } + + Ok(Snippet { text, tabstops }) } } @@ -39,6 +46,13 @@ fn parse_snippet<'a>( Some('$') => { source = parse_tabstop(&source[1..], text, tabstops)?; } + Some('\\') => { + source = &source[1..]; + if let Some(c) = source.chars().next() { + text.push(c); + source = &source[c.len_utf8()..]; + } + } Some('}') => { if nested { return Ok(source); @@ -48,7 +62,7 @@ fn parse_snippet<'a>( } } Some(_) => { - let chunk_end = source.find(&['}', '$']).unwrap_or(source.len()); + let chunk_end = source.find(&['}', '$', '\\']).unwrap_or(source.len()); let (chunk, rest) = source.split_at(chunk_end); text.push_str(chunk); source = rest; @@ -125,9 +139,22 @@ mod tests { assert_eq!(tabstops(&snippet), &[vec![4..4], vec![3..3], vec![8..8]]); } + #[test] + fn test_snippet_with_last_tabstop_at_end() { + let snippet = Snippet::parse(r#"foo.$1"#).unwrap(); + + // If the final tabstop is already at the end of the text, don't insert + // an additional tabstop at the end. + assert_eq!(snippet.text, r#"foo."#); + assert_eq!(tabstops(&snippet), &[vec![4..4]]); + } + #[test] fn test_snippet_with_explicit_final_tabstop() { let snippet = Snippet::parse(r#"
$0
"#).unwrap(); + + // If the final tabstop is explicitly specified via '$0', then + // don't insert an additional tabstop at the end. assert_eq!(snippet.text, r#"
"#); assert_eq!(tabstops(&snippet), &[vec![12..12], vec![14..14]]); } @@ -161,6 +188,13 @@ mod tests { ); } + #[test] + fn test_snippet_parsing_with_escaped_dollar_sign() { + let snippet = Snippet::parse("\"\\$schema\": $1").unwrap(); + assert_eq!(snippet.text, "\"$schema\": "); + assert_eq!(tabstops(&snippet), &[vec![11..11]]); + } + fn tabstops(snippet: &Snippet) -> Vec>> { snippet.tabstops.iter().map(|t| t.to_vec()).collect() } From 862ec01e7d4d55a15d5e6d34e4dde794388ac3bf Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 11 Mar 2022 11:44:02 -0800 Subject: [PATCH 09/14] Add API for handling custom requests from the language server --- crates/lsp/src/lsp.rs | 65 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 48d48ddf318ef5985863cef90681a1fe07d79e0d..3a70b10581f80623e940fa7c0cec739e5ccf95ec 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -4,7 +4,7 @@ use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite}; use gpui::{executor, Task}; use parking_lot::{Mutex, RwLock}; use postage::{barrier, prelude::Stream}; -use serde::{Deserialize, Serialize}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::{json, value::RawValue, Value}; use smol::{ channel, @@ -29,7 +29,8 @@ pub use lsp_types::*; const JSON_RPC_VERSION: &'static str = "2.0"; const CONTENT_LEN_HEADER: &'static str = "Content-Length: "; -type NotificationHandler = Box; +type NotificationHandler = + Box, &str, &mut channel::Sender>) -> Result<()>>; type ResponseHandler = Box)>; pub struct LanguageServer { @@ -80,6 +81,12 @@ struct AnyResponse<'a> { result: Option<&'a RawValue>, } +#[derive(Serialize)] +struct Response { + id: usize, + result: T, +} + #[derive(Serialize, Deserialize)] struct Notification<'a, T> { #[serde(borrow)] @@ -91,6 +98,8 @@ struct Notification<'a, T> { #[derive(Deserialize)] struct AnyNotification<'a> { + #[serde(default)] + id: Option, #[serde(borrow)] method: &'a str, #[serde(borrow)] @@ -152,6 +161,7 @@ impl LanguageServer { { let notification_handlers = notification_handlers.clone(); let response_handlers = response_handlers.clone(); + let mut outbound_tx = outbound_tx.clone(); async move { let _clear_response_handlers = ClearResponseHandlers(response_handlers.clone()); let mut buffer = Vec::new(); @@ -168,11 +178,13 @@ impl LanguageServer { buffer.resize(message_len, 0); stdout.read_exact(&mut buffer).await?; - if let Ok(AnyNotification { method, params }) = + if let Ok(AnyNotification { id, method, params }) = serde_json::from_slice(&buffer) { if let Some(handler) = notification_handlers.write().get_mut(method) { - handler(params.get()); + if let Err(e) = handler(id, params.get(), &mut outbound_tx) { + log::error!("error handling {} message: {:?}", method, e); + } } else { log::info!( "unhandled notification {}:\n{}", @@ -351,12 +363,11 @@ impl LanguageServer { { let prev_handler = self.notification_handlers.write().insert( T::METHOD, - Box::new( - move |notification| match serde_json::from_str(notification) { - Ok(notification) => f(notification), - Err(err) => log::error!("error parsing notification {}: {}", T::METHOD, err), - }, - ), + Box::new(move |_, params, _| { + let params = serde_json::from_str(params)?; + f(params); + Ok(()) + }), ); assert!( @@ -370,6 +381,40 @@ impl LanguageServer { } } + pub fn on_custom_request( + &mut self, + method: &'static str, + mut f: F, + ) -> Subscription + where + F: 'static + Send + Sync + FnMut(Params) -> Result, + Params: DeserializeOwned, + Resp: Serialize, + { + let prev_handler = self.notification_handlers.write().insert( + method, + Box::new(move |id, params, tx| { + if let Some(id) = id { + let params = serde_json::from_str(params)?; + let result = f(params)?; + let response = serde_json::to_vec(&Response { id, result })?; + tx.try_send(response)?; + } + Ok(()) + }), + ); + + assert!( + prev_handler.is_none(), + "registered multiple handlers for the same notification" + ); + + Subscription { + method, + notification_handlers: self.notification_handlers.clone(), + } + } + pub fn name<'a>(self: &'a Arc) -> &'a str { &self.name } From 7a68b2d371f0446657f818c37941d16a2347dceb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 11 Mar 2022 11:46:22 -0800 Subject: [PATCH 10/14] Provide JSON language server with settings schema --- Cargo.lock | 42 ++++++++++++++++++++++++++++++++ crates/language/src/language.rs | 8 ++++-- crates/workspace/Cargo.toml | 1 + crates/workspace/src/settings.rs | 12 ++++++--- crates/zed/src/language.rs | 11 +++++++++ 5 files changed, 69 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f066635bc229d8d9e7cba6b9964ab8dfa241a80..d738fe86cc796c17979589e8e409487e94d61e88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1574,6 +1574,12 @@ dependencies = [ "wio", ] +[[package]] +name = "dyn-clone" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf" + [[package]] name = "easy-parallel" version = "3.1.0" @@ -4139,6 +4145,30 @@ dependencies = [ "winapi", ] +[[package]] +name = "schemars" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6b5a3c80cea1ab61f4260238409510e814e38b4b563c06044edf91e7dc070e3" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ae4dce13e8614c46ac3c38ef1c0d668b101df6ac39817aebdaa26642ddae9b" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + [[package]] name = "scoped-tls" version = "1.0.0" @@ -4260,6 +4290,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_derive_internals" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_json" version = "1.0.64" @@ -5851,6 +5892,7 @@ dependencies = [ "parking_lot", "postage", "project", + "schemars", "serde", "serde_json", "smallvec", diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 5af18241b6d942f2abba949ced7a25128132ac5c..b94c15a8790815d20b45a5148a59017a0a6abb2a 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -95,6 +95,8 @@ pub trait LspAdapter: 'static + Send + Sync { fn initialization_options(&self) -> Option { None } + + fn register_handlers(&self, _: &mut lsp::LanguageServer) {} } #[derive(Clone, Debug, PartialEq, Eq)] @@ -319,13 +321,15 @@ impl LanguageRegistry { let server_binary_path = server_binary_path.await?; let server_args = adapter.server_args(); - lsp::LanguageServer::new( + let mut server = lsp::LanguageServer::new( &server_binary_path, server_args, &root_path, adapter.initialization_options(), background, - ) + )?; + adapter.register_handlers(&mut server); + Ok(server) })) } diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index c8cf21c571ffde6292b62ab0a790e06840471169..7ef0bd85439b74c9739814a2480584b3496918b9 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -24,6 +24,7 @@ futures = "0.3" log = "0.4" parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } +schemars = "0.8" serde = { version = "1", features = ["derive", "rc"] } serde_json = { version = "1", features = ["preserve_order"] } smallvec = { version = "1.6", features = ["union"] } diff --git a/crates/workspace/src/settings.rs b/crates/workspace/src/settings.rs index 62a6d4af8c910e832033245d3920aa8d12c24cf5..fbda0e2b714b844a460d84f1827dfcf3d2db928c 100644 --- a/crates/workspace/src/settings.rs +++ b/crates/workspace/src/settings.rs @@ -8,6 +8,7 @@ use language::Language; use parking_lot::Mutex; use postage::{prelude::Stream, watch}; use project::Fs; +use schemars::{schema_for, JsonSchema}; use serde::Deserialize; use std::{collections::HashMap, path::Path, sync::Arc, time::Duration}; use theme::{Theme, ThemeRegistry}; @@ -24,14 +25,14 @@ pub struct Settings { pub theme: Arc, } -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Debug, Default, Deserialize, JsonSchema)] pub struct LanguageOverride { pub tab_size: Option, pub soft_wrap: Option, pub preferred_line_length: Option, } -#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum SoftWrap { None, @@ -42,7 +43,7 @@ pub enum SoftWrap { #[derive(Clone)] pub struct SettingsFile(watch::Receiver); -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Debug, Default, Deserialize, JsonSchema)] struct SettingsFileContent { #[serde(default)] buffer_font_family: Option, @@ -94,6 +95,11 @@ impl SettingsFile { } impl Settings { + pub fn file_json_schema() -> String { + let schema = schema_for!(SettingsFileContent); + serde_json::to_string(&schema).unwrap() + } + pub fn from_files( defaults: Self, sources: Vec, diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index 0d69ebee690a02a7dff73593c538dd978f996835..5782c75c972a724c43435f656b377a5ada272fb7 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -530,6 +530,17 @@ impl LspAdapter for JsonLspAdapter { "provideFormatter": true })) } + + fn register_handlers(&self, lsp: &mut lsp::LanguageServer) { + lsp.on_custom_request::, Option, _>("vscode/content", |schema| { + if schema.get(0).map(String::as_str) == Some("zed://settings") { + Ok(Some(workspace::Settings::file_json_schema())) + } else { + Ok(None) + } + }) + .detach(); + } } pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegistry { From a137abe2de5663d01a5e6c9332ed2b5d46b2800e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 11 Mar 2022 13:02:20 -0800 Subject: [PATCH 11/14] Add a snippet unit test with an escaped curly brace --- crates/snippet/src/snippet.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/snippet/src/snippet.rs b/crates/snippet/src/snippet.rs index ecd343652797fea1c14f6637696b18171fbe5c76..8f4ca82c05c35a643a1e185d1e17be6af9a48946 100644 --- a/crates/snippet/src/snippet.rs +++ b/crates/snippet/src/snippet.rs @@ -189,10 +189,14 @@ mod tests { } #[test] - fn test_snippet_parsing_with_escaped_dollar_sign() { + fn test_snippet_parsing_with_escaped_chars() { let snippet = Snippet::parse("\"\\$schema\": $1").unwrap(); assert_eq!(snippet.text, "\"$schema\": "); assert_eq!(tabstops(&snippet), &[vec![11..11]]); + + let snippet = Snippet::parse("{a\\}").unwrap(); + assert_eq!(snippet.text, "{a}"); + assert_eq!(tabstops(&snippet), &[vec![3..3]]); } fn tabstops(snippet: &Snippet) -> Vec>> { From 2103eec4634c0515ce69767ffbc2570b05125f8a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 11 Mar 2022 13:19:10 -0800 Subject: [PATCH 12/14] Allow registering handlers for typed LSP requests --- crates/lsp/src/lsp.rs | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 3a70b10581f80623e940fa7c0cec739e5ccf95ec..883d78343f61132cfe06de7d333dfbb04251aa8b 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -356,40 +356,58 @@ impl LanguageServer { } } - pub fn on_notification(&mut self, mut f: F) -> Subscription + pub fn on_notification(&mut self, f: F) -> Subscription where T: notification::Notification, F: 'static + Send + Sync + FnMut(T::Params), + { + self.on_custom_notification(T::METHOD, f) + } + + pub fn on_request(&mut self, f: F) -> Subscription + where + T: request::Request, + F: 'static + Send + Sync + FnMut(T::Params) -> Result, + { + self.on_custom_request(T::METHOD, f) + } + + pub fn on_custom_notification( + &mut self, + method: &'static str, + mut f: F, + ) -> Subscription + where + F: 'static + Send + Sync + FnMut(Params), + Params: DeserializeOwned, { let prev_handler = self.notification_handlers.write().insert( - T::METHOD, + method, Box::new(move |_, params, _| { let params = serde_json::from_str(params)?; f(params); Ok(()) }), ); - assert!( prev_handler.is_none(), - "registered multiple handlers for the same notification" + "registered multiple handlers for the same LSP method" ); - Subscription { - method: T::METHOD, + method, notification_handlers: self.notification_handlers.clone(), } } - pub fn on_custom_request( + pub fn on_custom_request( &mut self, method: &'static str, mut f: F, ) -> Subscription where - F: 'static + Send + Sync + FnMut(Params) -> Result, + F: 'static + Send + Sync + FnMut(Params) -> Result, Params: DeserializeOwned, - Resp: Serialize, + Res: Serialize, { let prev_handler = self.notification_handlers.write().insert( method, @@ -403,12 +421,10 @@ impl LanguageServer { Ok(()) }), ); - assert!( prev_handler.is_none(), - "registered multiple handlers for the same notification" + "registered multiple handlers for the same LSP method" ); - Subscription { method, notification_handlers: self.notification_handlers.clone(), From 48848de82c0c4b849ffe31d2250d5c067a1bd1aa Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 11 Mar 2022 14:59:05 -0800 Subject: [PATCH 13/14] Store settings as a global via a gpui app_state --- crates/chat_panel/src/chat_panel.rs | 51 ++-- crates/contacts_panel/src/contacts_panel.rs | 10 +- crates/diagnostics/src/diagnostics.rs | 56 +--- crates/diagnostics/src/items.rs | 14 +- crates/editor/src/editor.rs | 267 +++++++----------- crates/editor/src/element.rs | 5 +- crates/editor/src/items.rs | 32 +-- crates/file_finder/src/file_finder.rs | 91 ++---- crates/go_to_line/src/go_to_line.rs | 23 +- crates/outline/src/outline.rs | 43 ++- crates/project_panel/src/project_panel.rs | 22 +- crates/project_symbols/src/project_symbols.rs | 30 +- crates/search/src/buffer_search.rs | 34 +-- crates/search/src/project_search.rs | 44 ++- crates/server/src/rpc.rs | 17 +- crates/theme_selector/src/theme_selector.rs | 147 ++++------ crates/workspace/src/lsp_status.rs | 9 +- crates/workspace/src/pane.rs | 8 +- crates/workspace/src/settings.rs | 54 ++-- crates/workspace/src/sidebar.rs | 26 +- crates/workspace/src/status_bar.rs | 13 +- crates/workspace/src/workspace.rs | 55 ++-- crates/zed/src/main.rs | 36 +-- crates/zed/src/test.rs | 7 +- crates/zed/src/zed.rs | 45 +-- 25 files changed, 406 insertions(+), 733 deletions(-) diff --git a/crates/chat_panel/src/chat_panel.rs b/crates/chat_panel/src/chat_panel.rs index 93c51a1b40e75d6db1b8528aff6f8048509eab71..ceeddc599a0d3d90b0328e2d6d94148b98e42002 100644 --- a/crates/chat_panel/src/chat_panel.rs +++ b/crates/chat_panel/src/chat_panel.rs @@ -12,7 +12,7 @@ use gpui::{ AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, }; -use postage::{prelude::Stream, watch}; +use postage::prelude::Stream; use std::sync::Arc; use time::{OffsetDateTime, UtcOffset}; use util::{ResultExt, TryFutureExt}; @@ -27,7 +27,6 @@ pub struct ChatPanel { message_list: ListState, input_editor: ViewHandle, channel_select: ViewHandle