From c485fc86a21567d694fd8b25d2f7b450f0d9241c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 27 Apr 2023 18:06:52 -0700 Subject: [PATCH 1/2] Add copilot.disabled_globs setting --- Cargo.lock | 2 + Cargo.toml | 1 + assets/settings/default.json | 7 +++ crates/editor/Cargo.toml | 1 + crates/editor/src/editor.rs | 37 +++++++++++-- crates/editor/src/editor_tests.rs | 91 +++++++++++++++++++++++++++++++ crates/editor/src/multi_buffer.rs | 11 +++- crates/project/Cargo.toml | 2 +- crates/settings/Cargo.toml | 1 + crates/settings/src/settings.rs | 57 ++++++++++--------- 10 files changed, 176 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b6ebbe5ed..af780197e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1983,6 +1983,7 @@ dependencies = [ "futures 0.3.25", "fuzzy", "git", + "glob", "gpui", "indoc", "itertools", @@ -5964,6 +5965,7 @@ dependencies = [ "collections", "fs", "futures 0.3.25", + "glob", "gpui", "json_comments", "postage", diff --git a/Cargo.toml b/Cargo.toml index 1ef283d135..15df687d41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,6 +77,7 @@ async-trait = { version = "0.1" } ctor = { version = "0.1" } env_logger = { version = "0.9" } futures = { version = "0.3" } +glob = { version = "0.3.1" } lazy_static = { version = "1.4.0" } log = { version = "0.4.16", features = ["kv_unstable_serde"] } ordered-float = { version = "2.1.1" } diff --git a/assets/settings/default.json b/assets/settings/default.json index b12bd00efa..b72dab05c7 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -115,6 +115,13 @@ // "git_gutter": "hide" "git_gutter": "tracked_files" }, + "copilot": { + // The set of glob patterns for which copilot should be disabled + // in any matching file. + "disabled_globs": [ + ".env" + ] + }, // Settings specific to journaling "journal": { // The path of the directory where journal entries are stored diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index feb55e1b2f..e8cb323a27 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -79,6 +79,7 @@ workspace = { path = "../workspace", features = ["test-support"] } ctor.workspace = true env_logger.workspace = true +glob.workspace = true rand.workspace = true unindent.workspace = true tree-sitter = "0.20" diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index eea418b211..9128cdb0ac 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2925,11 +2925,7 @@ impl Editor { let snapshot = self.buffer.read(cx).snapshot(cx); let cursor = self.selections.newest_anchor().head(); - let language_name = snapshot.language_at(cursor).map(|language| language.name()); - if !cx - .global::() - .show_copilot_suggestions(language_name.as_deref()) - { + if !self.is_copilot_enabled_at(cursor, &snapshot, cx) { self.clear_copilot_suggestions(cx); return None; } @@ -3080,6 +3076,37 @@ impl Editor { } } + fn is_copilot_enabled_at( + &self, + location: Anchor, + snapshot: &MultiBufferSnapshot, + cx: &mut ViewContext, + ) -> bool { + let settings = cx.global::(); + + let language_name = snapshot + .language_at(location) + .map(|language| language.name()); + if !settings.show_copilot_suggestions(language_name.as_deref()) { + 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 + } + fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool { self.display_map.read(cx).has_suggestion() } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index af4bc1674b..3099bb640d 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -6387,6 +6387,97 @@ async fn test_copilot_multibuffer( }); } +#[gpui::test] +async fn test_copilot_disabled_globs( + deterministic: Arc, + cx: &mut gpui::TestAppContext, +) { + let (copilot, copilot_lsp) = Copilot::fake(cx); + cx.update(|cx| { + let mut settings = Settings::test(cx); + settings.copilot.disabled_globs = vec![glob::Pattern::new(".env*").unwrap()]; + cx.set_global(settings); + cx.set_global(copilot) + }); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/test", + json!({ + ".env": "SECRET=something\n", + "README.md": "hello\n" + }), + ) + .await; + let project = Project::test(fs, ["/test".as_ref()], cx).await; + + let private_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/test/.env", cx) + }) + .await + .unwrap(); + let public_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/test/README.md", cx) + }) + .await + .unwrap(); + + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + private_buffer.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 0), + primary: None, + }], + cx, + ); + multibuffer.push_excerpts( + public_buffer.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 0), + primary: None, + }], + cx, + ); + multibuffer + }); + let (_, editor) = cx.add_window(|cx| build_editor(multibuffer, cx)); + + let mut copilot_requests = copilot_lsp + .handle_request::(move |_params, _cx| async move { + Ok(copilot::request::GetCompletionsResult { + completions: vec![copilot::request::Completion { + text: "next line".into(), + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)), + ..Default::default() + }], + }) + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |selections| { + selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)]) + }); + editor.next_copilot_suggestion(&Default::default(), cx); + }); + + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + assert!(copilot_requests.try_next().is_err()); + + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(2, 0)..Point::new(2, 0)]) + }); + editor.next_copilot_suggestion(&Default::default(), cx); + }); + + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + assert!(copilot_requests.try_next().is_ok()); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(row as u32, column as u32); point..point diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index bf2f12e82e..39e66d0f93 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -10,9 +10,9 @@ use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; pub use language::Completion; use language::{ char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape, - DiagnosticEntry, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline, - OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _, - ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped, + DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, + Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, + ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped, }; use std::{ borrow::Cow, @@ -2754,6 +2754,11 @@ impl MultiBufferSnapshot { self.trailing_excerpt_update_count } + pub fn file_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc> { + self.point_to_buffer_offset(point) + .and_then(|(buffer, _)| buffer.file()) + } + pub fn language_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc> { self.point_to_buffer_offset(point) .and_then(|(buffer, offset)| buffer.language_at(offset)) diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 56803bb062..46b00fc6ee 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -28,7 +28,6 @@ fs = { path = "../fs" } fsevent = { path = "../fsevent" } fuzzy = { path = "../fuzzy" } git = { path = "../git" } -glob = { version = "0.3.1" } gpui = { path = "../gpui" } language = { path = "../language" } lsp = { path = "../lsp" } @@ -43,6 +42,7 @@ anyhow.workspace = true async-trait.workspace = true backtrace = "0.3" futures.workspace = true +glob.workspace = true ignore = "0.4" lazy_static.workspace = true log.workspace = true diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 3a87832e15..00f6cda3a7 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -23,6 +23,7 @@ theme = { path = "../theme" } staff_mode = { path = "../staff_mode" } util = { path = "../util" } +glob.workspace = true json_comments = "0.2" postage.workspace = true schemars = "0.8" diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index f2082be6bb..260f9531a8 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -47,6 +47,7 @@ pub struct Settings { pub editor_overrides: EditorSettings, pub git: GitSettings, pub git_overrides: GitSettings, + pub copilot: CopilotSettings, pub journal_defaults: JournalSettings, pub journal_overrides: JournalSettings, pub terminal_defaults: TerminalSettings, @@ -61,29 +62,6 @@ pub struct Settings { pub base_keymap: BaseKeymap, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] -#[serde(rename_all = "snake_case")] -pub enum CopilotSettings { - #[default] - On, - Off, -} - -impl From for bool { - fn from(value: CopilotSettings) -> Self { - match value { - CopilotSettings::On => true, - CopilotSettings::Off => false, - } - } -} - -impl CopilotSettings { - pub fn is_on(&self) -> bool { - >::into(*self) - } -} - #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] pub enum BaseKeymap { #[default] @@ -150,6 +128,29 @@ impl TelemetrySettings { } } +#[derive(Clone, Debug, Default)] +pub struct CopilotSettings { + pub disabled_globs: Vec, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct CopilotSettingsContent { + #[serde(default)] + disabled_globs: Vec, +} + +impl From for CopilotSettings { + fn from(value: CopilotSettingsContent) -> Self { + Self { + disabled_globs: value + .disabled_globs + .into_iter() + .filter_map(|p| glob::Pattern::new(&p).ok()) + .collect(), + } + } +} + #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct GitSettings { pub git_gutter: Option, @@ -390,6 +391,8 @@ pub struct SettingsFileContent { #[serde(default)] pub buffer_font_features: Option, #[serde(default)] + pub copilot: Option, + #[serde(default)] pub active_pane_magnification: Option, #[serde(default)] pub cursor_blink: Option, @@ -438,8 +441,7 @@ pub struct LspSettings { pub initialization_options: Option, } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Features { pub copilot: bool, } @@ -506,6 +508,7 @@ impl Settings { show_copilot_suggestions: required(defaults.editor.show_copilot_suggestions), }, editor_overrides: Default::default(), + copilot: defaults.copilot.unwrap().into(), git: defaults.git.unwrap(), git_overrides: Default::default(), journal_defaults: defaults.journal, @@ -576,6 +579,9 @@ 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; + } self.editor_overrides = data.editor; self.git_overrides = data.git.unwrap_or_default(); self.journal_overrides = data.journal; @@ -751,6 +757,7 @@ impl Settings { show_copilot_suggestions: Some(true), }, editor_overrides: Default::default(), + copilot: Default::default(), journal_defaults: Default::default(), journal_overrides: Default::default(), terminal_defaults: Default::default(), From 8eb1312debea806ca5ef5d0d948cb017bd0ba25c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 2 May 2023 19:56:45 -0700 Subject: [PATCH 2/2] Add copilot menu item for enabling paths by glob --- Cargo.lock | 3 + crates/copilot_button/Cargo.toml | 1 + crates/copilot_button/src/copilot_button.rs | 133 ++++++-- crates/editor/src/editor.rs | 16 +- crates/settings/Cargo.toml | 1 + crates/settings/src/settings.rs | 352 +++++++++++++------- crates/settings/src/settings_file.rs | 42 +-- crates/workspace/Cargo.toml | 1 + crates/workspace/src/workspace.rs | 50 ++- crates/zed/src/main.rs | 4 +- crates/zed/src/menus.rs | 2 +- crates/zed/src/zed.rs | 59 +--- 12 files changed, 432 insertions(+), 232 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index af780197e8..d1f5631f6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/crates/copilot_button/Cargo.toml b/crates/copilot_button/Cargo.toml index 04e9165a75..2d42b192d9 100644 --- a/crates/copilot_button/Cargo.toml +++ b/crates/copilot_button/Cargo.toml @@ -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" } diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index a597bb7e47..93adc95efd 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -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, popup_menu: ViewHandle, editor_subscription: Option<(Subscription, usize)>, editor_enabled: Option, language: Option>, + path: Option>, } 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 { + pub fn new(app_state: Arc, cx: &mut ViewContext) -> 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) { let settings = cx.global::(); - 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::().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::() + .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::().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, + app_state: Arc, + path_to_disable: Option>, + 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::() + .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::() + .copilot + .disabled_globs + .clone() + .iter() + .map(|glob| glob.as_str().to_string()) + .collect::>() + }); + + 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::().show_copilot_suggestions(None); + let show_copilot_suggestions = cx.global::().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, cx: &mut AppContext) { let show_copilot_suggestions = cx .global::() - .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, 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) { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9128cdb0ac..69eb8170ac 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3084,26 +3084,14 @@ impl Editor { ) -> bool { let settings = cx.global::(); + 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 } diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 00f6cda3a7..ca59f996c3 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -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 diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 260f9531a8..80c9da82ba 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -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, -} - -impl From 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>, } #[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(content: &str) -> Result )?) } -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(); +} - let has_language_overrides = settings_content.contains(LANGUAGE_OVERRIDES); +fn update_object_in_settings_file<'a>( + old_object: &'a serde_json::Map, + new_object: &'a serde_json::Map, + text: &str, + syntax_tree: &Tree, + tab_size: usize, + key_path: &mut Vec<&'a str>, + edits: &mut Vec<(Range, 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, 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, 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, - new_object: &serde_json::Map, - 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 { @@ -1122,15 +1170,18 @@ mod tests { use super::*; use unindent::Unindent; - fn assert_new_settings, S2: Into>( - 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(), ); diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index c261879a58..eb9f1508c2 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -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::() { 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, String)> { + let this = cx.global::(); + let tab_size = cx.global::().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::(); - + let tab_size = cx.global::().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) } } diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index bd32522e4d..177dc0a292 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -19,6 +19,7 @@ test-support = [ ] [dependencies] +assets = { path = "../assets" } db = { path = "../db" } call = { path = "../call" } client = { path = "../client" } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1a622babb3..64fa9c7062 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -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 = env::var("ZED_WINDOW_SIZE") @@ -126,6 +126,8 @@ actions!( ] ); +actions!(zed, [OpenSettings]); + #[derive(Clone, PartialEq)] pub struct OpenPaths { pub paths: Vec, @@ -314,6 +316,18 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { .detach(); }); + cx.add_action({ + let app_state = app_state.clone(); + move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { + 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, + cx: &mut ViewContext, + default_content: impl 'static + Send + FnOnce() -> Rope, +) -> Task>> { + 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, diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 679fd39e2f..771775de57 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -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(); diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index e0a2f74731..741b61f323 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -12,7 +12,7 @@ pub fn menus() -> Vec> { 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), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 494739a967..4478a88837 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -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, 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| { - 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| { @@ -190,7 +173,8 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { cx.add_action({ let app_state = app_state.clone(); move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext| { - 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, cx: &mut gpui::AppContext) { move |_: &mut Workspace, _: &OpenDefaultSettings, cx: &mut ViewContext| { 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) { cx.prompt(PromptLevel::Info, &format!("{app_name} {version}"), &["OK"]); } -fn open_config_file( - path: &'static Path, - app_state: Arc, - cx: &mut ViewContext, - 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,