Compare commits
74 Commits
compl-menu
...
jump-into-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2673bb667 | ||
|
|
3632b36fde | ||
|
|
97e11fd5d2 | ||
|
|
150aa03c5f | ||
|
|
ebf6115c3c | ||
|
|
394af7481d | ||
|
|
7b0d63fffb | ||
|
|
f64bfe8c1d | ||
|
|
5b86845605 | ||
|
|
9782abf3c5 | ||
|
|
6231072d85 | ||
|
|
2094d50514 | ||
|
|
1e2fa3b022 | ||
|
|
8e81070091 | ||
|
|
1071814d41 | ||
|
|
3d3d8f20eb | ||
|
|
536a958c58 | ||
|
|
96ad022cd7 | ||
|
|
11260e6d37 | ||
|
|
d54662e683 | ||
|
|
2a17274ec2 | ||
|
|
b93cee8d27 | ||
|
|
837bbc851f | ||
|
|
0fe88a88b1 | ||
|
|
2b4f0deff5 | ||
|
|
4f2ab812fb | ||
|
|
3f40d76be4 | ||
|
|
6bb21b1e5e | ||
|
|
f7a7866d4a | ||
|
|
298b9df589 | ||
|
|
56d20fc0a3 | ||
|
|
fc00eaa161 | ||
|
|
7414e91a85 | ||
|
|
b51a162d22 | ||
|
|
78dde63337 | ||
|
|
c0b40d0bd0 | ||
|
|
0acb743dac | ||
|
|
8b2afab0d3 | ||
|
|
b79117c882 | ||
|
|
77abf13f42 | ||
|
|
433cb99170 | ||
|
|
670ade9546 | ||
|
|
4a6f071fde | ||
|
|
2ecbd97fe8 | ||
|
|
a0a095c6a3 | ||
|
|
4bfc107e3a | ||
|
|
6192c83f23 | ||
|
|
e8c9283700 | ||
|
|
2469122784 | ||
|
|
6898a31f06 | ||
|
|
94bfb93d35 | ||
|
|
1b83020dc8 | ||
|
|
613deb6421 | ||
|
|
672fc76832 | ||
|
|
5d7b6141fd | ||
|
|
6aad616165 | ||
|
|
918866b7de | ||
|
|
5b2653a1d1 | ||
|
|
ba44db7f49 | ||
|
|
ce97e4ddc1 | ||
|
|
63d8a43f9d | ||
|
|
81c118d67d | ||
|
|
e1ca5ed836 | ||
|
|
fa1b1c6aff | ||
|
|
0511f9268b | ||
|
|
70f82f84c6 | ||
|
|
1c4868979d | ||
|
|
c86cf2c3e1 | ||
|
|
7425d242bc | ||
|
|
b17f2089a2 | ||
|
|
68e3d79847 | ||
|
|
ed3e647ed7 | ||
|
|
6fa5a17586 | ||
|
|
a6b717b97b |
7
.github/ISSUE_TEMPLATE/0_feature_request.yml
vendored
7
.github/ISSUE_TEMPLATE/0_feature_request.yml
vendored
@@ -18,8 +18,11 @@ body:
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below. If you are unable to run the command, please include your Zed version and release channel, operating system and version, RAM amount, and architecture.
|
||||
label: Zed Version and System Specs
|
||||
description: Zed version, release channel, architecture (x86_64 or aarch64), OS (macOS version / Linux distro and version) and RAM amount.
|
||||
placeholder: |
|
||||
<!-- In Zed run `copy system specs into clipboard` from the Zed command palette and paste here. -->
|
||||
<!-- Alternatively spawn `request feature` and this field will be autopopulated -->
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
9
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
9
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
@@ -20,8 +20,13 @@ body:
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below. If you are unable to run the command, please include your Zed version and release channel, operating system and version, RAM amount, and architecture.
|
||||
label: Zed Version and System Specs
|
||||
description: Zed version, release channel, architecture (x86_64 or aarch64), OS (macOS version / Linux distro and version) and RAM amount.
|
||||
placeholder: |
|
||||
<!-- In Zed run `copy system specs into clipboard` from the Zed command palette and paste here. -->
|
||||
<!-- Alternatively spawn `file bug report` and this field will be autopopulated -->
|
||||
<!-- If Zed won't launch, include the equivalent with other relevant details (e.g. video card driver version for display bugs, etc) -->
|
||||
<!-- Zed Version: 0.xxx.x; Channel: Stable, OS: macOS xx.xx, RAM: XXGB, Architecture: x86_64"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
54
.github/workflows/ci.yml
vendored
54
.github/workflows/ci.yml
vendored
@@ -7,14 +7,10 @@ on:
|
||||
- "v[0-9]+.[0-9]+.x"
|
||||
tags:
|
||||
- "v*"
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
paths-ignore:
|
||||
- "docs/**/*"
|
||||
- ".github/workflows/community_*"
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
# Allow only one workflow per any non-`main` branch.
|
||||
@@ -28,6 +24,23 @@ env:
|
||||
RUSTFLAGS: "-D warnings"
|
||||
|
||||
jobs:
|
||||
check_docs_only:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
docs_only: ${{ steps.check_changes.outputs.docs_only }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check for non-docs changes
|
||||
id: check_changes
|
||||
run: |
|
||||
if git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep -qvE '^docs/'; then
|
||||
echo "docs_only=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "docs_only=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
migration_checks:
|
||||
name: Check Postgres and Protobuf migrations, mergability
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
@@ -96,6 +109,7 @@ jobs:
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: check_docs_only
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -103,29 +117,35 @@ jobs:
|
||||
clean: false
|
||||
|
||||
- name: cargo clippy
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
run: ./script/clippy
|
||||
|
||||
- name: Check unused dependencies
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
uses: bnjbvr/cargo-machete@main
|
||||
|
||||
- name: Check licenses
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
run: |
|
||||
script/check-licenses
|
||||
script/generate-licenses /tmp/zed_licenses_output
|
||||
|
||||
- name: Check for new vulnerable dependencies
|
||||
if: github.event_name == 'pull_request'
|
||||
if: github.event_name == 'pull_request' && needs.check_docs_only.outputs.docs_only == 'false'
|
||||
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4
|
||||
with:
|
||||
license-check: false
|
||||
|
||||
- name: Run tests
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
- name: Build collab
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
run: cargo build -p collab
|
||||
|
||||
- name: Build other binaries and features
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
run: |
|
||||
cargo build --workspace --bins --all-features
|
||||
cargo check -p gpui --features "macos-blade"
|
||||
@@ -139,6 +159,7 @@ jobs:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- buildjet-16vcpu-ubuntu-2204
|
||||
needs: check_docs_only
|
||||
steps:
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
@@ -149,21 +170,26 @@ jobs:
|
||||
clean: false
|
||||
|
||||
- name: Cache dependencies
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-provider: "buildjet"
|
||||
|
||||
- name: Install Linux dependencies
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
run: ./script/linux
|
||||
|
||||
- name: cargo clippy
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
run: ./script/clippy
|
||||
|
||||
- name: Run tests
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
- name: Build other binaries and features
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
run: |
|
||||
cargo build -p zed
|
||||
cargo check -p workspace
|
||||
@@ -174,6 +200,7 @@ jobs:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- buildjet-16vcpu-ubuntu-2204
|
||||
needs: check_docs_only
|
||||
steps:
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
@@ -184,15 +211,18 @@ jobs:
|
||||
clean: false
|
||||
|
||||
- name: Cache dependencies
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-provider: "buildjet"
|
||||
|
||||
- name: Install Clang & Mold
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
run: ./script/remote-server && ./script/install-mold 2.34.0
|
||||
|
||||
- name: Build Remote Server
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
run: cargo build -p remote_server
|
||||
|
||||
# todo(windows): Actually run the tests
|
||||
@@ -201,6 +231,7 @@ jobs:
|
||||
name: (Windows) Run Clippy and tests
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: hosted-windows-1
|
||||
needs: check_docs_only
|
||||
steps:
|
||||
# more info here:- https://github.com/rust-lang/cargo/issues/13020
|
||||
- name: Enable longer pathnames for git
|
||||
@@ -211,16 +242,19 @@ jobs:
|
||||
clean: false
|
||||
|
||||
- name: Cache dependencies
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-provider: "github"
|
||||
|
||||
- name: cargo clippy
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
# Windows can't run shell scripts, so we need to use `cargo xtask`.
|
||||
run: cargo xtask clippy
|
||||
|
||||
- name: Build Zed
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
run: cargo build
|
||||
|
||||
bundle-mac:
|
||||
@@ -289,14 +323,14 @@ jobs:
|
||||
mv target/x86_64-apple-darwin/release/Zed.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
||||
|
||||
- name: Upload app bundle (aarch64) to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.dmg
|
||||
path: target/aarch64-apple-darwin/release/Zed-aarch64.dmg
|
||||
|
||||
- name: Upload app bundle (x86_64) to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.dmg
|
||||
@@ -345,7 +379,7 @@ jobs:
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Linux bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
|
||||
@@ -391,7 +425,7 @@ jobs:
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Linux bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
fi
|
||||
echo "::set-output name=URL::$URL"
|
||||
- name: Get content
|
||||
uses: 2428392/gh-truncate-string-action@e6b5885fb83c81ca9a700a91b079baec2133be3e # v1.4.0
|
||||
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757 # v1.4.1
|
||||
id: get-content
|
||||
with:
|
||||
stringToTruncate: |
|
||||
|
||||
1
.github/workflows/docs.yml
vendored
1
.github/workflows/docs.yml
vendored
@@ -7,6 +7,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
check_formatting:
|
||||
|
||||
208
Cargo.lock
generated
208
Cargo.lock
generated
@@ -1,6 +1,6 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "activity_indicator"
|
||||
@@ -934,7 +934,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"futures-util",
|
||||
"http-types",
|
||||
"hyper 0.14.31",
|
||||
"hyper 0.14.32",
|
||||
"hyper-rustls 0.24.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1008,9 +1008,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-tungstenite"
|
||||
version = "0.28.1"
|
||||
version = "0.28.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6cd055774e8b7f2ff9e5f9646efb298f180c3b886cdf20ef27f5a0bfbe2677b"
|
||||
checksum = "1c348fb0b6d132c596eca3dcd941df48fb597aafcb07a738ec41c004b087dc99"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-tls",
|
||||
@@ -1166,9 +1166,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-config"
|
||||
version = "1.5.10"
|
||||
version = "1.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b49afaa341e8dd8577e1a2200468f98956d6eda50bcf4a53246cc00174ba924"
|
||||
checksum = "a5d1c2c88936a73c699225d0bc00684a534166b0cebc2659c3cdf08de8edc64c"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -1177,7 +1177,7 @@ dependencies = [
|
||||
"aws-sdk-sts",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-json 0.60.7",
|
||||
"aws-smithy-json",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
@@ -1208,9 +1208,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-runtime"
|
||||
version = "1.4.4"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5ac934720fbb46206292d2c75b57e67acfc56fe7dfd34fb9a02334af08409ea"
|
||||
checksum = "300a12520b4e6d08b73f77680f12c16e8ae43250d55100e0b2be46d78da16a48"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-sigv4",
|
||||
@@ -1234,15 +1234,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-kinesis"
|
||||
version = "1.52.0"
|
||||
version = "1.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e5d4932ecd8754ec808b57c13b5ab4965d2b568ae1c1984d1823a4e2aa3e7bc"
|
||||
checksum = "cb367ea65d5a59b230d7e670ba59d68d1e51fc53802bf0219effafed21dca23f"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-json 0.61.1",
|
||||
"aws-smithy-json",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
@@ -1256,9 +1256,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-s3"
|
||||
version = "1.65.0"
|
||||
version = "1.66.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3ba2c5c0f2618937ce3d4a5ad574b86775576fa24006bcb3128c6e2cbf3c34e"
|
||||
checksum = "154488d16ab0d627d15ab2832b57e68a16684c8c902f14cb8a75ec933fc94852"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -1267,7 +1267,7 @@ dependencies = [
|
||||
"aws-smithy-checksums",
|
||||
"aws-smithy-eventstream",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-json 0.61.1",
|
||||
"aws-smithy-json",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
@@ -1290,15 +1290,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sso"
|
||||
version = "1.50.0"
|
||||
version = "1.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05ca43a4ef210894f93096039ef1d6fa4ad3edfabb3be92b80908b9f2e4b4eab"
|
||||
checksum = "74995133da38f109a0eb8e8c886f9e80c713b6e9f2e6e5a6a1ba4450ce2ffc46"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-json 0.61.1",
|
||||
"aws-smithy-json",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
@@ -1312,15 +1312,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-ssooidc"
|
||||
version = "1.51.0"
|
||||
version = "1.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abaf490c2e48eed0bb8e2da2fb08405647bd7f253996e0f93b981958ea0f73b0"
|
||||
checksum = "e7062a779685cbf3b2401eb36151e2c6589fd5f3569b8a6bc2d199e5aaa1d059"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-json 0.61.1",
|
||||
"aws-smithy-json",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
@@ -1334,15 +1334,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sts"
|
||||
version = "1.51.0"
|
||||
version = "1.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b68fde0d69c8bfdc1060ea7da21df3e39f6014da316783336deff0a9ec28f4bf"
|
||||
checksum = "299dae7b1dc0ee50434453fa5a229dc4b22bd3ee50409ff16becf1f7346e0193"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-json 0.61.1",
|
||||
"aws-smithy-json",
|
||||
"aws-smithy-query",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
@@ -1386,9 +1386,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-async"
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62220bc6e97f946ddd51b5f1361f78996e704677afc518a4ff66b7a72ea1378c"
|
||||
checksum = "8aa8ff1492fd9fb99ae28e8467af0dbbb7c31512b16fabf1a0f10d7bb6ef78bb"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
@@ -1448,15 +1448,6 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-json"
|
||||
version = "0.60.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4683df9469ef09468dad3473d129960119a0d3593617542b7d52086c8486f2d6"
|
||||
dependencies = [
|
||||
"aws-smithy-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-json"
|
||||
version = "0.61.1"
|
||||
@@ -1478,9 +1469,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-runtime"
|
||||
version = "1.7.4"
|
||||
version = "1.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f20685047ca9d6f17b994a07f629c813f08b5bce65523e47124879e60103d45"
|
||||
checksum = "431a10d0e07e09091284ef04453dae4069283aa108d209974d67e77ae1caa658"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http",
|
||||
@@ -1493,7 +1484,7 @@ dependencies = [
|
||||
"http-body 0.4.6",
|
||||
"http-body 1.0.1",
|
||||
"httparse",
|
||||
"hyper 0.14.31",
|
||||
"hyper 0.14.32",
|
||||
"hyper-rustls 0.24.2",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
@@ -1522,9 +1513,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-types"
|
||||
version = "1.2.9"
|
||||
version = "1.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fbd94a32b3a7d55d3806fe27d98d3ad393050439dd05eb53ece36ec5e3d3510"
|
||||
checksum = "8ecbf4d5dfb169812e2b240a4350f15ad3c6b03a54074e5712818801615f2dc5"
|
||||
dependencies = [
|
||||
"base64-simd",
|
||||
"bytes 1.9.0",
|
||||
@@ -1584,7 +1575,7 @@ dependencies = [
|
||||
"headers",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.31",
|
||||
"hyper 0.14.32",
|
||||
"itoa",
|
||||
"matchit",
|
||||
"memchr",
|
||||
@@ -1750,15 +1741,6 @@ dependencies = [
|
||||
"bit-vec 0.6.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f"
|
||||
dependencies = [
|
||||
"bit-vec 0.7.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.8.0"
|
||||
@@ -1774,12 +1756,6 @@ version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22"
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.8.0"
|
||||
@@ -1828,7 +1804,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-graphics"
|
||||
version = "0.5.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e142a3a5e678eb6a13e642ad8401b1f3aa38e969#e142a3a5e678eb6a13e642ad8401b1f3aa38e969"
|
||||
source = "git+https://github.com/kvark/blade?rev=099555282605c7c4cca9e66a8f40148298347f80#099555282605c7c4cca9e66a8f40148298347f80"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"ash-window",
|
||||
@@ -1858,7 +1834,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-macros"
|
||||
version = "0.3.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e142a3a5e678eb6a13e642ad8401b1f3aa38e969#e142a3a5e678eb6a13e642ad8401b1f3aa38e969"
|
||||
source = "git+https://github.com/kvark/blade?rev=099555282605c7c4cca9e66a8f40148298347f80#099555282605c7c4cca9e66a8f40148298347f80"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1868,7 +1844,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-util"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e142a3a5e678eb6a13e642ad8401b1f3aa38e969#e142a3a5e678eb6a13e642ad8401b1f3aa38e969"
|
||||
source = "git+https://github.com/kvark/blade?rev=099555282605c7c4cca9e66a8f40148298347f80#099555282605c7c4cca9e66a8f40148298347f80"
|
||||
dependencies = [
|
||||
"blade-graphics",
|
||||
"bytemuck",
|
||||
@@ -2115,6 +2091,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"settings",
|
||||
"telemetry",
|
||||
"util",
|
||||
]
|
||||
|
||||
@@ -2255,9 +2232,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cargo_toml"
|
||||
version = "0.20.5"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88da5a13c620b4ca0078845707ea9c3faf11edbc3ffd8497d11d686211cd1ac0"
|
||||
checksum = "5fbd1fe9db3ebf71b89060adaf7b0504c2d6a425cf061313099547e382c2e472"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"toml 0.8.19",
|
||||
@@ -2522,7 +2499,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-native-tls",
|
||||
"async-recursion 0.3.2",
|
||||
"async-tungstenite 0.28.1",
|
||||
"async-tungstenite 0.28.2",
|
||||
"chrono",
|
||||
"clock",
|
||||
"cocoa 0.26.0",
|
||||
@@ -2547,6 +2524,7 @@ dependencies = [
|
||||
"settings",
|
||||
"sha2",
|
||||
"smol",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"text",
|
||||
"thiserror 1.0.69",
|
||||
@@ -2654,7 +2632,7 @@ dependencies = [
|
||||
"assistant_tool",
|
||||
"async-stripe",
|
||||
"async-trait",
|
||||
"async-tungstenite 0.28.1",
|
||||
"async-tungstenite 0.28.2",
|
||||
"audio",
|
||||
"aws-config",
|
||||
"aws-sdk-kinesis",
|
||||
@@ -2686,7 +2664,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"hex",
|
||||
"http_client",
|
||||
"hyper 0.14.31",
|
||||
"hyper 0.14.32",
|
||||
"indoc",
|
||||
"jsonwebtoken",
|
||||
"language",
|
||||
@@ -2781,6 +2759,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smallvec",
|
||||
"story",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"time",
|
||||
"time_format",
|
||||
@@ -2841,6 +2820,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
@@ -3938,6 +3918,7 @@ dependencies = [
|
||||
"snippet",
|
||||
"sum_tree",
|
||||
"task",
|
||||
"telemetry",
|
||||
"tempfile",
|
||||
"text",
|
||||
"theme",
|
||||
@@ -4373,6 +4354,7 @@ dependencies = [
|
||||
"serde_json_lenient",
|
||||
"settings",
|
||||
"task",
|
||||
"telemetry",
|
||||
"tempfile",
|
||||
"theme",
|
||||
"theme_extension",
|
||||
@@ -4406,6 +4388,7 @@ dependencies = [
|
||||
"serde",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
@@ -4422,12 +4405,13 @@ checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||
|
||||
[[package]]
|
||||
name = "fancy-regex"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7493d4c459da9f84325ad297371a6b2b8a162800873a22e3b6b6512e61d18c05"
|
||||
checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
|
||||
dependencies = [
|
||||
"bit-set 0.5.3",
|
||||
"regex",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4799,11 +4783,13 @@ dependencies = [
|
||||
"rope",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shlex",
|
||||
"smol",
|
||||
"tempfile",
|
||||
"text",
|
||||
"time",
|
||||
"util",
|
||||
"which 6.0.3",
|
||||
"windows 0.58.0",
|
||||
]
|
||||
|
||||
@@ -5604,9 +5590,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "heed"
|
||||
version = "0.20.5"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d4f449bab7320c56003d37732a917e18798e2f1709d80263face2b4f9436ddb"
|
||||
checksum = "bd54745cfacb7b97dee45e8fdb91814b62bccddb481debb7de0f9ee6b7bf5b43"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"byteorder",
|
||||
@@ -5629,9 +5615,9 @@ checksum = "eb3130048d404c57ce5a1ac61a903696e8fcde7e8c2991e9fcfc1f27c3ef74ff"
|
||||
|
||||
[[package]]
|
||||
name = "heed-types"
|
||||
version = "0.20.1"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d3f528b053a6d700b2734eabcd0fd49cb8230647aa72958467527b0b7917114"
|
||||
checksum = "13c255bdf46e07fb840d120a36dcc81f385140d7191c76a7391672675c01a55d"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"byteorder",
|
||||
@@ -5858,9 +5844,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.31"
|
||||
version = "0.14.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85"
|
||||
checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
|
||||
dependencies = [
|
||||
"bytes 1.9.0",
|
||||
"futures-channel",
|
||||
@@ -5873,7 +5859,7 @@ dependencies = [
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.8",
|
||||
"socket2 0.4.10",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -5908,7 +5894,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http 0.2.12",
|
||||
"hyper 0.14.31",
|
||||
"hyper 0.14.32",
|
||||
"log",
|
||||
"rustls 0.21.12",
|
||||
"rustls-native-certs 0.6.3",
|
||||
@@ -5941,7 +5927,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||
dependencies = [
|
||||
"bytes 1.9.0",
|
||||
"hyper 0.14.31",
|
||||
"hyper 0.14.32",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
@@ -6619,7 +6605,7 @@ checksum = "58d9afa5bc6eeafb78f710a2efc585f69099f8b6a99dc7eb826581e3773a6e31"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"async-tungstenite 0.28.1",
|
||||
"async-tungstenite 0.28.2",
|
||||
"futures 0.3.31",
|
||||
"jupyter-protocol",
|
||||
"serde",
|
||||
@@ -6996,7 +6982,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7586,9 +7572,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "metal"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21"
|
||||
version = "0.30.0"
|
||||
source = "git+https://github.com/gfx-rs/metal-rs?rev=ef768ff9d742ae6a0f4e83ddc8031264e7d460c4#ef768ff9d742ae6a0f4e83ddc8031264e7d460c4"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"block",
|
||||
@@ -7727,12 +7712,11 @@ checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
|
||||
|
||||
[[package]]
|
||||
name = "naga"
|
||||
version = "22.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8bd5a652b6faf21496f2cfd88fc49989c8db0825d1f6746b1a71a6ede24a63ad"
|
||||
version = "23.0.0"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=1a643291c2e8854ba7e4f5445a4388202731bfa1#1a643291c2e8854ba7e4f5445a4388202731bfa1"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bit-set 0.6.0",
|
||||
"bit-set 0.8.0",
|
||||
"bitflags 2.6.0",
|
||||
"cfg_aliases 0.1.1",
|
||||
"codespan-reporting",
|
||||
@@ -9601,6 +9585,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"terminal",
|
||||
"text",
|
||||
"toml 0.8.19",
|
||||
"unindent",
|
||||
"url",
|
||||
"util",
|
||||
@@ -10368,7 +10353,7 @@ dependencies = [
|
||||
"alacritty_terminal",
|
||||
"anyhow",
|
||||
"async-dispatcher",
|
||||
"async-tungstenite 0.28.1",
|
||||
"async-tungstenite 0.28.2",
|
||||
"base64 0.22.1",
|
||||
"client",
|
||||
"collections",
|
||||
@@ -10399,6 +10384,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"telemetry",
|
||||
"terminal",
|
||||
"terminal_view",
|
||||
"theme",
|
||||
@@ -10425,7 +10411,7 @@ dependencies = [
|
||||
"h2 0.3.26",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.31",
|
||||
"hyper 0.14.32",
|
||||
"hyper-rustls 0.24.2",
|
||||
"hyper-tls",
|
||||
"ipnet",
|
||||
@@ -10670,7 +10656,7 @@ name = "rpc"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-tungstenite 0.28.1",
|
||||
"async-tungstenite 0.28.2",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"collections",
|
||||
@@ -11299,9 +11285,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.23"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
|
||||
checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -12663,12 +12649,23 @@ dependencies = [
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "telemetry"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"futures 0.3.31",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"telemetry_events",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "telemetry_events"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"semantic_version",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -12859,7 +12856,6 @@ dependencies = [
|
||||
name = "theme_selector"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"client",
|
||||
"fs",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
@@ -12867,6 +12863,7 @@ dependencies = [
|
||||
"picker",
|
||||
"serde",
|
||||
"settings",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
@@ -12937,16 +12934,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tiktoken-rs"
|
||||
version = "0.5.9"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c314e7ce51440f9e8f5a497394682a57b7c323d0f4d0a6b1b13c429056e0e234"
|
||||
checksum = "44075987ee2486402f0808505dd65692163d243a337fc54363d49afac41087f6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.21.7",
|
||||
"bstr",
|
||||
"fancy-regex 0.12.0",
|
||||
"fancy-regex 0.13.0",
|
||||
"lazy_static",
|
||||
"parking_lot",
|
||||
"regex",
|
||||
"rustc-hash 1.1.0",
|
||||
]
|
||||
|
||||
@@ -13096,6 +13094,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smallvec",
|
||||
"story",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"tree-sitter-md",
|
||||
"ui",
|
||||
@@ -13469,9 +13468,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-c"
|
||||
version = "0.23.2"
|
||||
version = "0.23.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db56fadd8c3c6bc880dffcf1177c9d1c54a71a5207716db8660189082e63b587"
|
||||
checksum = "afd2b1bf1585dc2ef6d69e87d01db8adb059006649dd5f96f31aa789ee6e9c71"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
@@ -13586,9 +13585,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-json"
|
||||
version = "0.23.0"
|
||||
version = "0.24.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86a5d6b3ea17e06e7a34aabeadd68f5866c0d0f9359155d432095f8b751865e4"
|
||||
checksum = "4d727acca406c0020cffc6cf35516764f36c8e3dc4408e5ebe2cb35a947ec471"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
@@ -14229,7 +14228,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"headers",
|
||||
"http 0.2.12",
|
||||
"hyper 0.14.31",
|
||||
"hyper 0.14.32",
|
||||
"log",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
@@ -14874,6 +14873,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"settings",
|
||||
"telemetry",
|
||||
"ui",
|
||||
"util",
|
||||
"vim_mode_setting",
|
||||
@@ -14979,7 +14979,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -15980,7 +15980,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.167.0"
|
||||
version = "0.168.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -16077,6 +16077,7 @@ dependencies = [
|
||||
"tab_switcher",
|
||||
"task",
|
||||
"tasks_ui",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"terminal_view",
|
||||
"theme",
|
||||
@@ -16113,7 +16114,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_astro"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"zed_extension_api 0.1.0",
|
||||
@@ -16142,7 +16143,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_elixir"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@@ -16449,6 +16450,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"similar",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"theme",
|
||||
"tree-sitter-go",
|
||||
|
||||
22
Cargo.toml
22
Cargo.toml
@@ -117,6 +117,7 @@ members = [
|
||||
"crates/tab_switcher",
|
||||
"crates/task",
|
||||
"crates/tasks_ui",
|
||||
"crates/telemetry",
|
||||
"crates/telemetry_events",
|
||||
"crates/terminal",
|
||||
"crates/terminal_view",
|
||||
@@ -305,6 +306,7 @@ supermaven_api = { path = "crates/supermaven_api" }
|
||||
tab_switcher = { path = "crates/tab_switcher" }
|
||||
task = { path = "crates/task" }
|
||||
tasks_ui = { path = "crates/tasks_ui" }
|
||||
telemetry = { path = "crates/telemetry" }
|
||||
telemetry_events = { path = "crates/telemetry_events" }
|
||||
terminal = { path = "crates/terminal" }
|
||||
terminal_view = { path = "crates/terminal_view" }
|
||||
@@ -353,13 +355,13 @@ async-watch = "0.3.1"
|
||||
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
||||
base64 = "0.22"
|
||||
bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a13e642ad8401b1f3aa38e969" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a13e642ad8401b1f3aa38e969" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a13e642ad8401b1f3aa38e969" }
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "099555282605c7c4cca9e66a8f40148298347f80" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "099555282605c7c4cca9e66a8f40148298347f80" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "099555282605c7c4cca9e66a8f40148298347f80" }
|
||||
blake3 = "1.5.3"
|
||||
bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
cargo_toml = "0.20"
|
||||
cargo_toml = "0.21"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
cocoa = "0.26"
|
||||
@@ -383,7 +385,7 @@ futures-lite = "1.13"
|
||||
git2 = { version = "0.19", default-features = false }
|
||||
globset = "0.4"
|
||||
handlebars = "4.3"
|
||||
heed = { version = "0.20.1", features = ["read-txn-no-tls"] }
|
||||
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
|
||||
hex = "0.4.3"
|
||||
html5ever = "0.27.0"
|
||||
hyper = "0.14"
|
||||
@@ -470,7 +472,7 @@ sys-locale = "0.3.1"
|
||||
sysinfo = "0.31.0"
|
||||
tempfile = "3.9.0"
|
||||
thiserror = "1.0.29"
|
||||
tiktoken-rs = "0.5.9"
|
||||
tiktoken-rs = "0.6.0"
|
||||
time = { version = "0.3", features = [
|
||||
"macros",
|
||||
"parsing",
|
||||
@@ -496,7 +498,7 @@ tree-sitter-heex = { git = "https://github.com/zed-industries/tree-sitter-heex",
|
||||
tree-sitter-diff = "0.1.0"
|
||||
tree-sitter-html = "0.20"
|
||||
tree-sitter-jsdoc = "0.23"
|
||||
tree-sitter-json = "0.23"
|
||||
tree-sitter-json = "0.24"
|
||||
tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", rev = "9a23c1a96c0513d8fc6520972beedd419a973539" }
|
||||
tree-sitter-python = "0.23"
|
||||
tree-sitter-regex = "0.23"
|
||||
@@ -523,6 +525,12 @@ wasmtime-wasi = "24"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.201"
|
||||
zstd = "0.11"
|
||||
# Custom metal-rs is only needed for "macos-blade" feature of GPUI
|
||||
#TODO: switch to crates once these are published:
|
||||
# - https://github.com/gfx-rs/metal-rs/pull/335
|
||||
# - https://github.com/gfx-rs/metal-rs/pull/336
|
||||
# - https://github.com/gfx-rs/metal-rs/pull/337
|
||||
metal = { git = "https://github.com/gfx-rs/metal-rs", rev = "ef768ff9d742ae6a0f4e83ddc8031264e7d460c4" }
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
git = "https://github.com/zed-industries/async-stripe"
|
||||
|
||||
5
assets/icons/zed_assistant_2.svg
Normal file
5
assets/icons/zed_assistant_2.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.27772 1.38585L4.39187 4.07909C4.34653 4.21692 4.26946 4.34219 4.16685 4.44479C4.06425 4.5474 3.93898 4.62447 3.80115 4.66981L1.10791 5.55566L3.80115 6.44151C3.93898 6.48685 4.06425 6.56392 4.16685 6.66653C4.26946 6.76913 4.34653 6.8944 4.39187 7.03223L5.27772 9.72547L6.16357 7.03223C6.20891 6.8944 6.28598 6.76913 6.38859 6.66653C6.49119 6.56392 6.61646 6.48685 6.7543 6.44151L9.44753 5.55566L6.7543 4.66981C6.61646 4.62447 6.49119 4.5474 6.38859 4.44479C6.28598 4.34219 6.20891 4.21692 6.16357 4.07909L5.27772 1.38585Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.35938 12.3555C8.35938 12.0664 8.52734 11.8086 9.00781 11.3594L10.2031 10.2344C10.6094 9.85156 10.7891 9.60156 10.7891 9.34375C10.7891 9.05469 10.5781 8.85938 10.2734 8.85938C10.0391 8.85938 9.87109 8.95312 9.66406 9.21094C9.42578 9.50781 9.25391 9.60938 8.99219 9.60938C8.61719 9.60938 8.35156 9.35938 8.35156 9.01172C8.35156 8.25 9.26953 7.57812 10.3594 7.57812C11.4961 7.57812 12.3438 8.26172 12.3438 9.17969C12.3438 9.75391 12.0391 10.3008 11.418 10.8516L10.4961 11.6719V11.7344H11.8047C12.2578 11.7344 12.5391 11.9766 12.5391 12.3711C12.5391 12.7656 12.2656 13 11.8047 13H9.08203C8.65234 13 8.35938 12.7383 8.35938 12.3555Z" fill="black"/>
|
||||
<path d="M11.0834 1.38585V3.71918M9.91675 2.55248H12.2501" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,4 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 8.9V11C5.93097 11 5.06903 11 3 11V10.4L8 5.6V5H3V7.1" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M11 5L13 8L11 11" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M7 8.9V11C5.34478 11 4.65522 11 3 11V10.4L7 5.6V5H3V7.1" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M12 5L14 8L12 11" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M10 6.5L11 8L10 9.5" stroke="black" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 268 B After Width: | Height: | Size: 334 B |
@@ -230,8 +230,8 @@
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
"insert": "vim::InsertBefore",
|
||||
// tree-sitter related commands
|
||||
"[ x": "editor::SelectLargerSyntaxNode",
|
||||
"] x": "editor::SelectSmallerSyntaxNode",
|
||||
"[ x": "vim::SelectLargerSyntaxNode",
|
||||
"] x": "vim::SelectSmallerSyntaxNode",
|
||||
"] d": "editor::GoToDiagnostic",
|
||||
"[ d": "editor::GoToPrevDiagnostic",
|
||||
"] c": "editor::GoToHunk",
|
||||
|
||||
@@ -160,6 +160,9 @@
|
||||
/// Whether to show the signature help after completion or a bracket pair inserted.
|
||||
/// If `auto_signature_help` is enabled, this setting will be treated as enabled also.
|
||||
"show_signature_help_after_edits": false,
|
||||
/// Whether to show the inline completions next to the completions provided by a language server.
|
||||
/// Only has an effect if inline completion provider supports it.
|
||||
"show_inline_completions_in_menu": true,
|
||||
// Whether to show wrap guides (vertical rulers) in the editor.
|
||||
// Setting this to true will show a guide at the 'preferred_line_length' value
|
||||
// if 'soft_wrap' is set to 'preferred_line_length', and will show any
|
||||
@@ -254,7 +257,14 @@
|
||||
// Whether to show selected symbol occurrences in the scrollbar.
|
||||
"selected_symbol": true,
|
||||
// Whether to show diagnostic indicators in the scrollbar.
|
||||
"diagnostics": true
|
||||
"diagnostics": true,
|
||||
/// Forcefully enable or disable the scrollbar for each axis
|
||||
"axes": {
|
||||
/// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
|
||||
"horizontal": true,
|
||||
/// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings.
|
||||
"vertical": true
|
||||
}
|
||||
},
|
||||
// Enable middle-click paste on Linux.
|
||||
"middle_click_paste": true,
|
||||
@@ -304,6 +314,8 @@
|
||||
"vertical_scroll_margin": 3,
|
||||
// Whether to scroll when clicking near the edge of the visible text area.
|
||||
"autoscroll_on_clicks": false,
|
||||
// The number of characters to keep on either side when scrolling with the mouse
|
||||
"horizontal_scroll_margin": 5,
|
||||
// Scroll sensitivity multiplier. This multiplier is applied
|
||||
// to both the horizontal and vertical delta values while scrolling.
|
||||
"scroll_sensitivity": 1.0,
|
||||
@@ -469,8 +481,10 @@
|
||||
"default_width": 240
|
||||
},
|
||||
"chat_panel": {
|
||||
// Whether to show the chat panel button in the status bar.
|
||||
"button": true,
|
||||
// When to show the chat panel button in the status bar.
|
||||
// Can be 'never', 'always', or 'when_in_call',
|
||||
// or a boolean (interpreted as 'never'/'always').
|
||||
"button": "when_in_call",
|
||||
// Where to the chat panel. Can be 'left' or 'right'.
|
||||
"dock": "right",
|
||||
// Default width of the chat panel.
|
||||
|
||||
@@ -22,6 +22,7 @@ use paths::contexts_dir;
|
||||
use project::Project;
|
||||
use regex::Regex;
|
||||
use rpc::AnyProtoClient;
|
||||
use std::sync::LazyLock;
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
ffi::OsStr,
|
||||
@@ -753,8 +754,8 @@ impl ContextStore {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pattern = r" - \d+.zed.json$";
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
static ASSISTANT_CONTEXT_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r" - \d+.zed.json$").unwrap());
|
||||
|
||||
let metadata = fs.metadata(&path).await?;
|
||||
if let Some((file_name, metadata)) = path
|
||||
@@ -763,11 +764,15 @@ impl ContextStore {
|
||||
.zip(metadata)
|
||||
{
|
||||
// This is used to filter out contexts saved by the new assistant.
|
||||
if !re.is_match(file_name) {
|
||||
if !ASSISTANT_CONTEXT_REGEX.is_match(file_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(title) = re.replace(file_name, "").lines().next() {
|
||||
if let Some(title) = ASSISTANT_CONTEXT_REGEX
|
||||
.replace(file_name, "")
|
||||
.lines()
|
||||
.next()
|
||||
{
|
||||
contexts.push(SavedContextMetadata {
|
||||
title: title.to_string(),
|
||||
path,
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
mod active_thread;
|
||||
mod assistant_panel;
|
||||
mod assistant_settings;
|
||||
mod buffer_codegen;
|
||||
mod context;
|
||||
mod context_picker;
|
||||
mod context_store;
|
||||
mod context_strip;
|
||||
mod inline_assistant;
|
||||
mod inline_prompt_editor;
|
||||
mod message_editor;
|
||||
mod prompts;
|
||||
mod streaming_diff;
|
||||
mod terminal_codegen;
|
||||
mod terminal_inline_assistant;
|
||||
mod thread;
|
||||
mod thread_history;
|
||||
mod thread_store;
|
||||
mod ui;
|
||||
|
||||
use std::any::TypeId;
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::Client;
|
||||
@@ -79,10 +81,6 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, stdout_is_a_pty: bool, cx: &mu
|
||||
}
|
||||
|
||||
fn feature_gate_assistant2_actions(cx: &mut AppContext) {
|
||||
const ASSISTANT1_NAMESPACE: &str = "assistant";
|
||||
|
||||
let inline_assist_actions = [TypeId::of::<zed_actions::InlineAssist>()];
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(NAMESPACE);
|
||||
});
|
||||
@@ -91,17 +89,10 @@ fn feature_gate_assistant2_actions(cx: &mut AppContext) {
|
||||
if is_enabled {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.show_namespace(NAMESPACE);
|
||||
filter.hide_namespace(ASSISTANT1_NAMESPACE);
|
||||
|
||||
// We're hiding all of the `assistant: ` actions, but we want to
|
||||
// keep the inline assist action around so we can use the same
|
||||
// one in Assistant2.
|
||||
filter.show_action_types(inline_assist_actions.iter());
|
||||
});
|
||||
} else {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(NAMESPACE);
|
||||
filter.show_namespace(ASSISTANT1_NAMESPACE);
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
@@ -27,9 +27,22 @@ use crate::{NewThread, OpenHistory, ToggleFocus};
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(
|
||||
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(cx);
|
||||
});
|
||||
workspace
|
||||
.register_action(|workspace, _: &ToggleFocus, cx| {
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(cx);
|
||||
})
|
||||
.register_action(|workspace, _: &NewThread, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| panel.new_thread(cx));
|
||||
workspace.focus_panel::<AssistantPanel>(cx);
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &OpenHistory, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(cx);
|
||||
panel.update(cx, |panel, cx| panel.open_history(cx));
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
@@ -157,6 +170,12 @@ impl AssistantPanel {
|
||||
self.message_editor.focus_handle(cx).focus(cx);
|
||||
}
|
||||
|
||||
fn open_history(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.active_view = ActiveView::History;
|
||||
self.history.focus_handle(cx).focus(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
|
||||
let Some(thread) = self
|
||||
.thread_store
|
||||
@@ -257,7 +276,7 @@ impl Panel for AssistantPanel {
|
||||
}
|
||||
|
||||
fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
|
||||
Some(IconName::ZedAssistant)
|
||||
Some(IconName::ZedAssistant2)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||
@@ -572,9 +591,7 @@ impl Render for AssistantPanel {
|
||||
this.new_thread(cx);
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &OpenHistory, cx| {
|
||||
this.active_view = ActiveView::History;
|
||||
this.history.focus_handle(cx).focus(cx);
|
||||
cx.notify();
|
||||
this.open_history(cx);
|
||||
}))
|
||||
.child(self.render_toolbar(cx))
|
||||
.map(|parent| match self.active_view {
|
||||
|
||||
1475
crates/assistant2/src/buffer_codegen.rs
Normal file
1475
crates/assistant2/src/buffer_codegen.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,7 @@ pub struct Context {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum ContextKind {
|
||||
File,
|
||||
Directory,
|
||||
FetchedUrl,
|
||||
Thread,
|
||||
}
|
||||
@@ -33,6 +34,7 @@ pub fn attach_context_to_message(
|
||||
context: impl IntoIterator<Item = Context>,
|
||||
) {
|
||||
let mut file_context = String::new();
|
||||
let mut directory_context = String::new();
|
||||
let mut fetch_context = String::new();
|
||||
let mut thread_context = String::new();
|
||||
|
||||
@@ -42,6 +44,10 @@ pub fn attach_context_to_message(
|
||||
file_context.push_str(&context.text);
|
||||
file_context.push('\n');
|
||||
}
|
||||
ContextKind::Directory => {
|
||||
directory_context.push_str(&context.text);
|
||||
directory_context.push('\n');
|
||||
}
|
||||
ContextKind::FetchedUrl => {
|
||||
fetch_context.push_str(&context.name);
|
||||
fetch_context.push('\n');
|
||||
@@ -63,6 +69,11 @@ pub fn attach_context_to_message(
|
||||
context_text.push_str(&file_context);
|
||||
}
|
||||
|
||||
if !directory_context.is_empty() {
|
||||
context_text.push_str("The following directories are available:\n");
|
||||
context_text.push_str(&directory_context);
|
||||
}
|
||||
|
||||
if !fetch_context.is_empty() {
|
||||
context_text.push_str("The following fetched results are available\n");
|
||||
context_text.push_str(&fetch_context);
|
||||
@@ -73,5 +84,7 @@ pub fn attach_context_to_message(
|
||||
context_text.push_str(&thread_context);
|
||||
}
|
||||
|
||||
message.content.push(MessageContent::Text(context_text));
|
||||
if !context_text.is_empty() {
|
||||
message.content.push(MessageContent::Text(context_text));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod directory_context_picker;
|
||||
mod fetch_context_picker;
|
||||
mod file_context_picker;
|
||||
mod thread_context_picker;
|
||||
@@ -13,16 +14,25 @@ use ui::{prelude::*, ListItem, ListItemSpacing};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::ContextKind;
|
||||
use crate::context_picker::directory_context_picker::DirectoryContextPicker;
|
||||
use crate::context_picker::fetch_context_picker::FetchContextPicker;
|
||||
use crate::context_picker::file_context_picker::FileContextPicker;
|
||||
use crate::context_picker::thread_context_picker::ThreadContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::thread_store::ThreadStore;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ConfirmBehavior {
|
||||
KeepOpen,
|
||||
Close,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum ContextPickerMode {
|
||||
Default,
|
||||
File(View<FileContextPicker>),
|
||||
Directory(View<DirectoryContextPicker>),
|
||||
Fetch(View<FetchContextPicker>),
|
||||
Thread(View<ThreadContextPicker>),
|
||||
}
|
||||
@@ -37,19 +47,23 @@ impl ContextPicker {
|
||||
workspace: WeakView<Workspace>,
|
||||
thread_store: Option<WeakModel<ThreadStore>>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let mut entries = vec![
|
||||
ContextPickerEntry {
|
||||
name: "Directory".into(),
|
||||
icon: IconName::Folder,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "File".into(),
|
||||
kind: ContextKind::File,
|
||||
icon: IconName::File,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "Folder".into(),
|
||||
kind: ContextKind::Directory,
|
||||
icon: IconName::Folder,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "Fetch".into(),
|
||||
kind: ContextKind::FetchedUrl,
|
||||
icon: IconName::Globe,
|
||||
},
|
||||
];
|
||||
@@ -57,6 +71,7 @@ impl ContextPicker {
|
||||
if thread_store.is_some() {
|
||||
entries.push(ContextPickerEntry {
|
||||
name: "Thread".into(),
|
||||
kind: ContextKind::Thread,
|
||||
icon: IconName::MessageCircle,
|
||||
});
|
||||
}
|
||||
@@ -66,6 +81,7 @@ impl ContextPicker {
|
||||
workspace,
|
||||
thread_store,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
entries,
|
||||
selected_ix: 0,
|
||||
};
|
||||
@@ -92,6 +108,7 @@ impl FocusableView for ContextPicker {
|
||||
match &self.mode {
|
||||
ContextPickerMode::Default => self.picker.focus_handle(cx),
|
||||
ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx),
|
||||
ContextPickerMode::Directory(directory_picker) => directory_picker.focus_handle(cx),
|
||||
ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
|
||||
ContextPickerMode::Thread(thread_picker) => thread_picker.focus_handle(cx),
|
||||
}
|
||||
@@ -106,6 +123,9 @@ impl Render for ContextPicker {
|
||||
.map(|parent| match &self.mode {
|
||||
ContextPickerMode::Default => parent.child(self.picker.clone()),
|
||||
ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
|
||||
ContextPickerMode::Directory(directory_picker) => {
|
||||
parent.child(directory_picker.clone())
|
||||
}
|
||||
ContextPickerMode::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
|
||||
ContextPickerMode::Thread(thread_picker) => parent.child(thread_picker.clone()),
|
||||
})
|
||||
@@ -115,6 +135,7 @@ impl Render for ContextPicker {
|
||||
#[derive(Clone)]
|
||||
struct ContextPickerEntry {
|
||||
name: SharedString,
|
||||
kind: ContextKind,
|
||||
icon: IconName,
|
||||
}
|
||||
|
||||
@@ -123,6 +144,7 @@ pub(crate) struct ContextPickerDelegate {
|
||||
workspace: WeakView<Workspace>,
|
||||
thread_store: Option<WeakModel<ThreadStore>>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
entries: Vec<ContextPickerEntry>,
|
||||
selected_ix: usize,
|
||||
}
|
||||
@@ -155,40 +177,53 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||
if let Some(entry) = self.entries.get(self.selected_ix) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| {
|
||||
match entry.name.to_string().as_str() {
|
||||
"File" => {
|
||||
match entry.kind {
|
||||
ContextKind::File => {
|
||||
this.mode = ContextPickerMode::File(cx.new_view(|cx| {
|
||||
FileContextPicker::new(
|
||||
self.context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
self.confirm_behavior,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
"Fetch" => {
|
||||
ContextKind::Directory => {
|
||||
this.mode = ContextPickerMode::Directory(cx.new_view(|cx| {
|
||||
DirectoryContextPicker::new(
|
||||
self.context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
self.confirm_behavior,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
ContextKind::FetchedUrl => {
|
||||
this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
|
||||
FetchContextPicker::new(
|
||||
self.context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
self.confirm_behavior,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
"Thread" => {
|
||||
ContextKind::Thread => {
|
||||
if let Some(thread_store) = self.thread_store.as_ref() {
|
||||
this.mode = ContextPickerMode::Thread(cx.new_view(|cx| {
|
||||
ThreadContextPicker::new(
|
||||
thread_store.clone(),
|
||||
self.context_picker.clone(),
|
||||
self.context_store.clone(),
|
||||
self.confirm_behavior,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cx.focus_self();
|
||||
@@ -202,6 +237,7 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||
.update(cx, |this, cx| match this.mode {
|
||||
ContextPickerMode::Default => cx.emit(DismissEvent),
|
||||
ContextPickerMode::File(_)
|
||||
| ContextPickerMode::Directory(_)
|
||||
| ContextPickerMode::Fetch(_)
|
||||
| ContextPickerMode::Thread(_) => {}
|
||||
})
|
||||
|
||||
129
crates/assistant2/src/context_picker/directory_context_picker.rs
Normal file
129
crates/assistant2/src/context_picker/directory_context_picker.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
// TODO: Remove this once we've implemented the functionality.
|
||||
#![allow(unused)]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, WorktreeId};
|
||||
use ui::{prelude::*, ListItem};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_store::ContextStore;
|
||||
|
||||
pub struct DirectoryContextPicker {
|
||||
picker: View<Picker<DirectoryContextPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl DirectoryContextPicker {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let delegate = DirectoryContextPickerDelegate::new(
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
);
|
||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
||||
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for DirectoryContextPicker {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DirectoryContextPicker {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
self.picker.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DirectoryContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
matches: Vec<PathMatch>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl DirectoryContextPickerDelegate {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
) -> Self {
|
||||
Self {
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for DirectoryContextPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
"Search folders…".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, _query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
// TODO: Implement this once we fix the issues with the file context picker.
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
// TODO: Implement this once we fix the issues with the file context picker.
|
||||
match self.confirm_behavior {
|
||||
ConfirmBehavior::KeepOpen => {}
|
||||
ConfirmBehavior::Close => self.dismissed(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| {
|
||||
this.reset_mode();
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
_ix: usize,
|
||||
_selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ use ui::{prelude::*, ListItem, ViewContext};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::ContextKind;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_store::ContextStore;
|
||||
|
||||
pub struct FetchContextPicker {
|
||||
@@ -24,9 +24,15 @@ impl FetchContextPicker {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let delegate = FetchContextPickerDelegate::new(context_picker, workspace, context_store);
|
||||
let delegate = FetchContextPickerDelegate::new(
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
);
|
||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
||||
|
||||
Self { picker }
|
||||
@@ -56,6 +62,7 @@ pub struct FetchContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
url: String,
|
||||
}
|
||||
|
||||
@@ -64,11 +71,13 @@ impl FetchContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
) -> Self {
|
||||
FetchContextPickerDelegate {
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
url: String::new(),
|
||||
}
|
||||
}
|
||||
@@ -184,6 +193,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
|
||||
let http_client = workspace.read(cx).client().http_client().clone();
|
||||
let url = self.url.clone();
|
||||
let confirm_behavior = self.confirm_behavior;
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let text = Self::build_message(http_client, &url).await?;
|
||||
|
||||
@@ -192,7 +202,14 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
.context_store
|
||||
.update(cx, |context_store, _cx| {
|
||||
context_store.insert_context(ContextKind::FetchedUrl, url, text);
|
||||
})
|
||||
})?;
|
||||
|
||||
match confirm_behavior {
|
||||
ConfirmBehavior::KeepOpen => {}
|
||||
ConfirmBehavior::Close => this.delegate.dismissed(cx),
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
|
||||
anyhow::Ok(())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::fmt::Write as _;
|
||||
use std::ops::RangeInclusive;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -13,7 +13,7 @@ use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::ContextKind;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_store::ContextStore;
|
||||
|
||||
pub struct FileContextPicker {
|
||||
@@ -25,9 +25,15 @@ impl FileContextPicker {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let delegate = FileContextPickerDelegate::new(context_picker, workspace, context_store);
|
||||
let delegate = FileContextPickerDelegate::new(
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
);
|
||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
||||
|
||||
Self { picker }
|
||||
@@ -50,6 +56,7 @@ pub struct FileContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
matches: Vec<PathMatch>,
|
||||
selected_index: usize,
|
||||
}
|
||||
@@ -59,11 +66,13 @@ impl FileContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
) -> Self {
|
||||
Self {
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
@@ -79,44 +88,37 @@ impl FileContextPickerDelegate {
|
||||
if query.is_empty() {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().read(cx);
|
||||
let entries = workspace.recent_navigation_history(Some(10), cx);
|
||||
|
||||
let entries = entries
|
||||
let recent_matches = workspace
|
||||
.recent_navigation_history(Some(10), cx)
|
||||
.into_iter()
|
||||
.map(|entries| entries.0)
|
||||
.chain(project.worktrees(cx).flat_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let id = worktree.id();
|
||||
worktree
|
||||
.child_entries(Path::new(""))
|
||||
.filter(|entry| entry.kind.is_file())
|
||||
.map(move |entry| project::ProjectPath {
|
||||
worktree_id: id,
|
||||
path: entry.path.clone(),
|
||||
})
|
||||
}))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let path_prefix: Arc<str> = Arc::default();
|
||||
Task::ready(
|
||||
entries
|
||||
.into_iter()
|
||||
.filter_map(|entry| {
|
||||
let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
|
||||
let mut full_path = PathBuf::from(worktree.read(cx).root_name());
|
||||
full_path.push(&entry.path);
|
||||
Some(PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: entry.worktree_id.to_usize(),
|
||||
path: full_path.into(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
})
|
||||
.filter_map(|(project_path, _)| {
|
||||
let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
|
||||
Some(PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: project_path.worktree_id.to_usize(),
|
||||
path: project_path.path,
|
||||
path_prefix: worktree.read(cx).root_name().into(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
});
|
||||
|
||||
let file_matches = project.worktrees(cx).flat_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let path_prefix: Arc<str> = worktree.root_name().into();
|
||||
worktree.files(true, 0).map(move |entry| PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: worktree.id().to_usize(),
|
||||
path: entry.path.clone(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
})
|
||||
});
|
||||
|
||||
Task::ready(recent_matches.chain(file_matches).collect())
|
||||
} else {
|
||||
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
|
||||
let candidate_sets = worktrees
|
||||
@@ -201,6 +203,7 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
};
|
||||
let path = mat.path.clone();
|
||||
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
|
||||
let confirm_behavior = self.confirm_behavior;
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let Some(open_buffer_task) = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
@@ -214,22 +217,31 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
let buffer = open_buffer_task.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate.context_store.update(cx, |context_store, cx| {
|
||||
let mut text = String::new();
|
||||
text.push_str(&codeblock_fence_for_path(Some(&path), None));
|
||||
text.push_str(&buffer.read(cx).text());
|
||||
if !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
this.delegate
|
||||
.context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
let mut text = String::new();
|
||||
text.push_str(&codeblock_fence_for_path(Some(&path), None));
|
||||
text.push_str(&buffer.read(cx).text());
|
||||
if !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
text.push_str("```\n");
|
||||
text.push_str("```\n");
|
||||
|
||||
context_store.insert_context(
|
||||
ContextKind::File,
|
||||
path.to_string_lossy().to_string(),
|
||||
text,
|
||||
);
|
||||
})
|
||||
context_store.insert_context(
|
||||
ContextKind::File,
|
||||
path.to_string_lossy().to_string(),
|
||||
text,
|
||||
);
|
||||
})?;
|
||||
|
||||
match confirm_behavior {
|
||||
ConfirmBehavior::KeepOpen => {}
|
||||
ConfirmBehavior::Close => this.delegate.dismissed(cx),
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
|
||||
anyhow::Ok(())
|
||||
@@ -253,16 +265,30 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let path_match = &self.matches[ix];
|
||||
let file_name = path_match
|
||||
.path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let directory = path_match
|
||||
.path
|
||||
.parent()
|
||||
.map(|directory| format!("{}/", directory.to_string_lossy()));
|
||||
|
||||
let (file_name, directory) = if path_match.path.as_ref() == Path::new("") {
|
||||
(SharedString::from(path_match.path_prefix.clone()), None)
|
||||
} else {
|
||||
let file_name = path_match
|
||||
.path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.into();
|
||||
|
||||
let mut directory = format!("{}/", path_match.path_prefix);
|
||||
if let Some(parent) = path_match
|
||||
.path
|
||||
.parent()
|
||||
.filter(|parent| parent != &Path::new(""))
|
||||
{
|
||||
directory.push_str(&parent.to_string_lossy());
|
||||
directory.push('/');
|
||||
}
|
||||
|
||||
(file_name, Some(directory))
|
||||
};
|
||||
|
||||
Some(
|
||||
ListItem::new(ix).inset(true).toggle_state(selected).child(
|
||||
|
||||
@@ -6,7 +6,7 @@ use picker::{Picker, PickerDelegate};
|
||||
use ui::{prelude::*, ListItem};
|
||||
|
||||
use crate::context::ContextKind;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_store;
|
||||
use crate::thread::ThreadId;
|
||||
use crate::thread_store::ThreadStore;
|
||||
@@ -20,10 +20,15 @@ impl ThreadContextPicker {
|
||||
thread_store: WeakModel<ThreadStore>,
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
context_store: WeakModel<context_store::ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let delegate =
|
||||
ThreadContextPickerDelegate::new(thread_store, context_picker, context_store);
|
||||
let delegate = ThreadContextPickerDelegate::new(
|
||||
thread_store,
|
||||
context_picker,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
);
|
||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
||||
|
||||
ThreadContextPicker { picker }
|
||||
@@ -52,6 +57,7 @@ pub struct ThreadContextPickerDelegate {
|
||||
thread_store: WeakModel<ThreadStore>,
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
context_store: WeakModel<context_store::ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
matches: Vec<ThreadContextEntry>,
|
||||
selected_index: usize,
|
||||
}
|
||||
@@ -61,11 +67,13 @@ impl ThreadContextPickerDelegate {
|
||||
thread_store: WeakModel<ThreadStore>,
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
context_store: WeakModel<context_store::ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
) -> Self {
|
||||
ThreadContextPickerDelegate {
|
||||
thread_store,
|
||||
context_picker,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
@@ -180,6 +188,11 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
context_store.insert_context(ContextKind::Thread, entry.summary.clone(), text);
|
||||
})
|
||||
.ok();
|
||||
|
||||
match self.confirm_behavior {
|
||||
ConfirmBehavior::KeepOpen => {}
|
||||
ConfirmBehavior::Close => self.dismissed(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
|
||||
@@ -4,7 +4,7 @@ use gpui::{FocusHandle, Model, View, WeakModel, WeakView};
|
||||
use ui::{prelude::*, PopoverMenu, PopoverMenuHandle, Tooltip};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::ui::ContextPill;
|
||||
@@ -33,6 +33,7 @@ impl ContextStrip {
|
||||
workspace.clone(),
|
||||
thread_store.clone(),
|
||||
context_store.downgrade(),
|
||||
ConfirmBehavior::KeepOpen,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1127
crates/assistant2/src/inline_prompt_editor.rs
Normal file
1127
crates/assistant2/src/inline_prompt_editor.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,24 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakModel, WeakView};
|
||||
use gpui::{
|
||||
AppContext, DismissEvent, FocusableView, Model, Subscription, TextStyle, View, WeakModel,
|
||||
WeakView,
|
||||
};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use rope::Point;
|
||||
use settings::{update_settings_file, Settings};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding, PopoverMenuHandle,
|
||||
Tooltip,
|
||||
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding, PopoverMenu,
|
||||
PopoverMenuHandle, Tooltip,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::assistant_settings::AssistantSettings;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::context_strip::ContextStrip;
|
||||
use crate::thread::{RequestKind, Thread};
|
||||
@@ -27,9 +31,12 @@ pub struct MessageEditor {
|
||||
context_store: Model<ContextStore>,
|
||||
context_strip: View<ContextStrip>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
inline_context_picker: View<ContextPicker>,
|
||||
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
language_model_selector: View<LanguageModelSelector>,
|
||||
language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
use_tools: bool,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl MessageEditor {
|
||||
@@ -42,14 +49,31 @@ impl MessageEditor {
|
||||
) -> Self {
|
||||
let context_store = cx.new_model(|_cx| ContextStore::new());
|
||||
let context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
|
||||
let editor = cx.new_view(|cx| {
|
||||
let mut editor = Editor::auto_height(80, cx);
|
||||
let mut editor = Editor::auto_height(10, cx);
|
||||
editor.set_placeholder_text("Ask anything, @ to add context", cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
|
||||
editor
|
||||
});
|
||||
let inline_context_picker = cx.new_view(|cx| {
|
||||
ContextPicker::new(
|
||||
workspace.clone(),
|
||||
Some(thread_store.clone()),
|
||||
context_store.downgrade(),
|
||||
ConfirmBehavior::Close,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let subscriptions = vec![
|
||||
cx.subscribe(&editor, Self::handle_editor_event),
|
||||
cx.subscribe(
|
||||
&inline_context_picker,
|
||||
Self::handle_inline_context_picker_event,
|
||||
),
|
||||
];
|
||||
|
||||
Self {
|
||||
thread,
|
||||
@@ -66,6 +90,8 @@ impl MessageEditor {
|
||||
)
|
||||
}),
|
||||
context_picker_menu_handle,
|
||||
inline_context_picker,
|
||||
inline_context_picker_menu_handle,
|
||||
language_model_selector: cx.new_view(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
@@ -81,6 +107,7 @@ impl MessageEditor {
|
||||
}),
|
||||
language_model_selector_menu_handle: PopoverMenuHandle::default(),
|
||||
use_tools: false,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +170,40 @@ impl MessageEditor {
|
||||
None
|
||||
}
|
||||
|
||||
fn handle_editor_event(
|
||||
&mut self,
|
||||
editor: View<Editor>,
|
||||
event: &EditorEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
EditorEvent::SelectionsChanged { .. } => {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let newest_cursor = editor.selections.newest::<Point>(cx).head();
|
||||
if newest_cursor.column > 0 {
|
||||
let behind_cursor = Point::new(newest_cursor.row, newest_cursor.column - 1);
|
||||
let char_behind_cursor = snapshot.chars_at(behind_cursor).next();
|
||||
if char_behind_cursor == Some('@') {
|
||||
self.inline_context_picker_menu_handle.show(cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_inline_context_picker_event(
|
||||
&mut self,
|
||||
_inline_context_picker: View<ContextPicker>,
|
||||
_event: &DismissEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let editor_focus_handle = self.editor.focus_handle(cx);
|
||||
cx.focus(&editor_focus_handle);
|
||||
}
|
||||
|
||||
fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
let focus_handle = self.language_model_selector.focus_handle(cx).clone();
|
||||
@@ -199,6 +260,7 @@ impl Render for MessageEditor {
|
||||
let font_size = TextSize::Default.rems(cx);
|
||||
let line_height = font_size.to_pixels(cx.rem_size()) * 1.5;
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
let inline_context_picker = self.inline_context_picker.clone();
|
||||
let bg_color = cx.theme().colors().editor_background;
|
||||
|
||||
v_flex()
|
||||
@@ -211,7 +273,7 @@ impl Render for MessageEditor {
|
||||
.p_2()
|
||||
.bg(bg_color)
|
||||
.child(self.context_strip.clone())
|
||||
.child(div().id("thread_editor").overflow_y_scroll().h_12().child({
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
@@ -232,7 +294,18 @@ impl Render for MessageEditor {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
PopoverMenu::new("inline-context-picker")
|
||||
.menu(move |_cx| Some(inline_context_picker.clone()))
|
||||
.attach(gpui::Corner::TopLeft)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-16.0),
|
||||
})
|
||||
.with_handle(self.inline_context_picker_menu_handle.clone()),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
|
||||
192
crates/assistant2/src/terminal_codegen.rs
Normal file
192
crates/assistant2/src/terminal_codegen.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
use crate::inline_prompt_editor::CodegenStatus;
|
||||
use client::telemetry::Telemetry;
|
||||
use futures::{channel::mpsc, SinkExt, StreamExt};
|
||||
use gpui::{AppContext, EventEmitter, Model, ModelContext, Task};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelRequest};
|
||||
use language_models::report_assistant_event;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use terminal::Terminal;
|
||||
|
||||
pub struct TerminalCodegen {
|
||||
pub status: CodegenStatus,
|
||||
pub telemetry: Option<Arc<Telemetry>>,
|
||||
terminal: Model<Terminal>,
|
||||
generation: Task<()>,
|
||||
pub message_id: Option<String>,
|
||||
transaction: Option<TerminalTransaction>,
|
||||
}
|
||||
|
||||
impl EventEmitter<CodegenEvent> for TerminalCodegen {}
|
||||
|
||||
impl TerminalCodegen {
|
||||
pub fn new(terminal: Model<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
|
||||
Self {
|
||||
terminal,
|
||||
telemetry,
|
||||
status: CodegenStatus::Idle,
|
||||
generation: Task::ready(()),
|
||||
message_id: None,
|
||||
transaction: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let model_api_key = model.api_key(cx);
|
||||
let http_client = cx.http_client();
|
||||
let telemetry = self.telemetry.clone();
|
||||
self.status = CodegenStatus::Pending;
|
||||
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
|
||||
self.generation = cx.spawn(|this, mut cx| async move {
|
||||
let model_telemetry_id = model.telemetry_id();
|
||||
let model_provider_id = model.provider_id();
|
||||
let response = model.stream_completion_text(prompt, &cx).await;
|
||||
let generate = async {
|
||||
let message_id = response
|
||||
.as_ref()
|
||||
.ok()
|
||||
.and_then(|response| response.message_id.clone());
|
||||
|
||||
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
|
||||
|
||||
let task = cx.background_executor().spawn({
|
||||
let message_id = message_id.clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
async move {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let task = async {
|
||||
let mut chunks = response?.stream;
|
||||
while let Some(chunk) = chunks.next().await {
|
||||
if response_latency.is_none() {
|
||||
response_latency = Some(request_start.elapsed());
|
||||
}
|
||||
let chunk = chunk?;
|
||||
hunks_tx.send(chunk).await?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let result = task.await;
|
||||
|
||||
let error_message = result.as_ref().err().map(|error| error.to_string());
|
||||
report_assistant_event(
|
||||
AssistantEvent {
|
||||
conversation_id: None,
|
||||
kind: AssistantKind::InlineTerminal,
|
||||
message_id,
|
||||
phase: AssistantPhase::Response,
|
||||
model: model_telemetry_id,
|
||||
model_provider: model_provider_id.to_string(),
|
||||
response_latency,
|
||||
error_message,
|
||||
language_name: None,
|
||||
},
|
||||
telemetry,
|
||||
http_client,
|
||||
model_api_key,
|
||||
&executor,
|
||||
);
|
||||
|
||||
result?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
});
|
||||
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.message_id = message_id;
|
||||
})?;
|
||||
|
||||
while let Some(hunk) = hunks_rx.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(transaction) = &mut this.transaction {
|
||||
transaction.push(hunk, cx);
|
||||
cx.notify();
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
task.await?;
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let result = generate.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Err(error) = result {
|
||||
this.status = CodegenStatus::Error(error);
|
||||
} else {
|
||||
this.status = CodegenStatus::Done;
|
||||
}
|
||||
cx.emit(CodegenEvent::Finished);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn stop(&mut self, cx: &mut ModelContext<Self>) {
|
||||
self.status = CodegenStatus::Done;
|
||||
self.generation = Task::ready(());
|
||||
cx.emit(CodegenEvent::Finished);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn complete(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if let Some(transaction) = self.transaction.take() {
|
||||
transaction.complete(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if let Some(transaction) = self.transaction.take() {
|
||||
transaction.undo(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum CodegenEvent {
|
||||
Finished,
|
||||
}
|
||||
|
||||
pub const CLEAR_INPUT: &str = "\x15";
|
||||
const CARRIAGE_RETURN: &str = "\x0d";
|
||||
|
||||
struct TerminalTransaction {
|
||||
terminal: Model<Terminal>,
|
||||
}
|
||||
|
||||
impl TerminalTransaction {
|
||||
pub fn start(terminal: Model<Terminal>) -> Self {
|
||||
Self { terminal }
|
||||
}
|
||||
|
||||
pub fn push(&mut self, hunk: String, cx: &mut AppContext) {
|
||||
// Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
|
||||
let input = Self::sanitize_input(hunk);
|
||||
self.terminal
|
||||
.update(cx, |terminal, _| terminal.input(input));
|
||||
}
|
||||
|
||||
pub fn undo(&self, cx: &mut AppContext) {
|
||||
self.terminal
|
||||
.update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
|
||||
}
|
||||
|
||||
pub fn complete(&self, cx: &mut AppContext) {
|
||||
self.terminal.update(cx, |terminal, _| {
|
||||
terminal.input(CARRIAGE_RETURN.to_string())
|
||||
});
|
||||
}
|
||||
|
||||
fn sanitize_input(input: String) -> String {
|
||||
input.replace(['\r', '\n'], "")
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,29 @@
|
||||
use crate::assistant_settings::AssistantSettings;
|
||||
use crate::context::attach_context_to_message;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::context_strip::ContextStrip;
|
||||
use crate::inline_prompt_editor::{
|
||||
CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
|
||||
};
|
||||
use crate::prompts::PromptBuilder;
|
||||
use crate::terminal_codegen::{CodegenEvent, TerminalCodegen, CLEAR_INPUT};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::ToggleContextPicker;
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::{HashMap, VecDeque};
|
||||
use editor::{
|
||||
actions::{MoveDown, MoveUp, SelectAll},
|
||||
Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
|
||||
};
|
||||
use editor::{actions::SelectAll, MultiBuffer};
|
||||
use fs::Fs;
|
||||
use futures::{channel::mpsc, SinkExt, StreamExt};
|
||||
use gpui::{
|
||||
AppContext, Context, EventEmitter, FocusHandle, FocusableView, Global, Model, ModelContext,
|
||||
Subscription, Task, TextStyle, UpdateGlobal, View, WeakModel, WeakView,
|
||||
AppContext, Context, FocusableView, Global, Model, Subscription, UpdateGlobal, View, WeakModel,
|
||||
WeakView,
|
||||
};
|
||||
use language::Buffer;
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use language_models::report_assistant_event;
|
||||
use settings::{update_settings_file, Settings};
|
||||
use std::{cmp, sync::Arc, time::Instant};
|
||||
use std::sync::Arc;
|
||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use terminal::Terminal;
|
||||
use terminal_view::TerminalView;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, text_for_action, IconButtonShape, PopoverMenuHandle, Tooltip};
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
use workspace::{notifications::NotificationId, Toast, Workspace};
|
||||
|
||||
@@ -47,17 +39,6 @@ pub fn init(
|
||||
const DEFAULT_CONTEXT_LINES: usize = 50;
|
||||
const PROMPT_HISTORY_MAX_LEN: usize = 20;
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
|
||||
struct TerminalInlineAssistId(usize);
|
||||
|
||||
impl TerminalInlineAssistId {
|
||||
fn post_inc(&mut self) -> TerminalInlineAssistId {
|
||||
let id = *self;
|
||||
self.0 += 1;
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TerminalInlineAssistant {
|
||||
next_assist_id: TerminalInlineAssistId,
|
||||
assists: HashMap<TerminalInlineAssistId, TerminalInlineAssist>,
|
||||
@@ -98,10 +79,10 @@ impl TerminalInlineAssistant {
|
||||
MultiBuffer::singleton(cx.new_model(|cx| Buffer::local(String::new(), cx)), cx)
|
||||
});
|
||||
let context_store = cx.new_model(|_cx| ContextStore::new());
|
||||
let codegen = cx.new_model(|_| Codegen::new(terminal, self.telemetry.clone()));
|
||||
let codegen = cx.new_model(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
|
||||
|
||||
let prompt_editor = cx.new_view(|cx| {
|
||||
PromptEditor::new(
|
||||
PromptEditor::new_terminal(
|
||||
assist_id,
|
||||
self.prompt_history.clone(),
|
||||
prompt_buffer.clone(),
|
||||
@@ -150,11 +131,11 @@ impl TerminalInlineAssistant {
|
||||
|
||||
fn handle_prompt_editor_event(
|
||||
&mut self,
|
||||
prompt_editor: View<PromptEditor>,
|
||||
prompt_editor: View<PromptEditor<TerminalCodegen>>,
|
||||
event: &PromptEditorEvent,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let assist_id = prompt_editor.read(cx).id;
|
||||
let assist_id = prompt_editor.read(cx).id();
|
||||
match event {
|
||||
PromptEditorEvent::StartRequested => {
|
||||
self.start_assist(assist_id, cx);
|
||||
@@ -380,8 +361,8 @@ impl TerminalInlineAssistant {
|
||||
|
||||
struct TerminalInlineAssist {
|
||||
terminal: WeakView<TerminalView>,
|
||||
prompt_editor: Option<View<PromptEditor>>,
|
||||
codegen: Model<Codegen>,
|
||||
prompt_editor: Option<View<PromptEditor<TerminalCodegen>>>,
|
||||
codegen: Model<TerminalCodegen>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: Model<ContextStore>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
@@ -391,12 +372,12 @@ impl TerminalInlineAssist {
|
||||
pub fn new(
|
||||
assist_id: TerminalInlineAssistId,
|
||||
terminal: &View<TerminalView>,
|
||||
prompt_editor: View<PromptEditor>,
|
||||
prompt_editor: View<PromptEditor<TerminalCodegen>>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: Model<ContextStore>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self {
|
||||
let codegen = prompt_editor.read(cx).codegen.clone();
|
||||
let codegen = prompt_editor.read(cx).codegen().clone();
|
||||
Self {
|
||||
terminal: terminal.downgrade(),
|
||||
prompt_editor: Some(prompt_editor.clone()),
|
||||
@@ -447,660 +428,3 @@ impl TerminalInlineAssist {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PromptEditorEvent {
|
||||
StartRequested,
|
||||
StopRequested,
|
||||
ConfirmRequested { execute: bool },
|
||||
CancelRequested,
|
||||
DismissRequested,
|
||||
Resized { height_in_lines: u8 },
|
||||
}
|
||||
|
||||
struct PromptEditor {
|
||||
id: TerminalInlineAssistId,
|
||||
height_in_lines: u8,
|
||||
editor: View<Editor>,
|
||||
context_strip: View<ContextStrip>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
language_model_selector: View<LanguageModelSelector>,
|
||||
edited_since_done: bool,
|
||||
prompt_history: VecDeque<String>,
|
||||
prompt_history_ix: Option<usize>,
|
||||
pending_prompt: String,
|
||||
codegen: Model<Codegen>,
|
||||
_codegen_subscription: Subscription,
|
||||
editor_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl EventEmitter<PromptEditorEvent> for PromptEditor {}
|
||||
|
||||
impl Render for PromptEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let status = &self.codegen.read(cx).status;
|
||||
let mut buttons = Vec::new();
|
||||
|
||||
buttons.extend(match status {
|
||||
CodegenStatus::Idle => vec![
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
|
||||
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
|
||||
.into_any_element(),
|
||||
IconButton::new("start", IconName::SparkleAlt)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::for_action("Generate", &menu::Confirm, cx))
|
||||
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)))
|
||||
.into_any_element(),
|
||||
],
|
||||
CodegenStatus::Pending => vec![
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::text("Cancel Assist", cx))
|
||||
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
|
||||
.into_any_element(),
|
||||
IconButton::new("stop", IconName::Stop)
|
||||
.icon_color(Color::Error)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::with_meta(
|
||||
"Interrupt Generation",
|
||||
Some(&menu::Cancel),
|
||||
"Changes won't be discarded",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
|
||||
.into_any_element(),
|
||||
],
|
||||
CodegenStatus::Error(_) | CodegenStatus::Done => {
|
||||
let cancel = IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
|
||||
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
|
||||
.into_any_element();
|
||||
|
||||
let has_error = matches!(status, CodegenStatus::Error(_));
|
||||
if has_error || self.edited_since_done {
|
||||
vec![
|
||||
cancel,
|
||||
IconButton::new("restart", IconName::RotateCw)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::with_meta(
|
||||
"Restart Generation",
|
||||
Some(&menu::Confirm),
|
||||
"Changes will be discarded",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}))
|
||||
.into_any_element(),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
cancel,
|
||||
IconButton::new("accept", IconName::Check)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::for_action("Accept Generated Command", &menu::Confirm, cx)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
|
||||
}))
|
||||
.into_any_element(),
|
||||
IconButton::new("confirm", IconName::Play)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::for_action(
|
||||
"Execute Generated Command",
|
||||
&menu::SecondaryConfirm,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
|
||||
}))
|
||||
.into_any_element(),
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
v_flex()
|
||||
.border_y_1()
|
||||
.border_color(cx.theme().status().info_border)
|
||||
.py_2()
|
||||
.size_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.key_context("PromptEditor")
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.on_action(cx.listener(Self::toggle_context_picker))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::secondary_confirm))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::move_up))
|
||||
.on_action(cx.listener(Self::move_down))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_12()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(LanguageModelSelectorPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
IconButton::new("context", IconName::SettingsAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"Using {}",
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.active_model()
|
||||
.map(|model| model.name().0)
|
||||
.unwrap_or_else(|| "No model selected".into()),
|
||||
),
|
||||
None,
|
||||
"Change Model",
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
))
|
||||
.children(
|
||||
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
|
||||
let error_message = SharedString::from(error.to_string());
|
||||
Some(
|
||||
div()
|
||||
.id("error")
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(error_message.clone(), cx)
|
||||
})
|
||||
.child(
|
||||
Icon::new(IconName::XCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(div().flex_1().child(self.render_prompt_editor(cx)))
|
||||
.child(h_flex().gap_1().pr_4().children(buttons)),
|
||||
)
|
||||
.child(h_flex().child(self.context_strip.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for PromptEditor {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptEditor {
|
||||
const MAX_LINES: u8 = 8;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
id: TerminalInlineAssistId,
|
||||
prompt_history: VecDeque<String>,
|
||||
prompt_buffer: Model<MultiBuffer>,
|
||||
codegen: Model<Codegen>,
|
||||
fs: Arc<dyn Fs>,
|
||||
context_store: Model<ContextStore>,
|
||||
workspace: WeakView<Workspace>,
|
||||
thread_store: Option<WeakModel<ThreadStore>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let prompt_editor = cx.new_view(|cx| {
|
||||
let mut editor = Editor::new(
|
||||
EditorMode::AutoHeight {
|
||||
max_lines: Self::MAX_LINES as usize,
|
||||
},
|
||||
prompt_buffer,
|
||||
None,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
|
||||
editor.set_placeholder_text(Self::placeholder_text(cx), cx);
|
||||
editor
|
||||
});
|
||||
let context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
|
||||
let mut this = Self {
|
||||
id,
|
||||
height_in_lines: 1,
|
||||
editor: prompt_editor.clone(),
|
||||
context_strip: cx.new_view(|cx| {
|
||||
ContextStrip::new(
|
||||
context_store,
|
||||
workspace.clone(),
|
||||
thread_store.clone(),
|
||||
prompt_editor.focus_handle(cx),
|
||||
context_picker_menu_handle.clone(),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
context_picker_menu_handle,
|
||||
language_model_selector: cx.new_view(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
edited_since_done: false,
|
||||
prompt_history,
|
||||
prompt_history_ix: None,
|
||||
pending_prompt: String::new(),
|
||||
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
|
||||
editor_subscriptions: Vec::new(),
|
||||
codegen,
|
||||
};
|
||||
this.count_lines(cx);
|
||||
this.subscribe_to_editor(cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn placeholder_text(cx: &WindowContext) -> String {
|
||||
let context_keybinding = text_for_action(&crate::ToggleFocus, cx)
|
||||
.map(|keybinding| format!(" • {keybinding} for context"))
|
||||
.unwrap_or_default();
|
||||
|
||||
format!("Generate…{context_keybinding} ↓↑ for history")
|
||||
}
|
||||
|
||||
fn subscribe_to_editor(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.editor_subscriptions.clear();
|
||||
self.editor_subscriptions
|
||||
.push(cx.observe(&self.editor, Self::handle_prompt_editor_changed));
|
||||
self.editor_subscriptions
|
||||
.push(cx.subscribe(&self.editor, Self::handle_prompt_editor_events));
|
||||
}
|
||||
|
||||
fn prompt(&self, cx: &AppContext) -> String {
|
||||
self.editor.read(cx).text(cx)
|
||||
}
|
||||
|
||||
fn count_lines(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let height_in_lines = cmp::max(
|
||||
2, // Make the editor at least two lines tall, to account for padding and buttons.
|
||||
cmp::min(
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
|
||||
Self::MAX_LINES as u32,
|
||||
),
|
||||
) as u8;
|
||||
|
||||
if height_in_lines != self.height_in_lines {
|
||||
self.height_in_lines = height_in_lines;
|
||||
cx.emit(PromptEditorEvent::Resized { height_in_lines });
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_prompt_editor_changed(&mut self, _: View<Editor>, cx: &mut ViewContext<Self>) {
|
||||
self.count_lines(cx);
|
||||
}
|
||||
|
||||
fn handle_prompt_editor_events(
|
||||
&mut self,
|
||||
_: View<Editor>,
|
||||
event: &EditorEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
EditorEvent::Edited { .. } => {
|
||||
let prompt = self.editor.read(cx).text(cx);
|
||||
if self
|
||||
.prompt_history_ix
|
||||
.map_or(true, |ix| self.prompt_history[ix] != prompt)
|
||||
{
|
||||
self.prompt_history_ix.take();
|
||||
self.pending_prompt = prompt;
|
||||
}
|
||||
|
||||
self.edited_since_done = true;
|
||||
cx.notify();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
|
||||
match &self.codegen.read(cx).status {
|
||||
CodegenStatus::Idle => {
|
||||
self.editor
|
||||
.update(cx, |editor, _| editor.set_read_only(false));
|
||||
}
|
||||
CodegenStatus::Pending => {
|
||||
self.editor
|
||||
.update(cx, |editor, _| editor.set_read_only(true));
|
||||
}
|
||||
CodegenStatus::Done | CodegenStatus::Error(_) => {
|
||||
self.edited_since_done = false;
|
||||
self.editor
|
||||
.update(cx, |editor, _| editor.set_read_only(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
|
||||
self.context_picker_menu_handle.toggle(cx);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
|
||||
match &self.codegen.read(cx).status {
|
||||
CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
|
||||
cx.emit(PromptEditorEvent::CancelRequested);
|
||||
}
|
||||
CodegenStatus::Pending => {
|
||||
cx.emit(PromptEditorEvent::StopRequested);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||
match &self.codegen.read(cx).status {
|
||||
CodegenStatus::Idle => {
|
||||
if !self.editor.read(cx).text(cx).trim().is_empty() {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}
|
||||
}
|
||||
CodegenStatus::Pending => {
|
||||
cx.emit(PromptEditorEvent::DismissRequested);
|
||||
}
|
||||
CodegenStatus::Done => {
|
||||
if self.edited_since_done {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
} else {
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
|
||||
}
|
||||
}
|
||||
CodegenStatus::Error(_) => {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
|
||||
if matches!(self.codegen.read(cx).status, CodegenStatus::Done) {
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
|
||||
}
|
||||
}
|
||||
|
||||
fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
|
||||
if let Some(ix) = self.prompt_history_ix {
|
||||
if ix > 0 {
|
||||
self.prompt_history_ix = Some(ix - 1);
|
||||
let prompt = self.prompt_history[ix - 1].as_str();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.set_text(prompt, cx);
|
||||
editor.move_to_beginning(&Default::default(), cx);
|
||||
});
|
||||
}
|
||||
} else if !self.prompt_history.is_empty() {
|
||||
self.prompt_history_ix = Some(self.prompt_history.len() - 1);
|
||||
let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.set_text(prompt, cx);
|
||||
editor.move_to_beginning(&Default::default(), cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
|
||||
if let Some(ix) = self.prompt_history_ix {
|
||||
if ix < self.prompt_history.len() - 1 {
|
||||
self.prompt_history_ix = Some(ix + 1);
|
||||
let prompt = self.prompt_history[ix + 1].as_str();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.set_text(prompt, cx);
|
||||
editor.move_to_end(&Default::default(), cx)
|
||||
});
|
||||
} else {
|
||||
self.prompt_history_ix = None;
|
||||
let prompt = self.pending_prompt.as_str();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.set_text(prompt, cx);
|
||||
editor.move_to_end(&Default::default(), cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: if self.editor.read(cx).read_only(cx) {
|
||||
cx.theme().colors().text_disabled
|
||||
} else {
|
||||
cx.theme().colors().text
|
||||
},
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_fallbacks: settings.buffer_font.fallbacks.clone(),
|
||||
font_size: settings.buffer_font_size.into(),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
..Default::default()
|
||||
};
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CodegenEvent {
|
||||
Finished,
|
||||
}
|
||||
|
||||
impl EventEmitter<CodegenEvent> for Codegen {}
|
||||
|
||||
const CLEAR_INPUT: &str = "\x15";
|
||||
const CARRIAGE_RETURN: &str = "\x0d";
|
||||
|
||||
struct TerminalTransaction {
|
||||
terminal: Model<Terminal>,
|
||||
}
|
||||
|
||||
impl TerminalTransaction {
|
||||
pub fn start(terminal: Model<Terminal>) -> Self {
|
||||
Self { terminal }
|
||||
}
|
||||
|
||||
pub fn push(&mut self, hunk: String, cx: &mut AppContext) {
|
||||
// Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
|
||||
let input = Self::sanitize_input(hunk);
|
||||
self.terminal
|
||||
.update(cx, |terminal, _| terminal.input(input));
|
||||
}
|
||||
|
||||
pub fn undo(&self, cx: &mut AppContext) {
|
||||
self.terminal
|
||||
.update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
|
||||
}
|
||||
|
||||
pub fn complete(&self, cx: &mut AppContext) {
|
||||
self.terminal.update(cx, |terminal, _| {
|
||||
terminal.input(CARRIAGE_RETURN.to_string())
|
||||
});
|
||||
}
|
||||
|
||||
fn sanitize_input(input: String) -> String {
|
||||
input.replace(['\r', '\n'], "")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Codegen {
|
||||
status: CodegenStatus,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
terminal: Model<Terminal>,
|
||||
generation: Task<()>,
|
||||
message_id: Option<String>,
|
||||
transaction: Option<TerminalTransaction>,
|
||||
}
|
||||
|
||||
impl Codegen {
|
||||
pub fn new(terminal: Model<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
|
||||
Self {
|
||||
terminal,
|
||||
telemetry,
|
||||
status: CodegenStatus::Idle,
|
||||
generation: Task::ready(()),
|
||||
message_id: None,
|
||||
transaction: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let model_api_key = model.api_key(cx);
|
||||
let http_client = cx.http_client();
|
||||
let telemetry = self.telemetry.clone();
|
||||
self.status = CodegenStatus::Pending;
|
||||
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
|
||||
self.generation = cx.spawn(|this, mut cx| async move {
|
||||
let model_telemetry_id = model.telemetry_id();
|
||||
let model_provider_id = model.provider_id();
|
||||
let response = model.stream_completion_text(prompt, &cx).await;
|
||||
let generate = async {
|
||||
let message_id = response
|
||||
.as_ref()
|
||||
.ok()
|
||||
.and_then(|response| response.message_id.clone());
|
||||
|
||||
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
|
||||
|
||||
let task = cx.background_executor().spawn({
|
||||
let message_id = message_id.clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
async move {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let task = async {
|
||||
let mut chunks = response?.stream;
|
||||
while let Some(chunk) = chunks.next().await {
|
||||
if response_latency.is_none() {
|
||||
response_latency = Some(request_start.elapsed());
|
||||
}
|
||||
let chunk = chunk?;
|
||||
hunks_tx.send(chunk).await?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let result = task.await;
|
||||
|
||||
let error_message = result.as_ref().err().map(|error| error.to_string());
|
||||
report_assistant_event(
|
||||
AssistantEvent {
|
||||
conversation_id: None,
|
||||
kind: AssistantKind::InlineTerminal,
|
||||
message_id,
|
||||
phase: AssistantPhase::Response,
|
||||
model: model_telemetry_id,
|
||||
model_provider: model_provider_id.to_string(),
|
||||
response_latency,
|
||||
error_message,
|
||||
language_name: None,
|
||||
},
|
||||
telemetry,
|
||||
http_client,
|
||||
model_api_key,
|
||||
&executor,
|
||||
);
|
||||
|
||||
result?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
});
|
||||
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.message_id = message_id;
|
||||
})?;
|
||||
|
||||
while let Some(hunk) = hunks_rx.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(transaction) = &mut this.transaction {
|
||||
transaction.push(hunk, cx);
|
||||
cx.notify();
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
task.await?;
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let result = generate.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Err(error) = result {
|
||||
this.status = CodegenStatus::Error(error);
|
||||
} else {
|
||||
this.status = CodegenStatus::Done;
|
||||
}
|
||||
cx.emit(CodegenEvent::Finished);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn stop(&mut self, cx: &mut ModelContext<Self>) {
|
||||
self.status = CodegenStatus::Done;
|
||||
self.generation = Task::ready(());
|
||||
cx.emit(CodegenEvent::Finished);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn complete(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if let Some(transaction) = self.transaction.take() {
|
||||
transaction.complete(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if let Some(transaction) = self.transaction.take() {
|
||||
transaction.undo(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CodegenStatus {
|
||||
Idle,
|
||||
Pending,
|
||||
Done,
|
||||
Error(anyhow::Error),
|
||||
}
|
||||
|
||||
@@ -66,10 +66,10 @@ impl Render for ThreadHistory {
|
||||
threads[range]
|
||||
.iter()
|
||||
.map(|thread| {
|
||||
PastThread::new(
|
||||
h_flex().w_full().pb_1().child(PastThread::new(
|
||||
thread.clone(),
|
||||
history.assistant_panel.clone(),
|
||||
)
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
|
||||
@@ -40,6 +40,7 @@ schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
settings.workspace = true
|
||||
telemetry.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
|
||||
@@ -250,7 +250,9 @@ impl ActiveCall {
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let result = invite.await;
|
||||
if result.is_ok() {
|
||||
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("Participant Invited", cx)
|
||||
})?;
|
||||
} else {
|
||||
//TODO: report collaboration error
|
||||
log::error!("invite failed: {:?}", result);
|
||||
@@ -318,7 +320,7 @@ impl ActiveCall {
|
||||
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("accept incoming", cx)
|
||||
this.report_call_event("Incoming Call Accepted", cx)
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
@@ -331,7 +333,7 @@ impl ActiveCall {
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("no incoming call"))?;
|
||||
report_call_event_for_room("decline incoming", call.room_id, None, &self.client);
|
||||
telemetry::event!("Incoming Call Declined", room_id = call.room_id);
|
||||
self.client.send(proto::DeclineCall {
|
||||
room_id: call.room_id,
|
||||
})?;
|
||||
@@ -366,7 +368,7 @@ impl ActiveCall {
|
||||
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("join channel", cx)
|
||||
this.report_call_event("Channel Joined", cx)
|
||||
})?;
|
||||
Ok(room)
|
||||
})
|
||||
@@ -374,7 +376,7 @@ impl ActiveCall {
|
||||
|
||||
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.notify();
|
||||
self.report_call_event("hang up", cx);
|
||||
self.report_call_event("Call Ended", cx);
|
||||
|
||||
Audio::end_call(cx);
|
||||
|
||||
@@ -393,7 +395,7 @@ impl ActiveCall {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<u64>> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("share project", cx);
|
||||
self.report_call_event("Project Shared", cx);
|
||||
room.update(cx, |room, cx| room.share_project(project, cx))
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("no active call")))
|
||||
@@ -406,7 +408,7 @@ impl ActiveCall {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("unshare project", cx);
|
||||
self.report_call_event("Project Unshared", cx);
|
||||
room.update(cx, |room, cx| room.unshare_project(project, cx))
|
||||
} else {
|
||||
Err(anyhow!("no active call"))
|
||||
@@ -486,35 +488,15 @@ impl ActiveCall {
|
||||
pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) {
|
||||
if let Some(room) = self.room() {
|
||||
let room = room.read(cx);
|
||||
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client);
|
||||
telemetry::event!(
|
||||
operation,
|
||||
room_id = room.id(),
|
||||
channel_id = room.channel_id()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_room(
|
||||
operation: &'static str,
|
||||
room_id: u64,
|
||||
channel_id: Option<ChannelId>,
|
||||
client: &Arc<Client>,
|
||||
) {
|
||||
let telemetry = client.telemetry();
|
||||
|
||||
telemetry.report_call_event(operation, Some(room_id), channel_id)
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_channel(
|
||||
operation: &'static str,
|
||||
channel_id: ChannelId,
|
||||
client: &Arc<Client>,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
let room = ActiveCall::global(cx).read(cx).room();
|
||||
|
||||
let telemetry = client.telemetry();
|
||||
|
||||
telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::TestAppContext;
|
||||
|
||||
@@ -243,7 +243,9 @@ impl ActiveCall {
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let result = invite.await;
|
||||
if result.is_ok() {
|
||||
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("Participant Invited", cx)
|
||||
})?;
|
||||
} else {
|
||||
//TODO: report collaboration error
|
||||
log::error!("invite failed: {:?}", result);
|
||||
@@ -311,7 +313,7 @@ impl ActiveCall {
|
||||
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("accept incoming", cx)
|
||||
this.report_call_event("Incoming Call Accepted", cx)
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
@@ -324,7 +326,7 @@ impl ActiveCall {
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("no incoming call"))?;
|
||||
report_call_event_for_room("decline incoming", call.room_id, None, &self.client);
|
||||
telemetry::event!("Incoming Call Declined", room_id = call.room_id);
|
||||
self.client.send(proto::DeclineCall {
|
||||
room_id: call.room_id,
|
||||
})?;
|
||||
@@ -359,7 +361,7 @@ impl ActiveCall {
|
||||
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("join channel", cx)
|
||||
this.report_call_event("Channel Joined", cx)
|
||||
})?;
|
||||
Ok(room)
|
||||
})
|
||||
@@ -367,7 +369,7 @@ impl ActiveCall {
|
||||
|
||||
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.notify();
|
||||
self.report_call_event("hang up", cx);
|
||||
self.report_call_event("Call Ended", cx);
|
||||
|
||||
Audio::end_call(cx);
|
||||
|
||||
@@ -386,7 +388,7 @@ impl ActiveCall {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<u64>> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("share project", cx);
|
||||
self.report_call_event("Project Shared", cx);
|
||||
room.update(cx, |room, cx| room.share_project(project, cx))
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("no active call")))
|
||||
@@ -399,7 +401,7 @@ impl ActiveCall {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("unshare project", cx);
|
||||
self.report_call_event("Project Unshared", cx);
|
||||
room.update(cx, |room, cx| room.unshare_project(project, cx))
|
||||
} else {
|
||||
Err(anyhow!("no active call"))
|
||||
@@ -479,35 +481,15 @@ impl ActiveCall {
|
||||
pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) {
|
||||
if let Some(room) = self.room() {
|
||||
let room = room.read(cx);
|
||||
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client);
|
||||
telemetry::event!(
|
||||
operation,
|
||||
room_id = room.id(),
|
||||
channel_id = room.channel_id()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_room(
|
||||
operation: &'static str,
|
||||
room_id: u64,
|
||||
channel_id: Option<ChannelId>,
|
||||
client: &Arc<Client>,
|
||||
) {
|
||||
let telemetry = client.telemetry();
|
||||
|
||||
telemetry.report_call_event(operation, Some(room_id), channel_id)
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_channel(
|
||||
operation: &'static str,
|
||||
channel_id: ChannelId,
|
||||
client: &Arc<Client>,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
let room = ActiveCall::global(cx).read(cx).room();
|
||||
|
||||
let telemetry = client.telemetry();
|
||||
|
||||
telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::TestAppContext;
|
||||
|
||||
@@ -51,6 +51,7 @@ tokio-socks = { version = "0.5.2", default-features = false, features = ["future
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
worktree.workspace = true
|
||||
telemetry.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -4,7 +4,8 @@ use crate::{ChannelId, TelemetrySettings};
|
||||
use anyhow::Result;
|
||||
use clock::SystemClock;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::Future;
|
||||
use futures::channel::mpsc;
|
||||
use futures::{Future, StreamExt};
|
||||
use gpui::{AppContext, BackgroundExecutor, Task};
|
||||
use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request};
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -17,9 +18,8 @@ use std::io::Write;
|
||||
use std::time::Instant;
|
||||
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
|
||||
use telemetry_events::{
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, EditEvent, EditorEvent, Event,
|
||||
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, InlineCompletionRating,
|
||||
InlineCompletionRatingEvent, ReplEvent, SettingEvent,
|
||||
AppEvent, AssistantEvent, CallEvent, EditEvent, Event, EventRequestBody, EventWrapper,
|
||||
InlineCompletionEvent,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use worktree::{UpdatedEntriesSet, WorktreeId};
|
||||
@@ -245,7 +245,6 @@ impl Telemetry {
|
||||
})
|
||||
.detach();
|
||||
|
||||
// TODO: Replace all hardware stuff with nested SystemSpecs json
|
||||
let this = Arc::new(Self {
|
||||
clock,
|
||||
http_client: client,
|
||||
@@ -253,6 +252,21 @@ impl Telemetry {
|
||||
state,
|
||||
});
|
||||
|
||||
let (tx, mut rx) = mpsc::unbounded();
|
||||
::telemetry::init(tx);
|
||||
|
||||
cx.background_executor()
|
||||
.spawn({
|
||||
let this = Arc::downgrade(&this);
|
||||
async move {
|
||||
while let Some(event) = rx.next().await {
|
||||
let Some(state) = this.upgrade() else { break };
|
||||
state.report_event(Event::Flexible(event))
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// We should only ever have one instance of Telemetry, leak the subscription to keep it alive
|
||||
// rather than store in TelemetryState, complicating spawn as subscriptions are not Send
|
||||
std::mem::forget(cx.on_app_quit({
|
||||
@@ -320,27 +334,6 @@ impl Telemetry {
|
||||
drop(state);
|
||||
}
|
||||
|
||||
pub fn report_editor_event(
|
||||
self: &Arc<Self>,
|
||||
file_extension: Option<String>,
|
||||
vim_mode: bool,
|
||||
operation: &'static str,
|
||||
copilot_enabled: bool,
|
||||
copilot_enabled_for_language: bool,
|
||||
is_via_ssh: bool,
|
||||
) {
|
||||
let event = Event::Editor(EditorEvent {
|
||||
file_extension,
|
||||
vim_mode,
|
||||
operation: operation.into(),
|
||||
copilot_enabled,
|
||||
copilot_enabled_for_language,
|
||||
is_via_ssh,
|
||||
});
|
||||
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_inline_completion_event(
|
||||
self: &Arc<Self>,
|
||||
provider: String,
|
||||
@@ -356,24 +349,6 @@ impl Telemetry {
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_inline_completion_rating_event(
|
||||
self: &Arc<Self>,
|
||||
rating: InlineCompletionRating,
|
||||
input_events: Arc<str>,
|
||||
input_excerpt: Arc<str>,
|
||||
output_excerpt: Arc<str>,
|
||||
feedback: String,
|
||||
) {
|
||||
let event = Event::InlineCompletionRating(InlineCompletionRatingEvent {
|
||||
rating,
|
||||
input_events,
|
||||
input_excerpt,
|
||||
output_excerpt,
|
||||
feedback,
|
||||
});
|
||||
self.report_event(event);
|
||||
}
|
||||
|
||||
pub fn report_assistant_event(self: &Arc<Self>, event: AssistantEvent) {
|
||||
self.report_event(Event::Assistant(event));
|
||||
}
|
||||
@@ -401,22 +376,6 @@ impl Telemetry {
|
||||
event
|
||||
}
|
||||
|
||||
pub fn report_setting_event(self: &Arc<Self>, setting: &'static str, value: String) {
|
||||
let event = Event::Setting(SettingEvent {
|
||||
setting: setting.to_string(),
|
||||
value,
|
||||
});
|
||||
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_extension_event(self: &Arc<Self>, extension_id: Arc<str>, version: Arc<str>) {
|
||||
self.report_event(Event::Extension(ExtensionEvent {
|
||||
extension_id,
|
||||
version,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn log_edit_event(self: &Arc<Self>, environment: &'static str, is_via_ssh: bool) {
|
||||
let mut state = self.state.lock();
|
||||
let period_data = state.event_coalescer.log_event(environment);
|
||||
@@ -436,15 +395,6 @@ impl Telemetry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_action_event(self: &Arc<Self>, source: &'static str, action: String) {
|
||||
let event = Event::Action(ActionEvent {
|
||||
source: source.to_string(),
|
||||
action,
|
||||
});
|
||||
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_discovered_project_events(
|
||||
self: &Arc<Self>,
|
||||
worktree_id: WorktreeId,
|
||||
@@ -491,21 +441,6 @@ impl Telemetry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_repl_event(
|
||||
self: &Arc<Self>,
|
||||
kernel_language: String,
|
||||
kernel_status: String,
|
||||
repl_session_id: String,
|
||||
) {
|
||||
let event = Event::Repl(ReplEvent {
|
||||
kernel_language,
|
||||
kernel_status,
|
||||
repl_session_id,
|
||||
});
|
||||
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
fn report_event(self: &Arc<Self>, event: Event) {
|
||||
let mut state = self.state.lock();
|
||||
|
||||
|
||||
@@ -16,7 +16,9 @@ use util::TryFutureExt as _;
|
||||
|
||||
pub type UserId = u64;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
#[derive(
|
||||
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
pub struct ChannelId(pub u64);
|
||||
|
||||
impl std::fmt::Display for ChannelId {
|
||||
|
||||
@@ -8,7 +8,12 @@ It contains our back-end logic for collaboration, to which we connect from the Z
|
||||
|
||||
## Database setup
|
||||
|
||||
Before you can run the collab server locally, you'll need to set up a zed Postgres database.
|
||||
Before you can run the collab server locally, you'll need to set up a zed Postgres database. Follow the steps sequentially:
|
||||
|
||||
1. Ensure you have postgres installed. If not, install with `brew install postgresql@15`.
|
||||
2. Follow the steps on Brew's formula and verify your `$PATH` contains `/opt/homebrew/opt/postgresql@15/bin`.
|
||||
3. If you hadn't done it before, create the `postgres` user with `createuser -s postgres`.
|
||||
4. You are now ready to run the `bootstrap` script:
|
||||
|
||||
```sh
|
||||
script/bootstrap
|
||||
|
||||
@@ -610,6 +610,10 @@ fn for_snowflake(
|
||||
"Kernel Status Changed".to_string(),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
Event::Flexible(e) => (
|
||||
e.event_type.clone(),
|
||||
serde_json::to_value(&e.event_properties).unwrap(),
|
||||
),
|
||||
};
|
||||
|
||||
if let serde_json::Value::Object(ref mut map) = event_properties {
|
||||
|
||||
@@ -56,6 +56,7 @@ serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
story = { workspace = true, optional = true }
|
||||
telemetry.workspace = true
|
||||
theme.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use call::report_call_event_for_channel;
|
||||
use call::ActiveCall;
|
||||
use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelStore};
|
||||
use client::{
|
||||
proto::{self, PeerId},
|
||||
@@ -66,11 +66,13 @@ impl ChannelView {
|
||||
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",
|
||||
telemetry::event!(
|
||||
"Channel Notes Opened",
|
||||
channel_id,
|
||||
&workspace.read(cx).app_state().client,
|
||||
cx,
|
||||
room_id = ActiveCall::global(cx)
|
||||
.read(cx)
|
||||
.room()
|
||||
.map(|r| r.read(cx).id())
|
||||
);
|
||||
pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
|
||||
})?;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{collab_panel, ChatPanelSettings};
|
||||
use crate::{collab_panel, ChatPanelButton, ChatPanelSettings};
|
||||
use anyhow::Result;
|
||||
use call::{room, ActiveCall};
|
||||
use channel::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, ChannelStore};
|
||||
@@ -1135,7 +1135,14 @@ impl Panel for ChatPanel {
|
||||
}
|
||||
|
||||
fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
|
||||
Some(ui::IconName::MessageBubbles).filter(|_| ChatPanelSettings::get_global(cx).button)
|
||||
match ChatPanelSettings::get_global(cx).button {
|
||||
ChatPanelButton::Never => None,
|
||||
ChatPanelButton::Always => Some(ui::IconName::MessageBubbles),
|
||||
ChatPanelButton::WhenInCall => ActiveCall::global(cx)
|
||||
.read(cx)
|
||||
.room()
|
||||
.map(|_| ui::IconName::MessageBubbles),
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||
|
||||
@@ -14,7 +14,7 @@ use gpui::{
|
||||
};
|
||||
use panel_settings::MessageEditorSettings;
|
||||
pub use panel_settings::{
|
||||
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
|
||||
ChatPanelButton, ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
|
||||
};
|
||||
use release_channel::ReleaseChannel;
|
||||
use settings::Settings;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::Pixels;
|
||||
use schemars::JsonSchema;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use workspace::dock::DockPosition;
|
||||
|
||||
@@ -11,13 +11,82 @@ pub struct CollaborationPanelSettings {
|
||||
pub default_width: Pixels,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, Serialize, JsonSchema, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ChatPanelButton {
|
||||
Never,
|
||||
Always,
|
||||
#[default]
|
||||
WhenInCall,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ChatPanelButton {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct Visitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for Visitor {
|
||||
type Value = ChatPanelButton;
|
||||
|
||||
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
r#"a boolean or one of "never", "always", "when_in_call""#
|
||||
)
|
||||
}
|
||||
|
||||
fn visit_bool<E>(self, b: bool) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
match b {
|
||||
false => Ok(ChatPanelButton::Never),
|
||||
true => Ok(ChatPanelButton::Always),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
match s {
|
||||
"never" => Ok(ChatPanelButton::Never),
|
||||
"always" => Ok(ChatPanelButton::Always),
|
||||
"when_in_call" => Ok(ChatPanelButton::WhenInCall),
|
||||
_ => Err(E::unknown_variant(s, &["never", "always", "when_in_call"])),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(Visitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ChatPanelSettings {
|
||||
pub button: bool,
|
||||
pub button: ChatPanelButton,
|
||||
pub dock: DockPosition,
|
||||
pub default_width: Pixels,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct ChatPanelSettingsContent {
|
||||
/// When to show the panel button in the status bar.
|
||||
///
|
||||
/// Default: only when in a call
|
||||
pub button: Option<ChatPanelButton>,
|
||||
/// Where to dock the panel.
|
||||
///
|
||||
/// Default: right
|
||||
pub dock: Option<DockPosition>,
|
||||
/// Default width of the panel in pixels.
|
||||
///
|
||||
/// Default: 240
|
||||
pub default_width: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct NotificationPanelSettings {
|
||||
pub button: bool,
|
||||
@@ -66,7 +135,7 @@ impl Settings for CollaborationPanelSettings {
|
||||
impl Settings for ChatPanelSettings {
|
||||
const KEY: Option<&'static str> = Some("chat_panel");
|
||||
|
||||
type FileContent = PanelSettingsContent;
|
||||
type FileContent = ChatPanelSettingsContent;
|
||||
|
||||
fn load(
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
|
||||
@@ -25,6 +25,7 @@ settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
telemetry.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use client::{parse_zed_link, telemetry::Telemetry};
|
||||
use client::parse_zed_link;
|
||||
use collections::HashMap;
|
||||
use command_palette_hooks::{
|
||||
CommandInterceptResult, CommandPaletteFilter, CommandPaletteInterceptor,
|
||||
@@ -36,21 +36,26 @@ pub struct CommandPalette {
|
||||
picker: View<Picker<CommandPaletteDelegate>>,
|
||||
}
|
||||
|
||||
fn trim_consecutive_whitespaces(input: &str) -> String {
|
||||
/// Removes subsequent whitespace characters and double colons from the query.
|
||||
///
|
||||
/// This improves the likelihood of a match by either humanized name or keymap-style name.
|
||||
fn normalize_query(input: &str) -> String {
|
||||
let mut result = String::with_capacity(input.len());
|
||||
let mut last_char_was_whitespace = false;
|
||||
let mut last_char = None;
|
||||
|
||||
for char in input.trim().chars() {
|
||||
if char.is_whitespace() {
|
||||
if !last_char_was_whitespace {
|
||||
result.push(char);
|
||||
match (last_char, char) {
|
||||
(Some(':'), ':') => continue,
|
||||
(Some(last_char), char) if last_char.is_whitespace() && char.is_whitespace() => {
|
||||
continue
|
||||
}
|
||||
_ => {
|
||||
last_char = Some(char);
|
||||
}
|
||||
last_char_was_whitespace = true;
|
||||
} else {
|
||||
result.push(char);
|
||||
last_char_was_whitespace = false;
|
||||
}
|
||||
result.push(char);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
@@ -63,18 +68,12 @@ impl CommandPalette {
|
||||
let Some(previous_focus_handle) = cx.focused() else {
|
||||
return;
|
||||
};
|
||||
let telemetry = workspace.client().telemetry().clone();
|
||||
workspace.toggle_modal(cx, move |cx| {
|
||||
CommandPalette::new(previous_focus_handle, telemetry, query, cx)
|
||||
CommandPalette::new(previous_focus_handle, query, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn new(
|
||||
previous_focus_handle: FocusHandle,
|
||||
telemetry: Arc<Telemetry>,
|
||||
query: &str,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
fn new(previous_focus_handle: FocusHandle, query: &str, cx: &mut ViewContext<Self>) -> Self {
|
||||
let filter = CommandPaletteFilter::try_global(cx);
|
||||
|
||||
let commands = cx
|
||||
@@ -92,12 +91,8 @@ impl CommandPalette {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let delegate = CommandPaletteDelegate::new(
|
||||
cx.view().downgrade(),
|
||||
commands,
|
||||
telemetry,
|
||||
previous_focus_handle,
|
||||
);
|
||||
let delegate =
|
||||
CommandPaletteDelegate::new(cx.view().downgrade(), commands, previous_focus_handle);
|
||||
|
||||
let picker = cx.new_view(|cx| {
|
||||
let picker = Picker::uniform_list(delegate, cx);
|
||||
@@ -133,7 +128,6 @@ pub struct CommandPaletteDelegate {
|
||||
commands: Vec<Command>,
|
||||
matches: Vec<StringMatch>,
|
||||
selected_ix: usize,
|
||||
telemetry: Arc<Telemetry>,
|
||||
previous_focus_handle: FocusHandle,
|
||||
updating_matches: Option<(
|
||||
Task<()>,
|
||||
@@ -167,7 +161,6 @@ impl CommandPaletteDelegate {
|
||||
fn new(
|
||||
command_palette: WeakView<CommandPalette>,
|
||||
commands: Vec<Command>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
previous_focus_handle: FocusHandle,
|
||||
) -> Self {
|
||||
Self {
|
||||
@@ -176,7 +169,6 @@ impl CommandPaletteDelegate {
|
||||
matches: vec![],
|
||||
commands,
|
||||
selected_ix: 0,
|
||||
telemetry,
|
||||
previous_focus_handle,
|
||||
updating_matches: None,
|
||||
}
|
||||
@@ -271,7 +263,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
let mut commands = self.all_commands.clone();
|
||||
let hit_counts = cx.global::<HitCounts>().clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
let query = trim_consecutive_whitespaces(query.as_str());
|
||||
let query = normalize_query(query.as_str());
|
||||
async move {
|
||||
commands.sort_by_key(|action| {
|
||||
(
|
||||
@@ -367,9 +359,11 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
let action_ix = self.matches[self.selected_ix].candidate_id;
|
||||
let command = self.commands.swap_remove(action_ix);
|
||||
|
||||
self.telemetry
|
||||
.report_action_event("command palette", command.name.clone());
|
||||
|
||||
telemetry::event!(
|
||||
"Action Invoked",
|
||||
source = "command palette",
|
||||
action = command.name
|
||||
);
|
||||
self.matches.clear();
|
||||
self.commands.clear();
|
||||
HitCounts::update_global(cx, |hit_counts, _cx| {
|
||||
@@ -474,6 +468,25 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_query() {
|
||||
assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
|
||||
assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
|
||||
assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
|
||||
assert_eq!(
|
||||
normalize_query("editor::GoToDefinition"),
|
||||
"editor:GoToDefinition"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_query("editor::::GoToDefinition"),
|
||||
"editor:GoToDefinition"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_query("editor: :GoToDefinition"),
|
||||
"editor: :GoToDefinition"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_command_palette(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
@@ -544,6 +557,40 @@ mod tests {
|
||||
assert!(palette.delegate.matches.is_empty())
|
||||
});
|
||||
}
|
||||
#[gpui::test]
|
||||
async fn test_normalized_matches(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
let editor = cx.new_view(|cx| {
|
||||
let mut editor = Editor::single_line(cx);
|
||||
editor.set_text("abc", cx);
|
||||
editor
|
||||
});
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx);
|
||||
editor.update(cx, |editor, cx| editor.focus(cx))
|
||||
});
|
||||
|
||||
// Test normalize (trimming whitespace and double colons)
|
||||
cx.simulate_keystrokes("cmd-shift-p");
|
||||
|
||||
let palette = workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_modal::<CommandPalette>(cx)
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.picker
|
||||
.clone()
|
||||
});
|
||||
|
||||
cx.simulate_input("Editor:: Backspace");
|
||||
palette.update(cx, |palette, _| {
|
||||
assert_eq!(palette.delegate.matches[0].string, "editor: backspace");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_go_to_line(cx: &mut TestAppContext) {
|
||||
|
||||
@@ -59,6 +59,10 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
"Copilot"
|
||||
}
|
||||
|
||||
fn show_completions_in_menu() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_enabled(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
@@ -326,17 +330,15 @@ mod tests {
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
// We want to show both: the inline completion and the completion menu
|
||||
assert!(editor.context_menu_visible());
|
||||
assert!(editor.context_menu_contains_inline_completion());
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert!(!editor.context_menu_contains_inline_completion());
|
||||
assert!(!editor.has_active_inline_completion());
|
||||
// Since we have both, the copilot suggestion is not shown inline
|
||||
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
|
||||
assert_eq!(editor.display_text(cx), "one.\ntwo\nthree\n");
|
||||
|
||||
// Confirming a non-copilot completion inserts it and hides the context menu, without showing
|
||||
// the copilot suggestion afterwards.
|
||||
editor.context_menu_next(&Default::default(), cx);
|
||||
editor
|
||||
.confirm_completion(&Default::default(), cx)
|
||||
.unwrap()
|
||||
@@ -384,23 +386,12 @@ mod tests {
|
||||
|
||||
// Ensure existing inline completion is interpolated when inserting again.
|
||||
cx.simulate_keystroke("c");
|
||||
drop(handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.c|<>
|
||||
two
|
||||
three
|
||||
"},
|
||||
vec!["completion_a", "completion_b"],
|
||||
));
|
||||
executor.run_until_parked();
|
||||
cx.update_editor(|editor, cx| {
|
||||
// Since we have an LSP completion too, the inline completion is
|
||||
// shown in the menu now
|
||||
assert!(editor.context_menu_visible());
|
||||
assert!(editor.context_menu_contains_inline_completion());
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(!editor.context_menu_contains_inline_completion());
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
|
||||
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
@@ -416,16 +407,9 @@ mod tests {
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert!(editor.context_menu_contains_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
|
||||
|
||||
// Canceling should first hide the menu and make Copilot suggestion visible.
|
||||
editor.cancel(&Default::default(), cx);
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert!(!editor.context_menu_contains_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
|
||||
|
||||
@@ -928,8 +912,8 @@ mod tests {
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
assert!(editor.context_menu_contains_inline_completion());
|
||||
assert!(editor.has_active_inline_completion(),);
|
||||
assert!(!editor.context_menu_contains_inline_completion());
|
||||
assert!(!editor.has_active_inline_completion(),);
|
||||
assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ use editor::{
|
||||
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock},
|
||||
highlight_diagnostic_message,
|
||||
scroll::Autoscroll,
|
||||
Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
|
||||
Anchor, AnchorRangeExt, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer,
|
||||
RangeToAnchorExt, ToOffset,
|
||||
};
|
||||
use gpui::{
|
||||
actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,
|
||||
@@ -21,7 +22,8 @@ use gpui::{
|
||||
WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal,
|
||||
Bias, Buffer, BufferId, Diagnostic, DiagnosticEntry, DiagnosticSeverity, OffsetRangeExt, Point,
|
||||
Selection, SelectionGoal,
|
||||
};
|
||||
use lsp::LanguageServerId;
|
||||
use project::{DiagnosticSummary, Project, ProjectPath};
|
||||
@@ -38,9 +40,10 @@ use std::{
|
||||
use theme::ActiveTheme;
|
||||
pub use toolbar_controls::ToolbarControls;
|
||||
use ui::{h_flex, prelude::*, Icon, IconName, Label};
|
||||
use util::ResultExt;
|
||||
use util::{maybe, RangeExt, ResultExt};
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
|
||||
searchable::SearchableItemHandle,
|
||||
ItemNavHistory, ToolbarItemLocation, Workspace,
|
||||
};
|
||||
|
||||
@@ -255,26 +258,69 @@ impl ProjectDiagnosticsEditor {
|
||||
}
|
||||
|
||||
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
|
||||
workspace.activate_item(&existing, true, true, cx);
|
||||
} else {
|
||||
let workspace_handle = cx.view().downgrade();
|
||||
let diagnostic_to_select = maybe!({
|
||||
let active_item = workspace.active_item(cx)?;
|
||||
// Already on diagnostics, so don't update position / selection within.
|
||||
if active_item.downcast::<ProjectDiagnosticsEditor>().is_some() {
|
||||
None
|
||||
} else {
|
||||
let active_editor = active_item.act_as::<Editor>(cx)?.read(cx);
|
||||
let snapshot = active_editor.buffer().read(cx).snapshot(cx);
|
||||
let newest_selection = active_editor.selections.newest::<usize>(cx).range();
|
||||
let mut diagnostics =
|
||||
snapshot.diagnostics_in_range(newest_selection.clone(), false);
|
||||
let diagnostic_entry: DiagnosticEntry<usize> = dbg!(diagnostics.next())?;
|
||||
let buffer_id = diagnostic_entry
|
||||
.range
|
||||
.to_anchors(&snapshot)
|
||||
.start
|
||||
.buffer_id?;
|
||||
Some((buffer_id, diagnostic_entry.range, newest_selection))
|
||||
}
|
||||
});
|
||||
|
||||
let include_warnings = match cx.try_global::<IncludeWarnings>() {
|
||||
Some(include_warnings) => include_warnings.0,
|
||||
None => ProjectDiagnosticsSettings::get_global(cx).include_warnings,
|
||||
let diagnostics =
|
||||
if let Some(diagnostics) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
|
||||
// todo! dedupe
|
||||
if let Some((buffer_id, diagnostic_range, selection_range)) = diagnostic_to_select {
|
||||
diagnostics.update(cx, |diagnostics, cx| {
|
||||
diagnostics.set_selection_that_intersects_diagnostic(
|
||||
buffer_id,
|
||||
diagnostic_range,
|
||||
selection_range,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
workspace.activate_item(&diagnostics, true, true, cx);
|
||||
} else {
|
||||
let workspace_handle = cx.view().downgrade();
|
||||
|
||||
let include_warnings = match cx.try_global::<IncludeWarnings>() {
|
||||
Some(include_warnings) => include_warnings.0,
|
||||
None => ProjectDiagnosticsSettings::get_global(cx).include_warnings,
|
||||
};
|
||||
|
||||
let diagnostics = cx.new_view(|cx| {
|
||||
ProjectDiagnosticsEditor::new(
|
||||
workspace.project().clone(),
|
||||
include_warnings,
|
||||
workspace_handle,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
if let Some((buffer_id, diagnostic_range, selection_range)) = diagnostic_to_select {
|
||||
diagnostics.update(cx, |diagnostics, cx| {
|
||||
diagnostics.set_selection_that_intersects_diagnostic(
|
||||
buffer_id,
|
||||
diagnostic_range,
|
||||
selection_range,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
workspace.add_item_to_active_pane(Box::new(diagnostics), None, true, cx);
|
||||
};
|
||||
|
||||
let diagnostics = cx.new_view(|cx| {
|
||||
ProjectDiagnosticsEditor::new(
|
||||
workspace.project().clone(),
|
||||
include_warnings,
|
||||
workspace_handle,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
workspace.add_item_to_active_pane(Box::new(diagnostics), None, true, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) {
|
||||
@@ -631,6 +677,30 @@ impl ProjectDiagnosticsEditor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_selection_that_intersects_diagnostic(
|
||||
&self,
|
||||
buffer_id: BufferId,
|
||||
diagnostic_range: Range<usize>,
|
||||
selection_range: Range<usize>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.excerpts_for_range(range)
|
||||
/*
|
||||
let excerpts = buffer.excerpts_for_buffer_id(buffer_id, cx);
|
||||
for (excerpt_id, excerpt_range) in excerpts {
|
||||
let excerpt_range = excerpt_range.context.to_offset(snapshot.excerpt_containing(range));
|
||||
if excerpt_range.contains_inclusive(&diagnostic_range.to_offset(&snapshot)) {}
|
||||
}
|
||||
*/
|
||||
// editor.change_selections(Some(Autoscroll::center()), cx, |selections| {
|
||||
// selections.select_ranges(vec![diagnostic_range]);
|
||||
// })
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn check_invariants(&self, cx: &mut ViewContext<Self>) {
|
||||
let mut excerpts = Vec::new();
|
||||
@@ -810,6 +880,10 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(self.editor.clone()))
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ smol.workspace = true
|
||||
snippet.workspace = true
|
||||
sum_tree.workspace = true
|
||||
task.workspace = true
|
||||
telemetry.workspace = true
|
||||
text.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::cell::RefCell;
|
||||
use std::{cell::Cell, cmp::Reverse, ops::Range, rc::Rc};
|
||||
use std::{cmp::Reverse, ops::Range, rc::Rc};
|
||||
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
@@ -14,12 +14,8 @@ use multi_buffer::{Anchor, ExcerptId};
|
||||
use ordered_float::OrderedFloat;
|
||||
use project::{CodeAction, Completion, TaskSourceKind};
|
||||
use task::ResolvedTask;
|
||||
use ui::{
|
||||
h_flex, ActiveTheme as _, Color, FluentBuilder as _, InteractiveElement as _, IntoElement,
|
||||
Label, LabelCommon as _, LabelSize, ListItem, ParentElement as _, Popover,
|
||||
StatefulInteractiveElement as _, Styled, Toggleable as _,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use ui::{prelude::*, Color, IntoElement, ListItem, Pixels, Popover, Styled};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{
|
||||
@@ -30,6 +26,8 @@ use crate::{
|
||||
};
|
||||
use crate::{AcceptInlineCompletion, InlineCompletionMenuHint, InlineCompletionText};
|
||||
|
||||
pub const MAX_COMPLETIONS_ASIDE_WIDTH: Pixels = px(500.);
|
||||
|
||||
pub enum CodeContextMenu {
|
||||
Completions(CompletionsMenu),
|
||||
CodeActions(CodeActionsMenu),
|
||||
@@ -113,20 +111,33 @@ impl CodeContextMenu {
|
||||
CodeContextMenu::CodeActions(menu) => menu.origin(cursor_position),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
style: &EditorStyle,
|
||||
max_height_in_lines: u32,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> AnyElement {
|
||||
match self {
|
||||
CodeContextMenu::Completions(menu) => {
|
||||
menu.render(style, max_height_in_lines, workspace, cx)
|
||||
}
|
||||
CodeContextMenu::Completions(menu) => menu.render(style, max_height_in_lines, cx),
|
||||
CodeContextMenu::CodeActions(menu) => menu.render(style, max_height_in_lines, cx),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_aside(
|
||||
&self,
|
||||
style: &EditorStyle,
|
||||
max_height: Pixels,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<AnyElement> {
|
||||
match self {
|
||||
CodeContextMenu::Completions(menu) => {
|
||||
menu.render_aside(style, max_height, workspace, cx)
|
||||
}
|
||||
CodeContextMenu::CodeActions(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ContextMenuOrigin {
|
||||
@@ -146,7 +157,6 @@ pub struct CompletionsMenu {
|
||||
pub selected_item: usize,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
resolve_completions: bool,
|
||||
pub aside_was_displayed: Cell<bool>,
|
||||
show_completion_documentation: bool,
|
||||
}
|
||||
|
||||
@@ -164,7 +174,6 @@ impl CompletionsMenu {
|
||||
initial_position: Anchor,
|
||||
buffer: Model<Buffer>,
|
||||
completions: Box<[Completion]>,
|
||||
aside_was_displayed: bool,
|
||||
) -> Self {
|
||||
let match_candidates = completions
|
||||
.iter()
|
||||
@@ -184,7 +193,6 @@ impl CompletionsMenu {
|
||||
selected_item: 0,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
resolve_completions: true,
|
||||
aside_was_displayed: Cell::new(aside_was_displayed),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,7 +248,6 @@ impl CompletionsMenu {
|
||||
selected_item: 0,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
resolve_completions: false,
|
||||
aside_was_displayed: Cell::new(false),
|
||||
show_completion_documentation: false,
|
||||
}
|
||||
}
|
||||
@@ -318,7 +325,9 @@ impl CompletionsMenu {
|
||||
}
|
||||
}
|
||||
.into();
|
||||
self.selected_item = 0;
|
||||
if self.selected_item != 0 && self.selected_item + 1 < self.entries.len() {
|
||||
self.selected_item += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_selected_completion(
|
||||
@@ -366,11 +375,8 @@ impl CompletionsMenu {
|
||||
&self,
|
||||
style: &EditorStyle,
|
||||
max_height_in_lines: u32,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> AnyElement {
|
||||
let max_height = max_height_in_lines as f32 * cx.line_height();
|
||||
|
||||
let completions = self.completions.borrow_mut();
|
||||
let show_completion_documentation = self.show_completion_documentation;
|
||||
let widest_completion_ix = self
|
||||
@@ -397,70 +403,12 @@ impl CompletionsMenu {
|
||||
}) => provider_name.len(),
|
||||
})
|
||||
.map(|(ix, _)| ix);
|
||||
drop(completions);
|
||||
|
||||
let selected_item = self.selected_item;
|
||||
let style = style.clone();
|
||||
|
||||
let multiline_docs = match &self.entries[selected_item] {
|
||||
CompletionEntry::Match(mat) if show_completion_documentation => {
|
||||
match &completions[mat.candidate_id].documentation {
|
||||
Some(Documentation::MultiLinePlainText(text)) => {
|
||||
Some(div().child(SharedString::from(text.clone())))
|
||||
}
|
||||
Some(Documentation::MultiLineMarkdown(parsed)) if !parsed.text.is_empty() => {
|
||||
Some(div().child(render_parsed_markdown(
|
||||
"completions_markdown",
|
||||
parsed,
|
||||
&style,
|
||||
workspace,
|
||||
cx,
|
||||
)))
|
||||
}
|
||||
Some(Documentation::Undocumented) if self.aside_was_displayed.get() => {
|
||||
Some(div().child("No documentation"))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
CompletionEntry::InlineCompletionHint(hint) => Some(match &hint.text {
|
||||
InlineCompletionText::Edit { text, highlights } => div()
|
||||
.my_1()
|
||||
.rounded_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
gpui::StyledText::new(text.clone())
|
||||
.with_highlights(&style.text, highlights.clone()),
|
||||
),
|
||||
InlineCompletionText::Move(text) => div().child(text.clone()),
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let aside_contents = if let Some(multiline_docs) = multiline_docs {
|
||||
Some(multiline_docs)
|
||||
} else if show_completion_documentation && self.aside_was_displayed.get() {
|
||||
Some(div().child("Fetching documentation..."))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.aside_was_displayed.set(aside_contents.is_some());
|
||||
|
||||
let aside_contents = aside_contents.map(|div| {
|
||||
div.id("multiline_docs")
|
||||
.max_h(max_height)
|
||||
.flex_1()
|
||||
.px_1p5()
|
||||
.py_1()
|
||||
.min_w(px(260.))
|
||||
.max_w(px(640.))
|
||||
.w(px(500.))
|
||||
.overflow_y_scroll()
|
||||
.occlude()
|
||||
});
|
||||
|
||||
drop(completions);
|
||||
let completions = self.completions.clone();
|
||||
let matches = self.entries.clone();
|
||||
let style = style.clone();
|
||||
let list = uniform_list(
|
||||
cx.view().clone(),
|
||||
"completions",
|
||||
@@ -513,6 +461,7 @@ impl CompletionsMenu {
|
||||
(range, highlight)
|
||||
}),
|
||||
);
|
||||
|
||||
let completion_label =
|
||||
StyledText::new(completion.label.text.clone())
|
||||
.with_highlights(&style.text, highlights);
|
||||
@@ -563,17 +512,21 @@ impl CompletionsMenu {
|
||||
ListItem::new("inline-completion")
|
||||
.inset(true)
|
||||
.toggle_state(item_ix == selected_item)
|
||||
.start_slot(Icon::new(IconName::ZedPredict))
|
||||
.child(
|
||||
StyledText::new(format!(
|
||||
"{} Completion",
|
||||
SharedString::new_static(provider_name)
|
||||
))
|
||||
.with_highlights(&style.text, None),
|
||||
)
|
||||
.on_click(cx.listener(move |editor, _event, cx| {
|
||||
cx.stop_propagation();
|
||||
editor.accept_inline_completion(
|
||||
&AcceptInlineCompletion {},
|
||||
cx,
|
||||
);
|
||||
}))
|
||||
.child(
|
||||
StyledText::new(SharedString::new_static(provider_name))
|
||||
.with_highlights(&style.text, None),
|
||||
),
|
||||
})),
|
||||
),
|
||||
}
|
||||
})
|
||||
@@ -586,12 +539,71 @@ impl CompletionsMenu {
|
||||
.with_width_from_item(widest_completion_ix)
|
||||
.with_sizing_behavior(ListSizingBehavior::Infer);
|
||||
|
||||
Popover::new()
|
||||
.child(list)
|
||||
.when_some(aside_contents, |popover, aside_contents| {
|
||||
popover.aside(aside_contents)
|
||||
})
|
||||
.into_any_element()
|
||||
Popover::new().child(list).into_any_element()
|
||||
}
|
||||
|
||||
fn render_aside(
|
||||
&self,
|
||||
style: &EditorStyle,
|
||||
max_height: Pixels,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<AnyElement> {
|
||||
if !self.show_completion_documentation {
|
||||
return None;
|
||||
}
|
||||
|
||||
let multiline_docs = match &self.entries[self.selected_item] {
|
||||
CompletionEntry::Match(mat) => {
|
||||
match self.completions.borrow_mut()[mat.candidate_id]
|
||||
.documentation
|
||||
.as_ref()?
|
||||
{
|
||||
Documentation::MultiLinePlainText(text) => {
|
||||
div().child(SharedString::from(text.clone()))
|
||||
}
|
||||
Documentation::MultiLineMarkdown(parsed) if !parsed.text.is_empty() => div()
|
||||
.child(render_parsed_markdown(
|
||||
"completions_markdown",
|
||||
parsed,
|
||||
&style,
|
||||
workspace,
|
||||
cx,
|
||||
)),
|
||||
Documentation::MultiLineMarkdown(_) => return None,
|
||||
Documentation::SingleLine(_) => return None,
|
||||
Documentation::Undocumented => return None,
|
||||
}
|
||||
}
|
||||
CompletionEntry::InlineCompletionHint(hint) => match &hint.text {
|
||||
InlineCompletionText::Edit { text, highlights } => div()
|
||||
.mx_1()
|
||||
.rounded(px(6.))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
gpui::StyledText::new(text.clone())
|
||||
.with_highlights(&style.text, highlights.clone()),
|
||||
),
|
||||
InlineCompletionText::Move(text) => div().child(text.clone()),
|
||||
},
|
||||
};
|
||||
|
||||
Some(
|
||||
Popover::new()
|
||||
.child(
|
||||
multiline_docs
|
||||
.id("multiline_docs")
|
||||
.max_h(max_height)
|
||||
.px_2()
|
||||
.min_w(px(260.))
|
||||
.max_w(MAX_COMPLETIONS_ASIDE_WIDTH)
|
||||
.overflow_y_scroll()
|
||||
.occlude(),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
|
||||
|
||||
@@ -1370,7 +1370,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
this.report_editor_event("open", None, cx);
|
||||
this.report_editor_event("Editor Opened", None, cx);
|
||||
this
|
||||
}
|
||||
|
||||
@@ -2473,7 +2473,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
if self.hide_context_menu(cx).is_some() {
|
||||
if self.has_active_inline_completion() {
|
||||
if self.show_inline_completions_in_menu(cx) && self.has_active_inline_completion() {
|
||||
self.update_visible_inline_completion(cx);
|
||||
}
|
||||
return true;
|
||||
@@ -2856,6 +2856,7 @@ impl Editor {
|
||||
);
|
||||
}
|
||||
|
||||
let had_active_inline_completion = this.has_active_inline_completion();
|
||||
this.change_selections_inner(Some(Autoscroll::fit()), false, cx, |s| {
|
||||
s.select(new_selections)
|
||||
});
|
||||
@@ -2876,7 +2877,9 @@ impl Editor {
|
||||
this.show_signature_help(&ShowSignatureHelp, cx);
|
||||
}
|
||||
|
||||
this.trigger_completion_on_input(&text, true, cx);
|
||||
let trigger_in_words =
|
||||
this.show_inline_completions_in_menu(cx) || !had_active_inline_completion;
|
||||
this.trigger_completion_on_input(&text, trigger_in_words, cx);
|
||||
linked_editing_ranges::refresh_linked_ranges(this, cx);
|
||||
this.refresh_inline_completion(true, false, cx);
|
||||
});
|
||||
@@ -3682,10 +3685,6 @@ impl Editor {
|
||||
|
||||
let query = Self::completion_query(&self.buffer.read(cx).read(cx), position);
|
||||
|
||||
let aside_was_displayed = match self.context_menu.borrow().deref() {
|
||||
Some(CodeContextMenu::Completions(menu)) => menu.aside_was_displayed.get(),
|
||||
_ => false,
|
||||
};
|
||||
let trigger_kind = match &options.trigger {
|
||||
Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => {
|
||||
CompletionTriggerKind::TRIGGER_CHARACTER
|
||||
@@ -3720,7 +3719,6 @@ impl Editor {
|
||||
position,
|
||||
buffer.clone(),
|
||||
completions.into(),
|
||||
aside_was_displayed,
|
||||
);
|
||||
|
||||
menu.filter(query.as_deref(), cx.background_executor().clone())
|
||||
@@ -3746,9 +3744,12 @@ impl Editor {
|
||||
let mut menu = menu.unwrap();
|
||||
menu.resolve_selected_completion(editor.completion_provider.as_deref(), cx);
|
||||
|
||||
if let Some(hint) = editor.inline_completion_menu_hint(cx) {
|
||||
editor.hide_active_inline_completion(cx);
|
||||
menu.show_inline_completion_hint(hint);
|
||||
if editor.show_inline_completions_in_menu(cx) {
|
||||
if let Some(hint) = editor.inline_completion_menu_hint(cx) {
|
||||
menu.show_inline_completion_hint(hint);
|
||||
}
|
||||
} else {
|
||||
editor.discard_inline_completion(false, cx);
|
||||
}
|
||||
|
||||
*editor.context_menu.borrow_mut() =
|
||||
@@ -3757,9 +3758,14 @@ impl Editor {
|
||||
cx.notify();
|
||||
} else if editor.completion_tasks.len() <= 1 {
|
||||
// If there are no more completion tasks and the last menu was
|
||||
// empty, we should hide it. If it was already hidden, we should
|
||||
// also show the copilot completion when available.
|
||||
editor.hide_context_menu(cx);
|
||||
// empty, we should hide it.
|
||||
let was_hidden = editor.hide_context_menu(cx).is_none();
|
||||
// If it was already hidden and we don't show inline
|
||||
// completions in the menu, we should also show the
|
||||
// inline-completion when available.
|
||||
if was_hidden && editor.show_inline_completions_in_menu(cx) {
|
||||
editor.update_visible_inline_completion(cx);
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
@@ -3813,7 +3819,9 @@ impl Editor {
|
||||
return Some(Task::ready(Ok(())));
|
||||
}
|
||||
CompletionEntry::Match(mat) => {
|
||||
self.discard_inline_completion(true, cx);
|
||||
if self.show_inline_completions_in_menu(cx) {
|
||||
self.discard_inline_completion(true, cx);
|
||||
}
|
||||
mat
|
||||
}
|
||||
};
|
||||
@@ -4551,7 +4559,9 @@ impl Editor {
|
||||
_: &AcceptInlineCompletion,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.hide_context_menu(cx);
|
||||
if self.show_inline_completions_in_menu(cx) {
|
||||
self.hide_context_menu(cx);
|
||||
}
|
||||
|
||||
let Some(active_inline_completion) = self.active_inline_completion.as_ref() else {
|
||||
return;
|
||||
@@ -4700,17 +4710,6 @@ impl Editor {
|
||||
Some(active_inline_completion.completion)
|
||||
}
|
||||
|
||||
fn hide_active_inline_completion(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(active_inline_completion) = self.active_inline_completion.as_ref() {
|
||||
self.splice_inlays(
|
||||
active_inline_completion.inlay_ids.clone(),
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
self.clear_highlights::<InlineCompletionHighlight>(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_visible_inline_completion(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
|
||||
let selection = self.selections.newest_anchor();
|
||||
let cursor = selection.head();
|
||||
@@ -4718,7 +4717,11 @@ impl Editor {
|
||||
let offset_selection = selection.map(|endpoint| endpoint.to_offset(&multibuffer));
|
||||
let excerpt_id = cursor.excerpt_id;
|
||||
|
||||
if !offset_selection.is_empty()
|
||||
let completions_menu_has_precedence = !self.show_inline_completions_in_menu(cx)
|
||||
&& (self.context_menu.borrow().is_some()
|
||||
|| (!self.completion_tasks.is_empty() && !self.has_active_inline_completion()));
|
||||
if completions_menu_has_precedence
|
||||
|| !offset_selection.is_empty()
|
||||
|| self
|
||||
.active_inline_completion
|
||||
.as_ref()
|
||||
@@ -4742,16 +4745,10 @@ impl Editor {
|
||||
let edits = completion
|
||||
.edits
|
||||
.into_iter()
|
||||
.map(|(range, new_text)| {
|
||||
(
|
||||
multibuffer
|
||||
.anchor_in_excerpt(excerpt_id, range.start)
|
||||
.unwrap()
|
||||
..multibuffer
|
||||
.anchor_in_excerpt(excerpt_id, range.end)
|
||||
.unwrap(),
|
||||
new_text,
|
||||
)
|
||||
.flat_map(|(range, new_text)| {
|
||||
let start = multibuffer.anchor_in_excerpt(excerpt_id, range.start)?;
|
||||
let end = multibuffer.anchor_in_excerpt(excerpt_id, range.end)?;
|
||||
Some((start..end, new_text))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if edits.is_empty() {
|
||||
@@ -4782,34 +4779,32 @@ impl Editor {
|
||||
invalidation_row_range = edit_start_row..cursor_row;
|
||||
completion = InlineCompletion::Move(first_edit_start);
|
||||
} else {
|
||||
if !self.has_active_completions_menu() {
|
||||
if edits
|
||||
.iter()
|
||||
.all(|(range, _)| range.to_offset(&multibuffer).is_empty())
|
||||
{
|
||||
let mut inlays = Vec::new();
|
||||
for (range, new_text) in &edits {
|
||||
let inlay = Inlay::inline_completion(
|
||||
post_inc(&mut self.next_inlay_id),
|
||||
range.start,
|
||||
new_text.as_str(),
|
||||
);
|
||||
inlay_ids.push(inlay.id);
|
||||
inlays.push(inlay);
|
||||
}
|
||||
|
||||
self.splice_inlays(vec![], inlays, cx);
|
||||
} else {
|
||||
let background_color = cx.theme().status().deleted_background;
|
||||
self.highlight_text::<InlineCompletionHighlight>(
|
||||
edits.iter().map(|(range, _)| range.clone()).collect(),
|
||||
HighlightStyle {
|
||||
background_color: Some(background_color),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
if edits
|
||||
.iter()
|
||||
.all(|(range, _)| range.to_offset(&multibuffer).is_empty())
|
||||
{
|
||||
let mut inlays = Vec::new();
|
||||
for (range, new_text) in &edits {
|
||||
let inlay = Inlay::inline_completion(
|
||||
post_inc(&mut self.next_inlay_id),
|
||||
range.start,
|
||||
new_text.as_str(),
|
||||
);
|
||||
inlay_ids.push(inlay.id);
|
||||
inlays.push(inlay);
|
||||
}
|
||||
|
||||
self.splice_inlays(vec![], inlays, cx);
|
||||
} else {
|
||||
let background_color = cx.theme().status().deleted_background;
|
||||
self.highlight_text::<InlineCompletionHighlight>(
|
||||
edits.iter().map(|(range, _)| range.clone()).collect(),
|
||||
HighlightStyle {
|
||||
background_color: Some(background_color),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
invalidation_row_range = edit_start_row..edit_end_row;
|
||||
@@ -4829,7 +4824,7 @@ impl Editor {
|
||||
invalidation_range,
|
||||
});
|
||||
|
||||
if self.has_active_completions_menu() {
|
||||
if self.show_inline_completions_in_menu(cx) && self.has_active_completions_menu() {
|
||||
if let Some(hint) = self.inline_completion_menu_hint(cx) {
|
||||
match self.context_menu.borrow_mut().as_mut() {
|
||||
Some(CodeContextMenu::Completions(menu)) => {
|
||||
@@ -4855,7 +4850,7 @@ impl Editor {
|
||||
|
||||
let text = match &self.active_inline_completion.as_ref()?.completion {
|
||||
InlineCompletion::Edit(edits) => {
|
||||
inline_completion_edit_text(&editor_snapshot, edits, cx)
|
||||
inline_completion_edit_text(&editor_snapshot, edits, true, cx)
|
||||
}
|
||||
InlineCompletion::Move(target) => {
|
||||
let target_point =
|
||||
@@ -4880,6 +4875,13 @@ impl Editor {
|
||||
Some(self.inline_completion_provider.as_ref()?.provider.clone())
|
||||
}
|
||||
|
||||
fn show_inline_completions_in_menu(&self, cx: &AppContext) -> bool {
|
||||
EditorSettings::get_global(cx).show_inline_completions_in_menu
|
||||
&& self
|
||||
.inline_completion_provider()
|
||||
.map_or(false, |provider| provider.show_completions_in_menu())
|
||||
}
|
||||
|
||||
fn render_code_actions_indicator(
|
||||
&self,
|
||||
_style: &EditorStyle,
|
||||
@@ -5118,12 +5120,27 @@ impl Editor {
|
||||
) -> Option<AnyElement> {
|
||||
self.context_menu.borrow().as_ref().and_then(|menu| {
|
||||
if menu.visible() {
|
||||
Some(menu.render(
|
||||
Some(menu.render(style, max_height_in_lines, cx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render_context_menu_aside(
|
||||
&self,
|
||||
style: &EditorStyle,
|
||||
max_height: Pixels,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<AnyElement> {
|
||||
self.context_menu.borrow().as_ref().and_then(|menu| {
|
||||
if menu.visible() {
|
||||
menu.render_aside(
|
||||
style,
|
||||
max_height_in_lines,
|
||||
max_height,
|
||||
self.workspace.as_ref().map(|(w, _)| w.clone()),
|
||||
cx,
|
||||
))
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -5133,7 +5150,11 @@ impl Editor {
|
||||
fn hide_context_menu(&mut self, cx: &mut ViewContext<Self>) -> Option<CodeContextMenu> {
|
||||
cx.notify();
|
||||
self.completion_tasks.clear();
|
||||
self.context_menu.borrow_mut().take()
|
||||
let context_menu = self.context_menu.borrow_mut().take();
|
||||
if context_menu.is_some() && !self.show_inline_completions_in_menu(cx) {
|
||||
self.update_visible_inline_completion(cx);
|
||||
}
|
||||
context_menu
|
||||
}
|
||||
|
||||
fn show_snippet_choices(
|
||||
@@ -5422,7 +5443,8 @@ impl Editor {
|
||||
if end_point == start_point {
|
||||
let offset = text::ToOffset::to_offset(&range.start, &snapshot)
|
||||
.saturating_sub(1);
|
||||
start_point = TP::to_point(&offset, &snapshot);
|
||||
start_point =
|
||||
snapshot.clip_point(TP::to_point(&offset, &snapshot), Bias::Left);
|
||||
};
|
||||
|
||||
(start_point..end_point, empty_str.clone())
|
||||
@@ -10377,7 +10399,7 @@ impl Editor {
|
||||
self.end_transaction_at(Instant::now(), cx)
|
||||
}
|
||||
|
||||
fn start_transaction_at(&mut self, now: Instant, cx: &mut ViewContext<Self>) {
|
||||
pub fn start_transaction_at(&mut self, now: Instant, cx: &mut ViewContext<Self>) {
|
||||
self.end_selection(cx);
|
||||
if let Some(tx_id) = self
|
||||
.buffer
|
||||
@@ -10391,7 +10413,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn end_transaction_at(
|
||||
pub fn end_transaction_at(
|
||||
&mut self,
|
||||
now: Instant,
|
||||
cx: &mut ViewContext<Self>,
|
||||
@@ -12568,7 +12590,7 @@ impl Editor {
|
||||
|
||||
fn report_editor_event(
|
||||
&self,
|
||||
operation: &'static str,
|
||||
event_type: &'static str,
|
||||
file_extension: Option<String>,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
@@ -12605,15 +12627,14 @@ impl Editor {
|
||||
.show_inline_completions;
|
||||
|
||||
let project = project.read(cx);
|
||||
let telemetry = project.client().telemetry().clone();
|
||||
telemetry.report_editor_event(
|
||||
telemetry::event!(
|
||||
event_type,
|
||||
file_extension,
|
||||
vim_mode,
|
||||
operation,
|
||||
copilot_enabled,
|
||||
copilot_enabled_for_language,
|
||||
project.is_via_ssh(),
|
||||
)
|
||||
is_via_ssh = project.is_via_ssh(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines,
|
||||
@@ -14595,6 +14616,7 @@ pub fn diagnostic_block_renderer(
|
||||
fn inline_completion_edit_text(
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
edits: &Vec<(Range<Anchor>, String)>,
|
||||
include_deletions: bool,
|
||||
cx: &WindowContext,
|
||||
) -> InlineCompletionText {
|
||||
let edit_start = edits
|
||||
@@ -14618,12 +14640,24 @@ fn inline_completion_edit_text(
|
||||
offset = old_offset_range.end;
|
||||
|
||||
let start = text.len();
|
||||
text.push_str(new_text);
|
||||
let color = if include_deletions && new_text.is_empty() {
|
||||
text.extend(
|
||||
editor_snapshot
|
||||
.buffer_snapshot
|
||||
.chunks(old_offset_range.start..offset, false)
|
||||
.map(|chunk| chunk.text),
|
||||
);
|
||||
cx.theme().status().deleted_background
|
||||
} else {
|
||||
text.push_str(new_text);
|
||||
cx.theme().status().created_background
|
||||
};
|
||||
let end = text.len();
|
||||
|
||||
highlights.push((
|
||||
start..end,
|
||||
HighlightStyle {
|
||||
background_color: Some(cx.theme().status().created_background),
|
||||
background_color: Some(color),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
|
||||
@@ -18,6 +18,7 @@ pub struct EditorSettings {
|
||||
pub scroll_beyond_last_line: ScrollBeyondLastLine,
|
||||
pub vertical_scroll_margin: f32,
|
||||
pub autoscroll_on_clicks: bool,
|
||||
pub horizontal_scroll_margin: f32,
|
||||
pub scroll_sensitivity: f32,
|
||||
pub relative_line_numbers: bool,
|
||||
pub seed_search_query_from_cursor: SeedQuerySetting,
|
||||
@@ -34,6 +35,7 @@ pub struct EditorSettings {
|
||||
pub auto_signature_help: bool,
|
||||
pub show_signature_help_after_edits: bool,
|
||||
pub jupyter: Jupyter,
|
||||
pub show_inline_completions_in_menu: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
@@ -105,6 +107,7 @@ pub struct Scrollbar {
|
||||
pub search_results: bool,
|
||||
pub diagnostics: bool,
|
||||
pub cursors: bool,
|
||||
pub axes: ScrollbarAxes,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
@@ -132,6 +135,21 @@ pub enum ShowScrollbar {
|
||||
Never,
|
||||
}
|
||||
|
||||
/// Forcefully enable or disable the scrollbar for each axis
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct ScrollbarAxes {
|
||||
/// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
|
||||
///
|
||||
/// Default: true
|
||||
pub horizontal: bool,
|
||||
|
||||
/// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings.
|
||||
///
|
||||
/// Default: true
|
||||
pub vertical: bool,
|
||||
}
|
||||
|
||||
/// The key to use for adding multiple cursors
|
||||
///
|
||||
/// Default: alt
|
||||
@@ -219,6 +237,10 @@ pub struct EditorSettingsContent {
|
||||
///
|
||||
/// Default: false
|
||||
pub autoscroll_on_clicks: Option<bool>,
|
||||
/// The number of characters to keep on either side when scrolling with the mouse.
|
||||
///
|
||||
/// Default: 5.
|
||||
pub horizontal_scroll_margin: Option<f32>,
|
||||
/// Scroll sensitivity multiplier. This multiplier is applied
|
||||
/// to both the horizontal and vertical delta values while scrolling.
|
||||
///
|
||||
@@ -279,6 +301,12 @@ pub struct EditorSettingsContent {
|
||||
/// Default: false
|
||||
pub show_signature_help_after_edits: Option<bool>,
|
||||
|
||||
/// Whether to show the inline completions next to the completions provided by a language server.
|
||||
/// Only has an effect if inline completion provider supports it.
|
||||
///
|
||||
/// Default: true
|
||||
pub show_inline_completions_in_menu: Option<bool>,
|
||||
|
||||
/// Jupyter REPL settings.
|
||||
pub jupyter: Option<JupyterContent>,
|
||||
}
|
||||
@@ -328,6 +356,22 @@ pub struct ScrollbarContent {
|
||||
///
|
||||
/// Default: true
|
||||
pub cursors: Option<bool>,
|
||||
/// Forcefully enable or disable the scrollbar for each axis
|
||||
pub axes: Option<ScrollbarAxesContent>,
|
||||
}
|
||||
|
||||
/// Forcefully enable or disable the scrollbar for each axis
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct ScrollbarAxesContent {
|
||||
/// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
|
||||
///
|
||||
/// Default: true
|
||||
horizontal: Option<bool>,
|
||||
|
||||
/// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings.
|
||||
///
|
||||
/// Default: true
|
||||
vertical: Option<bool>,
|
||||
}
|
||||
|
||||
/// Gutter related settings
|
||||
|
||||
@@ -14367,7 +14367,7 @@ async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppCon
|
||||
fn test_inline_completion_text(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
// Test case 1: Simple insertion
|
||||
// Simple insertion
|
||||
{
|
||||
let window = cx.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
|
||||
@@ -14383,7 +14383,7 @@ fn test_inline_completion_text(cx: &mut TestAppContext) {
|
||||
let edits = vec![(edit_range, " beautiful".to_string())];
|
||||
|
||||
let InlineCompletionText::Edit { text, highlights } =
|
||||
inline_completion_edit_text(&snapshot, &edits, cx)
|
||||
inline_completion_edit_text(&snapshot, &edits, false, cx)
|
||||
else {
|
||||
panic!("Failed to generate inline completion text");
|
||||
};
|
||||
@@ -14399,7 +14399,7 @@ fn test_inline_completion_text(cx: &mut TestAppContext) {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Test case 2: Replacement
|
||||
// Replacement
|
||||
{
|
||||
let window = cx.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_simple("This is a test.", cx);
|
||||
@@ -14417,7 +14417,7 @@ fn test_inline_completion_text(cx: &mut TestAppContext) {
|
||||
)];
|
||||
|
||||
let InlineCompletionText::Edit { text, highlights } =
|
||||
inline_completion_edit_text(&snapshot, &edits, cx)
|
||||
inline_completion_edit_text(&snapshot, &edits, false, cx)
|
||||
else {
|
||||
panic!("Failed to generate inline completion text");
|
||||
};
|
||||
@@ -14433,7 +14433,7 @@ fn test_inline_completion_text(cx: &mut TestAppContext) {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Test case 3: Multiple edits
|
||||
// Multiple edits
|
||||
{
|
||||
let window = cx.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
|
||||
@@ -14458,7 +14458,7 @@ fn test_inline_completion_text(cx: &mut TestAppContext) {
|
||||
];
|
||||
|
||||
let InlineCompletionText::Edit { text, highlights } =
|
||||
inline_completion_edit_text(&snapshot, &edits, cx)
|
||||
inline_completion_edit_text(&snapshot, &edits, false, cx)
|
||||
else {
|
||||
panic!("Failed to generate inline completion text");
|
||||
};
|
||||
@@ -14479,7 +14479,7 @@ fn test_inline_completion_text(cx: &mut TestAppContext) {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Test case 4: Multiple lines with edits
|
||||
// Multiple lines with edits
|
||||
{
|
||||
let window = cx.add_window(|cx| {
|
||||
let buffer =
|
||||
@@ -14510,7 +14510,7 @@ fn test_inline_completion_text(cx: &mut TestAppContext) {
|
||||
];
|
||||
|
||||
let InlineCompletionText::Edit { text, highlights } =
|
||||
inline_completion_edit_text(&snapshot, &edits, cx)
|
||||
inline_completion_edit_text(&snapshot, &edits, false, cx)
|
||||
else {
|
||||
panic!("Failed to generate inline completion text");
|
||||
};
|
||||
@@ -14532,6 +14532,75 @@ fn test_inline_completion_text(cx: &mut TestAppContext) {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
// Deletion
|
||||
{
|
||||
let window = cx.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
|
||||
window
|
||||
.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 5))
|
||||
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 11));
|
||||
let edits = vec![(edit_range, "".to_string())];
|
||||
|
||||
let InlineCompletionText::Edit { text, highlights } =
|
||||
inline_completion_edit_text(&snapshot, &edits, true, cx)
|
||||
else {
|
||||
panic!("Failed to generate inline completion text");
|
||||
};
|
||||
|
||||
assert_eq!(text, "Hello, world!");
|
||||
assert_eq!(highlights.len(), 1);
|
||||
assert_eq!(highlights[0].0, 5..11);
|
||||
assert_eq!(
|
||||
highlights[0].1.background_color,
|
||||
Some(cx.theme().status().deleted_background)
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Insertion
|
||||
{
|
||||
let window = cx.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
|
||||
window
|
||||
.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 6))
|
||||
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 6));
|
||||
let edits = vec![(edit_range, " digital".to_string())];
|
||||
|
||||
let InlineCompletionText::Edit { text, highlights } =
|
||||
inline_completion_edit_text(&snapshot, &edits, true, cx)
|
||||
else {
|
||||
panic!("Failed to generate inline completion text");
|
||||
};
|
||||
|
||||
assert_eq!(text, "Hello, digital world!");
|
||||
assert_eq!(highlights.len(), 1);
|
||||
assert_eq!(highlights[0].0, 6..14);
|
||||
assert_eq!(
|
||||
highlights[0].1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
|
||||
point..point
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -321,6 +321,10 @@ impl InlineCompletionProvider for FakeInlineCompletionProvider {
|
||||
"Fake Completion Provider"
|
||||
}
|
||||
|
||||
fn show_completions_in_menu() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_enabled(
|
||||
&self,
|
||||
_buffer: &gpui::Model<language::Buffer>,
|
||||
|
||||
@@ -733,7 +733,7 @@ impl Item for Editor {
|
||||
project: Model<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.report_editor_event("save", None, cx);
|
||||
self.report_editor_event("Editor Saved", None, cx);
|
||||
let buffers = self.buffer().clone().read(cx).all_buffers();
|
||||
let buffers = buffers
|
||||
.into_iter()
|
||||
@@ -805,7 +805,7 @@ impl Item for Editor {
|
||||
.path
|
||||
.extension()
|
||||
.map(|a| a.to_string_lossy().to_string());
|
||||
self.report_editor_event("save", file_extension, cx);
|
||||
self.report_editor_event("Editor Saved", file_extension, cx);
|
||||
|
||||
project.update(cx, |project, cx| project.save_buffer_as(buffer, path, cx))
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ mod actions;
|
||||
pub(crate) mod autoscroll;
|
||||
pub(crate) mod scroll_amount;
|
||||
|
||||
use crate::editor_settings::ScrollBeyondLastLine;
|
||||
use crate::editor_settings::{ScrollBeyondLastLine, ScrollbarAxes};
|
||||
use crate::{
|
||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||
hover_popover::hide_hover,
|
||||
@@ -11,7 +11,10 @@ use crate::{
|
||||
InlayHintRefreshReason, MultiBufferSnapshot, RowExt, ToPoint,
|
||||
};
|
||||
pub use autoscroll::{Autoscroll, AutoscrollStrategy};
|
||||
use gpui::{point, px, AppContext, Entity, Global, Pixels, Task, ViewContext, WindowContext};
|
||||
use core::fmt::Debug;
|
||||
use gpui::{
|
||||
point, px, Along, AppContext, Axis, Entity, Global, Pixels, Task, ViewContext, WindowContext,
|
||||
};
|
||||
use language::{Bias, Point};
|
||||
pub use scroll_amount::ScrollAmount;
|
||||
use settings::Settings;
|
||||
@@ -60,10 +63,53 @@ impl ScrollAnchor {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum Axis {
|
||||
Vertical,
|
||||
Horizontal,
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AxisPair<T: Clone> {
|
||||
pub vertical: T,
|
||||
pub horizontal: T,
|
||||
}
|
||||
|
||||
pub fn axis_pair<T: Clone>(horizontal: T, vertical: T) -> AxisPair<T> {
|
||||
AxisPair {
|
||||
vertical,
|
||||
horizontal,
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> AxisPair<T> {
|
||||
pub fn as_xy(&self) -> (&T, &T) {
|
||||
(&self.horizontal, &self.vertical)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Along for AxisPair<T> {
|
||||
type Unit = T;
|
||||
|
||||
fn along(&self, axis: gpui::Axis) -> Self::Unit {
|
||||
match axis {
|
||||
gpui::Axis::Horizontal => self.horizontal.clone(),
|
||||
gpui::Axis::Vertical => self.vertical.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_along(&self, axis: gpui::Axis, f: impl FnOnce(Self::Unit) -> Self::Unit) -> Self {
|
||||
match axis {
|
||||
gpui::Axis::Horizontal => Self {
|
||||
horizontal: f(self.horizontal.clone()),
|
||||
vertical: self.vertical.clone(),
|
||||
},
|
||||
gpui::Axis::Vertical => Self {
|
||||
horizontal: self.horizontal.clone(),
|
||||
vertical: f(self.vertical.clone()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ScrollbarAxes> for AxisPair<bool> {
|
||||
fn from(value: ScrollbarAxes) -> Self {
|
||||
axis_pair(value.horizontal, value.vertical)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -136,7 +182,7 @@ pub struct ScrollManager {
|
||||
last_autoscroll: Option<(gpui::Point<f32>, f32, f32, AutoscrollStrategy)>,
|
||||
show_scrollbars: bool,
|
||||
hide_scrollbar_task: Option<Task<()>>,
|
||||
dragging_scrollbar: bool,
|
||||
dragging_scrollbar: AxisPair<bool>,
|
||||
visible_line_count: Option<f32>,
|
||||
forbid_vertical_scroll: bool,
|
||||
}
|
||||
@@ -150,7 +196,7 @@ impl ScrollManager {
|
||||
autoscroll_request: None,
|
||||
show_scrollbars: true,
|
||||
hide_scrollbar_task: None,
|
||||
dragging_scrollbar: false,
|
||||
dragging_scrollbar: axis_pair(false, false),
|
||||
last_autoscroll: None,
|
||||
visible_line_count: None,
|
||||
forbid_vertical_scroll: false,
|
||||
@@ -311,15 +357,18 @@ impl ScrollManager {
|
||||
self.autoscroll_request.map(|(autoscroll, _)| autoscroll)
|
||||
}
|
||||
|
||||
pub fn is_dragging_scrollbar(&self) -> bool {
|
||||
self.dragging_scrollbar
|
||||
pub fn is_dragging_scrollbar(&self, axis: Axis) -> bool {
|
||||
self.dragging_scrollbar.along(axis)
|
||||
}
|
||||
|
||||
pub fn set_is_dragging_scrollbar(&mut self, dragging: bool, cx: &mut ViewContext<Editor>) {
|
||||
if dragging != self.dragging_scrollbar {
|
||||
self.dragging_scrollbar = dragging;
|
||||
cx.notify();
|
||||
}
|
||||
pub fn set_is_dragging_scrollbar(
|
||||
&mut self,
|
||||
axis: Axis,
|
||||
dragging: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.dragging_scrollbar = self.dragging_scrollbar.apply_along(axis, |_| dragging);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn clamp_scroll_left(&mut self, max: f32) -> bool {
|
||||
|
||||
@@ -391,7 +391,7 @@ impl SelectionsCollection {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn change_with<R>(
|
||||
pub fn change_with<R>(
|
||||
&mut self,
|
||||
cx: &mut AppContext,
|
||||
change: impl FnOnce(&mut MutableSelectionsCollection) -> R,
|
||||
@@ -764,7 +764,7 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
|
||||
pub fn replace_cursors_with(
|
||||
&mut self,
|
||||
mut find_replacement_cursors: impl FnMut(&DisplaySnapshot) -> Vec<DisplayPoint>,
|
||||
find_replacement_cursors: impl FnOnce(&DisplaySnapshot) -> Vec<DisplayPoint>,
|
||||
) {
|
||||
let display_map = self.display_map();
|
||||
let new_selections = find_replacement_cursors(&display_map)
|
||||
|
||||
@@ -43,6 +43,7 @@ serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
task.workspace = true
|
||||
telemetry.workspace = true
|
||||
tempfile.workspace = true
|
||||
toml.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
@@ -1001,14 +1001,13 @@ impl ExtensionStore {
|
||||
extensions_to_unload.len() - reload_count
|
||||
);
|
||||
|
||||
if let Some(telemetry) = &self.telemetry {
|
||||
for extension_id in &extensions_to_load {
|
||||
if let Some(extension) = new_index.extensions.get(extension_id) {
|
||||
telemetry.report_extension_event(
|
||||
extension_id.clone(),
|
||||
extension.manifest.version.clone(),
|
||||
);
|
||||
}
|
||||
for extension_id in &extensions_to_load {
|
||||
if let Some(extension) = new_index.extensions.get(extension_id) {
|
||||
telemetry::event!(
|
||||
"Extension Loaded",
|
||||
extension_id,
|
||||
version = extension.manifest.version
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ semantic_version.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
telemetry.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -48,7 +48,8 @@ impl RenderOnce for ExtensionCard {
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.occlude()
|
||||
.block_mouse_down()
|
||||
.cursor_default()
|
||||
.size_full()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::telemetry::Telemetry;
|
||||
use gpui::{AnyElement, Div, StyleRefinement};
|
||||
use smallvec::SmallVec;
|
||||
use ui::{prelude::*, ButtonLike};
|
||||
@@ -8,17 +5,15 @@ use ui::{prelude::*, ButtonLike};
|
||||
#[derive(IntoElement)]
|
||||
pub struct FeatureUpsell {
|
||||
base: Div,
|
||||
telemetry: Arc<Telemetry>,
|
||||
text: SharedString,
|
||||
docs_url: Option<SharedString>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl FeatureUpsell {
|
||||
pub fn new(telemetry: Arc<Telemetry>, text: impl Into<SharedString>) -> Self {
|
||||
pub fn new(text: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
base: h_flex(),
|
||||
telemetry,
|
||||
text: text.into(),
|
||||
docs_url: None,
|
||||
children: SmallVec::new(),
|
||||
@@ -67,12 +62,13 @@ impl RenderOnce for FeatureUpsell {
|
||||
.child(Icon::new(IconName::ArrowUpRight)),
|
||||
)
|
||||
.on_click({
|
||||
let telemetry = self.telemetry.clone();
|
||||
let docs_url = docs_url.clone();
|
||||
move |_event, cx| {
|
||||
telemetry.report_app_event(format!(
|
||||
"feature upsell: viewed docs ({docs_url})"
|
||||
));
|
||||
telemetry::event!(
|
||||
"Documentation Viewed",
|
||||
source = "Feature Upsell",
|
||||
url = docs_url,
|
||||
);
|
||||
cx.open_url(&docs_url)
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -7,7 +7,6 @@ use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use client::telemetry::Telemetry;
|
||||
use client::ExtensionMetadata;
|
||||
use collections::{BTreeMap, BTreeSet};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
@@ -182,7 +181,6 @@ fn keywords_by_feature() -> &'static BTreeMap<Feature, Vec<&'static str>> {
|
||||
pub struct ExtensionsPage {
|
||||
workspace: WeakView<Workspace>,
|
||||
list: UniformListScrollHandle,
|
||||
telemetry: Arc<Telemetry>,
|
||||
is_fetching_extensions: bool,
|
||||
filter: ExtensionFilter,
|
||||
remote_extension_entries: Vec<ExtensionMetadata>,
|
||||
@@ -221,7 +219,6 @@ impl ExtensionsPage {
|
||||
let mut this = Self {
|
||||
workspace: workspace.weak_handle(),
|
||||
list: UniformListScrollHandle::new(),
|
||||
telemetry: workspace.client().telemetry().clone(),
|
||||
is_fetching_extensions: false,
|
||||
filter: ExtensionFilter::All,
|
||||
dev_extension_entries: Vec::new(),
|
||||
@@ -704,18 +701,15 @@ impl ExtensionsPage {
|
||||
|
||||
match status.clone() {
|
||||
ExtensionStatus::NotInstalled => (
|
||||
Button::new(SharedString::from(extension.id.clone()), "Install").on_click(
|
||||
cx.listener({
|
||||
let extension_id = extension.id.clone();
|
||||
move |this, _, cx| {
|
||||
this.telemetry
|
||||
.report_app_event("extensions: install extension".to_string());
|
||||
ExtensionStore::global(cx).update(cx, |store, cx| {
|
||||
store.install_latest_extension(extension_id.clone(), cx)
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
Button::new(SharedString::from(extension.id.clone()), "Install").on_click({
|
||||
let extension_id = extension.id.clone();
|
||||
move |_, cx| {
|
||||
telemetry::event!("Extension Installed");
|
||||
ExtensionStore::global(cx).update(cx, |store, cx| {
|
||||
store.install_latest_extension(extension_id.clone(), cx)
|
||||
});
|
||||
}
|
||||
}),
|
||||
None,
|
||||
),
|
||||
ExtensionStatus::Installing => (
|
||||
@@ -729,18 +723,15 @@ impl ExtensionsPage {
|
||||
),
|
||||
),
|
||||
ExtensionStatus::Installed(installed_version) => (
|
||||
Button::new(SharedString::from(extension.id.clone()), "Uninstall").on_click(
|
||||
cx.listener({
|
||||
let extension_id = extension.id.clone();
|
||||
move |this, _, cx| {
|
||||
this.telemetry
|
||||
.report_app_event("extensions: uninstall extension".to_string());
|
||||
ExtensionStore::global(cx).update(cx, |store, cx| {
|
||||
store.uninstall_extension(extension_id.clone(), cx)
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
Button::new(SharedString::from(extension.id.clone()), "Uninstall").on_click({
|
||||
let extension_id = extension.id.clone();
|
||||
move |_, cx| {
|
||||
telemetry::event!("Extension Uninstalled", extension_id);
|
||||
ExtensionStore::global(cx).update(cx, |store, cx| {
|
||||
store.uninstall_extension(extension_id.clone(), cx)
|
||||
});
|
||||
}
|
||||
}),
|
||||
if installed_version == extension.manifest.version {
|
||||
None
|
||||
} else {
|
||||
@@ -760,13 +751,11 @@ impl ExtensionsPage {
|
||||
})
|
||||
})
|
||||
.disabled(!is_compatible)
|
||||
.on_click(cx.listener({
|
||||
.on_click({
|
||||
let extension_id = extension.id.clone();
|
||||
let version = extension.manifest.version.clone();
|
||||
move |this, _, cx| {
|
||||
this.telemetry.report_app_event(
|
||||
"extensions: install extension".to_string(),
|
||||
);
|
||||
move |_, cx| {
|
||||
telemetry::event!("Extension Installed", extension_id, version);
|
||||
ExtensionStore::global(cx).update(cx, |store, cx| {
|
||||
store
|
||||
.upgrade_extension(
|
||||
@@ -777,7 +766,7 @@ impl ExtensionsPage {
|
||||
.detach_and_log_err(cx)
|
||||
});
|
||||
}
|
||||
})),
|
||||
}),
|
||||
)
|
||||
},
|
||||
),
|
||||
@@ -972,19 +961,16 @@ impl ExtensionsPage {
|
||||
let upsells_count = self.upsells.len();
|
||||
|
||||
v_flex().children(self.upsells.iter().enumerate().map(|(ix, feature)| {
|
||||
let telemetry = self.telemetry.clone();
|
||||
let upsell = match feature {
|
||||
Feature::Git => FeatureUpsell::new(
|
||||
telemetry,
|
||||
"Zed comes with basic Git support. More Git features are coming in the future.",
|
||||
)
|
||||
.docs_url("https://zed.dev/docs/git"),
|
||||
Feature::OpenIn => FeatureUpsell::new(
|
||||
telemetry,
|
||||
"Zed supports linking to a source line on GitHub and others.",
|
||||
)
|
||||
.docs_url("https://zed.dev/docs/git#git-integrations"),
|
||||
Feature::Vim => FeatureUpsell::new(telemetry, "Vim support is built-in to Zed!")
|
||||
Feature::Vim => FeatureUpsell::new("Vim support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/vim")
|
||||
.child(CheckboxWithLabel::new(
|
||||
"enable-vim",
|
||||
@@ -995,8 +981,7 @@ impl ExtensionsPage {
|
||||
ui::ToggleState::Unselected
|
||||
},
|
||||
cx.listener(move |this, selection, cx| {
|
||||
this.telemetry
|
||||
.report_app_event("feature upsell: toggle vim".to_string());
|
||||
telemetry::event!("Vim Mode Toggled", source = "Feature Upsell");
|
||||
this.update_settings::<VimModeSetting>(
|
||||
selection,
|
||||
cx,
|
||||
@@ -1004,36 +989,22 @@ impl ExtensionsPage {
|
||||
);
|
||||
}),
|
||||
)),
|
||||
Feature::LanguageBash => {
|
||||
FeatureUpsell::new(telemetry, "Shell support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/bash")
|
||||
}
|
||||
Feature::LanguageC => {
|
||||
FeatureUpsell::new(telemetry, "C support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/c")
|
||||
}
|
||||
Feature::LanguageCpp => {
|
||||
FeatureUpsell::new(telemetry, "C++ support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/cpp")
|
||||
}
|
||||
Feature::LanguageGo => {
|
||||
FeatureUpsell::new(telemetry, "Go support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/go")
|
||||
}
|
||||
Feature::LanguagePython => {
|
||||
FeatureUpsell::new(telemetry, "Python support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/python")
|
||||
}
|
||||
Feature::LanguageReact => {
|
||||
FeatureUpsell::new(telemetry, "React support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/typescript")
|
||||
}
|
||||
Feature::LanguageRust => {
|
||||
FeatureUpsell::new(telemetry, "Rust support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/rust")
|
||||
}
|
||||
Feature::LanguageBash => FeatureUpsell::new("Shell support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/bash"),
|
||||
Feature::LanguageC => FeatureUpsell::new("C support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/c"),
|
||||
Feature::LanguageCpp => FeatureUpsell::new("C++ support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/cpp"),
|
||||
Feature::LanguageGo => FeatureUpsell::new("Go support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/go"),
|
||||
Feature::LanguagePython => FeatureUpsell::new("Python support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/python"),
|
||||
Feature::LanguageReact => FeatureUpsell::new("React support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/typescript"),
|
||||
Feature::LanguageRust => FeatureUpsell::new("Rust support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/rust"),
|
||||
Feature::LanguageTypescript => {
|
||||
FeatureUpsell::new(telemetry, "Typescript support is built-in to Zed!")
|
||||
FeatureUpsell::new("Typescript support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/typescript")
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,13 +21,26 @@ const fn zed_repo_url() -> &'static str {
|
||||
"https://github.com/zed-industries/zed"
|
||||
}
|
||||
|
||||
const fn request_feature_url() -> &'static str {
|
||||
"https://github.com/zed-industries/zed/issues/new?assignees=&labels=admin+read%2Ctriage%2Cenhancement&projects=&template=0_feature_request.yml"
|
||||
fn request_feature_url(specs: &SystemSpecs) -> String {
|
||||
format!(
|
||||
concat!(
|
||||
"https://github.com/zed-industries/zed/issues/new",
|
||||
"?labels=admin+read%2Ctriage%2Cenhancement",
|
||||
"&template=0_feature_request.yml",
|
||||
"&environment={}"
|
||||
),
|
||||
urlencoding::encode(&specs.to_string())
|
||||
)
|
||||
}
|
||||
|
||||
fn file_bug_report_url(specs: &SystemSpecs) -> String {
|
||||
format!(
|
||||
"https://github.com/zed-industries/zed/issues/new?assignees=&labels=admin+read%2Ctriage%2Cbug&projects=&template=1_bug_report.yml&environment={}",
|
||||
concat!(
|
||||
"https://github.com/zed-industries/zed/issues/new",
|
||||
"?labels=admin+read%2Ctriage%2Cbug",
|
||||
"&template=1_bug_report.yml",
|
||||
"&environment={}"
|
||||
),
|
||||
urlencoding::encode(&specs.to_string())
|
||||
)
|
||||
}
|
||||
@@ -57,7 +70,15 @@ pub fn init(cx: &mut AppContext) {
|
||||
.detach();
|
||||
})
|
||||
.register_action(|_, _: &RequestFeature, cx| {
|
||||
cx.open_url(request_feature_url());
|
||||
let specs = SystemSpecs::new(cx);
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let specs = specs.await;
|
||||
cx.update(|cx| {
|
||||
cx.open_url(&request_feature_url(&specs));
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.register_action(move |_, _: &FileBugReport, cx| {
|
||||
let specs = SystemSpecs::new(cx);
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use std::{ops::RangeInclusive, sync::Arc, time::Duration};
|
||||
use std::{
|
||||
ops::RangeInclusive,
|
||||
sync::{Arc, LazyLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail};
|
||||
use bitflags::bitflags;
|
||||
@@ -34,7 +38,8 @@ const DEV_MODE: bool = true;
|
||||
const DEV_MODE: bool = false;
|
||||
|
||||
const DATABASE_KEY_NAME: &str = "email_address";
|
||||
const EMAIL_REGEX: &str = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b";
|
||||
static EMAIL_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b").unwrap());
|
||||
const FEEDBACK_CHAR_LIMIT: RangeInclusive<i32> = 10..=5000;
|
||||
const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
|
||||
"Feedback failed to submit, see error log for details.";
|
||||
@@ -320,7 +325,7 @@ impl FeedbackModal {
|
||||
let mut invalid_state_flags = InvalidStateFlags::empty();
|
||||
|
||||
let valid_email_address = match self.email_address_editor.read(cx).text_option(cx) {
|
||||
Some(email_address) => Regex::new(EMAIL_REGEX).unwrap().is_match(&email_address),
|
||||
Some(email_address) => EMAIL_REGEX.is_match(&email_address),
|
||||
None => true,
|
||||
};
|
||||
|
||||
|
||||
@@ -47,9 +47,12 @@ windows.workspace = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
|
||||
ashpd.workspace = true
|
||||
which.workspace = true
|
||||
shlex.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[features]
|
||||
test-support = ["gpui/test-support", "git/test-support"]
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ use git::GitHostingProviderRegistry;
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
use ashpd::desktop::trash;
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
use smol::process::Command;
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
use std::fs::File;
|
||||
#[cfg(unix)]
|
||||
use std::os::fd::AsFd;
|
||||
@@ -514,24 +516,7 @@ impl Fs for RealFs {
|
||||
|
||||
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
|
||||
smol::unblock(move || {
|
||||
let mut tmp_file = if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
||||
// Use the directory of the destination as temp dir to avoid
|
||||
// invalid cross-device link error, and XDG_CACHE_DIR for fallback.
|
||||
// See https://github.com/zed-industries/zed/pull/8437 for more details.
|
||||
NamedTempFile::new_in(path.parent().unwrap_or(paths::temp_dir()))
|
||||
} else if cfg!(target_os = "windows") {
|
||||
// If temp dir is set to a different drive than the destination,
|
||||
// we receive error:
|
||||
//
|
||||
// failed to persist temporary file:
|
||||
// The system cannot move the file to a different disk drive. (os error 17)
|
||||
//
|
||||
// So we use the directory of the destination as a temp dir to avoid it.
|
||||
// https://github.com/zed-industries/zed/issues/16571
|
||||
NamedTempFile::new_in(path.parent().unwrap_or(paths::temp_dir()))
|
||||
} else {
|
||||
NamedTempFile::new()
|
||||
}?;
|
||||
let mut tmp_file = create_temp_file(&path)?;
|
||||
tmp_file.write_all(data.as_bytes())?;
|
||||
tmp_file.persist(path)?;
|
||||
Ok::<(), anyhow::Error>(())
|
||||
@@ -546,13 +531,43 @@ impl Fs for RealFs {
|
||||
if let Some(path) = path.parent() {
|
||||
self.create_dir(path).await?;
|
||||
}
|
||||
let file = smol::fs::File::create(path).await?;
|
||||
let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
|
||||
for chunk in chunks(text, line_ending) {
|
||||
writer.write_all(chunk.as_bytes()).await?;
|
||||
match smol::fs::File::create(path).await {
|
||||
Ok(file) => {
|
||||
let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
|
||||
for chunk in chunks(text, line_ending) {
|
||||
writer.write_all(chunk.as_bytes()).await?;
|
||||
}
|
||||
writer.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
|
||||
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
||||
let target_path = path.to_path_buf();
|
||||
let temp_file = smol::unblock(move || create_temp_file(&target_path)).await?;
|
||||
|
||||
let temp_path = temp_file.into_temp_path();
|
||||
let temp_path_for_write = temp_path.to_path_buf();
|
||||
|
||||
let async_file = smol::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.open(&temp_path)
|
||||
.await?;
|
||||
|
||||
let mut writer = smol::io::BufWriter::with_capacity(buffer_size, async_file);
|
||||
|
||||
for chunk in chunks(text, line_ending) {
|
||||
writer.write_all(chunk.as_bytes()).await?;
|
||||
}
|
||||
writer.flush().await?;
|
||||
|
||||
write_to_file_as_root(temp_path_for_write, path.to_path_buf()).await
|
||||
} else {
|
||||
// Todo: Implement for Mac and Windows
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
writer.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
|
||||
@@ -1999,6 +2014,84 @@ fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator<Item = &str> {
|
||||
})
|
||||
}
|
||||
|
||||
fn create_temp_file(path: &Path) -> Result<NamedTempFile> {
|
||||
let temp_file = if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
||||
// Use the directory of the destination as temp dir to avoid
|
||||
// invalid cross-device link error, and XDG_CACHE_DIR for fallback.
|
||||
// See https://github.com/zed-industries/zed/pull/8437 for more details.
|
||||
NamedTempFile::new_in(path.parent().unwrap_or(paths::temp_dir()))?
|
||||
} else if cfg!(target_os = "windows") {
|
||||
// If temp dir is set to a different drive than the destination,
|
||||
// we receive error:
|
||||
//
|
||||
// failed to persist temporary file:
|
||||
// The system cannot move the file to a different disk drive. (os error 17)
|
||||
//
|
||||
// So we use the directory of the destination as a temp dir to avoid it.
|
||||
// https://github.com/zed-industries/zed/issues/16571
|
||||
NamedTempFile::new_in(path.parent().unwrap_or(paths::temp_dir()))?
|
||||
} else {
|
||||
NamedTempFile::new()?
|
||||
};
|
||||
|
||||
Ok(temp_file)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn write_to_file_as_root(_temp_file_path: PathBuf, _target_file_path: PathBuf) -> Result<()> {
|
||||
unimplemented!("write_to_file_as_root is not implemented")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn write_to_file_as_root(_temp_file_path: PathBuf, _target_file_path: PathBuf) -> Result<()> {
|
||||
unimplemented!("write_to_file_as_root is not implemented")
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
async fn write_to_file_as_root(temp_file_path: PathBuf, target_file_path: PathBuf) -> Result<()> {
|
||||
use shlex::try_quote;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use which::which;
|
||||
|
||||
let pkexec_path = smol::unblock(|| which("pkexec"))
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("pkexec not found in PATH"))?;
|
||||
|
||||
let script_file = smol::unblock(move || {
|
||||
let script_file = tempfile::Builder::new()
|
||||
.prefix("write-to-file-as-root-")
|
||||
.tempfile_in(paths::temp_dir())?;
|
||||
|
||||
writeln!(
|
||||
script_file.as_file(),
|
||||
"#!/usr/bin/env sh\nset -eu\ncat \"{}\" > \"{}\"",
|
||||
try_quote(&temp_file_path.to_string_lossy())?,
|
||||
try_quote(&target_file_path.to_string_lossy())?
|
||||
)?;
|
||||
|
||||
let mut perms = script_file.as_file().metadata()?.permissions();
|
||||
perms.set_mode(0o700); // rwx------
|
||||
script_file.as_file().set_permissions(perms)?;
|
||||
|
||||
Result::<_>::Ok(script_file)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let script_path = script_file.into_temp_path();
|
||||
|
||||
let output = Command::new(&pkexec_path)
|
||||
.arg("--disable-internal-agent")
|
||||
.arg(&script_path)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow::anyhow!("Failed to write to file as root"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn normalize_path(path: &Path) -> PathBuf {
|
||||
let mut components = path.components().peekable();
|
||||
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use async_trait::async_trait;
|
||||
@@ -15,9 +15,9 @@ use git::{
|
||||
};
|
||||
|
||||
fn pull_request_number_regex() -> &'static Regex {
|
||||
static PULL_REQUEST_NUMBER_REGEX: OnceLock<Regex> = OnceLock::new();
|
||||
|
||||
PULL_REQUEST_NUMBER_REGEX.get_or_init(|| Regex::new(r"\(#(\d+)\)$").unwrap())
|
||||
static PULL_REQUEST_NUMBER_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"\(#(\d+)\)$").unwrap());
|
||||
&PULL_REQUEST_NUMBER_REGEX
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
||||
@@ -136,8 +136,8 @@ font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "40391b7"
|
||||
foreign-types = "0.5"
|
||||
log.workspace = true
|
||||
media.workspace = true
|
||||
metal = "0.29"
|
||||
objc = "0.2"
|
||||
metal.workspace = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))'.dependencies]
|
||||
pathfinder_geometry = "0.5"
|
||||
|
||||
@@ -5,7 +5,10 @@ use std::{
|
||||
ops::{Deref, DerefMut},
|
||||
path::{Path, PathBuf},
|
||||
rc::{Rc, Weak},
|
||||
sync::{atomic::Ordering::SeqCst, Arc},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
@@ -16,6 +19,7 @@ use futures::{
|
||||
future::{LocalBoxFuture, Shared},
|
||||
Future, FutureExt,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use slotmap::SlotMap;
|
||||
|
||||
pub use async_context::*;
|
||||
@@ -30,11 +34,12 @@ use util::ResultExt;
|
||||
use crate::{
|
||||
current_platform, hash, init_app_menus, Action, ActionRegistry, Any, AnyView, AnyWindowHandle,
|
||||
Asset, AssetSource, BackgroundExecutor, ClipboardItem, Context, DispatchPhase, DisplayId,
|
||||
Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap, Keystroke, LayoutId,
|
||||
Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point,
|
||||
PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, Reservation,
|
||||
ScreenCaptureSource, SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem,
|
||||
View, ViewContext, Window, WindowAppearance, WindowContext, WindowHandle, WindowId,
|
||||
Entity, EventEmitter, FocusHandle, FocusId, ForegroundExecutor, Global, KeyBinding, Keymap,
|
||||
Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
|
||||
PlatformDisplay, Point, PromptBuilder, PromptHandle, PromptLevel, Render,
|
||||
RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString, SubscriberSet,
|
||||
Subscription, SvgRenderer, Task, TextSystem, View, ViewContext, Window, WindowAppearance,
|
||||
WindowContext, WindowHandle, WindowId,
|
||||
};
|
||||
|
||||
mod async_context;
|
||||
@@ -242,6 +247,7 @@ pub struct AppContext {
|
||||
pub(crate) new_view_observers: SubscriberSet<TypeId, NewViewListener>,
|
||||
pub(crate) windows: SlotMap<WindowId, Option<Window>>,
|
||||
pub(crate) window_handles: FxHashMap<WindowId, AnyWindowHandle>,
|
||||
pub(crate) focus_handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
|
||||
pub(crate) keymap: Rc<RefCell<Keymap>>,
|
||||
pub(crate) keyboard_layout: SharedString,
|
||||
pub(crate) global_action_listeners:
|
||||
@@ -302,8 +308,9 @@ impl AppContext {
|
||||
entities,
|
||||
new_view_observers: SubscriberSet::new(),
|
||||
new_model_observers: SubscriberSet::new(),
|
||||
window_handles: FxHashMap::default(),
|
||||
windows: SlotMap::with_key(),
|
||||
window_handles: FxHashMap::default(),
|
||||
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
|
||||
keymap: Rc::new(RefCell::new(Keymap::default())),
|
||||
keyboard_layout,
|
||||
global_action_listeners: FxHashMap::default(),
|
||||
@@ -439,6 +446,7 @@ impl AppContext {
|
||||
self.defer(move |_| activate());
|
||||
subscription
|
||||
}
|
||||
|
||||
pub(crate) fn observe_internal<W, E>(
|
||||
&mut self,
|
||||
entity: &E,
|
||||
@@ -569,6 +577,12 @@ impl AppContext {
|
||||
})
|
||||
}
|
||||
|
||||
/// Obtain a new [`FocusHandle`], which allows you to track and manipulate the keyboard focus
|
||||
/// for elements rendered within this window.
|
||||
pub fn focus_handle(&self) -> FocusHandle {
|
||||
FocusHandle::new(&self.focus_handles)
|
||||
}
|
||||
|
||||
/// Instructs the platform to activate the application by bringing it to the foreground.
|
||||
pub fn activate(&self, ignoring_other_apps: bool) {
|
||||
self.platform.activate(ignoring_other_apps);
|
||||
@@ -844,28 +858,25 @@ impl AppContext {
|
||||
|
||||
/// Repeatedly called during `flush_effects` to handle a focused handle being dropped.
|
||||
fn release_dropped_focus_handles(&mut self) {
|
||||
for window_handle in self.windows() {
|
||||
window_handle
|
||||
.update(self, |_, cx| {
|
||||
let mut blur_window = false;
|
||||
let focus = cx.window.focus;
|
||||
cx.window.focus_handles.write().retain(|handle_id, count| {
|
||||
if count.load(SeqCst) == 0 {
|
||||
if focus == Some(handle_id) {
|
||||
blur_window = true;
|
||||
}
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
if blur_window {
|
||||
cx.blur();
|
||||
self.focus_handles
|
||||
.clone()
|
||||
.write()
|
||||
.retain(|handle_id, count| {
|
||||
if count.load(SeqCst) == 0 {
|
||||
for window_handle in self.windows() {
|
||||
window_handle
|
||||
.update(self, |_, cx| {
|
||||
if cx.window.focus == Some(handle_id) {
|
||||
cx.blur();
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn apply_notify_effect(&mut self, emitter: EntityId) {
|
||||
|
||||
@@ -500,7 +500,7 @@ impl AnyElement {
|
||||
|
||||
if !focus_assigned {
|
||||
if let Some(focus_id) = cx.window.next_frame.focus {
|
||||
return FocusHandle::for_id(focus_id, &cx.window.focus_handles);
|
||||
return FocusHandle::for_id(focus_id, &cx.focus_handles);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -815,14 +815,8 @@ where
|
||||
Bounds { origin, size }
|
||||
}
|
||||
|
||||
/// Constructs a `Bounds` from a corner point and size.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use zed::{Bounds, Corner, Point};
|
||||
/// todo!
|
||||
/// ```
|
||||
/// Constructs a `Bounds` from a corner point and size. The specified corner will be placed at
|
||||
/// the specified origin.
|
||||
pub fn from_corner_and_size(corner: Corner, origin: Point<T>, size: Size<T>) -> Bounds<T> {
|
||||
let origin = match corner {
|
||||
Corner::TopLeft => origin,
|
||||
@@ -1003,6 +997,18 @@ where
|
||||
size: self.size.clone() + size(double_amount.clone(), double_amount),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extends the bounds different amounts in each direction.
|
||||
pub fn extend(&self, amount: Edges<T>) -> Bounds<T> {
|
||||
Bounds {
|
||||
origin: self.origin.clone() - point(amount.left.clone(), amount.top.clone()),
|
||||
size: self.size.clone()
|
||||
+ size(
|
||||
amount.left.clone() + amount.right.clone(),
|
||||
amount.top.clone() + amount.bottom.clone(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Bounds<T>
|
||||
@@ -1097,6 +1103,21 @@ impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output =
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Bounds<T>
|
||||
where
|
||||
T: Clone + Debug + Add<T, Output = T> + Sub<T, Output = T> + Default,
|
||||
{
|
||||
/// Computes the space available within outer bounds.
|
||||
pub fn space_within(&self, outer: &Self) -> Edges<T> {
|
||||
Edges {
|
||||
top: self.top().clone() - outer.top().clone(),
|
||||
right: outer.right().clone() - self.right().clone(),
|
||||
bottom: outer.bottom().clone() - self.bottom().clone(),
|
||||
left: self.left().clone() - outer.left().clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Rhs> Mul<Rhs> for Bounds<T>
|
||||
where
|
||||
T: Mul<Rhs, Output = Rhs> + Clone + Default + Debug,
|
||||
|
||||
@@ -344,15 +344,15 @@ impl<T> Flatten<T> for Result<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about the GPU GPUI is running on.
|
||||
#[derive(Default, Debug)]
|
||||
/// Information about the GPU GPUI is running on
|
||||
pub struct GPUSpecs {
|
||||
/// true if the GPU is really a fake (like llvmpipe) running on the CPU
|
||||
pub struct GpuSpecs {
|
||||
/// Whether the GPU is really a fake (like `llvmpipe`) running on the CPU.
|
||||
pub is_software_emulated: bool,
|
||||
/// Name of the device as reported by vulkan
|
||||
/// The name of the device, as reported by Vulkan.
|
||||
pub device_name: String,
|
||||
/// Name of the driver as reported by vulkan
|
||||
/// The name of the driver, as reported by Vulkan.
|
||||
pub driver_name: String,
|
||||
/// Further driver info as reported by vulkan
|
||||
/// Further information about the driver, as reported by Vulkan.
|
||||
pub driver_info: String,
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ mod windows;
|
||||
|
||||
use crate::{
|
||||
point, Action, AnyWindowHandle, AsyncWindowContext, BackgroundExecutor, Bounds, DevicePixels,
|
||||
DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GPUSpecs, GlyphId,
|
||||
DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GlyphId, GpuSpecs,
|
||||
ImageSource, Keymap, LineLayout, Pixels, PlatformInput, Point, RenderGlyphParams, RenderImage,
|
||||
RenderImageParams, RenderSvgParams, ScaledPixels, Scene, SharedString, Size, SvgRenderer,
|
||||
SvgSize, Task, TaskLabel, WindowContext, DEFAULT_WINDOW_SIZE,
|
||||
@@ -432,7 +432,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
WindowControls::default()
|
||||
}
|
||||
fn set_client_inset(&self, _inset: Pixels) {}
|
||||
fn gpu_specs(&self) -> Option<GPUSpecs>;
|
||||
fn gpu_specs(&self) -> Option<GpuSpecs>;
|
||||
|
||||
fn update_ime_position(&self, _bounds: Bounds<ScaledPixels>);
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
mod apple_compat;
|
||||
mod blade_atlas;
|
||||
mod blade_context;
|
||||
mod blade_renderer;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) use apple_compat::*;
|
||||
pub(crate) use blade_atlas::*;
|
||||
pub(crate) use blade_context::*;
|
||||
pub(crate) use blade_renderer::*;
|
||||
|
||||
60
crates/gpui/src/platform/blade/apple_compat.rs
Normal file
60
crates/gpui/src/platform/blade/apple_compat.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use super::{BladeContext, BladeRenderer, BladeSurfaceConfig};
|
||||
use blade_graphics as gpu;
|
||||
use std::{ffi::c_void, ptr::NonNull};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Context {
|
||||
inner: BladeContext,
|
||||
}
|
||||
impl Default for Context {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inner: BladeContext::new().unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Renderer = BladeRenderer;
|
||||
|
||||
pub unsafe fn new_renderer(
|
||||
context: Context,
|
||||
_native_window: *mut c_void,
|
||||
native_view: *mut c_void,
|
||||
bounds: crate::Size<f32>,
|
||||
transparent: bool,
|
||||
) -> Renderer {
|
||||
use raw_window_handle as rwh;
|
||||
struct RawWindow {
|
||||
view: *mut c_void,
|
||||
}
|
||||
|
||||
impl rwh::HasWindowHandle for RawWindow {
|
||||
fn window_handle(&self) -> Result<rwh::WindowHandle, rwh::HandleError> {
|
||||
let view = NonNull::new(self.view).unwrap();
|
||||
let handle = rwh::AppKitWindowHandle::new(view);
|
||||
Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) })
|
||||
}
|
||||
}
|
||||
impl rwh::HasDisplayHandle for RawWindow {
|
||||
fn display_handle(&self) -> Result<rwh::DisplayHandle, rwh::HandleError> {
|
||||
let handle = rwh::AppKitDisplayHandle::new();
|
||||
Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) })
|
||||
}
|
||||
}
|
||||
|
||||
BladeRenderer::new(
|
||||
&context.inner,
|
||||
&RawWindow {
|
||||
view: native_view as *mut _,
|
||||
},
|
||||
BladeSurfaceConfig {
|
||||
size: gpu::Extent {
|
||||
width: bounds.width as u32,
|
||||
height: bounds.height as u32,
|
||||
depth: 1,
|
||||
},
|
||||
transparent,
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
@@ -268,7 +268,7 @@ impl BladeAtlasState {
|
||||
fn flush(&mut self, encoder: &mut gpu::CommandEncoder) {
|
||||
self.flush_initializations(encoder);
|
||||
|
||||
let mut transfers = encoder.transfer();
|
||||
let mut transfers = encoder.transfer("atlas");
|
||||
for upload in self.uploads.drain(..) {
|
||||
let texture = &self.storage[upload.id];
|
||||
transfers.copy_buffer_to_texture(
|
||||
|
||||
24
crates/gpui/src/platform/blade/blade_context.rs
Normal file
24
crates/gpui/src/platform/blade/blade_context.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use blade_graphics as gpu;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg_attr(target_os = "macos", derive(Clone))]
|
||||
pub struct BladeContext {
|
||||
pub(super) gpu: Arc<gpu::Context>,
|
||||
}
|
||||
|
||||
impl BladeContext {
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
let gpu = Arc::new(
|
||||
unsafe {
|
||||
gpu::Context::init(gpu::ContextDesc {
|
||||
presentation: true,
|
||||
validation: false,
|
||||
device_id: 0, //TODO: hook up to user settings
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
.map_err(|e| anyhow::anyhow!("{:?}", e))?,
|
||||
);
|
||||
Ok(Self { gpu })
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
// Doing `if let` gives you nice scoping with passes/encoders
|
||||
#![allow(irrefutable_let_patterns)]
|
||||
|
||||
use super::{BladeAtlas, PATH_TEXTURE_FORMAT};
|
||||
use super::{BladeAtlas, BladeContext, PATH_TEXTURE_FORMAT};
|
||||
use crate::{
|
||||
AtlasTextureKind, AtlasTile, Background, Bounds, ContentMask, DevicePixels, GPUSpecs,
|
||||
AtlasTextureKind, AtlasTile, Background, Bounds, ContentMask, DevicePixels, GpuSpecs,
|
||||
MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad,
|
||||
ScaledPixels, Scene, Shadow, Size, Underline,
|
||||
};
|
||||
@@ -11,8 +11,6 @@ use bytemuck::{Pod, Zeroable};
|
||||
use collections::HashMap;
|
||||
#[cfg(target_os = "macos")]
|
||||
use media::core_video::CVMetalTextureCache;
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::{ffi::c_void, ptr::NonNull};
|
||||
|
||||
use blade_graphics as gpu;
|
||||
use blade_util::{BufferBelt, BufferBeltDescriptor};
|
||||
@@ -20,66 +18,6 @@ use std::{mem, sync::Arc};
|
||||
|
||||
const MAX_FRAME_TIME_MS: u32 = 10000;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Context {}
|
||||
#[cfg(target_os = "macos")]
|
||||
pub type Renderer = BladeRenderer;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub unsafe fn new_renderer(
|
||||
_context: self::Context,
|
||||
_native_window: *mut c_void,
|
||||
native_view: *mut c_void,
|
||||
bounds: crate::Size<f32>,
|
||||
transparent: bool,
|
||||
) -> Renderer {
|
||||
use raw_window_handle as rwh;
|
||||
struct RawWindow {
|
||||
view: *mut c_void,
|
||||
}
|
||||
|
||||
impl rwh::HasWindowHandle for RawWindow {
|
||||
fn window_handle(&self) -> Result<rwh::WindowHandle, rwh::HandleError> {
|
||||
let view = NonNull::new(self.view).unwrap();
|
||||
let handle = rwh::AppKitWindowHandle::new(view);
|
||||
Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) })
|
||||
}
|
||||
}
|
||||
impl rwh::HasDisplayHandle for RawWindow {
|
||||
fn display_handle(&self) -> Result<rwh::DisplayHandle, rwh::HandleError> {
|
||||
let handle = rwh::AppKitDisplayHandle::new();
|
||||
Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) })
|
||||
}
|
||||
}
|
||||
|
||||
let gpu = Arc::new(
|
||||
gpu::Context::init_windowed(
|
||||
&RawWindow {
|
||||
view: native_view as *mut _,
|
||||
},
|
||||
gpu::ContextDesc {
|
||||
validation: cfg!(debug_assertions),
|
||||
capture: false,
|
||||
overlay: false,
|
||||
},
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
BladeRenderer::new(
|
||||
gpu,
|
||||
BladeSurfaceConfig {
|
||||
size: gpu::Extent {
|
||||
width: bounds.width as u32,
|
||||
height: bounds.height as u32,
|
||||
depth: 1,
|
||||
},
|
||||
transparent,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Pod, Zeroable)]
|
||||
struct GlobalParams {
|
||||
@@ -354,10 +292,14 @@ pub struct BladeSurfaceConfig {
|
||||
pub transparent: bool,
|
||||
}
|
||||
|
||||
//Note: we could see some of these fields moved into `BladeContext`
|
||||
// so that they are shared between windows. E.g. `pipelines`.
|
||||
// But that is complicated by the fact that pipelines depend on
|
||||
// the format and alpha mode.
|
||||
pub struct BladeRenderer {
|
||||
gpu: Arc<gpu::Context>,
|
||||
surface: gpu::Surface,
|
||||
surface_config: gpu::SurfaceConfig,
|
||||
alpha_mode: gpu::AlphaMode,
|
||||
command_encoder: gpu::CommandEncoder,
|
||||
last_sync_point: Option<gpu::SyncPoint>,
|
||||
pipelines: BladePipelines,
|
||||
@@ -370,7 +312,11 @@ pub struct BladeRenderer {
|
||||
}
|
||||
|
||||
impl BladeRenderer {
|
||||
pub fn new(gpu: Arc<gpu::Context>, config: BladeSurfaceConfig) -> Self {
|
||||
pub fn new<I: raw_window_handle::HasWindowHandle + raw_window_handle::HasDisplayHandle>(
|
||||
context: &BladeContext,
|
||||
window: &I,
|
||||
config: BladeSurfaceConfig,
|
||||
) -> anyhow::Result<Self> {
|
||||
let surface_config = gpu::SurfaceConfig {
|
||||
size: config.size,
|
||||
usage: gpu::TextureUsage::TARGET,
|
||||
@@ -379,20 +325,23 @@ impl BladeRenderer {
|
||||
allow_exclusive_full_screen: false,
|
||||
transparent: config.transparent,
|
||||
};
|
||||
let surface_info = gpu.resize(surface_config);
|
||||
let surface = context
|
||||
.gpu
|
||||
.create_surface_configured(window, surface_config)
|
||||
.unwrap();
|
||||
|
||||
let command_encoder = gpu.create_command_encoder(gpu::CommandEncoderDesc {
|
||||
let command_encoder = context.gpu.create_command_encoder(gpu::CommandEncoderDesc {
|
||||
name: "main",
|
||||
buffer_count: 2,
|
||||
});
|
||||
let pipelines = BladePipelines::new(&gpu, surface_info);
|
||||
let pipelines = BladePipelines::new(&context.gpu, surface.info());
|
||||
let instance_belt = BufferBelt::new(BufferBeltDescriptor {
|
||||
memory: gpu::Memory::Shared,
|
||||
min_chunk_size: 0x1000,
|
||||
alignment: 0x40, // Vulkan `minStorageBufferOffsetAlignment` on Intel Xe
|
||||
});
|
||||
let atlas = Arc::new(BladeAtlas::new(&gpu));
|
||||
let atlas_sampler = gpu.create_sampler(gpu::SamplerDesc {
|
||||
let atlas = Arc::new(BladeAtlas::new(&context.gpu));
|
||||
let atlas_sampler = context.gpu.create_sampler(gpu::SamplerDesc {
|
||||
name: "atlas",
|
||||
mag_filter: gpu::FilterMode::Linear,
|
||||
min_filter: gpu::FilterMode::Linear,
|
||||
@@ -402,13 +351,13 @@ impl BladeRenderer {
|
||||
#[cfg(target_os = "macos")]
|
||||
let core_video_texture_cache = unsafe {
|
||||
use foreign_types::ForeignType as _;
|
||||
CVMetalTextureCache::new(gpu.metal_device().as_ptr()).unwrap()
|
||||
CVMetalTextureCache::new(context.gpu.metal_device().as_ptr()).unwrap()
|
||||
};
|
||||
|
||||
Self {
|
||||
gpu,
|
||||
Ok(Self {
|
||||
gpu: Arc::clone(&context.gpu),
|
||||
surface,
|
||||
surface_config,
|
||||
alpha_mode: surface_info.alpha,
|
||||
command_encoder,
|
||||
last_sync_point: None,
|
||||
pipelines,
|
||||
@@ -418,7 +367,7 @@ impl BladeRenderer {
|
||||
atlas_sampler,
|
||||
#[cfg(target_os = "macos")]
|
||||
core_video_texture_cache,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn wait_for_gpu(&mut self) {
|
||||
@@ -452,7 +401,8 @@ impl BladeRenderer {
|
||||
if always_resize || gpu_size != self.surface_config.size {
|
||||
self.wait_for_gpu();
|
||||
self.surface_config.size = gpu_size;
|
||||
self.gpu.resize(self.surface_config);
|
||||
self.gpu
|
||||
.reconfigure_surface(&mut self.surface, self.surface_config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,10 +410,10 @@ impl BladeRenderer {
|
||||
if transparent != self.surface_config.transparent {
|
||||
self.wait_for_gpu();
|
||||
self.surface_config.transparent = transparent;
|
||||
let surface_info = self.gpu.resize(self.surface_config);
|
||||
self.gpu
|
||||
.reconfigure_surface(&mut self.surface, self.surface_config);
|
||||
self.pipelines.destroy(&self.gpu);
|
||||
self.pipelines = BladePipelines::new(&self.gpu, surface_info);
|
||||
self.alpha_mode = surface_info.alpha;
|
||||
self.pipelines = BladePipelines::new(&self.gpu, self.surface.info());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,10 +427,10 @@ impl BladeRenderer {
|
||||
}
|
||||
|
||||
#[cfg_attr(target_os = "macos", allow(dead_code))]
|
||||
pub fn gpu_specs(&self) -> GPUSpecs {
|
||||
pub fn gpu_specs(&self) -> GpuSpecs {
|
||||
let info = self.gpu.device_information();
|
||||
|
||||
GPUSpecs {
|
||||
GpuSpecs {
|
||||
is_software_emulated: info.is_software_emulated,
|
||||
device_name: info.device_name.clone(),
|
||||
driver_name: info.driver_name.clone(),
|
||||
@@ -490,13 +440,13 @@ impl BladeRenderer {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn layer(&self) -> metal::MetalLayer {
|
||||
self.gpu.metal_layer().unwrap()
|
||||
self.surface.metal_layer()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn layer_ptr(&self) -> *mut metal::CAMetalLayer {
|
||||
use metal::foreign_types::ForeignType as _;
|
||||
self.gpu.metal_layer().unwrap().as_ptr()
|
||||
self.surface.metal_layer().as_ptr()
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
@@ -538,14 +488,17 @@ impl BladeRenderer {
|
||||
};
|
||||
|
||||
let vertex_buf = unsafe { self.instance_belt.alloc_typed(&vertices, &self.gpu) };
|
||||
let mut pass = self.command_encoder.render(gpu::RenderTargetSet {
|
||||
colors: &[gpu::RenderTarget {
|
||||
view: tex_info.raw_view,
|
||||
init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack),
|
||||
finish_op: gpu::FinishOp::Store,
|
||||
}],
|
||||
depth_stencil: None,
|
||||
});
|
||||
let mut pass = self.command_encoder.render(
|
||||
"paths",
|
||||
gpu::RenderTargetSet {
|
||||
colors: &[gpu::RenderTarget {
|
||||
view: tex_info.raw_view,
|
||||
init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack),
|
||||
finish_op: gpu::FinishOp::Store,
|
||||
}],
|
||||
depth_stencil: None,
|
||||
},
|
||||
);
|
||||
|
||||
let mut encoder = pass.with(&self.pipelines.path_rasterization);
|
||||
encoder.bind(
|
||||
@@ -566,6 +519,7 @@ impl BladeRenderer {
|
||||
self.instance_belt.destroy(&self.gpu);
|
||||
self.gpu.destroy_command_encoder(&mut self.command_encoder);
|
||||
self.pipelines.destroy(&self.gpu);
|
||||
self.gpu.destroy_surface(&mut self.surface);
|
||||
}
|
||||
|
||||
pub fn draw(&mut self, scene: &Scene) {
|
||||
@@ -575,7 +529,7 @@ impl BladeRenderer {
|
||||
|
||||
let frame = {
|
||||
profiling::scope!("acquire frame");
|
||||
self.gpu.acquire_frame()
|
||||
self.surface.acquire_frame()
|
||||
};
|
||||
self.command_encoder.init_texture(frame.texture());
|
||||
|
||||
@@ -584,21 +538,24 @@ impl BladeRenderer {
|
||||
self.surface_config.size.width as f32,
|
||||
self.surface_config.size.height as f32,
|
||||
],
|
||||
premultiplied_alpha: match self.alpha_mode {
|
||||
premultiplied_alpha: match self.surface.info().alpha {
|
||||
gpu::AlphaMode::Ignored | gpu::AlphaMode::PostMultiplied => 0,
|
||||
gpu::AlphaMode::PreMultiplied => 1,
|
||||
},
|
||||
pad: 0,
|
||||
};
|
||||
|
||||
if let mut pass = self.command_encoder.render(gpu::RenderTargetSet {
|
||||
colors: &[gpu::RenderTarget {
|
||||
view: frame.texture_view(),
|
||||
init_op: gpu::InitOp::Clear(gpu::TextureColor::TransparentBlack),
|
||||
finish_op: gpu::FinishOp::Store,
|
||||
}],
|
||||
depth_stencil: None,
|
||||
}) {
|
||||
if let mut pass = self.command_encoder.render(
|
||||
"main",
|
||||
gpu::RenderTargetSet {
|
||||
colors: &[gpu::RenderTarget {
|
||||
view: frame.texture_view(),
|
||||
init_op: gpu::InitOp::Clear(gpu::TextureColor::TransparentBlack),
|
||||
finish_op: gpu::FinishOp::Store,
|
||||
}],
|
||||
depth_stencil: None,
|
||||
},
|
||||
) {
|
||||
profiling::scope!("render pass");
|
||||
for batch in scene.batches() {
|
||||
match batch {
|
||||
|
||||
@@ -1,52 +1,45 @@
|
||||
#![allow(unused)]
|
||||
|
||||
use std::any::{type_name, Any};
|
||||
use std::cell::{self, RefCell};
|
||||
use std::env;
|
||||
use std::ffi::OsString;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::os::fd::{AsFd, AsRawFd, FromRawFd};
|
||||
use std::panic::{AssertUnwindSafe, Location};
|
||||
use std::rc::Weak;
|
||||
use std::{
|
||||
env,
|
||||
panic::AssertUnwindSafe,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
fs::File,
|
||||
io::Read as _,
|
||||
os::fd::{AsFd, AsRawFd, FromRawFd},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use async_task::Runnable;
|
||||
use calloop::channel::Channel;
|
||||
use calloop::{EventLoop, LoopHandle, LoopSignal};
|
||||
use flume::{Receiver, Sender};
|
||||
use calloop::{channel::Channel, LoopSignal};
|
||||
use futures::{channel::oneshot, future::FutureExt};
|
||||
use parking_lot::Mutex;
|
||||
use util::ResultExt;
|
||||
|
||||
use util::ResultExt as _;
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
use xkbcommon::xkb::{self, Keycode, Keysym, State};
|
||||
|
||||
use crate::platform::NoopTextSystem;
|
||||
use crate::{
|
||||
px, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
|
||||
ForegroundExecutor, Keymap, Keystroke, LinuxDispatcher, Menu, MenuItem, Modifiers, OwnedMenu,
|
||||
PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformInputHandler, PlatformTextSystem,
|
||||
PlatformWindow, Point, PromptLevel, Result, ScreenCaptureSource, SemanticVersion, SharedString,
|
||||
Size, Task, WindowAppearance, WindowOptions, WindowParams,
|
||||
ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
|
||||
Pixels, Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow, Point, Result,
|
||||
ScreenCaptureSource, Task, WindowAppearance, WindowParams,
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
pub(crate) const SCROLL_LINES: f32 = 3.0;
|
||||
|
||||
// Values match the defaults on GTK.
|
||||
// Taken from https://github.com/GNOME/gtk/blob/main/gtk/gtksettings.c#L320
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
|
||||
pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
|
||||
pub(crate) const KEYRING_LABEL: &str = "zed-github-account";
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
const FILE_PICKER_PORTAL_MISSING: &str =
|
||||
"Couldn't open file picker due to missing xdg-desktop-portal implementation.";
|
||||
|
||||
@@ -54,8 +47,9 @@ pub trait LinuxClient {
|
||||
fn compositor_name(&self) -> &'static str;
|
||||
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
|
||||
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
|
||||
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
|
||||
#[allow(unused)]
|
||||
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
|
||||
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
@@ -98,9 +92,9 @@ pub(crate) struct LinuxCommon {
|
||||
impl LinuxCommon {
|
||||
pub fn new(signal: LoopSignal) -> (Self, Channel<Runnable>) {
|
||||
let (main_sender, main_receiver) = calloop::channel::channel::<Runnable>();
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
let text_system = Arc::new(crate::CosmicTextSystem::new());
|
||||
|
||||
#[cfg(not(any(feature = "wayland", feature = "x11")))]
|
||||
let text_system = Arc::new(crate::NoopTextSystem::new());
|
||||
|
||||
@@ -218,7 +212,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
}
|
||||
}
|
||||
|
||||
fn activate(&self, ignoring_other_apps: bool) {
|
||||
fn activate(&self, _ignoring_other_apps: bool) {
|
||||
log::info!("activate is not implemented on Linux, ignoring the call")
|
||||
}
|
||||
|
||||
@@ -281,7 +275,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
|
||||
#[cfg(not(any(feature = "wayland", feature = "x11")))]
|
||||
done_tx.send(Ok(None));
|
||||
let _ = (done_tx.send(Ok(None)), options);
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
self.foreground_executor()
|
||||
@@ -306,7 +300,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
|
||||
err => err.into(),
|
||||
};
|
||||
done_tx.send(Err(result));
|
||||
let _ = done_tx.send(Err(result));
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -322,7 +316,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
Err(ashpd::Error::Response(_)) => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
};
|
||||
done_tx.send(result);
|
||||
let _ = done_tx.send(result);
|
||||
})
|
||||
.detach();
|
||||
done_rx
|
||||
@@ -332,7 +326,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
|
||||
#[cfg(not(any(feature = "wayland", feature = "x11")))]
|
||||
done_tx.send(Ok(None));
|
||||
let _ = (done_tx.send(Ok(None)), directory);
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
self.foreground_executor()
|
||||
@@ -356,7 +350,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
}
|
||||
err => err.into(),
|
||||
};
|
||||
done_tx.send(Err(result));
|
||||
let _ = done_tx.send(Err(result));
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -369,7 +363,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
Err(ashpd::Error::Response(_)) => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
};
|
||||
done_tx.send(result);
|
||||
let _ = done_tx.send(result);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
@@ -426,7 +420,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
|
||||
fn app_path(&self) -> Result<PathBuf> {
|
||||
// get the path of the executable of the current process
|
||||
let exe_path = std::env::current_exe()?;
|
||||
let exe_path = env::current_exe()?;
|
||||
Ok(exe_path)
|
||||
}
|
||||
|
||||
@@ -440,9 +434,9 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
self.with_common(|common| Some(common.menus.clone()))
|
||||
}
|
||||
|
||||
fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap) {}
|
||||
fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {}
|
||||
|
||||
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
|
||||
fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
|
||||
Err(anyhow::Error::msg(
|
||||
"Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet",
|
||||
))
|
||||
@@ -614,6 +608,7 @@ pub(super) fn reveal_path_internal(
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bool {
|
||||
let diff = a - b;
|
||||
diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE
|
||||
@@ -622,7 +617,7 @@ pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bo
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option<xkb::compose::State> {
|
||||
let mut locales = Vec::default();
|
||||
if let Some(locale) = std::env::var_os("LC_CTYPE") {
|
||||
if let Some(locale) = env::var_os("LC_CTYPE") {
|
||||
locales.push(locale);
|
||||
}
|
||||
locales.push(OsString::from("C"));
|
||||
@@ -650,6 +645,7 @@ pub(super) unsafe fn read_fd(mut fd: filedescriptor::FileDescriptor) -> Result<V
|
||||
}
|
||||
|
||||
impl CursorStyle {
|
||||
#[allow(unused)]
|
||||
pub(super) fn to_icon_name(&self) -> String {
|
||||
// Based on cursor names from https://gitlab.gnome.org/GNOME/adwaita-icon-theme (GNOME)
|
||||
// and https://github.com/KDE/breeze (KDE). Both of them seem to be also derived from
|
||||
@@ -682,10 +678,12 @@ impl CursorStyle {
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
impl Keystroke {
|
||||
pub(super) fn from_xkb(state: &State, modifiers: Modifiers, keycode: Keycode) -> Self {
|
||||
let mut modifiers = modifiers;
|
||||
|
||||
impl crate::Keystroke {
|
||||
pub(super) fn from_xkb(
|
||||
state: &State,
|
||||
mut modifiers: crate::Modifiers,
|
||||
keycode: Keycode,
|
||||
) -> Self {
|
||||
let key_utf32 = state.key_get_utf32(keycode);
|
||||
let key_utf8 = state.key_get_utf8(keycode);
|
||||
let key_sym = state.key_get_one_sym(keycode);
|
||||
@@ -759,7 +757,7 @@ impl Keystroke {
|
||||
let key_char =
|
||||
(key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8);
|
||||
|
||||
Keystroke {
|
||||
Self {
|
||||
modifiers,
|
||||
key,
|
||||
key_char,
|
||||
@@ -776,7 +774,6 @@ impl Keystroke {
|
||||
Keysym::dead_acute => Some("´".to_owned()),
|
||||
Keysym::dead_circumflex => Some("^".to_owned()),
|
||||
Keysym::dead_tilde => Some("~".to_owned()),
|
||||
Keysym::dead_perispomeni => Some("͂".to_owned()),
|
||||
Keysym::dead_macron => Some("¯".to_owned()),
|
||||
Keysym::dead_breve => Some("˘".to_owned()),
|
||||
Keysym::dead_abovedot => Some("˙".to_owned()),
|
||||
@@ -794,9 +791,7 @@ impl Keystroke {
|
||||
Keysym::dead_horn => Some("̛".to_owned()),
|
||||
Keysym::dead_stroke => Some("̶̶".to_owned()),
|
||||
Keysym::dead_abovecomma => Some("̓̓".to_owned()),
|
||||
Keysym::dead_psili => Some("᾿".to_owned()),
|
||||
Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()),
|
||||
Keysym::dead_dasia => Some("῾".to_owned()),
|
||||
Keysym::dead_doublegrave => Some("̏".to_owned()),
|
||||
Keysym::dead_belowring => Some("˳".to_owned()),
|
||||
Keysym::dead_belowmacron => Some("̱".to_owned()),
|
||||
@@ -830,7 +825,7 @@ impl Keystroke {
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
impl Modifiers {
|
||||
impl crate::Modifiers {
|
||||
pub(super) fn from_xkb(keymap_state: &State) -> Self {
|
||||
let shift = keymap_state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE);
|
||||
let alt = keymap_state.mod_name_is_active(xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE);
|
||||
@@ -838,7 +833,7 @@ impl Modifiers {
|
||||
keymap_state.mod_name_is_active(xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE);
|
||||
let platform =
|
||||
keymap_state.mod_name_is_active(xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE);
|
||||
Modifiers {
|
||||
Self {
|
||||
shift,
|
||||
alt,
|
||||
control,
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
use std::cell::{RefCell, RefMut};
|
||||
use std::hash::Hash;
|
||||
use std::os::fd::{AsRawFd, BorrowedFd};
|
||||
use std::path::PathBuf;
|
||||
use std::rc::{Rc, Weak};
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{
|
||||
cell::{RefCell, RefMut},
|
||||
hash::Hash,
|
||||
os::fd::{AsRawFd, BorrowedFd},
|
||||
path::PathBuf,
|
||||
rc::{Rc, Weak},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use calloop::timer::{TimeoutAction, Timer};
|
||||
use calloop::{EventLoop, LoopHandle};
|
||||
use calloop::{
|
||||
timer::{TimeoutAction, Timer},
|
||||
EventLoop, LoopHandle,
|
||||
};
|
||||
use calloop_wayland_source::WaylandSource;
|
||||
use collections::HashMap;
|
||||
use filedescriptor::Pipe;
|
||||
@@ -64,30 +68,28 @@ use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS};
|
||||
|
||||
use super::display::WaylandDisplay;
|
||||
use super::window::{ImeInput, WaylandWindowStatePtr};
|
||||
use crate::platform::linux::wayland::clipboard::{
|
||||
Clipboard, DataOffer, FILE_LIST_MIME_TYPE, TEXT_MIME_TYPE,
|
||||
};
|
||||
use crate::platform::linux::wayland::cursor::Cursor;
|
||||
use crate::platform::linux::wayland::serial::{SerialKind, SerialTracker};
|
||||
use crate::platform::linux::wayland::window::WaylandWindow;
|
||||
use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSource};
|
||||
use crate::platform::linux::LinuxClient;
|
||||
|
||||
use crate::platform::linux::{
|
||||
get_xkb_compose_state, is_within_click_distance, open_uri_internal, read_fd,
|
||||
reveal_path_internal,
|
||||
wayland::{
|
||||
clipboard::{Clipboard, DataOffer, FILE_LIST_MIME_TYPE, TEXT_MIME_TYPE},
|
||||
cursor::Cursor,
|
||||
serial::{SerialKind, SerialTracker},
|
||||
window::WaylandWindow,
|
||||
},
|
||||
xdg_desktop_portal::{Event as XDPEvent, XDPEventSource},
|
||||
LinuxClient,
|
||||
};
|
||||
use crate::platform::PlatformWindow;
|
||||
use crate::platform::{blade::BladeContext, PlatformWindow};
|
||||
use crate::{
|
||||
point, px, size, Bounds, DevicePixels, FileDropEvent, ForegroundExecutor, MouseExitEvent, Size,
|
||||
DOUBLE_CLICK_INTERVAL, SCROLL_LINES,
|
||||
point, px, size, AnyWindowHandle, Bounds, CursorStyle, DevicePixels, DisplayId, FileDropEvent,
|
||||
ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, LinuxCommon, Modifiers,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent,
|
||||
MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay, PlatformInput, Point, ScaledPixels,
|
||||
ScrollDelta, ScrollWheelEvent, Size, TouchPhase, WindowParams, DOUBLE_CLICK_INTERVAL,
|
||||
SCROLL_LINES,
|
||||
};
|
||||
use crate::{
|
||||
AnyWindowHandle, CursorStyle, DisplayId, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||
NavigationDirection, Pixels, PlatformDisplay, PlatformInput, Point, ScaledPixels, ScrollDelta,
|
||||
ScrollWheelEvent, TouchPhase,
|
||||
};
|
||||
use crate::{LinuxCommon, WindowParams};
|
||||
|
||||
/// Used to convert evdev scancode to xkb scancode
|
||||
const MIN_KEYCODE: u32 = 8;
|
||||
@@ -186,6 +188,7 @@ pub struct Output {
|
||||
pub(crate) struct WaylandClientState {
|
||||
serial_tracker: SerialTracker,
|
||||
globals: Globals,
|
||||
gpu_context: BladeContext,
|
||||
wl_seat: wl_seat::WlSeat, // TODO: Multi seat support
|
||||
wl_pointer: Option<wl_pointer::WlPointer>,
|
||||
wl_keyboard: Option<wl_keyboard::WlKeyboard>,
|
||||
@@ -459,6 +462,8 @@ impl WaylandClient {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let gpu_context = BladeContext::new().expect("Unable to init GPU context");
|
||||
|
||||
let seat = seat.unwrap();
|
||||
let globals = Globals::new(
|
||||
globals,
|
||||
@@ -512,6 +517,7 @@ impl WaylandClient {
|
||||
let mut state = Rc::new(RefCell::new(WaylandClientState {
|
||||
serial_tracker: SerialTracker::new(),
|
||||
globals,
|
||||
gpu_context,
|
||||
wl_seat: seat,
|
||||
wl_pointer: None,
|
||||
wl_keyboard: None,
|
||||
@@ -627,6 +633,7 @@ impl LinuxClient for WaylandClient {
|
||||
let (window, surface_id) = WaylandWindow::new(
|
||||
handle,
|
||||
state.globals.clone(),
|
||||
&state.gpu_context,
|
||||
WaylandClientStatePtr(Rc::downgrade(&self.0)),
|
||||
params,
|
||||
state.common.appearance,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use std::cell::{Ref, RefCell, RefMut};
|
||||
use std::ffi::c_void;
|
||||
use std::ptr::NonNull;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
ffi::c_void,
|
||||
ptr::NonNull,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use blade_graphics as gpu;
|
||||
use collections::HashMap;
|
||||
@@ -19,13 +21,14 @@ use wayland_protocols::xdg::shell::client::xdg_surface;
|
||||
use wayland_protocols::xdg::shell::client::xdg_toplevel::{self};
|
||||
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur;
|
||||
|
||||
use crate::platform::blade::{BladeRenderer, BladeSurfaceConfig};
|
||||
use crate::platform::linux::wayland::display::WaylandDisplay;
|
||||
use crate::platform::linux::wayland::serial::SerialKind;
|
||||
use crate::platform::{PlatformAtlas, PlatformInputHandler, PlatformWindow};
|
||||
use crate::platform::{
|
||||
blade::{BladeContext, BladeRenderer, BladeSurfaceConfig},
|
||||
linux::wayland::{display::WaylandDisplay, serial::SerialKind},
|
||||
PlatformAtlas, PlatformInputHandler, PlatformWindow,
|
||||
};
|
||||
use crate::scene::Scene;
|
||||
use crate::{
|
||||
px, size, AnyWindowHandle, Bounds, Decorations, GPUSpecs, Globals, Modifiers, Output, Pixels,
|
||||
px, size, AnyWindowHandle, Bounds, Decorations, Globals, GpuSpecs, Modifiers, Output, Pixels,
|
||||
PlatformDisplay, PlatformInput, Point, PromptLevel, RequestFrameOptions, ResizeEdge,
|
||||
ScaledPixels, Size, Tiling, WaylandClientStatePtr, WindowAppearance,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, WindowParams,
|
||||
@@ -123,37 +126,28 @@ impl WaylandWindowState {
|
||||
viewport: Option<wp_viewport::WpViewport>,
|
||||
client: WaylandClientStatePtr,
|
||||
globals: Globals,
|
||||
gpu_context: &BladeContext,
|
||||
options: WindowParams,
|
||||
) -> anyhow::Result<Self> {
|
||||
let raw = RawWindow {
|
||||
window: surface.id().as_ptr().cast::<c_void>(),
|
||||
display: surface
|
||||
.backend()
|
||||
.upgrade()
|
||||
.unwrap()
|
||||
.display_ptr()
|
||||
.cast::<c_void>(),
|
||||
};
|
||||
let gpu = Arc::new(
|
||||
unsafe {
|
||||
gpu::Context::init_windowed(
|
||||
&raw,
|
||||
gpu::ContextDesc {
|
||||
validation: false,
|
||||
capture: false,
|
||||
overlay: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
.map_err(|e| anyhow::anyhow!("{:?}", e))?,
|
||||
);
|
||||
let config = BladeSurfaceConfig {
|
||||
size: gpu::Extent {
|
||||
width: options.bounds.size.width.0 as u32,
|
||||
height: options.bounds.size.height.0 as u32,
|
||||
depth: 1,
|
||||
},
|
||||
transparent: true,
|
||||
let renderer = {
|
||||
let raw_window = RawWindow {
|
||||
window: surface.id().as_ptr().cast::<c_void>(),
|
||||
display: surface
|
||||
.backend()
|
||||
.upgrade()
|
||||
.unwrap()
|
||||
.display_ptr()
|
||||
.cast::<c_void>(),
|
||||
};
|
||||
let config = BladeSurfaceConfig {
|
||||
size: gpu::Extent {
|
||||
width: options.bounds.size.width.0 as u32,
|
||||
height: options.bounds.size.height.0 as u32,
|
||||
depth: 1,
|
||||
},
|
||||
transparent: true,
|
||||
};
|
||||
BladeRenderer::new(gpu_context, &raw_window, config)?
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
@@ -168,7 +162,7 @@ impl WaylandWindowState {
|
||||
globals,
|
||||
outputs: HashMap::default(),
|
||||
display: None,
|
||||
renderer: BladeRenderer::new(gpu, config),
|
||||
renderer,
|
||||
bounds: options.bounds,
|
||||
scale: 1.0,
|
||||
input_handler: None,
|
||||
@@ -266,6 +260,7 @@ impl WaylandWindow {
|
||||
pub fn new(
|
||||
handle: AnyWindowHandle,
|
||||
globals: Globals,
|
||||
gpu_context: &BladeContext,
|
||||
client: WaylandClientStatePtr,
|
||||
params: WindowParams,
|
||||
appearance: WindowAppearance,
|
||||
@@ -308,6 +303,7 @@ impl WaylandWindow {
|
||||
viewport,
|
||||
client,
|
||||
globals,
|
||||
gpu_context,
|
||||
params,
|
||||
)?)),
|
||||
callbacks: Rc::new(RefCell::new(Callbacks::default())),
|
||||
@@ -1019,7 +1015,7 @@ impl PlatformWindow for WaylandWindow {
|
||||
state.client.update_ime_position(bounds);
|
||||
}
|
||||
|
||||
fn gpu_specs(&self) -> Option<GPUSpecs> {
|
||||
fn gpu_specs(&self) -> Option<GpuSpecs> {
|
||||
self.borrow().renderer.gpu_specs().into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
use core::str;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::{Rc, Weak};
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::{BTreeMap, HashSet},
|
||||
ops::Deref,
|
||||
path::PathBuf,
|
||||
rc::{Rc, Weak},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use calloop::generic::{FdWrapper, Generic};
|
||||
use calloop::{EventLoop, LoopHandle, RegistrationToken};
|
||||
use calloop::{
|
||||
generic::{FdWrapper, Generic},
|
||||
EventLoop, LoopHandle, RegistrationToken,
|
||||
};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use collections::HashMap;
|
||||
@@ -15,44 +19,49 @@ use http_client::Url;
|
||||
use smallvec::SmallVec;
|
||||
use util::ResultExt;
|
||||
|
||||
use x11rb::connection::{Connection, RequestConnection};
|
||||
use x11rb::cursor;
|
||||
use x11rb::errors::ConnectionError;
|
||||
use x11rb::protocol::randr::ConnectionExt as _;
|
||||
use x11rb::protocol::xinput::ConnectionExt;
|
||||
use x11rb::protocol::xkb::ConnectionExt as _;
|
||||
use x11rb::protocol::xproto::{
|
||||
AtomEnum, ChangeWindowAttributesAux, ClientMessageData, ClientMessageEvent, ConnectionExt as _,
|
||||
EventMask, KeyPressEvent,
|
||||
use x11rb::{
|
||||
connection::{Connection, RequestConnection},
|
||||
cursor,
|
||||
errors::ConnectionError,
|
||||
protocol::randr::ConnectionExt as _,
|
||||
protocol::xinput::ConnectionExt,
|
||||
protocol::xkb::ConnectionExt as _,
|
||||
protocol::xproto::{
|
||||
AtomEnum, ChangeWindowAttributesAux, ClientMessageData, ClientMessageEvent,
|
||||
ConnectionExt as _, EventMask, KeyPressEvent,
|
||||
},
|
||||
protocol::{randr, render, xinput, xkb, xproto, Event},
|
||||
resource_manager::Database,
|
||||
wrapper::ConnectionExt as _,
|
||||
xcb_ffi::XCBConnection,
|
||||
};
|
||||
use x11rb::protocol::{randr, render, xinput, xkb, xproto, Event};
|
||||
use x11rb::resource_manager::Database;
|
||||
use x11rb::wrapper::ConnectionExt as _;
|
||||
use x11rb::xcb_ffi::XCBConnection;
|
||||
use xim::{x11rb::X11rbClient, Client};
|
||||
use xim::{AttributeName, InputStyle};
|
||||
use xim::{x11rb::X11rbClient, AttributeName, Client, InputStyle};
|
||||
use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION};
|
||||
use xkbcommon::xkb::{self as xkbc, LayoutIndex, ModMask};
|
||||
|
||||
use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::{LinuxCommon, PlatformWindow};
|
||||
use crate::{
|
||||
modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, ClipboardItem, CursorStyle,
|
||||
DisplayId, FileDropEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Pixels,
|
||||
Platform, PlatformDisplay, PlatformInput, Point, RequestFrameOptions, ScaledPixels,
|
||||
ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
|
||||
};
|
||||
|
||||
use super::{
|
||||
button_or_scroll_from_event_detail, get_valuator_axis_index, modifiers_from_state,
|
||||
pressed_button_from_mask, ButtonOrScroll, ScrollDirection,
|
||||
};
|
||||
use super::{X11Display, X11WindowStatePtr, XcbAtoms};
|
||||
use super::{XimCallbackEvent, XimHandler};
|
||||
use crate::platform::linux::platform::{DOUBLE_CLICK_INTERVAL, SCROLL_LINES};
|
||||
use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSource};
|
||||
use crate::platform::linux::{
|
||||
get_xkb_compose_state, is_within_click_distance, open_uri_internal, reveal_path_internal,
|
||||
|
||||
use crate::platform::{
|
||||
blade::BladeContext,
|
||||
linux::{
|
||||
get_xkb_compose_state, is_within_click_distance, open_uri_internal,
|
||||
platform::{DOUBLE_CLICK_INTERVAL, SCROLL_LINES},
|
||||
reveal_path_internal,
|
||||
xdg_desktop_portal::{Event as XDPEvent, XDPEventSource},
|
||||
LinuxClient,
|
||||
},
|
||||
LinuxCommon, PlatformWindow,
|
||||
};
|
||||
use crate::{
|
||||
modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, ClipboardItem, CursorStyle,
|
||||
DisplayId, FileDropEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Pixels,
|
||||
Platform, PlatformDisplay, PlatformInput, Point, RequestFrameOptions, ScaledPixels,
|
||||
ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
|
||||
};
|
||||
|
||||
/// Value for DeviceId parameters which selects all devices.
|
||||
@@ -158,6 +167,8 @@ pub struct X11ClientState {
|
||||
pub(crate) last_location: Point<Pixels>,
|
||||
pub(crate) current_count: usize,
|
||||
|
||||
gpu_context: BladeContext,
|
||||
|
||||
pub(crate) scale_factor: f32,
|
||||
|
||||
xkb_context: xkbc::Context,
|
||||
@@ -360,6 +371,8 @@ impl X11Client {
|
||||
let compose_state = get_xkb_compose_state(&xkb_context);
|
||||
let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection).unwrap();
|
||||
|
||||
let gpu_context = BladeContext::new().expect("Unable to init GPU context");
|
||||
|
||||
let scale_factor = resource_database
|
||||
.get_value("Xft.dpi", "Xft.dpi")
|
||||
.ok()
|
||||
@@ -428,6 +441,7 @@ impl X11Client {
|
||||
last_mouse_button: None,
|
||||
last_location: Point::new(px(0.0), px(0.0)),
|
||||
current_count: 0,
|
||||
gpu_context,
|
||||
scale_factor,
|
||||
|
||||
xkb_context,
|
||||
@@ -1299,6 +1313,7 @@ impl LinuxClient for X11Client {
|
||||
handle,
|
||||
X11ClientStatePtr(Rc::downgrade(&self.0)),
|
||||
state.common.foreground_executor.clone(),
|
||||
&state.gpu_context,
|
||||
params,
|
||||
&state.xcb_connection,
|
||||
state.client_side_decorations_supported,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
|
||||
use crate::platform::blade::{BladeContext, BladeRenderer, BladeSurfaceConfig};
|
||||
use crate::{
|
||||
platform::blade::{BladeRenderer, BladeSurfaceConfig},
|
||||
px, size, AnyWindowHandle, Bounds, Decorations, DevicePixels, ForegroundExecutor, GPUSpecs,
|
||||
px, size, AnyWindowHandle, Bounds, Decorations, DevicePixels, ForegroundExecutor, GpuSpecs,
|
||||
Modifiers, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler,
|
||||
PlatformWindow, Point, PromptLevel, RequestFrameOptions, ResizeEdge, ScaledPixels, Scene, Size,
|
||||
Tiling, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowDecorations,
|
||||
@@ -247,7 +247,6 @@ pub struct X11WindowState {
|
||||
x_root_window: xproto::Window,
|
||||
pub(crate) counter_id: sync::Counter,
|
||||
pub(crate) last_sync_counter: Option<sync::Int64>,
|
||||
_raw: RawWindow,
|
||||
bounds: Bounds<Pixels>,
|
||||
scale_factor: f32,
|
||||
renderer: BladeRenderer,
|
||||
@@ -358,6 +357,7 @@ impl X11WindowState {
|
||||
handle: AnyWindowHandle,
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
gpu_context: &BladeContext,
|
||||
params: WindowParams,
|
||||
xcb: &Rc<XCBConnection>,
|
||||
client_side_decorations_supported: bool,
|
||||
@@ -555,50 +555,39 @@ impl X11WindowState {
|
||||
|
||||
xcb.flush().with_context(|| "X11 Flush failed.")?;
|
||||
|
||||
let raw = RawWindow {
|
||||
connection: as_raw_xcb_connection::AsRawXcbConnection::as_raw_xcb_connection(xcb)
|
||||
as *mut _,
|
||||
screen_id: x_screen_index,
|
||||
window_id: x_window,
|
||||
visual_id: visual.id,
|
||||
let renderer = {
|
||||
let raw_window = RawWindow {
|
||||
connection: as_raw_xcb_connection::AsRawXcbConnection::as_raw_xcb_connection(
|
||||
xcb,
|
||||
) as *mut _,
|
||||
screen_id: x_screen_index,
|
||||
window_id: x_window,
|
||||
visual_id: visual.id,
|
||||
};
|
||||
let config = BladeSurfaceConfig {
|
||||
// Note: this has to be done after the GPU init, or otherwise
|
||||
// the sizes are immediately invalidated.
|
||||
size: query_render_extent(xcb, x_window)?,
|
||||
// We set it to transparent by default, even if we have client-side
|
||||
// decorations, since those seem to work on X11 even without `true` here.
|
||||
// If the window appearance changes, then the renderer will get updated
|
||||
// too
|
||||
transparent: false,
|
||||
};
|
||||
BladeRenderer::new(gpu_context, &raw_window, config)?
|
||||
};
|
||||
let gpu = Arc::new(
|
||||
unsafe {
|
||||
gpu::Context::init_windowed(
|
||||
&raw,
|
||||
gpu::ContextDesc {
|
||||
validation: false,
|
||||
capture: false,
|
||||
overlay: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
.map_err(|e| anyhow!("{:?}", e))?,
|
||||
);
|
||||
|
||||
let config = BladeSurfaceConfig {
|
||||
// Note: this has to be done after the GPU init, or otherwise
|
||||
// the sizes are immediately invalidated.
|
||||
size: query_render_extent(xcb, x_window)?,
|
||||
// We set it to transparent by default, even if we have client-side
|
||||
// decorations, since those seem to work on X11 even without `true` here.
|
||||
// If the window appearance changes, then the renderer will get updated
|
||||
// too
|
||||
transparent: false,
|
||||
};
|
||||
check_reply(|| "X11 MapWindow failed.", xcb.map_window(x_window))?;
|
||||
|
||||
let display = Rc::new(X11Display::new(xcb, scale_factor, x_screen_index)?);
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
executor,
|
||||
display,
|
||||
_raw: raw,
|
||||
x_root_window: visual_set.root,
|
||||
bounds: bounds.to_pixels(scale_factor),
|
||||
scale_factor,
|
||||
renderer: BladeRenderer::new(gpu, config),
|
||||
renderer,
|
||||
atoms: *atoms,
|
||||
input_handler: None,
|
||||
active: false,
|
||||
@@ -716,6 +705,7 @@ impl X11Window {
|
||||
handle: AnyWindowHandle,
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
gpu_context: &BladeContext,
|
||||
params: WindowParams,
|
||||
xcb: &Rc<XCBConnection>,
|
||||
client_side_decorations_supported: bool,
|
||||
@@ -730,6 +720,7 @@ impl X11Window {
|
||||
handle,
|
||||
client,
|
||||
executor,
|
||||
gpu_context,
|
||||
params,
|
||||
xcb,
|
||||
client_side_decorations_supported,
|
||||
@@ -1215,7 +1206,7 @@ impl PlatformWindow for X11Window {
|
||||
title.as_bytes(),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
.log_err();
|
||||
|
||||
check_reply(
|
||||
|| "X11 ChangeProperty8 on _NET_WM_NAME failed.",
|
||||
@@ -1227,8 +1218,8 @@ impl PlatformWindow for X11Window {
|
||||
title.as_bytes(),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
self.flush().unwrap();
|
||||
.log_err();
|
||||
self.flush().log_err();
|
||||
}
|
||||
|
||||
fn set_app_id(&mut self, app_id: &str) {
|
||||
@@ -1536,7 +1527,7 @@ impl PlatformWindow for X11Window {
|
||||
client.update_ime_position(bounds);
|
||||
}
|
||||
|
||||
fn gpu_specs(&self) -> Option<GPUSpecs> {
|
||||
fn gpu_specs(&self) -> Option<GpuSpecs> {
|
||||
self.0.state.borrow().renderer.gpu_specs().into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1102,7 +1102,7 @@ impl PlatformWindow for MacWindow {
|
||||
self.0.lock().renderer.sprite_atlas().clone()
|
||||
}
|
||||
|
||||
fn gpu_specs(&self) -> Option<crate::GPUSpecs> {
|
||||
fn gpu_specs(&self) -> Option<crate::GpuSpecs> {
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DispatchEventResult, GPUSpecs,
|
||||
AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DispatchEventResult, GpuSpecs,
|
||||
Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
|
||||
Point, RequestFrameOptions, ScaledPixels, Size, TestPlatform, TileId, WindowAppearance,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowParams,
|
||||
@@ -276,7 +276,7 @@ impl PlatformWindow for TestWindow {
|
||||
|
||||
fn update_ime_position(&self, _bounds: Bounds<ScaledPixels>) {}
|
||||
|
||||
fn gpu_specs(&self) -> Option<GPUSpecs> {
|
||||
fn gpu_specs(&self) -> Option<GpuSpecs> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,11 +28,12 @@ use windows::{
|
||||
UI::ViewManagement::UISettings,
|
||||
};
|
||||
|
||||
use crate::*;
|
||||
use crate::{platform::blade::BladeContext, *};
|
||||
|
||||
pub(crate) struct WindowsPlatform {
|
||||
state: RefCell<WindowsPlatformState>,
|
||||
raw_window_handles: RwLock<SmallVec<[HWND; 4]>>,
|
||||
gpu_context: BladeContext,
|
||||
// The below members will never change throughout the entire lifecycle of the app.
|
||||
icon: HICON,
|
||||
main_receiver: flume::Receiver<Runnable>,
|
||||
@@ -94,12 +95,14 @@ impl WindowsPlatform {
|
||||
let icon = load_icon().unwrap_or_default();
|
||||
let state = RefCell::new(WindowsPlatformState::new());
|
||||
let raw_window_handles = RwLock::new(SmallVec::new());
|
||||
let gpu_context = BladeContext::new().expect("Unable to init GPU context");
|
||||
let windows_version = WindowsVersion::new().expect("Error retrieve windows version");
|
||||
let validation_number = rand::random::<usize>();
|
||||
|
||||
Self {
|
||||
state,
|
||||
raw_window_handles,
|
||||
gpu_context,
|
||||
icon,
|
||||
main_receiver,
|
||||
dispatch_event,
|
||||
@@ -344,7 +347,12 @@ impl Platform for WindowsPlatform {
|
||||
handle: AnyWindowHandle,
|
||||
options: WindowParams,
|
||||
) -> Result<Box<dyn PlatformWindow>> {
|
||||
let window = WindowsWindow::new(handle, options, self.generate_creation_info())?;
|
||||
let window = WindowsWindow::new(
|
||||
handle,
|
||||
options,
|
||||
self.generate_creation_info(),
|
||||
&self.gpu_context,
|
||||
)?;
|
||||
let handle = window.get_raw_handle();
|
||||
self.raw_window_handles.write().push(handle);
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ use windows::{
|
||||
},
|
||||
};
|
||||
|
||||
use crate::platform::blade::BladeRenderer;
|
||||
use crate::platform::blade::{BladeContext, BladeRenderer};
|
||||
use crate::*;
|
||||
|
||||
pub(crate) struct WindowsWindow(pub Rc<WindowsWindowStatePtr>);
|
||||
@@ -78,6 +78,7 @@ impl WindowsWindowState {
|
||||
cs: &CREATESTRUCTW,
|
||||
current_cursor: HCURSOR,
|
||||
display: WindowsDisplay,
|
||||
gpu_context: &BladeContext,
|
||||
) -> Result<Self> {
|
||||
let scale_factor = {
|
||||
let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32;
|
||||
@@ -94,7 +95,7 @@ impl WindowsWindowState {
|
||||
};
|
||||
let border_offset = WindowBorderOffset::default();
|
||||
let is_minimized = None;
|
||||
let renderer = windows_renderer::windows_renderer(hwnd, transparent)?;
|
||||
let renderer = windows_renderer::init(gpu_context, hwnd, transparent)?;
|
||||
let callbacks = Callbacks::default();
|
||||
let input_handler = None;
|
||||
let system_key_handled = false;
|
||||
@@ -227,6 +228,7 @@ impl WindowsWindowStatePtr {
|
||||
cs,
|
||||
context.current_cursor,
|
||||
context.display,
|
||||
context.gpu_context,
|
||||
)?);
|
||||
|
||||
Ok(Rc::new_cyclic(|this| Self {
|
||||
@@ -340,7 +342,7 @@ pub(crate) struct Callbacks {
|
||||
pub(crate) appearance_changed: Option<Box<dyn FnMut()>>,
|
||||
}
|
||||
|
||||
struct WindowCreateContext {
|
||||
struct WindowCreateContext<'a> {
|
||||
inner: Option<Result<Rc<WindowsWindowStatePtr>>>,
|
||||
handle: AnyWindowHandle,
|
||||
hide_title_bar: bool,
|
||||
@@ -352,6 +354,7 @@ struct WindowCreateContext {
|
||||
windows_version: WindowsVersion,
|
||||
validation_number: usize,
|
||||
main_receiver: flume::Receiver<Runnable>,
|
||||
gpu_context: &'a BladeContext,
|
||||
}
|
||||
|
||||
impl WindowsWindow {
|
||||
@@ -359,6 +362,7 @@ impl WindowsWindow {
|
||||
handle: AnyWindowHandle,
|
||||
params: WindowParams,
|
||||
creation_info: WindowCreationInfo,
|
||||
gpu_context: &BladeContext,
|
||||
) -> Result<Self> {
|
||||
let WindowCreationInfo {
|
||||
icon,
|
||||
@@ -410,6 +414,7 @@ impl WindowsWindow {
|
||||
windows_version,
|
||||
validation_number,
|
||||
main_receiver,
|
||||
gpu_context,
|
||||
};
|
||||
let lpparam = Some(&context as *const _ as *const _);
|
||||
let creation_result = unsafe {
|
||||
@@ -770,7 +775,7 @@ impl PlatformWindow for WindowsWindow {
|
||||
self.0.hwnd
|
||||
}
|
||||
|
||||
fn gpu_specs(&self) -> Option<GPUSpecs> {
|
||||
fn gpu_specs(&self) -> Option<GpuSpecs> {
|
||||
Some(self.0.state.borrow().renderer.gpu_specs())
|
||||
}
|
||||
|
||||
@@ -1236,38 +1241,24 @@ fn set_window_composition_attribute(hwnd: HWND, color: Option<Color>, state: u32
|
||||
}
|
||||
|
||||
mod windows_renderer {
|
||||
use std::{num::NonZeroIsize, sync::Arc};
|
||||
|
||||
use blade_graphics as gpu;
|
||||
use crate::platform::blade::{BladeContext, BladeRenderer, BladeSurfaceConfig};
|
||||
use raw_window_handle as rwh;
|
||||
use std::num::NonZeroIsize;
|
||||
use windows::Win32::{Foundation::HWND, UI::WindowsAndMessaging::GWLP_HINSTANCE};
|
||||
|
||||
use crate::{
|
||||
get_window_long,
|
||||
platform::blade::{BladeRenderer, BladeSurfaceConfig},
|
||||
};
|
||||
use crate::get_window_long;
|
||||
|
||||
pub(super) fn windows_renderer(hwnd: HWND, transparent: bool) -> anyhow::Result<BladeRenderer> {
|
||||
pub(super) fn init(
|
||||
context: &BladeContext,
|
||||
hwnd: HWND,
|
||||
transparent: bool,
|
||||
) -> anyhow::Result<BladeRenderer> {
|
||||
let raw = RawWindow { hwnd };
|
||||
let gpu: Arc<gpu::Context> = Arc::new(
|
||||
unsafe {
|
||||
gpu::Context::init_windowed(
|
||||
&raw,
|
||||
gpu::ContextDesc {
|
||||
validation: false,
|
||||
capture: false,
|
||||
overlay: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
.map_err(|e| anyhow::anyhow!("{:?}", e))?,
|
||||
);
|
||||
let config = BladeSurfaceConfig {
|
||||
size: gpu::Extent::default(),
|
||||
size: Default::default(),
|
||||
transparent,
|
||||
};
|
||||
|
||||
Ok(BladeRenderer::new(gpu, config))
|
||||
BladeRenderer::new(context, &raw, config)
|
||||
}
|
||||
|
||||
struct RawWindow {
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
AnyView, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Background, Bounds,
|
||||
BoxShadow, Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener,
|
||||
DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter,
|
||||
FileDropEvent, Flatten, FontId, GPUSpecs, Global, GlobalElementId, GlyphId, Hsla, InputHandler,
|
||||
FileDropEvent, Flatten, FontId, Global, GlobalElementId, GlyphId, GpuSpecs, Hsla, InputHandler,
|
||||
IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke, KeystrokeEvent,
|
||||
KeystrokeObserver, LayoutId, LineLayoutIndex, Model, ModelContext, Modifiers,
|
||||
ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent,
|
||||
@@ -531,7 +531,6 @@ pub struct Window {
|
||||
pub(crate) tooltip_bounds: Option<TooltipBounds>,
|
||||
next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>>,
|
||||
pub(crate) dirty_views: FxHashSet<EntityId>,
|
||||
pub(crate) focus_handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
|
||||
focus_listeners: SubscriberSet<(), AnyWindowFocusListener>,
|
||||
focus_lost_listeners: SubscriberSet<(), AnyObserver>,
|
||||
default_prevented: bool,
|
||||
@@ -809,7 +808,6 @@ impl Window {
|
||||
next_tooltip_id: TooltipId::default(),
|
||||
tooltip_bounds: None,
|
||||
dirty_views: FxHashSet::default(),
|
||||
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
|
||||
focus_listeners: SubscriberSet::new(),
|
||||
focus_lost_listeners: SubscriberSet::new(),
|
||||
default_prevented: true,
|
||||
@@ -931,17 +929,11 @@ impl<'a> WindowContext<'a> {
|
||||
self.window.removed = true;
|
||||
}
|
||||
|
||||
/// Obtain a new [`FocusHandle`], which allows you to track and manipulate the keyboard focus
|
||||
/// for elements rendered within this window.
|
||||
pub fn focus_handle(&self) -> FocusHandle {
|
||||
FocusHandle::new(&self.window.focus_handles)
|
||||
}
|
||||
|
||||
/// Obtain the currently focused [`FocusHandle`]. If no elements are focused, returns `None`.
|
||||
pub fn focused(&self) -> Option<FocusHandle> {
|
||||
self.window
|
||||
.focus
|
||||
.and_then(|id| FocusHandle::for_id(id, &self.window.focus_handles))
|
||||
.and_then(|id| FocusHandle::for_id(id, &self.app.focus_handles))
|
||||
}
|
||||
|
||||
/// Move focus to the element associated with the given [`FocusHandle`].
|
||||
@@ -3021,7 +3013,7 @@ impl<'a> WindowContext<'a> {
|
||||
let event = FocusOutEvent {
|
||||
blurred: WeakFocusHandle {
|
||||
id: blurred_id,
|
||||
handles: Arc::downgrade(&cx.window.focus_handles),
|
||||
handles: Arc::downgrade(&cx.app.focus_handles),
|
||||
},
|
||||
};
|
||||
listener(event, cx)
|
||||
@@ -3816,7 +3808,7 @@ impl<'a> WindowContext<'a> {
|
||||
|
||||
/// Read information about the GPU backing this window.
|
||||
/// Currently returns None on Mac and Windows.
|
||||
pub fn gpu_specs(&self) -> Option<GPUSpecs> {
|
||||
pub fn gpu_specs(&self) -> Option<GpuSpecs> {
|
||||
self.window.platform_window.gpu_specs()
|
||||
}
|
||||
}
|
||||
@@ -4439,7 +4431,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
let event = FocusOutEvent {
|
||||
blurred: WeakFocusHandle {
|
||||
id: blurred_id,
|
||||
handles: Arc::downgrade(&cx.window.focus_handles),
|
||||
handles: Arc::downgrade(&cx.app.focus_handles),
|
||||
},
|
||||
};
|
||||
listener(view, event, cx)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
use std::sync::OnceLock;
|
||||
use std::{cell::RefCell, sync::LazyLock};
|
||||
|
||||
use anyhow::Result;
|
||||
use markup5ever_rcdom::{Handle, NodeData};
|
||||
@@ -10,13 +9,14 @@ use regex::Regex;
|
||||
use crate::html_element::HtmlElement;
|
||||
|
||||
fn empty_line_regex() -> &'static Regex {
|
||||
static REGEX: OnceLock<Regex> = OnceLock::new();
|
||||
REGEX.get_or_init(|| Regex::new(r"^\s*$").unwrap())
|
||||
static REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"^\s*$").expect("Failed to create empty_line_regex"));
|
||||
®EX
|
||||
}
|
||||
|
||||
fn more_than_three_newlines_regex() -> &'static Regex {
|
||||
static REGEX: OnceLock<Regex> = OnceLock::new();
|
||||
REGEX.get_or_init(|| Regex::new(r"\n{3,}").unwrap())
|
||||
static REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\n{3,}").unwrap());
|
||||
®EX
|
||||
}
|
||||
|
||||
pub enum StartTagOutcome {
|
||||
|
||||
@@ -20,6 +20,7 @@ pub struct InlineCompletion {
|
||||
pub trait InlineCompletionProvider: 'static + Sized {
|
||||
fn name() -> &'static str;
|
||||
fn display_name() -> &'static str;
|
||||
fn show_completions_in_menu() -> bool;
|
||||
fn is_enabled(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
@@ -59,6 +60,7 @@ pub trait InlineCompletionProviderHandle {
|
||||
cursor_position: language::Anchor,
|
||||
cx: &AppContext,
|
||||
) -> bool;
|
||||
fn show_completions_in_menu(&self) -> bool;
|
||||
fn refresh(
|
||||
&self,
|
||||
buffer: Model<Buffer>,
|
||||
@@ -95,6 +97,10 @@ where
|
||||
T::display_name()
|
||||
}
|
||||
|
||||
fn show_completions_in_menu(&self) -> bool {
|
||||
T::show_completions_in_menu()
|
||||
}
|
||||
|
||||
fn is_enabled(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
|
||||
@@ -18,10 +18,10 @@ use std::{
|
||||
};
|
||||
use theme::{ActiveTheme, SyntaxTheme, ThemeSettings};
|
||||
use ui::{
|
||||
h_flex, relative, tooltip_container, v_flex, Checkbox, Clickable, Color, FluentBuilder,
|
||||
IconButton, IconName, IconSize, InteractiveElement, Label, LabelCommon, LabelSize, LinkPreview,
|
||||
StatefulInteractiveElement, StyledExt, StyledImage, ToggleState, ViewContext, VisibleOnHover,
|
||||
VisualContext as _,
|
||||
h_flex, relative, tooltip_container, v_flex, ButtonCommon, Checkbox, Clickable, Color,
|
||||
FluentBuilder, IconButton, IconName, IconSize, InteractiveElement, Label, LabelCommon,
|
||||
LabelSize, LinkPreview, StatefulInteractiveElement, StyledExt, StyledImage, ToggleState,
|
||||
Tooltip, ViewContext, VisibleOnHover, VisualContext as _,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -385,6 +385,7 @@ fn render_markdown_code_block(
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(contents.to_string()));
|
||||
}
|
||||
})
|
||||
.tooltip(|cx| Tooltip::text("Copy code block", cx))
|
||||
.visible_on_hover("markdown-block");
|
||||
|
||||
cx.with_common_p(div())
|
||||
|
||||
@@ -19,7 +19,7 @@ anyhow.workspace = true
|
||||
core-foundation.workspace = true
|
||||
ctor.workspace = true
|
||||
foreign-types = "0.5"
|
||||
metal = "0.29"
|
||||
metal.workspace = true
|
||||
objc = "0.2"
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1990
crates/multi_buffer/src/multi_buffer_tests.rs
Normal file
1990
crates/multi_buffer/src/multi_buffer_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -68,6 +68,7 @@ snippet.workspace = true
|
||||
snippet_provider.workspace = true
|
||||
terminal.workspace = true
|
||||
text.workspace = true
|
||||
toml.workspace = true
|
||||
util.workspace = true
|
||||
url.workspace = true
|
||||
which.workspace = true
|
||||
|
||||
@@ -5,12 +5,12 @@ use crate::{
|
||||
ProjectItem as _, ProjectPath,
|
||||
};
|
||||
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use client::Client;
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use fs::Fs;
|
||||
use futures::{channel::oneshot, future::Shared, Future, FutureExt as _, StreamExt};
|
||||
use git::{blame::Blame, diff::BufferDiff};
|
||||
use git::{blame::Blame, diff::BufferDiff, repository::RepoPath};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context as _, EventEmitter, Model, ModelContext, Subscription,
|
||||
Task, WeakModel,
|
||||
@@ -24,8 +24,16 @@ use language::{
|
||||
Buffer, BufferEvent, Capability, DiskState, File as _, Language, Operation,
|
||||
};
|
||||
use rpc::{proto, AnyProtoClient, ErrorExt as _, TypedEnvelope};
|
||||
use serde::Deserialize;
|
||||
use smol::channel::Receiver;
|
||||
use std::{io, ops::Range, path::Path, str::FromStr as _, sync::Arc, time::Instant};
|
||||
use std::{
|
||||
io,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr as _,
|
||||
sync::Arc,
|
||||
time::Instant,
|
||||
};
|
||||
use text::{BufferId, LineEnding, Rope};
|
||||
use util::{debug_panic, maybe, ResultExt as _, TryFutureExt};
|
||||
use worktree::{File, PathChange, ProjectEntryId, UpdatedGitRepositoriesSet, Worktree, WorktreeId};
|
||||
@@ -1213,11 +1221,36 @@ impl BufferStore {
|
||||
|
||||
match file.worktree.read(cx) {
|
||||
Worktree::Local(worktree) => {
|
||||
let Some(repo) = worktree.local_git_repo(file.path()) else {
|
||||
return Task::ready(Err(anyhow!("no repository for buffer found")));
|
||||
let worktree_path = worktree.abs_path().clone();
|
||||
let Some((repo_entry, repo)) =
|
||||
worktree.repository_for_path(file.path()).and_then(|entry| {
|
||||
let repo = worktree.get_local_repo(&entry)?.repo().clone();
|
||||
Some((entry, repo))
|
||||
})
|
||||
else {
|
||||
// If we're not in a Git repo, check whether this is a Rust source
|
||||
// file in the Cargo registry (presumably opened with go-to-definition
|
||||
// from a normal Rust file). If so, we can put together a permalink
|
||||
// using crate metadata.
|
||||
if !buffer
|
||||
.language()
|
||||
.is_some_and(|lang| lang.name() == "Rust".into())
|
||||
{
|
||||
return Task::ready(Err(anyhow!("no permalink available")));
|
||||
}
|
||||
let file_path = worktree_path.join(file.path());
|
||||
return cx.spawn(|cx| async move {
|
||||
let provider_registry =
|
||||
cx.update(GitHostingProviderRegistry::default_global)?;
|
||||
get_permalink_in_rust_registry_src(provider_registry, file_path, selection)
|
||||
.map_err(|_| anyhow!("no permalink available"))
|
||||
});
|
||||
};
|
||||
|
||||
let path = file.path().clone();
|
||||
let path = match repo_entry.relativize(worktree, file.path()) {
|
||||
Ok(RepoPath(path)) => path,
|
||||
Err(e) => return Task::ready(Err(e)),
|
||||
};
|
||||
|
||||
cx.spawn(|cx| async move {
|
||||
const REMOTE_NAME: &str = "origin";
|
||||
@@ -1238,7 +1271,7 @@ impl BufferStore {
|
||||
|
||||
let path = path
|
||||
.to_str()
|
||||
.context("failed to convert buffer path to string")?;
|
||||
.ok_or_else(|| anyhow!("failed to convert path to string"))?;
|
||||
|
||||
Ok(provider.build_permalink(
|
||||
remote,
|
||||
@@ -2432,3 +2465,52 @@ fn deserialize_blame_buffer_response(
|
||||
remote_url: response.remote_url,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_permalink_in_rust_registry_src(
|
||||
provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
path: PathBuf,
|
||||
selection: Range<u32>,
|
||||
) -> Result<url::Url> {
|
||||
#[derive(Deserialize)]
|
||||
struct CargoVcsGit {
|
||||
sha1: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CargoVcsInfo {
|
||||
git: CargoVcsGit,
|
||||
path_in_vcs: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CargoPackage {
|
||||
repository: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CargoToml {
|
||||
package: CargoPackage,
|
||||
}
|
||||
|
||||
let Some((dir, cargo_vcs_info_json)) = path.ancestors().skip(1).find_map(|dir| {
|
||||
let json = std::fs::read_to_string(dir.join(".cargo_vcs_info.json")).ok()?;
|
||||
Some((dir, json))
|
||||
}) else {
|
||||
bail!("No .cargo_vcs_info.json found in parent directories")
|
||||
};
|
||||
let cargo_vcs_info = serde_json::from_str::<CargoVcsInfo>(&cargo_vcs_info_json)?;
|
||||
let cargo_toml = std::fs::read_to_string(dir.join("Cargo.toml"))?;
|
||||
let manifest = toml::from_str::<CargoToml>(&cargo_toml)?;
|
||||
let (provider, remote) = parse_git_remote_url(provider_registry, &manifest.package.repository)
|
||||
.ok_or_else(|| anyhow!("Failed to parse package.repository field of manifest"))?;
|
||||
let path = PathBuf::from(cargo_vcs_info.path_in_vcs).join(path.strip_prefix(dir).unwrap());
|
||||
let permalink = provider.build_permalink(
|
||||
remote,
|
||||
BuildPermalinkParams {
|
||||
sha: &cargo_vcs_info.git.sha1,
|
||||
path: &path.to_string_lossy(),
|
||||
selection: Some(selection),
|
||||
},
|
||||
);
|
||||
Ok(permalink)
|
||||
}
|
||||
|
||||
@@ -14,8 +14,7 @@ use crate::{
|
||||
|
||||
pub struct ProjectEnvironment {
|
||||
cli_environment: Option<HashMap<String, String>>,
|
||||
get_environment_task: Option<Shared<Task<Option<HashMap<String, String>>>>>,
|
||||
cached_shell_environments: HashMap<WorktreeId, HashMap<String, String>>,
|
||||
environments: HashMap<WorktreeId, Shared<Task<Option<HashMap<String, String>>>>>,
|
||||
environment_error_messages: HashMap<WorktreeId, EnvironmentErrorMessage>,
|
||||
}
|
||||
|
||||
@@ -35,27 +34,15 @@ impl ProjectEnvironment {
|
||||
|
||||
Self {
|
||||
cli_environment,
|
||||
get_environment_task: None,
|
||||
cached_shell_environments: Default::default(),
|
||||
environments: Default::default(),
|
||||
environment_error_messages: Default::default(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn set_cached(
|
||||
&mut self,
|
||||
shell_environments: &[(WorktreeId, HashMap<String, String>)],
|
||||
) {
|
||||
self.cached_shell_environments = shell_environments
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<HashMap<_, _>>();
|
||||
}
|
||||
|
||||
pub(crate) fn remove_worktree_environment(&mut self, worktree_id: WorktreeId) {
|
||||
self.cached_shell_environments.remove(&worktree_id);
|
||||
self.environment_error_messages.remove(&worktree_id);
|
||||
self.environments.remove(&worktree_id);
|
||||
}
|
||||
|
||||
/// Returns the inherited CLI environment, if this project was opened from the Zed CLI.
|
||||
@@ -91,96 +78,83 @@ impl ProjectEnvironment {
|
||||
worktree_abs_path: Option<Arc<Path>>,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Shared<Task<Option<HashMap<String, String>>>> {
|
||||
if let Some(task) = self.get_environment_task.as_ref() {
|
||||
if cfg!(any(test, feature = "test-support")) {
|
||||
return Task::ready(Some(HashMap::default())).shared();
|
||||
}
|
||||
|
||||
if let Some(cli_environment) = self.get_cli_environment() {
|
||||
return cx
|
||||
.spawn(|_, _| async move {
|
||||
let path = cli_environment
|
||||
.get("PATH")
|
||||
.map(|path| path.as_str())
|
||||
.unwrap_or_default();
|
||||
log::info!(
|
||||
"using project environment variables from CLI. PATH={:?}",
|
||||
path
|
||||
);
|
||||
Some(cli_environment)
|
||||
})
|
||||
.shared();
|
||||
}
|
||||
|
||||
let Some((worktree_id, worktree_abs_path)) = worktree_id.zip(worktree_abs_path) else {
|
||||
return Task::ready(None).shared();
|
||||
};
|
||||
|
||||
if let Some(task) = self.environments.get(&worktree_id) {
|
||||
task.clone()
|
||||
} else {
|
||||
let task = self
|
||||
.build_environment_task(worktree_id, worktree_abs_path, cx)
|
||||
.get_worktree_env(worktree_id, worktree_abs_path, cx)
|
||||
.shared();
|
||||
|
||||
self.get_environment_task = Some(task.clone());
|
||||
self.environments.insert(worktree_id, task.clone());
|
||||
task
|
||||
}
|
||||
}
|
||||
|
||||
fn build_environment_task(
|
||||
&mut self,
|
||||
worktree_id: Option<WorktreeId>,
|
||||
worktree_abs_path: Option<Arc<Path>>,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Task<Option<HashMap<String, String>>> {
|
||||
let worktree = worktree_id.zip(worktree_abs_path);
|
||||
|
||||
let cli_environment = self.get_cli_environment();
|
||||
if let Some(environment) = cli_environment {
|
||||
cx.spawn(|_, _| async move {
|
||||
let path = environment
|
||||
.get("PATH")
|
||||
.map(|path| path.as_str())
|
||||
.unwrap_or_default();
|
||||
log::info!(
|
||||
"using project environment variables from CLI. PATH={:?}",
|
||||
path
|
||||
);
|
||||
Some(environment)
|
||||
})
|
||||
} else if let Some((worktree_id, worktree_abs_path)) = worktree {
|
||||
self.get_worktree_env(worktree_id, worktree_abs_path, cx)
|
||||
} else {
|
||||
Task::ready(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_worktree_env(
|
||||
&mut self,
|
||||
worktree_id: WorktreeId,
|
||||
worktree_abs_path: Arc<Path>,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Task<Option<HashMap<String, String>>> {
|
||||
let cached_env = self.cached_shell_environments.get(&worktree_id).cloned();
|
||||
if let Some(env) = cached_env {
|
||||
Task::ready(Some(env))
|
||||
} else {
|
||||
let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone();
|
||||
let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let (mut shell_env, error_message) = cx
|
||||
.background_executor()
|
||||
.spawn({
|
||||
let cwd = worktree_abs_path.clone();
|
||||
async move { load_shell_environment(&cwd, &load_direnv).await }
|
||||
})
|
||||
.await;
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let (mut shell_env, error_message) = cx
|
||||
.background_executor()
|
||||
.spawn({
|
||||
let worktree_abs_path = worktree_abs_path.clone();
|
||||
async move {
|
||||
load_worktree_shell_environment(&worktree_abs_path, &load_direnv).await
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(shell_env) = shell_env.as_mut() {
|
||||
let path = shell_env
|
||||
.get("PATH")
|
||||
.map(|path| path.as_str())
|
||||
.unwrap_or_default();
|
||||
log::info!(
|
||||
"using project environment variables shell launched in {:?}. PATH={:?}",
|
||||
worktree_abs_path,
|
||||
path
|
||||
);
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.cached_shell_environments
|
||||
.insert(worktree_id, shell_env.clone());
|
||||
})
|
||||
.log_err();
|
||||
if let Some(shell_env) = shell_env.as_mut() {
|
||||
let path = shell_env
|
||||
.get("PATH")
|
||||
.map(|path| path.as_str())
|
||||
.unwrap_or_default();
|
||||
log::info!(
|
||||
"using project environment variables shell launched in {:?}. PATH={:?}",
|
||||
worktree_abs_path,
|
||||
path
|
||||
);
|
||||
|
||||
set_origin_marker(shell_env, EnvironmentOrigin::WorktreeShell);
|
||||
}
|
||||
set_origin_marker(shell_env, EnvironmentOrigin::WorktreeShell);
|
||||
}
|
||||
|
||||
if let Some(error) = error_message {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.environment_error_messages.insert(worktree_id, error);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
if let Some(error) = error_message {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.environment_error_messages.insert(worktree_id, error);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
shell_env
|
||||
})
|
||||
}
|
||||
shell_env
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,6 +187,42 @@ impl EnvironmentErrorMessage {
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_worktree_shell_environment(
|
||||
worktree_abs_path: &Path,
|
||||
load_direnv: &DirenvSettings,
|
||||
) -> (
|
||||
Option<HashMap<String, String>>,
|
||||
Option<EnvironmentErrorMessage>,
|
||||
) {
|
||||
match smol::fs::metadata(worktree_abs_path).await {
|
||||
Ok(meta) => {
|
||||
let dir = if meta.is_dir() {
|
||||
worktree_abs_path
|
||||
} else if let Some(parent) = worktree_abs_path.parent() {
|
||||
parent
|
||||
} else {
|
||||
return (
|
||||
None,
|
||||
Some(EnvironmentErrorMessage(format!(
|
||||
"Failed to load shell environment in {}: not a directory",
|
||||
worktree_abs_path.display()
|
||||
))),
|
||||
);
|
||||
};
|
||||
|
||||
load_shell_environment(&dir, load_direnv).await
|
||||
}
|
||||
Err(err) => (
|
||||
None,
|
||||
Some(EnvironmentErrorMessage(format!(
|
||||
"Failed to load shell environment in {}: {}",
|
||||
worktree_abs_path.display(),
|
||||
err
|
||||
))),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
async fn load_shell_environment(
|
||||
_dir: &Path,
|
||||
|
||||
@@ -1207,13 +1207,6 @@ impl Project {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
let tree_id = tree.read(cx).id();
|
||||
project.environment.update(cx, |environment, _| {
|
||||
environment.set_cached(&[(tree_id, HashMap::default())])
|
||||
});
|
||||
});
|
||||
|
||||
tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -10,13 +10,11 @@ use std::{
|
||||
io::{BufRead, BufReader, Read},
|
||||
ops::Range,
|
||||
path::Path,
|
||||
sync::{Arc, LazyLock, OnceLock},
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
use text::Anchor;
|
||||
use util::paths::PathMatcher;
|
||||
|
||||
static TEXT_REPLACEMENT_SPECIAL_CHARACTERS_REGEX: OnceLock<Regex> = OnceLock::new();
|
||||
|
||||
pub enum SearchResult {
|
||||
Buffer {
|
||||
buffer: Model<Buffer>,
|
||||
@@ -265,16 +263,17 @@ impl SearchQuery {
|
||||
regex, replacement, ..
|
||||
} => {
|
||||
if let Some(replacement) = replacement {
|
||||
let replacement = TEXT_REPLACEMENT_SPECIAL_CHARACTERS_REGEX
|
||||
.get_or_init(|| Regex::new(r"\\\\|\\n|\\t").unwrap())
|
||||
.replace_all(replacement, |c: &Captures| {
|
||||
match c.get(0).unwrap().as_str() {
|
||||
r"\\" => "\\",
|
||||
r"\n" => "\n",
|
||||
r"\t" => "\t",
|
||||
x => unreachable!("Unexpected escape sequence: {}", x),
|
||||
}
|
||||
});
|
||||
static TEXT_REPLACEMENT_SPECIAL_CHARACTERS_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"\\\\|\\n|\\t").unwrap());
|
||||
let replacement = TEXT_REPLACEMENT_SPECIAL_CHARACTERS_REGEX.replace_all(
|
||||
replacement,
|
||||
|c: &Captures| match c.get(0).unwrap().as_str() {
|
||||
r"\\" => "\\",
|
||||
r"\n" => "\n",
|
||||
r"\t" => "\t",
|
||||
x => unreachable!("Unexpected escape sequence: {}", x),
|
||||
},
|
||||
);
|
||||
Some(regex.replace(text, replacement))
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -433,7 +433,10 @@ impl Project {
|
||||
"windows" => "\r",
|
||||
_ => "\n",
|
||||
};
|
||||
Some(format!("{} {}{}", activate_keyword, quoted, line_ending))
|
||||
Some(format!(
|
||||
"{} {} ; clear{}",
|
||||
activate_keyword, quoted, line_ending
|
||||
))
|
||||
}
|
||||
|
||||
fn activate_python_virtual_environment(
|
||||
@@ -450,7 +453,7 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wrap_for_ssh(
|
||||
fn wrap_for_ssh(
|
||||
ssh_command: &SshCommand,
|
||||
command: Option<(&String, &Vec<String>)>,
|
||||
path: Option<&Path>,
|
||||
|
||||
@@ -43,6 +43,7 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
telemetry.workspace = true
|
||||
terminal.workspace = true
|
||||
terminal_view.workspace = true
|
||||
theme.workspace = true
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user