Compare commits

..

10 Commits

Author SHA1 Message Date
Julia Ryan
0ebf440247 Temporary change to test job 2025-09-03 13:11:13 -07:00
Julia Ryan
70c05262f4 Don't fail on error 2025-09-03 13:11:13 -07:00
Julia Ryan
82e55e4145 Change project name 2025-09-03 13:11:12 -07:00
Julia Ryan
fee9cf4101 Add deploy step 2025-09-03 13:11:12 -07:00
Julia Ryan
143d9fc95e Add TODO 2025-09-03 13:11:12 -07:00
Julia Ryan
3d6b46cf9a ci: Add nightly action to build gpui docs 2025-09-03 13:11:12 -07:00
localcc
bb2d833373 Revert "gpui: Fix overflow_hidden to support clip with border radius" (#37480)
This reverts commit 40199266b6.

The issue with the commit is: ContentMask<Pixels>::intersect is doing
intersection of corner radii which makes inner containers use the max
corner radius out of all the parents when it should be more complex to
correctly clip children (clip sorting..?)

Release Notes:

- N/A
2025-09-03 19:52:47 +00:00
Cole Miller
eedfc5be5a acp: Improve handling of invalid external agent server downloads (#37465)
Related to #37213, #37150

When listing previously-downloaded versions of an external agent, don't
try to use any downloads that are missing the agent entrypoint
(indicating that they're corrupt/unusable), and delete those versions,
so that we can attempt to download the latest version again.

Also report clearer errors when failing to start a session due to an
agent server entrypoint or root directory not existing.

Release Notes:

- N/A
2025-09-03 15:47:39 -04:00
Agus Zubiaga
0e76cc8036 acp: Display a new version call out when one is available (#37479)
<img width="500" alt="CleanShot 2025-09-03 at 16 13 59@2x"
src="https://github.com/user-attachments/assets/beb91365-28e2-4f87-a2c5-7136d37382c7"></img>



Release Notes:

- Agent Panel: Display a callout when a new version of an external agent
is available

---------

Co-authored-by: Cole Miller <cole@zed.dev>
2025-09-03 19:39:04 +00:00
Ben Kunkle
6bd5251882 settings_ui: Add test for default values (#37466)
Closes #ISSUE

Adds a test that checks that all settings have default values in
`default.json`. Currently only tests that settings supported by
SettingsUi have defaults, as more settings are added to the settings
editor they will be added to the test as well.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-03 15:25:30 -04:00
25 changed files with 684 additions and 777 deletions

View File

@@ -5,8 +5,8 @@ on:
# Fire every day at 7:00am UTC (Roughly before EU workday and after US workday)
- cron: "0 7 * * *"
push:
tags:
- "nightly"
branches:
- rustdoc-action
env:
CARGO_TERM_COLOR: always
@@ -18,118 +18,284 @@ env:
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
jobs:
style:
# style:
# timeout-minutes: 60
# name: Check formatting and Clippy lints
# if: github.repository_owner == 'zed-industries'
# runs-on:
# - self-hosted
# - macOS
# steps:
# - name: Checkout repo
# uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# with:
# clean: false
# fetch-depth: 0
# - name: Run style checks
# uses: ./.github/actions/check_style
# - name: Run clippy
# run: ./script/clippy
# tests:
# timeout-minutes: 60
# name: Run tests
# if: github.repository_owner == 'zed-industries'
# runs-on:
# - self-hosted
# - macOS
# needs: style
# steps:
# - name: Checkout repo
# uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# with:
# clean: false
# - name: Run tests
# uses: ./.github/actions/run_tests
# windows-tests:
# timeout-minutes: 60
# name: Run tests on Windows
# if: github.repository_owner == 'zed-industries'
# runs-on: [self-32vcpu-windows-2022]
# steps:
# - name: Checkout repo
# uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# with:
# clean: false
# - name: Configure CI
# run: |
# New-Item -ItemType Directory -Path "./../.cargo" -Force
# Copy-Item -Path "./.cargo/ci-config.toml" -Destination "./../.cargo/config.toml"
# - name: Run tests
# uses: ./.github/actions/run_tests_windows
# - name: Limit target directory size
# run: ./script/clear-target-dir-if-larger-than.ps1 1024
# - name: Clean CI config file
# if: always()
# run: Remove-Item -Recurse -Path "./../.cargo" -Force -ErrorAction SilentlyContinue
# bundle-mac:
# timeout-minutes: 60
# name: Create a macOS bundle
# if: github.repository_owner == 'zed-industries'
# runs-on:
# - self-mini-macos
# needs: tests
# env:
# MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
# MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
# APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
# APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
# APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
# steps:
# - name: Install Node
# uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
# with:
# node-version: "18"
# - name: Checkout repo
# uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# with:
# clean: false
# - name: Set release channel to nightly
# run: |
# set -eu
# version=$(git rev-parse --short HEAD)
# echo "Publishing version: ${version} on release channel nightly"
# echo "nightly" > crates/zed/RELEASE_CHANNEL
# - name: Setup Sentry CLI
# uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
# with:
# token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
# - name: Create macOS app bundle
# run: script/bundle-mac
# - name: Upload Zed Nightly
# run: script/upload-nightly macos
# bundle-linux-x86:
# timeout-minutes: 60
# name: Create a Linux *.tar.gz bundle for x86
# if: github.repository_owner == 'zed-industries'
# runs-on:
# - namespace-profile-16x32-ubuntu-2004 # ubuntu 20.04 for minimal glibc
# needs: tests
# steps:
# - name: Checkout repo
# uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# with:
# clean: false
# - name: Add Rust to the PATH
# run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# - name: Install Linux dependencies
# run: ./script/linux && ./script/install-mold 2.34.0
# - name: Setup Sentry CLI
# uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
# with:
# token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
# - name: Limit target directory size
# run: script/clear-target-dir-if-larger-than 100
# - name: Set release channel to nightly
# run: |
# set -euo pipefail
# version=$(git rev-parse --short HEAD)
# echo "Publishing version: ${version} on release channel nightly"
# echo "nightly" > crates/zed/RELEASE_CHANNEL
# - name: Create Linux .tar.gz bundle
# run: script/bundle-linux
# - name: Upload Zed Nightly
# run: script/upload-nightly linux-targz
# bundle-linux-arm:
# timeout-minutes: 60
# name: Create a Linux *.tar.gz bundle for ARM
# if: github.repository_owner == 'zed-industries'
# runs-on:
# - namespace-profile-8x32-ubuntu-2004-arm-m4 # ubuntu 20.04 for minimal glibc
# needs: tests
# steps:
# - name: Checkout repo
# uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# with:
# clean: false
# - name: Install Linux dependencies
# run: ./script/linux
# - name: Setup Sentry CLI
# uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
# with:
# token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
# - name: Limit target directory size
# run: script/clear-target-dir-if-larger-than 100
# - name: Set release channel to nightly
# run: |
# set -euo pipefail
# version=$(git rev-parse --short HEAD)
# echo "Publishing version: ${version} on release channel nightly"
# echo "nightly" > crates/zed/RELEASE_CHANNEL
# - name: Create Linux .tar.gz bundle
# run: script/bundle-linux
# - name: Upload Zed Nightly
# run: script/upload-nightly linux-targz
# freebsd:
# timeout-minutes: 60
# if: false && github.repository_owner == 'zed-industries'
# runs-on: github-8vcpu-ubuntu-2404
# needs: tests
# name: Build Zed on FreeBSD
# steps:
# - uses: actions/checkout@v4
# - name: Build FreeBSD remote-server
# id: freebsd-build
# uses: vmactions/freebsd-vm@c3ae29a132c8ef1924775414107a97cac042aad5 # v1.2.0
# with:
# # envs: "MYTOKEN MYTOKEN2"
# usesh: true
# release: 13.5
# copyback: true
# prepare: |
# pkg install -y \
# bash curl jq git \
# rustup-init cmake-core llvm-devel-lite pkgconf protobuf # ibx11 alsa-lib rust-bindgen-cli
# run: |
# freebsd-version
# sysctl hw.model
# sysctl hw.ncpu
# sysctl hw.physmem
# sysctl hw.usermem
# git config --global --add safe.directory /home/runner/work/zed/zed
# rustup-init --profile minimal --default-toolchain none -y
# . "$HOME/.cargo/env"
# ./script/bundle-freebsd
# mkdir -p out/
# mv "target/zed-remote-server-freebsd-x86_64.gz" out/
# rm -rf target/
# cargo clean
# - name: Upload Zed Nightly
# run: script/upload-nightly freebsd
# bundle-nix:
# name: Build and cache Nix package
# needs: tests
# secrets: inherit
# uses: ./.github/workflows/nix.yml
# bundle-windows-x64:
# timeout-minutes: 60
# name: Create a Windows installer
# if: github.repository_owner == 'zed-industries'
# runs-on: [self-32vcpu-windows-2022]
# needs: windows-tests
# env:
# AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
# AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }}
# AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }}
# ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }}
# CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }}
# ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }}
# FILE_DIGEST: SHA256
# TIMESTAMP_DIGEST: SHA256
# TIMESTAMP_SERVER: "http://timestamp.acs.microsoft.com"
# steps:
# - name: Checkout repo
# uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# with:
# clean: false
# - name: Set release channel to nightly
# working-directory: ${{ env.ZED_WORKSPACE }}
# run: |
# $ErrorActionPreference = "Stop"
# $version = git rev-parse --short HEAD
# Write-Host "Publishing version: $version on release channel nightly"
# "nightly" | Set-Content -Path "crates/zed/RELEASE_CHANNEL"
# - name: Setup Sentry CLI
# uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
# with:
# token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
# - name: Build Zed installer
# working-directory: ${{ env.ZED_WORKSPACE }}
# run: script/bundle-windows.ps1
# - name: Upload Zed Nightly
# working-directory: ${{ env.ZED_WORKSPACE }}
# run: script/upload-nightly.ps1 windows
gpui-docs:
timeout-minutes: 60
name: Check formatting and Clippy lints
name: Render rust docs for gpui
if: github.repository_owner == 'zed-industries'
# TODO: build for multiple targets?
runs-on:
- self-hosted
- macOS
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
fetch-depth: 0
- name: Run style checks
uses: ./.github/actions/check_style
- name: Run clippy
run: ./script/clippy
tests:
timeout-minutes: 60
name: Run tests
if: github.repository_owner == 'zed-industries'
runs-on:
- self-hosted
- macOS
needs: style
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Run tests
uses: ./.github/actions/run_tests
windows-tests:
timeout-minutes: 60
name: Run tests on Windows
if: github.repository_owner == 'zed-industries'
runs-on: [self-32vcpu-windows-2022]
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Configure CI
run: |
New-Item -ItemType Directory -Path "./../.cargo" -Force
Copy-Item -Path "./.cargo/ci-config.toml" -Destination "./../.cargo/config.toml"
- name: Run tests
uses: ./.github/actions/run_tests_windows
- name: Limit target directory size
run: ./script/clear-target-dir-if-larger-than.ps1 1024
- name: Clean CI config file
if: always()
run: Remove-Item -Recurse -Path "./../.cargo" -Force -ErrorAction SilentlyContinue
bundle-mac:
timeout-minutes: 60
name: Create a macOS bundle
if: github.repository_owner == 'zed-industries'
runs-on:
- self-mini-macos
needs: tests
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
steps:
- name: Install Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "18"
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Set release channel to nightly
run: |
set -eu
version=$(git rev-parse --short HEAD)
echo "Publishing version: ${version} on release channel nightly"
echo "nightly" > crates/zed/RELEASE_CHANNEL
- name: Setup Sentry CLI
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
with:
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
- name: Create macOS app bundle
run: script/bundle-mac
- name: Upload Zed Nightly
run: script/upload-nightly macos
bundle-linux-x86:
timeout-minutes: 60
name: Create a Linux *.tar.gz bundle for x86
if: github.repository_owner == 'zed-industries'
runs-on:
- namespace-profile-16x32-ubuntu-2004 # ubuntu 20.04 for minimal glibc
needs: tests
- github-16vcpu-ubuntu-2404
# needs: tests
continue-on-error: true
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -137,187 +303,61 @@ jobs:
clean: false
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Install Linux dependencies
run: ./script/linux && ./script/install-mold 2.34.0
- name: Setup Sentry CLI
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
with:
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 100
- name: Set release channel to nightly
run: |
set -euo pipefail
version=$(git rev-parse --short HEAD)
echo "Publishing version: ${version} on release channel nightly"
echo "nightly" > crates/zed/RELEASE_CHANNEL
- name: Build rustdocs
run: cargo doc -p gpui --no-deps
- name: Create Linux .tar.gz bundle
run: script/bundle-linux
- name: Upload Zed Nightly
run: script/upload-nightly linux-targz
bundle-linux-arm:
timeout-minutes: 60
name: Create a Linux *.tar.gz bundle for ARM
if: github.repository_owner == 'zed-industries'
runs-on:
- namespace-profile-8x32-ubuntu-2004-arm-m4 # ubuntu 20.04 for minimal glibc
needs: tests
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Check for broken links (in HTML)
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
with:
clean: false
args: --no-progress --exclude '^http' 'target/doc/'
fail: true
- name: Install Linux dependencies
run: ./script/linux
- name: Setup Sentry CLI
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
- name: Deploy rustdoc
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3
with:
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy target/doc --project-name=gpui-rustdoc
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 100
# update-nightly-tag:
# name: Update nightly tag
# if: github.repository_owner == 'zed-industries'
# runs-on: namespace-profile-2x4-ubuntu-2404
# needs:
# - bundle-mac
# - bundle-linux-x86
# - bundle-linux-arm
# - bundle-windows-x64
# steps:
# - name: Checkout repo
# uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# with:
# fetch-depth: 0
- name: Set release channel to nightly
run: |
set -euo pipefail
version=$(git rev-parse --short HEAD)
echo "Publishing version: ${version} on release channel nightly"
echo "nightly" > crates/zed/RELEASE_CHANNEL
# - name: Update nightly tag
# run: |
# if [ "$(git rev-parse nightly)" = "$(git rev-parse HEAD)" ]; then
# echo "Nightly tag already points to current commit. Skipping tagging."
# exit 0
# fi
# git config user.name github-actions
# git config user.email github-actions@github.com
# git tag -f nightly
# git push origin nightly --force
- name: Create Linux .tar.gz bundle
run: script/bundle-linux
- name: Upload Zed Nightly
run: script/upload-nightly linux-targz
freebsd:
timeout-minutes: 60
if: false && github.repository_owner == 'zed-industries'
runs-on: github-8vcpu-ubuntu-2404
needs: tests
name: Build Zed on FreeBSD
steps:
- uses: actions/checkout@v4
- name: Build FreeBSD remote-server
id: freebsd-build
uses: vmactions/freebsd-vm@c3ae29a132c8ef1924775414107a97cac042aad5 # v1.2.0
with:
# envs: "MYTOKEN MYTOKEN2"
usesh: true
release: 13.5
copyback: true
prepare: |
pkg install -y \
bash curl jq git \
rustup-init cmake-core llvm-devel-lite pkgconf protobuf # ibx11 alsa-lib rust-bindgen-cli
run: |
freebsd-version
sysctl hw.model
sysctl hw.ncpu
sysctl hw.physmem
sysctl hw.usermem
git config --global --add safe.directory /home/runner/work/zed/zed
rustup-init --profile minimal --default-toolchain none -y
. "$HOME/.cargo/env"
./script/bundle-freebsd
mkdir -p out/
mv "target/zed-remote-server-freebsd-x86_64.gz" out/
rm -rf target/
cargo clean
- name: Upload Zed Nightly
run: script/upload-nightly freebsd
bundle-nix:
name: Build and cache Nix package
needs: tests
secrets: inherit
uses: ./.github/workflows/nix.yml
bundle-windows-x64:
timeout-minutes: 60
name: Create a Windows installer
if: github.repository_owner == 'zed-industries'
runs-on: [self-32vcpu-windows-2022]
needs: windows-tests
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }}
ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }}
CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }}
ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }}
FILE_DIGEST: SHA256
TIMESTAMP_DIGEST: SHA256
TIMESTAMP_SERVER: "http://timestamp.acs.microsoft.com"
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Set release channel to nightly
working-directory: ${{ env.ZED_WORKSPACE }}
run: |
$ErrorActionPreference = "Stop"
$version = git rev-parse --short HEAD
Write-Host "Publishing version: $version on release channel nightly"
"nightly" | Set-Content -Path "crates/zed/RELEASE_CHANNEL"
- name: Setup Sentry CLI
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
with:
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
- name: Build Zed installer
working-directory: ${{ env.ZED_WORKSPACE }}
run: script/bundle-windows.ps1
- name: Upload Zed Nightly
working-directory: ${{ env.ZED_WORKSPACE }}
run: script/upload-nightly.ps1 windows
update-nightly-tag:
name: Update nightly tag
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404
needs:
- bundle-mac
- bundle-linux-x86
- bundle-linux-arm
- bundle-windows-x64
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
- name: Update nightly tag
run: |
if [ "$(git rev-parse nightly)" = "$(git rev-parse HEAD)" ]; then
echo "Nightly tag already points to current commit. Skipping tagging."
exit 0
fi
git config user.name github-actions
git config user.email github-actions@github.com
git tag -f nightly
git push origin nightly --force
- name: Create Sentry release
uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c # v3
env:
SENTRY_ORG: zed-dev
SENTRY_PROJECT: zed
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
with:
environment: production
# - name: Create Sentry release
# uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c # v3
# env:
# SENTRY_ORG: zed-dev
# SENTRY_PROJECT: zed
# SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
# with:
# environment: production

