Merge branch 'main' of https://github.com/jacobtread/zed into feat-git-commit-list

This commit is contained in:
Jacobtread 2025-08-18 00:03:18 +12:00
commit c79046effc
490 changed files with 21892 additions and 14140 deletions

View file

@ -414,6 +414,7 @@ impl GitStore {
pub fn init(client: &AnyProtoClient) {
client.add_entity_request_handler(Self::handle_get_remotes);
client.add_entity_request_handler(Self::handle_get_branches);
client.add_entity_request_handler(Self::handle_get_default_branch);
client.add_entity_request_handler(Self::handle_change_branch);
client.add_entity_request_handler(Self::handle_create_branch);
client.add_entity_request_handler(Self::handle_git_init);
@ -441,6 +442,7 @@ impl GitStore {
client.add_entity_request_handler(Self::handle_blame_buffer);
client.add_entity_message_handler(Self::handle_update_repository);
client.add_entity_message_handler(Self::handle_remove_repository);
client.add_entity_request_handler(Self::handle_git_clone);
}
pub fn is_local(&self) -> bool {
@ -1464,6 +1466,45 @@ impl GitStore {
}
}
pub fn git_clone(
&self,
repo: String,
path: impl Into<Arc<std::path::Path>>,
cx: &App,
) -> Task<Result<()>> {
let path = path.into();
match &self.state {
GitStoreState::Local { fs, .. } => {
let fs = fs.clone();
cx.background_executor()
.spawn(async move { fs.git_clone(&repo, &path).await })
}
GitStoreState::Ssh {
upstream_client,
upstream_project_id,
..
} => {
let request = upstream_client.request(proto::GitClone {
project_id: upstream_project_id.0,
abs_path: path.to_string_lossy().to_string(),
remote_repo: repo,
});
cx.background_spawn(async move {
let result = request.await?;
match result.success {
true => Ok(()),
false => Err(anyhow!("Git Clone failed")),
}
})
}
GitStoreState::Remote { .. } => {
Task::ready(Err(anyhow!("Git Clone isn't supported for remote users")))
}
}
}
async fn handle_update_repository(
this: Entity<Self>,
envelope: TypedEnvelope<proto::UpdateRepository>,
@ -1550,6 +1591,22 @@ impl GitStore {
Ok(proto::Ack {})
}
async fn handle_git_clone(
this: Entity<Self>,
envelope: TypedEnvelope<proto::GitClone>,
cx: AsyncApp,
) -> Result<proto::GitCloneResponse> {
let path: Arc<Path> = PathBuf::from(envelope.payload.abs_path).into();
let repo_name = envelope.payload.remote_repo;
let result = cx
.update(|cx| this.read(cx).git_clone(repo_name, path, cx))?
.await;
Ok(proto::GitCloneResponse {
success: result.is_ok(),
})
}
async fn handle_fetch(
this: Entity<Self>,
envelope: TypedEnvelope<proto::Fetch>,
@ -1838,6 +1895,23 @@ impl GitStore {
.collect::<Vec<_>>(),
})
}
async fn handle_get_default_branch(
this: Entity<Self>,
envelope: TypedEnvelope<proto::GetDefaultBranch>,
mut cx: AsyncApp,
) -> Result<proto::GetDefaultBranchResponse> {
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
let branch = repository_handle
.update(&mut cx, |repository_handle, _| {
repository_handle.default_branch()
})?
.await??
.map(Into::into);
Ok(proto::GetDefaultBranchResponse { branch })
}
async fn handle_create_branch(
this: Entity<Self>,
envelope: TypedEnvelope<proto::GitCreateBranch>,
@ -2275,7 +2349,7 @@ impl GitStore {
return None;
};
let mut paths = vec![];
let mut paths = Vec::new();
// All paths prefixed by a given repo will constitute a continuous range.
while let Some(path) = entries.get(ix)
&& let Some(repo_path) =
@ -2284,7 +2358,11 @@ impl GitStore {
paths.push((repo_path, ix));
ix += 1;
}
Some((repo, paths))
if paths.is_empty() {
None
} else {
Some((repo, paths))
}
});
tasks.push_back(task);
}
@ -4299,7 +4377,8 @@ impl Repository {
bail!("not a local repository")
};
let (snapshot, events) = this
.read_with(&mut cx, |this, _| {
.update(&mut cx, |this, _| {
this.paths_needing_status_update.clear();
compute_snapshot(
this.id,
this.work_directory_abs_path.clone(),
@ -4529,6 +4608,9 @@ impl Repository {
};
let paths = changed_paths.iter().cloned().collect::<Vec<_>>();
if paths.is_empty() {
return Ok(());
}
let statuses = backend.status(&paths).await?;
let changed_path_statuses = cx

View file

@ -390,13 +390,17 @@ impl LocalLspStore {
delegate.update_status(
adapter.name(),
BinaryStatus::Failed {
error: format!("{err}\n-- stderr--\n{log}"),
error: if log.is_empty() {
format!("{err:#}")
} else {
format!("{err:#}\n-- stderr --\n{log}")
},
},
);
let message =
format!("Failed to start language server {server_name:?}: {err:#?}");
log::error!("{message}");
log::error!("server stderr: {log}");
log::error!("Failed to start language server {server_name:?}: {err:?}");
if !log.is_empty() {
log::error!("server stderr: {log}");
}
None
}
}
@ -638,139 +642,27 @@ impl LocalLspStore {
language_server
.on_request::<lsp::request::RegisterCapability, _, _>({
let this = this.clone();
let lsp_store = this.clone();
move |params, cx| {
let lsp_store = this.clone();
let lsp_store = lsp_store.clone();
let mut cx = cx.clone();
async move {
for reg in params.registrations {
match reg.method.as_str() {
"workspace/didChangeWatchedFiles" => {
if let Some(options) = reg.register_options {
let options = serde_json::from_value(options)?;
lsp_store.update(&mut cx, |this, cx| {
this.as_local_mut()?.on_lsp_did_change_watched_files(
server_id, &reg.id, options, cx,
lsp_store
.update(&mut cx, |lsp_store, cx| {
if lsp_store.as_local().is_some() {
match lsp_store
.register_server_capabilities(server_id, params, cx)
{
Ok(()) => {}
Err(e) => {
log::error!(
"Failed to register server capabilities: {e:#}"
);
Some(())
})?;
}
}
"textDocument/rangeFormatting" => {
lsp_store.update(&mut cx, |lsp_store, cx| {
if let Some(server) =
lsp_store.language_server_for_id(server_id)
{
let options = reg
.register_options
.map(|options| {
serde_json::from_value::<
lsp::DocumentRangeFormattingOptions,
>(
options
)
})
.transpose()?;
let provider = match options {
None => OneOf::Left(true),
Some(options) => OneOf::Right(options),
};
server.update_capabilities(|capabilities| {
capabilities.document_range_formatting_provider =
Some(provider);
});
notify_server_capabilities_updated(&server, cx);
}
anyhow::Ok(())
})??;
};
}
"textDocument/onTypeFormatting" => {
lsp_store.update(&mut cx, |lsp_store, cx| {
if let Some(server) =
lsp_store.language_server_for_id(server_id)
{
let options = reg
.register_options
.map(|options| {
serde_json::from_value::<
lsp::DocumentOnTypeFormattingOptions,
>(
options
)
})
.transpose()?;
if let Some(options) = options {
server.update_capabilities(|capabilities| {
capabilities
.document_on_type_formatting_provider =
Some(options);
});
notify_server_capabilities_updated(&server, cx);
}
}
anyhow::Ok(())
})??;
}
"textDocument/formatting" => {
lsp_store.update(&mut cx, |lsp_store, cx| {
if let Some(server) =
lsp_store.language_server_for_id(server_id)
{
let options = reg
.register_options
.map(|options| {
serde_json::from_value::<
lsp::DocumentFormattingOptions,
>(
options
)
})
.transpose()?;
let provider = match options {
None => OneOf::Left(true),
Some(options) => OneOf::Right(options),
};
server.update_capabilities(|capabilities| {
capabilities.document_formatting_provider =
Some(provider);
});
notify_server_capabilities_updated(&server, cx);
}
anyhow::Ok(())
})??;
}
"workspace/didChangeConfiguration" => {
// Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings.
}
"textDocument/rename" => {
lsp_store.update(&mut cx, |lsp_store, cx| {
if let Some(server) =
lsp_store.language_server_for_id(server_id)
{
let options = reg
.register_options
.map(|options| {
serde_json::from_value::<lsp::RenameOptions>(
options,
)
})
.transpose()?;
let options = match options {
None => OneOf::Left(true),
Some(options) => OneOf::Right(options),
};
server.update_capabilities(|capabilities| {
capabilities.rename_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
}
anyhow::Ok(())
})??;
}
_ => log::warn!("unhandled capability registration: {reg:?}"),
}
}
})
.ok();
Ok(())
}
}
@ -779,79 +671,27 @@ impl LocalLspStore {
language_server
.on_request::<lsp::request::UnregisterCapability, _, _>({
let this = this.clone();
let lsp_store = this.clone();
move |params, cx| {
let lsp_store = this.clone();
let lsp_store = lsp_store.clone();
let mut cx = cx.clone();
async move {
for unreg in params.unregisterations.iter() {
match unreg.method.as_str() {
"workspace/didChangeWatchedFiles" => {
lsp_store.update(&mut cx, |lsp_store, cx| {
lsp_store
.as_local_mut()?
.on_lsp_unregister_did_change_watched_files(
server_id, &unreg.id, cx,
lsp_store
.update(&mut cx, |lsp_store, cx| {
if lsp_store.as_local().is_some() {
match lsp_store
.unregister_server_capabilities(server_id, params, cx)
{
Ok(()) => {}
Err(e) => {
log::error!(
"Failed to unregister server capabilities: {e:#}"
);
Some(())
})?;
}
"workspace/didChangeConfiguration" => {
// Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings.
}
"textDocument/rename" => {
lsp_store.update(&mut cx, |lsp_store, cx| {
if let Some(server) =
lsp_store.language_server_for_id(server_id)
{
server.update_capabilities(|capabilities| {
capabilities.rename_provider = None
});
notify_server_capabilities_updated(&server, cx);
}
})?;
}
}
"textDocument/rangeFormatting" => {
lsp_store.update(&mut cx, |lsp_store, cx| {
if let Some(server) =
lsp_store.language_server_for_id(server_id)
{
server.update_capabilities(|capabilities| {
capabilities.document_range_formatting_provider =
None
});
notify_server_capabilities_updated(&server, cx);
}
})?;
}
"textDocument/onTypeFormatting" => {
lsp_store.update(&mut cx, |lsp_store, cx| {
if let Some(server) =
lsp_store.language_server_for_id(server_id)
{
server.update_capabilities(|capabilities| {
capabilities.document_on_type_formatting_provider =
None;
});
notify_server_capabilities_updated(&server, cx);
}
})?;
}
"textDocument/formatting" => {
lsp_store.update(&mut cx, |lsp_store, cx| {
if let Some(server) =
lsp_store.language_server_for_id(server_id)
{
server.update_capabilities(|capabilities| {
capabilities.document_formatting_provider = None;
});
notify_server_capabilities_updated(&server, cx);
}
})?;
}
_ => log::warn!("unhandled capability unregistration: {unreg:?}"),
}
}
})
.ok();
Ok(())
}
}
@ -3519,6 +3359,16 @@ impl LocalLspStore {
Ok(workspace_config)
}
fn language_server_for_id(&self, id: LanguageServerId) -> Option<Arc<LanguageServer>> {
if let Some(LanguageServerState::Running { server, .. }) = self.language_servers.get(&id) {
Some(server.clone())
} else if let Some((_, server)) = self.supplementary_language_servers.get(&id) {
Some(Arc::clone(server))
} else {
None
}
}
}
fn notify_server_capabilities_updated(server: &LanguageServer, cx: &mut Context<LspStore>) {
@ -9434,16 +9284,7 @@ impl LspStore {
}
pub fn language_server_for_id(&self, id: LanguageServerId) -> Option<Arc<LanguageServer>> {
let local_lsp_store = self.as_local()?;
if let Some(LanguageServerState::Running { server, .. }) =
local_lsp_store.language_servers.get(&id)
{
Some(server.clone())
} else if let Some((_, server)) = local_lsp_store.supplementary_language_servers.get(&id) {
Some(Arc::clone(server))
} else {
None
}
self.as_local()?.language_server_for_id(id)
}
fn on_lsp_progress(
@ -11808,6 +11649,375 @@ impl LspStore {
.log_err();
}
}
fn register_server_capabilities(
&mut self,
server_id: LanguageServerId,
params: lsp::RegistrationParams,
cx: &mut Context<Self>,
) -> anyhow::Result<()> {
let server = self
.language_server_for_id(server_id)
.with_context(|| format!("no server {server_id} found"))?;
for reg in params.registrations {
match reg.method.as_str() {
"workspace/didChangeWatchedFiles" => {
if let Some(options) = reg.register_options {
let notify = if let Some(local_lsp_store) = self.as_local_mut() {
let caps = serde_json::from_value(options)?;
local_lsp_store
.on_lsp_did_change_watched_files(server_id, &reg.id, caps, cx);
true
} else {
false
};
if notify {
notify_server_capabilities_updated(&server, cx);
}
}
}
"workspace/didChangeConfiguration" => {
// Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings.
}
"workspace/symbol" => {
if let Some(options) = parse_register_capabilities(reg)? {
server.update_capabilities(|capabilities| {
capabilities.workspace_symbol_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
}
}
"workspace/fileOperations" => {
if let Some(options) = reg.register_options {
let caps = serde_json::from_value(options)?;
server.update_capabilities(|capabilities| {
capabilities
.workspace
.get_or_insert_default()
.file_operations = Some(caps);
});
notify_server_capabilities_updated(&server, cx);
}
}
"workspace/executeCommand" => {
if let Some(options) = reg.register_options {
let options = serde_json::from_value(options)?;
server.update_capabilities(|capabilities| {
capabilities.execute_command_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/rangeFormatting" => {
if let Some(options) = parse_register_capabilities(reg)? {
server.update_capabilities(|capabilities| {
capabilities.document_range_formatting_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/onTypeFormatting" => {
if let Some(options) = reg
.register_options
.map(serde_json::from_value)
.transpose()?
{
server.update_capabilities(|capabilities| {
capabilities.document_on_type_formatting_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/formatting" => {
if let Some(options) = parse_register_capabilities(reg)? {
server.update_capabilities(|capabilities| {
capabilities.document_formatting_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/rename" => {
if let Some(options) = parse_register_capabilities(reg)? {
server.update_capabilities(|capabilities| {
capabilities.rename_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/inlayHint" => {
if let Some(options) = parse_register_capabilities(reg)? {
server.update_capabilities(|capabilities| {
capabilities.inlay_hint_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/documentSymbol" => {
if let Some(options) = parse_register_capabilities(reg)? {
server.update_capabilities(|capabilities| {
capabilities.document_symbol_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/codeAction" => {
if let Some(options) = reg
.register_options
.map(serde_json::from_value)
.transpose()?
{
server.update_capabilities(|capabilities| {
capabilities.code_action_provider =
Some(lsp::CodeActionProviderCapability::Options(options));
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/definition" => {
if let Some(options) = parse_register_capabilities(reg)? {
server.update_capabilities(|capabilities| {
capabilities.definition_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/completion" => {
if let Some(caps) = reg
.register_options
.map(serde_json::from_value)
.transpose()?
{
server.update_capabilities(|capabilities| {
capabilities.completion_provider = Some(caps);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/hover" => {
if let Some(caps) = reg
.register_options
.map(serde_json::from_value)
.transpose()?
{
server.update_capabilities(|capabilities| {
capabilities.hover_provider = Some(caps);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/signatureHelp" => {
if let Some(caps) = reg
.register_options
.map(serde_json::from_value)
.transpose()?
{
server.update_capabilities(|capabilities| {
capabilities.signature_help_provider = Some(caps);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/didChange" => {
if let Some(sync_kind) = reg
.register_options
.and_then(|opts| opts.get("syncKind").cloned())
.map(serde_json::from_value::<lsp::TextDocumentSyncKind>)
.transpose()?
{
server.update_capabilities(|capabilities| {
capabilities.text_document_sync =
Some(lsp::TextDocumentSyncCapability::Kind(sync_kind));
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/codeLens" => {
if let Some(caps) = reg
.register_options
.map(serde_json::from_value)
.transpose()?
{
server.update_capabilities(|capabilities| {
capabilities.code_lens_provider = Some(caps);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/diagnostic" => {
if let Some(caps) = reg
.register_options
.map(serde_json::from_value)
.transpose()?
{
server.update_capabilities(|capabilities| {
capabilities.diagnostic_provider = Some(caps);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/colorProvider" => {
if let Some(caps) = reg
.register_options
.map(serde_json::from_value)
.transpose()?
{
server.update_capabilities(|capabilities| {
capabilities.color_provider = Some(caps);
});
notify_server_capabilities_updated(&server, cx);
}
}
_ => log::warn!("unhandled capability registration: {reg:?}"),
}
}
Ok(())
}
fn unregister_server_capabilities(
&mut self,
server_id: LanguageServerId,
params: lsp::UnregistrationParams,
cx: &mut Context<Self>,
) -> anyhow::Result<()> {
let server = self
.language_server_for_id(server_id)
.with_context(|| format!("no server {server_id} found"))?;
for unreg in params.unregisterations.iter() {
match unreg.method.as_str() {
"workspace/didChangeWatchedFiles" => {
let notify = if let Some(local_lsp_store) = self.as_local_mut() {
local_lsp_store
.on_lsp_unregister_did_change_watched_files(server_id, &unreg.id, cx);
true
} else {
false
};
if notify {
notify_server_capabilities_updated(&server, cx);
}
}
"workspace/didChangeConfiguration" => {
// Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings.
}
"workspace/symbol" => {
server.update_capabilities(|capabilities| {
capabilities.workspace_symbol_provider = None
});
notify_server_capabilities_updated(&server, cx);
}
"workspace/fileOperations" => {
server.update_capabilities(|capabilities| {
capabilities
.workspace
.get_or_insert_with(|| lsp::WorkspaceServerCapabilities {
workspace_folders: None,
file_operations: None,
})
.file_operations = None;
});
notify_server_capabilities_updated(&server, cx);
}
"workspace/executeCommand" => {
server.update_capabilities(|capabilities| {
capabilities.execute_command_provider = None;
});
notify_server_capabilities_updated(&server, cx);
}
"textDocument/rangeFormatting" => {
server.update_capabilities(|capabilities| {
capabilities.document_range_formatting_provider = None
});
notify_server_capabilities_updated(&server, cx);
}
"textDocument/onTypeFormatting" => {
server.update_capabilities(|capabilities| {
capabilities.document_on_type_formatting_provider = None;
});
notify_server_capabilities_updated(&server, cx);
}
"textDocument/formatting" => {
server.update_capabilities(|capabilities| {
capabilities.document_formatting_provider = None;
});
notify_server_capabilities_updated(&server, cx);
}
"textDocument/rename" => {
server.update_capabilities(|capabilities| capabilities.rename_provider = None);
notify_server_capabilities_updated(&server, cx);
}
"textDocument/codeAction" => {
server.update_capabilities(|capabilities| {
capabilities.code_action_provider = None;
});
notify_server_capabilities_updated(&server, cx);
}
"textDocument/definition" => {
server.update_capabilities(|capabilities| {
capabilities.definition_provider = None;
});
notify_server_capabilities_updated(&server, cx);
}
"textDocument/completion" => {
server.update_capabilities(|capabilities| {
capabilities.completion_provider = None;
});
notify_server_capabilities_updated(&server, cx);
}
"textDocument/hover" => {
server.update_capabilities(|capabilities| {
capabilities.hover_provider = None;
});
notify_server_capabilities_updated(&server, cx);
}
"textDocument/signatureHelp" => {
server.update_capabilities(|capabilities| {
capabilities.signature_help_provider = None;
});
notify_server_capabilities_updated(&server, cx);
}
"textDocument/didChange" => {
server.update_capabilities(|capabilities| {
capabilities.text_document_sync = None;
});
notify_server_capabilities_updated(&server, cx);
}
"textDocument/codeLens" => {
server.update_capabilities(|capabilities| {
capabilities.code_lens_provider = None;
});
notify_server_capabilities_updated(&server, cx);
}
"textDocument/diagnostic" => {
server.update_capabilities(|capabilities| {
capabilities.diagnostic_provider = None;
});
notify_server_capabilities_updated(&server, cx);
}
"textDocument/colorProvider" => {
server.update_capabilities(|capabilities| {
capabilities.color_provider = None;
});
notify_server_capabilities_updated(&server, cx);
}
_ => log::warn!("unhandled capability unregistration: {unreg:?}"),
}
}
Ok(())
}
}
// Registration with empty capabilities should be ignored.
// https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/formatting.ts#L67-L70
fn parse_register_capabilities<T: serde::de::DeserializeOwned>(
reg: lsp::Registration,
) -> anyhow::Result<Option<OneOf<bool, T>>> {
Ok(reg
.register_options
.map(|options| serde_json::from_value::<T>(options))
.transpose()?
.map(OneOf::Right))
}
fn subscribe_to_binary_statuses(

View file

@ -962,14 +962,19 @@ impl settings::Settings for DisableAiSettings {
type FileContent = Option<bool>;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
Ok(Self {
disable_ai: sources
.user
.or(sources.server)
.copied()
.flatten()
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?),
})
// For security reasons, settings can only make AI restrictions MORE strict, not less.
// (For example, if someone is working on a project that contractually
// requires no AI use, that should override the user's setting which
// permits AI use.)
// This also prevents an attacker from using project or server settings to enable AI when it should be disabled.
let disable_ai = sources
.project
.iter()
.chain(sources.user.iter())
.chain(sources.server.iter())
.any(|disabled| **disabled == Some(true));
Ok(Self { disable_ai })
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
@ -5508,3 +5513,153 @@ fn provide_inline_values(
variables
}
#[cfg(test)]
mod disable_ai_settings_tests {
use super::*;
use gpui::TestAppContext;
use settings::{Settings, SettingsSources};
#[gpui::test]
async fn test_disable_ai_settings_security(cx: &mut TestAppContext) {
cx.update(|cx| {
// Test 1: Default is false (AI enabled)
let sources = SettingsSources {
default: &Some(false),
global: None,
extensions: None,
user: None,
release_channel: None,
operating_system: None,
profile: None,
server: None,
project: &[],
};
let settings = DisableAiSettings::load(sources, cx).unwrap();
assert_eq!(settings.disable_ai, false, "Default should allow AI");
// Test 2: Global true, local false -> still disabled (local cannot re-enable)
let global_true = Some(true);
let local_false = Some(false);
let sources = SettingsSources {
default: &Some(false),
global: None,
extensions: None,
user: Some(&global_true),
release_channel: None,
operating_system: None,
profile: None,
server: None,
project: &[&local_false],
};
let settings = DisableAiSettings::load(sources, cx).unwrap();
assert_eq!(
settings.disable_ai, true,
"Local false cannot override global true"
);
// Test 3: Global false, local true -> disabled (local can make more restrictive)
let global_false = Some(false);
let local_true = Some(true);
let sources = SettingsSources {
default: &Some(false),
global: None,
extensions: None,
user: Some(&global_false),
release_channel: None,
operating_system: None,
profile: None,
server: None,
project: &[&local_true],
};
let settings = DisableAiSettings::load(sources, cx).unwrap();
assert_eq!(
settings.disable_ai, true,
"Local true can override global false"
);
// Test 4: Server can only make more restrictive (set to true)
let user_false = Some(false);
let server_true = Some(true);
let sources = SettingsSources {
default: &Some(false),
global: None,
extensions: None,
user: Some(&user_false),
release_channel: None,
operating_system: None,
profile: None,
server: Some(&server_true),
project: &[],
};
let settings = DisableAiSettings::load(sources, cx).unwrap();
assert_eq!(
settings.disable_ai, true,
"Server can set to true even if user is false"
);
// Test 5: Server false cannot override user true
let user_true = Some(true);
let server_false = Some(false);
let sources = SettingsSources {
default: &Some(false),
global: None,
extensions: None,
user: Some(&user_true),
release_channel: None,
operating_system: None,
profile: None,
server: Some(&server_false),
project: &[],
};
let settings = DisableAiSettings::load(sources, cx).unwrap();
assert_eq!(
settings.disable_ai, true,
"Server false cannot override user true"
);
// Test 6: Multiple local settings, any true disables AI
let global_false = Some(false);
let local_false3 = Some(false);
let local_true2 = Some(true);
let local_false4 = Some(false);
let sources = SettingsSources {
default: &Some(false),
global: None,
extensions: None,
user: Some(&global_false),
release_channel: None,
operating_system: None,
profile: None,
server: None,
project: &[&local_false3, &local_true2, &local_false4],
};
let settings = DisableAiSettings::load(sources, cx).unwrap();
assert_eq!(
settings.disable_ai, true,
"Any local true should disable AI"
);
// Test 7: All three sources can independently disable AI
let user_false2 = Some(false);
let server_false2 = Some(false);
let local_true3 = Some(true);
let sources = SettingsSources {
default: &Some(false),
global: None,
extensions: None,
user: Some(&user_false2),
release_channel: None,
operating_system: None,
profile: None,
server: Some(&server_false2),
project: &[&local_true3],
};
let settings = DisableAiSettings::load(sources, cx).unwrap();
assert_eq!(
settings.disable_ai, true,
"Local can disable even if user and server are false"
);
});
}
}

View file

@ -256,7 +256,7 @@ impl Project {
let local_path = if is_ssh_terminal { None } else { path.clone() };
let mut python_venv_activate_command = None;
let mut python_venv_activate_command = Task::ready(None);
let (spawn_task, shell) = match kind {
TerminalKind::Shell(_) => {
@ -265,6 +265,7 @@ impl Project {
python_venv_directory,
&settings.detect_venv,
&settings.shell,
cx,
);
}
@ -419,9 +420,12 @@ impl Project {
})
.detach();
if let Some(activate_command) = python_venv_activate_command {
this.activate_python_virtual_environment(activate_command, &terminal_handle, cx);
}
this.activate_python_virtual_environment(
python_venv_activate_command,
&terminal_handle,
cx,
);
terminal_handle
})
}
@ -539,12 +543,15 @@ impl Project {
venv_base_directory: &Path,
venv_settings: &VenvSettings,
shell: &Shell,
) -> Option<String> {
let venv_settings = venv_settings.as_option()?;
cx: &mut App,
) -> Task<Option<String>> {
let Some(venv_settings) = venv_settings.as_option() else {
return Task::ready(None);
};
let activate_keyword = match venv_settings.activate_script {
terminal_settings::ActivateScript::Default => match std::env::consts::OS {
"windows" => ".",
_ => "source",
_ => ".",
},
terminal_settings::ActivateScript::Nushell => "overlay use",
terminal_settings::ActivateScript::PowerShell => ".",
@ -589,30 +596,44 @@ impl Project {
.join(activate_script_name)
.to_string_lossy()
.to_string();
let quoted = shlex::try_quote(&path).ok()?;
smol::block_on(self.fs.metadata(path.as_ref()))
.ok()
.flatten()?;
Some(format!(
"{} {} ; clear{}",
activate_keyword, quoted, line_ending
))
let is_valid_path = self.resolve_abs_path(path.as_ref(), cx);
cx.background_spawn(async move {
let quoted = shlex::try_quote(&path).ok()?;
if is_valid_path.await.is_some_and(|meta| meta.is_file()) {
Some(format!(
"{} {} ; clear{}",
activate_keyword, quoted, line_ending
))
} else {
None
}
})
} else {
Some(format!(
Task::ready(Some(format!(
"{activate_keyword} {activate_script_name} {name}; clear{line_ending}",
name = venv_settings.venv_name
))
)))
}
}
fn activate_python_virtual_environment(
&self,
command: String,
command: Task<Option<String>>,
terminal_handle: &Entity<Terminal>,
cx: &mut App,
) {
terminal_handle.update(cx, |terminal, _| terminal.input(command.into_bytes()));
terminal_handle.update(cx, |_, cx| {
cx.spawn(async move |this, cx| {
if let Some(command) = command.await {
this.update(cx, |this, _| {
this.input(command.into_bytes());
})
.ok();
}
})
.detach()
});
}
pub fn local_terminal_handles(&self) -> &Vec<WeakEntity<terminal::Terminal>> {