Compare commits
10 Commits
assistant_
...
rustdoc-ac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ebf440247 | ||
|
|
70c05262f4 | ||
|
|
82e55e4145 | ||
|
|
fee9cf4101 | ||
|
|
143d9fc95e | ||
|
|
3d6b46cf9a | ||
|
|
bb2d833373 | ||
|
|
eedfc5be5a | ||
|
|
0e76cc8036 | ||
|
|
6bd5251882 |
602
.github/workflows/release_nightly.yml
vendored
602
.github/workflows/release_nightly.yml
vendored
@@ -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
1
Cargo.lock
generated
@@ -14906,6 +14906,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command_palette_hooks",
|
||||
"debugger_ui",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"gpui",
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
¤t_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 {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user