Detailed changes
@@ -7737,15 +7737,15 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<()> {
- if DisableAiSettings::get_global(cx).disable_ai {
- return None;
- }
-
let provider = self.edit_prediction_provider()?;
let cursor = self.selections.newest_anchor().head();
let (buffer, cursor_buffer_position) =
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
+ if DisableAiSettings::is_ai_disabled_for_buffer(Some(&buffer), cx) {
+ return None;
+ }
+
if !self.edit_predictions_enabled_in_buffer(&buffer, cursor_buffer_position, cx) {
self.discard_edit_prediction(EditPredictionDiscardReason::Ignored, cx);
return None;
@@ -7791,19 +7791,25 @@ impl Editor {
}
pub fn update_edit_prediction_settings(&mut self, cx: &mut Context<Self>) {
- if self.edit_prediction_provider.is_none() || DisableAiSettings::get_global(cx).disable_ai {
+ if self.edit_prediction_provider.is_none() {
self.edit_prediction_settings = EditPredictionSettings::Disabled;
self.discard_edit_prediction(EditPredictionDiscardReason::Ignored, cx);
- } else {
- let selection = self.selections.newest_anchor();
- let cursor = selection.head();
+ return;
+ }
- if let Some((buffer, cursor_buffer_position)) =
- self.buffer.read(cx).text_anchor_for_position(cursor, cx)
- {
- self.edit_prediction_settings =
- self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx);
+ let selection = self.selections.newest_anchor();
+ let cursor = selection.head();
+
+ if let Some((buffer, cursor_buffer_position)) =
+ self.buffer.read(cx).text_anchor_for_position(cursor, cx)
+ {
+ if DisableAiSettings::is_ai_disabled_for_buffer(Some(&buffer), cx) {
+ self.edit_prediction_settings = EditPredictionSettings::Disabled;
+ self.discard_edit_prediction(EditPredictionDiscardReason::Ignored, cx);
+ return;
}
+ self.edit_prediction_settings =
+ self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx);
}
}
@@ -8444,10 +8450,6 @@ impl Editor {
_window: &mut Window,
cx: &mut Context<Self>,
) -> Option<()> {
- if DisableAiSettings::get_global(cx).disable_ai {
- return None;
- }
-
if self.ime_transaction.is_some() {
self.discard_edit_prediction(EditPredictionDiscardReason::Ignored, cx);
return None;
@@ -8456,6 +8458,13 @@ impl Editor {
let selection = self.selections.newest_anchor();
let cursor = selection.head();
let multibuffer = self.buffer.read(cx).snapshot(cx);
+
+ // Check project-level disable_ai setting for the current buffer
+ if let Some((buffer, _)) = self.buffer.read(cx).text_anchor_for_position(cursor, cx) {
+ if DisableAiSettings::is_ai_disabled_for_buffer(Some(&buffer), cx) {
+ return None;
+ }
+ }
let offset_selection = selection.map(|endpoint| endpoint.to_offset(&multibuffer));
let excerpt_id = cursor.excerpt_id;
@@ -1314,7 +1314,10 @@ impl EditorElement {
// Handle diff review indicator when gutter is hovered in diff mode with AI enabled
let show_diff_review = editor.show_diff_review_button()
&& cx.has_flag::<DiffReviewFeatureFlag>()
- && !DisableAiSettings::get_global(cx).disable_ai;
+ && !DisableAiSettings::is_ai_disabled_for_buffer(
+ editor.buffer.read(cx).as_singleton().as_ref(),
+ cx,
+ );
let diff_review_indicator = if gutter_hovered && show_diff_review {
let is_visible = editor
@@ -9,7 +9,6 @@ use crate::{
use gpui::prelude::FluentBuilder;
use gpui::{Context, DismissEvent, Entity, Focusable as _, Pixels, Point, Subscription, Window};
use project::DisableAiSettings;
-use settings::Settings;
use std::ops::Range;
use text::PointUtf16;
use workspace::OpenInTerminal;
@@ -219,7 +218,10 @@ pub fn deploy_context_menu(
let evaluate_selection = window.is_action_available(&EvaluateSelectedText, cx);
let run_to_cursor = window.is_action_available(&RunToCursor, cx);
- let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
+ let disable_ai = DisableAiSettings::is_ai_disabled_for_buffer(
+ editor.buffer.read(cx).as_singleton().as_ref(),
+ cx,
+ );
let is_markdown = editor
.buffer()
@@ -18,7 +18,7 @@ use settings::{Settings as _, SettingsStore};
use util::{ResultExt as _, rel_path::RelPath};
use crate::{
- Project,
+ DisableAiSettings, Project,
project_settings::{ContextServerSettings, ProjectSettings},
worktree_store::WorktreeStore,
};
@@ -867,6 +867,19 @@ impl ContextServerStore {
}
async fn maintain_servers(this: WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {
+ // Don't start context servers if AI is disabled
+ let ai_disabled = this.update(cx, |_, cx| DisableAiSettings::get_global(cx).disable_ai)?;
+ if ai_disabled {
+ // Stop all running servers when AI is disabled
+ this.update(cx, |this, cx| {
+ let server_ids: Vec<_> = this.servers.keys().cloned().collect();
+ for id in server_ids {
+ let _ = this.stop_server(&id, cx);
+ }
+ })?;
+ return Ok(());
+ }
+
let (mut configured_servers, registry, worktree_store) = this.update(cx, |this, _| {
(
this.context_server_settings.clone(),
@@ -1076,11 +1076,29 @@ pub struct DisableAiSettings {
impl settings::Settings for DisableAiSettings {
fn from_settings(content: &settings::SettingsContent) -> Self {
Self {
- disable_ai: content.disable_ai.unwrap().0,
+ disable_ai: content.project.disable_ai.unwrap().0,
}
}
}
+impl DisableAiSettings {
+ /// Returns whether AI is disabled for the given file.
+ ///
+ /// This checks the project-level settings for the file's worktree,
+ /// allowing `disable_ai` to be configured per-project in `.zed/settings.json`.
+ pub fn is_ai_disabled_for_buffer(buffer: Option<&Entity<Buffer>>, cx: &App) -> bool {
+ Self::is_ai_disabled_for_file(buffer.and_then(|buffer| buffer.read(cx).file()), cx)
+ }
+
+ pub fn is_ai_disabled_for_file(file: Option<&Arc<dyn language::File>>, cx: &App) -> bool {
+ let location = file.map(|f| settings::SettingsLocation {
+ worktree_id: f.worktree_id(cx),
+ path: f.path().as_ref(),
+ });
+ Self::get(location, cx).disable_ai
+ }
+}
+
impl Project {
pub fn init(client: &Arc<Client>, cx: &mut App) {
connection_manager::init(client.clone(), cx);
@@ -12147,4 +12147,105 @@ mod disable_ai_settings_tests {
);
});
}
+
+ #[gpui::test]
+ async fn test_disable_ai_project_level_settings(cx: &mut TestAppContext) {
+ use settings::{LocalSettingsKind, LocalSettingsPath, SettingsLocation, SettingsStore};
+ use worktree::WorktreeId;
+
+ cx.update(|cx| {
+ settings::init(cx);
+
+ // Default should allow AI
+ assert!(
+ !DisableAiSettings::get_global(cx).disable_ai,
+ "Default should allow AI"
+ );
+ });
+
+ let worktree_id = WorktreeId::from_usize(1);
+ let rel_path = |path: &str| -> std::sync::Arc<util::rel_path::RelPath> {
+ std::sync::Arc::from(util::rel_path::RelPath::unix(path).unwrap())
+ };
+ let project_path = rel_path("project");
+ let settings_location = SettingsLocation {
+ worktree_id,
+ path: project_path.as_ref(),
+ };
+
+ // Test: Project-level disable_ai=true should disable AI for files in that project
+ cx.update_global::<SettingsStore, _>(|store, cx| {
+ store
+ .set_local_settings(
+ worktree_id,
+ LocalSettingsPath::InWorktree(project_path.clone()),
+ LocalSettingsKind::Settings,
+ Some(r#"{ "disable_ai": true }"#),
+ cx,
+ )
+ .unwrap();
+ });
+
+ cx.update(|cx| {
+ let settings = DisableAiSettings::get(Some(settings_location), cx);
+ assert!(
+ settings.disable_ai,
+ "Project-level disable_ai=true should disable AI for files in that project"
+ );
+ // Global should now also be true since project-level disable_ai is merged into global
+ assert!(
+ DisableAiSettings::get_global(cx).disable_ai,
+ "Global setting should be affected by project-level disable_ai=true"
+ );
+ });
+
+ // Test: Setting project-level to false should allow AI for that project
+ cx.update_global::<SettingsStore, _>(|store, cx| {
+ store
+ .set_local_settings(
+ worktree_id,
+ LocalSettingsPath::InWorktree(project_path.clone()),
+ LocalSettingsKind::Settings,
+ Some(r#"{ "disable_ai": false }"#),
+ cx,
+ )
+ .unwrap();
+ });
+
+ cx.update(|cx| {
+ let settings = DisableAiSettings::get(Some(settings_location), cx);
+ assert!(
+ !settings.disable_ai,
+ "Project-level disable_ai=false should allow AI"
+ );
+ // Global should also be false now
+ assert!(
+ !DisableAiSettings::get_global(cx).disable_ai,
+ "Global setting should be false when project-level is false"
+ );
+ });
+
+ // Test: User-level true + project-level false = AI disabled (saturation)
+ let disable_true = serde_json::json!({ "disable_ai": true }).to_string();
+ cx.update_global::<SettingsStore, _>(|store, cx| {
+ store.set_user_settings(&disable_true, cx).unwrap();
+ store
+ .set_local_settings(
+ worktree_id,
+ LocalSettingsPath::InWorktree(project_path.clone()),
+ LocalSettingsKind::Settings,
+ Some(r#"{ "disable_ai": false }"#),
+ cx,
+ )
+ .unwrap();
+ });
+
+ cx.update(|cx| {
+ let settings = DisableAiSettings::get(Some(settings_location), cx);
+ assert!(
+ settings.disable_ai,
+ "Project-level false cannot override user-level true (SaturatingBool)"
+ );
+ });
+ }
}
@@ -1173,6 +1173,54 @@ impl SettingsStore {
merged.merge_from_option(user_settings.for_profile(cx));
}
merged.merge_from_option(self.server_settings.as_deref());
+
+ // Merge `disable_ai` from all project/local settings into the global value.
+ // Since `SaturatingBool` uses OR logic, if any project has `disable_ai: true`,
+ // the global value will be true. This allows project-level `disable_ai` to
+ // affect the global setting used by UI elements without file context.
+ for local_settings in self.local_settings.values() {
+ merged
+ .project
+ .disable_ai
+ .merge_from(&local_settings.project.disable_ai);
+ }
+
+ self.merged_settings = Rc::new(merged);
+
+ for setting_value in self.setting_values.values_mut() {
+ let value = setting_value.from_settings(&self.merged_settings);
+ setting_value.set_global_value(value);
+ }
+ } else {
+ // When only a local path changed, we still need to recompute the global
+ // `disable_ai` value since it depends on all local settings.
+ let mut merged = (*self.merged_settings).clone();
+ // Reset disable_ai to compute fresh from base settings
+ merged.project.disable_ai = self.default_settings.project.disable_ai;
+ if let Some(global) = &self.global_settings {
+ merged
+ .project
+ .disable_ai
+ .merge_from(&global.project.disable_ai);
+ }
+ if let Some(user) = &self.user_settings {
+ merged
+ .project
+ .disable_ai
+ .merge_from(&user.content.project.disable_ai);
+ }
+ if let Some(server) = &self.server_settings {
+ merged
+ .project
+ .disable_ai
+ .merge_from(&server.project.disable_ai);
+ }
+ for local_settings in self.local_settings.values() {
+ merged
+ .project
+ .disable_ai
+ .merge_from(&local_settings.project.disable_ai);
+ }
self.merged_settings = Rc::new(merged);
for setting_value in self.setting_values.values_mut() {
@@ -181,7 +181,6 @@ impl VsCodeSettings {
collaboration_panel: None,
debugger: None,
diagnostics: None,
- disable_ai: None,
editor: self.editor_settings_content(),
extension: ExtensionSettingsContent::default(),
file_finder: None,
@@ -510,6 +509,7 @@ impl VsCodeSettings {
load_direnv: None,
slash_commands: None,
git_hosting_providers: None,
+ disable_ai: None,
}
}
@@ -10,7 +10,7 @@ use util::serde::default_true;
use crate::{
AllLanguageSettingsContent, DelayMs, ExtendingVec, ParseStatus, ProjectTerminalSettingsContent,
- RootUserSettings, SlashCommandSettings, fallible_options,
+ RootUserSettings, SaturatingBool, SlashCommandSettings, fallible_options,
};
#[with_fallible_options]
@@ -79,6 +79,11 @@ pub struct ProjectSettingsContent {
/// The list of custom Git hosting providers.
pub git_hosting_providers: Option<ExtendingVec<GitHostingProviderConfig>>,
+
+ /// Whether to disable all AI features in Zed.
+ ///
+ /// Default: false
+ pub disable_ai: Option<SaturatingBool>,
}
#[with_fallible_options]
@@ -197,11 +197,6 @@ pub struct SettingsContent {
// Settings related to calls in Zed
pub calls: Option<CallSettingsContent>,
- /// Whether to disable all AI features in Zed.
- ///
- /// Default: false
- pub disable_ai: Option<SaturatingBool>,
-
/// Settings for the which-key popup.
pub which_key: Option<WhichKeySettingsContent>,
@@ -6962,13 +6962,13 @@ fn ai_page() -> SettingsPage {
description: "Whether to disable all AI features in Zed.",
field: Box::new(SettingField {
json_path: Some("disable_ai"),
- pick: |settings_content| settings_content.disable_ai.as_ref(),
+ pick: |settings_content| settings_content.project.disable_ai.as_ref(),
write: |settings_content, value| {
- settings_content.disable_ai = value;
+ settings_content.project.disable_ai = value;
},
}),
metadata: None,
- files: USER,
+ files: USER | PROJECT,
}),
]
}
@@ -5222,7 +5222,7 @@ mod tests {
cx.update(|cx| {
SettingsStore::update_global(cx, |settings_store, cx| {
settings_store.update_user_settings(cx, |settings| {
- settings.disable_ai = Some(SaturatingBool(true));
+ settings.project.disable_ai = Some(SaturatingBool(true));
});
});
});