mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-06 11:06:58 +03:00
Merge branch 'main' into terminal-element
This commit is contained in:
commit
fd31e429f5
263
.github/workflows/ci.yml
vendored
263
.github/workflows/ci.yml
vendored
@ -1,144 +1,149 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "v[0-9]+.[0-9]+.x"
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "v[0-9]+.[0-9]+.x"
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
|
||||
concurrency:
|
||||
# Allow only one workflow per any non-`main` branch.
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Set up default .cargo/config.toml
|
||||
run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
|
||||
- name: Set up default .cargo/config.toml
|
||||
run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
|
||||
|
||||
- name: Run rustfmt
|
||||
uses: ./.github/actions/check_formatting
|
||||
- name: Run rustfmt
|
||||
uses: ./.github/actions/check_formatting
|
||||
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: rustfmt
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: rustfmt
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
- name: Build collab
|
||||
run: cargo build -p collab
|
||||
- name: Build collab
|
||||
run: cargo build -p collab
|
||||
|
||||
- name: Build other binaries
|
||||
run: cargo build --workspace --bins --all-features
|
||||
- name: Build other binaries
|
||||
run: cargo build --workspace --bins --all-features
|
||||
|
||||
bundle:
|
||||
name: Bundle app
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
needs: tests
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add wasm32-wasi
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Determine version and release channel
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
version=$(script/get-crate-version zed)
|
||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||
echo "Publishing version: ${version} on release channel ${channel}"
|
||||
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
|
||||
|
||||
expected_tag_name=""
|
||||
case ${channel} in
|
||||
stable)
|
||||
expected_tag_name="v${version}";;
|
||||
preview)
|
||||
expected_tag_name="v${version}-pre";;
|
||||
nightly)
|
||||
expected_tag_name="v${version}-nightly";;
|
||||
*)
|
||||
echo "can't publish a release on channel ${channel}"
|
||||
exit 1;;
|
||||
esac
|
||||
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
|
||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle
|
||||
|
||||
- name: Upload app bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v3
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
||||
path: target/release/Zed.dmg
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
name: Upload app bundle to release
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
files: target/release/Zed.dmg
|
||||
body: ""
|
||||
bundle:
|
||||
name: Bundle app
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
needs: tests
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add wasm32-wasi
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Determine version and release channel
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
version=$(script/get-crate-version zed)
|
||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||
echo "Publishing version: ${version} on release channel ${channel}"
|
||||
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
|
||||
|
||||
expected_tag_name=""
|
||||
case ${channel} in
|
||||
stable)
|
||||
expected_tag_name="v${version}";;
|
||||
preview)
|
||||
expected_tag_name="v${version}-pre";;
|
||||
nightly)
|
||||
expected_tag_name="v${version}-nightly";;
|
||||
*)
|
||||
echo "can't publish a release on channel ${channel}"
|
||||
exit 1;;
|
||||
esac
|
||||
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
|
||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle
|
||||
|
||||
- name: Upload app bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v3
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
||||
path: target/release/Zed.dmg
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
name: Upload app bundle to release
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
files: target/release/Zed.dmg
|
||||
body: ""
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
161
.github/workflows/release_nightly.yml
vendored
161
.github/workflows/release_nightly.yml
vendored
@ -1,98 +1,99 @@
|
||||
name: Release Nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Fire every night at 1:00am
|
||||
- cron: "0 1 * * *"
|
||||
push:
|
||||
tags:
|
||||
- "nightly"
|
||||
schedule:
|
||||
# Fire every night at 1:00am
|
||||
- cron: "0 1 * * *"
|
||||
push:
|
||||
tags:
|
||||
- "nightly"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Run rustfmt
|
||||
uses: ./.github/actions/check_formatting
|
||||
- name: Run rustfmt
|
||||
uses: ./.github/actions/check_formatting
|
||||
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: rustfmt
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: rustfmt
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
bundle:
|
||||
name: Bundle app
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
needs: tests
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add wasm32-wasi
|
||||
bundle:
|
||||
name: Bundle app
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
needs: tests
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add wasm32-wasi
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- 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: Set release channel to nightly, add nightly prefix to the final version
|
||||
run: |
|
||||
set -eu
|
||||
version=$(git rev-parse --short HEAD)
|
||||
echo "Publishing version: ${version} on release channel nightly"
|
||||
sed -i '' "s/version = \"\(.*\)\"/version = \"\1-nightly\"/" crates/zed2/Cargo.toml
|
||||
echo "nightly" > crates/zed/RELEASE_CHANNEL
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle -2
|
||||
- name: Create app bundle
|
||||
run: script/bundle -2
|
||||
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly
|
||||
|
69
Cargo.lock
generated
69
Cargo.lock
generated
@ -376,6 +376,47 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assistant2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ai2",
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"client2",
|
||||
"collections",
|
||||
"ctor",
|
||||
"editor2",
|
||||
"env_logger 0.9.3",
|
||||
"fs2",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"indoc",
|
||||
"isahc",
|
||||
"language2",
|
||||
"log",
|
||||
"menu2",
|
||||
"multi_buffer2",
|
||||
"ordered-float 2.10.0",
|
||||
"parking_lot 0.11.2",
|
||||
"project2",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"schemars",
|
||||
"search2",
|
||||
"semantic_index2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings2",
|
||||
"smol",
|
||||
"theme2",
|
||||
"tiktoken-rs",
|
||||
"ui2",
|
||||
"util",
|
||||
"uuid 1.4.1",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.4.1"
|
||||
@ -413,9 +454,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.3.15"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a"
|
||||
checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"futures-core",
|
||||
@ -1702,7 +1743,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.29.1"
|
||||
version = "0.30.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1789,7 +1830,7 @@ dependencies = [
|
||||
"clap 3.2.25",
|
||||
"client2",
|
||||
"clock",
|
||||
"collab_ui",
|
||||
"collab_ui2",
|
||||
"collections",
|
||||
"ctor",
|
||||
"dashmap",
|
||||
@ -2118,6 +2159,7 @@ dependencies = [
|
||||
"settings2",
|
||||
"smol",
|
||||
"theme2",
|
||||
"ui2",
|
||||
"util",
|
||||
]
|
||||
|
||||
@ -7071,6 +7113,18 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick_action_bar2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"assistant2",
|
||||
"editor2",
|
||||
"gpui2",
|
||||
"search2",
|
||||
"ui2",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.33"
|
||||
@ -11695,7 +11749,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.116.0"
|
||||
version = "0.117.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"ai",
|
||||
@ -11836,11 +11890,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed2"
|
||||
version = "0.109.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"activity_indicator2",
|
||||
"ai2",
|
||||
"anyhow",
|
||||
"assistant2",
|
||||
"async-compression",
|
||||
"async-recursion 0.3.2",
|
||||
"async-tar",
|
||||
@ -11891,6 +11946,7 @@ dependencies = [
|
||||
"postage",
|
||||
"project2",
|
||||
"project_panel2",
|
||||
"quick_action_bar2",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"rope2",
|
||||
@ -11899,6 +11955,7 @@ dependencies = [
|
||||
"rust-embed",
|
||||
"schemars",
|
||||
"search2",
|
||||
"semantic_index2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
|
@ -4,6 +4,7 @@ members = [
|
||||
"crates/activity_indicator2",
|
||||
"crates/ai",
|
||||
"crates/assistant",
|
||||
"crates/assistant2",
|
||||
"crates/audio",
|
||||
"crates/audio2",
|
||||
"crates/auto_update",
|
||||
@ -89,6 +90,7 @@ members = [
|
||||
"crates/project_panel",
|
||||
"crates/project_panel2",
|
||||
"crates/project_symbols",
|
||||
"crates/quick_action_bar2",
|
||||
"crates/recent_projects",
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
@ -134,6 +136,7 @@ resolver = "2"
|
||||
[workspace.dependencies]
|
||||
anyhow = { version = "1.0.57" }
|
||||
async-trait = { version = "0.1" }
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
# TODO: Switch back to the published version of `ctor` once:
|
||||
# 1. A new version of `ctor` is published with this change: https://github.com/mmastrac/rust-ctor/pull/295
|
||||
# 2. We've confirmed it's fine to update to the latest version of `ctor` (we're currently on v0.1.20).
|
||||
|
@ -17,18 +17,9 @@
|
||||
"cmd-enter": "menu::SecondaryConfirm",
|
||||
"escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"cmd-{": "pane::ActivatePrevItem",
|
||||
"cmd-}": "pane::ActivateNextItem",
|
||||
"alt-cmd-left": "pane::ActivatePrevItem",
|
||||
"alt-cmd-right": "pane::ActivateNextItem",
|
||||
"cmd-w": "pane::CloseActiveItem",
|
||||
"alt-cmd-t": "pane::CloseInactiveItems",
|
||||
"ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes",
|
||||
"cmd-k u": "pane::CloseCleanItems",
|
||||
"cmd-k cmd-w": "pane::CloseAllItems",
|
||||
"cmd-shift-w": "workspace::CloseWindow",
|
||||
"cmd-s": "workspace::Save",
|
||||
"cmd-shift-s": "workspace::SaveAs",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"cmd-o": "workspace::Open",
|
||||
"cmd-=": "zed::IncreaseBufferFontSize",
|
||||
"cmd-+": "zed::IncreaseBufferFontSize",
|
||||
"cmd--": "zed::DecreaseBufferFontSize",
|
||||
@ -38,15 +29,7 @@
|
||||
"cmd-h": "zed::Hide",
|
||||
"alt-cmd-h": "zed::HideOthers",
|
||||
"cmd-m": "zed::Minimize",
|
||||
"ctrl-cmd-f": "zed::ToggleFullScreen",
|
||||
"cmd-n": "workspace::NewFile",
|
||||
"cmd-shift-n": "workspace::NewWindow",
|
||||
"cmd-o": "workspace::Open",
|
||||
"alt-cmd-o": "projects::OpenRecent",
|
||||
"alt-cmd-b": "branches::OpenRecent",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"shift-escape": "workspace::ToggleZoom"
|
||||
"ctrl-cmd-f": "zed::ToggleFullScreen"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -284,6 +267,15 @@
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"cmd-{": "pane::ActivatePrevItem",
|
||||
"cmd-}": "pane::ActivateNextItem",
|
||||
"alt-cmd-left": "pane::ActivatePrevItem",
|
||||
"alt-cmd-right": "pane::ActivateNextItem",
|
||||
"cmd-w": "pane::CloseActiveItem",
|
||||
"alt-cmd-t": "pane::CloseInactiveItems",
|
||||
"ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes",
|
||||
"cmd-k u": "pane::CloseCleanItems",
|
||||
"cmd-k cmd-w": "pane::CloseAllItems",
|
||||
"cmd-f": "project_search::ToggleFocus",
|
||||
"cmd-g": "search::SelectNextMatch",
|
||||
"cmd-shift-g": "search::SelectPrevMatch",
|
||||
@ -389,6 +381,14 @@
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"alt-cmd-o": "projects::OpenRecent",
|
||||
"alt-cmd-b": "branches::OpenRecent",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
"cmd-s": "workspace::Save",
|
||||
"cmd-shift-s": "workspace::SaveAs",
|
||||
"cmd-n": "workspace::NewFile",
|
||||
"cmd-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"cmd-1": ["workspace::ActivatePane", 0],
|
||||
"cmd-2": ["workspace::ActivatePane", 1],
|
||||
"cmd-3": ["workspace::ActivatePane", 2],
|
||||
|
@ -104,7 +104,7 @@ pub struct OpenAIResponseStreamEvent {
|
||||
|
||||
pub async fn stream_completion(
|
||||
credential: ProviderCredential,
|
||||
executor: Arc<BackgroundExecutor>,
|
||||
executor: BackgroundExecutor,
|
||||
request: Box<dyn CompletionRequest>,
|
||||
) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
|
||||
let api_key = match credential {
|
||||
@ -197,11 +197,11 @@ pub async fn stream_completion(
|
||||
pub struct OpenAICompletionProvider {
|
||||
model: OpenAILanguageModel,
|
||||
credential: Arc<RwLock<ProviderCredential>>,
|
||||
executor: Arc<BackgroundExecutor>,
|
||||
executor: BackgroundExecutor,
|
||||
}
|
||||
|
||||
impl OpenAICompletionProvider {
|
||||
pub fn new(model_name: &str, executor: Arc<BackgroundExecutor>) -> Self {
|
||||
pub fn new(model_name: &str, executor: BackgroundExecutor) -> Self {
|
||||
let model = OpenAILanguageModel::load(model_name);
|
||||
let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials));
|
||||
Self {
|
||||
|
54
crates/assistant2/Cargo.toml
Normal file
54
crates/assistant2/Cargo.toml
Normal file
@ -0,0 +1,54 @@
|
||||
[package]
|
||||
name = "assistant2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/assistant.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
ai = { package = "ai2", path = "../ai2" }
|
||||
client = { package = "client2", path = "../client2" }
|
||||
collections = { path = "../collections"}
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
fs = { package = "fs2", path = "../fs2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
multi_buffer = { package = "multi_buffer2", path = "../multi_buffer2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
search = { package = "search2", path = "../search2" }
|
||||
semantic_index = { package = "semantic_index2", path = "../semantic_index2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
util = { path = "../util" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
|
||||
uuid.workspace = true
|
||||
log.workspace = true
|
||||
anyhow.workspace = true
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
futures.workspace = true
|
||||
indoc.workspace = true
|
||||
isahc.workspace = true
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
regex.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
tiktoken-rs.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ai = { package = "ai2", path = "../ai2", features = ["test-support"]}
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
project = { package = "project2", path = "../project2", features = ["test-support"] }
|
||||
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
log.workspace = true
|
||||
rand.workspace = true
|
63
crates/assistant2/README.zmd
Normal file
63
crates/assistant2/README.zmd
Normal file
@ -0,0 +1,63 @@
|
||||
> Hello, I'm Nathan Sobo.
|
||||
I'm currently your sole creator and the leader of Zed Industries.
|
||||
Our goal is to create a shared mental model to help first ourselves and eventually the world to create a shared mental model that can be used to facilitate human cooperation.
|
||||
|
||||
< Hello, Nathan! Welcome back to Zed. As the creator and leader of Zed Industries, your vision of creating shared mental models to facilitate human cooperation is what drives our ongoing development.
|
||||
|
||||
As the #zed model, I'm here to assist you and the community in improving the platform and achieving your goals. Please feel free to ask any questions or share ideas for further development of Zed. Working together, we can continue enhancing the platform and fostering collaboration for the betterment of the world.
|
||||
|
||||
> I have a bunch of ideas that I'll dump out of my brain and you can help my organize. Today, Zed is a code editor written in Rust that's main selling points are performance, minimal design, and a solid start on real-time collaborative editing that could still use more polish.
|
||||
|
||||
We've decided that our next milestone will be open-sourcing Zed on the Zed platform.
|
||||
|
||||
< Nathan, it's exciting to hear that you have more ideas to enhance Zed! Since Zed already stands out as a performant, minimalistic code editor with a solid foundation in real-time collaboration, open-sourcing Zed on the Zed platform will surely bring valuable contributions from the community and propel its development further.
|
||||
|
||||
Let's organize your ideas and create a plan for your next milestone. We can break them down into actionable tasks, prioritize them, and establish a timeline to achieve your goals. Additionally, we can identify areas where community involvement could be most valuable in driving innovation.
|
||||
|
||||
Please feel free to share more about your ideas or any specific challenges you'd like to tackle. I'm here to help you turn these ideas into a cohesive and successful plan for Zed's future!
|
||||
|
||||
@nathan> #zed, can you describe the conversation UI as you currently understand it.
|
||||
|
||||
< Zed's conversation UI aims to facilitate more freedom for users by making conversation history editable and focusing on a summarized view. Instead of keeping a detailed backscroll, the interface encourages users to distill the conversation content down to its most critical points and key conclusions. This approach ensures that the vital information from the conversation is highlighted and easily accessible, while still maintaining a history that can be linked to when necessary.
|
||||
|
||||
Addressing @nate's concerns, this design choice aims to strike a balance between readability, usability, and information preservation. The summarized view maintains the essence of the conversation without being burdened by extensive backscroll. Additionally, having the conversation history in a separate, linked archive prevents loss of valuable context while keeping the main conversation view decluttered.
|
||||
|
||||
@nathan> What details around the conversation view are worth exploring?
|
||||
|
||||
< @nathan, there are several aspects of the conversation view that are worth exploring and refining:
|
||||
|
||||
1. Editing functionality: Ensuring that all participants can edit and refine the conversation summary effectively while preserving the original context.
|
||||
2. Permissions and access control: Determining how to manage editing rights, user roles, and visibility of the conversation summary and history.
|
||||
3. Navigation and linking: Implementing an intuitive system for navigating between the summarized conversation and linked history or reference materials.
|
||||
4. Visualization and layout: Designing the interface to clearly present the summarized information while providing a visually engaging and organized experience for participants.
|
||||
5. Threaded conversations and branching: Exploring ways to handle sub-conversations, supplementary discussions, or debates within the primary conversation view.
|
||||
|
||||
Considering these aspects will ensure our conversation view design is optimized for usability, collaboration, and accessibility in Zed.
|
||||
|
||||
@nathan> @nate, which of these would you like to explore?
|
||||
|
||||
@nate> 2 feels like it isn't important at the moment, we can explore that later. Let's start with 4, which I think will lead us to discussion 3 and 5.
|
||||
|
||||
#zed share your thoughts on the points we need to consider to design a layout and visualization for a conversation view between you (#zed) and multuple peoople, or between multiple people and multiple bots (you and other bots).
|
||||
|
||||
@nathan> Agreed. I'm interested in threading I think more than anything. Or 4 yeah. I think we need to scope the threading conversation. Also, asking #zed to propose the solution... not sure it will be that effective but it's worth a try...
|
||||
|
||||
#zed describe a hypothetical conversation UI that meets the description.
|
||||
|
||||
< @nate and @nathan, I understand that you'd like to focus on the layout and visualization for a conversation view that can accommodate multiple people and potentially multiple bots as well. Here's a proposal for a conversation UI that meets the requirements:
|
||||
|
||||
1. Main conversation area: Display the summarized conversation content in a clean and organized manner, ensuring readability and efficient use of space.
|
||||
|
||||
2. Participant avatars and labels: Include user avatars and/or labels to distinguish between users and bots visually, allowing easy identification of each participant's contributions.
|
||||
|
||||
3. Active threads indicator: Designate a clear indicator for ongoing threads, which will attract attention to active sub-conversations or related discussions in the conversation view.
|
||||
|
||||
4. Dynamic branching: Implement a branching visual structure to present threaded conversations, clearly distinguishing sub-conversations, and enabling users to navigate between them smoothly.
|
||||
|
||||
5. Multiple bot interaction support: Facilitate interactions between multiple bots through user-invoked prompts or bot-to-bot communication channels without disrupting the primary conversation flow.
|
||||
|
||||
6. Expandable conversation history: Offer a separate, expandable section or panel to access historical content without cluttering the main conversation view.
|
||||
|
||||
7. Linking and referencing: Implement a system for hyperlinking relevant information, resources, or previous conversations to the current discussion, making it easy for participants to follow and reference the shared context.
|
||||
|
||||
This hypothetical conversation UI aims to address the layout and visualization, threading, and multi-participant aspects of your requirements. Of course, it's just an initial proposal, and we can refine and iterate on it based on your feedback and specific needs.
|
3
crates/assistant2/features.zmd
Normal file
3
crates/assistant2/features.zmd
Normal file
@ -0,0 +1,3 @@
|
||||
Push content to a deeper layer.
|
||||
A context can have multiple sublayers.
|
||||
You can enable or disable arbitrary sublayers at arbitrary nesting depths when viewing the document.
|
126
crates/assistant2/src/assistant.rs
Normal file
126
crates/assistant2/src/assistant.rs
Normal file
@ -0,0 +1,126 @@
|
||||
pub mod assistant_panel;
|
||||
mod assistant_settings;
|
||||
mod codegen;
|
||||
mod prompts;
|
||||
mod streaming_diff;
|
||||
|
||||
use ai::providers::open_ai::Role;
|
||||
use anyhow::Result;
|
||||
pub use assistant_panel::AssistantPanel;
|
||||
use assistant_settings::OpenAIModel;
|
||||
use chrono::{DateTime, Local};
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::{actions, AppContext};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc};
|
||||
use util::paths::CONVERSATIONS_DIR;
|
||||
|
||||
actions!(
|
||||
NewConversation,
|
||||
Assist,
|
||||
Split,
|
||||
CycleMessageRole,
|
||||
QuoteSelection,
|
||||
ToggleFocus,
|
||||
ResetKey,
|
||||
InlineAssist,
|
||||
ToggleIncludeConversation,
|
||||
ToggleRetrieveContext,
|
||||
);
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
struct MessageId(usize);
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct MessageMetadata {
|
||||
role: Role,
|
||||
sent_at: DateTime<Local>,
|
||||
status: MessageStatus,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
enum MessageStatus {
|
||||
Pending,
|
||||
Done,
|
||||
Error(Arc<str>),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedMessage {
|
||||
id: MessageId,
|
||||
start: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedConversation {
|
||||
id: Option<String>,
|
||||
zed: String,
|
||||
version: String,
|
||||
text: String,
|
||||
messages: Vec<SavedMessage>,
|
||||
message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: String,
|
||||
model: OpenAIModel,
|
||||
}
|
||||
|
||||
impl SavedConversation {
|
||||
const VERSION: &'static str = "0.1.0";
|
||||
}
|
||||
|
||||
struct SavedConversationMetadata {
|
||||
title: String,
|
||||
path: PathBuf,
|
||||
mtime: chrono::DateTime<chrono::Local>,
|
||||
}
|
||||
|
||||
impl SavedConversationMetadata {
|
||||
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
|
||||
fs.create_dir(&CONVERSATIONS_DIR).await?;
|
||||
|
||||
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
|
||||
let mut conversations = Vec::<SavedConversationMetadata>::new();
|
||||
while let Some(path) = paths.next().await {
|
||||
let path = path?;
|
||||
if path.extension() != Some(OsStr::new("json")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pattern = r" - \d+.zed.json$";
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
|
||||
let metadata = fs.metadata(&path).await?;
|
||||
if let Some((file_name, metadata)) = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.zip(metadata)
|
||||
{
|
||||
let title = re.replace(file_name, "");
|
||||
conversations.push(Self {
|
||||
title: title.into_owned(),
|
||||
path,
|
||||
mtime: metadata.mtime.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
|
||||
|
||||
Ok(conversations)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
assistant_panel::init(cx);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::init();
|
||||
}
|
||||
}
|
3486
crates/assistant2/src/assistant_panel.rs
Normal file
3486
crates/assistant2/src/assistant_panel.rs
Normal file
File diff suppressed because it is too large
Load Diff
80
crates/assistant2/src/assistant_settings.rs
Normal file
80
crates/assistant2/src/assistant_settings.rs
Normal file
@ -0,0 +1,80 @@
|
||||
use anyhow;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub enum OpenAIModel {
|
||||
#[serde(rename = "gpt-3.5-turbo-0613")]
|
||||
ThreePointFiveTurbo,
|
||||
#[serde(rename = "gpt-4-0613")]
|
||||
Four,
|
||||
#[serde(rename = "gpt-4-1106-preview")]
|
||||
FourTurbo,
|
||||
}
|
||||
|
||||
impl OpenAIModel {
|
||||
pub fn full_name(&self) -> &'static str {
|
||||
match self {
|
||||
OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo-0613",
|
||||
OpenAIModel::Four => "gpt-4-0613",
|
||||
OpenAIModel::FourTurbo => "gpt-4-1106-preview",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn short_name(&self) -> &'static str {
|
||||
match self {
|
||||
OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo",
|
||||
OpenAIModel::Four => "gpt-4",
|
||||
OpenAIModel::FourTurbo => "gpt-4-turbo",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cycle(&self) -> Self {
|
||||
match self {
|
||||
OpenAIModel::ThreePointFiveTurbo => OpenAIModel::Four,
|
||||
OpenAIModel::Four => OpenAIModel::FourTurbo,
|
||||
OpenAIModel::FourTurbo => OpenAIModel::ThreePointFiveTurbo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AssistantDockPosition {
|
||||
Left,
|
||||
Right,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct AssistantSettings {
|
||||
pub button: bool,
|
||||
pub dock: AssistantDockPosition,
|
||||
pub default_width: f32,
|
||||
pub default_height: f32,
|
||||
pub default_open_ai_model: OpenAIModel,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContent {
|
||||
pub button: Option<bool>,
|
||||
pub dock: Option<AssistantDockPosition>,
|
||||
pub default_width: Option<f32>,
|
||||
pub default_height: Option<f32>,
|
||||
pub default_open_ai_model: Option<OpenAIModel>,
|
||||
}
|
||||
|
||||
impl Settings for AssistantSettings {
|
||||
const KEY: Option<&'static str> = Some("assistant");
|
||||
|
||||
type FileContent = AssistantSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
}
|
||||
}
|
688
crates/assistant2/src/codegen.rs
Normal file
688
crates/assistant2/src/codegen.rs
Normal file
@ -0,0 +1,688 @@
|
||||
use crate::streaming_diff::{Hunk, StreamingDiff};
|
||||
use ai::completion::{CompletionProvider, CompletionRequest};
|
||||
use anyhow::Result;
|
||||
use editor::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
|
||||
use gpui::{EventEmitter, Model, ModelContext, Task};
|
||||
use language::{Rope, TransactionId};
|
||||
use multi_buffer;
|
||||
use std::{cmp, future, ops::Range, sync::Arc};
|
||||
|
||||
pub enum Event {
|
||||
Finished,
|
||||
Undone,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum CodegenKind {
|
||||
Transform { range: Range<Anchor> },
|
||||
Generate { position: Anchor },
|
||||
}
|
||||
|
||||
pub struct Codegen {
|
||||
provider: Arc<dyn CompletionProvider>,
|
||||
buffer: Model<MultiBuffer>,
|
||||
snapshot: MultiBufferSnapshot,
|
||||
kind: CodegenKind,
|
||||
last_equal_ranges: Vec<Range<Anchor>>,
|
||||
transaction_id: Option<TransactionId>,
|
||||
error: Option<anyhow::Error>,
|
||||
generation: Task<()>,
|
||||
idle: bool,
|
||||
_subscription: gpui::Subscription,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for Codegen {}
|
||||
|
||||
impl Codegen {
|
||||
pub fn new(
|
||||
buffer: Model<MultiBuffer>,
|
||||
kind: CodegenKind,
|
||||
provider: Arc<dyn CompletionProvider>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
Self {
|
||||
provider,
|
||||
buffer: buffer.clone(),
|
||||
snapshot,
|
||||
kind,
|
||||
last_equal_ranges: Default::default(),
|
||||
transaction_id: Default::default(),
|
||||
error: Default::default(),
|
||||
idle: true,
|
||||
generation: Task::ready(()),
|
||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_buffer_event(
|
||||
&mut self,
|
||||
_buffer: Model<MultiBuffer>,
|
||||
event: &multi_buffer::Event,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let multi_buffer::Event::TransactionUndone { transaction_id } = event {
|
||||
if self.transaction_id == Some(*transaction_id) {
|
||||
self.transaction_id = None;
|
||||
self.generation = Task::ready(());
|
||||
cx.emit(Event::Undone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn range(&self) -> Range<Anchor> {
|
||||
match &self.kind {
|
||||
CodegenKind::Transform { range } => range.clone(),
|
||||
CodegenKind::Generate { position } => position.bias_left(&self.snapshot)..*position,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> &CodegenKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
pub fn last_equal_ranges(&self) -> &[Range<Anchor>] {
|
||||
&self.last_equal_ranges
|
||||
}
|
||||
|
||||
pub fn idle(&self) -> bool {
|
||||
self.idle
|
||||
}
|
||||
|
||||
pub fn error(&self) -> Option<&anyhow::Error> {
|
||||
self.error.as_ref()
|
||||
}
|
||||
|
||||
pub fn start(&mut self, prompt: Box<dyn CompletionRequest>, cx: &mut ModelContext<Self>) {
|
||||
let range = self.range();
|
||||
let snapshot = self.snapshot.clone();
|
||||
let selected_text = snapshot
|
||||
.text_for_range(range.start..range.end)
|
||||
.collect::<Rope>();
|
||||
|
||||
let selection_start = range.start.to_point(&snapshot);
|
||||
let suggested_line_indent = snapshot
|
||||
.suggested_indents(selection_start.row..selection_start.row + 1, cx)
|
||||
.into_values()
|
||||
.next()
|
||||
.unwrap_or_else(|| snapshot.indent_size_for_line(selection_start.row));
|
||||
|
||||
let response = self.provider.complete(prompt);
|
||||
self.generation = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let generate = async {
|
||||
let mut edit_start = range.start.to_offset(&snapshot);
|
||||
|
||||
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
|
||||
let diff = cx.background_executor().spawn(async move {
|
||||
let chunks = strip_invalid_spans_from_codeblock(response.await?);
|
||||
futures::pin_mut!(chunks);
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
|
||||
let mut new_text = String::new();
|
||||
let mut base_indent = None;
|
||||
let mut line_indent = None;
|
||||
let mut first_line = true;
|
||||
|
||||
while let Some(chunk) = chunks.next().await {
|
||||
let chunk = chunk?;
|
||||
|
||||
let mut lines = chunk.split('\n').peekable();
|
||||
while let Some(line) = lines.next() {
|
||||
new_text.push_str(line);
|
||||
if line_indent.is_none() {
|
||||
if let Some(non_whitespace_ch_ix) =
|
||||
new_text.find(|ch: char| !ch.is_whitespace())
|
||||
{
|
||||
line_indent = Some(non_whitespace_ch_ix);
|
||||
base_indent = base_indent.or(line_indent);
|
||||
|
||||
let line_indent = line_indent.unwrap();
|
||||
let base_indent = base_indent.unwrap();
|
||||
let indent_delta = line_indent as i32 - base_indent as i32;
|
||||
let mut corrected_indent_len = cmp::max(
|
||||
0,
|
||||
suggested_line_indent.len as i32 + indent_delta,
|
||||
)
|
||||
as usize;
|
||||
if first_line {
|
||||
corrected_indent_len = corrected_indent_len
|
||||
.saturating_sub(selection_start.column as usize);
|
||||
}
|
||||
|
||||
let indent_char = suggested_line_indent.char();
|
||||
let mut indent_buffer = [0; 4];
|
||||
let indent_str =
|
||||
indent_char.encode_utf8(&mut indent_buffer);
|
||||
new_text.replace_range(
|
||||
..line_indent,
|
||||
&indent_str.repeat(corrected_indent_len),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if line_indent.is_some() {
|
||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
||||
new_text.clear();
|
||||
}
|
||||
|
||||
if lines.peek().is_some() {
|
||||
hunks_tx.send(diff.push_new("\n")).await?;
|
||||
line_indent = None;
|
||||
first_line = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
||||
hunks_tx.send(diff.finish()).await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
while let Some(hunks) = hunks_rx.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.last_equal_ranges.clear();
|
||||
|
||||
let transaction = this.buffer.update(cx, |buffer, cx| {
|
||||
// Avoid grouping assistant edits with user edits.
|
||||
buffer.finalize_last_transaction(cx);
|
||||
|
||||
buffer.start_transaction(cx);
|
||||
buffer.edit(
|
||||
hunks.into_iter().filter_map(|hunk| match hunk {
|
||||
Hunk::Insert { text } => {
|
||||
let edit_start = snapshot.anchor_after(edit_start);
|
||||
Some((edit_start..edit_start, text))
|
||||
}
|
||||
Hunk::Remove { len } => {
|
||||
let edit_end = edit_start + len;
|
||||
let edit_range = snapshot.anchor_after(edit_start)
|
||||
..snapshot.anchor_before(edit_end);
|
||||
edit_start = edit_end;
|
||||
Some((edit_range, String::new()))
|
||||
}
|
||||
Hunk::Keep { len } => {
|
||||
let edit_end = edit_start + len;
|
||||
let edit_range = snapshot.anchor_after(edit_start)
|
||||
..snapshot.anchor_before(edit_end);
|
||||
edit_start = edit_end;
|
||||
this.last_equal_ranges.push(edit_range);
|
||||
None
|
||||
}
|
||||
}),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
|
||||
buffer.end_transaction(cx)
|
||||
});
|
||||
|
||||
if let Some(transaction) = transaction {
|
||||
if let Some(first_transaction) = this.transaction_id {
|
||||
// Group all assistant edits into the first transaction.
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.merge_transactions(
|
||||
transaction,
|
||||
first_transaction,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
} else {
|
||||
this.transaction_id = Some(transaction);
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction(cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
|
||||
diff.await?;
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let result = generate.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.last_equal_ranges.clear();
|
||||
this.idle = true;
|
||||
if let Err(error) = result {
|
||||
this.error = Some(error);
|
||||
}
|
||||
cx.emit(Event::Finished);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
self.error.take();
|
||||
self.idle = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if let Some(transaction_id) = self.transaction_id {
|
||||
self.buffer
|
||||
.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_invalid_spans_from_codeblock(
|
||||
stream: impl Stream<Item = Result<String>>,
|
||||
) -> impl Stream<Item = Result<String>> {
|
||||
let mut first_line = true;
|
||||
let mut buffer = String::new();
|
||||
let mut starts_with_markdown_codeblock = false;
|
||||
let mut includes_start_or_end_span = false;
|
||||
stream.filter_map(move |chunk| {
|
||||
let chunk = match chunk {
|
||||
Ok(chunk) => chunk,
|
||||
Err(err) => return future::ready(Some(Err(err))),
|
||||
};
|
||||
buffer.push_str(&chunk);
|
||||
|
||||
if buffer.len() > "<|S|".len() && buffer.starts_with("<|S|") {
|
||||
includes_start_or_end_span = true;
|
||||
|
||||
buffer = buffer
|
||||
.strip_prefix("<|S|>")
|
||||
.or_else(|| buffer.strip_prefix("<|S|"))
|
||||
.unwrap_or(&buffer)
|
||||
.to_string();
|
||||
} else if buffer.ends_with("|E|>") {
|
||||
includes_start_or_end_span = true;
|
||||
} else if buffer.starts_with("<|")
|
||||
|| buffer.starts_with("<|S")
|
||||
|| buffer.starts_with("<|S|")
|
||||
|| buffer.ends_with("|")
|
||||
|| buffer.ends_with("|E")
|
||||
|| buffer.ends_with("|E|")
|
||||
{
|
||||
return future::ready(None);
|
||||
}
|
||||
|
||||
if first_line {
|
||||
if buffer == "" || buffer == "`" || buffer == "``" {
|
||||
return future::ready(None);
|
||||
} else if buffer.starts_with("```") {
|
||||
starts_with_markdown_codeblock = true;
|
||||
if let Some(newline_ix) = buffer.find('\n') {
|
||||
buffer.replace_range(..newline_ix + 1, "");
|
||||
first_line = false;
|
||||
} else {
|
||||
return future::ready(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut text = buffer.to_string();
|
||||
if starts_with_markdown_codeblock {
|
||||
text = text
|
||||
.strip_suffix("\n```\n")
|
||||
.or_else(|| text.strip_suffix("\n```"))
|
||||
.or_else(|| text.strip_suffix("\n``"))
|
||||
.or_else(|| text.strip_suffix("\n`"))
|
||||
.or_else(|| text.strip_suffix('\n'))
|
||||
.unwrap_or(&text)
|
||||
.to_string();
|
||||
}
|
||||
|
||||
if includes_start_or_end_span {
|
||||
text = text
|
||||
.strip_suffix("|E|>")
|
||||
.or_else(|| text.strip_suffix("E|>"))
|
||||
.or_else(|| text.strip_prefix("|>"))
|
||||
.or_else(|| text.strip_prefix(">"))
|
||||
.unwrap_or(&text)
|
||||
.to_string();
|
||||
};
|
||||
|
||||
if text.contains('\n') {
|
||||
first_line = false;
|
||||
}
|
||||
|
||||
let remainder = buffer.split_off(text.len());
|
||||
let result = if buffer.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Ok(buffer.clone()))
|
||||
};
|
||||
|
||||
buffer = remainder;
|
||||
future::ready(result)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use ai::test::FakeCompletionProvider;
|
||||
use futures::stream::{self};
|
||||
use gpui::{Context, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point};
|
||||
use rand::prelude::*;
|
||||
use serde::Serialize;
|
||||
use settings::SettingsStore;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DummyCompletionRequest {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl CompletionRequest for DummyCompletionRequest {
|
||||
fn data(&self) -> serde_json::Result<String> {
|
||||
serde_json::to_string(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
fn main() {
|
||||
let x = 0;
|
||||
for _ in 0..10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
|
||||
});
|
||||
let provider = Arc::new(FakeCompletionProvider::new());
|
||||
let codegen = cx.build_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
CodegenKind::Transform { range },
|
||||
provider.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let request = Box::new(DummyCompletionRequest {
|
||||
name: "test".to_string(),
|
||||
});
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
" let mut x = 0;\n",
|
||||
" while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
" }",
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
println!("CHUNK: {:?}", &chunk);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_autoindent_when_generating_past_indentation(
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
fn main() {
|
||||
le
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let position = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 6))
|
||||
});
|
||||
let provider = Arc::new(FakeCompletionProvider::new());
|
||||
let codegen = cx.build_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
CodegenKind::Generate { position },
|
||||
provider.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let request = Box::new(DummyCompletionRequest {
|
||||
name: "test".to_string(),
|
||||
});
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
"t mut x = 0;\n",
|
||||
"while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
"}", //
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_autoindent_when_generating_before_indentation(
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = concat!(
|
||||
"fn main() {\n",
|
||||
" \n",
|
||||
"}\n" //
|
||||
);
|
||||
let buffer =
|
||||
cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let position = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 2))
|
||||
});
|
||||
let provider = Arc::new(FakeCompletionProvider::new());
|
||||
let codegen = cx.build_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
CodegenKind::Generate { position },
|
||||
provider.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let request = Box::new(DummyCompletionRequest {
|
||||
name: "test".to_string(),
|
||||
});
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
"let mut x = 0;\n",
|
||||
"while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
"}", //
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
println!("{:?}", &chunk);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_strip_invalid_spans_from_codeblock() {
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("Lorem ipsum dolor", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks(
|
||||
"```html\n```js\nLorem ipsum dolor\n```\n```",
|
||||
2
|
||||
))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"```js\nLorem ipsum dolor\n```"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("``\nLorem ipsum dolor\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"``\nLorem ipsum dolor\n```"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("<|S|Lorem ipsum|E|>", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("<|S|>Lorem ipsum", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\n<|S|>Lorem ipsum\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\n<|S|Lorem ipsum|E|>\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
fn chunks(text: &str, size: usize) -> impl Stream<Item = Result<String>> {
|
||||
stream::iter(
|
||||
text.chars()
|
||||
.collect::<Vec<_>>()
|
||||
.chunks(size)
|
||||
.map(|chunk| Ok(chunk.iter().collect::<String>()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
(call_expression) @indent
|
||||
(field_expression) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
389
crates/assistant2/src/prompts.rs
Normal file
389
crates/assistant2/src/prompts.rs
Normal file
@ -0,0 +1,389 @@
|
||||
use ai::models::LanguageModel;
|
||||
use ai::prompts::base::{PromptArguments, PromptChain, PromptPriority, PromptTemplate};
|
||||
use ai::prompts::file_context::FileContext;
|
||||
use ai::prompts::generate::GenerateInlineContent;
|
||||
use ai::prompts::preamble::EngineerPreamble;
|
||||
use ai::prompts::repository_context::{PromptCodeSnippet, RepositoryContext};
|
||||
use ai::providers::open_ai::OpenAILanguageModel;
|
||||
use language::{BufferSnapshot, OffsetRangeExt, ToOffset};
|
||||
use std::cmp::{self, Reverse};
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> String {
|
||||
#[derive(Debug)]
|
||||
struct Match {
|
||||
collapse: Range<usize>,
|
||||
keep: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
let selected_range = selected_range.to_offset(buffer);
|
||||
let mut ts_matches = buffer.matches(0..buffer.len(), |grammar| {
|
||||
Some(&grammar.embedding_config.as_ref()?.query)
|
||||
});
|
||||
let configs = ts_matches
|
||||
.grammars()
|
||||
.iter()
|
||||
.map(|g| g.embedding_config.as_ref().unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let mut matches = Vec::new();
|
||||
while let Some(mat) = ts_matches.peek() {
|
||||
let config = &configs[mat.grammar_index];
|
||||
if let Some(collapse) = mat.captures.iter().find_map(|cap| {
|
||||
if Some(cap.index) == config.collapse_capture_ix {
|
||||
Some(cap.node.byte_range())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
let mut keep = Vec::new();
|
||||
for capture in mat.captures.iter() {
|
||||
if Some(capture.index) == config.keep_capture_ix {
|
||||
keep.push(capture.node.byte_range());
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ts_matches.advance();
|
||||
matches.push(Match { collapse, keep });
|
||||
} else {
|
||||
ts_matches.advance();
|
||||
}
|
||||
}
|
||||
matches.sort_unstable_by_key(|mat| (mat.collapse.start, Reverse(mat.collapse.end)));
|
||||
let mut matches = matches.into_iter().peekable();
|
||||
|
||||
let mut summary = String::new();
|
||||
let mut offset = 0;
|
||||
let mut flushed_selection = false;
|
||||
while let Some(mat) = matches.next() {
|
||||
// Keep extending the collapsed range if the next match surrounds
|
||||
// the current one.
|
||||
while let Some(next_mat) = matches.peek() {
|
||||
if mat.collapse.start <= next_mat.collapse.start
|
||||
&& mat.collapse.end >= next_mat.collapse.end
|
||||
{
|
||||
matches.next().unwrap();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if offset > mat.collapse.start {
|
||||
// Skip collapsed nodes that have already been summarized.
|
||||
offset = cmp::max(offset, mat.collapse.end);
|
||||
continue;
|
||||
}
|
||||
|
||||
if offset <= selected_range.start && selected_range.start <= mat.collapse.end {
|
||||
if !flushed_selection {
|
||||
// The collapsed node ends after the selection starts, so we'll flush the selection first.
|
||||
summary.extend(buffer.text_for_range(offset..selected_range.start));
|
||||
summary.push_str("<|S|");
|
||||
if selected_range.end == selected_range.start {
|
||||
summary.push_str(">");
|
||||
} else {
|
||||
summary.extend(buffer.text_for_range(selected_range.clone()));
|
||||
summary.push_str("|E|>");
|
||||
}
|
||||
offset = selected_range.end;
|
||||
flushed_selection = true;
|
||||
}
|
||||
|
||||
// If the selection intersects the collapsed node, we won't collapse it.
|
||||
if selected_range.end >= mat.collapse.start {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
summary.extend(buffer.text_for_range(offset..mat.collapse.start));
|
||||
for keep in mat.keep {
|
||||
summary.extend(buffer.text_for_range(keep));
|
||||
}
|
||||
offset = mat.collapse.end;
|
||||
}
|
||||
|
||||
// Flush selection if we haven't already done so.
|
||||
if !flushed_selection && offset <= selected_range.start {
|
||||
summary.extend(buffer.text_for_range(offset..selected_range.start));
|
||||
summary.push_str("<|S|");
|
||||
if selected_range.end == selected_range.start {
|
||||
summary.push_str(">");
|
||||
} else {
|
||||
summary.extend(buffer.text_for_range(selected_range.clone()));
|
||||
summary.push_str("|E|>");
|
||||
}
|
||||
offset = selected_range.end;
|
||||
}
|
||||
|
||||
summary.extend(buffer.text_for_range(offset..buffer.len()));
|
||||
summary
|
||||
}
|
||||
|
||||
pub fn generate_content_prompt(
|
||||
user_prompt: String,
|
||||
language_name: Option<&str>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
search_results: Vec<PromptCodeSnippet>,
|
||||
model: &str,
|
||||
project_name: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
// Using new Prompt Templates
|
||||
let openai_model: Arc<dyn LanguageModel> = Arc::new(OpenAILanguageModel::load(model));
|
||||
let lang_name = if let Some(language_name) = language_name {
|
||||
Some(language_name.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let args = PromptArguments {
|
||||
model: openai_model,
|
||||
language_name: lang_name.clone(),
|
||||
project_name,
|
||||
snippets: search_results.clone(),
|
||||
reserved_tokens: 1000,
|
||||
buffer: Some(buffer),
|
||||
selected_range: Some(range),
|
||||
user_prompt: Some(user_prompt.clone()),
|
||||
};
|
||||
|
||||
let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
|
||||
(PromptPriority::Mandatory, Box::new(EngineerPreamble {})),
|
||||
(
|
||||
PromptPriority::Ordered { order: 1 },
|
||||
Box::new(RepositoryContext {}),
|
||||
),
|
||||
(
|
||||
PromptPriority::Ordered { order: 0 },
|
||||
Box::new(FileContext {}),
|
||||
),
|
||||
(
|
||||
PromptPriority::Mandatory,
|
||||
Box::new(GenerateInlineContent {}),
|
||||
),
|
||||
];
|
||||
let chain = PromptChain::new(args, templates);
|
||||
let (prompt, _) = chain.generate(true)?;
|
||||
|
||||
anyhow::Ok(prompt)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{AppContext, Context};
|
||||
use indoc::indoc;
|
||||
use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point};
|
||||
use settings::SettingsStore;
|
||||
|
||||
pub(crate) fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)
|
||||
.with_embedding_query(
|
||||
r#"
|
||||
(
|
||||
[(line_comment) (attribute_item)]* @context
|
||||
.
|
||||
[
|
||||
(struct_item
|
||||
name: (_) @name)
|
||||
|
||||
(enum_item
|
||||
name: (_) @name)
|
||||
|
||||
(impl_item
|
||||
trait: (_)? @name
|
||||
"for"? @name
|
||||
type: (_) @name)
|
||||
|
||||
(trait_item
|
||||
name: (_) @name)
|
||||
|
||||
(function_item
|
||||
name: (_) @name
|
||||
body: (block
|
||||
"{" @keep
|
||||
"}" @keep) @collapse)
|
||||
|
||||
(macro_definition
|
||||
name: (_) @name)
|
||||
] @item
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_outline_for_prompt(cx: &mut AppContext) {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language_settings::init(cx);
|
||||
let text = indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
Self { a, b }
|
||||
}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {
|
||||
self.a
|
||||
}
|
||||
|
||||
pub fn b(&self) -> usize {
|
||||
self.b
|
||||
}
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(1, 4)..Point::new(1, 4)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
<|S|>a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(8, 12)..Point::new(8, 14)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {
|
||||
let <|S|a |E|>= 1;
|
||||
let b = 2;
|
||||
Self { a, b }
|
||||
}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(6, 0)..Point::new(6, 0)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
<|S|>
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(21, 0)..Point::new(21, 0)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
<|S|>"}
|
||||
);
|
||||
|
||||
// Ensure nested functions get collapsed properly.
|
||||
let text = indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
Self { a, b }
|
||||
}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {
|
||||
let a = 30;
|
||||
fn nested() -> usize {
|
||||
3
|
||||
}
|
||||
self.a + nested()
|
||||
}
|
||||
|
||||
pub fn b(&self) -> usize {
|
||||
self.b
|
||||
}
|
||||
}
|
||||
"};
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(0, 0)..Point::new(0, 0)),
|
||||
indoc! {"
|
||||
<|S|>struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
}
|
293
crates/assistant2/src/streaming_diff.rs
Normal file
293
crates/assistant2/src/streaming_diff.rs
Normal file
@ -0,0 +1,293 @@
|
||||
use collections::HashMap;
|
||||
use ordered_float::OrderedFloat;
|
||||
use std::{
|
||||
cmp,
|
||||
fmt::{self, Debug},
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
struct Matrix {
|
||||
cells: Vec<f64>,
|
||||
rows: usize,
|
||||
cols: usize,
|
||||
}
|
||||
|
||||
impl Matrix {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
cells: Vec::new(),
|
||||
rows: 0,
|
||||
cols: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn resize(&mut self, rows: usize, cols: usize) {
|
||||
self.cells.resize(rows * cols, 0.);
|
||||
self.rows = rows;
|
||||
self.cols = cols;
|
||||
}
|
||||
|
||||
fn get(&self, row: usize, col: usize) -> f64 {
|
||||
if row >= self.rows {
|
||||
panic!("row out of bounds")
|
||||
}
|
||||
|
||||
if col >= self.cols {
|
||||
panic!("col out of bounds")
|
||||
}
|
||||
self.cells[col * self.rows + row]
|
||||
}
|
||||
|
||||
fn set(&mut self, row: usize, col: usize, value: f64) {
|
||||
if row >= self.rows {
|
||||
panic!("row out of bounds")
|
||||
}
|
||||
|
||||
if col >= self.cols {
|
||||
panic!("col out of bounds")
|
||||
}
|
||||
|
||||
self.cells[col * self.rows + row] = value;
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Matrix {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(f)?;
|
||||
for i in 0..self.rows {
|
||||
for j in 0..self.cols {
|
||||
write!(f, "{:5}", self.get(i, j))?;
|
||||
}
|
||||
writeln!(f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Hunk {
|
||||
Insert { text: String },
|
||||
Remove { len: usize },
|
||||
Keep { len: usize },
|
||||
}
|
||||
|
||||
pub struct StreamingDiff {
|
||||
old: Vec<char>,
|
||||
new: Vec<char>,
|
||||
scores: Matrix,
|
||||
old_text_ix: usize,
|
||||
new_text_ix: usize,
|
||||
equal_runs: HashMap<(usize, usize), u32>,
|
||||
}
|
||||
|
||||
impl StreamingDiff {
|
||||
const INSERTION_SCORE: f64 = -1.;
|
||||
const DELETION_SCORE: f64 = -20.;
|
||||
const EQUALITY_BASE: f64 = 1.8;
|
||||
const MAX_EQUALITY_EXPONENT: i32 = 16;
|
||||
|
||||
pub fn new(old: String) -> Self {
|
||||
let old = old.chars().collect::<Vec<_>>();
|
||||
let mut scores = Matrix::new();
|
||||
scores.resize(old.len() + 1, 1);
|
||||
for i in 0..=old.len() {
|
||||
scores.set(i, 0, i as f64 * Self::DELETION_SCORE);
|
||||
}
|
||||
Self {
|
||||
old,
|
||||
new: Vec::new(),
|
||||
scores,
|
||||
old_text_ix: 0,
|
||||
new_text_ix: 0,
|
||||
equal_runs: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_new(&mut self, text: &str) -> Vec<Hunk> {
|
||||
self.new.extend(text.chars());
|
||||
self.scores.resize(self.old.len() + 1, self.new.len() + 1);
|
||||
|
||||
for j in self.new_text_ix + 1..=self.new.len() {
|
||||
self.scores.set(0, j, j as f64 * Self::INSERTION_SCORE);
|
||||
for i in 1..=self.old.len() {
|
||||
let insertion_score = self.scores.get(i, j - 1) + Self::INSERTION_SCORE;
|
||||
let deletion_score = self.scores.get(i - 1, j) + Self::DELETION_SCORE;
|
||||
let equality_score = if self.old[i - 1] == self.new[j - 1] {
|
||||
let mut equal_run = self.equal_runs.get(&(i - 1, j - 1)).copied().unwrap_or(0);
|
||||
equal_run += 1;
|
||||
self.equal_runs.insert((i, j), equal_run);
|
||||
|
||||
let exponent = cmp::min(equal_run as i32 / 4, Self::MAX_EQUALITY_EXPONENT);
|
||||
self.scores.get(i - 1, j - 1) + Self::EQUALITY_BASE.powi(exponent)
|
||||
} else {
|
||||
f64::NEG_INFINITY
|
||||
};
|
||||
|
||||
let score = insertion_score.max(deletion_score).max(equality_score);
|
||||
self.scores.set(i, j, score);
|
||||
}
|
||||
}
|
||||
|
||||
let mut max_score = f64::NEG_INFINITY;
|
||||
let mut next_old_text_ix = self.old_text_ix;
|
||||
let next_new_text_ix = self.new.len();
|
||||
for i in self.old_text_ix..=self.old.len() {
|
||||
let score = self.scores.get(i, next_new_text_ix);
|
||||
if score > max_score {
|
||||
max_score = score;
|
||||
next_old_text_ix = i;
|
||||
}
|
||||
}
|
||||
|
||||
let hunks = self.backtrack(next_old_text_ix, next_new_text_ix);
|
||||
self.old_text_ix = next_old_text_ix;
|
||||
self.new_text_ix = next_new_text_ix;
|
||||
hunks
|
||||
}
|
||||
|
||||
fn backtrack(&self, old_text_ix: usize, new_text_ix: usize) -> Vec<Hunk> {
|
||||
let mut pending_insert: Option<Range<usize>> = None;
|
||||
let mut hunks = Vec::new();
|
||||
let mut i = old_text_ix;
|
||||
let mut j = new_text_ix;
|
||||
while (i, j) != (self.old_text_ix, self.new_text_ix) {
|
||||
let insertion_score = if j > self.new_text_ix {
|
||||
Some((i, j - 1))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let deletion_score = if i > self.old_text_ix {
|
||||
Some((i - 1, j))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let equality_score = if i > self.old_text_ix && j > self.new_text_ix {
|
||||
if self.old[i - 1] == self.new[j - 1] {
|
||||
Some((i - 1, j - 1))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (prev_i, prev_j) = [insertion_score, deletion_score, equality_score]
|
||||
.iter()
|
||||
.max_by_key(|cell| cell.map(|(i, j)| OrderedFloat(self.scores.get(i, j))))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
if prev_i == i && prev_j == j - 1 {
|
||||
if let Some(pending_insert) = pending_insert.as_mut() {
|
||||
pending_insert.start = prev_j;
|
||||
} else {
|
||||
pending_insert = Some(prev_j..j);
|
||||
}
|
||||
} else {
|
||||
if let Some(range) = pending_insert.take() {
|
||||
hunks.push(Hunk::Insert {
|
||||
text: self.new[range].iter().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
let char_len = self.old[i - 1].len_utf8();
|
||||
if prev_i == i - 1 && prev_j == j {
|
||||
if let Some(Hunk::Remove { len }) = hunks.last_mut() {
|
||||
*len += char_len;
|
||||
} else {
|
||||
hunks.push(Hunk::Remove { len: char_len })
|
||||
}
|
||||
} else {
|
||||
if let Some(Hunk::Keep { len }) = hunks.last_mut() {
|
||||
*len += char_len;
|
||||
} else {
|
||||
hunks.push(Hunk::Keep { len: char_len })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i = prev_i;
|
||||
j = prev_j;
|
||||
}
|
||||
|
||||
if let Some(range) = pending_insert.take() {
|
||||
hunks.push(Hunk::Insert {
|
||||
text: self.new[range].iter().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
hunks.reverse();
|
||||
hunks
|
||||
}
|
||||
|
||||
pub fn finish(self) -> Vec<Hunk> {
|
||||
self.backtrack(self.old.len(), self.new.len())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::env;
|
||||
|
||||
use super::*;
|
||||
use rand::prelude::*;
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_diffs(mut rng: StdRng) {
|
||||
let old_text_len = env::var("OLD_TEXT_LEN")
|
||||
.map(|i| i.parse().expect("invalid `OLD_TEXT_LEN` variable"))
|
||||
.unwrap_or(10);
|
||||
let new_text_len = env::var("NEW_TEXT_LEN")
|
||||
.map(|i| i.parse().expect("invalid `NEW_TEXT_LEN` variable"))
|
||||
.unwrap_or(10);
|
||||
|
||||
let old = util::RandomCharIter::new(&mut rng)
|
||||
.take(old_text_len)
|
||||
.collect::<String>();
|
||||
log::info!("old text: {:?}", old);
|
||||
|
||||
let mut diff = StreamingDiff::new(old.clone());
|
||||
let mut hunks = Vec::new();
|
||||
let mut new_len = 0;
|
||||
let mut new = String::new();
|
||||
while new_len < new_text_len {
|
||||
let new_chunk_len = rng.gen_range(1..=new_text_len - new_len);
|
||||
let new_chunk = util::RandomCharIter::new(&mut rng)
|
||||
.take(new_len)
|
||||
.collect::<String>();
|
||||
log::info!("new chunk: {:?}", new_chunk);
|
||||
new_len += new_chunk_len;
|
||||
new.push_str(&new_chunk);
|
||||
let new_hunks = diff.push_new(&new_chunk);
|
||||
log::info!("hunks: {:?}", new_hunks);
|
||||
hunks.extend(new_hunks);
|
||||
}
|
||||
let final_hunks = diff.finish();
|
||||
log::info!("final hunks: {:?}", final_hunks);
|
||||
hunks.extend(final_hunks);
|
||||
|
||||
log::info!("new text: {:?}", new);
|
||||
let mut old_ix = 0;
|
||||
let mut new_ix = 0;
|
||||
let mut patched = String::new();
|
||||
for hunk in hunks {
|
||||
match hunk {
|
||||
Hunk::Keep { len } => {
|
||||
assert_eq!(&old[old_ix..old_ix + len], &new[new_ix..new_ix + len]);
|
||||
patched.push_str(&old[old_ix..old_ix + len]);
|
||||
old_ix += len;
|
||||
new_ix += len;
|
||||
}
|
||||
Hunk::Remove { len } => {
|
||||
old_ix += len;
|
||||
}
|
||||
Hunk::Insert { text } => {
|
||||
assert_eq!(text, &new[new_ix..new_ix + text.len()]);
|
||||
patched.push_str(&text);
|
||||
new_ix += text.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(patched, new);
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
use gpui::{
|
||||
Component, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
|
||||
Div, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
|
||||
ViewContext, WeakView,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use theme::ActiveTheme;
|
||||
use ui::{ButtonCommon, ButtonLike, ButtonStyle, Clickable, Disableable, Label};
|
||||
use ui::{prelude::*, ButtonLike, ButtonStyle, Label};
|
||||
use workspace::{
|
||||
item::{ItemEvent, ItemHandle},
|
||||
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
@ -36,54 +36,51 @@ impl EventEmitter<Event> for Breadcrumbs {}
|
||||
impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
|
||||
|
||||
impl Render for Breadcrumbs {
|
||||
type Element = Component<ButtonLike>;
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let button = ButtonLike::new("breadcrumbs")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.disabled(true);
|
||||
let element = h_stack().text_ui();
|
||||
|
||||
let active_item = match &self.active_item {
|
||||
Some(active_item) => active_item,
|
||||
None => return button.into_element(),
|
||||
let Some(active_item) = &self
|
||||
.active_item
|
||||
.as_ref()
|
||||
.filter(|item| item.downcast::<editor::Editor>().is_some())
|
||||
else {
|
||||
return element;
|
||||
};
|
||||
let not_editor = active_item.downcast::<editor::Editor>().is_none();
|
||||
|
||||
let breadcrumbs = match active_item.breadcrumbs(cx.theme(), cx) {
|
||||
Some(breadcrumbs) => breadcrumbs,
|
||||
None => return button.into_element(),
|
||||
}
|
||||
.into_iter()
|
||||
.map(|breadcrumb| {
|
||||
StyledText::new(breadcrumb.text)
|
||||
.with_highlights(&cx.text_style(), breadcrumb.highlights.unwrap_or_default())
|
||||
let Some(segments) = active_item.breadcrumbs(cx.theme(), cx) else {
|
||||
return element;
|
||||
};
|
||||
|
||||
let highlighted_segments = segments.into_iter().map(|segment| {
|
||||
StyledText::new(segment.text)
|
||||
.with_highlights(&cx.text_style(), segment.highlights.unwrap_or_default())
|
||||
.into_any()
|
||||
});
|
||||
let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
|
||||
Label::new("›").into_any_element()
|
||||
});
|
||||
|
||||
let button = button.children(Itertools::intersperse_with(breadcrumbs, || {
|
||||
Label::new(" › ").into_any_element()
|
||||
}));
|
||||
|
||||
if not_editor || !self.pane_focused {
|
||||
return button.into_element();
|
||||
}
|
||||
|
||||
// let this = cx.view().downgrade();
|
||||
button
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(false)
|
||||
.on_click(move |_, _cx| {
|
||||
todo!("outline::toggle");
|
||||
// this.update(cx, |this, cx| {
|
||||
// if let Some(workspace) = this.workspace.upgrade() {
|
||||
// workspace.update(cx, |_workspace, _cx| {
|
||||
// outline::toggle(workspace, &Default::default(), cx)
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
// .ok();
|
||||
})
|
||||
.into_element()
|
||||
element.child(
|
||||
ButtonLike::new("toggle outline view")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(h_stack().gap_1().children(breadcrumbs))
|
||||
// We disable the button when it is not focused
|
||||
// due to ... @julia what was the reason again?
|
||||
.disabled(!self.pane_focused)
|
||||
.on_click(move |_, _cx| {
|
||||
todo!("outline::toggle");
|
||||
// this.update(cx, |this, cx| {
|
||||
// if let Some(workspace) = this.workspace.upgrade() {
|
||||
// workspace.update(cx, |_workspace, _cx| {
|
||||
// outline::toggle(workspace, &Default::default(), cx)
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
// .ok();
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.29.1"
|
||||
version = "0.30.0"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
@ -81,7 +81,7 @@ settings = { package = "settings2", path = "../settings2", features = ["test-sup
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
|
||||
collab_ui = { path = "../collab_ui", features = ["test-support"] }
|
||||
collab_ui = { path = "../collab_ui2", package = "collab_ui2", features = ["test-support"] }
|
||||
|
||||
async-trait.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -13,7 +13,7 @@ use client::{
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::FakeFs;
|
||||
use futures::{channel::oneshot, StreamExt as _};
|
||||
use gpui::{BackgroundExecutor, Context, Model, TestAppContext, WindowHandle};
|
||||
use gpui::{BackgroundExecutor, Context, Model, TestAppContext, View, VisualTestContext};
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
|
||||
@ -602,14 +602,12 @@ impl TestClient {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
//todo(workspace)
|
||||
#[allow(dead_code)]
|
||||
pub fn build_workspace(
|
||||
&self,
|
||||
pub fn build_workspace<'a>(
|
||||
&'a self,
|
||||
project: &Model<Project>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> WindowHandle<Workspace> {
|
||||
cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
|
||||
cx: &'a mut TestAppContext,
|
||||
) -> (View<Workspace>, &'a mut VisualTestContext) {
|
||||
cx.add_window_view(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,454 +1,444 @@
|
||||
// use anyhow::{anyhow, Result};
|
||||
// use call::report_call_event_for_channel;
|
||||
// use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
|
||||
// use client::{
|
||||
// proto::{self, PeerId},
|
||||
// Collaborator, ParticipantIndex,
|
||||
// };
|
||||
// use collections::HashMap;
|
||||
// use editor::{CollaborationHub, Editor};
|
||||
// use gpui::{
|
||||
// actions,
|
||||
// elements::{ChildView, Label},
|
||||
// geometry::vector::Vector2F,
|
||||
// AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
|
||||
// ViewContext, ViewHandle,
|
||||
// };
|
||||
// use project::Project;
|
||||
// use smallvec::SmallVec;
|
||||
// use std::{
|
||||
// any::{Any, TypeId},
|
||||
// sync::Arc,
|
||||
// };
|
||||
// use util::ResultExt;
|
||||
// use workspace::{
|
||||
// item::{FollowableItem, Item, ItemEvent, ItemHandle},
|
||||
// register_followable_item,
|
||||
// searchable::SearchableItemHandle,
|
||||
// ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
|
||||
// };
|
||||
use anyhow::Result;
|
||||
use call::report_call_event_for_channel;
|
||||
use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
|
||||
use client::{
|
||||
proto::{self, PeerId},
|
||||
Collaborator, ParticipantIndex,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use editor::{CollaborationHub, Editor, EditorEvent};
|
||||
use gpui::{
|
||||
actions, AnyElement, AnyView, AppContext, Entity as _, EventEmitter, FocusableView,
|
||||
IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View, ViewContext,
|
||||
VisualContext as _, WindowContext,
|
||||
};
|
||||
use project::Project;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
sync::Arc,
|
||||
};
|
||||
use ui::Label;
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
item::{FollowableItem, Item, ItemEvent, ItemHandle},
|
||||
register_followable_item,
|
||||
searchable::SearchableItemHandle,
|
||||
ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
// actions!(channel_view, [Deploy]);
|
||||
actions!(Deploy);
|
||||
|
||||
// pub fn init(cx: &mut AppContext) {
|
||||
// register_followable_item::<ChannelView>(cx)
|
||||
// }
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
register_followable_item::<ChannelView>(cx)
|
||||
}
|
||||
|
||||
// pub struct ChannelView {
|
||||
// pub editor: ViewHandle<Editor>,
|
||||
// project: ModelHandle<Project>,
|
||||
// channel_store: ModelHandle<ChannelStore>,
|
||||
// channel_buffer: ModelHandle<ChannelBuffer>,
|
||||
// remote_id: Option<ViewId>,
|
||||
// _editor_event_subscription: Subscription,
|
||||
// }
|
||||
pub struct ChannelView {
|
||||
pub editor: View<Editor>,
|
||||
project: Model<Project>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_buffer: Model<ChannelBuffer>,
|
||||
remote_id: Option<ViewId>,
|
||||
_editor_event_subscription: Subscription,
|
||||
}
|
||||
|
||||
// impl ChannelView {
|
||||
// pub fn open(
|
||||
// channel_id: ChannelId,
|
||||
// workspace: ViewHandle<Workspace>,
|
||||
// cx: &mut AppContext,
|
||||
// ) -> Task<Result<ViewHandle<Self>>> {
|
||||
// let pane = workspace.read(cx).active_pane().clone();
|
||||
// let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
|
||||
// cx.spawn(|mut cx| async move {
|
||||
// let channel_view = channel_view.await?;
|
||||
// pane.update(&mut cx, |pane, cx| {
|
||||
// report_call_event_for_channel(
|
||||
// "open channel notes",
|
||||
// channel_id,
|
||||
// &workspace.read(cx).app_state().client,
|
||||
// cx,
|
||||
// );
|
||||
// pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
|
||||
// });
|
||||
// anyhow::Ok(channel_view)
|
||||
// })
|
||||
// }
|
||||
impl ChannelView {
|
||||
pub fn open(
|
||||
channel_id: ChannelId,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
let pane = workspace.read(cx).active_pane().clone();
|
||||
let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
|
||||
cx.spawn(|mut cx| async move {
|
||||
let channel_view = channel_view.await?;
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
report_call_event_for_channel(
|
||||
"open channel notes",
|
||||
channel_id,
|
||||
&workspace.read(cx).app_state().client,
|
||||
cx,
|
||||
);
|
||||
pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
|
||||
})?;
|
||||
anyhow::Ok(channel_view)
|
||||
})
|
||||
}
|
||||
|
||||
// pub fn open_in_pane(
|
||||
// channel_id: ChannelId,
|
||||
// pane: ViewHandle<Pane>,
|
||||
// workspace: ViewHandle<Workspace>,
|
||||
// cx: &mut AppContext,
|
||||
// ) -> Task<Result<ViewHandle<Self>>> {
|
||||
// let workspace = workspace.read(cx);
|
||||
// let project = workspace.project().to_owned();
|
||||
// let channel_store = ChannelStore::global(cx);
|
||||
// let language_registry = workspace.app_state().languages.clone();
|
||||
// let markdown = language_registry.language_for_name("Markdown");
|
||||
// let channel_buffer =
|
||||
// channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
|
||||
pub fn open_in_pane(
|
||||
channel_id: ChannelId,
|
||||
pane: View<Pane>,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().to_owned();
|
||||
let channel_store = ChannelStore::global(cx);
|
||||
let language_registry = workspace.app_state().languages.clone();
|
||||
let markdown = language_registry.language_for_name("Markdown");
|
||||
let channel_buffer =
|
||||
channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
|
||||
|
||||
// cx.spawn(|mut cx| async move {
|
||||
// let channel_buffer = channel_buffer.await?;
|
||||
// let markdown = markdown.await.log_err();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let channel_buffer = channel_buffer.await?;
|
||||
let markdown = markdown.await.log_err();
|
||||
|
||||
// channel_buffer.update(&mut cx, |buffer, cx| {
|
||||
// buffer.buffer().update(cx, |buffer, cx| {
|
||||
// buffer.set_language_registry(language_registry);
|
||||
// if let Some(markdown) = markdown {
|
||||
// buffer.set_language(Some(markdown), cx);
|
||||
// }
|
||||
// })
|
||||
// });
|
||||
channel_buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, cx| {
|
||||
buffer.set_language_registry(language_registry);
|
||||
if let Some(markdown) = markdown {
|
||||
buffer.set_language(Some(markdown), cx);
|
||||
}
|
||||
})
|
||||
})?;
|
||||
|
||||
// pane.update(&mut cx, |pane, cx| {
|
||||
// let buffer_id = channel_buffer.read(cx).remote_id(cx);
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
let buffer_id = channel_buffer.read(cx).remote_id(cx);
|
||||
|
||||
// let existing_view = pane
|
||||
// .items_of_type::<Self>()
|
||||
// .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
|
||||
let existing_view = pane
|
||||
.items_of_type::<Self>()
|
||||
.find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
|
||||
|
||||
// // If this channel buffer is already open in this pane, just return it.
|
||||
// if let Some(existing_view) = existing_view.clone() {
|
||||
// if existing_view.read(cx).channel_buffer == channel_buffer {
|
||||
// return existing_view;
|
||||
// }
|
||||
// }
|
||||
// If this channel buffer is already open in this pane, just return it.
|
||||
if let Some(existing_view) = existing_view.clone() {
|
||||
if existing_view.read(cx).channel_buffer == channel_buffer {
|
||||
return existing_view;
|
||||
}
|
||||
}
|
||||
|
||||
// let view = cx.add_view(|cx| {
|
||||
// let mut this = Self::new(project, channel_store, channel_buffer, cx);
|
||||
// this.acknowledge_buffer_version(cx);
|
||||
// this
|
||||
// });
|
||||
let view = cx.build_view(|cx| {
|
||||
let mut this = Self::new(project, channel_store, channel_buffer, cx);
|
||||
this.acknowledge_buffer_version(cx);
|
||||
this
|
||||
});
|
||||
|
||||
// // If the pane contained a disconnected view for this channel buffer,
|
||||
// // replace that.
|
||||
// if let Some(existing_item) = existing_view {
|
||||
// if let Some(ix) = pane.index_for_item(&existing_item) {
|
||||
// pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx)
|
||||
// .detach();
|
||||
// pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
|
||||
// }
|
||||
// }
|
||||
// If the pane contained a disconnected view for this channel buffer,
|
||||
// replace that.
|
||||
if let Some(existing_item) = existing_view {
|
||||
if let Some(ix) = pane.index_for_item(&existing_item) {
|
||||
pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx)
|
||||
.detach();
|
||||
pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
|
||||
}
|
||||
}
|
||||
|
||||
// view
|
||||
// })
|
||||
// .ok_or_else(|| anyhow!("pane was dropped"))
|
||||
// })
|
||||
// }
|
||||
view
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// pub fn new(
|
||||
// project: ModelHandle<Project>,
|
||||
// channel_store: ModelHandle<ChannelStore>,
|
||||
// channel_buffer: ModelHandle<ChannelBuffer>,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> Self {
|
||||
// let buffer = channel_buffer.read(cx).buffer();
|
||||
// let editor = cx.add_view(|cx| {
|
||||
// let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
// editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
|
||||
// channel_buffer.clone(),
|
||||
// )));
|
||||
// editor.set_read_only(
|
||||
// !channel_buffer
|
||||
// .read(cx)
|
||||
// .channel(cx)
|
||||
// .is_some_and(|c| c.can_edit_notes()),
|
||||
// );
|
||||
// editor
|
||||
// });
|
||||
// let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
|
||||
pub fn new(
|
||||
project: Model<Project>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_buffer: Model<ChannelBuffer>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let buffer = channel_buffer.read(cx).buffer();
|
||||
let editor = cx.build_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
|
||||
channel_buffer.clone(),
|
||||
)));
|
||||
editor.set_read_only(
|
||||
!channel_buffer
|
||||
.read(cx)
|
||||
.channel(cx)
|
||||
.is_some_and(|c| c.can_edit_notes()),
|
||||
);
|
||||
editor
|
||||
});
|
||||
let _editor_event_subscription =
|
||||
cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone()));
|
||||
|
||||
// cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
|
||||
// .detach();
|
||||
cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
|
||||
.detach();
|
||||
|
||||
// Self {
|
||||
// editor,
|
||||
// project,
|
||||
// channel_store,
|
||||
// channel_buffer,
|
||||
// remote_id: None,
|
||||
// _editor_event_subscription,
|
||||
// }
|
||||
// }
|
||||
Self {
|
||||
editor,
|
||||
project,
|
||||
channel_store,
|
||||
channel_buffer,
|
||||
remote_id: None,
|
||||
_editor_event_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
|
||||
// self.channel_buffer.read(cx).channel(cx)
|
||||
// }
|
||||
pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
|
||||
self.channel_buffer.read(cx).channel(cx)
|
||||
}
|
||||
|
||||
// fn handle_channel_buffer_event(
|
||||
// &mut self,
|
||||
// _: ModelHandle<ChannelBuffer>,
|
||||
// event: &ChannelBufferEvent,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) {
|
||||
// match event {
|
||||
// ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
|
||||
// editor.set_read_only(true);
|
||||
// cx.notify();
|
||||
// }),
|
||||
// ChannelBufferEvent::ChannelChanged => {
|
||||
// self.editor.update(cx, |editor, cx| {
|
||||
// editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
|
||||
// cx.emit(editor::Event::TitleChanged);
|
||||
// cx.notify()
|
||||
// });
|
||||
// }
|
||||
// ChannelBufferEvent::BufferEdited => {
|
||||
// if cx.is_self_focused() || self.editor.is_focused(cx) {
|
||||
// self.acknowledge_buffer_version(cx);
|
||||
// } else {
|
||||
// self.channel_store.update(cx, |store, cx| {
|
||||
// let channel_buffer = self.channel_buffer.read(cx);
|
||||
// store.notes_changed(
|
||||
// channel_buffer.channel_id,
|
||||
// channel_buffer.epoch(),
|
||||
// &channel_buffer.buffer().read(cx).version(),
|
||||
// cx,
|
||||
// )
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// ChannelBufferEvent::CollaboratorsChanged => {}
|
||||
// }
|
||||
// }
|
||||
fn handle_channel_buffer_event(
|
||||
&mut self,
|
||||
_: Model<ChannelBuffer>,
|
||||
event: &ChannelBufferEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
|
||||
editor.set_read_only(true);
|
||||
cx.notify();
|
||||
}),
|
||||
ChannelBufferEvent::ChannelChanged => {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
|
||||
cx.emit(editor::EditorEvent::TitleChanged);
|
||||
cx.notify()
|
||||
});
|
||||
}
|
||||
ChannelBufferEvent::BufferEdited => {
|
||||
if self.editor.read(cx).is_focused(cx) {
|
||||
self.acknowledge_buffer_version(cx);
|
||||
} else {
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
store.notes_changed(
|
||||
channel_buffer.channel_id,
|
||||
channel_buffer.epoch(),
|
||||
&channel_buffer.buffer().read(cx).version(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
ChannelBufferEvent::CollaboratorsChanged => {}
|
||||
}
|
||||
}
|
||||
|
||||
// fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) {
|
||||
// self.channel_store.update(cx, |store, cx| {
|
||||
// let channel_buffer = self.channel_buffer.read(cx);
|
||||
// store.acknowledge_notes_version(
|
||||
// channel_buffer.channel_id,
|
||||
// channel_buffer.epoch(),
|
||||
// &channel_buffer.buffer().read(cx).version(),
|
||||
// cx,
|
||||
// )
|
||||
// });
|
||||
// self.channel_buffer.update(cx, |buffer, cx| {
|
||||
// buffer.acknowledge_buffer_version(cx);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<ChannelView>) {
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
store.acknowledge_notes_version(
|
||||
channel_buffer.channel_id,
|
||||
channel_buffer.epoch(),
|
||||
&channel_buffer.buffer().read(cx).version(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.channel_buffer.update(cx, |buffer, cx| {
|
||||
buffer.acknowledge_buffer_version(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// impl Entity for ChannelView {
|
||||
// type Event = editor::Event;
|
||||
// }
|
||||
impl EventEmitter<EditorEvent> for ChannelView {}
|
||||
|
||||
// impl View for ChannelView {
|
||||
// fn ui_name() -> &'static str {
|
||||
// "ChannelView"
|
||||
// }
|
||||
impl Render for ChannelView {
|
||||
type Element = AnyView;
|
||||
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// ChildView::new(self.editor.as_any(), cx).into_any()
|
||||
// }
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
self.editor.clone().into()
|
||||
}
|
||||
}
|
||||
|
||||
// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// if cx.is_self_focused() {
|
||||
// self.acknowledge_buffer_version(cx);
|
||||
// cx.focus(self.editor.as_any())
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
impl FocusableView for ChannelView {
|
||||
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.editor.read(cx).focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
// impl Item for ChannelView {
|
||||
// fn act_as_type<'a>(
|
||||
// &'a self,
|
||||
// type_id: TypeId,
|
||||
// self_handle: &'a ViewHandle<Self>,
|
||||
// _: &'a AppContext,
|
||||
// ) -> Option<&'a AnyViewHandle> {
|
||||
// if type_id == TypeId::of::<Self>() {
|
||||
// Some(self_handle)
|
||||
// } else if type_id == TypeId::of::<Editor>() {
|
||||
// Some(&self.editor)
|
||||
// } else {
|
||||
// None
|
||||
// }
|
||||
// }
|
||||
impl Item for ChannelView {
|
||||
type Event = EditorEvent;
|
||||
|
||||
// fn tab_content<V: 'static>(
|
||||
// &self,
|
||||
// _: Option<usize>,
|
||||
// style: &theme::Tab,
|
||||
// cx: &gpui::AppContext,
|
||||
// ) -> AnyElement<V> {
|
||||
// let label = if let Some(channel) = self.channel(cx) {
|
||||
// match (
|
||||
// channel.can_edit_notes(),
|
||||
// self.channel_buffer.read(cx).is_connected(),
|
||||
// ) {
|
||||
// (true, true) => format!("#{}", channel.name),
|
||||
// (false, true) => format!("#{} (read-only)", channel.name),
|
||||
// (_, false) => format!("#{} (disconnected)", channel.name),
|
||||
// }
|
||||
// } else {
|
||||
// format!("channel notes (disconnected)")
|
||||
// };
|
||||
// Label::new(label, style.label.to_owned()).into_any()
|
||||
// }
|
||||
fn act_as_type<'a>(
|
||||
&'a self,
|
||||
type_id: TypeId,
|
||||
self_handle: &'a View<Self>,
|
||||
_: &'a AppContext,
|
||||
) -> Option<AnyView> {
|
||||
if type_id == TypeId::of::<Self>() {
|
||||
Some(self_handle.to_any())
|
||||
} else if type_id == TypeId::of::<Editor>() {
|
||||
Some(self.editor.to_any())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
|
||||
// Some(Self::new(
|
||||
// self.project.clone(),
|
||||
// self.channel_store.clone(),
|
||||
// self.channel_buffer.clone(),
|
||||
// cx,
|
||||
// ))
|
||||
// }
|
||||
fn tab_content(&self, _: Option<usize>, cx: &WindowContext) -> AnyElement {
|
||||
let label = if let Some(channel) = self.channel(cx) {
|
||||
match (
|
||||
channel.can_edit_notes(),
|
||||
self.channel_buffer.read(cx).is_connected(),
|
||||
) {
|
||||
(true, true) => format!("#{}", channel.name),
|
||||
(false, true) => format!("#{} (read-only)", channel.name),
|
||||
(_, false) => format!("#{} (disconnected)", channel.name),
|
||||
}
|
||||
} else {
|
||||
format!("channel notes (disconnected)")
|
||||
};
|
||||
Label::new(label).into_any_element()
|
||||
}
|
||||
|
||||
// fn is_singleton(&self, _cx: &AppContext) -> bool {
|
||||
// false
|
||||
// }
|
||||
fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<View<Self>> {
|
||||
Some(cx.build_view(|cx| {
|
||||
Self::new(
|
||||
self.project.clone(),
|
||||
self.channel_store.clone(),
|
||||
self.channel_buffer.clone(),
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
// fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
// self.editor
|
||||
// .update(cx, |editor, cx| editor.navigate(data, cx))
|
||||
// }
|
||||
fn is_singleton(&self, _cx: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
// fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
// self.editor
|
||||
// .update(cx, |editor, cx| Item::deactivated(editor, cx))
|
||||
// }
|
||||
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.navigate(data, cx))
|
||||
}
|
||||
|
||||
// fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||
// self.editor
|
||||
// .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
|
||||
// }
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| Item::deactivated(editor, cx))
|
||||
}
|
||||
|
||||
// fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
// Some(Box::new(self.editor.clone()))
|
||||
// }
|
||||
fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
|
||||
}
|
||||
|
||||
// fn show_toolbar(&self) -> bool {
|
||||
// true
|
||||
// }
|
||||
fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(self.editor.clone()))
|
||||
}
|
||||
|
||||
// fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
|
||||
// self.editor.read(cx).pixel_position_of_cursor(cx)
|
||||
// }
|
||||
fn show_toolbar(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
// fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
// editor::Editor::to_item_events(event)
|
||||
// }
|
||||
// }
|
||||
fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
|
||||
self.editor.read(cx).pixel_position_of_cursor(cx)
|
||||
}
|
||||
|
||||
// impl FollowableItem for ChannelView {
|
||||
// fn remote_id(&self) -> Option<workspace::ViewId> {
|
||||
// self.remote_id
|
||||
// }
|
||||
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
|
||||
Editor::to_item_events(event, f)
|
||||
}
|
||||
}
|
||||
|
||||
// fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
|
||||
// let channel_buffer = self.channel_buffer.read(cx);
|
||||
// if !channel_buffer.is_connected() {
|
||||
// return None;
|
||||
// }
|
||||
impl FollowableItem for ChannelView {
|
||||
fn remote_id(&self) -> Option<workspace::ViewId> {
|
||||
self.remote_id
|
||||
}
|
||||
|
||||
// Some(proto::view::Variant::ChannelView(
|
||||
// proto::view::ChannelView {
|
||||
// channel_id: channel_buffer.channel_id,
|
||||
// editor: if let Some(proto::view::Variant::Editor(proto)) =
|
||||
// self.editor.read(cx).to_state_proto(cx)
|
||||
// {
|
||||
// Some(proto)
|
||||
// } else {
|
||||
// None
|
||||
// },
|
||||
// },
|
||||
// ))
|
||||
// }
|
||||
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
if !channel_buffer.is_connected() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// fn from_state_proto(
|
||||
// pane: ViewHandle<workspace::Pane>,
|
||||
// workspace: ViewHandle<workspace::Workspace>,
|
||||
// remote_id: workspace::ViewId,
|
||||
// state: &mut Option<proto::view::Variant>,
|
||||
// cx: &mut AppContext,
|
||||
// ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
|
||||
// let Some(proto::view::Variant::ChannelView(_)) = state else {
|
||||
// return None;
|
||||
// };
|
||||
// let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
|
||||
// unreachable!()
|
||||
// };
|
||||
Some(proto::view::Variant::ChannelView(
|
||||
proto::view::ChannelView {
|
||||
channel_id: channel_buffer.channel_id,
|
||||
editor: if let Some(proto::view::Variant::Editor(proto)) =
|
||||
self.editor.read(cx).to_state_proto(cx)
|
||||
{
|
||||
Some(proto)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
// let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
|
||||
fn from_state_proto(
|
||||
pane: View<workspace::Pane>,
|
||||
workspace: View<workspace::Workspace>,
|
||||
remote_id: workspace::ViewId,
|
||||
state: &mut Option<proto::view::Variant>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<gpui::Task<anyhow::Result<View<Self>>>> {
|
||||
let Some(proto::view::Variant::ChannelView(_)) = state else {
|
||||
return None;
|
||||
};
|
||||
let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
// Some(cx.spawn(|mut cx| async move {
|
||||
// let this = open.await?;
|
||||
let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
|
||||
|
||||
// let task = this
|
||||
// .update(&mut cx, |this, cx| {
|
||||
// this.remote_id = Some(remote_id);
|
||||
Some(cx.spawn(|mut cx| async move {
|
||||
let this = open.await?;
|
||||
|
||||
// if let Some(state) = state.editor {
|
||||
// Some(this.editor.update(cx, |editor, cx| {
|
||||
// editor.apply_update_proto(
|
||||
// &this.project,
|
||||
// proto::update_view::Variant::Editor(proto::update_view::Editor {
|
||||
// selections: state.selections,
|
||||
// pending_selection: state.pending_selection,
|
||||
// scroll_top_anchor: state.scroll_top_anchor,
|
||||
// scroll_x: state.scroll_x,
|
||||
// scroll_y: state.scroll_y,
|
||||
// ..Default::default()
|
||||
// }),
|
||||
// cx,
|
||||
// )
|
||||
// }))
|
||||
// } else {
|
||||
// None
|
||||
// }
|
||||
// })
|
||||
// .ok_or_else(|| anyhow!("window was closed"))?;
|
||||
let task = this.update(&mut cx, |this, cx| {
|
||||
this.remote_id = Some(remote_id);
|
||||
|
||||
// if let Some(task) = task {
|
||||
// task.await?;
|
||||
// }
|
||||
if let Some(state) = state.editor {
|
||||
Some(this.editor.update(cx, |editor, cx| {
|
||||
editor.apply_update_proto(
|
||||
&this.project,
|
||||
proto::update_view::Variant::Editor(proto::update_view::Editor {
|
||||
selections: state.selections,
|
||||
pending_selection: state.pending_selection,
|
||||
scroll_top_anchor: state.scroll_top_anchor,
|
||||
scroll_x: state.scroll_x,
|
||||
scroll_y: state.scroll_y,
|
||||
..Default::default()
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?;
|
||||
|
||||
// Ok(this)
|
||||
// }))
|
||||
// }
|
||||
if let Some(task) = task {
|
||||
task.await?;
|
||||
}
|
||||
|
||||
// fn add_event_to_update_proto(
|
||||
// &self,
|
||||
// event: &Self::Event,
|
||||
// update: &mut Option<proto::update_view::Variant>,
|
||||
// cx: &AppContext,
|
||||
// ) -> bool {
|
||||
// self.editor
|
||||
// .read(cx)
|
||||
// .add_event_to_update_proto(event, update, cx)
|
||||
// }
|
||||
Ok(this)
|
||||
}))
|
||||
}
|
||||
|
||||
// fn apply_update_proto(
|
||||
// &mut self,
|
||||
// project: &ModelHandle<Project>,
|
||||
// message: proto::update_view::Variant,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> gpui::Task<anyhow::Result<()>> {
|
||||
// self.editor.update(cx, |editor, cx| {
|
||||
// editor.apply_update_proto(project, message, cx)
|
||||
// })
|
||||
// }
|
||||
fn add_event_to_update_proto(
|
||||
&self,
|
||||
event: &EditorEvent,
|
||||
update: &mut Option<proto::update_view::Variant>,
|
||||
cx: &WindowContext,
|
||||
) -> bool {
|
||||
self.editor
|
||||
.read(cx)
|
||||
.add_event_to_update_proto(event, update, cx)
|
||||
}
|
||||
|
||||
// fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
|
||||
// self.editor.update(cx, |editor, cx| {
|
||||
// editor.set_leader_peer_id(leader_peer_id, cx)
|
||||
// })
|
||||
// }
|
||||
fn apply_update_proto(
|
||||
&mut self,
|
||||
project: &Model<Project>,
|
||||
message: proto::update_view::Variant,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> gpui::Task<anyhow::Result<()>> {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.apply_update_proto(project, message, cx)
|
||||
})
|
||||
}
|
||||
|
||||
// fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
|
||||
// Editor::should_unfollow_on_event(event, cx)
|
||||
// }
|
||||
fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.set_leader_peer_id(leader_peer_id, cx)
|
||||
})
|
||||
}
|
||||
|
||||
// fn is_project_item(&self, _cx: &AppContext) -> bool {
|
||||
// false
|
||||
// }
|
||||
// }
|
||||
fn is_project_item(&self, _cx: &WindowContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
// struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
|
||||
fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
|
||||
Editor::to_follow_event(event)
|
||||
}
|
||||
}
|
||||
|
||||
// impl CollaborationHub for ChannelBufferCollaborationHub {
|
||||
// fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
|
||||
// self.0.read(cx).collaborators()
|
||||
// }
|
||||
struct ChannelBufferCollaborationHub(Model<ChannelBuffer>);
|
||||
|
||||
// fn user_participant_indices<'a>(
|
||||
// &self,
|
||||
// cx: &'a AppContext,
|
||||
// ) -> &'a HashMap<u64, ParticipantIndex> {
|
||||
// self.0.read(cx).user_store().read(cx).participant_indices()
|
||||
// }
|
||||
// }
|
||||
impl CollaborationHub for ChannelBufferCollaborationHub {
|
||||
fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
|
||||
self.0.read(cx).collaborators()
|
||||
}
|
||||
|
||||
fn user_participant_indices<'a>(
|
||||
&self,
|
||||
cx: &'a AppContext,
|
||||
) -> &'a HashMap<u64, ParticipantIndex> {
|
||||
self.0.read(cx).user_store().read(cx).participant_indices()
|
||||
}
|
||||
}
|
||||
|
@ -169,7 +169,7 @@ use editor::Editor;
|
||||
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, canvas, div, img, overlay, point, prelude::*, px, rems, serde_json, Action,
|
||||
actions, canvas, div, img, overlay, point, prelude::*, px, rems, serde_json, size, Action,
|
||||
AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter,
|
||||
FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, IntoElement, Length, Model,
|
||||
MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, Render, RenderOnce,
|
||||
@ -191,6 +191,7 @@ use workspace::{
|
||||
Workspace,
|
||||
};
|
||||
|
||||
use crate::channel_view::ChannelView;
|
||||
use crate::{face_pile::FacePile, CollaborationPanelSettings};
|
||||
|
||||
use self::channel_modal::ChannelModal;
|
||||
@ -1204,14 +1205,9 @@ impl CollabPanel {
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
}))
|
||||
.left_child(IconButton::new(0, Icon::Folder))
|
||||
.child(
|
||||
h_stack()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(render_tree_branch(is_last, cx))
|
||||
.child(Label::new(project_name.clone())),
|
||||
)
|
||||
.left_child(render_tree_branch(is_last, cx))
|
||||
.child(IconButton::new(0, Icon::Folder))
|
||||
.child(Label::new(project_name.clone()))
|
||||
.tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
|
||||
|
||||
// enum JoinProject {}
|
||||
@ -1299,70 +1295,20 @@ impl CollabPanel {
|
||||
is_last: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
// enum OpenSharedScreen {}
|
||||
let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
|
||||
|
||||
// let host_avatar_width = theme
|
||||
// .contact_avatar
|
||||
// .width
|
||||
// .or(theme.contact_avatar.height)
|
||||
// .unwrap_or(0.);
|
||||
// let tree_branch = theme.tree_branch;
|
||||
|
||||
// let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
|
||||
// peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
|
||||
// cx,
|
||||
// |mouse_state, cx| {
|
||||
// let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
|
||||
// let row = theme
|
||||
// .project_row
|
||||
// .in_state(is_selected)
|
||||
// .style_for(mouse_state);
|
||||
|
||||
// Flex::row()
|
||||
// .with_child(render_tree_branch(
|
||||
// tree_branch,
|
||||
// &row.name.text,
|
||||
// is_last,
|
||||
// vec2f(host_avatar_width, theme.row_height),
|
||||
// cx.font_cache(),
|
||||
// ))
|
||||
// .with_child(
|
||||
// Svg::new("icons/desktop.svg")
|
||||
// .with_color(theme.channel_hash.color)
|
||||
// .constrained()
|
||||
// .with_width(theme.channel_hash.width)
|
||||
// .aligned()
|
||||
// .left(),
|
||||
// )
|
||||
// .with_child(
|
||||
// Label::new("Screen", row.name.text.clone())
|
||||
// .aligned()
|
||||
// .left()
|
||||
// .contained()
|
||||
// .with_style(row.name.container)
|
||||
// .flex(1., false),
|
||||
// )
|
||||
// .constrained()
|
||||
// .with_height(theme.row_height)
|
||||
// .contained()
|
||||
// .with_style(row.container)
|
||||
// },
|
||||
// );
|
||||
// if peer_id.is_none() {
|
||||
// return handler.into_any();
|
||||
// }
|
||||
// handler
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
// workspace.update(cx, |workspace, cx| {
|
||||
// workspace.open_shared_screen(peer_id.unwrap(), cx)
|
||||
// });
|
||||
// }
|
||||
// })
|
||||
// .into_any()
|
||||
|
||||
div()
|
||||
ListItem::new(("screen", id))
|
||||
.left_child(render_tree_branch(is_last, cx))
|
||||
.child(IconButton::new(0, Icon::Screen))
|
||||
.child(Label::new("Screen"))
|
||||
.when_some(peer_id, |this, _| {
|
||||
this.on_click(cx.listener(move |this, _, cx| {
|
||||
this.workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_shared_screen(peer_id.unwrap(), cx)
|
||||
});
|
||||
}))
|
||||
.tooltip(move |cx| Tooltip::text(format!("Open shared screen"), cx))
|
||||
})
|
||||
}
|
||||
|
||||
fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
@ -1415,54 +1361,14 @@ impl CollabPanel {
|
||||
channel_id: ChannelId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
// enum ChannelNotes {}
|
||||
// let host_avatar_width = theme
|
||||
// .contact_avatar
|
||||
// .width
|
||||
// .or(theme.contact_avatar.height)
|
||||
// .unwrap_or(0.);
|
||||
|
||||
// MouseEventHandler::new::<ChannelNotes, _>(ix as usize, cx, |state, cx| {
|
||||
// let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
|
||||
// let row = theme.project_row.in_state(is_selected).style_for(state);
|
||||
|
||||
// Flex::<Self>::row()
|
||||
// .with_child(render_tree_branch(
|
||||
// tree_branch,
|
||||
// &row.name.text,
|
||||
// false,
|
||||
// vec2f(host_avatar_width, theme.row_height),
|
||||
// cx.font_cache(),
|
||||
// ))
|
||||
// .with_child(
|
||||
// Svg::new("icons/file.svg")
|
||||
// .with_color(theme.channel_hash.color)
|
||||
// .constrained()
|
||||
// .with_width(theme.channel_hash.width)
|
||||
// .aligned()
|
||||
// .left(),
|
||||
// )
|
||||
// .with_child(
|
||||
// Label::new("notes", theme.channel_name.text.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.channel_name.container)
|
||||
// .aligned()
|
||||
// .left()
|
||||
// .flex(1., true),
|
||||
// )
|
||||
// .constrained()
|
||||
// .with_height(theme.row_height)
|
||||
// .contained()
|
||||
// .with_style(*theme.channel_row.style_for(is_selected, state))
|
||||
// .with_padding_left(theme.channel_row.default_style().padding.left)
|
||||
// })
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .into_any()
|
||||
|
||||
div()
|
||||
ListItem::new("channel-notes")
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.open_channel_notes(channel_id, cx);
|
||||
}))
|
||||
.left_child(render_tree_branch(false, cx))
|
||||
.child(IconButton::new(0, Icon::File))
|
||||
.child(Label::new("notes"))
|
||||
.tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
|
||||
}
|
||||
|
||||
fn render_channel_chat(
|
||||
@ -1470,53 +1376,14 @@ impl CollabPanel {
|
||||
channel_id: ChannelId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
// enum ChannelChat {}
|
||||
// let host_avatar_width = theme
|
||||
// .contact_avatar
|
||||
// .width
|
||||
// .or(theme.contact_avatar.height)
|
||||
// .unwrap_or(0.);
|
||||
|
||||
// MouseEventHandler::new::<ChannelChat, _>(ix as usize, cx, |state, cx| {
|
||||
// let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
|
||||
// let row = theme.project_row.in_state(is_selected).style_for(state);
|
||||
|
||||
// Flex::<Self>::row()
|
||||
// .with_child(render_tree_branch(
|
||||
// tree_branch,
|
||||
// &row.name.text,
|
||||
// true,
|
||||
// vec2f(host_avatar_width, theme.row_height),
|
||||
// cx.font_cache(),
|
||||
// ))
|
||||
// .with_child(
|
||||
// Svg::new("icons/conversations.svg")
|
||||
// .with_color(theme.channel_hash.color)
|
||||
// .constrained()
|
||||
// .with_width(theme.channel_hash.width)
|
||||
// .aligned()
|
||||
// .left(),
|
||||
// )
|
||||
// .with_child(
|
||||
// Label::new("chat", theme.channel_name.text.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.channel_name.container)
|
||||
// .aligned()
|
||||
// .left()
|
||||
// .flex(1., true),
|
||||
// )
|
||||
// .constrained()
|
||||
// .with_height(theme.row_height)
|
||||
// .contained()
|
||||
// .with_style(*theme.channel_row.style_for(is_selected, state))
|
||||
// .with_padding_left(theme.channel_row.default_style().padding.left)
|
||||
// })
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .into_any()
|
||||
div()
|
||||
ListItem::new("channel-chat")
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.join_channel_chat(channel_id, cx);
|
||||
}))
|
||||
.left_child(render_tree_branch(true, cx))
|
||||
.child(IconButton::new(0, Icon::MessageBubbles))
|
||||
.child(Label::new("chat"))
|
||||
.tooltip(move |cx| Tooltip::text("Open Chat", cx))
|
||||
}
|
||||
|
||||
// fn render_channel_invite(
|
||||
@ -2069,8 +1936,7 @@ impl CollabPanel {
|
||||
|
||||
fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
todo!();
|
||||
// ChannelView::open(action.channel_id, workspace, cx).detach();
|
||||
ChannelView::open(channel_id, workspace, cx).detach();
|
||||
}
|
||||
}
|
||||
|
||||
@ -2753,6 +2619,9 @@ impl CollabPanel {
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.open_channel_notes(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| {
|
||||
Tooltip::text("Open channel notes", cx)
|
||||
}),
|
||||
@ -3119,30 +2988,24 @@ impl CollabPanel {
|
||||
}
|
||||
|
||||
fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let text_style = cx.text_style();
|
||||
let rem_size = cx.rem_size();
|
||||
let text_system = cx.text_system();
|
||||
let font_id = text_system.font_id(&text_style.font()).unwrap();
|
||||
let font_size = text_style.font_size.to_pixels(rem_size);
|
||||
let line_height = text_style.line_height_in_pixels(rem_size);
|
||||
let cap_height = text_system.cap_height(font_id, font_size);
|
||||
let baseline_offset = text_system.baseline_offset(font_id, font_size, line_height);
|
||||
let width = cx.rem_size() * 2.5;
|
||||
let line_height = cx.text_style().line_height_in_pixels(rem_size);
|
||||
let width = rem_size * 1.5;
|
||||
let thickness = px(2.);
|
||||
let color = cx.theme().colors().text;
|
||||
|
||||
canvas(move |bounds, cx| {
|
||||
let start_x = bounds.left() + (bounds.size.width / 2.) - (width / 2.);
|
||||
let end_x = bounds.right();
|
||||
let start_y = bounds.top();
|
||||
let end_y = bounds.top() + baseline_offset - (cap_height / 2.);
|
||||
let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
|
||||
let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
|
||||
let right = bounds.right();
|
||||
let top = bounds.top();
|
||||
|
||||
cx.paint_quad(
|
||||
Bounds::from_corners(
|
||||
point(start_x, start_y),
|
||||
point(start_x, top),
|
||||
point(
|
||||
start_x + thickness,
|
||||
if is_last { end_y } else { bounds.bottom() },
|
||||
if is_last { start_y } else { bounds.bottom() },
|
||||
),
|
||||
),
|
||||
Default::default(),
|
||||
@ -3151,7 +3014,7 @@ fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement
|
||||
Hsla::transparent_black(),
|
||||
);
|
||||
cx.paint_quad(
|
||||
Bounds::from_corners(point(start_x, end_y), point(end_x, end_y + thickness)),
|
||||
Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
|
||||
Default::default(),
|
||||
color,
|
||||
Default::default(),
|
||||
@ -3344,10 +3207,6 @@ impl Panel for CollabPanel {
|
||||
Box::new(ToggleFocus)
|
||||
}
|
||||
|
||||
fn has_focus(&self, cx: &gpui::WindowContext) -> bool {
|
||||
self.focus_handle.contains_focused(cx)
|
||||
}
|
||||
|
||||
fn persistent_name() -> &'static str {
|
||||
"CollabPanel"
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
// vcs_menu::init(cx);
|
||||
collab_titlebar_item::init(cx);
|
||||
collab_panel::init(cx);
|
||||
channel_view::init(cx);
|
||||
// chat_panel::init(cx);
|
||||
notifications::init(&app_state, cx);
|
||||
|
||||
|
@ -23,11 +23,13 @@ pub type HashMap<K, V> = std::collections::HashMap<K, V>;
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub type HashSet<T> = std::collections::HashSet<T>;
|
||||
|
||||
use std::any::TypeId;
|
||||
pub use std::collections::*;
|
||||
|
||||
// NEW TYPES
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CommandPaletteFilter {
|
||||
pub filtered_namespaces: HashSet<&'static str>,
|
||||
pub hidden_namespaces: HashSet<&'static str>,
|
||||
pub hidden_action_types: HashSet<TypeId>,
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
let filtered = cx.read(|cx| {
|
||||
if cx.has_global::<CommandPaletteFilter>() {
|
||||
let filter = cx.global::<CommandPaletteFilter>();
|
||||
filter.filtered_namespaces.contains(action.namespace())
|
||||
filter.hidden_namespaces.contains(action.namespace())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@ -430,7 +430,7 @@ mod tests {
|
||||
// Add namespace filter, and redeploy the palette
|
||||
cx.update(|cx| {
|
||||
cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
|
||||
filter.filtered_namespaces.insert("editor");
|
||||
filter.hidden_namespaces.insert("editor");
|
||||
})
|
||||
});
|
||||
|
||||
|
@ -49,7 +49,10 @@ impl CommandPalette {
|
||||
.filter_map(|action| {
|
||||
let name = gpui::remove_the_2(action.name());
|
||||
let namespace = name.split("::").next().unwrap_or("malformed action name");
|
||||
if filter.is_some_and(|f| f.filtered_namespaces.contains(namespace)) {
|
||||
if filter.is_some_and(|f| {
|
||||
f.hidden_namespaces.contains(namespace)
|
||||
|| f.hidden_action_types.contains(&action.type_id())
|
||||
}) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@ -433,7 +436,7 @@ mod tests {
|
||||
cx.update(|cx| {
|
||||
cx.set_global(CommandPaletteFilter::default());
|
||||
cx.update_global::<CommandPaletteFilter, _>(|filter, _| {
|
||||
filter.filtered_namespaces.insert("editor");
|
||||
filter.hidden_namespaces.insert("editor");
|
||||
})
|
||||
});
|
||||
|
||||
|
@ -28,7 +28,7 @@ theme = { path = "../theme" }
|
||||
lsp = { path = "../lsp" }
|
||||
node_runtime = { path = "../node_runtime"}
|
||||
util = { path = "../util" }
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
async-compression.workspace = true
|
||||
async-tar = "0.4.2"
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
|
@ -58,16 +58,16 @@ pub fn init(
|
||||
cx.update_default_global::<collections::CommandPaletteFilter, _, _>(move |filter, _cx| {
|
||||
match status {
|
||||
Status::Disabled => {
|
||||
filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
|
||||
filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE);
|
||||
filter.hidden_namespaces.insert(COPILOT_NAMESPACE);
|
||||
filter.hidden_namespaces.insert(COPILOT_AUTH_NAMESPACE);
|
||||
}
|
||||
Status::Authorized => {
|
||||
filter.filtered_namespaces.remove(COPILOT_NAMESPACE);
|
||||
filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
filter.hidden_namespaces.remove(COPILOT_NAMESPACE);
|
||||
filter.hidden_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
}
|
||||
_ => {
|
||||
filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
|
||||
filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
filter.hidden_namespaces.insert(COPILOT_NAMESPACE);
|
||||
filter.hidden_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -28,7 +28,8 @@ theme = { package = "theme2", path = "../theme2" }
|
||||
lsp = { package = "lsp2", path = "../lsp2" }
|
||||
node_runtime = { path = "../node_runtime"}
|
||||
util = { path = "../util" }
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
async-compression.workspace = true
|
||||
async-tar = "0.4.2"
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
|
@ -22,6 +22,7 @@ use request::StatusNotification;
|
||||
use settings::SettingsStore;
|
||||
use smol::{fs, io::BufReader, stream::StreamExt};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
ffi::OsString,
|
||||
mem,
|
||||
ops::Range,
|
||||
@ -32,13 +33,14 @@ use util::{
|
||||
fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt,
|
||||
};
|
||||
|
||||
// todo!()
|
||||
// const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
|
||||
actions!(SignIn, SignOut);
|
||||
|
||||
// todo!()
|
||||
// const COPILOT_NAMESPACE: &'static str = "copilot";
|
||||
actions!(Suggest, NextSuggestion, PreviousSuggestion, Reinstall);
|
||||
actions!(
|
||||
Suggest,
|
||||
NextSuggestion,
|
||||
PreviousSuggestion,
|
||||
Reinstall,
|
||||
SignIn,
|
||||
SignOut
|
||||
);
|
||||
|
||||
pub fn init(
|
||||
new_server_id: LanguageServerId,
|
||||
@ -51,52 +53,70 @@ pub fn init(
|
||||
move |cx| Copilot::start(new_server_id, http, node_runtime, cx)
|
||||
});
|
||||
cx.set_global(copilot.clone());
|
||||
cx.observe(&copilot, |handle, cx| {
|
||||
let copilot_action_types = [
|
||||
TypeId::of::<Suggest>(),
|
||||
TypeId::of::<NextSuggestion>(),
|
||||
TypeId::of::<PreviousSuggestion>(),
|
||||
TypeId::of::<Reinstall>(),
|
||||
];
|
||||
let copilot_auth_action_types = [TypeId::of::<SignOut>()];
|
||||
let copilot_no_auth_action_types = [TypeId::of::<SignIn>()];
|
||||
let status = handle.read(cx).status();
|
||||
let filter = cx.default_global::<collections::CommandPaletteFilter>();
|
||||
|
||||
// TODO
|
||||
// cx.observe(&copilot, |handle, cx| {
|
||||
// let status = handle.read(cx).status();
|
||||
// cx.update_default_global::<collections::CommandPaletteFilter, _, _>(move |filter, _cx| {
|
||||
// match status {
|
||||
// Status::Disabled => {
|
||||
// filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
|
||||
// filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE);
|
||||
// }
|
||||
// Status::Authorized => {
|
||||
// filter.filtered_namespaces.remove(COPILOT_NAMESPACE);
|
||||
// filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
// }
|
||||
// _ => {
|
||||
// filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
|
||||
// filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// })
|
||||
// .detach();
|
||||
match status {
|
||||
Status::Disabled => {
|
||||
filter.hidden_action_types.extend(copilot_action_types);
|
||||
filter.hidden_action_types.extend(copilot_auth_action_types);
|
||||
filter
|
||||
.hidden_action_types
|
||||
.extend(copilot_no_auth_action_types);
|
||||
}
|
||||
Status::Authorized => {
|
||||
filter
|
||||
.hidden_action_types
|
||||
.extend(copilot_no_auth_action_types);
|
||||
for type_id in copilot_action_types
|
||||
.iter()
|
||||
.chain(&copilot_auth_action_types)
|
||||
{
|
||||
filter.hidden_action_types.remove(type_id);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
filter.hidden_action_types.extend(copilot_action_types);
|
||||
filter.hidden_action_types.extend(copilot_auth_action_types);
|
||||
for type_id in &copilot_no_auth_action_types {
|
||||
filter.hidden_action_types.remove(type_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// sign_in::init(cx);
|
||||
// cx.add_global_action(|_: &SignIn, cx| {
|
||||
// if let Some(copilot) = Copilot::global(cx) {
|
||||
// copilot
|
||||
// .update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
// .detach_and_log_err(cx);
|
||||
// }
|
||||
// });
|
||||
// cx.add_global_action(|_: &SignOut, cx| {
|
||||
// if let Some(copilot) = Copilot::global(cx) {
|
||||
// copilot
|
||||
// .update(cx, |copilot, cx| copilot.sign_out(cx))
|
||||
// .detach_and_log_err(cx);
|
||||
// }
|
||||
// });
|
||||
|
||||
// cx.add_global_action(|_: &Reinstall, cx| {
|
||||
// if let Some(copilot) = Copilot::global(cx) {
|
||||
// copilot
|
||||
// .update(cx, |copilot, cx| copilot.reinstall(cx))
|
||||
// .detach();
|
||||
// }
|
||||
// });
|
||||
sign_in::init(cx);
|
||||
cx.on_action(|_: &SignIn, cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
cx.on_action(|_: &SignOut, cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_out(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
cx.on_action(|_: &Reinstall, cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.reinstall(cx))
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enum CopilotServer {
|
||||
|
@ -1,376 +1,213 @@
|
||||
// TODO add logging in
|
||||
// use crate::{request::PromptUserDeviceFlow, Copilot, Status};
|
||||
// use gpui::{
|
||||
// elements::*,
|
||||
// geometry::rect::RectF,
|
||||
// platform::{WindowBounds, WindowKind, WindowOptions},
|
||||
// AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext,
|
||||
// WindowHandle,
|
||||
// };
|
||||
// use theme::ui::modal;
|
||||
use crate::{request::PromptUserDeviceFlow, Copilot, Status};
|
||||
use gpui::{
|
||||
div, size, AppContext, Bounds, ClipboardItem, Div, Element, GlobalPixels, InteractiveElement,
|
||||
IntoElement, ParentElement, Point, Render, Stateful, Styled, ViewContext, VisualContext,
|
||||
WindowBounds, WindowHandle, WindowKind, WindowOptions,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{prelude::*, Button, Icon, IconElement, Label};
|
||||
|
||||
// #[derive(PartialEq, Eq, Debug, Clone)]
|
||||
// struct CopyUserCode;
|
||||
const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
|
||||
|
||||
// #[derive(PartialEq, Eq, Debug, Clone)]
|
||||
// struct OpenGithub;
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
let mut verification_window: Option<WindowHandle<CopilotCodeVerification>> = None;
|
||||
cx.observe(&copilot, move |copilot, cx| {
|
||||
let status = copilot.read(cx).status();
|
||||
|
||||
// const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
|
||||
match &status {
|
||||
crate::Status::SigningIn { prompt } => {
|
||||
if let Some(window) = verification_window.as_mut() {
|
||||
let updated = window
|
||||
.update(cx, |verification, cx| {
|
||||
verification.set_status(status.clone(), cx);
|
||||
cx.activate_window();
|
||||
})
|
||||
.is_ok();
|
||||
if !updated {
|
||||
verification_window = Some(create_copilot_auth_window(cx, &status));
|
||||
}
|
||||
} else if let Some(_prompt) = prompt {
|
||||
verification_window = Some(create_copilot_auth_window(cx, &status));
|
||||
}
|
||||
}
|
||||
Status::Authorized | Status::Unauthorized => {
|
||||
if let Some(window) = verification_window.as_ref() {
|
||||
window
|
||||
.update(cx, |verification, cx| {
|
||||
verification.set_status(status, cx);
|
||||
cx.activate(true);
|
||||
cx.activate_window();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if let Some(code_verification) = verification_window.take() {
|
||||
code_verification
|
||||
.update(cx, |_, cx| cx.remove_window())
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn init(cx: &mut AppContext) {
|
||||
// if let Some(copilot) = Copilot::global(cx) {
|
||||
// let mut verification_window: Option<WindowHandle<CopilotCodeVerification>> = None;
|
||||
// cx.observe(&copilot, move |copilot, cx| {
|
||||
// let status = copilot.read(cx).status();
|
||||
fn create_copilot_auth_window(
|
||||
cx: &mut AppContext,
|
||||
status: &Status,
|
||||
) -> WindowHandle<CopilotCodeVerification> {
|
||||
let window_size = size(GlobalPixels::from(280.), GlobalPixels::from(280.));
|
||||
let window_options = WindowOptions {
|
||||
bounds: WindowBounds::Fixed(Bounds::new(Point::default(), window_size)),
|
||||
titlebar: None,
|
||||
center: true,
|
||||
focus: true,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: true,
|
||||
display_id: None,
|
||||
};
|
||||
let window = cx.open_window(window_options, |cx| {
|
||||
cx.build_view(|_| CopilotCodeVerification::new(status.clone()))
|
||||
});
|
||||
window
|
||||
}
|
||||
|
||||
// match &status {
|
||||
// crate::Status::SigningIn { prompt } => {
|
||||
// if let Some(window) = verification_window.as_mut() {
|
||||
// let updated = window
|
||||
// .root(cx)
|
||||
// .map(|root| {
|
||||
// root.update(cx, |verification, cx| {
|
||||
// verification.set_status(status.clone(), cx);
|
||||
// cx.activate_window();
|
||||
// })
|
||||
// })
|
||||
// .is_some();
|
||||
// if !updated {
|
||||
// verification_window = Some(create_copilot_auth_window(cx, &status));
|
||||
// }
|
||||
// } else if let Some(_prompt) = prompt {
|
||||
// verification_window = Some(create_copilot_auth_window(cx, &status));
|
||||
// }
|
||||
// }
|
||||
// Status::Authorized | Status::Unauthorized => {
|
||||
// if let Some(window) = verification_window.as_ref() {
|
||||
// if let Some(verification) = window.root(cx) {
|
||||
// verification.update(cx, |verification, cx| {
|
||||
// verification.set_status(status, cx);
|
||||
// cx.platform().activate(true);
|
||||
// cx.activate_window();
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// _ => {
|
||||
// if let Some(code_verification) = verification_window.take() {
|
||||
// code_verification.update(cx, |cx| cx.remove_window());
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// .detach();
|
||||
// }
|
||||
// }
|
||||
pub struct CopilotCodeVerification {
|
||||
status: Status,
|
||||
connect_clicked: bool,
|
||||
}
|
||||
|
||||
// fn create_copilot_auth_window(
|
||||
// cx: &mut AppContext,
|
||||
// status: &Status,
|
||||
// ) -> WindowHandle<CopilotCodeVerification> {
|
||||
// let window_size = theme::current(cx).copilot.modal.dimensions();
|
||||
// let window_options = WindowOptions {
|
||||
// bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
|
||||
// titlebar: None,
|
||||
// center: true,
|
||||
// focus: true,
|
||||
// show: true,
|
||||
// kind: WindowKind::Normal,
|
||||
// is_movable: true,
|
||||
// screen: None,
|
||||
// };
|
||||
// cx.add_window(window_options, |_cx| {
|
||||
// CopilotCodeVerification::new(status.clone())
|
||||
// })
|
||||
// }
|
||||
impl CopilotCodeVerification {
|
||||
pub fn new(status: Status) -> Self {
|
||||
Self {
|
||||
status,
|
||||
connect_clicked: false,
|
||||
}
|
||||
}
|
||||
|
||||
// pub struct CopilotCodeVerification {
|
||||
// status: Status,
|
||||
// connect_clicked: bool,
|
||||
// }
|
||||
pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
|
||||
self.status = status;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// impl CopilotCodeVerification {
|
||||
// pub fn new(status: Status) -> Self {
|
||||
// Self {
|
||||
// status,
|
||||
// connect_clicked: false,
|
||||
// }
|
||||
// }
|
||||
fn render_device_code(
|
||||
data: &PromptUserDeviceFlow,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let copied = cx
|
||||
.read_from_clipboard()
|
||||
.map(|item| item.text() == &data.user_code)
|
||||
.unwrap_or(false);
|
||||
h_stack()
|
||||
.cursor_pointer()
|
||||
.justify_between()
|
||||
.on_mouse_down(gpui::MouseButton::Left, {
|
||||
let user_code = data.user_code.clone();
|
||||
move |_, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new(user_code.clone()));
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.child(Label::new(data.user_code.clone()))
|
||||
.child(div())
|
||||
.child(Label::new(if copied { "Copied!" } else { "Copy" }))
|
||||
}
|
||||
|
||||
// pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
|
||||
// self.status = status;
|
||||
// cx.notify();
|
||||
// }
|
||||
fn render_prompting_modal(
|
||||
connect_clicked: bool,
|
||||
data: &PromptUserDeviceFlow,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl Element {
|
||||
let connect_button_label = if connect_clicked {
|
||||
"Waiting for connection..."
|
||||
} else {
|
||||
"Connect to Github"
|
||||
};
|
||||
v_stack()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.child(Label::new(
|
||||
"Enable Copilot by connecting your existing license",
|
||||
))
|
||||
.child(Self::render_device_code(data, cx))
|
||||
.child(
|
||||
Label::new("Paste this code into GitHub after clicking the button below.")
|
||||
.size(ui::LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Button::new("connect-button", connect_button_label).on_click({
|
||||
let verification_uri = data.verification_uri.clone();
|
||||
cx.listener(move |this, _, cx| {
|
||||
cx.open_url(&verification_uri);
|
||||
this.connect_clicked = true;
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
fn render_enabled_modal() -> impl Element {
|
||||
v_stack()
|
||||
.child(Label::new("Copilot Enabled!"))
|
||||
.child(Label::new(
|
||||
"You can update your settings or sign out from the Copilot menu in the status bar.",
|
||||
))
|
||||
.child(
|
||||
Button::new("copilot-enabled-done-button", "Done")
|
||||
.on_click(|_, cx| cx.remove_window()),
|
||||
)
|
||||
}
|
||||
|
||||
// fn render_device_code(
|
||||
// data: &PromptUserDeviceFlow,
|
||||
// style: &theme::Copilot,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> impl IntoAnyElement<Self> {
|
||||
// let copied = cx
|
||||
// .read_from_clipboard()
|
||||
// .map(|item| item.text() == &data.user_code)
|
||||
// .unwrap_or(false);
|
||||
fn render_unauthorized_modal() -> impl Element {
|
||||
v_stack()
|
||||
.child(Label::new(
|
||||
"Enable Copilot by connecting your existing license.",
|
||||
))
|
||||
.child(
|
||||
Label::new("You must have an active Copilot license to use it in Zed.")
|
||||
.color(Color::Warning),
|
||||
)
|
||||
.child(
|
||||
Button::new("copilot-subscribe-button", "Subscibe on Github").on_click(|_, cx| {
|
||||
cx.remove_window();
|
||||
cx.open_url(COPILOT_SIGN_UP_URL)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// let device_code_style = &style.auth.prompting.device_code;
|
||||
impl Render for CopilotCodeVerification {
|
||||
type Element = Stateful<Div>;
|
||||
|
||||
// MouseEventHandler::new::<Self, _>(0, cx, |state, _cx| {
|
||||
// Flex::row()
|
||||
// .with_child(
|
||||
// Label::new(data.user_code.clone(), device_code_style.text.clone())
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(device_code_style.left_container)
|
||||
// .constrained()
|
||||
// .with_width(device_code_style.left),
|
||||
// )
|
||||
// .with_child(
|
||||
// Label::new(
|
||||
// if copied { "Copied!" } else { "Copy" },
|
||||
// device_code_style.cta.style_for(state).text.clone(),
|
||||
// )
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(*device_code_style.right_container.style_for(state))
|
||||
// .constrained()
|
||||
// .with_width(device_code_style.right),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(device_code_style.cta.style_for(state).container)
|
||||
// })
|
||||
// .on_click(gpui::platform::MouseButton::Left, {
|
||||
// let user_code = data.user_code.clone();
|
||||
// move |_, _, cx| {
|
||||
// cx.platform()
|
||||
// .write_to_clipboard(ClipboardItem::new(user_code.clone()));
|
||||
// cx.notify();
|
||||
// }
|
||||
// })
|
||||
// .with_cursor_style(gpui::platform::CursorStyle::PointingHand)
|
||||
// }
|
||||
|
||||
// fn render_prompting_modal(
|
||||
// connect_clicked: bool,
|
||||
// data: &PromptUserDeviceFlow,
|
||||
// style: &theme::Copilot,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> AnyElement<Self> {
|
||||
// enum ConnectButton {}
|
||||
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// Label::new(
|
||||
// "Enable Copilot by connecting",
|
||||
// style.auth.prompting.subheading.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new(
|
||||
// "your existing license.",
|
||||
// style.auth.prompting.subheading.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// .contained()
|
||||
// .with_style(style.auth.prompting.subheading.container),
|
||||
// )
|
||||
// .with_child(Self::render_device_code(data, &style, cx))
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// Label::new(
|
||||
// "Paste this code into GitHub after",
|
||||
// style.auth.prompting.hint.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new(
|
||||
// "clicking the button below.",
|
||||
// style.auth.prompting.hint.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// .contained()
|
||||
// .with_style(style.auth.prompting.hint.container.clone()),
|
||||
// )
|
||||
// .with_child(theme::ui::cta_button::<ConnectButton, _, _, _>(
|
||||
// if connect_clicked {
|
||||
// "Waiting for connection..."
|
||||
// } else {
|
||||
// "Connect to GitHub"
|
||||
// },
|
||||
// style.auth.content_width,
|
||||
// &style.auth.cta_button,
|
||||
// cx,
|
||||
// {
|
||||
// let verification_uri = data.verification_uri.clone();
|
||||
// move |_, verification, cx| {
|
||||
// cx.platform().open_url(&verification_uri);
|
||||
// verification.connect_clicked = true;
|
||||
// }
|
||||
// },
|
||||
// ))
|
||||
// .align_children_center()
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn render_enabled_modal(
|
||||
// style: &theme::Copilot,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> AnyElement<Self> {
|
||||
// enum DoneButton {}
|
||||
|
||||
// let enabled_style = &style.auth.authorized;
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Label::new("Copilot Enabled!", enabled_style.subheading.text.clone())
|
||||
// .contained()
|
||||
// .with_style(enabled_style.subheading.container)
|
||||
// .aligned(),
|
||||
// )
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// Label::new(
|
||||
// "You can update your settings or",
|
||||
// enabled_style.hint.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new(
|
||||
// "sign out from the Copilot menu in",
|
||||
// enabled_style.hint.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new("the status bar.", enabled_style.hint.text.clone()).aligned(),
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// .contained()
|
||||
// .with_style(enabled_style.hint.container),
|
||||
// )
|
||||
// .with_child(theme::ui::cta_button::<DoneButton, _, _, _>(
|
||||
// "Done",
|
||||
// style.auth.content_width,
|
||||
// &style.auth.cta_button,
|
||||
// cx,
|
||||
// |_, _, cx| cx.remove_window(),
|
||||
// ))
|
||||
// .align_children_center()
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn render_unauthorized_modal(
|
||||
// style: &theme::Copilot,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> AnyElement<Self> {
|
||||
// let unauthorized_style = &style.auth.not_authorized;
|
||||
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// Label::new(
|
||||
// "Enable Copilot by connecting",
|
||||
// unauthorized_style.subheading.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new(
|
||||
// "your existing license.",
|
||||
// unauthorized_style.subheading.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// .contained()
|
||||
// .with_style(unauthorized_style.subheading.container),
|
||||
// )
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// Label::new(
|
||||
// "You must have an active copilot",
|
||||
// unauthorized_style.warning.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new(
|
||||
// "license to use it in Zed.",
|
||||
// unauthorized_style.warning.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// .contained()
|
||||
// .with_style(unauthorized_style.warning.container),
|
||||
// )
|
||||
// .with_child(theme::ui::cta_button::<Self, _, _, _>(
|
||||
// "Subscribe on GitHub",
|
||||
// style.auth.content_width,
|
||||
// &style.auth.cta_button,
|
||||
// cx,
|
||||
// |_, _, cx| {
|
||||
// cx.remove_window();
|
||||
// cx.platform().open_url(COPILOT_SIGN_UP_URL)
|
||||
// },
|
||||
// ))
|
||||
// .align_children_center()
|
||||
// .into_any()
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl Entity for CopilotCodeVerification {
|
||||
// type Event = ();
|
||||
// }
|
||||
|
||||
// impl View for CopilotCodeVerification {
|
||||
// fn ui_name() -> &'static str {
|
||||
// "CopilotCodeVerification"
|
||||
// }
|
||||
|
||||
// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// cx.notify()
|
||||
// }
|
||||
|
||||
// fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// cx.notify()
|
||||
// }
|
||||
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// enum ConnectModal {}
|
||||
|
||||
// let style = theme::current(cx).clone();
|
||||
|
||||
// modal::<ConnectModal, _, _, _, _>(
|
||||
// "Connect Copilot to Zed",
|
||||
// &style.copilot.modal,
|
||||
// cx,
|
||||
// |cx| {
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// theme::ui::icon(&style.copilot.auth.header).into_any(),
|
||||
// match &self.status {
|
||||
// Status::SigningIn {
|
||||
// prompt: Some(prompt),
|
||||
// } => Self::render_prompting_modal(
|
||||
// self.connect_clicked,
|
||||
// &prompt,
|
||||
// &style.copilot,
|
||||
// cx,
|
||||
// ),
|
||||
// Status::Unauthorized => {
|
||||
// self.connect_clicked = false;
|
||||
// Self::render_unauthorized_modal(&style.copilot, cx)
|
||||
// }
|
||||
// Status::Authorized => {
|
||||
// self.connect_clicked = false;
|
||||
// Self::render_enabled_modal(&style.copilot, cx)
|
||||
// }
|
||||
// _ => Empty::new().into_any(),
|
||||
// },
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// },
|
||||
// )
|
||||
// .into_any()
|
||||
// }
|
||||
// }
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let prompt = match &self.status {
|
||||
Status::SigningIn {
|
||||
prompt: Some(prompt),
|
||||
} => Self::render_prompting_modal(self.connect_clicked, &prompt, cx).into_any_element(),
|
||||
Status::Unauthorized => {
|
||||
self.connect_clicked = false;
|
||||
Self::render_unauthorized_modal().into_any_element()
|
||||
}
|
||||
Status::Authorized => {
|
||||
self.connect_clicked = false;
|
||||
Self::render_enabled_modal().into_any_element()
|
||||
}
|
||||
_ => div().into_any_element(),
|
||||
};
|
||||
div()
|
||||
.id("copilot code verification")
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.items_center()
|
||||
.p_10()
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.child(ui::Label::new("Connect Copilot to Zed"))
|
||||
.child(IconElement::new(Icon::ZedXCopilot))
|
||||
.child(prompt)
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ use std::{
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
pub use toolbar_controls::ToolbarControls;
|
||||
use ui::{h_stack, Color, HighlightedLabel, Icon, IconElement, Label};
|
||||
use ui::{h_stack, prelude::*, HighlightedLabel, Icon, IconElement, Label};
|
||||
use util::TryFutureExt;
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
|
||||
@ -88,7 +88,7 @@ struct DiagnosticGroupState {
|
||||
block_count: usize,
|
||||
}
|
||||
|
||||
impl EventEmitter<ItemEvent> for ProjectDiagnosticsEditor {}
|
||||
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
|
||||
|
||||
impl Render for ProjectDiagnosticsEditor {
|
||||
type Element = Focusable<Div>;
|
||||
@ -158,7 +158,7 @@ impl ProjectDiagnosticsEditor {
|
||||
});
|
||||
let editor_event_subscription =
|
||||
cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| {
|
||||
Self::emit_item_event_for_editor_event(event, cx);
|
||||
cx.emit(event.clone());
|
||||
if event == &EditorEvent::Focused && this.path_states.is_empty() {
|
||||
cx.focus(&this.focus_handle);
|
||||
}
|
||||
@ -183,40 +183,6 @@ impl ProjectDiagnosticsEditor {
|
||||
this
|
||||
}
|
||||
|
||||
fn emit_item_event_for_editor_event(event: &EditorEvent, cx: &mut ViewContext<Self>) {
|
||||
match event {
|
||||
EditorEvent::Closed => cx.emit(ItemEvent::CloseItem),
|
||||
|
||||
EditorEvent::Saved | EditorEvent::TitleChanged => {
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::Reparsed => {
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::SelectionsChanged { local } if *local => {
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::DirtyChanged => {
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
}
|
||||
|
||||
EditorEvent::BufferEdited => {
|
||||
cx.emit(ItemEvent::Edit);
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => {
|
||||
cx.emit(ItemEvent::Edit);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
|
||||
workspace.activate_item(&existing, cx);
|
||||
@ -333,8 +299,7 @@ impl ProjectDiagnosticsEditor {
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.summary = this.project.read(cx).diagnostic_summary(false, cx);
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
@ -649,6 +614,12 @@ impl FocusableView for ProjectDiagnosticsEditor {
|
||||
}
|
||||
|
||||
impl Item for ProjectDiagnosticsEditor {
|
||||
type Event = EditorEvent;
|
||||
|
||||
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
|
||||
Editor::to_item_events(event, f)
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, |editor, cx| editor.deactivated(cx));
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ use lsp::DiagnosticSeverity;
|
||||
use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
|
||||
use sum_tree::{Bias, TreeMap};
|
||||
use tab_map::TabMap;
|
||||
use theme::{SyntaxTheme, Theme};
|
||||
use theme::{StatusColors, SyntaxTheme, Theme};
|
||||
use wrap_map::WrapMap;
|
||||
|
||||
pub use block_map::{
|
||||
@ -513,8 +513,8 @@ impl DisplaySnapshot {
|
||||
self.chunks(
|
||||
display_rows,
|
||||
language_aware,
|
||||
Some(editor_style.syntax.inlay_style),
|
||||
Some(editor_style.syntax.suggestion_style),
|
||||
Some(editor_style.inlays_style),
|
||||
Some(editor_style.suggestions_style),
|
||||
)
|
||||
.map(|chunk| {
|
||||
let mut highlight_style = chunk
|
||||
|
@ -993,7 +993,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::display_map::inlay_map::InlayMap;
|
||||
use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
|
||||
use gpui::{div, font, px, Element, Platform as _};
|
||||
use gpui::{div, font, px, Element};
|
||||
use multi_buffer::MultiBuffer;
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
@ -1185,11 +1185,7 @@ mod tests {
|
||||
fn test_blocks_on_wrapped_lines(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(|cx| init_test(cx));
|
||||
|
||||
let font_id = cx
|
||||
.test_platform
|
||||
.text_system()
|
||||
.font_id(&font("Helvetica"))
|
||||
.unwrap();
|
||||
let font_id = cx.text_system().font_id(&font("Helvetica")).unwrap();
|
||||
|
||||
let text = "one two three\nfour five six\nseven eight";
|
||||
|
||||
|
@ -1032,7 +1032,7 @@ mod tests {
|
||||
display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
|
||||
MultiBuffer,
|
||||
};
|
||||
use gpui::{font, px, test::observe, Platform};
|
||||
use gpui::{font, px, test::observe};
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
use smol::stream::StreamExt;
|
||||
|
@ -92,6 +92,7 @@ use std::{
|
||||
ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
sync::Weak,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
pub use sum_tree::Bias;
|
||||
@ -420,6 +421,25 @@ pub fn init(cx: &mut AppContext) {
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
cx.on_action(move |_: &workspace::NewFile, cx| {
|
||||
let app_state = cx.global::<Weak<workspace::AppState>>();
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
workspace::open_new(&app_state, cx, |workspace, cx| {
|
||||
Editor::new_file(workspace, &Default::default(), cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
cx.on_action(move |_: &workspace::NewWindow, cx| {
|
||||
let app_state = cx.global::<Weak<workspace::AppState>>();
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
workspace::open_new(&app_state, cx, |workspace, cx| {
|
||||
Editor::new_file(workspace, &Default::default(), cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
trait InvalidationRegion {
|
||||
@ -479,6 +499,8 @@ pub struct EditorStyle {
|
||||
pub scrollbar_width: Pixels,
|
||||
pub syntax: Arc<SyntaxTheme>,
|
||||
pub diagnostic_style: DiagnosticStyle,
|
||||
pub inlays_style: HighlightStyle,
|
||||
pub suggestions_style: HighlightStyle,
|
||||
}
|
||||
|
||||
type CompletionId = usize;
|
||||
@ -1675,8 +1697,7 @@ impl Editor {
|
||||
if let Some(project) = project.as_ref() {
|
||||
if buffer.read(cx).is_singleton() {
|
||||
project_subscriptions.push(cx.observe(project, |_, _, cx| {
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
}));
|
||||
}
|
||||
project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| {
|
||||
@ -1961,14 +1982,14 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext<Self>) {
|
||||
// self.cursor_shape = cursor_shape;
|
||||
// cx.notify();
|
||||
// }
|
||||
pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext<Self>) {
|
||||
self.cursor_shape = cursor_shape;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// pub fn set_collapse_matches(&mut self, collapse_matches: bool) {
|
||||
// self.collapse_matches = collapse_matches;
|
||||
// }
|
||||
pub fn set_collapse_matches(&mut self, collapse_matches: bool) {
|
||||
self.collapse_matches = collapse_matches;
|
||||
}
|
||||
|
||||
pub fn range_for_match<T: std::marker::Copy>(&self, range: &Range<T>) -> Range<T> {
|
||||
if self.collapse_matches {
|
||||
@ -1977,56 +1998,47 @@ impl Editor {
|
||||
range.clone()
|
||||
}
|
||||
|
||||
// pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext<Self>) {
|
||||
// if self.display_map.read(cx).clip_at_line_ends != clip {
|
||||
// self.display_map
|
||||
// .update(cx, |map, _| map.clip_at_line_ends = clip);
|
||||
// }
|
||||
// }
|
||||
pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext<Self>) {
|
||||
if self.display_map.read(cx).clip_at_line_ends != clip {
|
||||
self.display_map
|
||||
.update(cx, |map, _| map.clip_at_line_ends = clip);
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn set_keymap_context_layer<Tag: 'static>(
|
||||
// &mut self,
|
||||
// context: KeymapContext,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) {
|
||||
// self.keymap_context_layers
|
||||
// .insert(TypeId::of::<Tag>(), context);
|
||||
// cx.notify();
|
||||
// }
|
||||
pub fn set_keymap_context_layer<Tag: 'static>(
|
||||
&mut self,
|
||||
context: KeyContext,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.keymap_context_layers
|
||||
.insert(TypeId::of::<Tag>(), context);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// pub fn remove_keymap_context_layer<Tag: 'static>(&mut self, cx: &mut ViewContext<Self>) {
|
||||
// self.keymap_context_layers.remove(&TypeId::of::<Tag>());
|
||||
// cx.notify();
|
||||
// }
|
||||
pub fn remove_keymap_context_layer<Tag: 'static>(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.keymap_context_layers.remove(&TypeId::of::<Tag>());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// pub fn set_input_enabled(&mut self, input_enabled: bool) {
|
||||
// self.input_enabled = input_enabled;
|
||||
// }
|
||||
pub fn set_input_enabled(&mut self, input_enabled: bool) {
|
||||
self.input_enabled = input_enabled;
|
||||
}
|
||||
|
||||
// pub fn set_autoindent(&mut self, autoindent: bool) {
|
||||
// if autoindent {
|
||||
// self.autoindent_mode = Some(AutoindentMode::EachLine);
|
||||
// } else {
|
||||
// self.autoindent_mode = None;
|
||||
// }
|
||||
// }
|
||||
pub fn set_autoindent(&mut self, autoindent: bool) {
|
||||
if autoindent {
|
||||
self.autoindent_mode = Some(AutoindentMode::EachLine);
|
||||
} else {
|
||||
self.autoindent_mode = None;
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn read_only(&self) -> bool {
|
||||
// self.read_only
|
||||
// }
|
||||
pub fn read_only(&self) -> bool {
|
||||
self.read_only
|
||||
}
|
||||
|
||||
// pub fn set_read_only(&mut self, read_only: bool) {
|
||||
// self.read_only = read_only;
|
||||
// }
|
||||
|
||||
// pub fn set_field_editor_style(
|
||||
// &mut self,
|
||||
// style: Option<Arc<GetFieldEditorTheme>>,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) {
|
||||
// self.get_field_editor_theme = style;
|
||||
// cx.notify();
|
||||
// }
|
||||
pub fn set_read_only(&mut self, read_only: bool) {
|
||||
self.read_only = read_only;
|
||||
}
|
||||
|
||||
fn selections_did_change(
|
||||
&mut self,
|
||||
@ -2141,10 +2153,6 @@ impl Editor {
|
||||
cx.emit(SearchEvent::ActiveMatchChanged)
|
||||
}
|
||||
|
||||
if local {
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@ -7634,6 +7642,18 @@ impl Editor {
|
||||
.editor_style
|
||||
.diagnostic_style
|
||||
.clone(),
|
||||
// todo!("what about the rest of the highlight style parts for inlays and suggestions?")
|
||||
inlays_style: HighlightStyle {
|
||||
color: Some(cx.theme().status().hint),
|
||||
font_weight: Some(FontWeight::BOLD),
|
||||
fade_out: Some(0.6),
|
||||
..HighlightStyle::default()
|
||||
},
|
||||
suggestions_style: HighlightStyle {
|
||||
color: Some(cx.theme().status().predictive),
|
||||
fade_out: Some(0.6),
|
||||
..HighlightStyle::default()
|
||||
},
|
||||
},
|
||||
))
|
||||
.into_any_element()
|
||||
@ -8573,8 +8593,6 @@ impl Editor {
|
||||
self.update_visible_copilot_suggestion(cx);
|
||||
}
|
||||
cx.emit(EditorEvent::BufferEdited);
|
||||
cx.emit(ItemEvent::Edit);
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
cx.emit(SearchEvent::MatchesInvalidated);
|
||||
|
||||
if *sigleton_buffer_edited {
|
||||
@ -8622,20 +8640,14 @@ impl Editor {
|
||||
self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
|
||||
cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
|
||||
}
|
||||
multi_buffer::Event::Reparsed => {
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
multi_buffer::Event::DirtyChanged => {
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
}
|
||||
multi_buffer::Event::Saved
|
||||
| multi_buffer::Event::FileHandleChanged
|
||||
| multi_buffer::Event::Reloaded => {
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
multi_buffer::Event::Reparsed => cx.emit(EditorEvent::Reparsed),
|
||||
multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged),
|
||||
multi_buffer::Event::Saved => cx.emit(EditorEvent::Saved),
|
||||
multi_buffer::Event::FileHandleChanged | multi_buffer::Event::Reloaded => {
|
||||
cx.emit(EditorEvent::TitleChanged)
|
||||
}
|
||||
multi_buffer::Event::DiffBaseChanged => cx.emit(EditorEvent::DiffBaseChanged),
|
||||
multi_buffer::Event::Closed => cx.emit(ItemEvent::CloseItem),
|
||||
multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed),
|
||||
multi_buffer::Event::DiagnosticsUpdated => {
|
||||
self.refresh_active_diagnostics(cx);
|
||||
}
|
||||
@ -9279,7 +9291,7 @@ impl Render for Editor {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features,
|
||||
font_size: settings.buffer_font_size.into(),
|
||||
font_size: settings.buffer_font_size(cx).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
@ -9304,6 +9316,19 @@ impl Render for Editor {
|
||||
scrollbar_width: px(12.),
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
diagnostic_style: cx.theme().diagnostic_style(),
|
||||
// TODO kb find `HighlightStyle` usages
|
||||
// todo!("what about the rest of the highlight style parts?")
|
||||
inlays_style: HighlightStyle {
|
||||
color: Some(cx.theme().status().hint),
|
||||
font_weight: Some(FontWeight::BOLD),
|
||||
fade_out: Some(0.6),
|
||||
..HighlightStyle::default()
|
||||
},
|
||||
suggestions_style: HighlightStyle {
|
||||
color: Some(cx.theme().status().predictive),
|
||||
fade_out: Some(0.6),
|
||||
..HighlightStyle::default()
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ use futures::StreamExt;
|
||||
use gpui::{
|
||||
div,
|
||||
serde_json::{self, json},
|
||||
Div, Flatten, Platform, TestAppContext, VisualTestContext, WindowBounds, WindowOptions,
|
||||
Div, Flatten, TestAppContext, VisualTestContext, WindowBounds, WindowOptions,
|
||||
};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
@ -32,7 +32,7 @@ use util::{
|
||||
test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
|
||||
};
|
||||
use workspace::{
|
||||
item::{FollowEvent, FollowableEvents, FollowableItem, Item, ItemHandle},
|
||||
item::{FollowEvent, FollowableItem, Item, ItemHandle},
|
||||
NavigationEntry, ViewId,
|
||||
};
|
||||
|
||||
@ -345,7 +345,12 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
editor.update(cx, |view, cx| {
|
||||
view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::<f32>::zero(), cx);
|
||||
view.update_selection(
|
||||
DisplayPoint::new(3, 3),
|
||||
0,
|
||||
gpui::Point::<f32>::default(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
@ -356,7 +361,12 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
editor.update(cx, |view, cx| {
|
||||
view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::<f32>::zero(), cx);
|
||||
view.update_selection(
|
||||
DisplayPoint::new(1, 1),
|
||||
0,
|
||||
gpui::Point::<f32>::default(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
@ -368,7 +378,12 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) {
|
||||
|
||||
editor.update(cx, |view, cx| {
|
||||
view.end_selection(cx);
|
||||
view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::<f32>::zero(), cx);
|
||||
view.update_selection(
|
||||
DisplayPoint::new(3, 3),
|
||||
0,
|
||||
gpui::Point::<f32>::default(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
@ -380,7 +395,12 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) {
|
||||
|
||||
editor.update(cx, |view, cx| {
|
||||
view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx);
|
||||
view.update_selection(DisplayPoint::new(0, 0), 0, gpui::Point::<f32>::zero(), cx);
|
||||
view.update_selection(
|
||||
DisplayPoint::new(0, 0),
|
||||
0,
|
||||
gpui::Point::<f32>::default(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
@ -423,7 +443,12 @@ fn test_canceling_pending_selection(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
view.update(cx, |view, cx| {
|
||||
view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::<f32>::zero(), cx);
|
||||
view.update_selection(
|
||||
DisplayPoint::new(3, 3),
|
||||
0,
|
||||
gpui::Point::<f32>::default(),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
[DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
|
||||
@ -432,7 +457,12 @@ fn test_canceling_pending_selection(cx: &mut TestAppContext) {
|
||||
|
||||
view.update(cx, |view, cx| {
|
||||
view.cancel(&Cancel, cx);
|
||||
view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::<f32>::zero(), cx);
|
||||
view.update_selection(
|
||||
DisplayPoint::new(1, 1),
|
||||
0,
|
||||
gpui::Point::<f32>::default(),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
[DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
|
||||
@ -643,11 +673,21 @@ fn test_cancel(cx: &mut TestAppContext) {
|
||||
|
||||
view.update(cx, |view, cx| {
|
||||
view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx);
|
||||
view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::<f32>::zero(), cx);
|
||||
view.update_selection(
|
||||
DisplayPoint::new(1, 1),
|
||||
0,
|
||||
gpui::Point::<f32>::default(),
|
||||
cx,
|
||||
);
|
||||
view.end_selection(cx);
|
||||
|
||||
view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx);
|
||||
view.update_selection(DisplayPoint::new(0, 3), 0, gpui::Point::<f32>::zero(), cx);
|
||||
view.update_selection(
|
||||
DisplayPoint::new(0, 3),
|
||||
0,
|
||||
gpui::Point::<f32>::default(),
|
||||
cx,
|
||||
);
|
||||
view.end_selection(cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
@ -3238,9 +3278,7 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) {
|
||||
the lazy dog"});
|
||||
cx.update_editor(|e, cx| e.copy(&Copy, cx));
|
||||
assert_eq!(
|
||||
cx.test_platform
|
||||
.read_from_clipboard()
|
||||
.map(|item| item.text().to_owned()),
|
||||
cx.read_from_clipboard().map(|item| item.text().to_owned()),
|
||||
Some("fox jumps over\n".to_owned())
|
||||
);
|
||||
|
||||
@ -6478,7 +6516,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
|
||||
cx.subscribe(
|
||||
&follower.root_view(cx).unwrap(),
|
||||
move |_, _, event: &EditorEvent, cx| {
|
||||
if matches!(event.to_follow_event(), Some(FollowEvent::Unfollow)) {
|
||||
if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
|
||||
*is_still_following.borrow_mut() = false;
|
||||
}
|
||||
|
||||
|
@ -485,7 +485,7 @@ impl EditorElement {
|
||||
let modifiers = event.modifiers;
|
||||
if editor.has_pending_selection() && event.pressed_button == Some(MouseButton::Left) {
|
||||
let point_for_position = position_map.point_for_position(text_bounds, event.position);
|
||||
let mut scroll_delta = gpui::Point::<f32>::zero();
|
||||
let mut scroll_delta = gpui::Point::<f32>::default();
|
||||
let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0);
|
||||
let top = text_bounds.origin.y + vertical_margin;
|
||||
let bottom = text_bounds.lower_left().y - vertical_margin;
|
||||
@ -511,7 +511,7 @@ impl EditorElement {
|
||||
position: point_for_position.previous_valid,
|
||||
goal_column: point_for_position.exact_unclipped.column(),
|
||||
scroll_position: (position_map.snapshot.scroll_position() + scroll_delta)
|
||||
.clamp(&gpui::Point::zero(), &position_map.scroll_max),
|
||||
.clamp(&gpui::Point::default(), &position_map.scroll_max),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
@ -2803,35 +2803,48 @@ impl Element for EditorElement {
|
||||
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
let dispatch_context = self.editor.read(cx).dispatch_context(cx);
|
||||
cx.with_key_dispatch(dispatch_context, Some(focus_handle.clone()), |_, cx| {
|
||||
self.register_actions(cx);
|
||||
self.register_key_listeners(cx);
|
||||
cx.with_key_dispatch(
|
||||
Some(dispatch_context),
|
||||
Some(focus_handle.clone()),
|
||||
|_, cx| {
|
||||
self.register_actions(cx);
|
||||
self.register_key_listeners(cx);
|
||||
|
||||
// We call with_z_index to establish a new stacking context.
|
||||
cx.with_z_index(0, |cx| {
|
||||
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
|
||||
// Paint mouse listeners at z-index 0 so any elements we paint on top of the editor
|
||||
// take precedence.
|
||||
cx.with_z_index(0, |cx| {
|
||||
self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx);
|
||||
// We call with_z_index to establish a new stacking context.
|
||||
cx.with_z_index(0, |cx| {
|
||||
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
|
||||
// Paint mouse listeners at z-index 0 so any elements we paint on top of the editor
|
||||
// take precedence.
|
||||
cx.with_z_index(0, |cx| {
|
||||
self.paint_mouse_listeners(
|
||||
bounds,
|
||||
gutter_bounds,
|
||||
text_bounds,
|
||||
&layout,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
let input_handler =
|
||||
ElementInputHandler::new(bounds, self.editor.clone(), cx);
|
||||
cx.handle_input(&focus_handle, input_handler);
|
||||
|
||||
self.paint_background(gutter_bounds, text_bounds, &layout, cx);
|
||||
if layout.gutter_size.width > Pixels::ZERO {
|
||||
self.paint_gutter(gutter_bounds, &mut layout, cx);
|
||||
}
|
||||
self.paint_text(text_bounds, &mut layout, cx);
|
||||
|
||||
if !layout.blocks.is_empty() {
|
||||
cx.with_z_index(1, |cx| {
|
||||
cx.with_element_id(Some("editor_blocks"), |cx| {
|
||||
self.paint_blocks(bounds, &mut layout, cx);
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
let input_handler = ElementInputHandler::new(bounds, self.editor.clone(), cx);
|
||||
cx.handle_input(&focus_handle, input_handler);
|
||||
|
||||
self.paint_background(gutter_bounds, text_bounds, &layout, cx);
|
||||
if layout.gutter_size.width > Pixels::ZERO {
|
||||
self.paint_gutter(gutter_bounds, &mut layout, cx);
|
||||
}
|
||||
self.paint_text(text_bounds, &mut layout, cx);
|
||||
|
||||
if !layout.blocks.is_empty() {
|
||||
cx.with_element_id(Some("editor_blocks"), |cx| {
|
||||
self.paint_blocks(bounds, &mut layout, cx);
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,10 +32,10 @@ use std::{
|
||||
};
|
||||
use text::Selection;
|
||||
use theme::{ActiveTheme, Theme};
|
||||
use ui::{Color, Label};
|
||||
use ui::{h_stack, prelude::*, Label};
|
||||
use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle},
|
||||
item::{BreadcrumbText, FollowEvent, FollowableItemHandle},
|
||||
StatusItemView,
|
||||
};
|
||||
use workspace::{
|
||||
@ -46,27 +46,7 @@ use workspace::{
|
||||
|
||||
pub const MAX_TAB_TITLE_LEN: usize = 24;
|
||||
|
||||
impl FollowableEvents for EditorEvent {
|
||||
fn to_follow_event(&self) -> Option<workspace::item::FollowEvent> {
|
||||
match self {
|
||||
EditorEvent::Edited => Some(FollowEvent::Unfollow),
|
||||
EditorEvent::SelectionsChanged { local }
|
||||
| EditorEvent::ScrollPositionChanged { local, .. } => {
|
||||
if *local {
|
||||
Some(FollowEvent::Unfollow)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ItemEvent> for Editor {}
|
||||
|
||||
impl FollowableItem for Editor {
|
||||
type FollowableEvent = EditorEvent;
|
||||
fn remote_id(&self) -> Option<ViewId> {
|
||||
self.remote_id
|
||||
}
|
||||
@ -241,9 +221,24 @@ impl FollowableItem for Editor {
|
||||
}))
|
||||
}
|
||||
|
||||
fn to_follow_event(event: &EditorEvent) -> Option<workspace::item::FollowEvent> {
|
||||
match event {
|
||||
EditorEvent::Edited => Some(FollowEvent::Unfollow),
|
||||
EditorEvent::SelectionsChanged { local }
|
||||
| EditorEvent::ScrollPositionChanged { local, .. } => {
|
||||
if *local {
|
||||
Some(FollowEvent::Unfollow)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_event_to_update_proto(
|
||||
&self,
|
||||
event: &Self::FollowableEvent,
|
||||
event: &EditorEvent,
|
||||
update: &mut Option<proto::update_view::Variant>,
|
||||
cx: &WindowContext,
|
||||
) -> bool {
|
||||
@ -528,6 +523,8 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor)
|
||||
}
|
||||
|
||||
impl Item for Editor {
|
||||
type Event = EditorEvent;
|
||||
|
||||
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
if let Ok(data) = data.downcast::<NavigationData>() {
|
||||
let newest_selection = self.selections.newest::<Point>(cx);
|
||||
@ -586,28 +583,25 @@ impl Item for Editor {
|
||||
fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
|
||||
AnyElement::new(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(Label::new(self.title(cx).to_string()))
|
||||
.children(detail.and_then(|detail| {
|
||||
let path = path_for_buffer(&self.buffer, detail, false, cx)?;
|
||||
let description = path.to_string_lossy();
|
||||
let description = detail.and_then(|detail| {
|
||||
let path = path_for_buffer(&self.buffer, detail, false, cx)?;
|
||||
let description = path.to_string_lossy();
|
||||
let description = description.trim();
|
||||
|
||||
Some(
|
||||
div().child(
|
||||
Label::new(util::truncate_and_trailoff(
|
||||
&description,
|
||||
MAX_TAB_TITLE_LEN,
|
||||
))
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
})),
|
||||
)
|
||||
if description.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN))
|
||||
});
|
||||
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.child(Label::new(self.title(cx).to_string()))
|
||||
.when_some(description, |this, description| {
|
||||
this.child(Label::new(description).color(Color::Muted))
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn for_each_project_item(
|
||||
@ -841,6 +835,40 @@ impl Item for Editor {
|
||||
Some("Editor")
|
||||
}
|
||||
|
||||
fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) {
|
||||
match event {
|
||||
EditorEvent::Closed => f(ItemEvent::CloseItem),
|
||||
|
||||
EditorEvent::Saved | EditorEvent::TitleChanged => {
|
||||
f(ItemEvent::UpdateTab);
|
||||
f(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::Reparsed => {
|
||||
f(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::SelectionsChanged { local } if *local => {
|
||||
f(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::DirtyChanged => {
|
||||
f(ItemEvent::UpdateTab);
|
||||
}
|
||||
|
||||
EditorEvent::BufferEdited => {
|
||||
f(ItemEvent::Edit);
|
||||
f(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => {
|
||||
f(ItemEvent::Edit);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
project: Model<Project>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
|
@ -6,7 +6,7 @@ use gpui::{
|
||||
};
|
||||
use text::{Bias, Point};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{h_stack, v_stack, Color, Label, StyledExt};
|
||||
use ui::{h_stack, prelude::*, v_stack, Label};
|
||||
use util::paths::FILE_ROW_COLUMN_DELIMITER;
|
||||
|
||||
actions!(Toggle);
|
||||
|
@ -15,10 +15,10 @@ use smol::future::FutureExt;
|
||||
pub use test_context::*;
|
||||
|
||||
use crate::{
|
||||
current_platform, image_cache::ImageCache, Action, ActionRegistry, Any, AnyView,
|
||||
AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
|
||||
current_platform, image_cache::ImageCache, init_app_menus, Action, ActionRegistry, Any,
|
||||
AnyView, AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
|
||||
DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId,
|
||||
ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform,
|
||||
ForegroundExecutor, KeyBinding, Keymap, LayoutId, Menu, PathPromptOptions, Pixels, Platform,
|
||||
PlatformDisplay, Point, Render, SharedString, SubscriberSet, Subscription, SvgRenderer, Task,
|
||||
TextStyle, TextStyleRefinement, TextSystem, View, ViewContext, Window, WindowContext,
|
||||
WindowHandle, WindowId,
|
||||
@ -39,7 +39,10 @@ use std::{
|
||||
sync::{atomic::Ordering::SeqCst, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
use util::http::{self, HttpClient};
|
||||
use util::{
|
||||
http::{self, HttpClient},
|
||||
ResultExt,
|
||||
};
|
||||
|
||||
/// Temporary(?) wrapper around RefCell<AppContext> to help us debug any double borrows.
|
||||
/// Strongly consider removing after stabilization.
|
||||
@ -201,7 +204,7 @@ pub struct AppContext {
|
||||
pub(crate) windows: SlotMap<WindowId, Option<Window>>,
|
||||
pub(crate) keymap: Arc<Mutex<Keymap>>,
|
||||
pub(crate) global_action_listeners:
|
||||
HashMap<TypeId, Vec<Box<dyn Fn(&dyn Action, DispatchPhase, &mut Self)>>>,
|
||||
HashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
|
||||
pending_effects: VecDeque<Effect>,
|
||||
pub(crate) pending_notifications: HashSet<EntityId>,
|
||||
pub(crate) pending_global_notifications: HashSet<TypeId>,
|
||||
@ -275,6 +278,8 @@ impl AppContext {
|
||||
}),
|
||||
});
|
||||
|
||||
init_app_menus(platform.as_ref(), &mut *app.borrow_mut());
|
||||
|
||||
platform.on_quit(Box::new({
|
||||
let cx = app.clone();
|
||||
move || {
|
||||
@ -425,6 +430,10 @@ impl AppContext {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
self.platform.active_window()
|
||||
}
|
||||
|
||||
/// Opens a new window with the given option and the root view returned by the given function.
|
||||
/// The function is invoked with a `WindowContext`, which can be used to interact with window-specific
|
||||
/// functionality.
|
||||
@ -851,7 +860,6 @@ impl AppContext {
|
||||
}
|
||||
|
||||
/// Remove the global of the given type from the app context. Does not notify global observers.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn remove_global<G: Any>(&mut self) -> G {
|
||||
let global_type = TypeId::of::<G>();
|
||||
*self
|
||||
@ -962,9 +970,9 @@ impl AppContext {
|
||||
self.global_action_listeners
|
||||
.entry(TypeId::of::<A>())
|
||||
.or_default()
|
||||
.push(Box::new(move |action, phase, cx| {
|
||||
.push(Rc::new(move |action, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble {
|
||||
let action = action.as_any().downcast_ref().unwrap();
|
||||
let action = action.downcast_ref().unwrap();
|
||||
listener(action, cx)
|
||||
}
|
||||
}));
|
||||
@ -1015,6 +1023,90 @@ impl AppContext {
|
||||
activate();
|
||||
subscription
|
||||
}
|
||||
|
||||
pub(crate) fn clear_pending_keystrokes(&mut self) {
|
||||
for window in self.windows() {
|
||||
window
|
||||
.update(self, |_, cx| {
|
||||
cx.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.clear_pending_keystrokes()
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_action_available(&mut self, action: &dyn Action) -> bool {
|
||||
if let Some(window) = self.active_window() {
|
||||
if let Ok(window_action_available) =
|
||||
window.update(self, |_, cx| cx.is_action_available(action))
|
||||
{
|
||||
return window_action_available;
|
||||
}
|
||||
}
|
||||
|
||||
self.global_action_listeners
|
||||
.contains_key(&action.as_any().type_id())
|
||||
}
|
||||
|
||||
pub fn set_menus(&mut self, menus: Vec<Menu>) {
|
||||
self.platform.set_menus(menus, &self.keymap.lock());
|
||||
}
|
||||
|
||||
pub fn dispatch_action(&mut self, action: &dyn Action) {
|
||||
if let Some(active_window) = self.active_window() {
|
||||
active_window
|
||||
.update(self, |_, cx| cx.dispatch_action(action.boxed_clone()))
|
||||
.log_err();
|
||||
} else {
|
||||
self.propagate_event = true;
|
||||
|
||||
if let Some(mut global_listeners) = self
|
||||
.global_action_listeners
|
||||
.remove(&action.as_any().type_id())
|
||||
{
|
||||
for listener in &global_listeners {
|
||||
listener(action.as_any(), DispatchPhase::Capture, self);
|
||||
if !self.propagate_event {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
global_listeners.extend(
|
||||
self.global_action_listeners
|
||||
.remove(&action.as_any().type_id())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
self.global_action_listeners
|
||||
.insert(action.as_any().type_id(), global_listeners);
|
||||
}
|
||||
|
||||
if self.propagate_event {
|
||||
if let Some(mut global_listeners) = self
|
||||
.global_action_listeners
|
||||
.remove(&action.as_any().type_id())
|
||||
{
|
||||
for listener in global_listeners.iter().rev() {
|
||||
listener(action.as_any(), DispatchPhase::Bubble, self);
|
||||
if !self.propagate_event {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
global_listeners.extend(
|
||||
self.global_action_listeners
|
||||
.remove(&action.as_any().type_id())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
self.global_action_listeners
|
||||
.insert(action.as_any().type_id(), global_listeners);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Context for AppContext {
|
||||
|
@ -1,9 +1,10 @@
|
||||
use crate::{
|
||||
div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
|
||||
BackgroundExecutor, Bounds, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent,
|
||||
KeyDownEvent, Keystroke, Model, ModelContext, Pixels, PlatformWindow, Point, Render, Result,
|
||||
Size, Task, TestDispatcher, TestPlatform, TestWindow, TestWindowHandlers, View, ViewContext,
|
||||
VisualContext, WindowBounds, WindowContext, WindowHandle, WindowOptions,
|
||||
BackgroundExecutor, Bounds, ClipboardItem, Context, Div, Entity, EventEmitter,
|
||||
ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model, ModelContext, Pixels, Platform,
|
||||
PlatformWindow, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow,
|
||||
TestWindowHandlers, TextSystem, View, ViewContext, VisualContext, WindowBounds, WindowContext,
|
||||
WindowHandle, WindowOptions,
|
||||
};
|
||||
use anyhow::{anyhow, bail};
|
||||
use futures::{Stream, StreamExt};
|
||||
@ -16,6 +17,7 @@ pub struct TestAppContext {
|
||||
pub foreground_executor: ForegroundExecutor,
|
||||
pub dispatcher: TestDispatcher,
|
||||
pub test_platform: Rc<TestPlatform>,
|
||||
text_system: Arc<TextSystem>,
|
||||
}
|
||||
|
||||
impl Context for TestAppContext {
|
||||
@ -82,6 +84,7 @@ impl TestAppContext {
|
||||
let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
|
||||
let asset_source = Arc::new(());
|
||||
let http_client = util::http::FakeHttpClient::with_404_response();
|
||||
let text_system = Arc::new(TextSystem::new(platform.text_system()));
|
||||
|
||||
Self {
|
||||
app: AppContext::new(platform.clone(), asset_source, http_client),
|
||||
@ -89,6 +92,7 @@ impl TestAppContext {
|
||||
foreground_executor,
|
||||
dispatcher: dispatcher.clone(),
|
||||
test_platform: platform,
|
||||
text_system,
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,6 +159,18 @@ impl TestAppContext {
|
||||
(view, Box::leak(cx))
|
||||
}
|
||||
|
||||
pub fn text_system(&self) -> &Arc<TextSystem> {
|
||||
&self.text_system
|
||||
}
|
||||
|
||||
pub fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
self.test_platform.write_to_clipboard(item)
|
||||
}
|
||||
|
||||
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.test_platform.read_from_clipboard()
|
||||
}
|
||||
|
||||
pub fn simulate_new_path_selection(
|
||||
&self,
|
||||
select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
|
||||
@ -529,6 +545,10 @@ pub struct VisualTestContext<'a> {
|
||||
}
|
||||
|
||||
impl<'a> VisualTestContext<'a> {
|
||||
pub fn update<R>(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R {
|
||||
self.cx.update_window(self.window, |_, cx| f(cx)).unwrap()
|
||||
}
|
||||
|
||||
pub fn from_window(window: AnyWindowHandle, cx: &'a mut TestAppContext) -> Self {
|
||||
Self { cx, window }
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
use crate::{Bounds, Element, IntoElement, Pixels, StyleRefinement, Styled, WindowContext};
|
||||
use refineable::Refineable as _;
|
||||
|
||||
use crate::{Bounds, Element, IntoElement, Pixels, Style, StyleRefinement, Styled, WindowContext};
|
||||
|
||||
pub fn canvas(callback: impl 'static + FnOnce(Bounds<Pixels>, &mut WindowContext)) -> Canvas {
|
||||
Canvas {
|
||||
paint_callback: Box::new(callback),
|
||||
style: Default::default(),
|
||||
style: StyleRefinement::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,7 +34,9 @@ impl Element for Canvas {
|
||||
_: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (crate::LayoutId, Self::State) {
|
||||
let layout_id = cx.request_layout(&self.style.clone().into(), []);
|
||||
let mut style = Style::default();
|
||||
style.refine(&self.style);
|
||||
let layout_id = cx.request_layout(&style, []);
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,7 @@ pub trait InteractiveElement: Sized + Element {
|
||||
E: Debug,
|
||||
{
|
||||
if let Some(key_context) = key_context.try_into().log_err() {
|
||||
self.interactivity().key_context = key_context;
|
||||
self.interactivity().key_context = Some(key_context);
|
||||
}
|
||||
self
|
||||
}
|
||||
@ -737,7 +737,7 @@ impl DivState {
|
||||
|
||||
pub struct Interactivity {
|
||||
pub element_id: Option<ElementId>,
|
||||
pub key_context: KeyContext,
|
||||
pub key_context: Option<KeyContext>,
|
||||
pub focusable: bool,
|
||||
pub tracked_focus_handle: Option<FocusHandle>,
|
||||
pub scroll_handle: Option<ScrollHandle>,
|
||||
@ -1276,7 +1276,7 @@ impl Default for Interactivity {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
element_id: None,
|
||||
key_context: KeyContext::default(),
|
||||
key_context: None,
|
||||
focusable: false,
|
||||
tracked_focus_handle: None,
|
||||
scroll_handle: None,
|
||||
|
@ -102,7 +102,7 @@ impl Element for Overlay {
|
||||
|
||||
let mut desired = self.anchor_corner.get_bounds(origin, size);
|
||||
let limits = Bounds {
|
||||
origin: Point::zero(),
|
||||
origin: Point::default(),
|
||||
size: cx.viewport_size(),
|
||||
};
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -28,7 +28,7 @@ pub(crate) struct DispatchTree {
|
||||
pub(crate) struct DispatchNode {
|
||||
pub key_listeners: SmallVec<[KeyListener; 2]>,
|
||||
pub action_listeners: SmallVec<[DispatchActionListener; 16]>,
|
||||
pub context: KeyContext,
|
||||
pub context: Option<KeyContext>,
|
||||
parent: Option<DispatchNodeId>,
|
||||
}
|
||||
|
||||
@ -61,7 +61,7 @@ impl DispatchTree {
|
||||
self.keystroke_matchers.clear();
|
||||
}
|
||||
|
||||
pub fn push_node(&mut self, context: KeyContext) {
|
||||
pub fn push_node(&mut self, context: Option<KeyContext>) {
|
||||
let parent = self.node_stack.last().copied();
|
||||
let node_id = DispatchNodeId(self.nodes.len());
|
||||
self.nodes.push(DispatchNode {
|
||||
@ -69,34 +69,34 @@ impl DispatchTree {
|
||||
..Default::default()
|
||||
});
|
||||
self.node_stack.push(node_id);
|
||||
if !context.is_empty() {
|
||||
self.active_node().context = context.clone();
|
||||
if let Some(context) = context {
|
||||
self.active_node().context = Some(context.clone());
|
||||
self.context_stack.push(context);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop_node(&mut self) {
|
||||
let node_id = self.node_stack.pop().unwrap();
|
||||
if !self.nodes[node_id.0].context.is_empty() {
|
||||
if self.nodes[node_id.0].context.is_some() {
|
||||
self.context_stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_keystroke_matchers(&mut self) {
|
||||
pub fn clear_pending_keystrokes(&mut self) {
|
||||
self.keystroke_matchers.clear();
|
||||
}
|
||||
|
||||
/// Preserve keystroke matchers from previous frames to support multi-stroke
|
||||
/// bindings across multiple frames.
|
||||
pub fn preserve_keystroke_matchers(&mut self, old_tree: &mut Self, focus_id: Option<FocusId>) {
|
||||
pub fn preserve_pending_keystrokes(&mut self, old_tree: &mut Self, focus_id: Option<FocusId>) {
|
||||
if let Some(node_id) = focus_id.and_then(|focus_id| self.focusable_node_id(focus_id)) {
|
||||
let dispatch_path = self.dispatch_path(node_id);
|
||||
|
||||
self.context_stack.clear();
|
||||
for node_id in dispatch_path {
|
||||
let node = self.node(node_id);
|
||||
if !node.context.is_empty() {
|
||||
self.context_stack.push(node.context.clone());
|
||||
if let Some(context) = node.context.clone() {
|
||||
self.context_stack.push(context);
|
||||
}
|
||||
|
||||
if let Some((context_stack, matcher)) = old_tree
|
||||
@ -148,21 +148,33 @@ impl DispatchTree {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn available_actions(&self, target: FocusId) -> Vec<Box<dyn Action>> {
|
||||
pub fn available_actions(&self, target: DispatchNodeId) -> Vec<Box<dyn Action>> {
|
||||
let mut actions = Vec::new();
|
||||
if let Some(node) = self.focusable_node_ids.get(&target) {
|
||||
for node_id in self.dispatch_path(*node) {
|
||||
let node = &self.nodes[node_id.0];
|
||||
for DispatchActionListener { action_type, .. } in &node.action_listeners {
|
||||
// Intentionally silence these errors without logging.
|
||||
// If an action cannot be built by default, it's not available.
|
||||
actions.extend(self.action_registry.build_action_type(action_type).ok());
|
||||
}
|
||||
for node_id in self.dispatch_path(target) {
|
||||
let node = &self.nodes[node_id.0];
|
||||
for DispatchActionListener { action_type, .. } in &node.action_listeners {
|
||||
// Intentionally silence these errors without logging.
|
||||
// If an action cannot be built by default, it's not available.
|
||||
actions.extend(self.action_registry.build_action_type(action_type).ok());
|
||||
}
|
||||
}
|
||||
actions
|
||||
}
|
||||
|
||||
pub fn is_action_available(&self, action: &dyn Action, target: DispatchNodeId) -> bool {
|
||||
for node_id in self.dispatch_path(target) {
|
||||
let node = &self.nodes[node_id.0];
|
||||
if node
|
||||
.action_listeners
|
||||
.iter()
|
||||
.any(|listener| listener.action_type == action.as_any().type_id())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn bindings_for_action(
|
||||
&self,
|
||||
action: &dyn Action,
|
||||
@ -236,6 +248,11 @@ impl DispatchTree {
|
||||
self.focusable_node_ids.get(&target).copied()
|
||||
}
|
||||
|
||||
pub fn root_node_id(&self) -> DispatchNodeId {
|
||||
debug_assert!(!self.nodes.is_empty());
|
||||
DispatchNodeId(0)
|
||||
}
|
||||
|
||||
fn active_node_id(&self) -> DispatchNodeId {
|
||||
*self.node_stack.last().unwrap()
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
mod app_menu;
|
||||
mod keystroke;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac;
|
||||
@ -5,10 +6,10 @@ mod mac;
|
||||
mod test;
|
||||
|
||||
use crate::{
|
||||
point, size, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId,
|
||||
FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, LineLayout,
|
||||
Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene,
|
||||
SharedString, Size, TaskLabel,
|
||||
point, size, Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId,
|
||||
FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, Keymap,
|
||||
LineLayout, Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result,
|
||||
Scene, SharedString, Size, TaskLabel,
|
||||
};
|
||||
use anyhow::{anyhow, bail};
|
||||
use async_task::Runnable;
|
||||
@ -32,6 +33,7 @@ use std::{
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub use app_menu::*;
|
||||
pub use keystroke::*;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use mac::*;
|
||||
@ -44,7 +46,7 @@ pub(crate) fn current_platform() -> Rc<dyn Platform> {
|
||||
Rc::new(MacPlatform::new())
|
||||
}
|
||||
|
||||
pub trait Platform: 'static {
|
||||
pub(crate) trait Platform: 'static {
|
||||
fn background_executor(&self) -> BackgroundExecutor;
|
||||
fn foreground_executor(&self) -> ForegroundExecutor;
|
||||
fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
|
||||
@ -59,7 +61,7 @@ pub trait Platform: 'static {
|
||||
|
||||
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
|
||||
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
|
||||
fn main_window(&self) -> Option<AnyWindowHandle>;
|
||||
fn active_window(&self) -> Option<AnyWindowHandle>;
|
||||
fn open_window(
|
||||
&self,
|
||||
handle: AnyWindowHandle,
|
||||
@ -90,6 +92,11 @@ pub trait Platform: 'static {
|
||||
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_event(&self, callback: Box<dyn FnMut(InputEvent) -> bool>);
|
||||
|
||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
||||
|
||||
fn os_name(&self) -> &'static str;
|
||||
fn os_version(&self) -> Result<SemanticVersion>;
|
||||
fn app_version(&self) -> Result<SemanticVersion>;
|
||||
|
77
crates/gpui2/src/platform/app_menu.rs
Normal file
77
crates/gpui2/src/platform/app_menu.rs
Normal file
@ -0,0 +1,77 @@
|
||||
use crate::{Action, AppContext, Platform};
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct Menu<'a> {
|
||||
pub name: &'a str,
|
||||
pub items: Vec<MenuItem<'a>>,
|
||||
}
|
||||
|
||||
pub enum MenuItem<'a> {
|
||||
Separator,
|
||||
Submenu(Menu<'a>),
|
||||
Action {
|
||||
name: &'a str,
|
||||
action: Box<dyn Action>,
|
||||
os_action: Option<OsAction>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> MenuItem<'a> {
|
||||
pub fn separator() -> Self {
|
||||
Self::Separator
|
||||
}
|
||||
|
||||
pub fn submenu(menu: Menu<'a>) -> Self {
|
||||
Self::Submenu(menu)
|
||||
}
|
||||
|
||||
pub fn action(name: &'a str, action: impl Action) -> Self {
|
||||
Self::Action {
|
||||
name,
|
||||
action: Box::new(action),
|
||||
os_action: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn os_action(name: &'a str, action: impl Action, os_action: OsAction) -> Self {
|
||||
Self::Action {
|
||||
name,
|
||||
action: Box::new(action),
|
||||
os_action: Some(os_action),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub enum OsAction {
|
||||
Cut,
|
||||
Copy,
|
||||
Paste,
|
||||
SelectAll,
|
||||
Undo,
|
||||
Redo,
|
||||
}
|
||||
|
||||
pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &mut AppContext) {
|
||||
platform.on_will_open_app_menu(Box::new({
|
||||
let cx = cx.to_async();
|
||||
move || {
|
||||
cx.update(|cx| cx.clear_pending_keystrokes()).ok();
|
||||
}
|
||||
}));
|
||||
|
||||
platform.on_validate_app_menu_command(Box::new({
|
||||
let cx = cx.to_async();
|
||||
move |action| {
|
||||
cx.update(|cx| cx.is_action_available(action))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}));
|
||||
|
||||
platform.on_app_menu_action(Box::new({
|
||||
let cx = cx.to_async();
|
||||
move |action| {
|
||||
cx.update(|cx| cx.dispatch_action(action)).log_err();
|
||||
}
|
||||
}));
|
||||
}
|
@ -7,6 +7,7 @@ use std::{
|
||||
use crate::DisplayId;
|
||||
use collections::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
pub use sys::CVSMPTETime as SmtpeTime;
|
||||
pub use sys::CVTimeStamp as VideoTimestamp;
|
||||
|
||||
pub(crate) struct MacDisplayLinker {
|
||||
@ -153,7 +154,7 @@ mod sys {
|
||||
kCVTimeStampTopField | kCVTimeStampBottomField;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct CVSMPTETime {
|
||||
pub subframes: i16,
|
||||
pub subframe_divisor: i16,
|
||||
|
@ -1,18 +1,19 @@
|
||||
use super::BoolExt;
|
||||
use super::{events::key_to_native, BoolExt};
|
||||
use crate::{
|
||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor,
|
||||
InputEvent, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem, MacWindow,
|
||||
PathPromptOptions, Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow, Result,
|
||||
SemanticVersion, VideoTimestamp, WindowOptions,
|
||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
|
||||
ForegroundExecutor, InputEvent, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker,
|
||||
MacTextSystem, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
|
||||
PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use block::ConcreteBlock;
|
||||
use cocoa::{
|
||||
appkit::{
|
||||
NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
|
||||
NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypeString, NSSavePanel, NSWindow,
|
||||
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
|
||||
NSPasteboardTypeString, NSSavePanel, NSWindow,
|
||||
},
|
||||
base::{id, nil, BOOL, YES},
|
||||
base::{id, nil, selector, BOOL, YES},
|
||||
foundation::{
|
||||
NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSString,
|
||||
NSUInteger, NSURL,
|
||||
@ -155,12 +156,12 @@ pub struct MacPlatformState {
|
||||
reopen: Option<Box<dyn FnMut()>>,
|
||||
quit: Option<Box<dyn FnMut()>>,
|
||||
event: Option<Box<dyn FnMut(InputEvent) -> bool>>,
|
||||
// menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
|
||||
// validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
|
||||
menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
|
||||
validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
|
||||
will_open_menu: Option<Box<dyn FnMut()>>,
|
||||
menu_actions: Vec<Box<dyn Action>>,
|
||||
open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
|
||||
finish_launching: Option<Box<dyn FnOnce()>>,
|
||||
// menu_actions: Vec<Box<dyn Action>>,
|
||||
}
|
||||
|
||||
impl MacPlatform {
|
||||
@ -179,12 +180,12 @@ impl MacPlatform {
|
||||
reopen: None,
|
||||
quit: None,
|
||||
event: None,
|
||||
menu_command: None,
|
||||
validate_menu_command: None,
|
||||
will_open_menu: None,
|
||||
menu_actions: Default::default(),
|
||||
open_urls: None,
|
||||
finish_launching: None,
|
||||
// menu_command: None,
|
||||
// validate_menu_command: None,
|
||||
// menu_actions: Default::default(),
|
||||
}))
|
||||
}
|
||||
|
||||
@ -200,151 +201,153 @@ impl MacPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
// unsafe fn create_menu_bar(
|
||||
// &self,
|
||||
// menus: Vec<Menu>,
|
||||
// delegate: id,
|
||||
// actions: &mut Vec<Box<dyn Action>>,
|
||||
// keystroke_matcher: &KeymapMatcher,
|
||||
// ) -> id {
|
||||
// let application_menu = NSMenu::new(nil).autorelease();
|
||||
// application_menu.setDelegate_(delegate);
|
||||
unsafe fn create_menu_bar(
|
||||
&self,
|
||||
menus: Vec<Menu>,
|
||||
delegate: id,
|
||||
actions: &mut Vec<Box<dyn Action>>,
|
||||
keymap: &Keymap,
|
||||
) -> id {
|
||||
let application_menu = NSMenu::new(nil).autorelease();
|
||||
application_menu.setDelegate_(delegate);
|
||||
|
||||
// for menu_config in menus {
|
||||
// let menu = NSMenu::new(nil).autorelease();
|
||||
// menu.setTitle_(ns_string(menu_config.name));
|
||||
// menu.setDelegate_(delegate);
|
||||
for menu_config in menus {
|
||||
let menu = NSMenu::new(nil).autorelease();
|
||||
menu.setTitle_(ns_string(menu_config.name));
|
||||
menu.setDelegate_(delegate);
|
||||
|
||||
// for item_config in menu_config.items {
|
||||
// menu.addItem_(self.create_menu_item(
|
||||
// item_config,
|
||||
// delegate,
|
||||
// actions,
|
||||
// keystroke_matcher,
|
||||
// ));
|
||||
// }
|
||||
for item_config in menu_config.items {
|
||||
menu.addItem_(self.create_menu_item(item_config, delegate, actions, keymap));
|
||||
}
|
||||
|
||||
// let menu_item = NSMenuItem::new(nil).autorelease();
|
||||
// menu_item.setSubmenu_(menu);
|
||||
// application_menu.addItem_(menu_item);
|
||||
let menu_item = NSMenuItem::new(nil).autorelease();
|
||||
menu_item.setSubmenu_(menu);
|
||||
application_menu.addItem_(menu_item);
|
||||
|
||||
// if menu_config.name == "Window" {
|
||||
// let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
// app.setWindowsMenu_(menu);
|
||||
// }
|
||||
// }
|
||||
if menu_config.name == "Window" {
|
||||
let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
app.setWindowsMenu_(menu);
|
||||
}
|
||||
}
|
||||
|
||||
// application_menu
|
||||
// }
|
||||
application_menu
|
||||
}
|
||||
|
||||
// unsafe fn create_menu_item(
|
||||
// &self,
|
||||
// item: MenuItem,
|
||||
// delegate: id,
|
||||
// actions: &mut Vec<Box<dyn Action>>,
|
||||
// keystroke_matcher: &KeymapMatcher,
|
||||
// ) -> id {
|
||||
// match item {
|
||||
// MenuItem::Separator => NSMenuItem::separatorItem(nil),
|
||||
// MenuItem::Action {
|
||||
// name,
|
||||
// action,
|
||||
// os_action,
|
||||
// } => {
|
||||
// // TODO
|
||||
// let keystrokes = keystroke_matcher
|
||||
// .bindings_for_action(action.id())
|
||||
// .find(|binding| binding.action().eq(action.as_ref()))
|
||||
// .map(|binding| binding.keystrokes());
|
||||
// let selector = match os_action {
|
||||
// Some(crate::OsAction::Cut) => selector("cut:"),
|
||||
// Some(crate::OsAction::Copy) => selector("copy:"),
|
||||
// Some(crate::OsAction::Paste) => selector("paste:"),
|
||||
// Some(crate::OsAction::SelectAll) => selector("selectAll:"),
|
||||
// Some(crate::OsAction::Undo) => selector("undo:"),
|
||||
// Some(crate::OsAction::Redo) => selector("redo:"),
|
||||
// None => selector("handleGPUIMenuItem:"),
|
||||
// };
|
||||
unsafe fn create_menu_item(
|
||||
&self,
|
||||
item: MenuItem,
|
||||
delegate: id,
|
||||
actions: &mut Vec<Box<dyn Action>>,
|
||||
keymap: &Keymap,
|
||||
) -> id {
|
||||
match item {
|
||||
MenuItem::Separator => NSMenuItem::separatorItem(nil),
|
||||
MenuItem::Action {
|
||||
name,
|
||||
action,
|
||||
os_action,
|
||||
} => {
|
||||
let keystrokes = keymap
|
||||
.bindings_for_action(action.type_id())
|
||||
.find(|binding| binding.action().partial_eq(action.as_ref()))
|
||||
.map(|binding| binding.keystrokes());
|
||||
|
||||
// let item;
|
||||
// if let Some(keystrokes) = keystrokes {
|
||||
// if keystrokes.len() == 1 {
|
||||
// let keystroke = &keystrokes[0];
|
||||
// let mut mask = NSEventModifierFlags::empty();
|
||||
// for (modifier, flag) in &[
|
||||
// (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
|
||||
// (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask),
|
||||
// (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask),
|
||||
// (keystroke.shift, NSEventModifierFlags::NSShiftKeyMask),
|
||||
// ] {
|
||||
// if *modifier {
|
||||
// mask |= *flag;
|
||||
// }
|
||||
// }
|
||||
let selector = match os_action {
|
||||
Some(crate::OsAction::Cut) => selector("cut:"),
|
||||
Some(crate::OsAction::Copy) => selector("copy:"),
|
||||
Some(crate::OsAction::Paste) => selector("paste:"),
|
||||
Some(crate::OsAction::SelectAll) => selector("selectAll:"),
|
||||
Some(crate::OsAction::Undo) => selector("undo:"),
|
||||
Some(crate::OsAction::Redo) => selector("redo:"),
|
||||
None => selector("handleGPUIMenuItem:"),
|
||||
};
|
||||
|
||||
// item = NSMenuItem::alloc(nil)
|
||||
// .initWithTitle_action_keyEquivalent_(
|
||||
// ns_string(name),
|
||||
// selector,
|
||||
// ns_string(key_to_native(&keystroke.key).as_ref()),
|
||||
// )
|
||||
// .autorelease();
|
||||
// item.setKeyEquivalentModifierMask_(mask);
|
||||
// }
|
||||
// // For multi-keystroke bindings, render the keystroke as part of the title.
|
||||
// else {
|
||||
// use std::fmt::Write;
|
||||
let item;
|
||||
if let Some(keystrokes) = keystrokes {
|
||||
if keystrokes.len() == 1 {
|
||||
let keystroke = &keystrokes[0];
|
||||
let mut mask = NSEventModifierFlags::empty();
|
||||
for (modifier, flag) in &[
|
||||
(
|
||||
keystroke.modifiers.command,
|
||||
NSEventModifierFlags::NSCommandKeyMask,
|
||||
),
|
||||
(
|
||||
keystroke.modifiers.control,
|
||||
NSEventModifierFlags::NSControlKeyMask,
|
||||
),
|
||||
(
|
||||
keystroke.modifiers.alt,
|
||||
NSEventModifierFlags::NSAlternateKeyMask,
|
||||
),
|
||||
(
|
||||
keystroke.modifiers.shift,
|
||||
NSEventModifierFlags::NSShiftKeyMask,
|
||||
),
|
||||
] {
|
||||
if *modifier {
|
||||
mask |= *flag;
|
||||
}
|
||||
}
|
||||
|
||||
// let mut name = format!("{name} [");
|
||||
// for (i, keystroke) in keystrokes.iter().enumerate() {
|
||||
// if i > 0 {
|
||||
// name.push(' ');
|
||||
// }
|
||||
// write!(&mut name, "{}", keystroke).unwrap();
|
||||
// }
|
||||
// name.push(']');
|
||||
item = NSMenuItem::alloc(nil)
|
||||
.initWithTitle_action_keyEquivalent_(
|
||||
ns_string(name),
|
||||
selector,
|
||||
ns_string(key_to_native(&keystroke.key).as_ref()),
|
||||
)
|
||||
.autorelease();
|
||||
item.setKeyEquivalentModifierMask_(mask);
|
||||
}
|
||||
// For multi-keystroke bindings, render the keystroke as part of the title.
|
||||
else {
|
||||
use std::fmt::Write;
|
||||
|
||||
// item = NSMenuItem::alloc(nil)
|
||||
// .initWithTitle_action_keyEquivalent_(
|
||||
// ns_string(&name),
|
||||
// selector,
|
||||
// ns_string(""),
|
||||
// )
|
||||
// .autorelease();
|
||||
// }
|
||||
// } else {
|
||||
// item = NSMenuItem::alloc(nil)
|
||||
// .initWithTitle_action_keyEquivalent_(
|
||||
// ns_string(name),
|
||||
// selector,
|
||||
// ns_string(""),
|
||||
// )
|
||||
// .autorelease();
|
||||
// }
|
||||
let mut name = format!("{name} [");
|
||||
for (i, keystroke) in keystrokes.iter().enumerate() {
|
||||
if i > 0 {
|
||||
name.push(' ');
|
||||
}
|
||||
write!(&mut name, "{}", keystroke).unwrap();
|
||||
}
|
||||
name.push(']');
|
||||
|
||||
// let tag = actions.len() as NSInteger;
|
||||
// let _: () = msg_send![item, setTag: tag];
|
||||
// actions.push(action);
|
||||
// item
|
||||
// }
|
||||
// MenuItem::Submenu(Menu { name, items }) => {
|
||||
// let item = NSMenuItem::new(nil).autorelease();
|
||||
// let submenu = NSMenu::new(nil).autorelease();
|
||||
// submenu.setDelegate_(delegate);
|
||||
// for item in items {
|
||||
// submenu.addItem_(self.create_menu_item(
|
||||
// item,
|
||||
// delegate,
|
||||
// actions,
|
||||
// keystroke_matcher,
|
||||
// ));
|
||||
// }
|
||||
// item.setSubmenu_(submenu);
|
||||
// item.setTitle_(ns_string(name));
|
||||
// item
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
item = NSMenuItem::alloc(nil)
|
||||
.initWithTitle_action_keyEquivalent_(
|
||||
ns_string(&name),
|
||||
selector,
|
||||
ns_string(""),
|
||||
)
|
||||
.autorelease();
|
||||
}
|
||||
} else {
|
||||
item = NSMenuItem::alloc(nil)
|
||||
.initWithTitle_action_keyEquivalent_(
|
||||
ns_string(name),
|
||||
selector,
|
||||
ns_string(""),
|
||||
)
|
||||
.autorelease();
|
||||
}
|
||||
|
||||
let tag = actions.len() as NSInteger;
|
||||
let _: () = msg_send![item, setTag: tag];
|
||||
actions.push(action);
|
||||
item
|
||||
}
|
||||
MenuItem::Submenu(Menu { name, items }) => {
|
||||
let item = NSMenuItem::new(nil).autorelease();
|
||||
let submenu = NSMenu::new(nil).autorelease();
|
||||
submenu.setDelegate_(delegate);
|
||||
for item in items {
|
||||
submenu.addItem_(self.create_menu_item(item, delegate, actions, keymap));
|
||||
}
|
||||
item.setSubmenu_(submenu);
|
||||
item.setTitle_(ns_string(name));
|
||||
item
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Platform for MacPlatform {
|
||||
@ -479,8 +482,8 @@ impl Platform for MacPlatform {
|
||||
MacDisplay::find_by_id(id).map(|screen| Rc::new(screen) as Rc<_>)
|
||||
}
|
||||
|
||||
fn main_window(&self) -> Option<AnyWindowHandle> {
|
||||
MacWindow::main_window()
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
MacWindow::active_window()
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
@ -631,6 +634,18 @@ impl Platform for MacPlatform {
|
||||
self.0.lock().event = Some(callback);
|
||||
}
|
||||
|
||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
|
||||
self.0.lock().menu_command = Some(callback);
|
||||
}
|
||||
|
||||
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
|
||||
self.0.lock().will_open_menu = Some(callback);
|
||||
}
|
||||
|
||||
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
|
||||
self.0.lock().validate_menu_command = Some(callback);
|
||||
}
|
||||
|
||||
fn os_name(&self) -> &'static str {
|
||||
"macOS"
|
||||
}
|
||||
@ -673,6 +688,15 @@ impl Platform for MacPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap) {
|
||||
unsafe {
|
||||
let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
let mut state = self.0.lock();
|
||||
let actions = &mut state.menu_actions;
|
||||
app.setMainMenu_(self.create_menu_bar(menus, app.delegate(), actions, keymap));
|
||||
}
|
||||
}
|
||||
|
||||
fn local_timezone(&self) -> UtcOffset {
|
||||
unsafe {
|
||||
let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone];
|
||||
@ -681,32 +705,6 @@ impl Platform for MacPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
// fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>) {
|
||||
// self.0.lock().menu_command = Some(callback);
|
||||
// }
|
||||
|
||||
// fn on_will_open_menu(&self, callback: Box<dyn FnMut()>) {
|
||||
// self.0.lock().will_open_menu = Some(callback);
|
||||
// }
|
||||
|
||||
// fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
|
||||
// self.0.lock().validate_menu_command = Some(callback);
|
||||
// }
|
||||
|
||||
// fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &KeymapMatcher) {
|
||||
// unsafe {
|
||||
// let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
// let mut state = self.0.lock();
|
||||
// let actions = &mut state.menu_actions;
|
||||
// app.setMainMenu_(self.create_menu_bar(
|
||||
// menus,
|
||||
// app.delegate(),
|
||||
// actions,
|
||||
// keystroke_matcher,
|
||||
// ));
|
||||
// }
|
||||
// }
|
||||
|
||||
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
|
||||
unsafe {
|
||||
let bundle: id = NSBundle::mainBundle();
|
||||
@ -956,7 +954,7 @@ unsafe fn path_from_objc(path: id) -> PathBuf {
|
||||
PathBuf::from(path)
|
||||
}
|
||||
|
||||
unsafe fn get_foreground_platform(object: &mut Object) -> &MacPlatform {
|
||||
unsafe fn get_mac_platform(object: &mut Object) -> &MacPlatform {
|
||||
let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR);
|
||||
assert!(!platform_ptr.is_null());
|
||||
&*(platform_ptr as *const MacPlatform)
|
||||
@ -965,7 +963,7 @@ unsafe fn get_foreground_platform(object: &mut Object) -> &MacPlatform {
|
||||
extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) {
|
||||
unsafe {
|
||||
if let Some(event) = InputEvent::from_native(native_event, None) {
|
||||
let platform = get_foreground_platform(this);
|
||||
let platform = get_mac_platform(this);
|
||||
if let Some(callback) = platform.0.lock().event.as_mut() {
|
||||
if !callback(event) {
|
||||
return;
|
||||
@ -981,7 +979,7 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
|
||||
let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
|
||||
|
||||
let platform = get_foreground_platform(this);
|
||||
let platform = get_mac_platform(this);
|
||||
let callback = platform.0.lock().finish_launching.take();
|
||||
if let Some(callback) = callback {
|
||||
callback();
|
||||
@ -991,7 +989,7 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
|
||||
|
||||
extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_windows: bool) {
|
||||
if !has_open_windows {
|
||||
let platform = unsafe { get_foreground_platform(this) };
|
||||
let platform = unsafe { get_mac_platform(this) };
|
||||
if let Some(callback) = platform.0.lock().reopen.as_mut() {
|
||||
callback();
|
||||
}
|
||||
@ -999,21 +997,21 @@ extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_wi
|
||||
}
|
||||
|
||||
extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) {
|
||||
let platform = unsafe { get_foreground_platform(this) };
|
||||
let platform = unsafe { get_mac_platform(this) };
|
||||
if let Some(callback) = platform.0.lock().become_active.as_mut() {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) {
|
||||
let platform = unsafe { get_foreground_platform(this) };
|
||||
let platform = unsafe { get_mac_platform(this) };
|
||||
if let Some(callback) = platform.0.lock().resign_active.as_mut() {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
|
||||
let platform = unsafe { get_foreground_platform(this) };
|
||||
let platform = unsafe { get_mac_platform(this) };
|
||||
if let Some(callback) = platform.0.lock().quit.as_mut() {
|
||||
callback();
|
||||
}
|
||||
@ -1035,49 +1033,47 @@ extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) {
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let platform = unsafe { get_foreground_platform(this) };
|
||||
let platform = unsafe { get_mac_platform(this) };
|
||||
if let Some(callback) = platform.0.lock().open_urls.as_mut() {
|
||||
callback(urls);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn handle_menu_item(__this: &mut Object, _: Sel, __item: id) {
|
||||
todo!()
|
||||
// unsafe {
|
||||
// let platform = get_foreground_platform(this);
|
||||
// let mut platform = platform.0.lock();
|
||||
// if let Some(mut callback) = platform.menu_command.take() {
|
||||
// let tag: NSInteger = msg_send![item, tag];
|
||||
// let index = tag as usize;
|
||||
// if let Some(action) = platform.menu_actions.get(index) {
|
||||
// callback(action.as_ref());
|
||||
// }
|
||||
// platform.menu_command = Some(callback);
|
||||
// }
|
||||
// }
|
||||
extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
|
||||
unsafe {
|
||||
let platform = get_mac_platform(this);
|
||||
let mut platform = platform.0.lock();
|
||||
if let Some(mut callback) = platform.menu_command.take() {
|
||||
let tag: NSInteger = msg_send![item, tag];
|
||||
let index = tag as usize;
|
||||
if let Some(action) = platform.menu_actions.get(index) {
|
||||
callback(action.as_ref());
|
||||
}
|
||||
platform.menu_command = Some(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn validate_menu_item(__this: &mut Object, _: Sel, __item: id) -> bool {
|
||||
todo!()
|
||||
// unsafe {
|
||||
// let mut result = false;
|
||||
// let platform = get_foreground_platform(this);
|
||||
// let mut platform = platform.0.lock();
|
||||
// if let Some(mut callback) = platform.validate_menu_command.take() {
|
||||
// let tag: NSInteger = msg_send![item, tag];
|
||||
// let index = tag as usize;
|
||||
// if let Some(action) = platform.menu_actions.get(index) {
|
||||
// result = callback(action.as_ref());
|
||||
// }
|
||||
// platform.validate_menu_command = Some(callback);
|
||||
// }
|
||||
// result
|
||||
// }
|
||||
extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool {
|
||||
unsafe {
|
||||
let mut result = false;
|
||||
let platform = get_mac_platform(this);
|
||||
let mut platform = platform.0.lock();
|
||||
if let Some(mut callback) = platform.validate_menu_command.take() {
|
||||
let tag: NSInteger = msg_send![item, tag];
|
||||
let index = tag as usize;
|
||||
if let Some(action) = platform.menu_actions.get(index) {
|
||||
result = callback(action.as_ref());
|
||||
}
|
||||
platform.validate_menu_command = Some(callback);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) {
|
||||
unsafe {
|
||||
let platform = get_foreground_platform(this);
|
||||
let platform = get_mac_platform(this);
|
||||
let mut platform = platform.0.lock();
|
||||
if let Some(mut callback) = platform.will_open_menu.take() {
|
||||
callback();
|
||||
|
@ -662,7 +662,7 @@ impl MacWindow {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main_window() -> Option<AnyWindowHandle> {
|
||||
pub fn active_window() -> Option<AnyWindowHandle> {
|
||||
unsafe {
|
||||
let app = NSApplication::sharedApplication(nil);
|
||||
let main_window: id = msg_send![app, mainWindow];
|
||||
|
@ -15,7 +15,7 @@ impl TestDisplay {
|
||||
id: DisplayId(1),
|
||||
uuid: uuid::Uuid::new_v4(),
|
||||
bounds: Bounds::from_corners(
|
||||
Point::zero(),
|
||||
Point::default(),
|
||||
Point::new(GlobalPixels(1920.), GlobalPixels(1080.)),
|
||||
),
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor,
|
||||
Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions,
|
||||
Keymap, Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::VecDeque;
|
||||
@ -127,7 +127,7 @@ impl Platform for TestPlatform {
|
||||
self.displays().iter().find(|d| d.id() == id).cloned()
|
||||
}
|
||||
|
||||
fn main_window(&self) -> Option<crate::AnyWindowHandle> {
|
||||
fn active_window(&self) -> Option<crate::AnyWindowHandle> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
@ -147,18 +147,25 @@ impl Platform for TestPlatform {
|
||||
fn set_display_link_output_callback(
|
||||
&self,
|
||||
_display_id: DisplayId,
|
||||
_callback: Box<dyn FnMut(&crate::VideoTimestamp, &crate::VideoTimestamp) + Send>,
|
||||
mut callback: Box<dyn FnMut(&crate::VideoTimestamp, &crate::VideoTimestamp) + Send>,
|
||||
) {
|
||||
unimplemented!()
|
||||
let timestamp = crate::VideoTimestamp {
|
||||
version: 0,
|
||||
video_time_scale: 0,
|
||||
video_time: 0,
|
||||
host_time: 0,
|
||||
rate_scalar: 0.0,
|
||||
video_refresh_period: 0,
|
||||
smpte_time: crate::SmtpeTime::default(),
|
||||
flags: 0,
|
||||
reserved: 0,
|
||||
};
|
||||
callback(×tamp, ×tamp)
|
||||
}
|
||||
|
||||
fn start_display_link(&self, _display_id: DisplayId) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn start_display_link(&self, _display_id: DisplayId) {}
|
||||
|
||||
fn stop_display_link(&self, _display_id: DisplayId) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn stop_display_link(&self, _display_id: DisplayId) {}
|
||||
|
||||
fn open_url(&self, _url: &str) {
|
||||
unimplemented!()
|
||||
@ -205,6 +212,14 @@ impl Platform for TestPlatform {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_menus(&self, _menus: Vec<crate::Menu>, _keymap: &Keymap) {}
|
||||
|
||||
fn on_app_menu_action(&self, _callback: Box<dyn FnMut(&dyn crate::Action)>) {}
|
||||
|
||||
fn on_will_open_app_menu(&self, _callback: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_validate_app_menu_command(&self, _callback: Box<dyn FnMut(&dyn crate::Action) -> bool>) {}
|
||||
|
||||
fn os_name(&self) -> &'static str {
|
||||
"test"
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ impl PlatformWindow for TestWindow {
|
||||
}
|
||||
|
||||
fn mouse_position(&self) -> Point<Pixels> {
|
||||
Point::zero()
|
||||
Point::default()
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
@ -223,7 +223,7 @@ impl PlatformAtlas for TestAtlas {
|
||||
},
|
||||
tile_id: TileId(tile_id),
|
||||
bounds: crate::Bounds {
|
||||
origin: Point::zero(),
|
||||
origin: Point::default(),
|
||||
size,
|
||||
},
|
||||
},
|
||||
|
@ -385,7 +385,7 @@ impl Default for Style {
|
||||
min_size: Size::auto(),
|
||||
max_size: Size::auto(),
|
||||
aspect_ratio: None,
|
||||
gap: Size::zero(),
|
||||
gap: Size::default(),
|
||||
// Aligment
|
||||
align_items: None,
|
||||
align_self: None,
|
||||
|
@ -430,7 +430,7 @@ impl<'a> WindowContext<'a> {
|
||||
self.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.clear_keystroke_matchers();
|
||||
.clear_pending_keystrokes();
|
||||
self.app.push_effect(Effect::FocusChanged {
|
||||
window_handle: self.window.handle,
|
||||
focused: Some(focus_id),
|
||||
@ -453,19 +453,21 @@ impl<'a> WindowContext<'a> {
|
||||
}
|
||||
|
||||
pub fn dispatch_action(&mut self, action: Box<dyn Action>) {
|
||||
if let Some(focus_handle) = self.focused() {
|
||||
self.defer(move |cx| {
|
||||
if let Some(node_id) = cx
|
||||
.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.focusable_node_id(focus_handle.id)
|
||||
{
|
||||
cx.propagate_event = true;
|
||||
cx.dispatch_action_on_node(node_id, action);
|
||||
}
|
||||
})
|
||||
}
|
||||
let focus_handle = self.focused();
|
||||
|
||||
self.defer(move |cx| {
|
||||
let node_id = focus_handle
|
||||
.and_then(|handle| {
|
||||
cx.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.focusable_node_id(handle.id)
|
||||
})
|
||||
.unwrap_or_else(|| cx.window.current_frame.dispatch_tree.root_node_id());
|
||||
|
||||
cx.propagate_event = true;
|
||||
cx.dispatch_action_on_node(node_id, action);
|
||||
})
|
||||
}
|
||||
|
||||
/// Schedules the given function to be run at the end of the current effect cycle, allowing entities
|
||||
@ -802,6 +804,22 @@ impl<'a> WindowContext<'a> {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn is_action_available(&self, action: &dyn Action) -> bool {
|
||||
let target = self
|
||||
.focused()
|
||||
.and_then(|focused_handle| {
|
||||
self.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.focusable_node_id(focused_handle.id)
|
||||
})
|
||||
.unwrap_or_else(|| self.window.current_frame.dispatch_tree.root_node_id());
|
||||
self.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.is_action_available(action, target)
|
||||
}
|
||||
|
||||
/// The position of the mouse relative to the window.
|
||||
pub fn mouse_position(&self) -> Point<Pixels> {
|
||||
self.window.mouse_position
|
||||
@ -1154,8 +1172,19 @@ impl<'a> WindowContext<'a> {
|
||||
self.start_frame();
|
||||
|
||||
self.with_z_index(0, |cx| {
|
||||
let available_space = cx.window.viewport_size.map(Into::into);
|
||||
root_view.draw(Point::zero(), available_space, cx);
|
||||
cx.with_key_dispatch(Some(KeyContext::default()), None, |_, cx| {
|
||||
for (action_type, action_listeners) in &cx.app.global_action_listeners {
|
||||
for action_listener in action_listeners.iter().cloned() {
|
||||
cx.window.current_frame.dispatch_tree.on_action(
|
||||
*action_type,
|
||||
Rc::new(move |action, phase, cx| action_listener(action, phase, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let available_space = cx.window.viewport_size.map(Into::into);
|
||||
root_view.draw(Point::default(), available_space, cx);
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(active_drag) = self.app.active_drag.take() {
|
||||
@ -1180,7 +1209,7 @@ impl<'a> WindowContext<'a> {
|
||||
self.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.preserve_keystroke_matchers(
|
||||
.preserve_pending_keystrokes(
|
||||
&mut self.window.previous_frame.dispatch_tree,
|
||||
self.window.focus,
|
||||
);
|
||||
@ -1341,75 +1370,79 @@ impl<'a> WindowContext<'a> {
|
||||
}
|
||||
|
||||
fn dispatch_key_event(&mut self, event: &dyn Any) {
|
||||
if let Some(node_id) = self.window.focus.and_then(|focus_id| {
|
||||
self.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.focusable_node_id(focus_id)
|
||||
}) {
|
||||
let dispatch_path = self
|
||||
.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.dispatch_path(node_id);
|
||||
let node_id = self
|
||||
.window
|
||||
.focus
|
||||
.and_then(|focus_id| {
|
||||
self.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.focusable_node_id(focus_id)
|
||||
})
|
||||
.unwrap_or_else(|| self.window.current_frame.dispatch_tree.root_node_id());
|
||||
|
||||
let mut actions: Vec<Box<dyn Action>> = Vec::new();
|
||||
let dispatch_path = self
|
||||
.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.dispatch_path(node_id);
|
||||
|
||||
// Capture phase
|
||||
let mut context_stack: SmallVec<[KeyContext; 16]> = SmallVec::new();
|
||||
self.propagate_event = true;
|
||||
let mut actions: Vec<Box<dyn Action>> = Vec::new();
|
||||
|
||||
for node_id in &dispatch_path {
|
||||
let node = self.window.current_frame.dispatch_tree.node(*node_id);
|
||||
// Capture phase
|
||||
let mut context_stack: SmallVec<[KeyContext; 16]> = SmallVec::new();
|
||||
self.propagate_event = true;
|
||||
|
||||
if !node.context.is_empty() {
|
||||
context_stack.push(node.context.clone());
|
||||
}
|
||||
for node_id in &dispatch_path {
|
||||
let node = self.window.current_frame.dispatch_tree.node(*node_id);
|
||||
|
||||
for key_listener in node.key_listeners.clone() {
|
||||
key_listener(event, DispatchPhase::Capture, self);
|
||||
if !self.propagate_event {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Some(context) = node.context.clone() {
|
||||
context_stack.push(context);
|
||||
}
|
||||
|
||||
// Bubble phase
|
||||
for node_id in dispatch_path.iter().rev() {
|
||||
// Handle low level key events
|
||||
let node = self.window.current_frame.dispatch_tree.node(*node_id);
|
||||
for key_listener in node.key_listeners.clone() {
|
||||
key_listener(event, DispatchPhase::Bubble, self);
|
||||
if !self.propagate_event {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Match keystrokes
|
||||
let node = self.window.current_frame.dispatch_tree.node(*node_id);
|
||||
if !node.context.is_empty() {
|
||||
if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
|
||||
if let Some(found) = self
|
||||
.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.dispatch_key(&key_down_event.keystroke, &context_stack)
|
||||
{
|
||||
actions.push(found.boxed_clone())
|
||||
}
|
||||
}
|
||||
|
||||
context_stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
for action in actions {
|
||||
self.dispatch_action_on_node(node_id, action);
|
||||
for key_listener in node.key_listeners.clone() {
|
||||
key_listener(event, DispatchPhase::Capture, self);
|
||||
if !self.propagate_event {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bubble phase
|
||||
for node_id in dispatch_path.iter().rev() {
|
||||
// Handle low level key events
|
||||
let node = self.window.current_frame.dispatch_tree.node(*node_id);
|
||||
for key_listener in node.key_listeners.clone() {
|
||||
key_listener(event, DispatchPhase::Bubble, self);
|
||||
if !self.propagate_event {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Match keystrokes
|
||||
let node = self.window.current_frame.dispatch_tree.node(*node_id);
|
||||
if node.context.is_some() {
|
||||
if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
|
||||
if let Some(found) = self
|
||||
.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.dispatch_key(&key_down_event.keystroke, &context_stack)
|
||||
{
|
||||
actions.push(found.boxed_clone())
|
||||
}
|
||||
}
|
||||
|
||||
context_stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
for action in actions {
|
||||
self.dispatch_action_on_node(node_id, action);
|
||||
if !self.propagate_event {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_action_on_node(&mut self, node_id: DispatchNodeId, action: Box<dyn Action>) {
|
||||
@ -1493,14 +1526,21 @@ impl<'a> WindowContext<'a> {
|
||||
}
|
||||
|
||||
pub fn available_actions(&self) -> Vec<Box<dyn Action>> {
|
||||
if let Some(focus_id) = self.window.focus {
|
||||
self.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.available_actions(focus_id)
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
let node_id = self
|
||||
.window
|
||||
.focus
|
||||
.and_then(|focus_id| {
|
||||
self.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.focusable_node_id(focus_id)
|
||||
})
|
||||
.unwrap_or_else(|| self.window.current_frame.dispatch_tree.root_node_id());
|
||||
|
||||
self.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.available_actions(node_id)
|
||||
}
|
||||
|
||||
pub fn bindings_for_action(&self, action: &dyn Action) -> Vec<KeyBinding> {
|
||||
@ -1526,7 +1566,7 @@ impl<'a> WindowContext<'a> {
|
||||
let context_stack = dispatch_tree
|
||||
.dispatch_path(node_id)
|
||||
.into_iter()
|
||||
.map(|node_id| dispatch_tree.node(node_id).context.clone())
|
||||
.filter_map(|node_id| dispatch_tree.node(node_id).context.clone())
|
||||
.collect();
|
||||
dispatch_tree.bindings_for_action(action, &context_stack)
|
||||
}
|
||||
@ -1556,7 +1596,7 @@ impl<'a> WindowContext<'a> {
|
||||
//========== ELEMENT RELATED FUNCTIONS ===========
|
||||
pub fn with_key_dispatch<R>(
|
||||
&mut self,
|
||||
context: KeyContext,
|
||||
context: Option<KeyContext>,
|
||||
focus_handle: Option<FocusHandle>,
|
||||
f: impl FnOnce(Option<FocusHandle>, &mut Self) -> R,
|
||||
) -> R {
|
||||
@ -2819,3 +2859,9 @@ impl From<(&'static str, EntityId)> for ElementId {
|
||||
ElementId::NamedInteger(name.into(), id.as_u64() as usize)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&'static str, usize)> for ElementId {
|
||||
fn from((name, id): (&'static str, usize)) -> Self {
|
||||
ElementId::NamedInteger(name.into(), id)
|
||||
}
|
||||
}
|
||||
|
@ -95,8 +95,6 @@ mod tests {
|
||||
.iter()
|
||||
.map(|(name, color)| (name.to_string(), (*color).into()))
|
||||
.collect(),
|
||||
inlay_style: HighlightStyle::default(),
|
||||
suggestion_style: HighlightStyle::default(),
|
||||
};
|
||||
|
||||
let capture_names = &[
|
||||
|
@ -10,7 +10,7 @@ doctest = false
|
||||
|
||||
[dependencies]
|
||||
util = { path = "../util" }
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
async-compression.workspace = true
|
||||
async-tar = "0.4.2"
|
||||
futures.workspace = true
|
||||
async-trait.workspace = true
|
||||
|
@ -55,7 +55,6 @@ pub struct ProjectPanel {
|
||||
clipboard_entry: Option<ClipboardEntry>,
|
||||
_dragged_entry_destination: Option<Arc<Path>>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
has_focus: bool,
|
||||
width: Option<f32>,
|
||||
pending_serialization: Task<Option<()>>,
|
||||
}
|
||||
@ -172,7 +171,6 @@ impl ProjectPanel {
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
cx.on_focus(&focus_handle, Self::focus_in).detach();
|
||||
cx.on_blur(&focus_handle, Self::focus_out).detach();
|
||||
|
||||
cx.subscribe(&project, |this, project, event, cx| match event {
|
||||
project::Event::ActiveEntryChanged(Some(entry_id)) => {
|
||||
@ -238,7 +236,6 @@ impl ProjectPanel {
|
||||
// context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
|
||||
_dragged_entry_destination: None,
|
||||
_workspace: workspace.weak_handle(),
|
||||
has_focus: false,
|
||||
width: None,
|
||||
pending_serialization: Task::ready(None),
|
||||
};
|
||||
@ -356,16 +353,11 @@ impl ProjectPanel {
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if !self.has_focus {
|
||||
self.has_focus = true;
|
||||
if !self.focus_handle.contains_focused(cx) {
|
||||
cx.emit(Event::Focus);
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: &mut ViewContext<Self>) {
|
||||
self.has_focus = false;
|
||||
}
|
||||
|
||||
fn deploy_context_menu(
|
||||
&mut self,
|
||||
position: Point<Pixels>,
|
||||
@ -1557,10 +1549,6 @@ impl Panel for ProjectPanel {
|
||||
Box::new(ToggleFocus)
|
||||
}
|
||||
|
||||
fn has_focus(&self, _: &WindowContext) -> bool {
|
||||
self.has_focus
|
||||
}
|
||||
|
||||
fn persistent_name() -> &'static str {
|
||||
"Project Panel"
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView, Workspac
|
||||
pub struct QuickActionBar {
|
||||
buffer_search_bar: ViewHandle<BufferSearchBar>,
|
||||
active_item: Option<Box<dyn ItemHandle>>,
|
||||
_inlay_hints_enabled_subscription: Option<Subscription>,
|
||||
inlay_hints_enabled_subscription: Option<Subscription>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ impl QuickActionBar {
|
||||
Self {
|
||||
buffer_search_bar,
|
||||
active_item: None,
|
||||
_inlay_hints_enabled_subscription: None,
|
||||
inlay_hints_enabled_subscription: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
}
|
||||
}
|
||||
@ -161,12 +161,12 @@ impl ToolbarItemView for QuickActionBar {
|
||||
match active_pane_item {
|
||||
Some(active_item) => {
|
||||
self.active_item = Some(active_item.boxed_clone());
|
||||
self._inlay_hints_enabled_subscription.take();
|
||||
self.inlay_hints_enabled_subscription.take();
|
||||
|
||||
if let Some(editor) = active_item.downcast::<Editor>() {
|
||||
let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
|
||||
let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
|
||||
self._inlay_hints_enabled_subscription =
|
||||
self.inlay_hints_enabled_subscription =
|
||||
Some(cx.observe(&editor, move |_, editor, cx| {
|
||||
let editor = editor.read(cx);
|
||||
let new_inlay_hints_enabled = editor.inlay_hints_enabled();
|
||||
|
22
crates/quick_action_bar2/Cargo.toml
Normal file
22
crates/quick_action_bar2/Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "quick_action_bar2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/quick_action_bar.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
assistant = { package = "assistant2", path = "../assistant2" }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
search = { package = "search2", path = "../search2" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
193
crates/quick_action_bar2/src/quick_action_bar.rs
Normal file
193
crates/quick_action_bar2/src/quick_action_bar.rs
Normal file
@ -0,0 +1,193 @@
|
||||
use assistant::{AssistantPanel, InlineAssist};
|
||||
use editor::Editor;
|
||||
|
||||
use gpui::{
|
||||
Action, ClickEvent, Div, ElementId, EventEmitter, InteractiveElement, ParentElement, Render,
|
||||
Stateful, Styled, Subscription, View, ViewContext, WeakView,
|
||||
};
|
||||
use search::BufferSearchBar;
|
||||
use ui::{prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, Tooltip};
|
||||
use workspace::{
|
||||
item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
};
|
||||
|
||||
pub struct QuickActionBar {
|
||||
buffer_search_bar: View<BufferSearchBar>,
|
||||
active_item: Option<Box<dyn ItemHandle>>,
|
||||
_inlay_hints_enabled_subscription: Option<Subscription>,
|
||||
workspace: WeakView<Workspace>,
|
||||
}
|
||||
|
||||
impl QuickActionBar {
|
||||
pub fn new(buffer_search_bar: View<BufferSearchBar>, workspace: &Workspace) -> Self {
|
||||
Self {
|
||||
buffer_search_bar,
|
||||
active_item: None,
|
||||
_inlay_hints_enabled_subscription: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
}
|
||||
}
|
||||
|
||||
fn active_editor(&self) -> Option<View<Editor>> {
|
||||
self.active_item
|
||||
.as_ref()
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for QuickActionBar {
|
||||
type Element = Stateful<Div>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let Some(editor) = self.active_editor() else {
|
||||
return div().id("empty quick action bar");
|
||||
};
|
||||
|
||||
let inlay_hints_button = Some(QuickActionBarButton::new(
|
||||
"toggle inlay hints",
|
||||
Icon::InlayHint,
|
||||
editor.read(cx).inlay_hints_enabled(),
|
||||
Box::new(editor::ToggleInlayHints),
|
||||
"Toggle Inlay Hints",
|
||||
{
|
||||
let editor = editor.clone();
|
||||
move |_, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
|
||||
});
|
||||
}
|
||||
},
|
||||
))
|
||||
.filter(|_| editor.read(cx).supports_inlay_hints(cx));
|
||||
|
||||
let search_button = Some(QuickActionBarButton::new(
|
||||
"toggle buffer search",
|
||||
Icon::MagnifyingGlass,
|
||||
!self.buffer_search_bar.read(cx).is_dismissed(),
|
||||
Box::new(search::buffer_search::Deploy { focus: false }),
|
||||
"Buffer Search",
|
||||
{
|
||||
let buffer_search_bar = self.buffer_search_bar.clone();
|
||||
move |_, cx| {
|
||||
buffer_search_bar.update(cx, |search_bar, cx| search_bar.toggle(cx));
|
||||
}
|
||||
},
|
||||
))
|
||||
.filter(|_| editor.is_singleton(cx));
|
||||
|
||||
let assistant_button = QuickActionBarButton::new(
|
||||
"toggle inline assistant",
|
||||
Icon::MagicWand,
|
||||
false,
|
||||
Box::new(InlineAssist),
|
||||
"Inline Assist",
|
||||
{
|
||||
let workspace = self.workspace.clone();
|
||||
move |_, cx| {
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
AssistantPanel::inline_assist(workspace, &InlineAssist, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
h_stack()
|
||||
.id("quick action bar")
|
||||
.p_1()
|
||||
.gap_2()
|
||||
.children(inlay_hints_button)
|
||||
.children(search_button)
|
||||
.child(assistant_button)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolbarItemEvent> for QuickActionBar {}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct QuickActionBarButton {
|
||||
id: ElementId,
|
||||
icon: Icon,
|
||||
toggled: bool,
|
||||
action: Box<dyn Action>,
|
||||
tooltip: SharedString,
|
||||
on_click: Box<dyn Fn(&ClickEvent, &mut WindowContext)>,
|
||||
}
|
||||
|
||||
impl QuickActionBarButton {
|
||||
fn new(
|
||||
id: impl Into<ElementId>,
|
||||
icon: Icon,
|
||||
toggled: bool,
|
||||
action: Box<dyn Action>,
|
||||
tooltip: impl Into<SharedString>,
|
||||
on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
icon,
|
||||
toggled,
|
||||
action,
|
||||
tooltip: tooltip.into(),
|
||||
on_click: Box::new(on_click),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for QuickActionBarButton {
|
||||
type Rendered = IconButton;
|
||||
|
||||
fn render(self, _: &mut WindowContext) -> Self::Rendered {
|
||||
let tooltip = self.tooltip.clone();
|
||||
let action = self.action.boxed_clone();
|
||||
|
||||
IconButton::new(self.id.clone(), self.icon)
|
||||
.size(ButtonSize::Compact)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.selected(self.toggled)
|
||||
.tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
|
||||
.on_click(move |event, cx| (self.on_click)(event, cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolbarItemView for QuickActionBar {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> ToolbarItemLocation {
|
||||
match active_pane_item {
|
||||
Some(active_item) => {
|
||||
self.active_item = Some(active_item.boxed_clone());
|
||||
self._inlay_hints_enabled_subscription.take();
|
||||
|
||||
if let Some(editor) = active_item.downcast::<Editor>() {
|
||||
let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
|
||||
let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
|
||||
self._inlay_hints_enabled_subscription =
|
||||
Some(cx.observe(&editor, move |_, editor, cx| {
|
||||
let editor = editor.read(cx);
|
||||
let new_inlay_hints_enabled = editor.inlay_hints_enabled();
|
||||
let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
|
||||
let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
|
||||
|| supports_inlay_hints != new_supports_inlay_hints;
|
||||
inlay_hints_enabled = new_inlay_hints_enabled;
|
||||
supports_inlay_hints = new_supports_inlay_hints;
|
||||
if should_notify {
|
||||
cx.notify()
|
||||
}
|
||||
}));
|
||||
ToolbarItemLocation::PrimaryRight
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.active_item = None;
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -10,15 +10,15 @@ use collections::HashMap;
|
||||
use editor::{Editor, EditorMode};
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
actions, div, red, Action, AppContext, Div, EventEmitter, InteractiveElement as _, IntoElement,
|
||||
ParentElement as _, Render, Styled, Subscription, Task, View, ViewContext, VisualContext as _,
|
||||
WeakView, WindowContext,
|
||||
actions, div, red, Action, AppContext, Div, EventEmitter, FocusableView,
|
||||
InteractiveElement as _, IntoElement, ParentElement as _, Render, Styled, Subscription, Task,
|
||||
View, ViewContext, VisualContext as _, WeakView, WindowContext,
|
||||
};
|
||||
use project::search::SearchQuery;
|
||||
use serde::Deserialize;
|
||||
use std::{any::Any, sync::Arc};
|
||||
|
||||
use ui::{h_stack, Icon, IconButton, IconElement};
|
||||
use ui::{h_stack, Clickable, Icon, IconButton, IconElement};
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
item::ItemHandle,
|
||||
@ -161,16 +161,6 @@ impl Render for BufferSearchBar {
|
||||
|
||||
Some(ui::Label::new(message))
|
||||
});
|
||||
let nav_button_for_direction = |icon, direction| {
|
||||
render_nav_button(
|
||||
icon,
|
||||
self.active_match_index.is_some(),
|
||||
cx.listener(move |this, _, cx| match direction {
|
||||
Direction::Prev => this.select_prev_match(&Default::default(), cx),
|
||||
Direction::Next => this.select_next_match(&Default::default(), cx),
|
||||
}),
|
||||
)
|
||||
};
|
||||
let should_show_replace_input = self.replace_enabled && supported_options.replacement;
|
||||
let replace_all = should_show_replace_input
|
||||
.then(|| super::render_replace_button(ReplaceAll, ui::Icon::ReplaceAll));
|
||||
@ -237,20 +227,32 @@ impl Render for BufferSearchBar {
|
||||
h_stack()
|
||||
.gap_0p5()
|
||||
.flex_none()
|
||||
.child(self.render_action_button())
|
||||
.child(self.render_action_button(cx))
|
||||
.children(match_count)
|
||||
.child(nav_button_for_direction(
|
||||
.child(render_nav_button(
|
||||
ui::Icon::ChevronLeft,
|
||||
Direction::Prev,
|
||||
self.active_match_index.is_some(),
|
||||
cx.listener(move |this, _, cx| {
|
||||
this.select_prev_match(&Default::default(), cx);
|
||||
}),
|
||||
))
|
||||
.child(nav_button_for_direction(
|
||||
.child(render_nav_button(
|
||||
ui::Icon::ChevronRight,
|
||||
Direction::Next,
|
||||
self.active_match_index.is_some(),
|
||||
cx.listener(move |this, _, cx| {
|
||||
this.select_next_match(&Default::default(), cx);
|
||||
}),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for BufferSearchBar {
|
||||
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.query_editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolbarItemView for BufferSearchBar {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
@ -311,13 +313,7 @@ impl BufferSearchBar {
|
||||
pane.update(cx, |this, cx| {
|
||||
this.toolbar().update(cx, |this, cx| {
|
||||
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
|
||||
search_bar.update(cx, |this, cx| {
|
||||
if this.is_dismissed() {
|
||||
this.show(cx);
|
||||
} else {
|
||||
this.dismiss(&Dismiss, cx);
|
||||
}
|
||||
});
|
||||
search_bar.update(cx, |this, cx| this.toggle(cx));
|
||||
return;
|
||||
}
|
||||
let view = cx.build_view(|cx| BufferSearchBar::new(cx));
|
||||
@ -481,6 +477,14 @@ impl BufferSearchBar {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn toggle(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.is_dismissed() {
|
||||
self.show(cx);
|
||||
} else {
|
||||
self.dismiss(&Dismiss, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
if self.active_searchable_item.is_none() {
|
||||
return false;
|
||||
@ -582,12 +586,14 @@ impl BufferSearchBar {
|
||||
self.update_matches(cx)
|
||||
}
|
||||
|
||||
fn render_action_button(&self) -> impl IntoElement {
|
||||
fn render_action_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
// let tooltip_style = theme.tooltip.clone();
|
||||
|
||||
// let style = theme.search.action_button.clone();
|
||||
|
||||
IconButton::new(0, ui::Icon::SelectAll).action(Box::new(SelectAllMatches))
|
||||
IconButton::new("select-all", ui::Icon::SelectAll).on_click(cx.listener(|this, _, cx| {
|
||||
this.select_all_matches(&SelectAllMatches, cx);
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
|
||||
|
@ -124,6 +124,17 @@ pub fn update_settings_file<T: Settings>(
|
||||
|
||||
pub fn load_default_keymap(cx: &mut AppContext) {
|
||||
for path in ["keymaps/default.json", "keymaps/vim.json"] {
|
||||
// TODO: Remove this conditional when we're ready to add Vim support.
|
||||
// Right now we're avoiding loading the Vim keymap to silence the warnings
|
||||
// about invalid action bindings.
|
||||
if path.contains("vim") {
|
||||
let _: Option<()> = Err(format!(
|
||||
"TODO: Skipping {path} until we're ready to add Vim support"
|
||||
))
|
||||
.log_err();
|
||||
continue;
|
||||
}
|
||||
|
||||
KeymapFile::load_asset(path, cx).unwrap();
|
||||
}
|
||||
|
||||
@ -132,39 +143,3 @@ pub fn load_default_keymap(cx: &mut AppContext) {
|
||||
// KeymapFile::load_asset(asset_path, cx).unwrap();
|
||||
// }
|
||||
}
|
||||
|
||||
pub fn handle_keymap_file_changes(
|
||||
mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
cx.spawn(move |cx| async move {
|
||||
// let mut settings_subscription = None;
|
||||
while let Some(user_keymap_content) = user_keymap_file_rx.next().await {
|
||||
if let Some(keymap_content) = KeymapFile::parse(&user_keymap_content).log_err() {
|
||||
cx.update(|cx| reload_keymaps(cx, &keymap_content)).ok();
|
||||
|
||||
// todo!()
|
||||
// let mut old_base_keymap = cx.read(|cx| *settings::get::<BaseKeymap>(cx));
|
||||
// drop(settings_subscription);
|
||||
// settings_subscription = Some(cx.update(|cx| {
|
||||
// cx.observe_global::<SettingsStore, _>(move |cx| {
|
||||
// let new_base_keymap = *settings::get::<BaseKeymap>(cx);
|
||||
// if new_base_keymap != old_base_keymap {
|
||||
// old_base_keymap = new_base_keymap.clone();
|
||||
// reload_keymaps(cx, &keymap_content);
|
||||
// }
|
||||
// })
|
||||
// }));
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) {
|
||||
// todo!()
|
||||
// cx.clear_bindings();
|
||||
load_default_keymap(cx);
|
||||
keymap_content.clone().add_to_cx(cx).log_err();
|
||||
// cx.set_menus(menus::menus());
|
||||
}
|
||||
|
@ -812,7 +812,7 @@ impl Element for TerminalElement {
|
||||
let theme = cx.theme();
|
||||
|
||||
let dispatch_context = self.terminal_view.read(cx).dispatch_context(cx);
|
||||
self.interactivity().key_context = dispatch_context;
|
||||
self.interactivity().key_context = Some(dispatch_context);
|
||||
cx.paint_quad(
|
||||
bounds,
|
||||
Default::default(),
|
||||
|
@ -415,10 +415,6 @@ impl Panel for TerminalPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn has_focus(&self, cx: &WindowContext) -> bool {
|
||||
self.pane.read(cx).has_focus(cx)
|
||||
}
|
||||
|
||||
fn persistent_name() -> &'static str {
|
||||
"TerminalPanel"
|
||||
}
|
||||
|
@ -9,11 +9,10 @@ pub mod terminal_panel;
|
||||
// use crate::terminal_element::TerminalElement;
|
||||
use editor::{scroll::autoscroll::Autoscroll, Editor};
|
||||
use gpui::{
|
||||
actions, div, point, px, size, Action, AnyElement, AppContext, Bounds, Div, Element,
|
||||
EventEmitter, FocusEvent, FocusHandle, Focusable, FocusableElement, FocusableView, Font,
|
||||
FontStyle, FontWeight, InputHandler, InteractiveElement, KeyContext, KeyDownEvent, Keystroke,
|
||||
Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Render, SharedString, Styled,
|
||||
Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
|
||||
actions, div, point, px, size, Action, AnyElement, AppContext, Bounds, Div, EventEmitter,
|
||||
FocusEvent, FocusHandle, Focusable, FocusableElement, FocusableView, Font, FontStyle,
|
||||
FontWeight, InputHandler, KeyContext, KeyDownEvent, Keystroke, Model, MouseButton,
|
||||
MouseDownEvent, Pixels, Render, Task, View, VisualContext, WeakView, Subscription
|
||||
};
|
||||
use language::Bias;
|
||||
use persistence::TERMINAL_DB;
|
||||
@ -28,13 +27,13 @@ use terminal::{
|
||||
};
|
||||
use terminal_element::TerminalElement;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{h_stack, prelude::*, ContextMenu, Icon, IconElement, Label};
|
||||
use util::{paths::PathLikeWithPosition, ResultExt};
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent},
|
||||
notifications::NotifyResultExt,
|
||||
register_deserializable_item,
|
||||
searchable::{SearchEvent, SearchOptions, SearchableItem},
|
||||
ui::{ContextMenu, Icon, IconElement, Label},
|
||||
CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
@ -793,6 +792,8 @@ impl InputHandler for TerminalView {
|
||||
}
|
||||
|
||||
impl Item for TerminalView {
|
||||
type Event = ItemEvent;
|
||||
|
||||
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
|
||||
Some(self.terminal().read(cx).title().into())
|
||||
}
|
||||
@ -800,7 +801,8 @@ impl Item for TerminalView {
|
||||
fn tab_content(&self, _detail: Option<usize>, cx: &WindowContext) -> AnyElement {
|
||||
let title = self.terminal().read(cx).title();
|
||||
|
||||
div()
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.child(IconElement::new(Icon::Terminal))
|
||||
.child(Label::new(title))
|
||||
.into_any()
|
||||
@ -900,6 +902,10 @@ impl Item for TerminalView {
|
||||
.detach();
|
||||
self.workspace_id = workspace.database_id();
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
|
||||
f(*event)
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchableItem for TerminalView {
|
||||
|
@ -5,7 +5,7 @@ use crate::ColorScale;
|
||||
use crate::{SystemColors, ThemeColors};
|
||||
|
||||
pub(crate) fn neutral() -> ColorScaleSet {
|
||||
slate()
|
||||
sand()
|
||||
}
|
||||
|
||||
impl ThemeColors {
|
||||
@ -29,12 +29,12 @@ impl ThemeColors {
|
||||
element_disabled: neutral().light_alpha().step_3(),
|
||||
drop_target_background: blue().light_alpha().step_2(),
|
||||
ghost_element_background: system.transparent,
|
||||
ghost_element_hover: neutral().light_alpha().step_4(),
|
||||
ghost_element_active: neutral().light_alpha().step_5(),
|
||||
ghost_element_hover: neutral().light_alpha().step_3(),
|
||||
ghost_element_active: neutral().light_alpha().step_4(),
|
||||
ghost_element_selected: neutral().light_alpha().step_5(),
|
||||
ghost_element_disabled: neutral().light_alpha().step_3(),
|
||||
text: yellow().light().step_9(),
|
||||
text_muted: neutral().light().step_11(),
|
||||
text: neutral().light().step_12(),
|
||||
text_muted: neutral().light().step_10(),
|
||||
text_placeholder: neutral().light().step_10(),
|
||||
text_disabled: neutral().light().step_9(),
|
||||
text_accent: blue().light().step_11(),
|
||||
@ -55,13 +55,13 @@ impl ThemeColors {
|
||||
editor_gutter_background: neutral().light().step_1(), // todo!("pick the right colors")
|
||||
editor_subheader_background: neutral().light().step_2(),
|
||||
editor_active_line_background: neutral().light_alpha().step_3(),
|
||||
editor_line_number: neutral().light_alpha().step_3(), // todo!("pick the right colors")
|
||||
editor_active_line_number: neutral().light_alpha().step_3(), // todo!("pick the right colors")
|
||||
editor_highlighted_line_background: neutral().light_alpha().step_4(), // todo!("pick the right colors")
|
||||
editor_invisible: neutral().light_alpha().step_4(), // todo!("pick the right colors")
|
||||
editor_wrap_guide: neutral().light_alpha().step_4(), // todo!("pick the right colors")
|
||||
editor_active_wrap_guide: neutral().light_alpha().step_4(), // todo!("pick the right colors")
|
||||
editor_document_highlight_read_background: neutral().light_alpha().step_4(), // todo!("pick the right colors")
|
||||
editor_line_number: neutral().light().step_10(),
|
||||
editor_active_line_number: neutral().light().step_11(),
|
||||
editor_highlighted_line_background: neutral().light_alpha().step_3(),
|
||||
editor_invisible: neutral().light().step_10(),
|
||||
editor_wrap_guide: neutral().light_alpha().step_7(),
|
||||
editor_active_wrap_guide: neutral().light_alpha().step_8(), // todo!("pick the right colors")
|
||||
editor_document_highlight_read_background: neutral().light_alpha().step_3(), // todo!("pick the right colors")
|
||||
editor_document_highlight_write_background: neutral().light_alpha().step_4(), // todo!("pick the right colors")
|
||||
terminal_background: neutral().light().step_1(),
|
||||
terminal_ansi_black: black().light().step_12(),
|
||||
|
@ -1,47 +1,51 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
default_color_scales,
|
||||
one_themes::{one_dark, one_family},
|
||||
Theme, ThemeFamily,
|
||||
Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, ThemeColors,
|
||||
ThemeFamily, ThemeStyles,
|
||||
};
|
||||
|
||||
// fn zed_pro_daylight() -> Theme {
|
||||
// Theme {
|
||||
// id: "zed_pro_daylight".to_string(),
|
||||
// name: "Zed Pro Daylight".into(),
|
||||
// appearance: Appearance::Light,
|
||||
// styles: ThemeStyles {
|
||||
// system: SystemColors::default(),
|
||||
// colors: ThemeColors::light(),
|
||||
// status: StatusColors::light(),
|
||||
// player: PlayerColors::light(),
|
||||
// syntax: Arc::new(SyntaxTheme::light()),
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
fn zed_pro_daylight() -> Theme {
|
||||
Theme {
|
||||
id: "zed_pro_daylight".to_string(),
|
||||
name: "Zed Pro Daylight".into(),
|
||||
appearance: Appearance::Light,
|
||||
styles: ThemeStyles {
|
||||
system: SystemColors::default(),
|
||||
colors: ThemeColors::light(),
|
||||
status: StatusColors::light(),
|
||||
player: PlayerColors::light(),
|
||||
syntax: Arc::new(SyntaxTheme::light()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// pub(crate) fn zed_pro_moonlight() -> Theme {
|
||||
// Theme {
|
||||
// id: "zed_pro_moonlight".to_string(),
|
||||
// name: "Zed Pro Moonlight".into(),
|
||||
// appearance: Appearance::Dark,
|
||||
// styles: ThemeStyles {
|
||||
// system: SystemColors::default(),
|
||||
// colors: ThemeColors::dark(),
|
||||
// status: StatusColors::dark(),
|
||||
// player: PlayerColors::dark(),
|
||||
// syntax: Arc::new(SyntaxTheme::dark()),
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
pub(crate) fn zed_pro_moonlight() -> Theme {
|
||||
Theme {
|
||||
id: "zed_pro_moonlight".to_string(),
|
||||
name: "Zed Pro Moonlight".into(),
|
||||
appearance: Appearance::Dark,
|
||||
styles: ThemeStyles {
|
||||
system: SystemColors::default(),
|
||||
colors: ThemeColors::dark(),
|
||||
status: StatusColors::dark(),
|
||||
player: PlayerColors::dark(),
|
||||
syntax: Arc::new(SyntaxTheme::dark()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn zed_pro_family() -> ThemeFamily {
|
||||
// ThemeFamily {
|
||||
// id: "zed_pro".to_string(),
|
||||
// name: "Zed Pro".into(),
|
||||
// author: "Zed Team".into(),
|
||||
// themes: vec![zed_pro_daylight(), zed_pro_moonlight()],
|
||||
// scales: default_color_scales(),
|
||||
// }
|
||||
// }
|
||||
pub fn zed_pro_family() -> ThemeFamily {
|
||||
ThemeFamily {
|
||||
id: "zed_pro".to_string(),
|
||||
name: "Zed Pro".into(),
|
||||
author: "Zed Team".into(),
|
||||
themes: vec![zed_pro_daylight(), zed_pro_moonlight()],
|
||||
scales: default_color_scales(),
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ThemeFamily {
|
||||
fn default() -> Self {
|
||||
|
@ -194,8 +194,6 @@ pub(crate) fn one_dark() -> Theme {
|
||||
("variable.special".into(), red.into()),
|
||||
("variant".into(), HighlightStyle::default()),
|
||||
],
|
||||
inlay_style: HighlightStyle::default(),
|
||||
suggestion_style: HighlightStyle::default(),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ use gpui::{HighlightStyle, SharedString};
|
||||
use refineable::Refineable;
|
||||
|
||||
use crate::{
|
||||
one_themes::one_family, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors,
|
||||
Theme, ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily,
|
||||
one_themes::one_family, zed_pro_family, Appearance, PlayerColors, StatusColors, SyntaxTheme,
|
||||
SystemColors, Theme, ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily,
|
||||
};
|
||||
|
||||
pub struct ThemeRegistry {
|
||||
@ -117,7 +117,7 @@ impl Default for ThemeRegistry {
|
||||
themes: HashMap::default(),
|
||||
};
|
||||
|
||||
this.insert_theme_families([one_family()]);
|
||||
this.insert_theme_families([zed_pro_family(), one_family()]);
|
||||
|
||||
this
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ pub struct ThemeSettings {
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AdjustedBufferFontSize(Option<Pixels>);
|
||||
pub struct AdjustedBufferFontSize(Pixels);
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ThemeSettingsContent {
|
||||
@ -69,12 +69,10 @@ impl BufferLineHeight {
|
||||
}
|
||||
|
||||
impl ThemeSettings {
|
||||
pub fn buffer_font_size(&self, cx: &mut AppContext) -> Pixels {
|
||||
let font_size = *cx
|
||||
.default_global::<AdjustedBufferFontSize>()
|
||||
.0
|
||||
.get_or_insert(self.buffer_font_size.into());
|
||||
font_size.max(MIN_FONT_SIZE)
|
||||
pub fn buffer_font_size(&self, cx: &AppContext) -> Pixels {
|
||||
cx.try_global::<AdjustedBufferFontSize>()
|
||||
.map_or(self.buffer_font_size, |size| size.0)
|
||||
.max(MIN_FONT_SIZE)
|
||||
}
|
||||
|
||||
pub fn line_height(&self) -> f32 {
|
||||
@ -83,9 +81,9 @@ impl ThemeSettings {
|
||||
}
|
||||
|
||||
pub fn adjusted_font_size(size: Pixels, cx: &mut AppContext) -> Pixels {
|
||||
if let Some(adjusted_size) = cx.default_global::<AdjustedBufferFontSize>().0 {
|
||||
if let Some(AdjustedBufferFontSize(adjusted_size)) = cx.try_global::<AdjustedBufferFontSize>() {
|
||||
let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
|
||||
let delta = adjusted_size - buffer_font_size;
|
||||
let delta = *adjusted_size - buffer_font_size;
|
||||
size + delta
|
||||
} else {
|
||||
size
|
||||
@ -95,18 +93,19 @@ pub fn adjusted_font_size(size: Pixels, cx: &mut AppContext) -> Pixels {
|
||||
|
||||
pub fn adjust_font_size(cx: &mut AppContext, f: fn(&mut Pixels)) {
|
||||
let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
|
||||
let adjusted_size = cx
|
||||
.default_global::<AdjustedBufferFontSize>()
|
||||
.0
|
||||
.get_or_insert(buffer_font_size);
|
||||
f(adjusted_size);
|
||||
*adjusted_size = (*adjusted_size).max(MIN_FONT_SIZE - buffer_font_size);
|
||||
let mut adjusted_size = cx
|
||||
.try_global::<AdjustedBufferFontSize>()
|
||||
.map_or(buffer_font_size, |adjusted_size| adjusted_size.0);
|
||||
|
||||
f(&mut adjusted_size);
|
||||
adjusted_size = adjusted_size.max(MIN_FONT_SIZE);
|
||||
cx.set_global(AdjustedBufferFontSize(adjusted_size));
|
||||
cx.refresh();
|
||||
}
|
||||
|
||||
pub fn reset_font_size(cx: &mut AppContext) {
|
||||
if cx.has_global::<AdjustedBufferFontSize>() {
|
||||
cx.global_mut::<AdjustedBufferFontSize>().0 = None;
|
||||
cx.remove_global::<AdjustedBufferFontSize>();
|
||||
cx.refresh();
|
||||
}
|
||||
}
|
||||
|
@ -8,12 +8,6 @@ use crate::{
|
||||
#[derive(Clone, Default)]
|
||||
pub struct SyntaxTheme {
|
||||
pub highlights: Vec<(String, HighlightStyle)>,
|
||||
// todo!("Remove this in favor of StatusColor.hint")
|
||||
// If this should be overridable we should move it to ThemeColors
|
||||
pub inlay_style: HighlightStyle,
|
||||
// todo!("Remove this in favor of StatusColor.prediction")
|
||||
// If this should be overridable we should move it to ThemeColors
|
||||
pub suggestion_style: HighlightStyle,
|
||||
}
|
||||
|
||||
impl SyntaxTheme {
|
||||
@ -22,8 +16,8 @@ impl SyntaxTheme {
|
||||
highlights: vec![
|
||||
("attribute".into(), cyan().light().step_11().into()),
|
||||
("boolean".into(), tomato().light().step_11().into()),
|
||||
("comment".into(), neutral().light().step_11().into()),
|
||||
("comment.doc".into(), iris().light().step_12().into()),
|
||||
("comment".into(), neutral().light().step_10().into()),
|
||||
("comment.doc".into(), iris().light().step_11().into()),
|
||||
("constant".into(), red().light().step_9().into()),
|
||||
("constructor".into(), red().light().step_9().into()),
|
||||
("embedded".into(), red().light().step_9().into()),
|
||||
@ -32,11 +26,11 @@ impl SyntaxTheme {
|
||||
("enum".into(), red().light().step_9().into()),
|
||||
("function".into(), red().light().step_9().into()),
|
||||
("hint".into(), red().light().step_9().into()),
|
||||
("keyword".into(), orange().light().step_11().into()),
|
||||
("keyword".into(), orange().light().step_9().into()),
|
||||
("label".into(), red().light().step_9().into()),
|
||||
("link_text".into(), red().light().step_9().into()),
|
||||
("link_uri".into(), red().light().step_9().into()),
|
||||
("number".into(), red().light().step_9().into()),
|
||||
("number".into(), purple().light().step_10().into()),
|
||||
("operator".into(), red().light().step_9().into()),
|
||||
("predictive".into(), red().light().step_9().into()),
|
||||
("preproc".into(), red().light().step_9().into()),
|
||||
@ -49,16 +43,16 @@ impl SyntaxTheme {
|
||||
),
|
||||
(
|
||||
"punctuation.delimiter".into(),
|
||||
neutral().light().step_11().into(),
|
||||
neutral().light().step_10().into(),
|
||||
),
|
||||
(
|
||||
"punctuation.list_marker".into(),
|
||||
blue().light().step_11().into(),
|
||||
),
|
||||
("punctuation.special".into(), red().light().step_9().into()),
|
||||
("string".into(), jade().light().step_11().into()),
|
||||
("string".into(), jade().light().step_9().into()),
|
||||
("string.escape".into(), red().light().step_9().into()),
|
||||
("string.regex".into(), tomato().light().step_11().into()),
|
||||
("string.regex".into(), tomato().light().step_9().into()),
|
||||
("string.special".into(), red().light().step_9().into()),
|
||||
(
|
||||
"string.special.symbol".into(),
|
||||
@ -67,13 +61,11 @@ impl SyntaxTheme {
|
||||
("tag".into(), red().light().step_9().into()),
|
||||
("text.literal".into(), red().light().step_9().into()),
|
||||
("title".into(), red().light().step_9().into()),
|
||||
("type".into(), red().light().step_9().into()),
|
||||
("type".into(), cyan().light().step_9().into()),
|
||||
("variable".into(), red().light().step_9().into()),
|
||||
("variable.special".into(), red().light().step_9().into()),
|
||||
("variant".into(), red().light().step_9().into()),
|
||||
],
|
||||
inlay_style: tomato().light().step_1().into(), // todo!("nate: use a proper style")
|
||||
suggestion_style: orange().light().step_1().into(), // todo!("nate: use proper style")
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,8 +124,6 @@ impl SyntaxTheme {
|
||||
("variable.special".into(), red().dark().step_11().into()),
|
||||
("variant".into(), red().dark().step_11().into()),
|
||||
],
|
||||
inlay_style: neutral().dark().step_11().into(), // todo!("nate: use a proper style")
|
||||
suggestion_style: orange().dark().step_11().into(), // todo!("nate: use a proper style")
|
||||
}
|
||||
}
|
||||
|
||||
@ -152,8 +142,6 @@ impl SyntaxTheme {
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
inlay_style: HighlightStyle::default(),
|
||||
suggestion_style: HighlightStyle::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ mod context_menu;
|
||||
mod disclosure;
|
||||
mod divider;
|
||||
mod icon;
|
||||
mod indicator;
|
||||
mod keybinding;
|
||||
mod label;
|
||||
mod list;
|
||||
@ -24,6 +25,7 @@ pub use context_menu::*;
|
||||
pub use disclosure::*;
|
||||
pub use divider::*;
|
||||
pub use icon::*;
|
||||
pub use indicator::*;
|
||||
pub use keybinding::*;
|
||||
pub use label::*;
|
||||
pub use list::*;
|
||||
|
@ -359,11 +359,7 @@ impl RenderOnce for ButtonLike {
|
||||
},
|
||||
)
|
||||
.when_some(self.tooltip, |this, tooltip| {
|
||||
if !self.selected {
|
||||
this.tooltip(move |cx| tooltip(cx))
|
||||
} else {
|
||||
this
|
||||
}
|
||||
this.tooltip(move |cx| tooltip(cx))
|
||||
})
|
||||
.children(self.children)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use gpui::{Action, AnyView, DefiniteLength};
|
||||
use gpui::{AnyView, DefiniteLength};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize};
|
||||
@ -39,10 +39,6 @@ impl IconButton {
|
||||
self.selected_icon = icon.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn action(self, action: Box<dyn Action>) -> Self {
|
||||
self.on_click(move |_event, cx| cx.dispatch_action(action.boxed_clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for IconButton {
|
||||
|
@ -1,15 +1,26 @@
|
||||
use gpui::{rems, svg, IntoElement, Svg};
|
||||
use gpui::{rems, svg, IntoElement, Rems, Svg};
|
||||
use strum::EnumIter;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum IconSize {
|
||||
XSmall,
|
||||
Small,
|
||||
#[default]
|
||||
Medium,
|
||||
}
|
||||
|
||||
impl IconSize {
|
||||
pub fn rems(self) -> Rems {
|
||||
match self {
|
||||
IconSize::XSmall => rems(12. / 16.),
|
||||
IconSize::Small => rems(14. / 16.),
|
||||
IconSize::Medium => rems(16. / 16.),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Copy, Clone, EnumIter)]
|
||||
pub enum Icon {
|
||||
Ai,
|
||||
@ -81,6 +92,8 @@ pub enum Icon {
|
||||
Shift,
|
||||
Option,
|
||||
Return,
|
||||
Update,
|
||||
ZedXCopilot,
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
@ -109,6 +122,7 @@ impl Icon {
|
||||
Icon::Close => "icons/x.svg",
|
||||
Icon::Collab => "icons/user_group_16.svg",
|
||||
Icon::Copilot => "icons/copilot.svg",
|
||||
|
||||
Icon::CopilotInit => "icons/copilot_init.svg",
|
||||
Icon::CopilotError => "icons/copilot_error.svg",
|
||||
Icon::CopilotDisabled => "icons/copilot_disabled.svg",
|
||||
@ -155,6 +169,8 @@ impl Icon {
|
||||
Icon::Shift => "icons/shift.svg",
|
||||
Icon::Option => "icons/option.svg",
|
||||
Icon::Return => "icons/return.svg",
|
||||
Icon::Update => "icons/update.svg",
|
||||
Icon::ZedXCopilot => "icons/zed_x_copilot.svg",
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -170,13 +186,8 @@ impl RenderOnce for IconElement {
|
||||
type Rendered = Svg;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let svg_size = match self.size {
|
||||
IconSize::Small => rems(14. / 16.),
|
||||
IconSize::Medium => rems(16. / 16.),
|
||||
};
|
||||
|
||||
svg()
|
||||
.size(svg_size)
|
||||
.size(self.size.rems())
|
||||
.flex_none()
|
||||
.path(self.path)
|
||||
.text_color(self.color.color(cx))
|
||||
|
60
crates/ui2/src/components/indicator.rs
Normal file
60
crates/ui2/src/components/indicator.rs
Normal file
@ -0,0 +1,60 @@
|
||||
use gpui::{Div, Position};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub enum IndicatorStyle {
|
||||
#[default]
|
||||
Dot,
|
||||
Bar,
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Indicator {
|
||||
position: Position,
|
||||
style: IndicatorStyle,
|
||||
color: Color,
|
||||
}
|
||||
|
||||
impl Indicator {
|
||||
pub fn dot() -> Self {
|
||||
Self {
|
||||
position: Position::Relative,
|
||||
style: IndicatorStyle::Dot,
|
||||
color: Color::Default,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bar() -> Self {
|
||||
Self {
|
||||
position: Position::Relative,
|
||||
style: IndicatorStyle::Dot,
|
||||
color: Color::Default,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn absolute(mut self) -> Self {
|
||||
self.position = Position::Absolute;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Indicator {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
div()
|
||||
.flex_none()
|
||||
.map(|this| match self.style {
|
||||
IndicatorStyle::Dot => this.w_1p5().h_1p5().rounded_full(),
|
||||
IndicatorStyle::Bar => this.w_full().h_1p5().rounded_t_md(),
|
||||
})
|
||||
.when(self.position == Position::Absolute, |this| this.absolute())
|
||||
.bg(self.color.color(cx))
|
||||
}
|
||||
}
|
@ -1,180 +1,7 @@
|
||||
use std::ops::Range;
|
||||
mod highlighted_label;
|
||||
mod label;
|
||||
mod label_like;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::styled_ext::StyledExt;
|
||||
use gpui::{relative, Div, HighlightStyle, IntoElement, StyledText, WindowContext};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
|
||||
pub enum LabelSize {
|
||||
#[default]
|
||||
Default,
|
||||
Small,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum LineHeightStyle {
|
||||
#[default]
|
||||
TextLabel,
|
||||
/// Sets the line height to 1
|
||||
UILabel,
|
||||
}
|
||||
|
||||
#[derive(IntoElement, Clone)]
|
||||
pub struct Label {
|
||||
label: SharedString,
|
||||
size: LabelSize,
|
||||
line_height_style: LineHeightStyle,
|
||||
color: Color,
|
||||
strikethrough: bool,
|
||||
}
|
||||
|
||||
impl RenderOnce for Label {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
div()
|
||||
.when(self.strikethrough, |this| {
|
||||
this.relative().child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_1_2()
|
||||
.w_full()
|
||||
.h_px()
|
||||
.bg(Color::Hidden.color(cx)),
|
||||
)
|
||||
})
|
||||
.map(|this| match self.size {
|
||||
LabelSize::Default => this.text_ui(),
|
||||
LabelSize::Small => this.text_ui_sm(),
|
||||
})
|
||||
.when(self.line_height_style == LineHeightStyle::UILabel, |this| {
|
||||
this.line_height(relative(1.))
|
||||
})
|
||||
.text_color(self.color.color(cx))
|
||||
.child(self.label.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Label {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
size: LabelSize::Default,
|
||||
line_height_style: LineHeightStyle::default(),
|
||||
color: Color::Default,
|
||||
strikethrough: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: LabelSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
|
||||
self.line_height_style = line_height_style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
|
||||
self.strikethrough = strikethrough;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct HighlightedLabel {
|
||||
label: SharedString,
|
||||
size: LabelSize,
|
||||
color: Color,
|
||||
highlight_indices: Vec<usize>,
|
||||
strikethrough: bool,
|
||||
}
|
||||
|
||||
impl RenderOnce for HighlightedLabel {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let highlight_color = cx.theme().colors().text_accent;
|
||||
|
||||
let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
|
||||
let mut highlights: Vec<(Range<usize>, HighlightStyle)> = Vec::new();
|
||||
|
||||
while let Some(start_ix) = highlight_indices.next() {
|
||||
let mut end_ix = start_ix;
|
||||
|
||||
loop {
|
||||
end_ix = end_ix + self.label[end_ix..].chars().next().unwrap().len_utf8();
|
||||
if let Some(&next_ix) = highlight_indices.peek() {
|
||||
if next_ix == end_ix {
|
||||
end_ix = next_ix;
|
||||
highlight_indices.next();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
highlights.push((
|
||||
start_ix..end_ix,
|
||||
HighlightStyle {
|
||||
color: Some(highlight_color),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.when(self.strikethrough, |this| {
|
||||
this.relative().child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_px()
|
||||
.my_auto()
|
||||
.w_full()
|
||||
.h_px()
|
||||
.bg(Color::Hidden.color(cx)),
|
||||
)
|
||||
})
|
||||
.map(|this| match self.size {
|
||||
LabelSize::Default => this.text_ui(),
|
||||
LabelSize::Small => this.text_ui_sm(),
|
||||
})
|
||||
.child(StyledText::new(self.label).with_highlights(&cx.text_style(), highlights))
|
||||
}
|
||||
}
|
||||
|
||||
impl HighlightedLabel {
|
||||
/// shows a label with the given characters highlighted.
|
||||
/// characters are identified by utf8 byte position.
|
||||
pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
size: LabelSize::Default,
|
||||
color: Color::Default,
|
||||
highlight_indices,
|
||||
strikethrough: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: LabelSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
|
||||
self.strikethrough = strikethrough;
|
||||
self
|
||||
}
|
||||
}
|
||||
pub use highlighted_label::*;
|
||||
pub use label::*;
|
||||
pub use label_like::*;
|
||||
|
86
crates/ui2/src/components/label/highlighted_label.rs
Normal file
86
crates/ui2/src/components/label/highlighted_label.rs
Normal file
@ -0,0 +1,86 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::{HighlightStyle, StyledText};
|
||||
|
||||
use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct HighlightedLabel {
|
||||
base: LabelLike,
|
||||
label: SharedString,
|
||||
highlight_indices: Vec<usize>,
|
||||
}
|
||||
|
||||
impl HighlightedLabel {
|
||||
/// Constructs a label with the given characters highlighted.
|
||||
/// Characters are identified by UTF-8 byte position.
|
||||
pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
|
||||
Self {
|
||||
base: LabelLike::new(),
|
||||
label: label.into(),
|
||||
highlight_indices,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LabelCommon for HighlightedLabel {
|
||||
fn size(mut self, size: LabelSize) -> Self {
|
||||
self.base = self.base.size(size);
|
||||
self
|
||||
}
|
||||
|
||||
fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
|
||||
self.base = self.base.line_height_style(line_height_style);
|
||||
self
|
||||
}
|
||||
|
||||
fn color(mut self, color: Color) -> Self {
|
||||
self.base = self.base.color(color);
|
||||
self
|
||||
}
|
||||
|
||||
fn strikethrough(mut self, strikethrough: bool) -> Self {
|
||||
self.base = self.base.strikethrough(strikethrough);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for HighlightedLabel {
|
||||
type Rendered = LabelLike;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let highlight_color = cx.theme().colors().text_accent;
|
||||
|
||||
let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
|
||||
let mut highlights: Vec<(Range<usize>, HighlightStyle)> = Vec::new();
|
||||
|
||||
while let Some(start_ix) = highlight_indices.next() {
|
||||
let mut end_ix = start_ix;
|
||||
|
||||
loop {
|
||||
end_ix = end_ix + self.label[end_ix..].chars().next().unwrap().len_utf8();
|
||||
if let Some(&next_ix) = highlight_indices.peek() {
|
||||
if next_ix == end_ix {
|
||||
end_ix = next_ix;
|
||||
highlight_indices.next();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
highlights.push((
|
||||
start_ix..end_ix,
|
||||
HighlightStyle {
|
||||
color: Some(highlight_color),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let mut text_style = cx.text_style().clone();
|
||||
text_style.color = self.base.color.color(cx);
|
||||
|
||||
LabelLike::new().child(StyledText::new(self.label).with_highlights(&text_style, highlights))
|
||||
}
|
||||
}
|
48
crates/ui2/src/components/label/label.rs
Normal file
48
crates/ui2/src/components/label/label.rs
Normal file
@ -0,0 +1,48 @@
|
||||
use gpui::WindowContext;
|
||||
|
||||
use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Label {
|
||||
base: LabelLike,
|
||||
label: SharedString,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
base: LabelLike::new(),
|
||||
label: label.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LabelCommon for Label {
|
||||
fn size(mut self, size: LabelSize) -> Self {
|
||||
self.base = self.base.size(size);
|
||||
self
|
||||
}
|
||||
|
||||
fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
|
||||
self.base = self.base.line_height_style(line_height_style);
|
||||
self
|
||||
}
|
||||
|
||||
fn color(mut self, color: Color) -> Self {
|
||||
self.base = self.base.color(color);
|
||||
self
|
||||
}
|
||||
|
||||
fn strikethrough(mut self, strikethrough: bool) -> Self {
|
||||
self.base = self.base.strikethrough(strikethrough);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Label {
|
||||
type Rendered = LabelLike;
|
||||
|
||||
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
|
||||
self.base.child(self.label)
|
||||
}
|
||||
}
|
102
crates/ui2/src/components/label/label_like.rs
Normal file
102
crates/ui2/src/components/label/label_like.rs
Normal file
@ -0,0 +1,102 @@
|
||||
use gpui::{relative, AnyElement, Div, Styled};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
|
||||
pub enum LabelSize {
|
||||
#[default]
|
||||
Default,
|
||||
Small,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum LineHeightStyle {
|
||||
#[default]
|
||||
TextLabel,
|
||||
/// Sets the line height to 1
|
||||
UILabel,
|
||||
}
|
||||
|
||||
pub trait LabelCommon {
|
||||
fn size(self, size: LabelSize) -> Self;
|
||||
fn line_height_style(self, line_height_style: LineHeightStyle) -> Self;
|
||||
fn color(self, color: Color) -> Self;
|
||||
fn strikethrough(self, strikethrough: bool) -> Self;
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct LabelLike {
|
||||
size: LabelSize,
|
||||
line_height_style: LineHeightStyle,
|
||||
pub(crate) color: Color,
|
||||
strikethrough: bool,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl LabelLike {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
size: LabelSize::Default,
|
||||
line_height_style: LineHeightStyle::default(),
|
||||
color: Color::Default,
|
||||
strikethrough: false,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LabelCommon for LabelLike {
|
||||
fn size(mut self, size: LabelSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
|
||||
self.line_height_style = line_height_style;
|
||||
self
|
||||
}
|
||||
|
||||
fn color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
fn strikethrough(mut self, strikethrough: bool) -> Self {
|
||||
self.strikethrough = strikethrough;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for LabelLike {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for LabelLike {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
div()
|
||||
.when(self.strikethrough, |this| {
|
||||
this.relative().child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_1_2()
|
||||
.w_full()
|
||||
.h_px()
|
||||
.bg(Color::Hidden.color(cx)),
|
||||
)
|
||||
})
|
||||
.map(|this| match self.size {
|
||||
LabelSize::Default => this.text_ui(),
|
||||
LabelSize::Small => this.text_ui_sm(),
|
||||
})
|
||||
.when(self.line_height_style == LineHeightStyle::UILabel, |this| {
|
||||
this.line_height(relative(1.))
|
||||
})
|
||||
.text_color(self.color.color(cx))
|
||||
.children(self.children)
|
||||
}
|
||||
}
|
@ -51,5 +51,13 @@ impl Render for IconButtonStory {
|
||||
.tooltip(|cx| Tooltip::text("Open messages", cx)),
|
||||
),
|
||||
)
|
||||
.child(Story::label("Selected with `tooltip`"))
|
||||
.child(
|
||||
div().w_8().child(
|
||||
IconButton::new("selected_with_tooltip", Icon::InlayHint)
|
||||
.selected(true)
|
||||
.tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -23,5 +23,9 @@ impl Render for LabelStory {
|
||||
"Héllo, world!",
|
||||
vec![0, 1, 3, 8, 9, 13],
|
||||
))
|
||||
.child(Story::label("Highlighted with `color`"))
|
||||
.child(
|
||||
HighlightedLabel::new("Hello, world!", vec![0, 1, 2, 7, 8, 12]).color(Color::Error),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -84,6 +84,7 @@ impl Render for Tooltip {
|
||||
.px_2()
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.child(self.title.clone())
|
||||
.when_some(self.key_binding.clone(), |this, key_binding| {
|
||||
this.justify_between().child(key_binding)
|
||||
|
@ -8,5 +8,6 @@ pub use crate::clickable::*;
|
||||
pub use crate::disableable::*;
|
||||
pub use crate::fixed::*;
|
||||
pub use crate::selectable::*;
|
||||
pub use crate::{ButtonCommon, Color, StyledExt};
|
||||
pub use crate::{h_stack, v_stack};
|
||||
pub use crate::{ButtonCommon, Color, LabelCommon, StyledExt};
|
||||
pub use theme::ActiveTheme;
|
||||
|
@ -70,8 +70,7 @@ pub trait StyledExt: Styled + Sized {
|
||||
/// or other places that text needs to match the user's buffer font size.
|
||||
fn text_buffer(self, cx: &mut WindowContext) -> Self {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
|
||||
self.text_size(settings.buffer_font_size)
|
||||
self.text_size(settings.buffer_font_size(cx))
|
||||
}
|
||||
|
||||
/// The [`Surface`](ui2::ElevationIndex::Surface) elevation level, located above the app background, is the standard level for all elements
|
||||
|
@ -44,12 +44,18 @@ impl<'a, T: ?Sized> From<&'a T> for ArcCow<'a, T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Arc<T>> for ArcCow<'_, T> {
|
||||
impl<T: ?Sized> From<Arc<T>> for ArcCow<'_, T> {
|
||||
fn from(s: Arc<T>) -> Self {
|
||||
Self::Owned(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> From<&'_ Arc<T>> for ArcCow<'_, T> {
|
||||
fn from(s: &'_ Arc<T>) -> Self {
|
||||
Self::Owned(s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ArcCow<'_, str> {
|
||||
fn from(value: String) -> Self {
|
||||
Self::Owned(value.into())
|
||||
|
@ -101,7 +101,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
// will be initialized as disabled by default, so we filter its commands
|
||||
// out when starting up.
|
||||
cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
|
||||
filter.filtered_namespaces.insert("vim");
|
||||
filter.hidden_namespaces.insert("vim");
|
||||
});
|
||||
cx.update_global(|vim: &mut Vim, cx: &mut AppContext| {
|
||||
vim.set_enabled(settings::get::<VimModeSetting>(cx).0, cx)
|
||||
@ -477,9 +477,9 @@ impl Vim {
|
||||
|
||||
cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
|
||||
if self.enabled {
|
||||
filter.filtered_namespaces.remove("vim");
|
||||
filter.hidden_namespaces.remove("vim");
|
||||
} else {
|
||||
filter.filtered_namespaces.insert("vim");
|
||||
filter.hidden_namespaces.insert("vim");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -259,6 +259,8 @@ impl FocusableView for WelcomePage {
|
||||
}
|
||||
|
||||
impl Item for WelcomePage {
|
||||
type Event = ItemEvent;
|
||||
|
||||
fn tab_content(&self, _: Option<usize>, _: &WindowContext) -> AnyElement {
|
||||
"Welcome to Zed!".into_any()
|
||||
}
|
||||
@ -278,4 +280,8 @@ impl Item for WelcomePage {
|
||||
_settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
|
||||
}))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
|
||||
f(*event)
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
|
||||
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
|
||||
fn size(&self, cx: &WindowContext) -> f32;
|
||||
fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>);
|
||||
// todo!("We should have a icon tooltip method, rather than using persistant_name")
|
||||
fn icon(&self, cx: &WindowContext) -> Option<ui::Icon>;
|
||||
fn toggle_action(&self) -> Box<dyn Action>;
|
||||
fn icon_label(&self, _: &WindowContext) -> Option<String> {
|
||||
@ -36,7 +37,6 @@ pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
|
||||
}
|
||||
fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
|
||||
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
|
||||
fn has_focus(&self, cx: &WindowContext) -> bool;
|
||||
}
|
||||
|
||||
pub trait PanelHandle: Send + Sync {
|
||||
@ -53,7 +53,6 @@ pub trait PanelHandle: Send + Sync {
|
||||
fn icon(&self, cx: &WindowContext) -> Option<ui::Icon>;
|
||||
fn toggle_action(&self, cx: &WindowContext) -> Box<dyn Action>;
|
||||
fn icon_label(&self, cx: &WindowContext) -> Option<String>;
|
||||
fn has_focus(&self, cx: &WindowContext) -> bool;
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
|
||||
fn to_any(&self) -> AnyView;
|
||||
}
|
||||
@ -114,10 +113,6 @@ where
|
||||
self.read(cx).icon_label(cx)
|
||||
}
|
||||
|
||||
fn has_focus(&self, cx: &WindowContext) -> bool {
|
||||
self.read(cx).has_focus(cx)
|
||||
}
|
||||
|
||||
fn to_any(&self) -> AnyView {
|
||||
self.clone().into()
|
||||
}
|
||||
@ -319,7 +314,7 @@ impl Dock {
|
||||
}
|
||||
PanelEvent::ZoomIn => {
|
||||
this.set_panel_zoomed(&panel.to_any(), true, cx);
|
||||
if !panel.has_focus(cx) {
|
||||
if !panel.focus_handle(cx).contains_focused(cx) {
|
||||
cx.focus_view(&panel);
|
||||
}
|
||||
workspace
|
||||
@ -729,7 +724,10 @@ impl Render for PanelButtons {
|
||||
.trigger(
|
||||
IconButton::new(name, icon)
|
||||
.selected(is_active_button)
|
||||
.action(action.boxed_clone())
|
||||
.on_click({
|
||||
let action = action.boxed_clone();
|
||||
move |_, cx| cx.dispatch_action(action.boxed_clone())
|
||||
})
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action(tooltip.clone(), &*action, cx)
|
||||
}),
|
||||
@ -760,7 +758,7 @@ pub mod test {
|
||||
pub position: DockPosition,
|
||||
pub zoomed: bool,
|
||||
pub active: bool,
|
||||
pub has_focus: bool,
|
||||
pub focus_handle: FocusHandle,
|
||||
pub size: f32,
|
||||
}
|
||||
actions!(ToggleTestPanel);
|
||||
@ -768,12 +766,12 @@ pub mod test {
|
||||
impl EventEmitter<PanelEvent> for TestPanel {}
|
||||
|
||||
impl TestPanel {
|
||||
pub fn new(position: DockPosition) -> Self {
|
||||
pub fn new(position: DockPosition, cx: &mut WindowContext) -> Self {
|
||||
Self {
|
||||
position,
|
||||
zoomed: false,
|
||||
active: false,
|
||||
has_focus: false,
|
||||
focus_handle: cx.focus_handle(),
|
||||
size: 300.,
|
||||
}
|
||||
}
|
||||
@ -832,15 +830,11 @@ pub mod test {
|
||||
fn set_active(&mut self, active: bool, _cx: &mut ViewContext<Self>) {
|
||||
self.active = active;
|
||||
}
|
||||
|
||||
fn has_focus(&self, _cx: &WindowContext) -> bool {
|
||||
self.has_focus
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for TestPanel {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
unimplemented!()
|
||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ impl Settings for ItemSettings {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash, Debug)]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)]
|
||||
pub enum ItemEvent {
|
||||
CloseItem,
|
||||
UpdateTab,
|
||||
@ -92,7 +92,9 @@ pub struct BreadcrumbText {
|
||||
pub highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
|
||||
}
|
||||
|
||||
pub trait Item: FocusableView + EventEmitter<ItemEvent> {
|
||||
pub trait Item: FocusableView + EventEmitter<Self::Event> {
|
||||
type Event;
|
||||
|
||||
fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
|
||||
fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
|
||||
fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
|
||||
@ -155,6 +157,8 @@ pub trait Item: FocusableView + EventEmitter<ItemEvent> {
|
||||
unimplemented!("reload() must be implemented if can_save() returns true")
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event, f: impl FnMut(ItemEvent));
|
||||
|
||||
fn act_as_type<'a>(
|
||||
&'a self,
|
||||
type_id: TypeId,
|
||||
@ -206,12 +210,12 @@ pub trait Item: FocusableView + EventEmitter<ItemEvent> {
|
||||
}
|
||||
|
||||
pub trait ItemHandle: 'static + Send {
|
||||
fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
|
||||
fn subscribe_to_item_events(
|
||||
&self,
|
||||
cx: &mut WindowContext,
|
||||
handler: Box<dyn Fn(&ItemEvent, &mut WindowContext) + Send>,
|
||||
handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
|
||||
) -> gpui::Subscription;
|
||||
fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
|
||||
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString>;
|
||||
fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString>;
|
||||
fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement;
|
||||
@ -285,20 +289,20 @@ impl dyn ItemHandle {
|
||||
}
|
||||
|
||||
impl<T: Item> ItemHandle for View<T> {
|
||||
fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
|
||||
self.focus_handle(cx)
|
||||
}
|
||||
|
||||
fn subscribe_to_item_events(
|
||||
&self,
|
||||
cx: &mut WindowContext,
|
||||
handler: Box<dyn Fn(&ItemEvent, &mut WindowContext) + Send>,
|
||||
handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
|
||||
) -> gpui::Subscription {
|
||||
cx.subscribe(self, move |_, event, cx| {
|
||||
handler(event, cx);
|
||||
T::to_item_events(event, |item_event| handler(item_event, cx));
|
||||
})
|
||||
}
|
||||
|
||||
fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
|
||||
self.focus_handle(cx)
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
|
||||
self.read(cx).tab_tooltip_text(cx)
|
||||
}
|
||||
@ -461,7 +465,7 @@ impl<T: Item> ItemHandle for View<T> {
|
||||
}
|
||||
}
|
||||
|
||||
match event {
|
||||
T::to_item_events(event, |event| match event {
|
||||
ItemEvent::CloseItem => {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.close_item_by_id(item.item_id(), crate::SaveIntent::Close, cx)
|
||||
@ -489,7 +493,7 @@ impl<T: Item> ItemHandle for View<T> {
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
cx.on_blur(&self.focus_handle(cx), move |workspace, cx| {
|
||||
@ -655,12 +659,7 @@ pub enum FollowEvent {
|
||||
Unfollow,
|
||||
}
|
||||
|
||||
pub trait FollowableEvents {
|
||||
fn to_follow_event(&self) -> Option<FollowEvent>;
|
||||
}
|
||||
|
||||
pub trait FollowableItem: Item {
|
||||
type FollowableEvent: FollowableEvents;
|
||||
fn remote_id(&self) -> Option<ViewId>;
|
||||
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>;
|
||||
fn from_state_proto(
|
||||
@ -670,9 +669,10 @@ pub trait FollowableItem: Item {
|
||||
state: &mut Option<proto::view::Variant>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<Task<Result<View<Self>>>>;
|
||||
fn to_follow_event(event: &Self::Event) -> Option<FollowEvent>;
|
||||
fn add_event_to_update_proto(
|
||||
&self,
|
||||
event: &Self::FollowableEvent,
|
||||
event: &Self::Event,
|
||||
update: &mut Option<proto::update_view::Variant>,
|
||||
cx: &WindowContext,
|
||||
) -> bool;
|
||||
@ -683,7 +683,6 @@ pub trait FollowableItem: Item {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>>;
|
||||
fn is_project_item(&self, cx: &WindowContext) -> bool;
|
||||
|
||||
fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>);
|
||||
}
|
||||
|
||||
@ -739,10 +738,7 @@ impl<T: FollowableItem> FollowableItemHandle for View<T> {
|
||||
}
|
||||
|
||||
fn to_follow_event(&self, event: &dyn Any) -> Option<FollowEvent> {
|
||||
event
|
||||
.downcast_ref()
|
||||
.map(T::FollowableEvent::to_follow_event)
|
||||
.flatten()
|
||||
T::to_follow_event(event.downcast_ref()?)
|
||||
}
|
||||
|
||||
fn apply_update_proto(
|
||||
@ -929,6 +925,12 @@ pub mod test {
|
||||
}
|
||||
|
||||
impl Item for TestItem {
|
||||
type Event = ItemEvent;
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
|
||||
f(*event)
|
||||
}
|
||||
|
||||
fn tab_description(&self, detail: usize, _: &AppContext) -> Option<SharedString> {
|
||||
self.tab_descriptions.as_ref().and_then(|descriptions| {
|
||||
let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
item::{Item, ItemHandle, ItemSettings, WeakItemHandle},
|
||||
item::{ClosePosition, Item, ItemHandle, ItemSettings, WeakItemHandle},
|
||||
toolbar::Toolbar,
|
||||
workspace_settings::{AutosaveSetting, WorkspaceSettings},
|
||||
NewCenterTerminal, NewFile, NewSearch, SplitDirection, ToggleZoom, Workspace,
|
||||
@ -27,7 +27,8 @@ use std::{
|
||||
};
|
||||
|
||||
use ui::{
|
||||
h_stack, prelude::*, right_click_menu, Color, Icon, IconButton, IconElement, Label, Tooltip,
|
||||
h_stack, prelude::*, right_click_menu, ButtonSize, Color, Icon, IconButton, IconSize,
|
||||
Indicator, Label, Tooltip,
|
||||
};
|
||||
use ui::{v_stack, ContextMenu};
|
||||
use util::truncate_and_remove_front;
|
||||
@ -1418,22 +1419,7 @@ impl Pane {
|
||||
cx: &mut ViewContext<'_, Pane>,
|
||||
) -> impl IntoElement {
|
||||
let label = item.tab_content(Some(detail), cx);
|
||||
let close_icon = || {
|
||||
let id = item.item_id();
|
||||
|
||||
div()
|
||||
.id(ix)
|
||||
.invisible()
|
||||
.group_hover("", |style| style.visible())
|
||||
.child(
|
||||
IconButton::new("close_tab", Icon::Close).on_click(cx.listener(
|
||||
move |pane, _, cx| {
|
||||
pane.close_item_by_id(id, SaveIntent::Close, cx)
|
||||
.detach_and_log_err(cx);
|
||||
},
|
||||
)),
|
||||
)
|
||||
};
|
||||
let close_side = &ItemSettings::get_global(cx).close_position;
|
||||
|
||||
let (text_color, tab_bg, tab_hover_bg, tab_active_bg) = match ix == self.active_item_index {
|
||||
false => (
|
||||
@ -1450,102 +1436,129 @@ impl Pane {
|
||||
),
|
||||
};
|
||||
|
||||
let close_right = ItemSettings::get_global(cx).close_position.right();
|
||||
let is_active = ix == self.active_item_index;
|
||||
|
||||
let indicator = {
|
||||
let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
|
||||
(true, _) => Some(Color::Warning),
|
||||
(_, true) => Some(Color::Accent),
|
||||
(false, false) => None,
|
||||
};
|
||||
|
||||
h_stack()
|
||||
.w_3()
|
||||
.h_3()
|
||||
.justify_center()
|
||||
.absolute()
|
||||
.map(|this| match close_side {
|
||||
ClosePosition::Left => this.right_1(),
|
||||
ClosePosition::Right => this.left_1(),
|
||||
})
|
||||
.when_some(indicator_color, |this, indicator_color| {
|
||||
this.child(Indicator::dot().color(indicator_color))
|
||||
})
|
||||
};
|
||||
|
||||
let close_button = {
|
||||
let id = item.item_id();
|
||||
|
||||
h_stack()
|
||||
.invisible()
|
||||
.w_3()
|
||||
.h_3()
|
||||
.justify_center()
|
||||
.absolute()
|
||||
.map(|this| match close_side {
|
||||
ClosePosition::Left => this.left_1(),
|
||||
ClosePosition::Right => this.right_1(),
|
||||
})
|
||||
.group_hover("", |style| style.visible())
|
||||
.child(
|
||||
// TODO: Fix button size
|
||||
IconButton::new("close tab", Icon::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::None)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(cx.listener(move |pane, _, cx| {
|
||||
pane.close_item_by_id(id, SaveIntent::Close, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})),
|
||||
)
|
||||
};
|
||||
|
||||
let tab = div()
|
||||
.group("")
|
||||
.id(ix)
|
||||
.cursor_pointer()
|
||||
.when_some(item.tab_tooltip_text(cx), |div, text| {
|
||||
div.tooltip(move |cx| cx.build_view(|cx| Tooltip::new(text.clone())).into())
|
||||
})
|
||||
.on_click(cx.listener(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx)))
|
||||
// .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx))
|
||||
// .drag_over::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))
|
||||
// .on_drop(|_view, state: View<DraggedTab>, cx| {
|
||||
// eprintln!("{:?}", state.read(cx));
|
||||
// })
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
// todo!("Nate - I need to do some work to balance all the items in the tab once things stablize")
|
||||
.map(|this| {
|
||||
if close_right {
|
||||
this.pl_3().pr_1()
|
||||
} else {
|
||||
this.pr_1().pr_3()
|
||||
}
|
||||
})
|
||||
.py_1()
|
||||
.bg(tab_bg)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.text_color(if is_active {
|
||||
cx.theme().colors().text
|
||||
} else {
|
||||
cx.theme().colors().text_muted
|
||||
})
|
||||
.bg(tab_bg)
|
||||
// 30px @ 16px/rem
|
||||
.h(rems(1.875))
|
||||
.map(|this| {
|
||||
let is_first_item = ix == 0;
|
||||
let is_last_item = ix == self.items.len() - 1;
|
||||
match ix.cmp(&self.active_item_index) {
|
||||
cmp::Ordering::Less => this.border_l().mr_px(),
|
||||
cmp::Ordering::Greater => {
|
||||
if is_last_item {
|
||||
this.mr_px().ml_px()
|
||||
cmp::Ordering::Less => {
|
||||
if is_first_item {
|
||||
this.pl_px().pr_px().border_b()
|
||||
} else {
|
||||
this.border_r().ml_px()
|
||||
this.border_l().pr_px().border_b()
|
||||
}
|
||||
}
|
||||
cmp::Ordering::Greater => {
|
||||
if is_last_item {
|
||||
this.pr_px().pl_px().border_b()
|
||||
} else {
|
||||
this.border_r().pl_px().border_b()
|
||||
}
|
||||
}
|
||||
cmp::Ordering::Equal => {
|
||||
if is_first_item {
|
||||
this.pl_px().border_r().pb_px()
|
||||
} else {
|
||||
this.border_l().border_r().pb_px()
|
||||
}
|
||||
}
|
||||
cmp::Ordering::Equal => this.border_l().border_r(),
|
||||
}
|
||||
})
|
||||
// .hover(|h| h.bg(tab_hover_bg))
|
||||
// .active(|a| a.bg(tab_active_bg))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
h_stack()
|
||||
.group("")
|
||||
.id(ix)
|
||||
.relative()
|
||||
.h_full()
|
||||
.cursor_pointer()
|
||||
.when_some(item.tab_tooltip_text(cx), |div, text| {
|
||||
div.tooltip(move |cx| cx.build_view(|cx| Tooltip::new(text.clone())).into())
|
||||
})
|
||||
.on_click(
|
||||
cx.listener(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx)),
|
||||
)
|
||||
// .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx))
|
||||
// .drag_over::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))
|
||||
// .on_drop(|_view, state: View<DraggedTab>, cx| {
|
||||
// eprintln!("{:?}", state.read(cx));
|
||||
// })
|
||||
.px_5()
|
||||
// .hover(|h| h.bg(tab_hover_bg))
|
||||
// .active(|a| a.bg(tab_active_bg))
|
||||
.gap_1()
|
||||
.text_color(text_color)
|
||||
.children(
|
||||
item.has_conflict(cx)
|
||||
.then(|| {
|
||||
div().border().border_color(gpui::red()).child(
|
||||
IconElement::new(Icon::ExclamationTriangle)
|
||||
.size(ui::IconSize::Small)
|
||||
.color(Color::Warning),
|
||||
)
|
||||
})
|
||||
.or(item.is_dirty(cx).then(|| {
|
||||
div().border().border_color(gpui::red()).child(
|
||||
IconElement::new(Icon::ExclamationTriangle)
|
||||
.size(ui::IconSize::Small)
|
||||
.color(Color::Info),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.children((!close_right).then(|| close_icon()))
|
||||
.child(label)
|
||||
.children(close_right.then(|| close_icon())),
|
||||
.child(indicator)
|
||||
.child(close_button)
|
||||
.child(label),
|
||||
);
|
||||
|
||||
right_click_menu(ix).trigger(tab).menu(|cx| {
|
||||
ContextMenu::build(cx, |menu, cx| {
|
||||
menu.action(
|
||||
"Close Active Item",
|
||||
CloseActiveItem { save_intent: None }.boxed_clone(),
|
||||
)
|
||||
.action("Close Inactive Items", CloseInactiveItems.boxed_clone())
|
||||
.action("Close Clean Items", CloseCleanItems.boxed_clone())
|
||||
.action("Close Items To The Left", CloseItemsToTheLeft.boxed_clone())
|
||||
.action(
|
||||
"Close Items To The Right",
|
||||
CloseItemsToTheRight.boxed_clone(),
|
||||
)
|
||||
.action(
|
||||
"Close All Items",
|
||||
CloseAllItems { save_intent: None }.boxed_clone(),
|
||||
)
|
||||
menu.action("Close", CloseActiveItem { save_intent: None }.boxed_clone())
|
||||
.action("Close Others", CloseInactiveItems.boxed_clone())
|
||||
.separator()
|
||||
.action("Close Left", CloseItemsToTheLeft.boxed_clone())
|
||||
.action("Close Right", CloseItemsToTheRight.boxed_clone())
|
||||
.separator()
|
||||
.action("Close Clean", CloseCleanItems.boxed_clone())
|
||||
.action(
|
||||
"Close All",
|
||||
CloseAllItems { save_intent: None }.boxed_clone(),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -1565,116 +1578,118 @@ impl Pane {
|
||||
// Left Side
|
||||
.child(
|
||||
h_stack()
|
||||
.px_2()
|
||||
.flex()
|
||||
.flex_none()
|
||||
.gap_1()
|
||||
.px_1()
|
||||
.border_b()
|
||||
.border_r()
|
||||
.border_color(cx.theme().colors().border)
|
||||
// Nav Buttons
|
||||
.child(
|
||||
div().border().border_color(gpui::red()).child(
|
||||
IconButton::new("navigate_backward", Icon::ArrowLeft)
|
||||
.on_click({
|
||||
let view = cx.view().clone();
|
||||
move |_, cx| view.update(cx, Self::navigate_backward)
|
||||
})
|
||||
.disabled(!self.can_navigate_backward()),
|
||||
),
|
||||
IconButton::new("navigate_backward", Icon::ArrowLeft)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click({
|
||||
let view = cx.view().clone();
|
||||
move |_, cx| view.update(cx, Self::navigate_backward)
|
||||
})
|
||||
.disabled(!self.can_navigate_backward()),
|
||||
)
|
||||
.child(
|
||||
div().border().border_color(gpui::red()).child(
|
||||
IconButton::new("navigate_forward", Icon::ArrowRight)
|
||||
.on_click({
|
||||
let view = cx.view().clone();
|
||||
move |_, cx| view.update(cx, Self::navigate_backward)
|
||||
})
|
||||
.disabled(!self.can_navigate_forward()),
|
||||
),
|
||||
IconButton::new("navigate_forward", Icon::ArrowRight)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click({
|
||||
let view = cx.view().clone();
|
||||
move |_, cx| view.update(cx, Self::navigate_backward)
|
||||
})
|
||||
.disabled(!self.can_navigate_forward()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().h_full().child(
|
||||
div().id("tabs").flex().overflow_x_scroll().children(
|
||||
self.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.zip(self.tab_details(cx))
|
||||
.map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)),
|
||||
div()
|
||||
.relative()
|
||||
.flex_1()
|
||||
.h_full()
|
||||
.overflow_hidden_x()
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.z_index(1)
|
||||
.size_full()
|
||||
.border_b()
|
||||
.border_color(cx.theme().colors().border),
|
||||
)
|
||||
.child(
|
||||
h_stack().id("tabs").z_index(2).children(
|
||||
self.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.zip(self.tab_details(cx))
|
||||
.map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
// Right Side
|
||||
.child(
|
||||
div()
|
||||
.px_1()
|
||||
h_stack()
|
||||
.flex()
|
||||
.flex_none()
|
||||
.gap_2()
|
||||
// Nav Buttons
|
||||
.gap_1()
|
||||
.px_1()
|
||||
.border_b()
|
||||
.border_l()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_px()
|
||||
.child(
|
||||
div()
|
||||
.bg(gpui::blue())
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
.child(IconButton::new("plus", Icon::Plus).on_click(
|
||||
cx.listener(|this, _, cx| {
|
||||
let menu = ContextMenu::build(cx, |menu, cx| {
|
||||
menu.action("New File", NewFile.boxed_clone())
|
||||
.action(
|
||||
"New Terminal",
|
||||
NewCenterTerminal.boxed_clone(),
|
||||
)
|
||||
.action("New Search", NewSearch.boxed_clone())
|
||||
});
|
||||
cx.subscribe(
|
||||
&menu,
|
||||
|this, _, event: &DismissEvent, cx| {
|
||||
this.focus(cx);
|
||||
this.new_item_menu = None;
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
this.new_item_menu = Some(menu);
|
||||
}),
|
||||
))
|
||||
.when_some(self.new_item_menu.as_ref(), |el, new_item_menu| {
|
||||
el.child(Self::render_menu_overlay(new_item_menu))
|
||||
}),
|
||||
IconButton::new("plus", Icon::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
let menu = ContextMenu::build(cx, |menu, cx| {
|
||||
menu.action("New File", NewFile.boxed_clone())
|
||||
.action(
|
||||
"New Terminal",
|
||||
NewCenterTerminal.boxed_clone(),
|
||||
)
|
||||
.action("New Search", NewSearch.boxed_clone())
|
||||
});
|
||||
cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| {
|
||||
this.focus(cx);
|
||||
this.new_item_menu = None;
|
||||
})
|
||||
.detach();
|
||||
this.new_item_menu = Some(menu);
|
||||
})),
|
||||
)
|
||||
.when_some(self.new_item_menu.as_ref(), |el, new_item_menu| {
|
||||
el.child(Self::render_menu_overlay(new_item_menu))
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
.child(IconButton::new("split", Icon::Split).on_click(
|
||||
cx.listener(|this, _, cx| {
|
||||
let menu = ContextMenu::build(cx, |menu, cx| {
|
||||
menu.action("Split Right", SplitRight.boxed_clone())
|
||||
.action("Split Left", SplitLeft.boxed_clone())
|
||||
.action("Split Up", SplitUp.boxed_clone())
|
||||
.action("Split Down", SplitDown.boxed_clone())
|
||||
});
|
||||
cx.subscribe(
|
||||
&menu,
|
||||
|this, _, event: &DismissEvent, cx| {
|
||||
this.focus(cx);
|
||||
this.split_item_menu = None;
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
this.split_item_menu = Some(menu);
|
||||
}),
|
||||
))
|
||||
.when_some(
|
||||
self.split_item_menu.as_ref(),
|
||||
|el, split_item_menu| {
|
||||
el.child(Self::render_menu_overlay(split_item_menu))
|
||||
},
|
||||
),
|
||||
),
|
||||
IconButton::new("split", Icon::Split)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
let menu = ContextMenu::build(cx, |menu, cx| {
|
||||
menu.action("Split Right", SplitRight.boxed_clone())
|
||||
.action("Split Left", SplitLeft.boxed_clone())
|
||||
.action("Split Up", SplitUp.boxed_clone())
|
||||
.action("Split Down", SplitDown.boxed_clone())
|
||||
});
|
||||
cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| {
|
||||
this.focus(cx);
|
||||
this.split_item_menu = None;
|
||||
})
|
||||
.detach();
|
||||
this.split_item_menu = Some(menu);
|
||||
})),
|
||||
)
|
||||
.when_some(self.split_item_menu.as_ref(), |el, split_item_menu| {
|
||||
el.child(Self::render_menu_overlay(split_item_menu))
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
@ -2092,6 +2107,8 @@ impl Render for Pane {
|
||||
v_stack()
|
||||
.key_context("Pane")
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.on_focus_in({
|
||||
let this = this.clone();
|
||||
move |event, cx| {
|
||||
@ -2159,7 +2176,6 @@ impl Render for Pane {
|
||||
pane.close_all_items(action, cx)
|
||||
.map(|task| task.detach_and_log_err(cx));
|
||||
}))
|
||||
.size_full()
|
||||
.on_action(
|
||||
cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
|
||||
pane.close_active_item(action, cx)
|
||||
|
@ -59,7 +59,6 @@ impl SharedScreen {
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for SharedScreen {}
|
||||
impl EventEmitter<ItemEvent> for SharedScreen {}
|
||||
|
||||
impl FocusableView for SharedScreen {
|
||||
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
|
||||
@ -79,9 +78,12 @@ impl Render for SharedScreen {
|
||||
}
|
||||
|
||||
impl Item for SharedScreen {
|
||||
type Event = Event;
|
||||
|
||||
fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
|
||||
Some(format!("{}'s screen", self.user.github_login).into())
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(nav_history) = self.nav_history.as_mut() {
|
||||
nav_history.push::<()>(None, cx);
|
||||
@ -111,4 +113,10 @@ impl Item for SharedScreen {
|
||||
let track = self.track.upgrade()?;
|
||||
Some(cx.build_view(|cx| Self::new(&track, self.peer_id, self.user.clone(), cx)))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
|
||||
match event {
|
||||
Event::Close => f(ItemEvent::CloseItem),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,22 +62,13 @@ impl Render for StatusBar {
|
||||
)
|
||||
.child(
|
||||
// Right Dock
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
// Terminal
|
||||
div()
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
.child(IconButton::new("status-assistant", Icon::Ai)),
|
||||
)
|
||||
.child(
|
||||
// Terminal
|
||||
div()
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
.child(IconButton::new("status-chat", Icon::MessageBubbles)),
|
||||
),
|
||||
h_stack().gap_1().child(
|
||||
// Terminal
|
||||
div()
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
.child(IconButton::new("status-chat", Icon::MessageBubbles)),
|
||||
),
|
||||
)
|
||||
.child(self.render_right_tools(cx)),
|
||||
)
|
||||
|
@ -1,10 +1,10 @@
|
||||
use crate::ItemHandle;
|
||||
use gpui::{
|
||||
div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View,
|
||||
AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View,
|
||||
ViewContext, WindowContext,
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use ui::{h_stack, v_stack, Icon, IconButton};
|
||||
use ui::{h_stack, v_stack};
|
||||
|
||||
pub enum ToolbarItemEvent {
|
||||
ChangeLocation(ToolbarItemLocation),
|
||||
@ -87,25 +87,7 @@ impl Render for Toolbar {
|
||||
.child(
|
||||
h_stack()
|
||||
.justify_between()
|
||||
// Toolbar left side
|
||||
.children(self.items.iter().map(|(child, _)| child.to_any()))
|
||||
// Toolbar right side
|
||||
.child(
|
||||
h_stack()
|
||||
.p_1()
|
||||
.child(
|
||||
div()
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
.child(IconButton::new("buffer-search", Icon::MagnifyingGlass)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
.child(IconButton::new("inline-assist", Icon::MagicWand)),
|
||||
),
|
||||
),
|
||||
.children(self.items.iter().map(|(child, _)| child.to_any())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
pub use toolbar::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
||||
pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
||||
pub use ui;
|
||||
use util::ResultExt;
|
||||
use uuid::Uuid;
|
||||
@ -212,27 +212,31 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
init_settings(cx);
|
||||
notifications::init(cx);
|
||||
|
||||
// cx.add_global_action({
|
||||
// let app_state = Arc::downgrade(&app_state);
|
||||
// move |_: &Open, cx: &mut AppContext| {
|
||||
// let mut paths = cx.prompt_for_paths(PathPromptOptions {
|
||||
// files: true,
|
||||
// directories: true,
|
||||
// multiple: true,
|
||||
// });
|
||||
cx.on_action(Workspace::close_global);
|
||||
cx.on_action(restart);
|
||||
|
||||
// if let Some(app_state) = app_state.upgrade() {
|
||||
// cx.spawn(move |mut cx| async move {
|
||||
// if let Some(paths) = paths.recv().await.flatten() {
|
||||
// cx.update(|cx| {
|
||||
// open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
|
||||
// });
|
||||
// }
|
||||
// })
|
||||
// .detach();
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
cx.on_action({
|
||||
let app_state = Arc::downgrade(&app_state);
|
||||
move |_: &Open, cx: &mut AppContext| {
|
||||
let mut paths = cx.prompt_for_paths(PathPromptOptions {
|
||||
files: true,
|
||||
directories: true,
|
||||
multiple: true,
|
||||
});
|
||||
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
cx.spawn(move |mut cx| async move {
|
||||
if let Some(paths) = paths.await.log_err().flatten() {
|
||||
cx.update(|cx| {
|
||||
open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
type ProjectItemBuilders =
|
||||
@ -1076,7 +1080,6 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
// todo!(Non-window-actions)
|
||||
pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
|
||||
cx.windows().iter().find(|window| {
|
||||
window
|
||||
@ -1094,21 +1097,18 @@ impl Workspace {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn close(
|
||||
&mut self,
|
||||
_: &CloseWindow,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
pub fn close_window(&mut self, _: &CloseWindow, cx: &mut ViewContext<Self>) {
|
||||
let window = cx.window_handle();
|
||||
let prepare = self.prepare_to_close(false, cx);
|
||||
Some(cx.spawn(|_, mut cx| async move {
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
if prepare.await? {
|
||||
window.update(&mut cx, |_, cx| {
|
||||
cx.remove_window();
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
|
||||
pub fn prepare_to_close(
|
||||
@ -1542,7 +1542,7 @@ impl Workspace {
|
||||
|
||||
if let Some(active_panel) = dock.active_panel() {
|
||||
if was_visible {
|
||||
if active_panel.has_focus(cx) {
|
||||
if active_panel.focus_handle(cx).contains_focused(cx) {
|
||||
focus_center = true;
|
||||
}
|
||||
} else {
|
||||
@ -1589,7 +1589,9 @@ impl Workspace {
|
||||
/// Focus the panel of the given type if it isn't already focused. If it is
|
||||
/// already focused, then transfer focus back to the workspace center.
|
||||
pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.focus_or_unfocus_panel::<T>(cx, |panel, cx| !panel.has_focus(cx));
|
||||
self.focus_or_unfocus_panel::<T>(cx, |panel, cx| {
|
||||
!panel.focus_handle(cx).contains_focused(cx)
|
||||
});
|
||||
}
|
||||
|
||||
/// Focus or unfocus the given panel type, depending on the given callback.
|
||||
@ -1681,7 +1683,7 @@ impl Workspace {
|
||||
if Some(dock.position()) != dock_to_reveal {
|
||||
if let Some(panel) = dock.active_panel() {
|
||||
if panel.is_zoomed(cx) {
|
||||
focus_center |= panel.has_focus(cx);
|
||||
focus_center |= panel.focus_handle(cx).contains_focused(cx);
|
||||
dock.set_open(false, cx);
|
||||
}
|
||||
}
|
||||
@ -2075,6 +2077,7 @@ impl Workspace {
|
||||
}
|
||||
if &pane == self.active_pane() {
|
||||
self.active_item_path_changed(cx);
|
||||
self.update_active_view_for_followers(cx);
|
||||
}
|
||||
}
|
||||
pane::Event::ChangeItemTitle => {
|
||||
@ -2325,42 +2328,44 @@ impl Workspace {
|
||||
}))
|
||||
}
|
||||
|
||||
// pub fn follow_next_collaborator(
|
||||
// &mut self,
|
||||
// _: &FollowNextCollaborator,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> Option<Task<Result<()>>> {
|
||||
// let collaborators = self.project.read(cx).collaborators();
|
||||
// let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
|
||||
// let mut collaborators = collaborators.keys().copied();
|
||||
// for peer_id in collaborators.by_ref() {
|
||||
// if peer_id == leader_id {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// collaborators.next()
|
||||
// } else if let Some(last_leader_id) =
|
||||
// self.last_leaders_by_pane.get(&self.active_pane.downgrade())
|
||||
// {
|
||||
// if collaborators.contains_key(last_leader_id) {
|
||||
// Some(*last_leader_id)
|
||||
// } else {
|
||||
// None
|
||||
// pub fn follow_next_collaborator(
|
||||
// &mut self,
|
||||
// _: &FollowNextCollaborator,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) {
|
||||
// let collaborators = self.project.read(cx).collaborators();
|
||||
// let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
|
||||
// let mut collaborators = collaborators.keys().copied();
|
||||
// for peer_id in collaborators.by_ref() {
|
||||
// if peer_id == leader_id {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// collaborators.next()
|
||||
// } else if let Some(last_leader_id) =
|
||||
// self.last_leaders_by_pane.get(&self.active_pane.downgrade())
|
||||
// {
|
||||
// if collaborators.contains_key(last_leader_id) {
|
||||
// Some(*last_leader_id)
|
||||
// } else {
|
||||
// None
|
||||
// };
|
||||
|
||||
// let pane = self.active_pane.clone();
|
||||
// let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
|
||||
// else {
|
||||
// return None;
|
||||
// };
|
||||
// if Some(leader_id) == self.unfollow(&pane, cx) {
|
||||
// return None;
|
||||
// }
|
||||
// self.follow(leader_id, cx)
|
||||
// } else {
|
||||
// None
|
||||
// };
|
||||
|
||||
// let pane = self.active_pane.clone();
|
||||
// let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
|
||||
// else {
|
||||
// return;
|
||||
// };
|
||||
// if Some(leader_id) == self.unfollow(&pane, cx) {
|
||||
// return;
|
||||
// }
|
||||
// if let Some(task) = self.follow(leader_id, cx) {
|
||||
// task.detach();
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn follow(
|
||||
&mut self,
|
||||
@ -2409,6 +2414,18 @@ impl Workspace {
|
||||
self.start_following(leader_id, cx)
|
||||
}
|
||||
|
||||
// // if you're already following, find the right pane and focus it.
|
||||
// for (pane, state) in &self.follower_states {
|
||||
// if leader_id == state.leader_id {
|
||||
// cx.focus(pane);
|
||||
// return None;
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Otherwise, follow.
|
||||
// self.start_following(leader_id, cx)
|
||||
// }
|
||||
|
||||
pub fn unfollow(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Option<PeerId> {
|
||||
let state = self.follower_states.remove(pane)?;
|
||||
let leader_id = state.leader_id;
|
||||
@ -2625,8 +2642,6 @@ impl Workspace {
|
||||
update: proto::UpdateFollowers,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
dbg!("process_leader_update", &update);
|
||||
|
||||
match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
|
||||
proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
|
||||
this.update(cx, |this, _| {
|
||||
@ -2742,18 +2757,18 @@ impl Workspace {
|
||||
fn update_active_view_for_followers(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let mut is_project_item = true;
|
||||
let mut update = proto::UpdateActiveView::default();
|
||||
if self.active_pane.read(cx).has_focus(cx) {
|
||||
let item = self
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.to_followable_item_handle(cx));
|
||||
if let Some(item) = item {
|
||||
is_project_item = item.is_project_item(cx);
|
||||
update = proto::UpdateActiveView {
|
||||
id: item
|
||||
.remote_id(&self.app_state.client, cx)
|
||||
.map(|id| id.to_proto()),
|
||||
leader_id: self.leader_for_pane(&self.active_pane),
|
||||
};
|
||||
|
||||
if let Some(item) = self.active_item(cx) {
|
||||
if item.focus_handle(cx).contains_focused(cx) {
|
||||
if let Some(item) = item.to_followable_item_handle(cx) {
|
||||
is_project_item = item.is_project_item(cx);
|
||||
update = proto::UpdateActiveView {
|
||||
id: item
|
||||
.remote_id(&self.app_state.client, cx)
|
||||
.map(|id| id.to_proto()),
|
||||
leader_id: self.leader_for_pane(&self.active_pane),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3221,13 +3236,8 @@ impl Workspace {
|
||||
|
||||
fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
|
||||
self.add_workspace_actions_listeners(div, cx)
|
||||
// cx.add_async_action(Workspace::open);
|
||||
// cx.add_async_action(Workspace::follow_next_collaborator);
|
||||
// cx.add_async_action(Workspace::close);
|
||||
.on_action(cx.listener(Self::close_inactive_items_and_panes))
|
||||
.on_action(cx.listener(Self::close_all_items_and_panes))
|
||||
// cx.add_global_action(Workspace::close_global);
|
||||
// cx.add_global_action(restart);
|
||||
.on_action(cx.listener(Self::save_all))
|
||||
.on_action(cx.listener(Self::add_folder_to_project))
|
||||
.on_action(cx.listener(|workspace, _: &Unfollow, cx| {
|
||||
@ -3276,6 +3286,9 @@ impl Workspace {
|
||||
workspace.close_all_docks(cx);
|
||||
}),
|
||||
)
|
||||
.on_action(cx.listener(Workspace::open))
|
||||
.on_action(cx.listener(Workspace::close_window))
|
||||
|
||||
// cx.add_action(Workspace::activate_pane_at_index);
|
||||
// cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
|
||||
// workspace.reopen_closed_item(cx).detach();
|
||||
@ -3880,8 +3893,6 @@ impl WorkspaceStore {
|
||||
let leader_id = envelope.original_sender_id()?;
|
||||
let update = envelope.payload;
|
||||
|
||||
dbg!("handle_upate_followers");
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
for workspace in &this.workspaces {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.116.0"
|
||||
version = "0.117.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
@ -78,7 +78,7 @@ workspace = { path = "../workspace" }
|
||||
welcome = { path = "../welcome" }
|
||||
zed-actions = {path = "../zed-actions"}
|
||||
anyhow.workspace = true
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
async-compression.workspace = true
|
||||
async-tar = "0.4.2"
|
||||
async-recursion = "0.3"
|
||||
async-trait.workspace = true
|
||||
|
@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed2"
|
||||
version = "0.109.0"
|
||||
version = "2.0.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
@ -49,13 +49,13 @@ lsp = { package = "lsp2", path = "../lsp2" }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
# language_tools = { path = "../language_tools" }
|
||||
node_runtime = { path = "../node_runtime" }
|
||||
# assistant = { path = "../assistant" }
|
||||
assistant = { package = "assistant2", path = "../assistant2" }
|
||||
outline = { package = "outline2", path = "../outline2" }
|
||||
# plugin_runtime = { path = "../plugin_runtime",optional = true }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
project_panel = { package = "project_panel2", path = "../project_panel2" }
|
||||
# project_symbols = { path = "../project_symbols" }
|
||||
# quick_action_bar = { path = "../quick_action_bar" }
|
||||
quick_action_bar = { package = "quick_action_bar2", path = "../quick_action_bar2" }
|
||||
# recent_projects = { path = "../recent_projects" }
|
||||
rope = { package = "rope2", path = "../rope2"}
|
||||
rpc = { package = "rpc2", path = "../rpc2" }
|
||||
@ -68,13 +68,13 @@ terminal_view = { package = "terminal_view2", path = "../terminal_view2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
|
||||
util = { path = "../util" }
|
||||
# semantic_index = { path = "../semantic_index" }
|
||||
semantic_index = { package = "semantic_index2", path = "../semantic_index2" }
|
||||
# vim = { path = "../vim" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
welcome = { package = "welcome2", path = "../welcome2" }
|
||||
zed_actions = {package = "zed_actions2", path = "../zed_actions2"}
|
||||
anyhow.workspace = true
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
async-compression.workspace = true
|
||||
async-tar = "0.4.2"
|
||||
async-recursion = "0.3"
|
||||
async-trait.workspace = true
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user