1
Cargo.lock generated
View File

@@ -14906,6 +14906,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"command_palette_hooks",
"debugger_ui",
"editor",
"feature_flags",
"gpui",

View File

@@ -45,11 +45,20 @@ pub fn init(cx: &mut App) {
pub struct AgentServerDelegate {
project: Entity<Project>,
status_tx: Option<watch::Sender<SharedString>>,
new_version_available: Option<watch::Sender<Option<String>>>,
}
impl AgentServerDelegate {
pub fn new(project: Entity<Project>, status_tx: Option<watch::Sender<SharedString>>) -> Self {
Self { project, status_tx }
pub fn new(
project: Entity<Project>,
status_tx: Option<watch::Sender<SharedString>>,
new_version_tx: Option<watch::Sender<Option<String>>>,
) -> Self {
Self {
project,
status_tx,
new_version_available: new_version_tx,
}
}
pub fn project(&self) -> &Entity<Project> {
@@ -73,6 +82,7 @@ impl AgentServerDelegate {
)));
};
let status_tx = self.status_tx;
let new_version_available = self.new_version_available;
cx.spawn(async move |cx| {
if !ignore_system_version {
@@ -101,9 +111,11 @@ impl AgentServerDelegate {
continue;
};
if let Some(version) = file_name
.to_str()
.and_then(|name| semver::Version::from_str(&name).ok())
if let Some(name) = file_name.to_str()
&& let Some(version) = semver::Version::from_str(name).ok()
&& fs
.is_file(&dir.join(file_name).join(&entrypoint_path))
.await
{
versions.push((version, file_name.to_owned()));
} else {
@@ -146,6 +158,7 @@ impl AgentServerDelegate {
cx.background_spawn({
let file_name = file_name.clone();
let dir = dir.clone();
let fs = fs.clone();
async move {
let latest_version =
node_runtime.npm_package_latest_version(&package_name).await;
@@ -160,6 +173,9 @@ impl AgentServerDelegate {
)
.await
.log_err();
if let Some(mut new_version_available) = new_version_available {
new_version_available.send(Some(latest_version)).ok();
}
}
}
})
@@ -171,7 +187,7 @@ impl AgentServerDelegate {
}
let dir = dir.clone();
cx.background_spawn(Self::download_latest_version(
fs,
fs.clone(),
dir.clone(),
node_runtime,
package_name,
@@ -179,14 +195,18 @@ impl AgentServerDelegate {
.await?
.into()
};
let agent_server_path = dir.join(version).join(entrypoint_path);
let agent_server_path_exists = fs.is_file(&agent_server_path).await;
anyhow::ensure!(
agent_server_path_exists,
"Missing entrypoint path {} after installation",
agent_server_path.to_string_lossy()
);
anyhow::Ok(AgentServerCommand {
path: node_path,
args: vec![
dir.join(version)
.join(entrypoint_path)
.to_string_lossy()
.to_string(),
],
args: vec![agent_server_path.to_string_lossy().to_string()],
env: Default::default(),
})
})

View File

@@ -76,6 +76,7 @@ impl AgentServer for ClaudeCode {
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>> {
let root_dir = root_dir.to_path_buf();
let fs = delegate.project().read(cx).fs().clone();
let server_name = self.name();
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).claude.clone()
@@ -109,6 +110,13 @@ impl AgentServer for ClaudeCode {
.insert("ANTHROPIC_API_KEY".to_owned(), api_key.key);
}
let root_dir_exists = fs.is_dir(&root_dir).await;
anyhow::ensure!(
root_dir_exists,
"Session root {} does not exist or is not a directory",
root_dir.to_string_lossy()
);
crate::acp::connect(server_name, command.clone(), &root_dir, cx).await
})
}

View File

@@ -498,7 +498,7 @@ pub async fn new_test_thread(
current_dir: impl AsRef<Path>,
cx: &mut TestAppContext,
) -> Entity<AcpThread> {
let delegate = AgentServerDelegate::new(project.clone(), None);
let delegate = AgentServerDelegate::new(project.clone(), None, None);
let connection = cx
.update(|cx| server.connect(current_dir.as_ref(), delegate, cx))

View File

@@ -36,6 +36,7 @@ impl AgentServer for Gemini {
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>> {
let root_dir = root_dir.to_path_buf();
let fs = delegate.project().read(cx).fs().clone();
let server_name = self.name();
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).gemini.clone()
@@ -74,6 +75,13 @@ impl AgentServer for Gemini {
.insert("GEMINI_API_KEY".to_owned(), api_key.key);
}
let root_dir_exists = fs.is_dir(&root_dir).await;
anyhow::ensure!(
root_dir_exists,
"Session root {} does not exist or is not a directory",
root_dir.to_string_lossy()
);
let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await;
match &result {
Ok(connection) => {
@@ -92,7 +100,7 @@ impl AgentServer for Gemini {
log::error!("connected to gemini, but missing prompt_capabilities.image (version is {current_version})");
return Err(LoadError::Unsupported {
current_version: current_version.into(),
command: command.path.to_string_lossy().to_string().into(),
command: (command.path.to_string_lossy().to_string() + " " + &command.args.join(" ")).into(),
minimum_version: Self::MINIMUM_VERSION.into(),
}
.into());
@@ -129,7 +137,7 @@ impl AgentServer for Gemini {
if !supported {
return Err(LoadError::Unsupported {
current_version: current_version.into(),
command: command.path.to_string_lossy().to_string().into(),
command: (command.path.to_string_lossy().to_string() + " " + &command.args.join(" ")).into(),
minimum_version: Self::MINIMUM_VERSION.into(),
}
.into());

View File

@@ -700,7 +700,7 @@ impl MessageEditor {
self.project.read(cx).fs().clone(),
self.history_store.clone(),
));
let delegate = AgentServerDelegate::new(self.project.clone(), None);
let delegate = AgentServerDelegate::new(self.project.clone(), None, None);
let connection = server.connect(Path::new(""), delegate, cx);
cx.spawn(async move |_, cx| {
let agent = connection.await?;

View File

@@ -46,7 +46,7 @@ use text::Anchor;
use theme::ThemeSettings;
use ui::{
Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding,
PopoverMenuHandle, Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*,
PopoverMenuHandle, Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*,
};
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, Workspace};
@@ -288,6 +288,7 @@ pub struct AcpThreadView {
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
is_loading_contents: bool,
new_server_version_available: Option<SharedString>,
_cancel_task: Option<Task<()>>,
_subscriptions: [Subscription; 3],
}
@@ -416,9 +417,23 @@ impl AcpThreadView {
_subscriptions: subscriptions,
_cancel_task: None,
focus_handle: cx.focus_handle(),
new_server_version_available: None,
}
}
fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.thread_state = Self::initial_state(
self.agent.clone(),
None,
self.workspace.clone(),
self.project.clone(),
window,
cx,
);
self.new_server_version_available.take();
cx.notify();
}
fn initial_state(
agent: Rc<dyn AgentServer>,
resume_thread: Option<DbThreadMetadata>,
@@ -451,8 +466,13 @@ impl AcpThreadView {
})
.next()
.unwrap_or_else(|| paths::home_dir().as_path().into());
let (tx, mut rx) = watch::channel("Loading…".into());
let delegate = AgentServerDelegate::new(project.clone(), Some(tx));
let (status_tx, mut status_rx) = watch::channel("Loading…".into());
let (new_version_available_tx, mut new_version_available_rx) = watch::channel(None);
let delegate = AgentServerDelegate::new(
project.clone(),
Some(status_tx),
Some(new_version_available_tx),
);
let connect_task = agent.connect(&root_dir, delegate, cx);
let load_task = cx.spawn_in(window, async move |this, cx| {
@@ -627,10 +647,23 @@ impl AcpThreadView {
.log_err();
});
cx.spawn(async move |this, cx| {
while let Ok(new_version) = new_version_available_rx.recv().await {
if let Some(new_version) = new_version {
this.update(cx, |this, cx| {
this.new_server_version_available = Some(new_version.into());
cx.notify();
})
.log_err();
}
}
})
.detach();
let loading_view = cx.new(|cx| {
let update_title_task = cx.spawn(async move |this, cx| {
loop {
let status = rx.recv().await?;
let status = status_rx.recv().await?;
this.update(cx, |this: &mut LoadingView, cx| {
this.title = status;
cx.notify();
@@ -672,15 +705,7 @@ impl AcpThreadView {
.map_or(false, |provider| provider.is_authenticated(cx))
{
this.update(cx, |this, cx| {
this.thread_state = Self::initial_state(
agent.clone(),
None,
this.workspace.clone(),
this.project.clone(),
window,
cx,
);
cx.notify();
this.reset(window, cx);
})
.ok();
}
@@ -1443,7 +1468,6 @@ impl AcpThreadView {
cx.notify();
self.auth_task =
Some(cx.spawn_in(window, {
let project = self.project.clone();
let agent = self.agent.clone();
async move |this, cx| {
let result = authenticate.await;
@@ -1472,14 +1496,7 @@ impl AcpThreadView {
}
this.handle_thread_error(err, cx);
} else {
this.thread_state = Self::initial_state(
agent,
None,
this.workspace.clone(),
project.clone(),
window,
cx,
)
this.reset(window, cx);
}
this.auth_task.take()
})
@@ -1501,7 +1518,7 @@ impl AcpThreadView {
let cwd = project.first_project_directory(cx);
let shell = project.terminal_settings(&cwd, cx).shell.clone();
let delegate = AgentServerDelegate::new(project_entity.clone(), None);
let delegate = AgentServerDelegate::new(project_entity.clone(), None, None);
let command = ClaudeCode::login_command(delegate, cx);
window.spawn(cx, async move |cx| {
@@ -4800,6 +4817,38 @@ impl AcpThreadView {
Some(div().child(content))
}
fn render_new_version_callout(&self, version: &SharedString, cx: &mut Context<Self>) -> Div {
v_flex().w_full().justify_end().child(
h_flex()
.p_2()
.pr_3()
.w_full()
.gap_1p5()
.border_t_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().element_background)
.child(
h_flex()
.flex_1()
.gap_1p5()
.child(
Icon::new(IconName::Download)
.color(Color::Accent)
.size(IconSize::Small),
)
.child(Label::new("New version available").size(LabelSize::Small)),
)
.child(
Button::new("update-button", format!("Update to v{}", version))
.label_size(LabelSize::Small)
.style(ButtonStyle::Tinted(TintColor::Accent))
.on_click(cx.listener(|this, _, window, cx| {
this.reset(window, cx);
})),
),
)
}
fn get_current_model_name(&self, cx: &App) -> SharedString {
// For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet")
// For ACP agents, use the agent name (e.g., "Claude Code", "Gemini CLI")
@@ -5210,6 +5259,12 @@ impl Render for AcpThreadView {
})
.children(self.render_thread_retry_status_callout(window, cx))
.children(self.render_thread_error(window, cx))
.when_some(
self.new_server_version_available.as_ref().filter(|_| {
!has_messages || !matches!(self.thread_state, ThreadState::Ready { .. })
}),
|this, version| this.child(self.render_new_version_callout(&version, cx)),
)
.children(
if let Some(usage_callout) = self.render_usage_callout(line_height, cx) {
Some(usage_callout.into_any_element())

View File

@@ -2669,11 +2669,7 @@ impl AssistantContext {
}
pub fn summarize(&mut self, mut replace_old: bool, cx: &mut Context<Self>) {
let registry = LanguageModelRegistry::read_global(cx);
let Some(model) = registry
.thread_summary_model()
.or_else(|| registry.default_model())
else {
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
return;
};

View File

@@ -6043,7 +6043,6 @@ impl EditorElement {
window.with_content_mask(
Some(ContentMask {
bounds: layout.position_map.text_hitbox.bounds,
..Default::default()
}),
|window| {
let editor = self.editor.read(cx);
@@ -6986,15 +6985,9 @@ impl EditorElement {
} else {
let mut bounds = layout.hitbox.bounds;
bounds.origin.x += layout.gutter_hitbox.bounds.size.width;
window.with_content_mask(
Some(ContentMask {
bounds,
..Default::default()
}),
|window| {
block.element.paint(window, cx);
},
)
window.with_content_mask(Some(ContentMask { bounds }), |window| {
block.element.paint(window, cx);
})
}
}
}
@@ -8297,13 +8290,9 @@ impl Element for EditorElement {
}
let rem_size = self.rem_size(cx);
let content_mask = ContentMask {
bounds,
..Default::default()
};
window.with_rem_size(rem_size, |window| {
window.with_text_style(Some(text_style), |window| {
window.with_content_mask(Some(content_mask), |window| {
window.with_content_mask(Some(ContentMask { bounds }), |window| {
let (mut snapshot, is_read_only) = self.editor.update(cx, |editor, cx| {
(editor.snapshot(window, cx), editor.read_only(cx))
});
@@ -9411,13 +9400,9 @@ impl Element for EditorElement {
..Default::default()
};
let rem_size = self.rem_size(cx);
let content_mask = ContentMask {
bounds,
..Default::default()
};
window.with_rem_size(rem_size, |window| {
window.with_text_style(Some(text_style), |window| {
window.with_content_mask(Some(content_mask), |window| {
window.with_content_mask(Some(ContentMask { bounds }), |window| {
self.paint_mouse_listeners(layout, window, cx);
self.paint_background(layout, window, cx);
self.paint_indent_guides(layout, window, cx);

View File

@@ -1,228 +0,0 @@
use gpui::{
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
rgb, size,
};
struct Example {}
impl Render for Example {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.font_family(".SystemUIFont")
.flex()
.flex_col()
.size_full()
.p_4()
.gap_4()
.bg(rgb(0x505050))
.justify_center()
.items_center()
.text_center()
.shadow_lg()
.text_sm()
.text_color(rgb(0xffffff))
.child(
div()
.overflow_hidden()
.rounded(px(32.))
.border(px(8.))
.border_color(gpui::white())
.text_color(gpui::white())
.child(
div()
.bg(gpui::black())
.py_2()
.px_7()
.border_l_2()
.border_r_2()
.border_b_3()
.border_color(gpui::red())
.child("Let build applications with GPUI"),
)
.child(
div()
.bg(rgb(0x222222))
.text_sm()
.py_1()
.px_7()
.border_l_3()
.border_r_3()
.border_color(gpui::green())
.child("The fast, productive UI framework for Rust"),
)
.child(
div()
.bg(rgb(0x222222))
.w_full()
.flex()
.flex_row()
.text_sm()
.text_color(rgb(0xc0c0c0))
.child(
div()
.flex_1()
.p_2()
.border_3()
.border_dashed()
.border_color(gpui::blue())
.child("Rust"),
)
.child(
div()
.flex_1()
.p_2()
.border_t_3()
.border_r_3()
.border_b_3()
.border_dashed()
.border_color(gpui::blue())
.child("GPU Rendering"),
),
),
)
.child(
div()
.flex()
.flex_col()
.w(px(320.))
.gap_1()
.overflow_hidden()
.rounded(px(16.))
.child(
div()
.w_full()
.p_2()
.bg(gpui::red())
.child("Clip background"),
),
)
.child(
div()
.flex()
.flex_col()
.w(px(320.))
.gap_1()
.rounded(px(16.))
.child(
div()
.w_full()
.p_2()
.bg(gpui::yellow())
.text_color(gpui::black())
.child("No content mask"),
),
)
.child(
div()
.flex()
.flex_col()
.w(px(320.))
.gap_1()
.overflow_hidden()
.rounded(px(16.))
.child(
div()
.w_full()
.p_2()
.border_4()
.border_color(gpui::blue())
.bg(gpui::blue().alpha(0.4))
.child("Clip borders"),
),
)
.child(
div()
.flex()
.flex_col()
.w(px(320.))
.gap_1()
.overflow_hidden()
.rounded(px(20.))
.child(
div().w_full().border_2().border_color(gpui::black()).child(
div()
.size_full()
.bg(gpui::green().alpha(0.4))
.p_2()
.border_8()
.border_color(gpui::green())
.child("Clip nested elements"),
),
),
)
.child(
div()
.flex()
.flex_col()
.w(px(320.))
.gap_1()
.overflow_hidden()
.rounded(px(32.))
.child(
div()
.w_full()
.p_2()
.bg(gpui::black())
.border_2()
.border_dashed()
.rounded_lg()
.border_color(gpui::white())
.child("dash border full and rounded"),
)
.child(
div()
.w_full()
.flex()
.flex_row()
.gap_2()
.child(
div()
.w_full()
.p_2()
.bg(gpui::black())
.border_x_2()
.border_dashed()
.rounded_lg()
.border_color(gpui::white())
.child("border x"),
)
.child(
div()
.w_full()
.p_2()
.bg(gpui::black())
.border_y_2()
.border_dashed()
.rounded_lg()
.border_color(gpui::white())
.child("border y"),
),
)
.child(
div()
.w_full()
.p_2()
.bg(gpui::black())
.border_2()
.border_dashed()
.border_color(gpui::white())
.child("border full and no rounded"),
),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(800.), px(600.)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| Example {}),
)
.unwrap();
cx.activate(true);
});
}

View File

@@ -8,10 +8,10 @@
//! If all of your elements are the same height, see [`crate::UniformList`] for a simpler API
use crate::{
AnyElement, App, AvailableSpace, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element,
EntityId, FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId,
IntoElement, Overflow, Pixels, Point, ScrollDelta, ScrollWheelEvent, Size, Style,
StyleRefinement, Styled, Window, point, px, size,
AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId,
FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, IntoElement,
Overflow, Pixels, Point, ScrollDelta, ScrollWheelEvent, Size, Style, StyleRefinement, Styled,
Window, point, px, size,
};
use collections::VecDeque;
use refineable::Refineable as _;
@@ -705,7 +705,6 @@ impl StateInner {
&mut self,
bounds: Bounds<Pixels>,
padding: Edges<Pixels>,
corner_radii: Corners<Pixels>,
autoscroll: bool,
render_item: &mut RenderItemFn,
window: &mut Window,
@@ -729,15 +728,9 @@ impl StateInner {
let mut item_origin = bounds.origin + Point::new(px(0.), padding.top);
item_origin.y -= layout_response.scroll_top.offset_in_item;
for item in &mut layout_response.item_layouts {
window.with_content_mask(
Some(ContentMask {
bounds,
corner_radii,
}),
|window| {
item.element.prepaint_at(item_origin, window, cx);
},
);
window.with_content_mask(Some(ContentMask { bounds }), |window| {
item.element.prepaint_at(item_origin, window, cx);
});
if let Some(autoscroll_bounds) = window.take_autoscroll()
&& autoscroll
@@ -959,34 +952,19 @@ impl Element for List {
state.items = new_items;
}
let rem_size = window.rem_size();
let padding = style.padding.to_pixels(bounds.size.into(), rem_size);
let corner_radii = style.corner_radii.to_pixels(rem_size);
let layout = match state.prepaint_items(
bounds,
padding,
corner_radii,
true,
&mut self.render_item,
window,
cx,
) {
Ok(layout) => layout,
Err(autoscroll_request) => {
state.logical_scroll_top = Some(autoscroll_request);
state
.prepaint_items(
bounds,
padding,
corner_radii,
false,
&mut self.render_item,
window,
cx,
)
.unwrap()
}
};
let padding = style
.padding
.to_pixels(bounds.size.into(), window.rem_size());
let layout =
match state.prepaint_items(bounds, padding, true, &mut self.render_item, window, cx) {
Ok(layout) => layout,
Err(autoscroll_request) => {
state.logical_scroll_top = Some(autoscroll_request);
state
.prepaint_items(bounds, padding, false, &mut self.render_item, window, cx)
.unwrap()
}
};
state.last_layout_bounds = Some(bounds);
state.last_padding = Some(padding);
@@ -1004,17 +982,11 @@ impl Element for List {
cx: &mut App,
) {
let current_view = window.current_view();
window.with_content_mask(
Some(ContentMask {
bounds,
..Default::default()
}),
|window| {
for item in &mut prepaint.layout.item_layouts {
item.element.paint(window, cx);
}
},
);
window.with_content_mask(Some(ContentMask { bounds }), |window| {
for item in &mut prepaint.layout.item_layouts {
item.element.paint(window, cx);
}
});
let list_state = self.state.clone();
let height = bounds.size.height;

View File

@@ -411,10 +411,7 @@ impl Element for UniformList {
(self.render_items)(visible_range.clone(), window, cx)
};
let content_mask = ContentMask {
bounds,
..Default::default()
};
let content_mask = ContentMask { bounds };
window.with_content_mask(Some(content_mask), |window| {
for (mut item, ix) in items.into_iter().zip(visible_range.clone()) {
let item_origin = padded_bounds.origin

View File

@@ -53,11 +53,6 @@ struct Corners {
bottom_left: f32,
}
struct ContentMask {
bounds: Bounds,
corner_radii: Corners,
}
struct Edges {
top: f32,
right: f32,
@@ -445,7 +440,7 @@ struct Quad {
order: u32,
border_style: u32,
bounds: Bounds,
content_mask: ContentMask,
content_mask: Bounds,
background: Background,
border_color: Hsla,
corner_radii: Corners,
@@ -483,7 +478,7 @@ fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta
out.background_color1 = gradient.color1;
out.border_color = hsla_to_rgba(quad.border_color);
out.quad_id = instance_id;
out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask.bounds);
out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask);
return out;
}
@@ -496,19 +491,8 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
let quad = b_quads[input.quad_id];
// Signed distance field threshold for inclusion of pixels. 0.5 is the
// minimum distance between the center of the pixel and the edge.
let antialias_threshold = 0.5;
var background_color = gradient_color(quad.background, input.position.xy, quad.bounds,
let background_color = gradient_color(quad.background, input.position.xy, quad.bounds,
input.background_solid, input.background_color0, input.background_color1);
var border_color = input.border_color;
// Apply content_mask corner radii clipping
let clip_sdf = quad_sdf(input.position.xy, quad.content_mask.bounds, quad.content_mask.corner_radii);
let clip_alpha = saturate(antialias_threshold - clip_sdf);
background_color.a *= clip_alpha;
border_color.a *= clip_alpha;
let unrounded = quad.corner_radii.top_left == 0.0 &&
quad.corner_radii.bottom_left == 0.0 &&
@@ -529,6 +513,10 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
let point = input.position.xy - quad.bounds.origin;
let center_to_point = point - half_size;
// Signed distance field threshold for inclusion of pixels. 0.5 is the
// minimum distance between the center of the pixel and the edge.
let antialias_threshold = 0.5;
// Radius of the nearest corner
let corner_radius = pick_corner_radius(center_to_point, quad.corner_radii);
@@ -619,6 +607,8 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
var color = background_color;
if (border_sdf < antialias_threshold) {
var border_color = input.border_color;
// Dashed border logic when border_style == 1
if (quad.border_style == 1) {
// Position along the perimeter in "dash space", where each dash
@@ -654,11 +644,7 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
let is_horizontal =
corner_center_to_point.x <
corner_center_to_point.y;
var border_width = select(border.y, border.x, is_horizontal);
// When border width of some side is 0, we need to use the other side width for dash velocity.
if (border_width == 0.0) {
border_width = select(border.x, border.y, is_horizontal);
}
let border_width = select(border.y, border.x, is_horizontal);
dash_velocity = dv_numerator / border_width;
t = select(point.y, point.x, is_horizontal) * dash_velocity;
max_t = select(size.y, size.x, is_horizontal) * dash_velocity;
@@ -870,7 +856,7 @@ struct Shadow {
blur_radius: f32,
bounds: Bounds,
corner_radii: Corners,
content_mask: ContentMask,
content_mask: Bounds,
color: Hsla,
}
var<storage, read> b_shadows: array<Shadow>;
@@ -898,7 +884,7 @@ fn vs_shadow(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) ins
out.position = to_device_position(unit_vertex, shadow.bounds);
out.color = hsla_to_rgba(shadow.color);
out.shadow_id = instance_id;
out.clip_distances = distance_from_clip_rect(unit_vertex, shadow.bounds, shadow.content_mask.bounds);
out.clip_distances = distance_from_clip_rect(unit_vertex, shadow.bounds, shadow.content_mask);
return out;
}
@@ -913,6 +899,7 @@ fn fs_shadow(input: ShadowVarying) -> @location(0) vec4<f32> {
let half_size = shadow.bounds.size / 2.0;
let center = shadow.bounds.origin + half_size;
let center_to_point = input.position.xy - center;
let corner_radius = pick_corner_radius(center_to_point, shadow.corner_radii);
// The signal is only non-zero in a limited range, so don't waste samples
@@ -1040,7 +1027,7 @@ struct Underline {
order: u32,
pad: u32,
bounds: Bounds,
content_mask: ContentMask,
content_mask: Bounds,
color: Hsla,
thickness: f32,
wavy: u32,
@@ -1064,7 +1051,7 @@ fn vs_underline(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index)
out.position = to_device_position(unit_vertex, underline.bounds);
out.color = hsla_to_rgba(underline.color);
out.underline_id = instance_id;
out.clip_distances = distance_from_clip_rect(unit_vertex, underline.bounds, underline.content_mask.bounds);
out.clip_distances = distance_from_clip_rect(unit_vertex, underline.bounds, underline.content_mask);
return out;
}
@@ -1106,7 +1093,7 @@ struct MonochromeSprite {
order: u32,
pad: u32,
bounds: Bounds,
content_mask: ContentMask,
content_mask: Bounds,
color: Hsla,
tile: AtlasTile,
transformation: TransformationMatrix,
@@ -1130,7 +1117,7 @@ fn vs_mono_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index
out.tile_position = to_tile_position(unit_vertex, sprite.tile);
out.color = hsla_to_rgba(sprite.color);
out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask.bounds);
out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask);
return out;
}
@@ -1152,7 +1139,7 @@ struct PolychromeSprite {
grayscale: u32,
opacity: f32,
bounds: Bounds,
content_mask: ContentMask,
content_mask: Bounds,
corner_radii: Corners,
tile: AtlasTile,
}
@@ -1174,7 +1161,7 @@ fn vs_poly_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index
out.position = to_device_position(unit_vertex, sprite.bounds);
out.tile_position = to_tile_position(unit_vertex, sprite.tile);
out.sprite_id = instance_id;
out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask.bounds);
out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask);
return out;
}
@@ -1247,12 +1234,3 @@ fn fs_surface(input: SurfaceVarying) -> @location(0) vec4<f32> {
return ycbcr_to_RGB * y_cb_cr;
}
fn max_corner_radii(a: Corners, b: Corners) -> Corners {
return Corners(
max(a.top_left, b.top_left),
max(a.top_right, b.top_right),
max(a.bottom_right, b.bottom_right),
max(a.bottom_left, b.bottom_left)
);
}

View File

@@ -99,21 +99,8 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
constant Quad *quads
[[buffer(QuadInputIndex_Quads)]]) {
Quad quad = quads[input.quad_id];
// Signed distance field threshold for inclusion of pixels. 0.5 is the
// minimum distance between the center of the pixel and the edge.
const float antialias_threshold = 0.5;
float4 background_color = fill_color(quad.background, input.position.xy, quad.bounds,
input.background_solid, input.background_color0, input.background_color1);
float4 border_color = input.border_color;
// Apply content_mask corner radii clipping
float clip_sdf = quad_sdf(input.position.xy, quad.content_mask.bounds,
quad.content_mask.corner_radii);
float clip_alpha = saturate(antialias_threshold - clip_sdf);
background_color.a *= clip_alpha;
border_color *= clip_alpha;
bool unrounded = quad.corner_radii.top_left == 0.0 &&
quad.corner_radii.bottom_left == 0.0 &&
@@ -134,6 +121,10 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
float2 point = input.position.xy - float2(quad.bounds.origin.x, quad.bounds.origin.y);
float2 center_to_point = point - half_size;
// Signed distance field threshold for inclusion of pixels. 0.5 is the
// minimum distance between the center of the pixel and the edge.
const float antialias_threshold = 0.5;
// Radius of the nearest corner
float corner_radius = pick_corner_radius(center_to_point, quad.corner_radii);
@@ -173,6 +164,7 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
straight_border_inner_corner_to_point.x > 0.0 ||
straight_border_inner_corner_to_point.y > 0.0;
// Whether the point is far enough inside the quad, such that the pixels are
// not affected by the straight border.
bool is_within_inner_straight_border =
@@ -216,6 +208,8 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
float4 color = background_color;
if (border_sdf < antialias_threshold) {
float4 border_color = input.border_color;
// Dashed border logic when border_style == 1
if (quad.border_style == 1) {
// Position along the perimeter in "dash space", where each dash
@@ -250,10 +244,6 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
// perimeter. This way each line starts and ends with a dash.
bool is_horizontal = corner_center_to_point.x < corner_center_to_point.y;
float border_width = is_horizontal ? border.x : border.y;
// When border width of some side is 0, we need to use the other side width for dash velocity.
if (border_width == 0.0) {
border_width = is_horizontal ? border.y : border.x;
}
dash_velocity = dv_numerator / border_width;
t = is_horizontal ? point.x : point.y;
t *= dash_velocity;

View File

@@ -453,16 +453,11 @@ float quarter_ellipse_sdf(float2 pt, float2 radii) {
**
*/
struct ContentMask {
Bounds bounds;
Corners corner_radii;
};
struct Quad {
uint order;
uint border_style;
Bounds bounds;
ContentMask content_mask;
Bounds content_mask;
Background background;
Hsla border_color;
Corners corner_radii;
@@ -501,7 +496,7 @@ QuadVertexOutput quad_vertex(uint vertex_id: SV_VertexID, uint quad_id: SV_Insta
quad.background.solid,
quad.background.colors
);
float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask.bounds);
float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask);
float4 border_color = hsla_to_rgba(quad.border_color);
QuadVertexOutput output;
@@ -517,21 +512,8 @@ QuadVertexOutput quad_vertex(uint vertex_id: SV_VertexID, uint quad_id: SV_Insta
float4 quad_fragment(QuadFragmentInput input): SV_Target {
Quad quad = quads[input.quad_id];
// Signed distance field threshold for inclusion of pixels. 0.5 is the
// minimum distance between the center of the pixel and the edge.
const float antialias_threshold = 0.5;
float4 background_color = gradient_color(quad.background, input.position.xy, quad.bounds,
input.background_solid, input.background_color0, input.background_color1);
float4 border_color = input.border_color;
// Apply content_mask corner radii clipping
float clip_sdf = quad_sdf(input.position.xy, quad.content_mask.bounds,
quad.content_mask.corner_radii);
float clip_alpha = saturate(antialias_threshold - clip_sdf);
background_color.a *= clip_alpha;
border_color *= clip_alpha;
input.background_solid, input.background_color0, input.background_color1);
bool unrounded = quad.corner_radii.top_left == 0.0 &&
quad.corner_radii.top_right == 0.0 &&
@@ -552,6 +534,10 @@ float4 quad_fragment(QuadFragmentInput input): SV_Target {
float2 the_point = input.position.xy - quad.bounds.origin;
float2 center_to_point = the_point - half_size;
// Signed distance field threshold for inclusion of pixels. 0.5 is the
// minimum distance between the center of the pixel and the edge.
const float antialias_threshold = 0.5;
// Radius of the nearest corner
float corner_radius = pick_corner_radius(center_to_point, quad.corner_radii);
@@ -634,6 +620,7 @@ float4 quad_fragment(QuadFragmentInput input): SV_Target {
float4 color = background_color;
if (border_sdf < antialias_threshold) {
float4 border_color = input.border_color;
// Dashed border logic when border_style == 1
if (quad.border_style == 1) {
// Position along the perimeter in "dash space", where each dash
@@ -668,10 +655,6 @@ float4 quad_fragment(QuadFragmentInput input): SV_Target {
// perimeter. This way each line starts and ends with a dash.
bool is_horizontal = corner_center_to_point.x < corner_center_to_point.y;
float border_width = is_horizontal ? border.x : border.y;
// When border width of some side is 0, we need to use the other side width for dash velocity.
if (border_width == 0.0) {
border_width = is_horizontal ? border.y : border.x;
}
dash_velocity = dv_numerator / border_width;
t = is_horizontal ? the_point.x : the_point.y;
t *= dash_velocity;
@@ -822,7 +805,7 @@ struct Shadow {
float blur_radius;
Bounds bounds;
Corners corner_radii;
ContentMask content_mask;
Bounds content_mask;
Hsla color;
};
@@ -851,7 +834,7 @@ ShadowVertexOutput shadow_vertex(uint vertex_id: SV_VertexID, uint shadow_id: SV
bounds.size += 2.0 * margin;
float4 device_position = to_device_position(unit_vertex, bounds);
float4 clip_distance = distance_from_clip_rect(unit_vertex, bounds, shadow.content_mask.bounds);
float4 clip_distance = distance_from_clip_rect(unit_vertex, bounds, shadow.content_mask);
float4 color = hsla_to_rgba(shadow.color);
ShadowVertexOutput output;
@@ -1004,7 +987,7 @@ struct Underline {
uint order;
uint pad;
Bounds bounds;
ContentMask content_mask;
Bounds content_mask;
Hsla color;
float thickness;
uint wavy;
@@ -1030,7 +1013,7 @@ UnderlineVertexOutput underline_vertex(uint vertex_id: SV_VertexID, uint underli
Underline underline = underlines[underline_id];
float4 device_position = to_device_position(unit_vertex, underline.bounds);
float4 clip_distance = distance_from_clip_rect(unit_vertex, underline.bounds,
underline.content_mask.bounds);
underline.content_mask);
float4 color = hsla_to_rgba(underline.color);
UnderlineVertexOutput output;
@@ -1078,7 +1061,7 @@ struct MonochromeSprite {
uint order;
uint pad;
Bounds bounds;
ContentMask content_mask;
Bounds content_mask;
Hsla color;
AtlasTile tile;
TransformationMatrix transformation;
@@ -1105,7 +1088,7 @@ MonochromeSpriteVertexOutput monochrome_sprite_vertex(uint vertex_id: SV_VertexI
MonochromeSprite sprite = mono_sprites[sprite_id];
float4 device_position =
to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation);
float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask.bounds);
float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask);
float2 tile_position = to_tile_position(unit_vertex, sprite.tile);
float4 color = hsla_to_rgba(sprite.color);
@@ -1135,7 +1118,7 @@ struct PolychromeSprite {
uint grayscale;
float opacity;
Bounds bounds;
ContentMask content_mask;
Bounds content_mask;
Corners corner_radii;
AtlasTile tile;
};
@@ -1160,7 +1143,7 @@ PolychromeSpriteVertexOutput polychrome_sprite_vertex(uint vertex_id: SV_VertexI
PolychromeSprite sprite = poly_sprites[sprite_id];
float4 device_position = to_device_position(unit_vertex, sprite.bounds);
float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds,
sprite.content_mask.bounds);
sprite.content_mask);
float2 tile_position = to_tile_position(unit_vertex, sprite.tile);
PolychromeSpriteVertexOutput output;

View File

@@ -601,19 +601,7 @@ impl Style {
(false, false) => Bounds::from_corners(min, max),
};
let corner_radii = self.corner_radii.to_pixels(rem_size);
let border_widths = self.border_widths.to_pixels(rem_size);
Some(ContentMask {
bounds: Bounds {
origin: bounds.origin - point(border_widths.left, border_widths.top),
size: bounds.size
+ size(
border_widths.left + border_widths.right,
border_widths.top + border_widths.bottom,
),
},
corner_radii,
})
Some(ContentMask { bounds })
}
}
}
@@ -673,16 +661,64 @@ impl Style {
if self.is_border_visible() {
let border_widths = self.border_widths.to_pixels(rem_size);
let max_border_width = border_widths.max();
let max_corner_radius = corner_radii.max();
let top_bounds = Bounds::from_corners(
bounds.origin,
bounds.top_right() + point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
);
let bottom_bounds = Bounds::from_corners(
bounds.bottom_left() - point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
bounds.bottom_right(),
);
let left_bounds = Bounds::from_corners(
top_bounds.bottom_left(),
bottom_bounds.origin + point(max_border_width, Pixels::ZERO),
);
let right_bounds = Bounds::from_corners(
top_bounds.bottom_right() - point(max_border_width, Pixels::ZERO),
bottom_bounds.top_right(),
);
let mut background = self.border_color.unwrap_or_default();
background.a = 0.;
window.paint_quad(quad(
let quad = quad(
bounds,
corner_radii,
background,
border_widths,
self.border_color.unwrap_or_default(),
self.border_style,
));
);
window.with_content_mask(Some(ContentMask { bounds: top_bounds }), |window| {
window.paint_quad(quad.clone());
});
window.with_content_mask(
Some(ContentMask {
bounds: right_bounds,
}),
|window| {
window.paint_quad(quad.clone());
},
);
window.with_content_mask(
Some(ContentMask {
bounds: bottom_bounds,
}),
|window| {
window.paint_quad(quad.clone());
},
);
window.with_content_mask(
Some(ContentMask {
bounds: left_bounds,
}),
|window| {
window.paint_quad(quad);
},
);
}
#[cfg(debug_assertions)]

View File

@@ -1283,8 +1283,6 @@ pub(crate) struct DispatchEventResult {
pub struct ContentMask<P: Clone + Debug + Default + PartialEq> {
/// The bounds
pub bounds: Bounds<P>,
/// The corner radii of the content mask.
pub corner_radii: Corners<P>,
}
impl ContentMask<Pixels> {
@@ -1292,31 +1290,13 @@ impl ContentMask<Pixels> {
pub fn scale(&self, factor: f32) -> ContentMask<ScaledPixels> {
ContentMask {
bounds: self.bounds.scale(factor),
corner_radii: self.corner_radii.scale(factor),
}
}
/// Intersect the content mask with the given content mask.
pub fn intersect(&self, other: &Self) -> Self {
let bounds = self.bounds.intersect(&other.bounds);
ContentMask {
bounds,
corner_radii: Corners {
top_left: self.corner_radii.top_left.max(other.corner_radii.top_left),
top_right: self
.corner_radii
.top_right
.max(other.corner_radii.top_right),
bottom_right: self
.corner_radii
.bottom_right
.max(other.corner_radii.bottom_right),
bottom_left: self
.corner_radii
.bottom_left
.max(other.corner_radii.bottom_left),
},
}
ContentMask { bounds }
}
}
@@ -2577,7 +2557,6 @@ impl Window {
origin: Point::default(),
size: self.viewport_size,
},
..Default::default()
})
}

View File

@@ -27,6 +27,7 @@ pub struct SettingsUiEntry {
/// The path in the settings JSON file for this setting. Relative to parent
/// None implies `#[serde(flatten)]` or `Settings::KEY.is_none()` for top level settings
pub path: Option<&'static str>,
/// What is displayed for the text for this entry
pub title: &'static str,
pub item: SettingsUiItem,
}
@@ -95,7 +96,7 @@ impl<T: serde::Serialize> SettingsValue<T> {
pub struct SettingsUiItemDynamic {
pub options: Vec<SettingsUiEntry>,
pub determine_option: fn(&serde_json::Value, &mut App) -> usize,
pub determine_option: fn(&serde_json::Value, &App) -> usize,
}
pub struct SettingsUiItemGroup {

View File

@@ -13,6 +13,7 @@ path = "src/settings_ui.rs"
[features]
default = []
test-support = []
[dependencies]
anyhow.workspace = true
@@ -29,6 +30,10 @@ ui.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
debugger_ui.workspace = true
# Uncomment other workspace dependencies as needed
# assistant.workspace = true
# client.workspace = true

View File

@@ -151,7 +151,9 @@ struct UiEntry {
next_sibling: Option<usize>,
// expanded: bool,
render: Option<SettingsUiItemSingle>,
select_descendant: Option<fn(&serde_json::Value, &mut App) -> usize>,
/// For dynamic items this is a way to select a value from a list of values
/// this is always none for non-dynamic items
select_descendant: Option<fn(&serde_json::Value, &App) -> usize>,
}
impl UiEntry {
@@ -177,7 +179,7 @@ impl UiEntry {
}
}
struct SettingsUiTree {
pub struct SettingsUiTree {
root_entry_indices: Vec<usize>,
entries: Vec<UiEntry>,
active_entry_index: usize,
@@ -242,7 +244,7 @@ fn build_tree_item(
}
impl SettingsUiTree {
fn new(cx: &App) -> Self {
pub fn new(cx: &App) -> Self {
let settings_store = SettingsStore::global(cx);
let mut tree = vec![];
let mut root_entry_indices = vec![];
@@ -269,6 +271,62 @@ impl SettingsUiTree {
active_entry_index,
}
}
// todo(settings_ui): Make sure `Item::None` paths are added to the paths tree,
// so that we can keep none/skip and still test in CI that all settings have
#[cfg(feature = "test-support")]
pub fn all_paths(&self, cx: &App) -> Vec<Vec<&'static str>> {
fn all_paths_rec(
tree: &[UiEntry],
paths: &mut Vec<Vec<&'static str>>,
current_path: &mut Vec<&'static str>,
idx: usize,
cx: &App,
) {
let child = &tree[idx];
let mut pushed_path = false;
if let Some(path) = child.path.as_ref() {
current_path.push(path);
paths.push(current_path.clone());
pushed_path = true;
}
// todo(settings_ui): handle dynamic nodes here
let selected_descendant_index = child
.select_descendant
.map(|select_descendant| {
read_settings_value_from_path(
SettingsStore::global(cx).raw_default_settings(),
&current_path,
)
.map(|value| select_descendant(value, cx))
})
.and_then(|selected_descendant_index| {
selected_descendant_index.map(|index| child.nth_descendant_index(tree, index))
});
if let Some(selected_descendant_index) = selected_descendant_index {
// just silently fail if we didn't find a setting value for the path
if let Some(descendant_index) = selected_descendant_index {
all_paths_rec(tree, paths, current_path, descendant_index, cx);
}
} else if let Some(desc_idx) = child.first_descendant_index() {
let mut desc_idx = Some(desc_idx);
while let Some(descendant_index) = desc_idx {
all_paths_rec(&tree, paths, current_path, descendant_index, cx);
desc_idx = tree[descendant_index].next_sibling;
}
}
if pushed_path {
current_path.pop();
}
}
let mut paths = Vec::new();
for &index in &self.root_entry_indices {
all_paths_rec(&self.entries, &mut paths, &mut Vec::new(), index, cx);
}
paths
}
}
fn render_nav(tree: &SettingsUiTree, _window: &mut Window, cx: &mut Context<SettingsPage>) -> Div {
@@ -444,9 +502,9 @@ fn render_item_single(
}
}
fn read_settings_value_from_path<'a>(
pub fn read_settings_value_from_path<'a>(
settings_contents: &'a serde_json::Value,
path: &[&'static str],
path: &[&str],
) -> Option<&'a serde_json::Value> {
// todo(settings_ui) make non recursive, and move to `settings` alongside SettingsValue, and add method to SettingsValue to get nested
let Some((key, remaining)) = path.split_first() else {

View File

@@ -1184,7 +1184,7 @@ impl Element for TerminalElement {
cx: &mut App,
) {
let paint_start = Instant::now();
window.with_content_mask(Some(ContentMask { bounds, ..Default::default() }), |window| {
window.with_content_mask(Some(ContentMask { bounds }), |window| {
let scroll_top = self.terminal_view.read(cx).scroll_top;
window.paint_quad(fill(bounds, layout.background_color));

View File

@@ -303,13 +303,9 @@ impl Element for Scrollbar {
window: &mut Window,
_: &mut App,
) -> Self::PrepaintState {
window.with_content_mask(
Some(ContentMask {
bounds,
..Default::default()
}),
|window| window.insert_hitbox(bounds, HitboxBehavior::Normal),
)
window.with_content_mask(Some(ContentMask { bounds }), |window| {
window.insert_hitbox(bounds, HitboxBehavior::Normal)
})
}
fn paint(
@@ -323,11 +319,7 @@ impl Element for Scrollbar {
cx: &mut App,
) {
const EXTRA_PADDING: Pixels = px(5.0);
let content_mask = ContentMask {
bounds,
..Default::default()
};
window.with_content_mask(Some(content_mask), |window| {
window.with_content_mask(Some(ContentMask { bounds }), |window| {
let axis = self.kind;
let colors = cx.theme().colors();
let thumb_state = self.state.thumb_state.get();

View File

@@ -188,6 +188,7 @@ itertools.workspace = true
language = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
settings_ui = { workspace = true, features = ["test-support"] }
terminal_view = { workspace = true, features = ["test-support"] }
tree-sitter-md.workspace = true
tree-sitter-rust.workspace = true

View File

@@ -4855,4 +4855,34 @@ mod tests {
"BUG FOUND: Project settings were overwritten when opening via command - original custom content was lost"
);
}
#[gpui::test]
fn test_settings_defaults(cx: &mut TestAppContext) {
cx.update(|cx| {
settings::init(cx);
workspace::init_settings(cx);
title_bar::init(cx);
editor::init_settings(cx);
debugger_ui::init(cx);
});
let default_json =
cx.read(|cx| cx.global::<SettingsStore>().raw_default_settings().clone());
let all_paths = cx.read(|cx| settings_ui::SettingsUiTree::new(cx).all_paths(cx));
let mut failures = Vec::new();
for path in all_paths {
if settings_ui::read_settings_value_from_path(&default_json, &path).is_none() {
failures.push(path);
}
}
if !failures.is_empty() {
panic!(
"No default value found for paths: {:#?}",
failures
.into_iter()
.map(|path| path.join("."))
.collect::<Vec<_>>()
);
}
}
}