//! T3 Code's settings scope: "Applying settings for <project> across
//! <environment>" at the top of the pages with environment settings. The
//! environment picker edits this device, one connected device, or all of
//! them at once; the project picker edits a project's overrides on them.
//! Rows that are a preference of this device ignore the selection.
//!
//! Each render works out what the selection shows ([`Shown`]): the values
//! the controls display, where the selected environments disagree, and
//! which settings the project overrides. Rows read it by their anchor, and
//! every change is turned into a [`SettingsPatch`] for each environment.
use std::{
collections::{BTreeMap, HashMap, HashSet},
path::PathBuf,
rc::Rc,
};
use gpui_kit::component::{
Icon, h_flex,
menu::{DropdownMenu as _, PopupMenuItem},
v_flex,
};
use gpui_kit::prelude::FluentBuilder as _;
use gpui_kit::*;
use super::SettingsView;
use crate::settings::{self, ENVIRONMENT_KEYS, PROJECT_KEYS, Settings, SettingsPatch};
use crate::ui::icons::Lucide;
use crate::ui::metal::{self, MetalButton};
use crate::ui::workspace::{self, SettingsEnvironment};
/// The rows that edit environment settings, by anchor: their title and the
/// settings they show.
pub(crate) const SCOPED_ROWS: &[(&str, &str, &[&str])] = &[
("default-model", "Default model", &["default_model", "default_reasoning"]),
("default-permissions", "Permissions", &["default_access"]),
("new-threads", "Workspace", &["workspace"]),
("worktree-submodules", "Submodules", &["submodules"]),
("auto-resume-limited-threads", "Auto-resume limited threads", &["auto_resume_limited"]),
("snooze-limited-threads", "Snooze limited threads", &["snooze_limited"]),
("auto-settle-merged-threads", "Auto-settle merged threads", &["auto_settle_merged"]),
("auto-settle-inactive-threads", "Auto-settle inactive threads", &["auto_settle_inactive"]),
("days-before-auto-settle", "Days of inactivity before auto-settle", &["inactive_days"]),
("provider-update-checks", "Provider update checks", &["update_checks"]),
("continue-threads-after-server-update", "Continue threads after restarts", &["continue_after_restart"]),
("start-from-origin", "Start from origin", &["start_from_origin"]),
("text-generation-model", "Text generation model", &["text_model", "text_reasoning"]),
("agent-browser-access", "Agent browser access", &["agent_browser"]),
("worktree-branch-naming", "Worktree branch naming", &["branch_naming"]),
("branch-prefix", "Branch prefix", &["branch_prefix"]),
("branch-naming-instructions", "Branch naming instructions", &["branch_instructions"]),
("source-control-writing-style", "Source control writing style", &["writing_style", "writing_instructions"]),
("follow-change-request-templates", "Follow change request templates", &["follow_pr_templates"]),
("storage-worktrees-location", "Worktree location", &["worktrees_dir"]),
("storage-worktrees-deleted", "Delete worktrees with deleted threads", &["cleanup_deleted_worktrees"]),
("storage-worktrees-inactive", "Delete inactive worktrees", &["cleanup_inactive_worktrees"]),
("storage-worktrees-merged", "Delete merged worktrees", &["cleanup_merged_worktrees"]),
("storage-worktrees-unchanged", "Delete unchanged worktrees", &["cleanup_unchanged_worktrees"]),
];
/// What the open settings page shows: the selection's values where it
/// applies, this device's elsewhere.
pub(crate) fn shown_settings(cx: &App) -> Settings {
cx.try_global::<Shown>().map(|shown| shown.settings.clone()).unwrap_or_else(settings::load)
}
fn row_keys(id: &str) -> Option<&'static [&'static str]> {
SCOPED_ROWS.iter().find(|(row, _, _)| *row == id).map(|(_, _, keys)| *keys)
}
/// The titles of the rows showing any of `keys`, for saying what differs.
pub(crate) fn row_titles(keys: &[String]) -> Vec<&'static str> {
SCOPED_ROWS
.iter()
.filter(|(_, _, row_keys)| row_keys.iter().any(|key| keys.iter().any(|candidate| candidate == key)))
.map(|(_, title, _)| *title)
.collect()
}
/// Which environments a change goes to.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) enum EnvironmentPick {
#[default]
All,
/// One environment: this device (`None`) or a connection, by id.
One(Option<String>),
}
/// The settings view's selection, kept as pages change.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct Scope {
pub environment: EnvironmentPick,
/// A project by name: the same project on every environment.
pub project: Option<String>,
}
/// One place a change is written: an environment, and the project's path
/// there when a project is picked.
#[derive(Clone, Debug)]
pub(crate) struct WriteTarget {
pub environment: Option<String>,
pub name: String,
pub project: Option<PathBuf>,
/// This device's preferences with the target's settings, as an edit
/// there starts from.
pub settings: Settings,
}
/// What the open page shows for the selection. Rows find it as a global.
pub(crate) struct Shown {
pub view: WeakEntity<SettingsView>,
/// The page edits environment settings, so rows apply the selection.
pub scoped: bool,
/// This device's preferences, with the first selected environment's
/// settings (and the project's overrides) in place.
pub settings: Settings,
pub project: Option<String>,
pub targets: Vec<WriteTarget>,
/// Why environment settings can't change right now.
pub locked: Option<SharedString>,
/// Settings the selected environments disagree on: each one's value.
pub mixed: HashMap<String, Vec<(String, String)>>,
/// Settings the picked project overrides on any selected environment.
pub overridden: HashSet<String>,
}
impl Global for Shown {}
impl Shown {
pub fn compute(
view: WeakEntity<SettingsView>,
scope: &Scope,
scoped: bool,
environments: &[SettingsEnvironment],
) -> Self {
let local = settings::load();
let mut shown = Shown {
view,
scoped,
settings: local.clone(),
project: scope.project.clone(),
targets: vec![WriteTarget { environment: None, name: String::new(), project: None, settings: local.clone() }],
locked: None,
mixed: HashMap::new(),
overridden: HashSet::new(),
};
if !scoped {
return shown;
}
let selected: Vec<&SettingsEnvironment> = match &scope.environment {
EnvironmentPick::All => environments.iter().filter(|env| env.connected && env.settings.is_some()).collect(),
EnvironmentPick::One(id) => match environments.iter().find(|env| env.id == *id) {
None => {
shown.locked = Some("That environment is no longer connected.".into());
Vec::new()
}
Some(env) if !env.connected => {
shown.locked = Some(format!("{} is offline; its settings stay as they are.", env.name).into());
Vec::new()
}
Some(env) if env.settings.is_none() => {
shown.locked = Some(
format!("{} doesn't share its settings yet. Update the app there to edit them here.", env.name)
.into(),
);
Vec::new()
}
Some(env) => vec![env],
},
};
// Each selected environment's settings as the selection sees them.
let mut effective: Vec<(WriteTarget, Settings, Option<serde_json::Map<String, serde_json::Value>>)> = Vec::new();
for env in selected {
let Some(settings) = env.settings.as_ref() else { continue };
let target = |project: Option<PathBuf>| WriteTarget {
environment: env.id.clone(),
name: env.name.clone(),
project,
settings: Settings::default(),
};
match &scope.project {
None => effective.push((target(None), settings.clone(), None)),
Some(name) => {
for (_, path) in env.projects.iter().filter(|(project, _)| project == name) {
let overrides = settings.project_overrides.get(&path.display().to_string()).cloned();
effective.push((target(Some(path.clone())), settings.for_project(path), overrides));
}
}
}
}
if effective.is_empty() {
if shown.locked.is_none() {
shown.locked = Some(match &scope.project {
Some(name) => format!("{name} isn't a project on the selected environments.").into(),
None => "No connected environment shares its settings.".into(),
});
}
shown.targets.clear();
return shown;
}
let values: Vec<serde_json::Map<String, serde_json::Value>> =
effective.iter().map(|(_, settings, _)| without_overrides(settings.environment_values())).collect();
shown.settings = settings::with_values(&local, &values[0]);
shown.settings.project_overrides = local.project_overrides.clone();
for key in ENVIRONMENT_KEYS {
let first = values[0].get(*key);
if values.iter().any(|other| other.get(*key) != first) {
let list = effective
.iter()
.zip(&values)
.map(|((target, _, _), values)| {
(target.name.clone(), describe(values.get(*key).unwrap_or(&serde_json::Value::Null)))
})
.collect();
shown.mixed.insert(key.to_string(), list);
}
}
shown.overridden = effective.iter().flat_map(|(_, _, overrides)| overrides.iter().flat_map(|map| map.keys().cloned())).collect();
shown.targets = effective
.into_iter()
.zip(&values)
.map(|((target, _, _), values)| WriteTarget { settings: settings::with_values(&local, values), ..target })
.collect();
shown
}
/// How a row with anchor `id` appears, when the selection touches it.
pub fn row(&self, id: &str) -> Option<RowScope> {
if !self.scoped {
return None;
}
let keys = row_keys(id)?;
let overridable = keys.iter().all(|key| PROJECT_KEYS.contains(key));
let locked = self.locked.clone().or_else(|| {
(self.project.is_some() && !overridable)
.then(|| "Applies to the whole environment. Pick All projects to change it.".into())
});
let mixed: Vec<(String, String)> = keys.iter().filter_map(|key| self.mixed.get(*key)).flatten().cloned().collect();
Some(RowScope {
keys,
locked,
mixed: (!mixed.is_empty()).then_some(mixed),
project: self.project.is_some() && overridable,
overridden: keys.iter().any(|key| self.overridden.contains(*key)),
view: self.view.clone(),
})
}
}
/// What the selection means for one row.
pub(crate) struct RowScope {
pub keys: &'static [&'static str],
pub locked: Option<SharedString>,
/// Each selected environment's value, when they differ.
pub mixed: Option<Vec<(String, String)>>,
/// The row edits the picked project's override.
pub project: bool,
pub overridden: bool,
pub view: WeakEntity<SettingsView>,
}
fn without_overrides(mut values: serde_json::Map<String, serde_json::Value>) -> serde_json::Map<String, serde_json::Value> {
values.remove("project_overrides");
values
}
/// A setting's value in a few words, for the Mixed tooltip.
fn describe(value: &serde_json::Value) -> String {
match value {
serde_json::Value::Null => "Automatic".into(),
serde_json::Value::Bool(true) => "On".into(),
serde_json::Value::Bool(false) => "Off".into(),
serde_json::Value::String(text) if text.is_empty() => "Empty".into(),
serde_json::Value::String(text) => {
let text = text.replace('_', " ");
let mut chars = text.chars();
match chars.next() {
Some(first) => first.to_uppercase().chain(chars).collect(),
None => text,
}
}
other => other.to_string(),
}
}
/// The patch writing `values` to `target`: environment settings, or the
/// project's overrides of them.
fn patch_for(target: &WriteTarget, values: &serde_json::Map<String, serde_json::Value>) -> SettingsPatch {
match &target.project {
None => SettingsPatch { values: values.clone(), projects: BTreeMap::new() },
Some(path) => {
let overrides = values.iter().filter(|(key, _)| PROJECT_KEYS.contains(&key.as_str())).map(|(k, v)| (k.clone(), v.clone()));
SettingsPatch { values: Default::default(), projects: BTreeMap::from([(path.display().to_string(), overrides.collect())]) }
}
}
}
impl SettingsView {
/// Whether the open page has environment settings, and so the sentence.
pub(super) fn page_scoped(&self) -> bool {
use super::Page;
matches!(self.page, Page::General | Page::Integrations | Page::SourceControl | Page::Storage)
}
fn environments(&self, cx: &App) -> Vec<SettingsEnvironment> {
self.workspace
.upgrade()
.map(|workspace| workspace.read(cx).settings_environments())
.unwrap_or_default()
}
/// Works the selection out afresh.
pub(super) fn compute_shown(&self, cx: &mut Context<Self>) -> Shown {
let environments = self.environments(cx);
Shown::compute(cx.weak_entity(), &self.scope, self.page_scoped(), &environments)
}
/// Applies an edit made on a page: this device's preferences here, and
/// environment settings to each selected environment (or the project's
/// overrides there). The edit runs on each environment's own settings,
/// so picking the shown value of a Mixed setting still reaches the
/// environments that differ.
pub(super) fn apply(&mut self, change: impl Fn(&mut Settings), window: &mut Window, cx: &mut Context<Self>) {
let shown = self.compute_shown(cx);
let environment_key = |key: &String| shown.scoped && ENVIRONMENT_KEYS.contains(&key.as_str());
let changes = |before: &Settings| {
let mut after = before.clone();
change(&mut after);
after.normalize();
settings::changed_values(before, &after)
};
let device: serde_json::Map<_, _> = changes(&shown.settings)
.into_iter()
.filter(|(key, _)| key != "project_overrides" && !environment_key(key))
.collect();
if !device.is_empty() {
settings::update(|settings| *settings = settings::with_values(settings, &device));
}
if shown.scoped && shown.locked.is_none() {
let patches: Vec<(Option<String>, SettingsPatch)> = shown
.targets
.iter()
.map(|target| {
let values = changes(&target.settings).into_iter().filter(|(key, _)| environment_key(key)).collect();
(target.environment.clone(), patch_for(target, &values))
})
.collect();
let _ = self.workspace.update(cx, |workspace, cx| {
for (environment, patch) in patches {
workspace.write_settings(environment.as_deref(), patch, cx);
}
});
}
self.changed(window, cx);
}
/// Lets the picked project follow its environments again for `keys`.
pub(crate) fn inherit(&mut self, keys: &[&str], window: &mut Window, cx: &mut Context<Self>) {
let shown = self.compute_shown(cx);
let _ = self.workspace.update(cx, |workspace, cx| {
for target in &shown.targets {
let Some(path) = &target.project else { continue };
let drop = keys.iter().map(|key| (key.to_string(), serde_json::Value::Null)).collect();
let patch = SettingsPatch { values: Default::default(), projects: BTreeMap::from([(path.display().to_string(), drop)]) };
workspace.write_settings(target.environment.as_deref(), patch, cx);
}
});
self.changed(window, cx);
}
fn set_scope(&mut self, scope: Scope, window: &mut Window, cx: &mut Context<Self>) {
let _ = window;
self.scope = scope;
// The next render puts the new selection's values in the fields.
self.scoped_inputs = None;
cx.notify();
}
/// "Applying settings for <project> across <environment>".
pub(super) fn render_scope_sentence(&self, cx: &mut Context<Self>) -> AnyElement {
let p = metal::palette(cx);
let environments = self.environments(cx);
let view = cx.weak_entity();
// Projects by name, across every environment.
let mut projects: Vec<String> = Vec::new();
for (name, _) in environments.iter().flat_map(|env| &env.projects) {
if !projects.contains(name) {
projects.push(name.clone());
}
}
let project_icon = |name: &str| metal::badge(workspace::initials(name), workspace::tint_for(name), p);
let project_label: SharedString = match &self.scope.project {
Some(name) if projects.contains(name) => name.clone().into(),
Some(_) => "Unavailable project".into(),
None => "All projects".into(),
};
let project_menu = {
let view = view.clone();
let scope = self.scope.clone();
let projects = projects.clone();
picker("scope-project", self.scope.project.as_deref().map(|name| project_icon(name).into_any_element()), project_label)
.dropdown_menu_with_anchor(Anchor::TopLeft, move |mut menu, _, cx| {
let p = metal::palette(cx);
let pick = |project: Option<String>| {
let view = view.clone();
let scope = Scope { project, ..scope.clone() };
move |_: &ClickEvent, window: &mut Window, cx: &mut App| {
let scope = scope.clone();
let _ = view.update(cx, |view, cx| view.set_scope(scope, window, cx));
}
};
menu = menu
.min_w(px(220.))
.max_h(px(420.))
.scrollable(true)
.item(PopupMenuItem::new("All projects").checked(scope.project.is_none()).on_click(pick(None)));
if !projects.is_empty() {
menu = menu.separator();
}
for name in &projects {
let label = name.clone();
let (initials, tint) = (workspace::initials(name), workspace::tint_for(name));
menu = menu.item(
PopupMenuItem::element(move |_, _| {
h_flex()
.gap(px(8.))
.child(metal::badge(initials.clone(), tint, p))
.child(div().text_size(px(13.)).child(label.clone()))
})
.checked(scope.project.as_deref() == Some(name.as_str()))
.on_click(pick(Some(name.clone()))),
);
}
menu
})
};
let single = environments.len() < 2;
let selected = match &self.scope.environment {
EnvironmentPick::One(id) => Some(environments.iter().find(|env| env.id == *id)),
EnvironmentPick::All => None,
};
let environment_label: SharedString = match selected {
Some(Some(env)) => env.name.clone().into(),
Some(None) => "Unavailable environment".into(),
None if single => environments.first().map(|env| env.name.clone()).unwrap_or_default().into(),
None => "All environments".into(),
};
let environment_icon = match selected {
Some(Some(env)) => Some(machine_icon(env.id.is_none())),
None if single => Some(machine_icon(true)),
_ => None,
};
let entries: Rc<Vec<(Option<String>, String, bool)>> =
Rc::new(environments.iter().map(|env| (env.id.clone(), env.name.clone(), env.connected)).collect());
let environment_menu = {
let view = view.clone();
let scope = self.scope.clone();
let entries = entries.clone();
picker("scope-environment", environment_icon.map(|icon| icon.size(px(13.)).text_color(p.ink2).into_any_element()), environment_label)
.dropdown_menu_with_anchor(Anchor::TopLeft, move |mut menu, _, cx| {
let p = metal::palette(cx);
let pick = |environment: EnvironmentPick| {
let view = view.clone();
let scope = Scope { environment, ..scope.clone() };
move |_: &ClickEvent, window: &mut Window, cx: &mut App| {
let scope = scope.clone();
let _ = view.update(cx, |view, cx| view.set_scope(scope, window, cx));
}
};
menu = menu.min_w(px(240.));
if entries.len() > 1 {
menu = menu
.item(
PopupMenuItem::new("All environments")
.icon(Icon::new(Lucide::Layers))
.checked(scope.environment == EnvironmentPick::All)
.on_click(pick(EnvironmentPick::All)),
)
.separator();
}
for (id, name, connected) in entries.iter() {
let (label, connected, local) = (name.clone(), *connected, id.is_none());
let checked = scope.environment == EnvironmentPick::One(id.clone())
|| (entries.len() == 1 && scope.environment == EnvironmentPick::All);
menu = menu.item(
PopupMenuItem::element(move |_, _| {
h_flex()
.w_full()
.gap(px(8.))
.child(machine_icon(local).size(px(13.)).text_color(p.ink2))
.child(div().flex_1().text_size(px(13.)).child(label.clone()))
.when(!connected, |this| {
this.child(div().text_size(px(11.5)).text_color(p.ink3).child("Offline"))
})
})
.checked(checked)
.on_click(pick(EnvironmentPick::One(id.clone()))),
);
}
menu
})
};
let connective = if matches!(self.scope.environment, EnvironmentPick::One(_)) || single { "on" } else { "across" };
let text = |text: &'static str| div().flex_none().text_size(px(14.)).text_color(p.ink3).child(text);
v_flex()
.gap(px(6.))
.child(
h_flex()
.id("settings-scope")
.flex_wrap()
.px(px(16.))
.gap(px(6.))
.child(h_flex().gap(px(4.)).child(text("Applying settings for")).child(project_menu))
.child(h_flex().gap(px(4.)).child(text(connective)).child(environment_menu)),
)
.into_any_element()
}
/// Puts the selection's text settings into their fields when they
/// changed: on picking another scope, or when another device changed them.
/// Reads what the last render worked out, as the workspace may be busy.
pub(super) fn sync_scoped_inputs(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let shown = shown_settings(cx);
let values = [
shown.branch_prefix.clone(),
shown.worktrees_dir.clone(),
shown.branch_instructions.clone(),
shown.writing_instructions.clone(),
];
if self.scoped_inputs.as_ref() == Some(&values) {
return;
}
let [prefix, worktrees, branch_instructions, writing_instructions] = values.clone();
self.branch_prefix.update(cx, |input, cx| input.set_value(prefix, window, cx));
self.worktrees_dir.update(cx, |input, cx| input.set_value(worktrees, window, cx));
self.branch_instructions.update(cx, |input, cx| input.set_value(branch_instructions, window, cx));
self.writing_instructions.update(cx, |input, cx| input.set_value(writing_instructions, window, cx));
self.scoped_inputs = Some(values);
}
}
/// The laptop for this device, a server for the others.
fn machine_icon(local: bool) -> Icon {
Icon::new(if local { Lucide::Laptop } else { Lucide::Server })
}
/// One of the sentence's pickers: a quiet button with a caret.
fn picker(id: &'static str, icon: Option<AnyElement>, label: SharedString) -> MetalButton {
let button = MetalButton::new(id).ghost().compact().label(label).caret().max_w(px(288.));
match icon {
Some(icon) => button.leading(icon),
None => button,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ui::settings::search::ITEMS;
#[::core::prelude::v1::test]
fn scoped_rows_cover_the_environment_settings() {
for (id, _, keys) in SCOPED_ROWS {
assert!(ITEMS.iter().any(|item| item.id == *id), "{id} isn't a settings row");
for key in *keys {
assert!(ENVIRONMENT_KEYS.contains(key), "{key} isn't an environment setting");
}
}
for key in ENVIRONMENT_KEYS {
assert!(SCOPED_ROWS.iter().any(|(_, _, keys)| keys.contains(key)), "no row shows {key}");
}
}
fn environment(id: Option<&str>, settings: Settings, projects: &[(&str, &str)]) -> SettingsEnvironment {
SettingsEnvironment {
id: id.map(str::to_string),
name: id.unwrap_or("here").to_string(),
connected: true,
settings: Some(settings),
projects: projects.iter().map(|(name, path)| (name.to_string(), PathBuf::from(path))).collect(),
}
}
#[::core::prelude::v1::test]
fn environments_disagreeing_show_as_mixed() {
let mut other = Settings::default();
other.workspace = settings::WorkspaceMode::Worktree;
let environments = [environment(None, Settings::default(), &[]), environment(Some("box"), other, &[])];
let shown = Shown::compute(WeakEntity::new_invalid(), &Scope::default(), true, &environments);
assert_eq!(shown.targets.len(), 2);
assert_eq!(shown.mixed["workspace"], [("here".to_string(), "Local".to_string()), ("box".into(), "Worktree".into())]);
assert!(shown.row("new-threads").unwrap().mixed.is_some());
assert!(shown.row("worktree-submodules").unwrap().mixed.is_none());
// A device preference is no environment's business.
assert!(shown.row("time-format").is_none());
}
#[::core::prelude::v1::test]
fn a_project_writes_its_overrides_where_it_exists() {
let mut box_settings = Settings::default();
box_settings
.project_overrides
.insert("/srv/app".into(), serde_json::Map::from_iter([("submodules".to_string(), serde_json::json!("skip"))]));
let environments = [
environment(None, Settings::default(), &[("app", "/home/me/app")]),
environment(Some("box"), box_settings, &[("app", "/srv/app"), ("other", "/srv/other")]),
];
let scope = Scope { environment: EnvironmentPick::All, project: Some("app".into()) };
let shown = Shown::compute(WeakEntity::new_invalid(), &scope, true, &environments);
let paths: Vec<_> = shown.targets.iter().filter_map(|target| target.project.clone()).collect();
assert_eq!(paths, [PathBuf::from("/home/me/app"), PathBuf::from("/srv/app")]);
assert!(shown.overridden.contains("submodules"));
let row = shown.row("worktree-submodules").unwrap();
assert!(row.project && row.overridden && row.mixed.is_some() && row.locked.is_none());
// Environment-wide settings can't be overridden.
assert!(shown.row("storage-worktrees-location").unwrap().locked.is_some());
let values = serde_json::Map::from_iter([
("workspace".to_string(), serde_json::json!("worktree")),
("worktrees_dir".to_string(), serde_json::json!("/tmp")),
]);
let patch = patch_for(&shown.targets[1], &values);
assert!(patch.values.is_empty());
assert_eq!(patch.projects["/srv/app"].keys().collect::<Vec<_>>(), ["workspace"]);
}
#[::core::prelude::v1::test]
fn an_offline_environment_is_left_alone() {
let mut offline = environment(Some("box"), Settings::default(), &[]);
offline.connected = false;
let environments = [environment(None, Settings::default(), &[]), offline];
let all = Shown::compute(WeakEntity::new_invalid(), &Scope::default(), true, &environments);
assert_eq!(all.targets.len(), 1);
let one = Scope { environment: EnvironmentPick::One(Some("box".into())), project: None };
let shown = Shown::compute(WeakEntity::new_invalid(), &one, true, &environments);
assert!(shown.targets.is_empty() && shown.locked.is_some());
}
}