Detailed changes
@@ -1362,6 +1362,7 @@ name = "copilot_button"
version = "0.1.0"
dependencies = [
"anyhow",
+ "assets",
"context_menu",
"copilot",
"editor",
@@ -5968,6 +5969,7 @@ dependencies = [
"glob",
"gpui",
"json_comments",
+ "lazy_static",
"postage",
"pretty_assertions",
"schemars",
@@ -8455,6 +8457,7 @@ name = "workspace"
version = "0.1.0"
dependencies = [
"anyhow",
+ "assets",
"async-recursion 1.0.0",
"bincode",
"call",
@@ -9,6 +9,7 @@ path = "src/copilot_button.rs"
doctest = false
[dependencies]
+assets = { path = "../assets" }
copilot = { path = "../copilot" }
editor = { path = "../editor" }
context_menu = { path = "../context_menu" }
@@ -1,18 +1,19 @@
+use anyhow::Result;
use context_menu::{ContextMenu, ContextMenuItem};
use copilot::{Copilot, Reinstall, SignOut, Status};
-use editor::Editor;
+use editor::{scroll::autoscroll::Autoscroll, Editor};
use gpui::{
elements::*,
platform::{CursorStyle, MouseButton},
- AnyElement, AppContext, Element, Entity, MouseState, Subscription, View, ViewContext,
- ViewHandle, WindowContext,
+ AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View,
+ ViewContext, ViewHandle, WeakViewHandle, WindowContext,
};
use settings::{settings_file::SettingsFile, Settings};
-use std::sync::Arc;
-use util::ResultExt;
+use std::{path::Path, sync::Arc};
+use util::{paths, ResultExt};
use workspace::{
- item::ItemHandle, notifications::simple_message_notification::OsOpen, StatusItemView, Toast,
- Workspace,
+ create_and_open_local_file, item::ItemHandle,
+ notifications::simple_message_notification::OsOpen, AppState, StatusItemView, Toast, Workspace,
};
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
@@ -20,10 +21,12 @@ const COPILOT_STARTING_TOAST_ID: usize = 1337;
const COPILOT_ERROR_TOAST_ID: usize = 1338;
pub struct CopilotButton {
+ app_state: Arc<AppState>,
popup_menu: ViewHandle<ContextMenu>,
editor_subscription: Option<(Subscription, usize)>,
editor_enabled: Option<bool>,
language: Option<Arc<str>>,
+ path: Option<Arc<Path>>,
}
impl Entity for CopilotButton {
@@ -51,7 +54,7 @@ impl View for CopilotButton {
let enabled = self
.editor_enabled
- .unwrap_or(settings.show_copilot_suggestions(None));
+ .unwrap_or(settings.show_copilot_suggestions(None, None));
Stack::new()
.with_child(
@@ -131,7 +134,7 @@ impl View for CopilotButton {
}
impl CopilotButton {
- pub fn new(cx: &mut ViewContext<Self>) -> Self {
+ pub fn new(app_state: Arc<AppState>, cx: &mut ViewContext<Self>) -> Self {
let menu = cx.add_view(|cx| {
let mut menu = ContextMenu::new(cx);
menu.set_position_mode(OverlayPositionMode::Local);
@@ -146,10 +149,12 @@ impl CopilotButton {
.detach();
Self {
+ app_state,
popup_menu: menu,
editor_subscription: None,
editor_enabled: None,
language: None,
+ path: None,
}
}
@@ -176,10 +181,10 @@ impl CopilotButton {
pub fn deploy_copilot_menu(&mut self, cx: &mut ViewContext<Self>) {
let settings = cx.global::<Settings>();
- let mut menu_options = Vec::with_capacity(6);
+ let mut menu_options = Vec::with_capacity(8);
if let Some(language) = self.language.clone() {
- let language_enabled = settings.show_copilot_suggestions(Some(language.as_ref()));
+ let language_enabled = settings.copilot_enabled_for_language(Some(language.as_ref()));
menu_options.push(ContextMenuItem::handler(
format!(
"{} Suggestions for {}",
@@ -190,7 +195,38 @@ impl CopilotButton {
));
}
- let globally_enabled = cx.global::<Settings>().show_copilot_suggestions(None);
+ if let Some(path) = self.path.as_ref() {
+ let path_enabled = settings.copilot_enabled_for_path(path);
+ let app_state = Arc::downgrade(&self.app_state);
+ let path = path.clone();
+ menu_options.push(ContextMenuItem::handler(
+ format!(
+ "{} Suggestions for This Path",
+ if path_enabled { "Hide" } else { "Show" }
+ ),
+ move |cx| {
+ if let Some((workspace, app_state)) = cx
+ .root_view()
+ .clone()
+ .downcast::<Workspace>()
+ .zip(app_state.upgrade())
+ {
+ let workspace = workspace.downgrade();
+ cx.spawn(|_, cx| {
+ configure_disabled_globs(
+ workspace,
+ app_state,
+ path_enabled.then_some(path.clone()),
+ cx,
+ )
+ })
+ .detach_and_log_err(cx);
+ }
+ },
+ ));
+ }
+
+ let globally_enabled = cx.global::<Settings>().features.copilot;
menu_options.push(ContextMenuItem::handler(
if globally_enabled {
"Hide Suggestions for All Files"
@@ -236,10 +272,14 @@ impl CopilotButton {
let language_name = snapshot
.language_at(suggestion_anchor)
.map(|language| language.name());
+ let path = snapshot
+ .file_at(suggestion_anchor)
+ .map(|file| file.path().clone());
- self.language = language_name.clone();
-
- self.editor_enabled = Some(settings.show_copilot_suggestions(language_name.as_deref()));
+ self.editor_enabled =
+ Some(settings.show_copilot_suggestions(language_name.as_deref(), path.as_deref()));
+ self.language = language_name;
+ self.path = path;
cx.notify()
}
@@ -260,8 +300,63 @@ impl StatusItemView for CopilotButton {
}
}
+async fn configure_disabled_globs(
+ workspace: WeakViewHandle<Workspace>,
+ app_state: Arc<AppState>,
+ path_to_disable: Option<Arc<Path>>,
+ mut cx: AsyncAppContext,
+) -> Result<()> {
+ let settings_editor = workspace
+ .update(&mut cx, |_, cx| {
+ create_and_open_local_file(&paths::SETTINGS, app_state, cx, || {
+ Settings::initial_user_settings_content(&assets::Assets)
+ .as_ref()
+ .into()
+ })
+ })?
+ .await?
+ .downcast::<Editor>()
+ .unwrap();
+
+ settings_editor.downgrade().update(&mut cx, |item, cx| {
+ let text = item.buffer().read(cx).snapshot(cx).text();
+
+ let edits = SettingsFile::update_unsaved(&text, cx, |file| {
+ let copilot = file.copilot.get_or_insert_with(Default::default);
+ let globs = copilot.disabled_globs.get_or_insert_with(|| {
+ cx.global::<Settings>()
+ .copilot
+ .disabled_globs
+ .clone()
+ .iter()
+ .map(|glob| glob.as_str().to_string())
+ .collect::<Vec<_>>()
+ });
+
+ if let Some(path_to_disable) = &path_to_disable {
+ globs.push(path_to_disable.to_string_lossy().into_owned());
+ } else {
+ globs.clear();
+ }
+ });
+
+ if !edits.is_empty() {
+ item.change_selections(Some(Autoscroll::newest()), cx, |selections| {
+ selections.select_ranges(edits.iter().map(|e| e.0.clone()));
+ });
+
+ // When *enabling* a path, don't actually perform an edit, just select the range.
+ if path_to_disable.is_some() {
+ item.edit(edits.iter().cloned(), cx);
+ }
+ }
+ })?;
+
+ anyhow::Ok(())
+}
+
fn toggle_copilot_globally(cx: &mut AppContext) {
- let show_copilot_suggestions = cx.global::<Settings>().show_copilot_suggestions(None);
+ let show_copilot_suggestions = cx.global::<Settings>().show_copilot_suggestions(None, None);
SettingsFile::update(cx, move |file_contents| {
file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
});
@@ -270,7 +365,7 @@ fn toggle_copilot_globally(cx: &mut AppContext) {
fn toggle_copilot_for_language(language: Arc<str>, cx: &mut AppContext) {
let show_copilot_suggestions = cx
.global::<Settings>()
- .show_copilot_suggestions(Some(&language));
+ .show_copilot_suggestions(Some(&language), None);
SettingsFile::update(cx, move |file_contents| {
file_contents.languages.insert(
@@ -280,13 +375,13 @@ fn toggle_copilot_for_language(language: Arc<str>, cx: &mut AppContext) {
..Default::default()
},
);
- })
+ });
}
fn hide_copilot(cx: &mut AppContext) {
SettingsFile::update(cx, move |file_contents| {
file_contents.features.copilot = Some(false)
- })
+ });
}
fn initiate_sign_in(cx: &mut WindowContext) {
@@ -3084,26 +3084,14 @@ impl Editor {
) -> bool {
let settings = cx.global::<Settings>();
+ let path = snapshot.file_at(location).map(|file| file.path());
let language_name = snapshot
.language_at(location)
.map(|language| language.name());
- if !settings.show_copilot_suggestions(language_name.as_deref()) {
+ if !settings.show_copilot_suggestions(language_name.as_deref(), path.map(|p| p.as_ref())) {
return false;
}
- let file = snapshot.file_at(location);
- if let Some(file) = file {
- let path = file.path();
- if settings
- .copilot
- .disabled_globs
- .iter()
- .any(|glob| glob.matches_path(path))
- {
- return false;
- }
- }
-
true
}
@@ -25,6 +25,7 @@ util = { path = "../util" }
glob.workspace = true
json_comments = "0.2"
+lazy_static.workspace = true
postage.workspace = true
schemars = "0.8"
serde.workspace = true
@@ -7,6 +7,7 @@ use gpui::{
font_cache::{FamilyId, FontCache},
fonts, AssetSource,
};
+use lazy_static::lazy_static;
use schemars::{
gen::{SchemaGenerator, SchemaSettings},
schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
@@ -18,14 +19,19 @@ use sqlez::{
bindable::{Bind, Column, StaticColumnCount},
statement::Statement,
};
-use std::{collections::HashMap, num::NonZeroU32, str, sync::Arc};
+use std::{
+ borrow::Cow, collections::HashMap, num::NonZeroU32, ops::Range, path::Path, str, sync::Arc,
+};
use theme::{Theme, ThemeRegistry};
-use tree_sitter::Query;
+use tree_sitter::{Query, Tree};
use util::{RangeExt, ResultExt as _};
pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
pub use watched_json::watch_files;
+pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json";
+pub const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json";
+
#[derive(Clone)]
pub struct Settings {
pub features: Features,
@@ -136,19 +142,7 @@ pub struct CopilotSettings {
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct CopilotSettingsContent {
#[serde(default)]
- disabled_globs: Vec<String>,
-}
-
-impl From<CopilotSettingsContent> for CopilotSettings {
- fn from(value: CopilotSettingsContent) -> Self {
- Self {
- disabled_globs: value
- .disabled_globs
- .into_iter()
- .filter_map(|p| glob::Pattern::new(&p).ok())
- .collect(),
- }
- }
+ pub disabled_globs: Option<Vec<String>>,
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
@@ -453,6 +447,13 @@ pub struct FeaturesContent {
}
impl Settings {
+ pub fn initial_user_settings_content(assets: &'static impl AssetSource) -> Cow<'static, str> {
+ match assets.load(INITIAL_USER_SETTINGS_ASSET_PATH).unwrap() {
+ Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
+ Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
+ }
+ }
+
/// Fill out the settings corresponding to the default.json file, overrides will be set later
pub fn defaults(
assets: impl AssetSource,
@@ -466,7 +467,7 @@ impl Settings {
}
let defaults: SettingsFileContent = parse_json_with_comments(
- str::from_utf8(assets.load("settings/default.json").unwrap().as_ref()).unwrap(),
+ str::from_utf8(assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap().as_ref()).unwrap(),
)
.unwrap();
@@ -508,7 +509,16 @@ impl Settings {
show_copilot_suggestions: required(defaults.editor.show_copilot_suggestions),
},
editor_overrides: Default::default(),
- copilot: defaults.copilot.unwrap().into(),
+ copilot: CopilotSettings {
+ disabled_globs: defaults
+ .copilot
+ .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,
@@ -579,8 +589,13 @@ impl Settings {
merge(&mut self.base_keymap, data.base_keymap);
merge(&mut self.features.copilot, data.features.copilot);
- if let Some(copilot) = data.copilot.map(CopilotSettings::from) {
- self.copilot = copilot;
+ if let Some(copilot) = data.copilot {
+ if let Some(disabled_globs) = copilot.disabled_globs {
+ self.copilot.disabled_globs = disabled_globs
+ .into_iter()
+ .filter_map(|s| glob::Pattern::new(&s).ok())
+ .collect()
+ }
}
self.editor_overrides = data.editor;
self.git_overrides = data.git.unwrap_or_default();
@@ -608,11 +623,34 @@ impl Settings {
&self.features
}
- pub fn show_copilot_suggestions(&self, language: Option<&str>) -> bool {
- self.features.copilot
- && self.language_setting(language, |settings| {
- settings.show_copilot_suggestions.map(Into::into)
- })
+ pub fn show_copilot_suggestions(&self, language: Option<&str>, path: Option<&Path>) -> bool {
+ if !self.features.copilot {
+ return false;
+ }
+
+ if !self.copilot_enabled_for_language(language) {
+ return false;
+ }
+
+ if let Some(path) = path {
+ if !self.copilot_enabled_for_path(path) {
+ return false;
+ }
+ }
+
+ true
+ }
+
+ pub fn copilot_enabled_for_path(&self, path: &Path) -> bool {
+ !self
+ .copilot
+ .disabled_globs
+ .iter()
+ .any(|glob| glob.matches_path(path))
+ }
+
+ pub fn copilot_enabled_for_language(&self, language: Option<&str>) -> bool {
+ self.language_setting(language, |settings| settings.show_copilot_suggestions)
}
pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
@@ -866,17 +904,8 @@ pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T>
)?)
}
-fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_value: &Value) {
- const LANGUAGE_OVERRIDES: &'static str = "language_overrides";
- const LANGAUGES: &'static str = "languages";
-
- let mut parser = tree_sitter::Parser::new();
- parser.set_language(tree_sitter_json::language()).unwrap();
- let tree = parser.parse(&settings_content, None).unwrap();
-
- let mut cursor = tree_sitter::QueryCursor::new();
-
- let query = Query::new(
+lazy_static! {
+ static ref PAIR_QUERY: Query = Query::new(
tree_sitter_json::language(),
"
(pair
@@ -885,14 +914,65 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu
",
)
.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));
+ }
- let has_language_overrides = settings_content.contains(LANGUAGE_OVERRIDES);
+ 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..settings_content.len();
- let matches = cursor.matches(&query, tree.root_node(), settings_content.as_bytes());
+ 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;
@@ -915,10 +995,10 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu
first_key_start.get_or_insert_with(|| key_range.start);
- let found_key = settings_content
+ let found_key = text
.get(key_range.clone())
.map(|key_text| {
- if key_path[depth] == LANGAUGES && has_language_overrides {
+ if key_path[depth] == LANGUAGES && has_language_overrides {
return key_text == format!("\"{}\"", LANGUAGE_OVERRIDES);
} else {
return key_text == format!("\"{}\"", key_path[depth]);
@@ -942,12 +1022,11 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu
// We found the exact key we want, insert the new value
if depth == key_path.len() {
- let new_val = serde_json::to_string_pretty(new_value)
- .expect("Could not serialize new json field to string");
- settings_content.replace_range(existing_value_range, &new_val);
+ 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] == LANGAUGES {
+ let new_key = if has_language_overrides && key_path[depth] == LANGUAGES {
LANGUAGE_OVERRIDES
} else {
key_path[depth]
@@ -956,7 +1035,7 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu
// 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 == &LANGAUGES {
+ 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 });
@@ -966,7 +1045,7 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu
if let Some(first_key_start) = first_key_start {
let mut row = 0;
let mut column = 0;
- for (ix, char) in settings_content.char_indices() {
+ for (ix, char) in text.char_indices() {
if ix == first_key_start {
break;
}
@@ -981,37 +1060,29 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu
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 content = format!(r#""{new_key}": {new_val},"#);
- settings_content.insert_str(first_key_start, &content);
-
- settings_content.insert_str(
- first_key_start + content.len(),
- &format!("\n{:width$}", ' ', width = 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(' ');
- settings_content.insert_str(first_key_start, &content);
+ (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 new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
-
- settings_content.replace_range(existing_value_range, &new_val);
+ let mut new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
if depth == 0 {
- settings_content.push('\n');
+ new_val.push('\n');
}
+
+ (existing_value_range, new_val)
}
}
}
-fn to_pretty_json(
- value: &serde_json::Value,
- indent_size: usize,
- indent_prefix_len: usize,
-) -> String {
+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());
@@ -1038,13 +1109,16 @@ fn to_pretty_json(
adjusted_text
}
-pub fn update_settings_file(
- mut text: String,
+/// 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),
-) -> String {
+) -> 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() {
@@ -1062,51 +1136,25 @@ pub fn update_settings_file(
}
}
+ 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);
-
- fn apply_changes_to_json_text(
- old_object: &serde_json::Map<String, Value>,
- new_object: &serde_json::Map<String, Value>,
- current_key_path: Vec<&str>,
- json_text: &mut String,
- ) {
- for (key, old_value) in old_object.iter() {
- // We know that these two are from the same shape of object, so we can just unwrap
- let new_value = new_object.get(key).unwrap();
-
- if old_value != new_value {
- match new_value {
- Value::Bool(_) | Value::Number(_) | Value::String(_) => {
- let mut key_path = current_key_path.clone();
- key_path.push(key);
- write_settings_key(json_text, &key_path, &new_value);
- }
- Value::Object(new_sub_object) => {
- let mut key_path = current_key_path.clone();
- key_path.push(key);
- if let Value::Object(old_sub_object) = old_value {
- apply_changes_to_json_text(
- old_sub_object,
- new_sub_object,
- key_path,
- json_text,
- );
- } else {
- unimplemented!("This function doesn't support changing values from simple values to objects yet");
- }
- }
- Value::Null | Value::Array(_) => {
- unimplemented!("We only support objects and simple values");
- }
- }
- }
- }
- }
-
- apply_changes_to_json_text(&old_object, &new_object, vec![], &mut text);
-
- text
+ 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> {
@@ -1122,15 +1170,18 @@ mod tests {
use super::*;
use unindent::Unindent;
- fn assert_new_settings<S1: Into<String>, S2: Into<String>>(
- old_json: S1,
+ fn assert_new_settings(
+ old_json: String,
update: fn(&mut SettingsFileContent),
- expected_new_json: S2,
+ expected_new_json: String,
) {
- let old_json = old_json.into();
let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap_or_default();
- let new_json = update_settings_file(old_json, old_content, update);
- pretty_assertions::assert_eq!(new_json, expected_new_json.into());
+ 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]
@@ -1171,6 +1222,63 @@ mod tests {
);
}
+ #[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(
@@ -1354,7 +1462,7 @@ mod tests {
#[test]
fn test_update_telemetry_setting() {
assert_new_settings(
- "{}",
+ "{}".into(),
|settings| settings.telemetry.set_diagnostics(true),
r#"
{
@@ -1370,7 +1478,7 @@ mod tests {
#[test]
fn test_update_object_empty_doc() {
assert_new_settings(
- "",
+ "".into(),
|settings| settings.telemetry.set_diagnostics(true),
r#"
{
@@ -1423,7 +1531,7 @@ mod tests {
#[test]
fn write_key_no_document() {
assert_new_settings(
- "",
+ "".to_string(),
|settings| settings.theme = Some("summerfruit-light".to_string()),
r#"
{
@@ -1437,16 +1545,16 @@ mod tests {
#[test]
fn test_write_theme_into_single_line_settings_without_theme() {
assert_new_settings(
- r#"{ "a": "", "ok": true }"#,
+ r#"{ "a": "", "ok": true }"#.to_string(),
|settings| settings.theme = Some("summerfruit-light".to_string()),
- r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#,
+ r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#.to_string(),
);
}
#[test]
fn test_write_theme_pre_object_whitespace() {
assert_new_settings(
- r#" { "a": "", "ok": true }"#,
+ r#" { "a": "", "ok": true }"#.to_string(),
|settings| settings.theme = Some("summerfruit-light".to_string()),
r#" { "theme": "summerfruit-light", "a": "", "ok": true }"#.unindent(),
);
@@ -1,9 +1,9 @@
-use crate::{update_settings_file, watched_json::WatchedJsonFile, SettingsFileContent};
+use crate::{update_settings_file, watched_json::WatchedJsonFile, Settings, SettingsFileContent};
use anyhow::Result;
use assets::Assets;
use fs::Fs;
-use gpui::{AppContext, AssetSource};
-use std::{io::ErrorKind, path::Path, sync::Arc};
+use gpui::AppContext;
+use std::{io::ErrorKind, ops::Range, path::Path, sync::Arc};
// TODO: Switch SettingsFile to open a worktree and buffer for synchronization
// And instant updates in the Zed editor
@@ -33,14 +33,7 @@ impl SettingsFile {
Err(err) => {
if let Some(e) = err.downcast_ref::<std::io::Error>() {
if e.kind() == ErrorKind::NotFound {
- return Ok(std::str::from_utf8(
- Assets
- .load("settings/initial_user_settings.json")
- .unwrap()
- .as_ref(),
- )
- .unwrap()
- .to_string());
+ return Ok(Settings::initial_user_settings_content(&Assets).to_string());
}
}
return Err(err);
@@ -48,28 +41,39 @@ impl SettingsFile {
}
}
+ pub fn update_unsaved(
+ text: &str,
+ 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,
update: impl 'static + Send + FnOnce(&mut SettingsFileContent),
) {
let this = cx.global::<SettingsFile>();
-
+ let tab_size = cx.global::<Settings>().tab_size(Some("JSON"));
let current_file_content = this.settings_file_content.current();
-
let fs = this.fs.clone();
let path = this.path.clone();
cx.background()
.spawn(async move {
let old_text = SettingsFile::load_settings(path, &fs).await?;
-
- let new_text = update_settings_file(old_text, current_file_content, update);
-
+ let edits = update_settings_file(&old_text, current_file_content, tab_size, update);
+ let mut new_text = old_text;
+ for (range, replacement) in edits.into_iter().rev() {
+ new_text.replace_range(range, &replacement);
+ }
fs.atomic_write(path.to_path_buf(), new_text).await?;
-
- Ok(()) as Result<()>
+ anyhow::Ok(())
})
- .detach_and_log_err(cx);
+ .detach_and_log_err(cx)
}
}
@@ -19,6 +19,7 @@ test-support = [
]
[dependencies]
+assets = { path = "../assets" }
db = { path = "../db" }
call = { path = "../call" }
client = { path = "../client" }
@@ -14,9 +14,8 @@ pub mod sidebar;
mod status_bar;
mod toolbar;
-pub use smallvec;
-
use anyhow::{anyhow, Context, Result};
+use assets::Assets;
use call::ActiveCall;
use client::{
proto::{self, PeerId},
@@ -47,13 +46,14 @@ use gpui::{
ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
-use language::LanguageRegistry;
+use language::{LanguageRegistry, Rope};
use std::{
any::TypeId,
borrow::Cow,
cmp, env,
future::Future,
path::{Path, PathBuf},
+ str,
sync::Arc,
time::Duration,
};
@@ -82,7 +82,7 @@ use status_bar::StatusBar;
pub use status_bar::StatusItemView;
use theme::{Theme, ThemeRegistry};
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
-use util::ResultExt;
+use util::{paths, ResultExt};
lazy_static! {
static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
@@ -126,6 +126,8 @@ actions!(
]
);
+actions!(zed, [OpenSettings]);
+
#[derive(Clone, PartialEq)]
pub struct OpenPaths {
pub paths: Vec<PathBuf>,
@@ -314,6 +316,18 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
.detach();
});
+ cx.add_action({
+ let app_state = app_state.clone();
+ move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
+ create_and_open_local_file(&paths::SETTINGS, app_state.clone(), cx, || {
+ Settings::initial_user_settings_content(&Assets)
+ .as_ref()
+ .into()
+ })
+ .detach_and_log_err(cx);
+ }
+ });
+
let client = &app_state.client;
client.add_view_request_handler(Workspace::handle_follow);
client.add_view_message_handler(Workspace::handle_unfollow);
@@ -2981,6 +2995,34 @@ pub fn open_new(
})
}
+pub fn create_and_open_local_file(
+ path: &'static Path,
+ app_state: Arc<AppState>,
+ cx: &mut ViewContext<Workspace>,
+ default_content: impl 'static + Send + FnOnce() -> Rope,
+) -> Task<Result<Box<dyn ItemHandle>>> {
+ cx.spawn(|workspace, mut cx| async move {
+ let fs = &app_state.fs;
+ if !fs.is_file(path).await {
+ fs.create_file(path, Default::default()).await?;
+ fs.save(path, &default_content(), Default::default())
+ .await?;
+ }
+
+ let mut items = workspace
+ .update(&mut cx, |workspace, cx| {
+ workspace.with_local_workspace(&app_state, cx, |workspace, cx| {
+ workspace.open_paths(vec![path.to_path_buf()], false, cx)
+ })
+ })?
+ .await?
+ .await;
+
+ let item = items.pop().flatten();
+ item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
+ })
+}
+
pub fn join_remote_project(
project_id: u64,
follow_user_id: u64,
@@ -44,9 +44,9 @@ use theme::ThemeRegistry;
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
use workspace::{
self, dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile,
- Workspace,
+ OpenSettings, Workspace,
};
-use zed::{self, build_window_options, initialize_workspace, languages, menus, OpenSettings};
+use zed::{self, build_window_options, initialize_workspace, languages, menus};
fn main() {
let http = http::client();
@@ -12,7 +12,7 @@ pub fn menus() -> Vec<Menu<'static>> {
MenuItem::submenu(Menu {
name: "Preferences",
items: vec![
- MenuItem::action("Open Settings", super::OpenSettings),
+ MenuItem::action("Open Settings", workspace::OpenSettings),
MenuItem::action("Open Key Bindings", super::OpenKeymap),
MenuItem::action("Open Default Settings", super::OpenDefaultSettings),
MenuItem::action("Open Default Key Bindings", super::OpenDefaultKeymap),
@@ -20,22 +20,21 @@ use gpui::{
geometry::vector::vec2f,
impl_actions,
platform::{Platform, PromptLevel, TitlebarOptions, WindowBounds, WindowKind, WindowOptions},
- AssetSource, ViewContext,
+ ViewContext,
};
-use language::Rope;
pub use lsp;
pub use project;
use project_panel::ProjectPanel;
use search::{BufferSearchBar, ProjectSearchBar};
use serde::Deserialize;
use serde_json::to_string_pretty;
-use settings::Settings;
-use std::{borrow::Cow, env, path::Path, str, sync::Arc};
+use settings::{Settings, DEFAULT_SETTINGS_ASSET_PATH};
+use std::{borrow::Cow, str, sync::Arc};
use terminal_view::terminal_button::TerminalButton;
use util::{channel::ReleaseChannel, paths, ResultExt};
use uuid::Uuid;
pub use workspace;
-use workspace::{sidebar::SidebarSide, AppState, Restart, Workspace};
+use workspace::{create_and_open_local_file, sidebar::SidebarSide, AppState, Restart, Workspace};
#[derive(Deserialize, Clone, PartialEq)]
pub struct OpenBrowser {
@@ -56,7 +55,6 @@ actions!(
ToggleFullScreen,
Quit,
DebugElements,
- OpenSettings,
OpenLog,
OpenLicenses,
OpenTelemetryLog,
@@ -148,21 +146,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
})
.detach_and_log_err(cx);
});
- cx.add_action({
- let app_state = app_state.clone();
- move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
- open_config_file(&paths::SETTINGS, app_state.clone(), cx, || {
- str::from_utf8(
- Assets
- .load("settings/initial_user_settings.json")
- .unwrap()
- .as_ref(),
- )
- .unwrap()
- .into()
- });
- }
- });
cx.add_action({
let app_state = app_state.clone();
move |workspace: &mut Workspace, _: &OpenLog, cx: &mut ViewContext<Workspace>| {
@@ -190,7 +173,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
cx.add_action({
let app_state = app_state.clone();
move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
- open_config_file(&paths::KEYMAP, app_state.clone(), cx, Default::default);
+ create_and_open_local_file(&paths::KEYMAP, app_state.clone(), cx, Default::default)
+ .detach_and_log_err(cx);
}
});
cx.add_action({
@@ -210,7 +194,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
move |_: &mut Workspace, _: &OpenDefaultSettings, cx: &mut ViewContext<Workspace>| {
open_bundled_file(
app_state.clone(),
- "settings/default.json",
+ DEFAULT_SETTINGS_ASSET_PATH,
"Default Settings",
"JSON",
cx,
@@ -316,7 +300,7 @@ pub fn initialize_workspace(
});
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.clone(), cx));
let diagnostic_summary =
cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
let activity_indicator =
@@ -480,33 +464,6 @@ fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
cx.prompt(PromptLevel::Info, &format!("{app_name} {version}"), &["OK"]);
}
-fn open_config_file(
- path: &'static Path,
- app_state: Arc<AppState>,
- cx: &mut ViewContext<Workspace>,
- default_content: impl 'static + Send + FnOnce() -> Rope,
-) {
- cx.spawn(|workspace, mut cx| async move {
- let fs = &app_state.fs;
- if !fs.is_file(path).await {
- fs.create_file(path, Default::default()).await?;
- fs.save(path, &default_content(), Default::default())
- .await?;
- }
-
- workspace
- .update(&mut cx, |workspace, cx| {
- workspace.with_local_workspace(&app_state, cx, |workspace, cx| {
- workspace.open_paths(vec![path.to_path_buf()], false, cx)
- })
- })?
- .await?
- .await;
- Ok::<_, anyhow::Error>(())
- })
- .detach_and_log_err(cx)
-}
-
fn open_log_file(
workspace: &mut Workspace,
app_state: Arc<AppState>,