mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
Start using the SettingsStore in the app
This commit is contained in:
parent
316f791a77
commit
9a6a2d9d27
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -1404,6 +1404,7 @@ dependencies = [
|
|||||||
"context_menu",
|
"context_menu",
|
||||||
"copilot",
|
"copilot",
|
||||||
"editor",
|
"editor",
|
||||||
|
"fs",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui",
|
"gpui",
|
||||||
"settings",
|
"settings",
|
||||||
@ -6847,6 +6848,7 @@ name = "theme_selector"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"editor",
|
"editor",
|
||||||
|
"fs",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
"log",
|
"log",
|
||||||
@ -8315,6 +8317,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"db",
|
"db",
|
||||||
"editor",
|
"editor",
|
||||||
|
"fs",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
"install_cli",
|
"install_cli",
|
||||||
|
@ -12,6 +12,7 @@ doctest = false
|
|||||||
assets = { path = "../assets" }
|
assets = { path = "../assets" }
|
||||||
copilot = { path = "../copilot" }
|
copilot = { path = "../copilot" }
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
|
fs = { path = "../fs" }
|
||||||
context_menu = { path = "../context_menu" }
|
context_menu = { path = "../context_menu" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
|
@ -2,13 +2,14 @@ use anyhow::Result;
|
|||||||
use context_menu::{ContextMenu, ContextMenuItem};
|
use context_menu::{ContextMenu, ContextMenuItem};
|
||||||
use copilot::{Copilot, SignOut, Status};
|
use copilot::{Copilot, SignOut, Status};
|
||||||
use editor::{scroll::autoscroll::Autoscroll, Editor};
|
use editor::{scroll::autoscroll::Autoscroll, Editor};
|
||||||
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*,
|
elements::*,
|
||||||
platform::{CursorStyle, MouseButton},
|
platform::{CursorStyle, MouseButton},
|
||||||
AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View,
|
AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View,
|
||||||
ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||||
};
|
};
|
||||||
use settings::{settings_file::SettingsFile, Settings};
|
use settings::{update_settings_file, Settings, SettingsStore};
|
||||||
use std::{path::Path, sync::Arc};
|
use std::{path::Path, sync::Arc};
|
||||||
use util::{paths, ResultExt};
|
use util::{paths, ResultExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
@ -26,6 +27,7 @@ pub struct CopilotButton {
|
|||||||
editor_enabled: Option<bool>,
|
editor_enabled: Option<bool>,
|
||||||
language: Option<Arc<str>>,
|
language: Option<Arc<str>>,
|
||||||
path: Option<Arc<Path>>,
|
path: Option<Arc<Path>>,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for CopilotButton {
|
impl Entity for CopilotButton {
|
||||||
@ -143,7 +145,7 @@ impl View for CopilotButton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CopilotButton {
|
impl CopilotButton {
|
||||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
|
||||||
let button_view_id = cx.view_id();
|
let button_view_id = cx.view_id();
|
||||||
let menu = cx.add_view(|cx| {
|
let menu = cx.add_view(|cx| {
|
||||||
let mut menu = ContextMenu::new(button_view_id, cx);
|
let mut menu = ContextMenu::new(button_view_id, cx);
|
||||||
@ -164,17 +166,19 @@ impl CopilotButton {
|
|||||||
editor_enabled: None,
|
editor_enabled: None,
|
||||||
language: None,
|
language: None,
|
||||||
path: None,
|
path: None,
|
||||||
|
fs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deploy_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) {
|
pub fn deploy_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
let mut menu_options = Vec::with_capacity(2);
|
let mut menu_options = Vec::with_capacity(2);
|
||||||
|
let fs = self.fs.clone();
|
||||||
|
|
||||||
menu_options.push(ContextMenuItem::handler("Sign In", |cx| {
|
menu_options.push(ContextMenuItem::handler("Sign In", |cx| {
|
||||||
initiate_sign_in(cx)
|
initiate_sign_in(cx)
|
||||||
}));
|
}));
|
||||||
menu_options.push(ContextMenuItem::handler("Disable Copilot", |cx| {
|
menu_options.push(ContextMenuItem::handler("Disable Copilot", move |cx| {
|
||||||
hide_copilot(cx)
|
hide_copilot(fs.clone(), cx)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
self.popup_menu.update(cx, |menu, cx| {
|
self.popup_menu.update(cx, |menu, cx| {
|
||||||
@ -189,10 +193,12 @@ impl CopilotButton {
|
|||||||
|
|
||||||
pub fn deploy_copilot_menu(&mut self, cx: &mut ViewContext<Self>) {
|
pub fn deploy_copilot_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
let settings = cx.global::<Settings>();
|
let settings = cx.global::<Settings>();
|
||||||
|
let fs = self.fs.clone();
|
||||||
|
|
||||||
let mut menu_options = Vec::with_capacity(8);
|
let mut menu_options = Vec::with_capacity(8);
|
||||||
|
|
||||||
if let Some(language) = self.language.clone() {
|
if let Some(language) = self.language.clone() {
|
||||||
|
let fs = fs.clone();
|
||||||
let language_enabled = settings.copilot_enabled_for_language(Some(language.as_ref()));
|
let language_enabled = settings.copilot_enabled_for_language(Some(language.as_ref()));
|
||||||
menu_options.push(ContextMenuItem::handler(
|
menu_options.push(ContextMenuItem::handler(
|
||||||
format!(
|
format!(
|
||||||
@ -200,7 +206,7 @@ impl CopilotButton {
|
|||||||
if language_enabled { "Hide" } else { "Show" },
|
if language_enabled { "Hide" } else { "Show" },
|
||||||
language
|
language
|
||||||
),
|
),
|
||||||
move |cx| toggle_copilot_for_language(language.clone(), cx),
|
move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,7 +241,7 @@ impl CopilotButton {
|
|||||||
} else {
|
} else {
|
||||||
"Show Suggestions for All Files"
|
"Show Suggestions for All Files"
|
||||||
},
|
},
|
||||||
|cx| toggle_copilot_globally(cx),
|
move |cx| toggle_copilot_globally(fs.clone(), cx),
|
||||||
));
|
));
|
||||||
|
|
||||||
menu_options.push(ContextMenuItem::Separator);
|
menu_options.push(ContextMenuItem::Separator);
|
||||||
@ -322,7 +328,9 @@ async fn configure_disabled_globs(
|
|||||||
settings_editor.downgrade().update(&mut cx, |item, cx| {
|
settings_editor.downgrade().update(&mut cx, |item, cx| {
|
||||||
let text = item.buffer().read(cx).snapshot(cx).text();
|
let text = item.buffer().read(cx).snapshot(cx).text();
|
||||||
|
|
||||||
let edits = SettingsFile::update_unsaved(&text, cx, |file| {
|
let edits = cx
|
||||||
|
.global::<SettingsStore>()
|
||||||
|
.update::<Settings>(&text, |file| {
|
||||||
let copilot = file.copilot.get_or_insert_with(Default::default);
|
let copilot = file.copilot.get_or_insert_with(Default::default);
|
||||||
let globs = copilot.disabled_globs.get_or_insert_with(|| {
|
let globs = copilot.disabled_globs.get_or_insert_with(|| {
|
||||||
cx.global::<Settings>()
|
cx.global::<Settings>()
|
||||||
@ -356,19 +364,19 @@ async fn configure_disabled_globs(
|
|||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_copilot_globally(cx: &mut AppContext) {
|
fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||||
let show_copilot_suggestions = cx.global::<Settings>().show_copilot_suggestions(None, None);
|
let show_copilot_suggestions = cx.global::<Settings>().show_copilot_suggestions(None, None);
|
||||||
SettingsFile::update(cx, move |file_contents| {
|
update_settings_file(fs, cx, move |file_contents| {
|
||||||
file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
|
file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_copilot_for_language(language: Arc<str>, cx: &mut AppContext) {
|
fn toggle_copilot_for_language(language: Arc<str>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||||
let show_copilot_suggestions = cx
|
let show_copilot_suggestions = cx
|
||||||
.global::<Settings>()
|
.global::<Settings>()
|
||||||
.show_copilot_suggestions(Some(&language), None);
|
.show_copilot_suggestions(Some(&language), None);
|
||||||
|
|
||||||
SettingsFile::update(cx, move |file_contents| {
|
update_settings_file(fs, cx, move |file_contents| {
|
||||||
file_contents.languages.insert(
|
file_contents.languages.insert(
|
||||||
language,
|
language,
|
||||||
settings::EditorSettings {
|
settings::EditorSettings {
|
||||||
@ -379,8 +387,8 @@ fn toggle_copilot_for_language(language: Arc<str>, cx: &mut AppContext) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hide_copilot(cx: &mut AppContext) {
|
fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||||
SettingsFile::update(cx, move |file_contents| {
|
update_settings_file(fs, cx, move |file_contents| {
|
||||||
file_contents.features.copilot = Some(false)
|
file_contents.features.copilot = Some(false)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ path = "src/settings.rs"
|
|||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
test-support = []
|
test-support = ["theme/test-support", "gpui/test-support", "fs/test-support"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
assets = { path = "../assets" }
|
assets = { path = "../assets" }
|
||||||
@ -39,6 +39,7 @@ tree-sitter-json = "*"
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
fs = { path = "../fs", features = ["test-support"] }
|
fs = { path = "../fs", features = ["test-support"] }
|
||||||
|
theme = { path = "../theme", features = ["test-support"] }
|
||||||
|
|
||||||
pretty_assertions = "1.3.0"
|
pretty_assertions = "1.3.0"
|
||||||
unindent.workspace = true
|
unindent.workspace = true
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::{parse_json_with_comments, Settings};
|
use crate::{settings_store::parse_json_with_comments, Settings};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use assets::Assets;
|
use assets::Assets;
|
||||||
use collections::BTreeMap;
|
use collections::BTreeMap;
|
||||||
|
@ -1,34 +1,31 @@
|
|||||||
mod keymap_file;
|
mod keymap_file;
|
||||||
pub mod settings_file;
|
mod settings_file;
|
||||||
pub mod settings_store;
|
mod settings_store;
|
||||||
pub mod watched_json;
|
|
||||||
|
|
||||||
use anyhow::{bail, Result};
|
use anyhow::bail;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
font_cache::{FamilyId, FontCache},
|
font_cache::{FamilyId, FontCache},
|
||||||
fonts, AssetSource,
|
fonts, AppContext, AssetSource,
|
||||||
};
|
};
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use schemars::{
|
use schemars::{
|
||||||
gen::{SchemaGenerator, SchemaSettings},
|
gen::{SchemaGenerator, SchemaSettings},
|
||||||
schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
|
schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
|
||||||
JsonSchema,
|
JsonSchema,
|
||||||
};
|
};
|
||||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use settings_store::Setting;
|
||||||
use sqlez::{
|
use sqlez::{
|
||||||
bindable::{Bind, Column, StaticColumnCount},
|
bindable::{Bind, Column, StaticColumnCount},
|
||||||
statement::Statement,
|
statement::Statement,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{borrow::Cow, collections::HashMap, num::NonZeroU32, path::Path, str, sync::Arc};
|
||||||
borrow::Cow, collections::HashMap, num::NonZeroU32, ops::Range, path::Path, str, sync::Arc,
|
|
||||||
};
|
|
||||||
use theme::{Theme, ThemeRegistry};
|
use theme::{Theme, ThemeRegistry};
|
||||||
use tree_sitter::{Query, Tree};
|
use util::ResultExt as _;
|
||||||
use util::{RangeExt, ResultExt as _};
|
|
||||||
|
|
||||||
pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
|
pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
|
||||||
pub use watched_json::watch_files;
|
pub use settings_file::*;
|
||||||
|
pub use settings_store::SettingsStore;
|
||||||
|
|
||||||
pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json";
|
pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json";
|
||||||
pub const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json";
|
pub const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json";
|
||||||
@ -69,6 +66,92 @@ pub struct Settings {
|
|||||||
pub base_keymap: BaseKeymap,
|
pub base_keymap: BaseKeymap,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Setting for Settings {
|
||||||
|
type FileContent = SettingsFileContent;
|
||||||
|
|
||||||
|
fn load(
|
||||||
|
defaults: &Self::FileContent,
|
||||||
|
user_values: &[&Self::FileContent],
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> Self {
|
||||||
|
let buffer_font_features = defaults.buffer_font_features.clone().unwrap();
|
||||||
|
let themes = cx.global::<Arc<ThemeRegistry>>();
|
||||||
|
|
||||||
|
let mut this = Self {
|
||||||
|
buffer_font_family: cx
|
||||||
|
.font_cache()
|
||||||
|
.load_family(
|
||||||
|
&[defaults.buffer_font_family.as_ref().unwrap()],
|
||||||
|
&buffer_font_features,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
buffer_font_family_name: defaults.buffer_font_family.clone().unwrap(),
|
||||||
|
buffer_font_features,
|
||||||
|
buffer_font_size: defaults.buffer_font_size.unwrap(),
|
||||||
|
active_pane_magnification: defaults.active_pane_magnification.unwrap(),
|
||||||
|
default_buffer_font_size: defaults.buffer_font_size.unwrap(),
|
||||||
|
confirm_quit: defaults.confirm_quit.unwrap(),
|
||||||
|
cursor_blink: defaults.cursor_blink.unwrap(),
|
||||||
|
hover_popover_enabled: defaults.hover_popover_enabled.unwrap(),
|
||||||
|
show_completions_on_input: defaults.show_completions_on_input.unwrap(),
|
||||||
|
show_call_status_icon: defaults.show_call_status_icon.unwrap(),
|
||||||
|
vim_mode: defaults.vim_mode.unwrap(),
|
||||||
|
autosave: defaults.autosave.unwrap(),
|
||||||
|
default_dock_anchor: defaults.default_dock_anchor.unwrap(),
|
||||||
|
editor_defaults: EditorSettings {
|
||||||
|
tab_size: defaults.editor.tab_size,
|
||||||
|
hard_tabs: defaults.editor.hard_tabs,
|
||||||
|
soft_wrap: defaults.editor.soft_wrap,
|
||||||
|
preferred_line_length: defaults.editor.preferred_line_length,
|
||||||
|
remove_trailing_whitespace_on_save: defaults
|
||||||
|
.editor
|
||||||
|
.remove_trailing_whitespace_on_save,
|
||||||
|
ensure_final_newline_on_save: defaults.editor.ensure_final_newline_on_save,
|
||||||
|
format_on_save: defaults.editor.format_on_save.clone(),
|
||||||
|
formatter: defaults.editor.formatter.clone(),
|
||||||
|
enable_language_server: defaults.editor.enable_language_server,
|
||||||
|
show_copilot_suggestions: defaults.editor.show_copilot_suggestions,
|
||||||
|
show_whitespaces: defaults.editor.show_whitespaces,
|
||||||
|
},
|
||||||
|
editor_overrides: Default::default(),
|
||||||
|
copilot: CopilotSettings {
|
||||||
|
disabled_globs: defaults
|
||||||
|
.copilot
|
||||||
|
.clone()
|
||||||
|
.unwrap()
|
||||||
|
.disabled_globs
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| glob::Pattern::new(&s).unwrap())
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
git: defaults.git.unwrap(),
|
||||||
|
git_overrides: Default::default(),
|
||||||
|
journal_defaults: defaults.journal.clone(),
|
||||||
|
journal_overrides: Default::default(),
|
||||||
|
terminal_defaults: defaults.terminal.clone(),
|
||||||
|
terminal_overrides: Default::default(),
|
||||||
|
language_defaults: defaults.languages.clone(),
|
||||||
|
language_overrides: Default::default(),
|
||||||
|
lsp: defaults.lsp.clone(),
|
||||||
|
theme: themes.get(defaults.theme.as_ref().unwrap()).unwrap(),
|
||||||
|
telemetry_defaults: defaults.telemetry,
|
||||||
|
telemetry_overrides: Default::default(),
|
||||||
|
auto_update: defaults.auto_update.unwrap(),
|
||||||
|
base_keymap: Default::default(),
|
||||||
|
features: Features {
|
||||||
|
copilot: defaults.features.copilot.unwrap(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for value in user_values.into_iter().copied().cloned() {
|
||||||
|
this.set_user_settings(value, themes.as_ref(), cx.font_cache());
|
||||||
|
}
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
|
||||||
pub enum BaseKeymap {
|
pub enum BaseKeymap {
|
||||||
#[default]
|
#[default]
|
||||||
@ -477,7 +560,7 @@ impl Settings {
|
|||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
|
||||||
let defaults: SettingsFileContent = parse_json_with_comments(
|
let defaults: SettingsFileContent = settings_store::parse_json_with_comments(
|
||||||
str::from_utf8(assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap().as_ref()).unwrap(),
|
str::from_utf8(assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap().as_ref()).unwrap(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@ -914,686 +997,3 @@ fn merge<T: Copy>(target: &mut T, value: Option<T>) {
|
|||||||
*target = value;
|
*target = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
|
|
||||||
Ok(serde_json::from_reader(
|
|
||||||
json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
|
|
||||||
)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref PAIR_QUERY: Query = Query::new(
|
|
||||||
tree_sitter_json::language(),
|
|
||||||
"
|
|
||||||
(pair
|
|
||||||
key: (string) @key
|
|
||||||
value: (_) @value)
|
|
||||||
",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_object_in_settings_file<'a>(
|
|
||||||
old_object: &'a serde_json::Map<String, Value>,
|
|
||||||
new_object: &'a serde_json::Map<String, Value>,
|
|
||||||
text: &str,
|
|
||||||
syntax_tree: &Tree,
|
|
||||||
tab_size: usize,
|
|
||||||
key_path: &mut Vec<&'a str>,
|
|
||||||
edits: &mut Vec<(Range<usize>, String)>,
|
|
||||||
) {
|
|
||||||
for (key, old_value) in old_object.iter() {
|
|
||||||
key_path.push(key);
|
|
||||||
let new_value = new_object.get(key).unwrap_or(&Value::Null);
|
|
||||||
|
|
||||||
// If the old and new values are both objects, then compare them key by key,
|
|
||||||
// preserving the comments and formatting of the unchanged parts. Otherwise,
|
|
||||||
// replace the old value with the new value.
|
|
||||||
if let (Value::Object(old_sub_object), Value::Object(new_sub_object)) =
|
|
||||||
(old_value, new_value)
|
|
||||||
{
|
|
||||||
update_object_in_settings_file(
|
|
||||||
old_sub_object,
|
|
||||||
new_sub_object,
|
|
||||||
text,
|
|
||||||
syntax_tree,
|
|
||||||
tab_size,
|
|
||||||
key_path,
|
|
||||||
edits,
|
|
||||||
)
|
|
||||||
} else if old_value != new_value {
|
|
||||||
let (range, replacement) =
|
|
||||||
update_key_in_settings_file(text, syntax_tree, &key_path, tab_size, &new_value);
|
|
||||||
edits.push((range, replacement));
|
|
||||||
}
|
|
||||||
|
|
||||||
key_path.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_key_in_settings_file(
|
|
||||||
text: &str,
|
|
||||||
syntax_tree: &Tree,
|
|
||||||
key_path: &[&str],
|
|
||||||
tab_size: usize,
|
|
||||||
new_value: impl Serialize,
|
|
||||||
) -> (Range<usize>, String) {
|
|
||||||
const LANGUAGE_OVERRIDES: &'static str = "language_overrides";
|
|
||||||
const LANGUAGES: &'static str = "languages";
|
|
||||||
|
|
||||||
let mut cursor = tree_sitter::QueryCursor::new();
|
|
||||||
|
|
||||||
let has_language_overrides = text.contains(LANGUAGE_OVERRIDES);
|
|
||||||
|
|
||||||
let mut depth = 0;
|
|
||||||
let mut last_value_range = 0..0;
|
|
||||||
let mut first_key_start = None;
|
|
||||||
let mut existing_value_range = 0..text.len();
|
|
||||||
let matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes());
|
|
||||||
for mat in matches {
|
|
||||||
if mat.captures.len() != 2 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let key_range = mat.captures[0].node.byte_range();
|
|
||||||
let value_range = mat.captures[1].node.byte_range();
|
|
||||||
|
|
||||||
// Don't enter sub objects until we find an exact
|
|
||||||
// match for the current keypath
|
|
||||||
if last_value_range.contains_inclusive(&value_range) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
last_value_range = value_range.clone();
|
|
||||||
|
|
||||||
if key_range.start > existing_value_range.end {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
first_key_start.get_or_insert_with(|| key_range.start);
|
|
||||||
|
|
||||||
let found_key = text
|
|
||||||
.get(key_range.clone())
|
|
||||||
.map(|key_text| {
|
|
||||||
if key_path[depth] == LANGUAGES && has_language_overrides {
|
|
||||||
return key_text == format!("\"{}\"", LANGUAGE_OVERRIDES);
|
|
||||||
} else {
|
|
||||||
return key_text == format!("\"{}\"", key_path[depth]);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
if found_key {
|
|
||||||
existing_value_range = value_range;
|
|
||||||
// Reset last value range when increasing in depth
|
|
||||||
last_value_range = existing_value_range.start..existing_value_range.start;
|
|
||||||
depth += 1;
|
|
||||||
|
|
||||||
if depth == key_path.len() {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
first_key_start = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We found the exact key we want, insert the new value
|
|
||||||
if depth == key_path.len() {
|
|
||||||
let new_val = to_pretty_json(&new_value, tab_size, tab_size * depth);
|
|
||||||
(existing_value_range, new_val)
|
|
||||||
} else {
|
|
||||||
// We have key paths, construct the sub objects
|
|
||||||
let new_key = if has_language_overrides && key_path[depth] == LANGUAGES {
|
|
||||||
LANGUAGE_OVERRIDES
|
|
||||||
} else {
|
|
||||||
key_path[depth]
|
|
||||||
};
|
|
||||||
|
|
||||||
// We don't have the key, construct the nested objects
|
|
||||||
let mut new_value = serde_json::to_value(new_value).unwrap();
|
|
||||||
for key in key_path[(depth + 1)..].iter().rev() {
|
|
||||||
if has_language_overrides && key == &LANGUAGES {
|
|
||||||
new_value = serde_json::json!({ LANGUAGE_OVERRIDES.to_string(): new_value });
|
|
||||||
} else {
|
|
||||||
new_value = serde_json::json!({ key.to_string(): new_value });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(first_key_start) = first_key_start {
|
|
||||||
let mut row = 0;
|
|
||||||
let mut column = 0;
|
|
||||||
for (ix, char) in text.char_indices() {
|
|
||||||
if ix == first_key_start {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if char == '\n' {
|
|
||||||
row += 1;
|
|
||||||
column = 0;
|
|
||||||
} else {
|
|
||||||
column += char.len_utf8();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if row > 0 {
|
|
||||||
// depth is 0 based, but division needs to be 1 based.
|
|
||||||
let new_val = to_pretty_json(&new_value, column / (depth + 1), column);
|
|
||||||
let space = ' ';
|
|
||||||
let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column);
|
|
||||||
(first_key_start..first_key_start, content)
|
|
||||||
} else {
|
|
||||||
let new_val = serde_json::to_string(&new_value).unwrap();
|
|
||||||
let mut content = format!(r#""{new_key}": {new_val},"#);
|
|
||||||
content.push(' ');
|
|
||||||
(first_key_start..first_key_start, content)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
new_value = serde_json::json!({ new_key.to_string(): new_value });
|
|
||||||
let indent_prefix_len = 4 * depth;
|
|
||||||
let mut new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
|
|
||||||
if depth == 0 {
|
|
||||||
new_val.push('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
(existing_value_range, new_val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_pretty_json(value: &impl Serialize, indent_size: usize, indent_prefix_len: usize) -> String {
|
|
||||||
const SPACES: [u8; 32] = [b' '; 32];
|
|
||||||
|
|
||||||
debug_assert!(indent_size <= SPACES.len());
|
|
||||||
debug_assert!(indent_prefix_len <= SPACES.len());
|
|
||||||
|
|
||||||
let mut output = Vec::new();
|
|
||||||
let mut ser = serde_json::Serializer::with_formatter(
|
|
||||||
&mut output,
|
|
||||||
serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]),
|
|
||||||
);
|
|
||||||
|
|
||||||
value.serialize(&mut ser).unwrap();
|
|
||||||
let text = String::from_utf8(output).unwrap();
|
|
||||||
|
|
||||||
let mut adjusted_text = String::new();
|
|
||||||
for (i, line) in text.split('\n').enumerate() {
|
|
||||||
if i > 0 {
|
|
||||||
adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap());
|
|
||||||
}
|
|
||||||
adjusted_text.push_str(line);
|
|
||||||
adjusted_text.push('\n');
|
|
||||||
}
|
|
||||||
adjusted_text.pop();
|
|
||||||
adjusted_text
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the settings file with the given callback.
|
|
||||||
///
|
|
||||||
/// Returns a new JSON string and the offset where the first edit occurred.
|
|
||||||
fn update_settings_file(
|
|
||||||
text: &str,
|
|
||||||
mut old_file_content: SettingsFileContent,
|
|
||||||
tab_size: NonZeroU32,
|
|
||||||
update: impl FnOnce(&mut SettingsFileContent),
|
|
||||||
) -> Vec<(Range<usize>, String)> {
|
|
||||||
let mut new_file_content = old_file_content.clone();
|
|
||||||
update(&mut new_file_content);
|
|
||||||
|
|
||||||
if new_file_content.languages.len() != old_file_content.languages.len() {
|
|
||||||
for language in new_file_content.languages.keys() {
|
|
||||||
old_file_content
|
|
||||||
.languages
|
|
||||||
.entry(language.clone())
|
|
||||||
.or_default();
|
|
||||||
}
|
|
||||||
for language in old_file_content.languages.keys() {
|
|
||||||
new_file_content
|
|
||||||
.languages
|
|
||||||
.entry(language.clone())
|
|
||||||
.or_default();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut parser = tree_sitter::Parser::new();
|
|
||||||
parser.set_language(tree_sitter_json::language()).unwrap();
|
|
||||||
let tree = parser.parse(text, None).unwrap();
|
|
||||||
|
|
||||||
let old_object = to_json_object(old_file_content);
|
|
||||||
let new_object = to_json_object(new_file_content);
|
|
||||||
let mut key_path = Vec::new();
|
|
||||||
let mut edits = Vec::new();
|
|
||||||
update_object_in_settings_file(
|
|
||||||
&old_object,
|
|
||||||
&new_object,
|
|
||||||
&text,
|
|
||||||
&tree,
|
|
||||||
tab_size.get() as usize,
|
|
||||||
&mut key_path,
|
|
||||||
&mut edits,
|
|
||||||
);
|
|
||||||
edits.sort_unstable_by_key(|e| e.0.start);
|
|
||||||
return edits;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_json_object(settings_file: SettingsFileContent) -> serde_json::Map<String, Value> {
|
|
||||||
let tmp = serde_json::to_value(settings_file).unwrap();
|
|
||||||
match tmp {
|
|
||||||
Value::Object(map) => map,
|
|
||||||
_ => unreachable!("SettingsFileContent represents a JSON map"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use unindent::Unindent;
|
|
||||||
|
|
||||||
fn assert_new_settings(
|
|
||||||
old_json: String,
|
|
||||||
update: fn(&mut SettingsFileContent),
|
|
||||||
expected_new_json: String,
|
|
||||||
) {
|
|
||||||
let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap_or_default();
|
|
||||||
let edits = update_settings_file(&old_json, old_content, 4.try_into().unwrap(), update);
|
|
||||||
let mut new_json = old_json;
|
|
||||||
for (range, replacement) in edits.into_iter().rev() {
|
|
||||||
new_json.replace_range(range, &replacement);
|
|
||||||
}
|
|
||||||
pretty_assertions::assert_eq!(new_json, expected_new_json);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_update_language_overrides_copilot() {
|
|
||||||
assert_new_settings(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"language_overrides": {
|
|
||||||
"JSON": {
|
|
||||||
"show_copilot_suggestions": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
|settings| {
|
|
||||||
settings.languages.insert(
|
|
||||||
"Rust".into(),
|
|
||||||
EditorSettings {
|
|
||||||
show_copilot_suggestions: Some(true),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"language_overrides": {
|
|
||||||
"Rust": {
|
|
||||||
"show_copilot_suggestions": true
|
|
||||||
},
|
|
||||||
"JSON": {
|
|
||||||
"show_copilot_suggestions": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_update_copilot_globs() {
|
|
||||||
assert_new_settings(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
|settings| {
|
|
||||||
settings.copilot = Some(CopilotSettingsContent {
|
|
||||||
disabled_globs: Some(vec![]),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"copilot": {
|
|
||||||
"disabled_globs": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_new_settings(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"copilot": {
|
|
||||||
"disabled_globs": [
|
|
||||||
"**/*.json"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
|settings| {
|
|
||||||
settings
|
|
||||||
.copilot
|
|
||||||
.get_or_insert(Default::default())
|
|
||||||
.disabled_globs
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.push(".env".into());
|
|
||||||
},
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"copilot": {
|
|
||||||
"disabled_globs": [
|
|
||||||
"**/*.json",
|
|
||||||
".env"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_update_copilot() {
|
|
||||||
assert_new_settings(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"languages": {
|
|
||||||
"JSON": {
|
|
||||||
"show_copilot_suggestions": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
|settings| {
|
|
||||||
settings.editor.show_copilot_suggestions = Some(true);
|
|
||||||
},
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"show_copilot_suggestions": true,
|
|
||||||
"languages": {
|
|
||||||
"JSON": {
|
|
||||||
"show_copilot_suggestions": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_update_language_copilot() {
|
|
||||||
assert_new_settings(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"languages": {
|
|
||||||
"JSON": {
|
|
||||||
"show_copilot_suggestions": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
|settings| {
|
|
||||||
settings.languages.insert(
|
|
||||||
"Rust".into(),
|
|
||||||
EditorSettings {
|
|
||||||
show_copilot_suggestions: Some(true),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"languages": {
|
|
||||||
"Rust": {
|
|
||||||
"show_copilot_suggestions": true
|
|
||||||
},
|
|
||||||
"JSON": {
|
|
||||||
"show_copilot_suggestions": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_update_telemetry_setting_multiple_fields() {
|
|
||||||
assert_new_settings(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"telemetry": {
|
|
||||||
"metrics": false,
|
|
||||||
"diagnostics": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
|settings| {
|
|
||||||
settings.telemetry.set_diagnostics(true);
|
|
||||||
settings.telemetry.set_metrics(true);
|
|
||||||
},
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"telemetry": {
|
|
||||||
"metrics": true,
|
|
||||||
"diagnostics": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_update_telemetry_setting_weird_formatting() {
|
|
||||||
assert_new_settings(
|
|
||||||
r#"{
|
|
||||||
"telemetry": { "metrics": false, "diagnostics": true }
|
|
||||||
}"#
|
|
||||||
.unindent(),
|
|
||||||
|settings| settings.telemetry.set_diagnostics(false),
|
|
||||||
r#"{
|
|
||||||
"telemetry": { "metrics": false, "diagnostics": false }
|
|
||||||
}"#
|
|
||||||
.unindent(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_update_telemetry_setting_other_fields() {
|
|
||||||
assert_new_settings(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"telemetry": {
|
|
||||||
"metrics": false,
|
|
||||||
"diagnostics": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
|settings| settings.telemetry.set_diagnostics(false),
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"telemetry": {
|
|
||||||
"metrics": false,
|
|
||||||
"diagnostics": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_update_telemetry_setting_empty_telemetry() {
|
|
||||||
assert_new_settings(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"telemetry": {}
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
|settings| settings.telemetry.set_diagnostics(false),
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"telemetry": {
|
|
||||||
"diagnostics": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_update_telemetry_setting_pre_existing() {
|
|
||||||
assert_new_settings(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"telemetry": {
|
|
||||||
"diagnostics": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
|settings| settings.telemetry.set_diagnostics(false),
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"telemetry": {
|
|
||||||
"diagnostics": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_update_telemetry_setting() {
|
|
||||||
assert_new_settings(
|
|
||||||
"{}".into(),
|
|
||||||
|settings| settings.telemetry.set_diagnostics(true),
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"telemetry": {
|
|
||||||
"diagnostics": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_update_object_empty_doc() {
|
|
||||||
assert_new_settings(
|
|
||||||
"".into(),
|
|
||||||
|settings| settings.telemetry.set_diagnostics(true),
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"telemetry": {
|
|
||||||
"diagnostics": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_write_theme_into_settings_with_theme() {
|
|
||||||
assert_new_settings(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"theme": "One Dark"
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
|settings| settings.theme = Some("summerfruit-light".to_string()),
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"theme": "summerfruit-light"
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_write_theme_into_empty_settings() {
|
|
||||||
assert_new_settings(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
|settings| settings.theme = Some("summerfruit-light".to_string()),
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"theme": "summerfruit-light"
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn write_key_no_document() {
|
|
||||||
assert_new_settings(
|
|
||||||
"".to_string(),
|
|
||||||
|settings| settings.theme = Some("summerfruit-light".to_string()),
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"theme": "summerfruit-light"
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_write_theme_into_single_line_settings_without_theme() {
|
|
||||||
assert_new_settings(
|
|
||||||
r#"{ "a": "", "ok": true }"#.to_string(),
|
|
||||||
|settings| settings.theme = Some("summerfruit-light".to_string()),
|
|
||||||
r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#.to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_write_theme_pre_object_whitespace() {
|
|
||||||
assert_new_settings(
|
|
||||||
r#" { "a": "", "ok": true }"#.to_string(),
|
|
||||||
|settings| settings.theme = Some("summerfruit-light".to_string()),
|
|
||||||
r#" { "theme": "summerfruit-light", "a": "", "ok": true }"#.unindent(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_write_theme_into_multi_line_settings_without_theme() {
|
|
||||||
assert_new_settings(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"a": "b"
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
|settings| settings.theme = Some("summerfruit-light".to_string()),
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"theme": "summerfruit-light",
|
|
||||||
"a": "b"
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,34 +1,134 @@
|
|||||||
use crate::{update_settings_file, watched_json::WatchedJsonFile, Settings, SettingsFileContent};
|
use crate::{
|
||||||
|
settings_store::parse_json_with_comments, settings_store::SettingsStore, KeymapFileContent,
|
||||||
|
Settings, SettingsFileContent, DEFAULT_SETTINGS_ASSET_PATH,
|
||||||
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use assets::Assets;
|
use assets::Assets;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::AppContext;
|
use futures::{channel::mpsc, StreamExt};
|
||||||
use std::{io::ErrorKind, ops::Range, path::Path, sync::Arc};
|
use gpui::{executor::Background, AppContext, AssetSource};
|
||||||
|
use std::{borrow::Cow, io::ErrorKind, path::PathBuf, str, sync::Arc, time::Duration};
|
||||||
|
use util::{paths, ResultExt};
|
||||||
|
|
||||||
// TODO: Switch SettingsFile to open a worktree and buffer for synchronization
|
pub fn default_settings() -> Cow<'static, str> {
|
||||||
// And instant updates in the Zed editor
|
match Assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap() {
|
||||||
#[derive(Clone)]
|
Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
|
||||||
pub struct SettingsFile {
|
Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
|
||||||
path: &'static Path,
|
}
|
||||||
settings_file_content: WatchedJsonFile<SettingsFileContent>,
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn test_settings() -> String {
|
||||||
|
let mut value =
|
||||||
|
parse_json_with_comments::<serde_json::Value>(default_settings().as_ref()).unwrap();
|
||||||
|
util::merge_non_null_json_value_into(
|
||||||
|
serde_json::json!({
|
||||||
|
"buffer_font_family": "Courier",
|
||||||
|
"buffer_font_features": {},
|
||||||
|
"default_buffer_font_size": 14,
|
||||||
|
"preferred_line_length": 80,
|
||||||
|
"theme": theme::EMPTY_THEME_NAME,
|
||||||
|
}),
|
||||||
|
&mut value,
|
||||||
|
);
|
||||||
|
serde_json::to_string(&value).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn watch_config_file(
|
||||||
|
executor: Arc<Background>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
|
path: PathBuf,
|
||||||
|
) -> mpsc::UnboundedReceiver<String> {
|
||||||
|
let (tx, rx) = mpsc::unbounded();
|
||||||
|
executor
|
||||||
|
.spawn(async move {
|
||||||
|
let events = fs.watch(&path, Duration::from_millis(100)).await;
|
||||||
|
futures::pin_mut!(events);
|
||||||
|
loop {
|
||||||
|
if let Ok(contents) = fs.load(&path).await {
|
||||||
|
if !tx.unbounded_send(contents).is_ok() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if events.next().await.is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
rx
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SettingsFile {
|
pub fn handle_keymap_file_changes(
|
||||||
pub fn new(
|
mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
|
||||||
path: &'static Path,
|
cx: &mut AppContext,
|
||||||
settings_file_content: WatchedJsonFile<SettingsFileContent>,
|
) {
|
||||||
fs: Arc<dyn Fs>,
|
cx.spawn(move |mut cx| async move {
|
||||||
) -> Self {
|
let mut settings_subscription = None;
|
||||||
SettingsFile {
|
while let Some(user_keymap_content) = user_keymap_file_rx.next().await {
|
||||||
path,
|
if let Ok(keymap_content) =
|
||||||
settings_file_content,
|
parse_json_with_comments::<KeymapFileContent>(&user_keymap_content)
|
||||||
fs,
|
{
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.clear_bindings();
|
||||||
|
KeymapFileContent::load_defaults(cx);
|
||||||
|
keymap_content.clone().add_to_cx(cx).log_err();
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut old_base_keymap = cx.read(|cx| cx.global::<Settings>().base_keymap.clone());
|
||||||
|
drop(settings_subscription);
|
||||||
|
settings_subscription = Some(cx.update(|cx| {
|
||||||
|
cx.observe_global::<Settings, _>(move |cx| {
|
||||||
|
let settings = cx.global::<Settings>();
|
||||||
|
if settings.base_keymap != old_base_keymap {
|
||||||
|
old_base_keymap = settings.base_keymap.clone();
|
||||||
|
|
||||||
|
cx.clear_bindings();
|
||||||
|
KeymapFileContent::load_defaults(cx);
|
||||||
|
keymap_content.clone().add_to_cx(cx).log_err();
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_settings(path: &Path, fs: &Arc<dyn Fs>) -> Result<String> {
|
pub fn handle_settings_file_changes(
|
||||||
match fs.load(path).await {
|
mut user_settings_file_rx: mpsc::UnboundedReceiver<String>,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) {
|
||||||
|
let user_settings_content = cx.background().block(user_settings_file_rx.next()).unwrap();
|
||||||
|
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||||
|
store
|
||||||
|
.set_user_settings(&user_settings_content, cx)
|
||||||
|
.log_err();
|
||||||
|
|
||||||
|
// TODO - remove the Settings global, use the SettingsStore instead.
|
||||||
|
store.register_setting::<Settings>(cx);
|
||||||
|
cx.set_global(store.get::<Settings>(None).clone());
|
||||||
|
});
|
||||||
|
cx.spawn(move |mut cx| async move {
|
||||||
|
while let Some(user_settings_content) = user_settings_file_rx.next().await {
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||||
|
store
|
||||||
|
.set_user_settings(&user_settings_content, cx)
|
||||||
|
.log_err();
|
||||||
|
|
||||||
|
// TODO - remove the Settings global, use the SettingsStore instead.
|
||||||
|
cx.set_global(store.get::<Settings>(None).clone());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_settings(fs: &Arc<dyn Fs>) -> Result<String> {
|
||||||
|
match fs.load(&paths::SETTINGS).await {
|
||||||
result @ Ok(_) => result,
|
result @ Ok(_) => result,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
if let Some(e) = err.downcast_ref::<std::io::Error>() {
|
if let Some(e) = err.downcast_ref::<std::io::Error>() {
|
||||||
@ -41,48 +141,41 @@ impl SettingsFile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_unsaved(
|
pub fn update_settings_file(
|
||||||
text: &str,
|
fs: Arc<dyn Fs>,
|
||||||
cx: &AppContext,
|
|
||||||
update: impl FnOnce(&mut SettingsFileContent),
|
|
||||||
) -> Vec<(Range<usize>, String)> {
|
|
||||||
let this = cx.global::<SettingsFile>();
|
|
||||||
let tab_size = cx.global::<Settings>().tab_size(Some("JSON"));
|
|
||||||
let current_file_content = this.settings_file_content.current();
|
|
||||||
update_settings_file(&text, current_file_content, tab_size, update)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(
|
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
update: impl 'static + Send + FnOnce(&mut SettingsFileContent),
|
update: impl 'static + Send + FnOnce(&mut SettingsFileContent),
|
||||||
) {
|
) {
|
||||||
let this = cx.global::<SettingsFile>();
|
cx.spawn(|cx| async move {
|
||||||
let tab_size = cx.global::<Settings>().tab_size(Some("JSON"));
|
let old_text = cx
|
||||||
let current_file_content = this.settings_file_content.current();
|
.background()
|
||||||
let fs = this.fs.clone();
|
.spawn({
|
||||||
let path = this.path.clone();
|
let fs = fs.clone();
|
||||||
|
async move { load_settings(&fs).await }
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let edits = cx.read(|cx| {
|
||||||
|
cx.global::<SettingsStore>()
|
||||||
|
.update::<Settings>(&old_text, update)
|
||||||
|
});
|
||||||
|
|
||||||
cx.background()
|
|
||||||
.spawn(async move {
|
|
||||||
let old_text = SettingsFile::load_settings(path, &fs).await?;
|
|
||||||
let edits = update_settings_file(&old_text, current_file_content, tab_size, update);
|
|
||||||
let mut new_text = old_text;
|
let mut new_text = old_text;
|
||||||
for (range, replacement) in edits.into_iter().rev() {
|
for (range, replacement) in edits.into_iter().rev() {
|
||||||
new_text.replace_range(range, &replacement);
|
new_text.replace_range(range, &replacement);
|
||||||
}
|
}
|
||||||
fs.atomic_write(path.to_path_buf(), new_text).await?;
|
|
||||||
|
cx.background()
|
||||||
|
.spawn(async move { fs.atomic_write(paths::SETTINGS.clone(), new_text).await })
|
||||||
|
.await?;
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx)
|
.detach_and_log_err(cx);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
|
||||||
watch_files, watched_json::watch_settings_file, EditorSettings, Settings, SoftWrap,
|
|
||||||
};
|
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
use gpui::{actions, elements::*, Action, Entity, TestAppContext, View, ViewContext};
|
use gpui::{actions, elements::*, Action, Entity, TestAppContext, View, ViewContext};
|
||||||
use theme::ThemeRegistry;
|
use theme::ThemeRegistry;
|
||||||
@ -107,7 +200,6 @@ mod tests {
|
|||||||
async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
|
async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
|
||||||
let executor = cx.background();
|
let executor = cx.background();
|
||||||
let fs = FakeFs::new(executor.clone());
|
let fs = FakeFs::new(executor.clone());
|
||||||
let font_cache = cx.font_cache();
|
|
||||||
|
|
||||||
actions!(test, [A, B]);
|
actions!(test, [A, B]);
|
||||||
// From the Atom keymap
|
// From the Atom keymap
|
||||||
@ -145,25 +237,26 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let settings_file =
|
|
||||||
WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
|
|
||||||
let keymaps_file =
|
|
||||||
WatchedJsonFile::new(fs.clone(), &executor, "/keymap.json".as_ref()).await;
|
|
||||||
|
|
||||||
let default_settings = cx.read(Settings::test);
|
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
|
let mut store = SettingsStore::default();
|
||||||
|
store.set_default_settings(&test_settings(), cx).unwrap();
|
||||||
|
cx.set_global(store);
|
||||||
|
cx.set_global(ThemeRegistry::new(Assets, cx.font_cache().clone()));
|
||||||
cx.add_global_action(|_: &A, _cx| {});
|
cx.add_global_action(|_: &A, _cx| {});
|
||||||
cx.add_global_action(|_: &B, _cx| {});
|
cx.add_global_action(|_: &B, _cx| {});
|
||||||
cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
|
cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
|
||||||
cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
|
cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
|
||||||
watch_files(
|
|
||||||
default_settings,
|
let settings_rx = watch_config_file(
|
||||||
settings_file,
|
executor.clone(),
|
||||||
ThemeRegistry::new((), font_cache),
|
fs.clone(),
|
||||||
keymaps_file,
|
PathBuf::from("/settings.json"),
|
||||||
cx,
|
);
|
||||||
)
|
let keymap_rx =
|
||||||
|
watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
|
||||||
|
|
||||||
|
handle_keymap_file_changes(keymap_rx, cx);
|
||||||
|
handle_settings_file_changes(settings_rx, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.foreground().run_until_parked();
|
cx.foreground().run_until_parked();
|
||||||
@ -255,113 +348,4 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_watch_settings_files(cx: &mut gpui::TestAppContext) {
|
|
||||||
let executor = cx.background();
|
|
||||||
let fs = FakeFs::new(executor.clone());
|
|
||||||
let font_cache = cx.font_cache();
|
|
||||||
|
|
||||||
fs.save(
|
|
||||||
"/settings.json".as_ref(),
|
|
||||||
&r#"
|
|
||||||
{
|
|
||||||
"buffer_font_size": 24,
|
|
||||||
"soft_wrap": "editor_width",
|
|
||||||
"tab_size": 8,
|
|
||||||
"language_overrides": {
|
|
||||||
"Markdown": {
|
|
||||||
"tab_size": 2,
|
|
||||||
"preferred_line_length": 100,
|
|
||||||
"soft_wrap": "preferred_line_length"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.into(),
|
|
||||||
Default::default(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let source = WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
|
|
||||||
|
|
||||||
let default_settings = cx.read(Settings::test).with_language_defaults(
|
|
||||||
"JavaScript",
|
|
||||||
EditorSettings {
|
|
||||||
tab_size: Some(2.try_into().unwrap()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
cx.update(|cx| {
|
|
||||||
watch_settings_file(
|
|
||||||
default_settings.clone(),
|
|
||||||
source,
|
|
||||||
ThemeRegistry::new((), font_cache),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.foreground().run_until_parked();
|
|
||||||
let settings = cx.read(|cx| cx.global::<Settings>().clone());
|
|
||||||
assert_eq!(settings.buffer_font_size, 24.0);
|
|
||||||
|
|
||||||
assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
|
|
||||||
assert_eq!(
|
|
||||||
settings.soft_wrap(Some("Markdown")),
|
|
||||||
SoftWrap::PreferredLineLength
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
settings.soft_wrap(Some("JavaScript")),
|
|
||||||
SoftWrap::EditorWidth
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(settings.preferred_line_length(None), 80);
|
|
||||||
assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
|
|
||||||
assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
|
|
||||||
|
|
||||||
assert_eq!(settings.tab_size(None).get(), 8);
|
|
||||||
assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
|
|
||||||
assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
|
|
||||||
|
|
||||||
fs.save(
|
|
||||||
"/settings.json".as_ref(),
|
|
||||||
&"(garbage)".into(),
|
|
||||||
Default::default(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
// fs.remove_file("/settings.json".as_ref(), Default::default())
|
|
||||||
// .await
|
|
||||||
// .unwrap();
|
|
||||||
|
|
||||||
cx.foreground().run_until_parked();
|
|
||||||
let settings = cx.read(|cx| cx.global::<Settings>().clone());
|
|
||||||
assert_eq!(settings.buffer_font_size, 24.0);
|
|
||||||
|
|
||||||
assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
|
|
||||||
assert_eq!(
|
|
||||||
settings.soft_wrap(Some("Markdown")),
|
|
||||||
SoftWrap::PreferredLineLength
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
settings.soft_wrap(Some("JavaScript")),
|
|
||||||
SoftWrap::EditorWidth
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(settings.preferred_line_length(None), 80);
|
|
||||||
assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
|
|
||||||
assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
|
|
||||||
|
|
||||||
assert_eq!(settings.tab_size(None).get(), 8);
|
|
||||||
assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
|
|
||||||
assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
|
|
||||||
|
|
||||||
fs.remove_file("/settings.json".as_ref(), Default::default())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
cx.foreground().run_until_parked();
|
|
||||||
let settings = cx.read(|cx| cx.global::<Settings>().clone());
|
|
||||||
assert_eq!(settings.buffer_font_size, default_settings.buffer_font_size);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
||||||
|
use gpui::AppContext;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{de::DeserializeOwned, Deserialize as _, Serialize};
|
use serde::{de::DeserializeOwned, Deserialize as _, Serialize};
|
||||||
@ -18,7 +19,7 @@ use util::{merge_non_null_json_value_into, RangeExt, ResultExt as _};
|
|||||||
/// A value that can be defined as a user setting.
|
/// A value that can be defined as a user setting.
|
||||||
///
|
///
|
||||||
/// Settings can be loaded from a combination of multiple JSON files.
|
/// Settings can be loaded from a combination of multiple JSON files.
|
||||||
pub trait Setting: 'static + Debug {
|
pub trait Setting: 'static {
|
||||||
/// The name of a key within the JSON file from which this setting should
|
/// The name of a key within the JSON file from which this setting should
|
||||||
/// be deserialized. If this is `None`, then the setting will be deserialized
|
/// be deserialized. If this is `None`, then the setting will be deserialized
|
||||||
/// from the root object.
|
/// from the root object.
|
||||||
@ -32,7 +33,11 @@ pub trait Setting: 'static + Debug {
|
|||||||
///
|
///
|
||||||
/// The user values are ordered from least specific (the global settings file)
|
/// The user values are ordered from least specific (the global settings file)
|
||||||
/// to most specific (the innermost local settings file).
|
/// to most specific (the innermost local settings file).
|
||||||
fn load(default_value: &Self::FileContent, user_values: &[&Self::FileContent]) -> Self;
|
fn load(
|
||||||
|
default_value: &Self::FileContent,
|
||||||
|
user_values: &[&Self::FileContent],
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> Self;
|
||||||
|
|
||||||
fn load_via_json_merge(
|
fn load_via_json_merge(
|
||||||
default_value: &Self::FileContent,
|
default_value: &Self::FileContent,
|
||||||
@ -66,7 +71,7 @@ struct SettingValue<T> {
|
|||||||
local_values: Vec<(Arc<Path>, T)>,
|
local_values: Vec<(Arc<Path>, T)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
trait AnySettingValue: Debug {
|
trait AnySettingValue {
|
||||||
fn key(&self) -> Option<&'static str>;
|
fn key(&self) -> Option<&'static str>;
|
||||||
fn setting_type_name(&self) -> &'static str;
|
fn setting_type_name(&self) -> &'static str;
|
||||||
fn deserialize_setting(&self, json: &serde_json::Value) -> Result<DeserializedSetting>;
|
fn deserialize_setting(&self, json: &serde_json::Value) -> Result<DeserializedSetting>;
|
||||||
@ -74,6 +79,7 @@ trait AnySettingValue: Debug {
|
|||||||
&self,
|
&self,
|
||||||
default_value: &DeserializedSetting,
|
default_value: &DeserializedSetting,
|
||||||
custom: &[&DeserializedSetting],
|
custom: &[&DeserializedSetting],
|
||||||
|
cx: &AppContext,
|
||||||
) -> Box<dyn Any>;
|
) -> Box<dyn Any>;
|
||||||
fn value_for_path(&self, path: Option<&Path>) -> &dyn Any;
|
fn value_for_path(&self, path: Option<&Path>) -> &dyn Any;
|
||||||
fn set_global_value(&mut self, value: Box<dyn Any>);
|
fn set_global_value(&mut self, value: Box<dyn Any>);
|
||||||
@ -89,7 +95,7 @@ struct DeserializedSettingMap {
|
|||||||
|
|
||||||
impl SettingsStore {
|
impl SettingsStore {
|
||||||
/// Add a new type of setting to the store.
|
/// Add a new type of setting to the store.
|
||||||
pub fn register_setting<T: Setting>(&mut self) {
|
pub fn register_setting<T: Setting>(&mut self, cx: &AppContext) {
|
||||||
let setting_type_id = TypeId::of::<T>();
|
let setting_type_id = TypeId::of::<T>();
|
||||||
|
|
||||||
let entry = self.setting_values.entry(setting_type_id);
|
let entry = self.setting_values.entry(setting_type_id);
|
||||||
@ -112,24 +118,26 @@ impl SettingsStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(default_deserialized_value) = default_settings.typed.get(&setting_type_id) {
|
if let Some(default_deserialized_value) = default_settings.typed.get(&setting_type_id) {
|
||||||
setting_value.set_global_value(
|
setting_value.set_global_value(setting_value.load_setting(
|
||||||
setting_value.load_setting(default_deserialized_value, &user_values_stack),
|
default_deserialized_value,
|
||||||
);
|
&user_values_stack,
|
||||||
|
cx,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the value of a setting.
|
/// Get the value of a setting.
|
||||||
///
|
///
|
||||||
/// Panics if settings have not yet been loaded, or there is no default
|
/// Panics if the given setting type has not been registered, or if there is no
|
||||||
/// value for this setting.
|
/// value for this setting.
|
||||||
pub fn get<T: Setting>(&self, path: Option<&Path>) -> &T {
|
pub fn get<T: Setting>(&self, path: Option<&Path>) -> &T {
|
||||||
self.setting_values
|
self.setting_values
|
||||||
.get(&TypeId::of::<T>())
|
.get(&TypeId::of::<T>())
|
||||||
.unwrap()
|
.expect("unregistered setting type")
|
||||||
.value_for_path(path)
|
.value_for_path(path)
|
||||||
.downcast_ref::<T>()
|
.downcast_ref::<T>()
|
||||||
.unwrap()
|
.expect("no default value for setting type")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the value of a setting.
|
/// Update the value of a setting.
|
||||||
@ -138,7 +146,7 @@ impl SettingsStore {
|
|||||||
pub fn update<T: Setting>(
|
pub fn update<T: Setting>(
|
||||||
&self,
|
&self,
|
||||||
text: &str,
|
text: &str,
|
||||||
update: impl Fn(&mut T::FileContent),
|
update: impl FnOnce(&mut T::FileContent),
|
||||||
) -> Vec<(Range<usize>, String)> {
|
) -> Vec<(Range<usize>, String)> {
|
||||||
let setting_type_id = TypeId::of::<T>();
|
let setting_type_id = TypeId::of::<T>();
|
||||||
let old_content = self
|
let old_content = self
|
||||||
@ -210,7 +218,11 @@ impl SettingsStore {
|
|||||||
/// Set the default settings via a JSON string.
|
/// Set the default settings via a JSON string.
|
||||||
///
|
///
|
||||||
/// The string should contain a JSON object with a default value for every setting.
|
/// The string should contain a JSON object with a default value for every setting.
|
||||||
pub fn set_default_settings(&mut self, default_settings_content: &str) -> Result<()> {
|
pub fn set_default_settings(
|
||||||
|
&mut self,
|
||||||
|
default_settings_content: &str,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Result<()> {
|
||||||
let deserialized_setting_map = self.load_setting_map(default_settings_content)?;
|
let deserialized_setting_map = self.load_setting_map(default_settings_content)?;
|
||||||
if deserialized_setting_map.typed.len() != self.setting_values.len() {
|
if deserialized_setting_map.typed.len() != self.setting_values.len() {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
@ -223,16 +235,20 @@ impl SettingsStore {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
self.default_deserialized_settings = Some(deserialized_setting_map);
|
self.default_deserialized_settings = Some(deserialized_setting_map);
|
||||||
self.recompute_values(false, None, None);
|
self.recompute_values(false, None, None, cx);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the user settings via a JSON string.
|
/// Set the user settings via a JSON string.
|
||||||
pub fn set_user_settings(&mut self, user_settings_content: &str) -> Result<()> {
|
pub fn set_user_settings(
|
||||||
|
&mut self,
|
||||||
|
user_settings_content: &str,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Result<()> {
|
||||||
let user_settings = self.load_setting_map(user_settings_content)?;
|
let user_settings = self.load_setting_map(user_settings_content)?;
|
||||||
let old_user_settings =
|
let old_user_settings =
|
||||||
mem::replace(&mut self.user_deserialized_settings, Some(user_settings));
|
mem::replace(&mut self.user_deserialized_settings, Some(user_settings));
|
||||||
self.recompute_values(true, None, old_user_settings);
|
self.recompute_values(true, None, old_user_settings, cx);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,6 +257,7 @@ impl SettingsStore {
|
|||||||
&mut self,
|
&mut self,
|
||||||
path: Arc<Path>,
|
path: Arc<Path>,
|
||||||
settings_content: Option<&str>,
|
settings_content: Option<&str>,
|
||||||
|
cx: &mut AppContext,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let removed_map = if let Some(settings_content) = settings_content {
|
let removed_map = if let Some(settings_content) = settings_content {
|
||||||
self.local_deserialized_settings
|
self.local_deserialized_settings
|
||||||
@ -249,7 +266,7 @@ impl SettingsStore {
|
|||||||
} else {
|
} else {
|
||||||
self.local_deserialized_settings.remove(&path)
|
self.local_deserialized_settings.remove(&path)
|
||||||
};
|
};
|
||||||
self.recompute_values(true, Some(&path), removed_map);
|
self.recompute_values(true, Some(&path), removed_map, cx);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,6 +275,7 @@ impl SettingsStore {
|
|||||||
user_settings_changed: bool,
|
user_settings_changed: bool,
|
||||||
changed_local_path: Option<&Path>,
|
changed_local_path: Option<&Path>,
|
||||||
old_settings_map: Option<DeserializedSettingMap>,
|
old_settings_map: Option<DeserializedSettingMap>,
|
||||||
|
cx: &AppContext,
|
||||||
) {
|
) {
|
||||||
// Identify all of the setting types that have changed.
|
// Identify all of the setting types that have changed.
|
||||||
let new_settings_map = if let Some(changed_path) = changed_local_path {
|
let new_settings_map = if let Some(changed_path) = changed_local_path {
|
||||||
@ -300,9 +318,11 @@ impl SettingsStore {
|
|||||||
|
|
||||||
// If the global settings file changed, reload the global value for the field.
|
// If the global settings file changed, reload the global value for the field.
|
||||||
if changed_local_path.is_none() {
|
if changed_local_path.is_none() {
|
||||||
setting_value.set_global_value(
|
setting_value.set_global_value(setting_value.load_setting(
|
||||||
setting_value.load_setting(default_deserialized_value, &user_values_stack),
|
default_deserialized_value,
|
||||||
);
|
&user_values_stack,
|
||||||
|
cx,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload the local values for the setting.
|
// Reload the local values for the setting.
|
||||||
@ -344,7 +364,7 @@ impl SettingsStore {
|
|||||||
// Load the local value for the field.
|
// Load the local value for the field.
|
||||||
setting_value.set_local_value(
|
setting_value.set_local_value(
|
||||||
path.clone(),
|
path.clone(),
|
||||||
setting_value.load_setting(default_deserialized_value, &user_values_stack),
|
setting_value.load_setting(default_deserialized_value, &user_values_stack, cx),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -398,13 +418,14 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
|
|||||||
&self,
|
&self,
|
||||||
default_value: &DeserializedSetting,
|
default_value: &DeserializedSetting,
|
||||||
user_values: &[&DeserializedSetting],
|
user_values: &[&DeserializedSetting],
|
||||||
|
cx: &AppContext,
|
||||||
) -> Box<dyn Any> {
|
) -> Box<dyn Any> {
|
||||||
let default_value = default_value.0.downcast_ref::<T::FileContent>().unwrap();
|
let default_value = default_value.0.downcast_ref::<T::FileContent>().unwrap();
|
||||||
let values: SmallVec<[&T::FileContent; 6]> = user_values
|
let values: SmallVec<[&T::FileContent; 6]> = user_values
|
||||||
.iter()
|
.iter()
|
||||||
.map(|value| value.0.downcast_ref().unwrap())
|
.map(|value| value.0.downcast_ref().unwrap())
|
||||||
.collect();
|
.collect();
|
||||||
Box::new(T::load(default_value, &values))
|
Box::new(T::load(default_value, &values, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_setting(&self, json: &serde_json::Value) -> Result<DeserializedSetting> {
|
fn deserialize_setting(&self, json: &serde_json::Value) -> Result<DeserializedSetting> {
|
||||||
@ -420,7 +441,9 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.global_value.as_ref().unwrap()
|
self.global_value
|
||||||
|
.as_ref()
|
||||||
|
.expect("no default value for setting")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_global_value(&mut self, value: Box<dyn Any>) {
|
fn set_global_value(&mut self, value: Box<dyn Any>) {
|
||||||
@ -436,21 +459,21 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for SettingsStore {
|
// impl Debug for SettingsStore {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
return f
|
// return f
|
||||||
.debug_struct("SettingsStore")
|
// .debug_struct("SettingsStore")
|
||||||
.field(
|
// .field(
|
||||||
"setting_value_sets_by_type",
|
// "setting_value_sets_by_type",
|
||||||
&self
|
// &self
|
||||||
.setting_values
|
// .setting_values
|
||||||
.values()
|
// .values()
|
||||||
.map(|set| (set.setting_type_name(), set))
|
// .map(|set| (set.setting_type_name(), set))
|
||||||
.collect::<HashMap<_, _>>(),
|
// .collect::<HashMap<_, _>>(),
|
||||||
)
|
// )
|
||||||
.finish_non_exhaustive();
|
// .finish_non_exhaustive();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
fn update_value_in_json_text<'a>(
|
fn update_value_in_json_text<'a>(
|
||||||
text: &str,
|
text: &str,
|
||||||
@ -503,14 +526,6 @@ fn update_value_in_json_text<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref PAIR_QUERY: tree_sitter::Query = tree_sitter::Query::new(
|
|
||||||
tree_sitter_json::language(),
|
|
||||||
"(pair key: (string) @key value: (_) @value)",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn replace_value_in_json_text(
|
fn replace_value_in_json_text(
|
||||||
text: &str,
|
text: &str,
|
||||||
syntax_tree: &tree_sitter::Tree,
|
syntax_tree: &tree_sitter::Tree,
|
||||||
@ -521,6 +536,14 @@ fn replace_value_in_json_text(
|
|||||||
const LANGUAGE_OVERRIDES: &'static str = "language_overrides";
|
const LANGUAGE_OVERRIDES: &'static str = "language_overrides";
|
||||||
const LANGUAGES: &'static str = "languages";
|
const LANGUAGES: &'static str = "languages";
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref PAIR_QUERY: tree_sitter::Query = tree_sitter::Query::new(
|
||||||
|
tree_sitter_json::language(),
|
||||||
|
"(pair key: (string) @key value: (_) @value)",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
let mut cursor = tree_sitter::QueryCursor::new();
|
let mut cursor = tree_sitter::QueryCursor::new();
|
||||||
|
|
||||||
let has_language_overrides = text.contains(LANGUAGE_OVERRIDES);
|
let has_language_overrides = text.contains(LANGUAGE_OVERRIDES);
|
||||||
@ -666,7 +689,7 @@ fn to_pretty_json(value: &impl Serialize, indent_size: usize, indent_prefix_len:
|
|||||||
adjusted_text
|
adjusted_text
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
|
pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
|
||||||
Ok(serde_json::from_reader(
|
Ok(serde_json::from_reader(
|
||||||
json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
|
json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
|
||||||
)?)
|
)?)
|
||||||
@ -678,12 +701,12 @@ mod tests {
|
|||||||
use serde_derive::Deserialize;
|
use serde_derive::Deserialize;
|
||||||
use unindent::Unindent;
|
use unindent::Unindent;
|
||||||
|
|
||||||
#[test]
|
#[gpui::test]
|
||||||
fn test_settings_store_basic() {
|
fn test_settings_store_basic(cx: &mut AppContext) {
|
||||||
let mut store = SettingsStore::default();
|
let mut store = SettingsStore::default();
|
||||||
store.register_setting::<UserSettings>();
|
store.register_setting::<UserSettings>(cx);
|
||||||
store.register_setting::<TurboSetting>();
|
store.register_setting::<TurboSetting>(cx);
|
||||||
store.register_setting::<MultiKeySettings>();
|
store.register_setting::<MultiKeySettings>(cx);
|
||||||
|
|
||||||
// error - missing required field in default settings
|
// error - missing required field in default settings
|
||||||
store
|
store
|
||||||
@ -695,6 +718,7 @@ mod tests {
|
|||||||
"staff": false
|
"staff": false
|
||||||
}
|
}
|
||||||
}"#,
|
}"#,
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
@ -709,6 +733,7 @@ mod tests {
|
|||||||
"staff": false
|
"staff": false
|
||||||
}
|
}
|
||||||
}"#,
|
}"#,
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
@ -723,6 +748,7 @@ mod tests {
|
|||||||
"staff": false
|
"staff": false
|
||||||
}
|
}
|
||||||
}"#,
|
}"#,
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@ -750,6 +776,7 @@ mod tests {
|
|||||||
"user": { "age": 31 },
|
"user": { "age": 31 },
|
||||||
"key1": "a"
|
"key1": "a"
|
||||||
}"#,
|
}"#,
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@ -767,12 +794,14 @@ mod tests {
|
|||||||
.set_local_settings(
|
.set_local_settings(
|
||||||
Path::new("/root1").into(),
|
Path::new("/root1").into(),
|
||||||
Some(r#"{ "user": { "staff": true } }"#),
|
Some(r#"{ "user": { "staff": true } }"#),
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
store
|
store
|
||||||
.set_local_settings(
|
.set_local_settings(
|
||||||
Path::new("/root1/subdir").into(),
|
Path::new("/root1/subdir").into(),
|
||||||
Some(r#"{ "user": { "name": "Jane Doe" } }"#),
|
Some(r#"{ "user": { "name": "Jane Doe" } }"#),
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@ -780,6 +809,7 @@ mod tests {
|
|||||||
.set_local_settings(
|
.set_local_settings(
|
||||||
Path::new("/root2").into(),
|
Path::new("/root2").into(),
|
||||||
Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#),
|
Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#),
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@ -816,8 +846,8 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[gpui::test]
|
||||||
fn test_setting_store_assign_json_before_register() {
|
fn test_setting_store_assign_json_before_register(cx: &mut AppContext) {
|
||||||
let mut store = SettingsStore::default();
|
let mut store = SettingsStore::default();
|
||||||
store
|
store
|
||||||
.set_default_settings(
|
.set_default_settings(
|
||||||
@ -830,11 +860,14 @@ mod tests {
|
|||||||
},
|
},
|
||||||
"key1": "x"
|
"key1": "x"
|
||||||
}"#,
|
}"#,
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
store.set_user_settings(r#"{ "turbo": false }"#).unwrap();
|
store
|
||||||
store.register_setting::<UserSettings>();
|
.set_user_settings(r#"{ "turbo": false }"#, cx)
|
||||||
store.register_setting::<TurboSetting>();
|
.unwrap();
|
||||||
|
store.register_setting::<UserSettings>(cx);
|
||||||
|
store.register_setting::<TurboSetting>(cx);
|
||||||
|
|
||||||
assert_eq!(store.get::<TurboSetting>(None), &TurboSetting(false));
|
assert_eq!(store.get::<TurboSetting>(None), &TurboSetting(false));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -846,7 +879,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
store.register_setting::<MultiKeySettings>();
|
store.register_setting::<MultiKeySettings>(cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
store.get::<MultiKeySettings>(None),
|
store.get::<MultiKeySettings>(None),
|
||||||
&MultiKeySettings {
|
&MultiKeySettings {
|
||||||
@ -856,11 +889,12 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[gpui::test]
|
||||||
fn test_setting_store_update() {
|
fn test_setting_store_update(cx: &mut AppContext) {
|
||||||
let mut store = SettingsStore::default();
|
let mut store = SettingsStore::default();
|
||||||
store.register_setting::<UserSettings>();
|
store.register_setting::<MultiKeySettings>(cx);
|
||||||
store.register_setting::<LanguageSettings>();
|
store.register_setting::<UserSettings>(cx);
|
||||||
|
store.register_setting::<LanguageSettings>(cx);
|
||||||
|
|
||||||
// entries added and updated
|
// entries added and updated
|
||||||
check_settings_update::<LanguageSettings>(
|
check_settings_update::<LanguageSettings>(
|
||||||
@ -890,6 +924,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}"#
|
}"#
|
||||||
.unindent(),
|
.unindent(),
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
// weird formatting
|
// weird formatting
|
||||||
@ -904,6 +939,33 @@ mod tests {
|
|||||||
"user": { "age": 37, "name": "Max", "staff": true }
|
"user": { "age": 37, "name": "Max", "staff": true }
|
||||||
}"#
|
}"#
|
||||||
.unindent(),
|
.unindent(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
// single-line formatting, other keys
|
||||||
|
check_settings_update::<MultiKeySettings>(
|
||||||
|
&mut store,
|
||||||
|
r#"{ "one": 1, "two": 2 }"#.unindent(),
|
||||||
|
|settings| settings.key1 = Some("x".into()),
|
||||||
|
r#"{ "key1": "x", "one": 1, "two": 2 }"#.unindent(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
// empty object
|
||||||
|
check_settings_update::<UserSettings>(
|
||||||
|
&mut store,
|
||||||
|
r#"{
|
||||||
|
"user": {}
|
||||||
|
}"#
|
||||||
|
.unindent(),
|
||||||
|
|settings| settings.age = Some(37),
|
||||||
|
r#"{
|
||||||
|
"user": {
|
||||||
|
"age": 37
|
||||||
|
}
|
||||||
|
}"#
|
||||||
|
.unindent(),
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
// no content
|
// no content
|
||||||
@ -918,6 +980,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
"#
|
"#
|
||||||
.unindent(),
|
.unindent(),
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -926,8 +989,9 @@ mod tests {
|
|||||||
old_json: String,
|
old_json: String,
|
||||||
update: fn(&mut T::FileContent),
|
update: fn(&mut T::FileContent),
|
||||||
expected_new_json: String,
|
expected_new_json: String,
|
||||||
|
cx: &mut AppContext,
|
||||||
) {
|
) {
|
||||||
store.set_user_settings(&old_json).ok();
|
store.set_user_settings(&old_json, cx).ok();
|
||||||
let edits = store.update::<T>(&old_json, update);
|
let edits = store.update::<T>(&old_json, update);
|
||||||
let mut new_json = old_json;
|
let mut new_json = old_json;
|
||||||
for (range, replacement) in edits.into_iter().rev() {
|
for (range, replacement) in edits.into_iter().rev() {
|
||||||
@ -954,7 +1018,11 @@ mod tests {
|
|||||||
const KEY: Option<&'static str> = Some("user");
|
const KEY: Option<&'static str> = Some("user");
|
||||||
type FileContent = UserSettingsJson;
|
type FileContent = UserSettingsJson;
|
||||||
|
|
||||||
fn load(default_value: &UserSettingsJson, user_values: &[&UserSettingsJson]) -> Self {
|
fn load(
|
||||||
|
default_value: &UserSettingsJson,
|
||||||
|
user_values: &[&UserSettingsJson],
|
||||||
|
_: &AppContext,
|
||||||
|
) -> Self {
|
||||||
Self::load_via_json_merge(default_value, user_values)
|
Self::load_via_json_merge(default_value, user_values)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -966,7 +1034,11 @@ mod tests {
|
|||||||
const KEY: Option<&'static str> = Some("turbo");
|
const KEY: Option<&'static str> = Some("turbo");
|
||||||
type FileContent = Option<bool>;
|
type FileContent = Option<bool>;
|
||||||
|
|
||||||
fn load(default_value: &Option<bool>, user_values: &[&Option<bool>]) -> Self {
|
fn load(
|
||||||
|
default_value: &Option<bool>,
|
||||||
|
user_values: &[&Option<bool>],
|
||||||
|
_: &AppContext,
|
||||||
|
) -> Self {
|
||||||
Self::load_via_json_merge(default_value, user_values)
|
Self::load_via_json_merge(default_value, user_values)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -991,6 +1063,7 @@ mod tests {
|
|||||||
fn load(
|
fn load(
|
||||||
default_value: &MultiKeySettingsJson,
|
default_value: &MultiKeySettingsJson,
|
||||||
user_values: &[&MultiKeySettingsJson],
|
user_values: &[&MultiKeySettingsJson],
|
||||||
|
_: &AppContext,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self::load_via_json_merge(default_value, user_values)
|
Self::load_via_json_merge(default_value, user_values)
|
||||||
}
|
}
|
||||||
@ -1020,7 +1093,11 @@ mod tests {
|
|||||||
|
|
||||||
type FileContent = JournalSettingsJson;
|
type FileContent = JournalSettingsJson;
|
||||||
|
|
||||||
fn load(default_value: &JournalSettingsJson, user_values: &[&JournalSettingsJson]) -> Self {
|
fn load(
|
||||||
|
default_value: &JournalSettingsJson,
|
||||||
|
user_values: &[&JournalSettingsJson],
|
||||||
|
_: &AppContext,
|
||||||
|
) -> Self {
|
||||||
Self::load_via_json_merge(default_value, user_values)
|
Self::load_via_json_merge(default_value, user_values)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1041,7 +1118,7 @@ mod tests {
|
|||||||
|
|
||||||
type FileContent = Self;
|
type FileContent = Self;
|
||||||
|
|
||||||
fn load(default_value: &Self, user_values: &[&Self]) -> Self {
|
fn load(default_value: &Self, user_values: &[&Self], _: &AppContext) -> Self {
|
||||||
Self::load_via_json_merge(default_value, user_values)
|
Self::load_via_json_merge(default_value, user_values)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,9 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = ["gpui/test-support"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
path = "src/theme.rs"
|
path = "src/theme.rs"
|
||||||
doctest = false
|
doctest = false
|
||||||
|
@ -20,15 +20,26 @@ pub struct ThemeRegistry {
|
|||||||
next_theme_id: AtomicUsize,
|
next_theme_id: AtomicUsize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub const EMPTY_THEME_NAME: &'static str = "empty-theme";
|
||||||
|
|
||||||
impl ThemeRegistry {
|
impl ThemeRegistry {
|
||||||
pub fn new(source: impl AssetSource, font_cache: Arc<FontCache>) -> Arc<Self> {
|
pub fn new(source: impl AssetSource, font_cache: Arc<FontCache>) -> Arc<Self> {
|
||||||
Arc::new(Self {
|
let this = Arc::new(Self {
|
||||||
assets: Box::new(source),
|
assets: Box::new(source),
|
||||||
themes: Default::default(),
|
themes: Default::default(),
|
||||||
theme_data: Default::default(),
|
theme_data: Default::default(),
|
||||||
next_theme_id: Default::default(),
|
next_theme_id: Default::default(),
|
||||||
font_cache,
|
font_cache,
|
||||||
})
|
});
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
this.themes.lock().insert(
|
||||||
|
EMPTY_THEME_NAME.to_string(),
|
||||||
|
gpui::fonts::with_font_cache(this.font_cache.clone(), || Arc::new(Theme::default())),
|
||||||
|
);
|
||||||
|
|
||||||
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list(&self, staff: bool) -> impl Iterator<Item = ThemeMeta> + '_ {
|
pub fn list(&self, staff: bool) -> impl Iterator<Item = ThemeMeta> + '_ {
|
||||||
|
@ -11,6 +11,7 @@ doctest = false
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
|
fs = { path = "../fs" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
picker = { path = "../picker" }
|
picker = { path = "../picker" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
use fs::Fs;
|
||||||
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
||||||
use gpui::{actions, elements::*, AnyElement, AppContext, Element, MouseState, ViewContext};
|
use gpui::{actions, elements::*, AnyElement, AppContext, Element, MouseState, ViewContext};
|
||||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||||
use settings::{settings_file::SettingsFile, Settings};
|
use settings::{update_settings_file, Settings};
|
||||||
use staff_mode::StaffMode;
|
use staff_mode::StaffMode;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use theme::{Theme, ThemeMeta, ThemeRegistry};
|
use theme::{Theme, ThemeMeta, ThemeRegistry};
|
||||||
@ -18,7 +19,8 @@ pub fn init(cx: &mut AppContext) {
|
|||||||
pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||||
workspace.toggle_modal(cx, |workspace, cx| {
|
workspace.toggle_modal(cx, |workspace, cx| {
|
||||||
let themes = workspace.app_state().themes.clone();
|
let themes = workspace.app_state().themes.clone();
|
||||||
cx.add_view(|cx| ThemeSelector::new(ThemeSelectorDelegate::new(themes, cx), cx))
|
let fs = workspace.app_state().fs.clone();
|
||||||
|
cx.add_view(|cx| ThemeSelector::new(ThemeSelectorDelegate::new(fs, themes, cx), cx))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,6 +42,7 @@ pub fn reload(themes: Arc<ThemeRegistry>, cx: &mut AppContext) {
|
|||||||
pub type ThemeSelector = Picker<ThemeSelectorDelegate>;
|
pub type ThemeSelector = Picker<ThemeSelectorDelegate>;
|
||||||
|
|
||||||
pub struct ThemeSelectorDelegate {
|
pub struct ThemeSelectorDelegate {
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
registry: Arc<ThemeRegistry>,
|
registry: Arc<ThemeRegistry>,
|
||||||
theme_data: Vec<ThemeMeta>,
|
theme_data: Vec<ThemeMeta>,
|
||||||
matches: Vec<StringMatch>,
|
matches: Vec<StringMatch>,
|
||||||
@ -49,7 +52,11 @@ pub struct ThemeSelectorDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ThemeSelectorDelegate {
|
impl ThemeSelectorDelegate {
|
||||||
fn new(registry: Arc<ThemeRegistry>, cx: &mut ViewContext<ThemeSelector>) -> Self {
|
fn new(
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
registry: Arc<ThemeRegistry>,
|
||||||
|
cx: &mut ViewContext<ThemeSelector>,
|
||||||
|
) -> Self {
|
||||||
let settings = cx.global::<Settings>();
|
let settings = cx.global::<Settings>();
|
||||||
|
|
||||||
let original_theme = settings.theme.clone();
|
let original_theme = settings.theme.clone();
|
||||||
@ -68,6 +75,7 @@ impl ThemeSelectorDelegate {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
|
fs,
|
||||||
registry,
|
registry,
|
||||||
theme_data: theme_names,
|
theme_data: theme_names,
|
||||||
matches,
|
matches,
|
||||||
@ -121,7 +129,7 @@ impl PickerDelegate for ThemeSelectorDelegate {
|
|||||||
self.selection_completed = true;
|
self.selection_completed = true;
|
||||||
|
|
||||||
let theme_name = cx.global::<Settings>().theme.meta.name.clone();
|
let theme_name = cx.global::<Settings>().theme.meta.name.clone();
|
||||||
SettingsFile::update(cx, |settings_content| {
|
update_settings_file(self.fs.clone(), cx, |settings_content| {
|
||||||
settings_content.theme = Some(theme_name);
|
settings_content.theme = Some(theme_name);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ test-support = []
|
|||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
|
fs = { path = "../fs" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
db = { path = "../db" }
|
db = { path = "../db" }
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions,
|
||||||
@ -7,7 +5,9 @@ use gpui::{
|
|||||||
AppContext, Task, ViewContext,
|
AppContext, Task, ViewContext,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||||
use settings::{settings_file::SettingsFile, BaseKeymap, Settings};
|
use project::Fs;
|
||||||
|
use settings::{update_settings_file, BaseKeymap, Settings};
|
||||||
|
use std::sync::Arc;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
@ -23,8 +23,9 @@ pub fn toggle(
|
|||||||
_: &ToggleBaseKeymapSelector,
|
_: &ToggleBaseKeymapSelector,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) {
|
) {
|
||||||
workspace.toggle_modal(cx, |_, cx| {
|
workspace.toggle_modal(cx, |workspace, cx| {
|
||||||
cx.add_view(|cx| BaseKeymapSelector::new(BaseKeymapSelectorDelegate::new(cx), cx))
|
let fs = workspace.app_state().fs.clone();
|
||||||
|
cx.add_view(|cx| BaseKeymapSelector::new(BaseKeymapSelectorDelegate::new(fs, cx), cx))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,10 +34,11 @@ pub type BaseKeymapSelector = Picker<BaseKeymapSelectorDelegate>;
|
|||||||
pub struct BaseKeymapSelectorDelegate {
|
pub struct BaseKeymapSelectorDelegate {
|
||||||
matches: Vec<StringMatch>,
|
matches: Vec<StringMatch>,
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BaseKeymapSelectorDelegate {
|
impl BaseKeymapSelectorDelegate {
|
||||||
fn new(cx: &mut ViewContext<BaseKeymapSelector>) -> Self {
|
fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<BaseKeymapSelector>) -> Self {
|
||||||
let base = cx.global::<Settings>().base_keymap;
|
let base = cx.global::<Settings>().base_keymap;
|
||||||
let selected_index = BaseKeymap::OPTIONS
|
let selected_index = BaseKeymap::OPTIONS
|
||||||
.iter()
|
.iter()
|
||||||
@ -45,6 +47,7 @@ impl BaseKeymapSelectorDelegate {
|
|||||||
Self {
|
Self {
|
||||||
matches: Vec::new(),
|
matches: Vec::new(),
|
||||||
selected_index,
|
selected_index,
|
||||||
|
fs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -119,7 +122,9 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
|
|||||||
fn confirm(&mut self, cx: &mut ViewContext<BaseKeymapSelector>) {
|
fn confirm(&mut self, cx: &mut ViewContext<BaseKeymapSelector>) {
|
||||||
if let Some(selection) = self.matches.get(self.selected_index) {
|
if let Some(selection) = self.matches.get(self.selected_index) {
|
||||||
let base_keymap = BaseKeymap::from_names(&selection.string);
|
let base_keymap = BaseKeymap::from_names(&selection.string);
|
||||||
SettingsFile::update(cx, move |settings| settings.base_keymap = Some(base_keymap));
|
update_settings_file(self.fs.clone(), cx, move |settings| {
|
||||||
|
settings.base_keymap = Some(base_keymap)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
cx.emit(PickerEvent::Dismiss);
|
cx.emit(PickerEvent::Dismiss);
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ use gpui::{
|
|||||||
elements::{Flex, Label, ParentElement},
|
elements::{Flex, Label, ParentElement},
|
||||||
AnyElement, AppContext, Element, Entity, Subscription, View, ViewContext, WeakViewHandle,
|
AnyElement, AppContext, Element, Entity, Subscription, View, ViewContext, WeakViewHandle,
|
||||||
};
|
};
|
||||||
use settings::{settings_file::SettingsFile, Settings};
|
use settings::{update_settings_file, Settings};
|
||||||
|
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::Item, open_new, sidebar::SidebarSide, AppState, PaneBackdrop, Welcome, Workspace,
|
item::Item, open_new, sidebar::SidebarSide, AppState, PaneBackdrop, Welcome, Workspace,
|
||||||
@ -169,10 +169,13 @@ impl View for WelcomePage {
|
|||||||
metrics,
|
metrics,
|
||||||
0,
|
0,
|
||||||
cx,
|
cx,
|
||||||
|_, checked, cx| {
|
|this, checked, cx| {
|
||||||
SettingsFile::update(cx, move |file| {
|
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||||
|
let fs = workspace.read(cx).app_state().fs.clone();
|
||||||
|
update_settings_file(fs, cx, move |file| {
|
||||||
file.telemetry.set_metrics(checked)
|
file.telemetry.set_metrics(checked)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.contained()
|
.contained()
|
||||||
@ -185,10 +188,13 @@ impl View for WelcomePage {
|
|||||||
diagnostics,
|
diagnostics,
|
||||||
0,
|
0,
|
||||||
cx,
|
cx,
|
||||||
|_, checked, cx| {
|
|this, checked, cx| {
|
||||||
SettingsFile::update(cx, move |file| {
|
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||||
|
let fs = workspace.read(cx).app_state().fs.clone();
|
||||||
|
update_settings_file(fs, cx, move |file| {
|
||||||
file.telemetry.set_diagnostics(checked)
|
file.telemetry.set_diagnostics(checked)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.contained()
|
.contained()
|
||||||
|
@ -24,8 +24,8 @@ use parking_lot::Mutex;
|
|||||||
use project::Fs;
|
use project::Fs;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{
|
use settings::{
|
||||||
self, settings_file::SettingsFile, KeymapFileContent, Settings, SettingsFileContent,
|
default_settings, handle_keymap_file_changes, handle_settings_file_changes, watch_config_file,
|
||||||
WorkingDirectory,
|
Settings, SettingsStore, WorkingDirectory,
|
||||||
};
|
};
|
||||||
use simplelog::ConfigBuilder;
|
use simplelog::ConfigBuilder;
|
||||||
use smol::process::Command;
|
use smol::process::Command;
|
||||||
@ -37,6 +37,7 @@ use std::{
|
|||||||
os::unix::prelude::OsStrExt,
|
os::unix::prelude::OsStrExt,
|
||||||
panic,
|
panic,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
str,
|
||||||
sync::{Arc, Weak},
|
sync::{Arc, Weak},
|
||||||
thread,
|
thread,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
@ -46,7 +47,6 @@ use util::http::{self, HttpClient};
|
|||||||
use welcome::{show_welcome_experience, FIRST_OPEN};
|
use welcome::{show_welcome_experience, FIRST_OPEN};
|
||||||
|
|
||||||
use fs::RealFs;
|
use fs::RealFs;
|
||||||
use settings::watched_json::WatchedJsonFile;
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
use staff_mode::StaffMode;
|
use staff_mode::StaffMode;
|
||||||
use theme::ThemeRegistry;
|
use theme::ThemeRegistry;
|
||||||
@ -75,10 +75,11 @@ fn main() {
|
|||||||
load_embedded_fonts(&app);
|
load_embedded_fonts(&app);
|
||||||
|
|
||||||
let fs = Arc::new(RealFs);
|
let fs = Arc::new(RealFs);
|
||||||
|
|
||||||
let themes = ThemeRegistry::new(Assets, app.font_cache());
|
let themes = ThemeRegistry::new(Assets, app.font_cache());
|
||||||
let default_settings = Settings::defaults(Assets, &app.font_cache(), &themes);
|
let user_settings_file_rx =
|
||||||
let config_files = load_config_files(&app, fs.clone());
|
watch_config_file(app.background(), fs.clone(), paths::SETTINGS.clone());
|
||||||
|
let user_keymap_file_rx =
|
||||||
|
watch_config_file(app.background(), fs.clone(), paths::KEYMAP.clone());
|
||||||
|
|
||||||
let login_shell_env_loaded = if stdout_is_a_pty() {
|
let login_shell_env_loaded = if stdout_is_a_pty() {
|
||||||
Task::ready(())
|
Task::ready(())
|
||||||
@ -126,26 +127,18 @@ fn main() {
|
|||||||
|
|
||||||
app.run(move |cx| {
|
app.run(move |cx| {
|
||||||
cx.set_global(*RELEASE_CHANNEL);
|
cx.set_global(*RELEASE_CHANNEL);
|
||||||
|
cx.set_global(themes.clone());
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
cx.set_global(StaffMode(true));
|
cx.set_global(StaffMode(true));
|
||||||
|
|
||||||
let (settings_file_content, keymap_file) = cx.background().block(config_files).unwrap();
|
let mut store = SettingsStore::default();
|
||||||
|
store
|
||||||
//Setup settings global before binding actions
|
.set_default_settings(default_settings().as_ref(), cx)
|
||||||
cx.set_global(SettingsFile::new(
|
.unwrap();
|
||||||
&paths::SETTINGS,
|
cx.set_global(store);
|
||||||
settings_file_content.clone(),
|
handle_settings_file_changes(user_settings_file_rx, cx);
|
||||||
fs.clone(),
|
handle_keymap_file_changes(user_keymap_file_rx, cx);
|
||||||
));
|
|
||||||
|
|
||||||
settings::watch_files(
|
|
||||||
default_settings,
|
|
||||||
settings_file_content,
|
|
||||||
themes.clone(),
|
|
||||||
keymap_file,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
if !stdout_is_a_pty() {
|
if !stdout_is_a_pty() {
|
||||||
upload_previous_panics(http.clone(), cx);
|
upload_previous_panics(http.clone(), cx);
|
||||||
@ -585,27 +578,6 @@ async fn watch_themes(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_config_files(
|
|
||||||
app: &App,
|
|
||||||
fs: Arc<dyn Fs>,
|
|
||||||
) -> oneshot::Receiver<(
|
|
||||||
WatchedJsonFile<SettingsFileContent>,
|
|
||||||
WatchedJsonFile<KeymapFileContent>,
|
|
||||||
)> {
|
|
||||||
let executor = app.background();
|
|
||||||
let (tx, rx) = oneshot::channel();
|
|
||||||
executor
|
|
||||||
.clone()
|
|
||||||
.spawn(async move {
|
|
||||||
let settings_file =
|
|
||||||
WatchedJsonFile::new(fs.clone(), &executor, paths::SETTINGS.clone()).await;
|
|
||||||
let keymap_file = WatchedJsonFile::new(fs, &executor, paths::KEYMAP.clone()).await;
|
|
||||||
tx.send((settings_file, keymap_file)).ok()
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
rx
|
|
||||||
}
|
|
||||||
|
|
||||||
fn connect_to_cli(
|
fn connect_to_cli(
|
||||||
server_name: &str,
|
server_name: &str,
|
||||||
) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
|
) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
|
||||||
|
@ -323,7 +323,7 @@ pub fn initialize_workspace(
|
|||||||
});
|
});
|
||||||
|
|
||||||
let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx));
|
let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx));
|
||||||
let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(cx));
|
let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
|
||||||
let diagnostic_summary =
|
let diagnostic_summary =
|
||||||
cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
|
cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
|
||||||
let activity_indicator =
|
let activity_indicator =
|
||||||
|
Loading…
Reference in New Issue
Block a user