Compare commits
119 Commits
compl-menu
...
add_subwor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
404a268412 | ||
|
|
7aede4a348 | ||
|
|
8a724acc4d | ||
|
|
7595d36943 | ||
|
|
d25c2ff866 | ||
|
|
7f33f31ebe | ||
|
|
5df409971c | ||
|
|
8ccc7b81c8 | ||
|
|
a0e4464a33 | ||
|
|
204af9cac5 | ||
|
|
a2022d7da3 | ||
|
|
b51a28b75f | ||
|
|
dbb76100e5 | ||
|
|
6b92e0b5da | ||
|
|
2930211af9 | ||
|
|
1449377278 | ||
|
|
831930aad0 | ||
|
|
bc32b4d016 | ||
|
|
fac5118f10 | ||
|
|
7184b15f48 | ||
|
|
e82af55d64 | ||
|
|
dcd21e6f23 | ||
|
|
9efa13116d | ||
|
|
6dbc12f6af | ||
|
|
a8afc63a91 | ||
|
|
7c6feeb3a8 | ||
|
|
fadf9ff4f4 | ||
|
|
9b2bc458e3 | ||
|
|
ca9cee85e1 | ||
|
|
c01403b4b1 | ||
|
|
8ee04bf04a | ||
|
|
4ed0e5160f | ||
|
|
72e56eee7a | ||
|
|
306fc19739 | ||
|
|
4fbb568f42 | ||
|
|
7913b6a5a2 | ||
|
|
d566792ae1 | ||
|
|
62f5ca562e | ||
|
|
4eb8492308 | ||
|
|
d824baeece | ||
|
|
8a858fee7c | ||
|
|
cbd2e81a7e | ||
|
|
b25d8ecb75 | ||
|
|
7c03e11cfc | ||
|
|
f3fc4d6279 | ||
|
|
e4493d60dc | ||
|
|
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
|
||||
|
||||
62
.github/workflows/ci.yml
vendored
62
.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'
|
||||
@@ -81,6 +94,10 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
# To support writing comments that they will certainly be revisited.
|
||||
- name: Check for todo! and FIXME comments
|
||||
run: script/check-todos
|
||||
|
||||
- name: Run style checks
|
||||
uses: ./.github/actions/check_style
|
||||
|
||||
@@ -96,6 +113,7 @@ jobs:
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: check_docs_only
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -103,29 +121,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 +163,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 +174,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 +204,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 +215,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 +235,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 +246,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 +327,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
|
||||
@@ -326,6 +364,8 @@ jobs:
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -345,7 +385,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
|
||||
@@ -372,6 +412,8 @@ jobs:
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -391,7 +433,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:
|
||||
|
||||
218
Cargo.lock
generated
218
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"
|
||||
@@ -83,8 +83,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alacritty_terminal"
|
||||
version = "0.24.1-dev"
|
||||
source = "git+https://github.com/alacritty/alacritty?rev=91d034ff8b53867143c005acfaa14609147c9a2c#91d034ff8b53867143c005acfaa14609147c9a2c"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52b9241459831fee2f22fcda52ddaf9e449b6627334a0f40f13a1b3344018060"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.6.0",
|
||||
@@ -493,6 +494,7 @@ dependencies = [
|
||||
"project",
|
||||
"proto",
|
||||
"rand 0.8.5",
|
||||
"release_channel",
|
||||
"rope",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -934,7 +936,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 +1010,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 +1168,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 +1179,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 +1210,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 +1236,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 +1258,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 +1269,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 +1292,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 +1314,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 +1336,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 +1388,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 +1450,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 +1471,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 +1486,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 +1515,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 +1577,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 +1743,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 +1758,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 +1806,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 +1836,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 +1846,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 +2093,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"settings",
|
||||
"telemetry",
|
||||
"util",
|
||||
]
|
||||
|
||||
@@ -2255,9 +2234,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 +2501,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 +2526,7 @@ dependencies = [
|
||||
"settings",
|
||||
"sha2",
|
||||
"smol",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"text",
|
||||
"thiserror 1.0.69",
|
||||
@@ -2654,7 +2634,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 +2666,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"hex",
|
||||
"http_client",
|
||||
"hyper 0.14.31",
|
||||
"hyper 0.14.32",
|
||||
"indoc",
|
||||
"jsonwebtoken",
|
||||
"language",
|
||||
@@ -2781,6 +2761,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smallvec",
|
||||
"story",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"time",
|
||||
"time_format",
|
||||
@@ -2841,6 +2822,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
@@ -3700,6 +3682,7 @@ dependencies = [
|
||||
"ctor",
|
||||
"editor",
|
||||
"env_logger 0.11.5",
|
||||
"feature_flags",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
@@ -3938,6 +3921,7 @@ dependencies = [
|
||||
"snippet",
|
||||
"sum_tree",
|
||||
"task",
|
||||
"telemetry",
|
||||
"tempfile",
|
||||
"text",
|
||||
"theme",
|
||||
@@ -4373,6 +4357,7 @@ dependencies = [
|
||||
"serde_json_lenient",
|
||||
"settings",
|
||||
"task",
|
||||
"telemetry",
|
||||
"tempfile",
|
||||
"theme",
|
||||
"theme_extension",
|
||||
@@ -4406,6 +4391,7 @@ dependencies = [
|
||||
"serde",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
@@ -4422,12 +4408,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 +4786,13 @@ dependencies = [
|
||||
"rope",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shlex",
|
||||
"smol",
|
||||
"tempfile",
|
||||
"text",
|
||||
"time",
|
||||
"util",
|
||||
"which 6.0.3",
|
||||
"windows 0.58.0",
|
||||
]
|
||||
|
||||
@@ -5181,8 +5170,12 @@ dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"db",
|
||||
"editor",
|
||||
"futures 0.3.31",
|
||||
"git",
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"project",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -5604,9 +5597,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 +5622,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 +5851,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 +5866,7 @@ dependencies = [
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.8",
|
||||
"socket2 0.4.10",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -5908,7 +5901,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 +5934,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 +6612,7 @@ checksum = "58d9afa5bc6eeafb78f710a2efc585f69099f8b6a99dc7eb826581e3773a6e31"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"async-tungstenite 0.28.1",
|
||||
"async-tungstenite 0.28.2",
|
||||
"futures 0.3.31",
|
||||
"jupyter-protocol",
|
||||
"serde",
|
||||
@@ -7586,9 +7579,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",
|
||||
@@ -7710,6 +7702,7 @@ dependencies = [
|
||||
"sum_tree",
|
||||
"text",
|
||||
"theme",
|
||||
"tree-sitter",
|
||||
"util",
|
||||
]
|
||||
|
||||
@@ -7727,12 +7720,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 +9593,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"terminal",
|
||||
"text",
|
||||
"toml 0.8.19",
|
||||
"unindent",
|
||||
"url",
|
||||
"util",
|
||||
@@ -10368,7 +10361,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 +10392,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"telemetry",
|
||||
"terminal",
|
||||
"terminal_view",
|
||||
"theme",
|
||||
@@ -10425,7 +10419,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 +10664,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 +11293,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 +12657,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 +12864,6 @@ dependencies = [
|
||||
name = "theme_selector"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"client",
|
||||
"fs",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
@@ -12867,6 +12871,7 @@ dependencies = [
|
||||
"picker",
|
||||
"serde",
|
||||
"settings",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
@@ -12937,16 +12942,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 +13102,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smallvec",
|
||||
"story",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"tree-sitter-md",
|
||||
"ui",
|
||||
@@ -13469,9 +13476,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 +13593,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",
|
||||
@@ -13976,6 +13983,8 @@ dependencies = [
|
||||
"futures-lite 1.13.0",
|
||||
"git2",
|
||||
"globset",
|
||||
"itertools 0.13.0",
|
||||
"libc",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
@@ -14229,7 +14238,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"headers",
|
||||
"http 0.2.12",
|
||||
"hyper 0.14.31",
|
||||
"hyper 0.14.32",
|
||||
"log",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
@@ -14874,6 +14883,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"settings",
|
||||
"telemetry",
|
||||
"ui",
|
||||
"util",
|
||||
"vim_mode_setting",
|
||||
@@ -15980,7 +15990,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.167.0"
|
||||
version = "0.168.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -16077,6 +16087,7 @@ dependencies = [
|
||||
"tab_switcher",
|
||||
"task",
|
||||
"tasks_ui",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"terminal_view",
|
||||
"theme",
|
||||
@@ -16113,7 +16124,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_astro"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"zed_extension_api 0.1.0",
|
||||
@@ -16142,7 +16153,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 +16460,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"similar",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"theme",
|
||||
"tree-sitter-go",
|
||||
|
||||
24
Cargo.toml
24
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" }
|
||||
@@ -335,7 +337,7 @@ zeta = { path = "crates/zeta" }
|
||||
#
|
||||
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "91d034ff8b53867143c005acfaa14609147c9a2c" }
|
||||
alacritty_terminal = "0.24"
|
||||
any_vec = "0.14"
|
||||
anyhow = "1.0.86"
|
||||
arrayvec = { version = "0.7.4", features = ["serde"] }
|
||||
@@ -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"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.7633 4.2078H4.23674L4.3551 5.5189H10.1429L9.99592 6.87645H6.20408L6.33877 8.16255H9.86939L9.66122 9.92379L8 10.3275L6.3102 9.92021L6.20408 8.86633H4.7102L4.87755 10.7955L8 11.6457L11.0694 10.8812L11.7633 4.2078ZM2 2H14L12.9061 12.7818L7.98775 14L3.09388 12.7818L2 2Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.58 2H2.5V12.08C2.5 12.5892 2.70229 13.0776 3.06235 13.4376C3.42242 13.7977 3.91078 14 4.42 14H12.58C13.0892 14 13.5776 13.7977 13.9376 13.4376C14.2977 13.0776 14.5 12.5892 14.5 12.08V3.92C14.5 3.41078 14.2977 2.92242 13.9376 2.56235C13.5776 2.20229 13.0892 2 12.58 2ZM3.358 11.6285C3.34615 12.6668 3.96437 13.2311 4.96636 13.232H4.96621C6.06429 13.2456 6.70951 12.4798 6.63088 11.3867H5.48085C5.4899 11.601 5.47243 11.8974 5.36026 12.0313C5.27992 12.1441 5.16183 12.2005 5.00645 12.2005C4.67402 12.1952 4.50788 11.9534 4.50788 11.4753V9.19488C4.50788 8.94247 4.54407 8.75168 4.61645 8.62283C4.73423 8.38524 5.17961 8.3584 5.34825 8.58663C5.47804 8.71252 5.48974 9.04683 5.48101 9.26757H6.63104C6.66099 8.70582 6.53494 8.10381 6.20079 7.80913C5.65853 7.23521 4.37403 7.26765 3.82039 7.80102C3.51213 8.07495 3.358 8.47525 3.358 9.00159V11.6285ZM7.04116 11.3867C7.01043 12.4573 7.50713 13.2473 8.61739 13.232L8.61723 13.2317C10.1571 13.2967 10.5874 11.592 9.96023 10.4759C9.74995 10.1097 9.16994 9.80702 8.71379 9.62981C8.36155 9.46772 8.21038 9.3086 8.20711 8.92079C8.20711 8.55559 8.35983 8.37291 8.66543 8.37291C8.83688 8.37291 8.95357 8.42939 9.01519 8.54217C9.10317 8.6754 9.12454 9.0409 9.11565 9.26742H10.1612C10.1866 8.71627 10.0554 8.11739 9.75509 7.81303C9.26822 7.22257 7.99791 7.24909 7.5115 7.82504C7.0109 8.29179 6.97783 9.4437 7.3346 9.96848C7.49278 10.205 7.75143 10.409 8.1107 10.5809C8.15897 10.6051 8.21552 10.6314 8.27633 10.6598C8.53247 10.7792 8.86416 10.9338 8.97119 11.1046C9.16073 11.3241 9.13593 11.8913 9.00333 12.0877C8.9336 12.1952 8.81285 12.2489 8.64141 12.2489C8.25703 12.2785 8.09666 11.8534 8.12677 11.3867H7.04116ZM10.5474 11.3867C10.5167 12.4573 11.0134 13.2473 12.1236 13.232L12.1235 13.2317C13.6634 13.2967 14.0936 11.592 13.4665 10.4759C13.2562 10.1097 12.6762 9.80702 12.2201 9.62981C11.8678 9.46772 11.7166 9.3086 11.7134 8.92079C11.7134 8.55559 11.8661 8.37291 12.1717 8.37291C12.3431 8.37291 12.4598 8.42939 12.5214 8.54217C12.6094 8.6754 12.6308 9.0409 12.6219 9.26742H13.6674C13.6928 8.71627 13.5617 8.11739 13.2614 7.81303C12.7745 7.22257 11.5042 7.24909 11.0178 7.82504C10.5172 8.29179 10.4841 9.4437 10.8409 9.96848C10.999 10.205 11.2577 10.409 11.617 10.5809C11.6652 10.6051 11.7218 10.6314 11.7826 10.6598C12.0387 10.7792 12.3704 10.9338 12.4775 11.1046C12.667 11.3241 12.6422 11.8913 12.5096 12.0877C12.4399 12.1952 12.3191 12.2489 12.1477 12.2489C11.7633 12.2785 11.6029 11.8534 11.633 11.3867H10.5474Z" fill="black"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 403 B After Width: | Height: | Size: 2.6 KiB |
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 |
@@ -448,7 +448,6 @@
|
||||
"cmd-s": "workspace::Save",
|
||||
"cmd-k s": "workspace::SaveWithoutFormat",
|
||||
"cmd-shift-s": "workspace::SaveAs",
|
||||
"cmd-n": "workspace::NewFile",
|
||||
"cmd-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"cmd-1": ["workspace::ActivatePane", 0],
|
||||
@@ -495,6 +494,7 @@
|
||||
"context": "Workspace && !Terminal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "workspace::NewFile",
|
||||
"cmd-shift-r": "task::Spawn",
|
||||
"cmd-alt-r": "task::Rerun",
|
||||
"ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
|
||||
@@ -761,6 +761,7 @@
|
||||
"cmd-v": "terminal::Paste",
|
||||
"cmd-a": "editor::SelectAll",
|
||||
"cmd-k": "terminal::Clear",
|
||||
"cmd-n": "workspace::NewTerminal",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
// Some nice conveniences
|
||||
"cmd-backspace": ["terminal::SendText", "\u0015"],
|
||||
|
||||
@@ -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",
|
||||
@@ -389,6 +389,9 @@
|
||||
"bindings": {
|
||||
"w": "vim::Word",
|
||||
"shift-w": ["vim::Word", { "ignorePunctuation": true }],
|
||||
// Subword TextObject
|
||||
// "w": "vim::Subword",
|
||||
// "shift-w": ["vim::Subword", { "ignorePunctuation": true }],
|
||||
"t": "vim::Tag",
|
||||
"s": "vim::Sentence",
|
||||
"p": "vim::Paragraph",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1556,6 +1556,7 @@ impl ContextEditor {
|
||||
let mut editor = Editor::for_buffer(context.read(cx).buffer().clone(), None, cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor.set_show_line_numbers(false, cx);
|
||||
editor.set_show_scrollbars(false, cx);
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.set_show_runnables(false, cx);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -50,6 +50,7 @@ parking_lot.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
proto.workspace = true
|
||||
release_channel.workspace = true
|
||||
rope.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -3,8 +3,9 @@ use std::sync::Arc;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
list, AnyElement, AppContext, Empty, ListAlignment, ListState, Model, StyleRefinement,
|
||||
Subscription, TextStyleRefinement, View, WeakView,
|
||||
list, AbsoluteLength, AnyElement, AppContext, CornersRefinement, DefiniteLength,
|
||||
EdgesRefinement, Empty, Length, ListAlignment, ListState, Model, StyleRefinement, Subscription,
|
||||
TextStyleRefinement, View, WeakView,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::Role;
|
||||
@@ -89,10 +90,11 @@ impl ActiveThread {
|
||||
self.list_state.splice(old_len..old_len, 1);
|
||||
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
let colors = cx.theme().colors();
|
||||
let ui_font_size = TextSize::Default.rems(cx);
|
||||
let buffer_font_size = theme_settings.buffer_font_size;
|
||||
|
||||
let buffer_font_size = TextSize::Small.rems(cx);
|
||||
let mut text_style = cx.text_style();
|
||||
|
||||
text_style.refine(&TextStyleRefinement {
|
||||
font_family: Some(theme_settings.ui_font.family.clone()),
|
||||
font_size: Some(ui_font_size.into()),
|
||||
@@ -105,6 +107,32 @@ impl ActiveThread {
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: cx.theme().players().local().selection,
|
||||
code_block: StyleRefinement {
|
||||
margin: EdgesRefinement {
|
||||
top: Some(Length::Definite(rems(0.5).into())),
|
||||
left: Some(Length::Definite(rems(0.).into())),
|
||||
right: Some(Length::Definite(rems(0.).into())),
|
||||
bottom: Some(Length::Definite(rems(1.).into())),
|
||||
},
|
||||
padding: EdgesRefinement {
|
||||
top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
|
||||
left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
|
||||
right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
|
||||
bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
|
||||
},
|
||||
background: Some(colors.editor_foreground.opacity(0.01).into()),
|
||||
border_color: Some(colors.border_variant.opacity(0.25)),
|
||||
border_widths: EdgesRefinement {
|
||||
top: Some(AbsoluteLength::Pixels(Pixels(1.0))),
|
||||
left: Some(AbsoluteLength::Pixels(Pixels(1.))),
|
||||
right: Some(AbsoluteLength::Pixels(Pixels(1.))),
|
||||
bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
|
||||
},
|
||||
corner_radii: CornersRefinement {
|
||||
top_left: Some(AbsoluteLength::Pixels(Pixels(2.))),
|
||||
top_right: Some(AbsoluteLength::Pixels(Pixels(2.))),
|
||||
bottom_right: Some(AbsoluteLength::Pixels(Pixels(2.))),
|
||||
bottom_left: Some(AbsoluteLength::Pixels(Pixels(2.))),
|
||||
},
|
||||
text: Some(TextStyleRefinement {
|
||||
font_family: Some(theme_settings.buffer_font.family.clone()),
|
||||
font_size: Some(buffer_font_size.into()),
|
||||
@@ -114,8 +142,8 @@ impl ActiveThread {
|
||||
},
|
||||
inline_code: TextStyleRefinement {
|
||||
font_family: Some(theme_settings.buffer_font.family.clone()),
|
||||
font_size: Some(ui_font_size.into()),
|
||||
background_color: Some(cx.theme().colors().editor_background),
|
||||
font_size: Some(buffer_font_size.into()),
|
||||
background_color: Some(colors.editor_foreground.opacity(0.01)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
@@ -204,11 +232,12 @@ impl ActiveThread {
|
||||
};
|
||||
|
||||
let context = self.thread.read(cx).context_for_message(message_id);
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
let (role_icon, role_name) = match message.role {
|
||||
Role::User => (IconName::Person, "You"),
|
||||
Role::Assistant => (IconName::ZedAssistant, "Assistant"),
|
||||
Role::System => (IconName::Settings, "System"),
|
||||
let (role_icon, role_name, role_color) = match message.role {
|
||||
Role::User => (IconName::Person, "You", Color::Muted),
|
||||
Role::Assistant => (IconName::ZedAssistant, "Assistant", Color::Accent),
|
||||
Role::System => (IconName::Settings, "System", Color::Default),
|
||||
};
|
||||
|
||||
div()
|
||||
@@ -218,31 +247,35 @@ impl ActiveThread {
|
||||
.child(
|
||||
v_flex()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_color(colors.border_variant)
|
||||
.bg(colors.editor_background)
|
||||
.rounded_md()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.py_1p5()
|
||||
.px_2p5()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.border_color(colors.border_variant)
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(role_icon)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
.color(role_color),
|
||||
)
|
||||
.child(Label::new(role_name).size(LabelSize::XSmall)),
|
||||
.child(
|
||||
Label::new(role_name)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(role_color),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(v_flex().px_2().py_1().text_ui(cx).child(markdown.clone()))
|
||||
.child(v_flex().p_2p5().text_ui(cx).child(markdown.clone()))
|
||||
.when_some(context, |parent, context| {
|
||||
parent.child(
|
||||
h_flex().flex_wrap().gap_2().p_1p5().children(
|
||||
h_flex().flex_wrap().gap_1().p_1p5().children(
|
||||
context
|
||||
.iter()
|
||||
.map(|context| ContextPill::new(context.clone())),
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
mod active_thread;
|
||||
mod assistant_model_selector;
|
||||
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 +82,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 +90,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);
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
85
crates/assistant2/src/assistant_model_selector.rs
Normal file
85
crates/assistant2/src/assistant_model_selector.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use fs::Fs;
|
||||
use gpui::View;
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, ButtonLike, PopoverMenuHandle, Tooltip};
|
||||
|
||||
use crate::{assistant_settings::AssistantSettings, ToggleModelSelector};
|
||||
|
||||
pub struct AssistantModelSelector {
|
||||
selector: View<LanguageModelSelector>,
|
||||
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
}
|
||||
|
||||
impl AssistantModelSelector {
|
||||
pub(crate) fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self {
|
||||
Self {
|
||||
selector: cx.new_view(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _cx| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
menu_handle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AssistantModelSelector {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
let focus_handle = self.selector.focus_handle(cx).clone();
|
||||
|
||||
LanguageModelSelectorPopoverMenu::new(
|
||||
self.selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.overflow_x_hidden()
|
||||
.flex_grow()
|
||||
.whitespace_nowrap()
|
||||
.child(match active_model {
|
||||
Some(model) => h_flex()
|
||||
.child(
|
||||
Label::new(model.name().0)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element(),
|
||||
_ => Label::new("No model selected")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
|
||||
}),
|
||||
)
|
||||
.with_handle(self.menu_handle.clone())
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -9,20 +10,30 @@ use gpui::{
|
||||
WeakModel, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use release_channel::ReleaseChannel;
|
||||
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,26 +48,35 @@ 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(),
|
||||
let mut entries = Vec::new();
|
||||
entries.push(ContextPickerEntry {
|
||||
name: "File".into(),
|
||||
kind: ContextKind::File,
|
||||
icon: IconName::File,
|
||||
});
|
||||
let release_channel = ReleaseChannel::global(cx);
|
||||
// The directory context picker isn't fully implemented yet, so limit it
|
||||
// to development builds.
|
||||
if release_channel == ReleaseChannel::Dev {
|
||||
entries.push(ContextPickerEntry {
|
||||
name: "Folder".into(),
|
||||
kind: ContextKind::Directory,
|
||||
icon: IconName::Folder,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "File".into(),
|
||||
icon: IconName::File,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "Fetch".into(),
|
||||
icon: IconName::Globe,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
entries.push(ContextPickerEntry {
|
||||
name: "Fetch".into(),
|
||||
kind: ContextKind::FetchedUrl,
|
||||
icon: IconName::Globe,
|
||||
});
|
||||
|
||||
if thread_store.is_some() {
|
||||
entries.push(ContextPickerEntry {
|
||||
name: "Thread".into(),
|
||||
kind: ContextKind::Thread,
|
||||
icon: IconName::MessageCircle,
|
||||
});
|
||||
}
|
||||
@@ -66,6 +86,7 @@ impl ContextPicker {
|
||||
workspace,
|
||||
thread_store,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
entries,
|
||||
selected_ix: 0,
|
||||
};
|
||||
@@ -92,6 +113,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 +128,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 +140,7 @@ impl Render for ContextPicker {
|
||||
#[derive(Clone)]
|
||||
struct ContextPickerEntry {
|
||||
name: SharedString,
|
||||
kind: ContextKind,
|
||||
icon: IconName,
|
||||
}
|
||||
|
||||
@@ -123,6 +149,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 +182,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 +242,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(_) => {}
|
||||
})
|
||||
|
||||
249
crates/assistant2/src/context_picker/directory_context_picker.rs
Normal file
249
crates/assistant2/src/context_picker/directory_context_picker.rs
Normal file
@@ -0,0 +1,249 @@
|
||||
// TODO: Remove this when we finish the implementation.
|
||||
#![allow(unused)]
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
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::ContextKind;
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
fn search(
|
||||
&mut self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &View<Workspace>,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
if query.is_empty() {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().read(cx);
|
||||
let directory_matches = project.worktrees(cx).flat_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let path_prefix: Arc<str> = worktree.root_name().into();
|
||||
worktree.directories(false, 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: true,
|
||||
})
|
||||
});
|
||||
|
||||
Task::ready(directory_matches.collect())
|
||||
} else {
|
||||
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
|
||||
let candidate_sets = worktrees
|
||||
.into_iter()
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
|
||||
PathMatchCandidateSet {
|
||||
snapshot: worktree.snapshot(),
|
||||
include_ignored: worktree
|
||||
.root_entry()
|
||||
.map_or(false, |entry| entry.is_ignored),
|
||||
include_root_name: true,
|
||||
candidates: project::Candidates::Directories,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
fuzzy::match_path_sets(
|
||||
candidate_sets.as_slice(),
|
||||
query.as_str(),
|
||||
None,
|
||||
false,
|
||||
100,
|
||||
&cancellation_flag,
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<()> {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
let search_task = self.search(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let mut paths = search_task.await;
|
||||
let empty_path = Path::new("");
|
||||
paths.retain(|path_match| path_match.path.as_ref() != empty_path);
|
||||
|
||||
this.update(&mut cx, |this, _cx| {
|
||||
this.delegate.matches = paths;
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let Some(mat) = self.matches.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(project) = workspace
|
||||
.upgrade()
|
||||
.map(|workspace| workspace.read(cx).project().clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
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 {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let mut text = String::new();
|
||||
|
||||
// TODO: Add the files from the selected directory.
|
||||
|
||||
this.delegate
|
||||
.context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
context_store.insert_context(
|
||||
ContextKind::Directory,
|
||||
path.to_string_lossy().to_string(),
|
||||
text,
|
||||
);
|
||||
})?;
|
||||
|
||||
match confirm_behavior {
|
||||
ConfirmBehavior::KeepOpen => {}
|
||||
ConfirmBehavior::Close => this.delegate.dismissed(cx),
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(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> {
|
||||
let path_match = &self.matches[ix];
|
||||
let directory_name = path_match.path.to_string_lossy().to_string();
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.child(h_flex().gap_2().child(Label::new(directory_name))),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -190,7 +192,9 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let mat = &self.matches[self.selected_index];
|
||||
let Some(mat) = self.matches.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(project) = workspace
|
||||
@@ -201,6 +205,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 +219,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 +267,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,
|
||||
}
|
||||
@@ -146,7 +154,9 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let entry = &self.matches[self.selected_index];
|
||||
let Some(entry) = self.matches.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(thread_store) = self.thread_store.upgrade() else {
|
||||
return;
|
||||
@@ -180,6 +190,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>>) {
|
||||
@@ -203,7 +218,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.child(thread.summary.clone()),
|
||||
.child(Label::new(thread.summary.clone())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
1098
crates/assistant2/src/inline_prompt_editor.rs
Normal file
1098
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 settings::{update_settings_file, Settings};
|
||||
use language_model_selector::LanguageModelSelector;
|
||||
use rope::Point;
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding, PopoverMenuHandle,
|
||||
Tooltip,
|
||||
prelude::*, ButtonLike, ElevationIndex, KeyBinding, PopoverMenu, PopoverMenuHandle,
|
||||
SwitchWithLabel,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::assistant_settings::AssistantSettings;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::assistant_model_selector::AssistantModelSelector;
|
||||
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>,
|
||||
language_model_selector: View<LanguageModelSelector>,
|
||||
language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
inline_context_picker: View<ContextPicker>,
|
||||
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
model_selector: View<AssistantModelSelector>,
|
||||
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
use_tools: bool,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl MessageEditor {
|
||||
@@ -42,14 +49,32 @@ 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 model_selector_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,26 +91,19 @@ impl MessageEditor {
|
||||
)
|
||||
}),
|
||||
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, _cx| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
inline_context_picker,
|
||||
inline_context_picker_menu_handle,
|
||||
model_selector: cx.new_view(|cx| {
|
||||
AssistantModelSelector::new(fs, model_selector_menu_handle.clone(), cx)
|
||||
}),
|
||||
language_model_selector_menu_handle: PopoverMenuHandle::default(),
|
||||
model_selector_menu_handle,
|
||||
use_tools: false,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
|
||||
self.language_model_selector_menu_handle.toggle(cx);
|
||||
self.model_selector_menu_handle.toggle(cx)
|
||||
}
|
||||
|
||||
fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
|
||||
@@ -143,48 +161,38 @@ impl MessageEditor {
|
||||
None
|
||||
}
|
||||
|
||||
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();
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
LanguageModelSelectorPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.overflow_x_hidden()
|
||||
.flex_grow()
|
||||
.whitespace_nowrap()
|
||||
.child(match active_model {
|
||||
Some(model) => h_flex()
|
||||
.child(
|
||||
Label::new(model.name().0)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element(),
|
||||
_ => Label::new("No model selected")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
|
||||
}),
|
||||
)
|
||||
.with_handle(self.language_model_selector_menu_handle.clone())
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,6 +207,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 +220,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,11 +241,22 @@ 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()
|
||||
.child(CheckboxWithLabel::new(
|
||||
.child(SwitchWithLabel::new(
|
||||
"use-tools",
|
||||
Label::new("Tools"),
|
||||
self.use_tools.into(),
|
||||
@@ -248,22 +268,19 @@ impl Render for MessageEditor {
|
||||
}),
|
||||
))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(self.render_language_model_selector(cx))
|
||||
.child(
|
||||
ButtonLike::new("chat")
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.child(Label::new("Submit"))
|
||||
.children(
|
||||
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
)
|
||||
.on_click(move |_event, cx| {
|
||||
focus_handle.dispatch_action(&Chat, cx);
|
||||
}),
|
||||
),
|
||||
h_flex().gap_1().child(self.model_selector.clone()).child(
|
||||
ButtonLike::new("chat")
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.child(Label::new("Submit"))
|
||||
.children(
|
||||
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
)
|
||||
.on_click(move |_event, cx| {
|
||||
focus_handle.dispatch_action(&Chat, cx);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
},
|
||||
|
||||
@@ -39,10 +39,17 @@ impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
|
||||
impl Render for Breadcrumbs {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
const MAX_SEGMENTS: usize = 12;
|
||||
let element = h_flex().text_ui(cx);
|
||||
|
||||
let element = h_flex()
|
||||
.id("breadcrumb-container")
|
||||
.flex_grow()
|
||||
.overflow_x_scroll()
|
||||
.text_ui(cx);
|
||||
|
||||
let Some(active_item) = self.active_item.as_ref() else {
|
||||
return element;
|
||||
};
|
||||
|
||||
let Some(mut segments) = active_item.breadcrumbs(cx.theme(), cx) else {
|
||||
return element;
|
||||
};
|
||||
@@ -52,6 +59,7 @@ impl Render for Breadcrumbs {
|
||||
prefix_end_ix,
|
||||
segments.len().saturating_sub(MAX_SEGMENTS / 2),
|
||||
);
|
||||
|
||||
if suffix_start_ix > prefix_end_ix {
|
||||
segments.splice(
|
||||
prefix_end_ix..suffix_start_ix,
|
||||
@@ -82,6 +90,7 @@ impl Render for Breadcrumbs {
|
||||
});
|
||||
|
||||
let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
|
||||
|
||||
match active_item
|
||||
.downcast::<Editor>()
|
||||
.map(|editor| editor.downgrade())
|
||||
@@ -102,14 +111,14 @@ impl Render for Breadcrumbs {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
let focus_handle = editor.read(cx).focus_handle(cx);
|
||||
Tooltip::for_action_in(
|
||||
"Show symbol outline",
|
||||
"Show Symbol Outline",
|
||||
&editor::actions::ToggleOutline,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
Tooltip::for_action(
|
||||
"Show symbol outline",
|
||||
"Show Symbol Outline",
|
||||
&editor::actions::ToggleOutline,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -18,6 +18,12 @@ use std::{
|
||||
use tempfile::NamedTempFile;
|
||||
use util::paths::PathWithPosition;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
use {
|
||||
std::io::IsTerminal,
|
||||
util::{load_login_shell_environment, load_shell_from_passwd, ResultExt},
|
||||
};
|
||||
|
||||
struct Detect;
|
||||
|
||||
trait InstalledApp {
|
||||
@@ -161,7 +167,16 @@ fn main() -> Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
// On Linux, desktop entry uses `cli` to spawn `zed`, so we need to load env vars from the shell
|
||||
// since it doesn't inherit env vars from the terminal.
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
if !std::io::stdout().is_terminal() {
|
||||
load_shell_from_passwd().log_err();
|
||||
load_login_shell_environment().log_err();
|
||||
}
|
||||
|
||||
let env = Some(std::env::vars().collect::<HashMap<_, _>>());
|
||||
|
||||
let exit_status = Arc::new(Mutex::new(None));
|
||||
let mut paths = vec![];
|
||||
let mut urls = vec![];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -252,7 +252,7 @@ spec:
|
||||
value: "${AUTO_JOIN_CHANNEL_ID}"
|
||||
securityContext:
|
||||
capabilities:
|
||||
# FIXME - Switch to the more restrictive `PERFMON` capability.
|
||||
# TODO - Switch to the more restrictive `PERFMON` capability.
|
||||
# This capability isn't yet available in a stable version of Debian.
|
||||
add: ["SYS_ADMIN"]
|
||||
terminationGracePeriodSeconds: 10
|
||||
|
||||
@@ -279,6 +279,7 @@ pub async fn post_panic(
|
||||
|
||||
let report: telemetry_events::PanicRequest = serde_json::from_slice(&body)
|
||||
.map_err(|_| Error::http(StatusCode::BAD_REQUEST, "invalid json".into()))?;
|
||||
let incident_id = uuid::Uuid::new_v4().to_string();
|
||||
let panic = report.panic;
|
||||
|
||||
if panic.os_name == "Linux" && panic.os_version == Some("1.0.0".to_string()) {
|
||||
@@ -288,11 +289,37 @@ pub async fn post_panic(
|
||||
))?;
|
||||
}
|
||||
|
||||
if let Some(blob_store_client) = app.blob_store_client.as_ref() {
|
||||
let response = blob_store_client
|
||||
.head_object()
|
||||
.bucket(CRASH_REPORTS_BUCKET)
|
||||
.key(incident_id.clone() + ".json")
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if response.is_ok() {
|
||||
log::info!("We've already uploaded this crash");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
blob_store_client
|
||||
.put_object()
|
||||
.bucket(CRASH_REPORTS_BUCKET)
|
||||
.key(incident_id.clone() + ".json")
|
||||
.acl(aws_sdk_s3::types::ObjectCannedAcl::PublicRead)
|
||||
.body(ByteStream::from(body.to_vec()))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| log::error!("Failed to upload crash: {}", e))
|
||||
.ok();
|
||||
}
|
||||
|
||||
tracing::error!(
|
||||
service = "client",
|
||||
version = %panic.app_version,
|
||||
os_name = %panic.os_name,
|
||||
os_version = %panic.os_version.clone().unwrap_or_default(),
|
||||
incident_id = %incident_id,
|
||||
installation_id = %panic.installation_id.clone().unwrap_or_default(),
|
||||
description = %panic.payload,
|
||||
backtrace = %panic.backtrace.join("\n"),
|
||||
@@ -331,10 +358,19 @@ pub async fn post_panic(
|
||||
panic.app_version
|
||||
)))
|
||||
.add_field({
|
||||
let hostname = app.config.blob_store_url.clone().unwrap_or_default();
|
||||
let hostname = hostname.strip_prefix("https://").unwrap_or_else(|| {
|
||||
hostname.strip_prefix("http://").unwrap_or_default()
|
||||
});
|
||||
|
||||
slack::Text::markdown(format!(
|
||||
"*OS:*\n{} {}",
|
||||
"*{} {}:*\n<https://{}.{}/{}.json|{}…>",
|
||||
panic.os_name,
|
||||
panic.os_version.unwrap_or_default()
|
||||
panic.os_version.unwrap_or_default(),
|
||||
CRASH_REPORTS_BUCKET,
|
||||
hostname,
|
||||
incident_id,
|
||||
incident_id.chars().take(8).collect::<String>(),
|
||||
))
|
||||
})
|
||||
})
|
||||
@@ -361,6 +397,12 @@ pub async fn post_panic(
|
||||
}
|
||||
|
||||
fn report_to_slack(panic: &Panic) -> bool {
|
||||
// Panics on macOS should make their way to Slack as a crash report,
|
||||
// so we don't need to send them a second time via this channel.
|
||||
if panic.os_name == "macOS" {
|
||||
return false;
|
||||
}
|
||||
|
||||
if panic.payload.contains("ERROR_SURFACE_LOST_KHR") {
|
||||
return false;
|
||||
}
|
||||
@@ -610,6 +652,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");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ collections.workspace = true
|
||||
ctor.workspace = true
|
||||
editor.workspace = true
|
||||
env_logger.workspace = true
|
||||
feature_flags.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@@ -14,6 +14,7 @@ use editor::{
|
||||
scroll::Autoscroll,
|
||||
Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
|
||||
};
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use gpui::{
|
||||
actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,
|
||||
FocusableView, Global, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement,
|
||||
@@ -21,7 +22,8 @@ use gpui::{
|
||||
WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal,
|
||||
Bias, Buffer, BufferRow, BufferSnapshot, Diagnostic, DiagnosticEntry, DiagnosticSeverity,
|
||||
Point, Selection, SelectionGoal, ToTreeSitterPoint,
|
||||
};
|
||||
use lsp::LanguageServerId;
|
||||
use project::{DiagnosticSummary, Project, ProjectPath};
|
||||
@@ -29,9 +31,10 @@ use project_diagnostics_settings::ProjectDiagnosticsSettings;
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cmp,
|
||||
cmp::Ordering,
|
||||
mem,
|
||||
ops::Range,
|
||||
ops::{Range, RangeInclusive},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
@@ -41,6 +44,7 @@ use ui::{h_flex, prelude::*, Icon, IconName, Label};
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
|
||||
searchable::SearchableItemHandle,
|
||||
ItemNavHistory, ToolbarItemLocation, Workspace,
|
||||
};
|
||||
|
||||
@@ -421,31 +425,28 @@ impl ProjectDiagnosticsEditor {
|
||||
blocks: Default::default(),
|
||||
block_count: 0,
|
||||
};
|
||||
let mut pending_range: Option<(Range<Point>, usize)> = None;
|
||||
let mut pending_range: Option<(Range<Point>, Range<Point>, usize)> = None;
|
||||
let mut is_first_excerpt_for_group = true;
|
||||
for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
|
||||
let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
|
||||
if let Some((range, start_ix)) = &mut pending_range {
|
||||
if let Some(entry) = resolved_entry.as_ref() {
|
||||
if entry.range.start.row <= range.end.row + 1 + self.context * 2 {
|
||||
range.end = range.end.max(entry.range.end);
|
||||
let expanded_range = resolved_entry.as_ref().map(|entry| {
|
||||
context_range_for_entry(entry, self.context, &snapshot, cx)
|
||||
});
|
||||
if let Some((range, context_range, start_ix)) = &mut pending_range {
|
||||
if let Some(expanded_range) = expanded_range.clone() {
|
||||
// If the entries are overlapping or next to each-other, merge them into one excerpt.
|
||||
if context_range.end.row + 1 >= expanded_range.start.row {
|
||||
context_range.end = context_range.end.max(expanded_range.end);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let excerpt_start =
|
||||
Point::new(range.start.row.saturating_sub(self.context), 0);
|
||||
let excerpt_end = snapshot.clip_point(
|
||||
Point::new(range.end.row + self.context, u32::MAX),
|
||||
Bias::Left,
|
||||
);
|
||||
|
||||
let excerpt_id = excerpts
|
||||
.insert_excerpts_after(
|
||||
prev_excerpt_id,
|
||||
buffer.clone(),
|
||||
[ExcerptRange {
|
||||
context: excerpt_start..excerpt_end,
|
||||
context: context_range.clone(),
|
||||
primary: Some(range.clone()),
|
||||
}],
|
||||
cx,
|
||||
@@ -502,8 +503,9 @@ impl ProjectDiagnosticsEditor {
|
||||
pending_range.take();
|
||||
}
|
||||
|
||||
if let Some(entry) = resolved_entry {
|
||||
pending_range = Some((entry.range.clone(), ix));
|
||||
if let Some(entry) = resolved_entry.as_ref() {
|
||||
let range = entry.range.clone();
|
||||
pending_range = Some((range, expanded_range.unwrap(), ix));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -810,6 +812,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
|
||||
}
|
||||
@@ -918,3 +924,169 @@ fn compare_diagnostics(
|
||||
})
|
||||
.then_with(|| old.diagnostic.message.cmp(&new.diagnostic.message))
|
||||
}
|
||||
|
||||
const DIAGNOSTIC_EXPANSION_ROW_LIMIT: u32 = 32;
|
||||
|
||||
fn context_range_for_entry(
|
||||
entry: &DiagnosticEntry<Point>,
|
||||
context: u32,
|
||||
snapshot: &BufferSnapshot,
|
||||
cx: &AppContext,
|
||||
) -> Range<Point> {
|
||||
if cx.is_staff() {
|
||||
if let Some(rows) = heuristic_syntactic_expand(
|
||||
entry.range.clone(),
|
||||
DIAGNOSTIC_EXPANSION_ROW_LIMIT,
|
||||
snapshot,
|
||||
cx,
|
||||
) {
|
||||
return Range {
|
||||
start: Point::new(*rows.start(), 0),
|
||||
end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
|
||||
};
|
||||
}
|
||||
}
|
||||
Range {
|
||||
start: Point::new(entry.range.start.row.saturating_sub(context), 0),
|
||||
end: snapshot.clip_point(
|
||||
Point::new(entry.range.end.row + context, u32::MAX),
|
||||
Bias::Left,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Expands the input range using syntax information from TreeSitter. This expansion will be limited
|
||||
/// to the specified `max_row_count`.
|
||||
///
|
||||
/// If there is a containing outline item that is less than `max_row_count`, it will be returned.
|
||||
/// Otherwise fairly arbitrary heuristics are applied to attempt to return a logical block of code.
|
||||
fn heuristic_syntactic_expand<'a>(
|
||||
input_range: Range<Point>,
|
||||
max_row_count: u32,
|
||||
snapshot: &'a BufferSnapshot,
|
||||
cx: &'a AppContext,
|
||||
) -> Option<RangeInclusive<BufferRow>> {
|
||||
let input_row_count = input_range.end.row - input_range.start.row;
|
||||
if input_row_count > max_row_count {
|
||||
return None;
|
||||
}
|
||||
|
||||
// If the outline node contains the diagnostic and is small enough, just use that.
|
||||
let outline_range = snapshot.outline_range_containing(input_range.clone());
|
||||
if let Some(outline_range) = outline_range.clone() {
|
||||
// Remove blank lines from start and end
|
||||
if let Some(start_row) = (outline_range.start.row..outline_range.end.row)
|
||||
.find(|row| !snapshot.line_indent_for_row(*row).is_line_blank())
|
||||
{
|
||||
if let Some(end_row) = (outline_range.start.row..outline_range.end.row + 1)
|
||||
.rev()
|
||||
.find(|row| !snapshot.line_indent_for_row(*row).is_line_blank())
|
||||
{
|
||||
let row_count = end_row.saturating_sub(start_row);
|
||||
if row_count <= max_row_count {
|
||||
return Some(RangeInclusive::new(
|
||||
outline_range.start.row,
|
||||
outline_range.end.row,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut node = snapshot.syntax_ancestor(input_range.clone())?;
|
||||
loop {
|
||||
let node_start = Point::from_ts_point(node.start_position());
|
||||
let node_end = Point::from_ts_point(node.end_position());
|
||||
let node_range = node_start..node_end;
|
||||
let row_count = node_end.row - node_start.row + 1;
|
||||
|
||||
// Stop if we've exceeded the row count or reached an outline node. Then, find the interval
|
||||
// of node children which contains the query range. For example, this allows just returning
|
||||
// the header of a declaration rather than the entire declaration.
|
||||
if row_count > max_row_count || outline_range == Some(node_range.clone()) {
|
||||
let mut cursor = node.walk();
|
||||
let mut included_child_start = None;
|
||||
let mut included_child_end = None;
|
||||
let mut previous_end = node_start;
|
||||
if cursor.goto_first_child() {
|
||||
loop {
|
||||
let child_node = cursor.node();
|
||||
let child_range = previous_end..Point::from_ts_point(child_node.end_position());
|
||||
if included_child_start.is_none() && child_range.contains(&input_range.start) {
|
||||
included_child_start = Some(child_range.start);
|
||||
}
|
||||
if child_range.contains(&input_range.end) {
|
||||
included_child_end = Some(child_range.end);
|
||||
}
|
||||
previous_end = child_range.end;
|
||||
if !cursor.goto_next_sibling() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let end = included_child_end.unwrap_or(node_range.end);
|
||||
if let Some(start) = included_child_start {
|
||||
let row_count = end.row - start.row;
|
||||
if row_count < max_row_count {
|
||||
return Some(RangeInclusive::new(start.row, end.row));
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Expanding to ancestor started on {} node exceeding row limit of {max_row_count}.",
|
||||
node.grammar_name()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let node_name = node.grammar_name();
|
||||
let node_row_range = RangeInclusive::new(node_range.start.row, node_range.end.row);
|
||||
if node_name.ends_with("block") {
|
||||
return Some(node_row_range);
|
||||
} else if node_name.ends_with("statement") || node_name.ends_with("declaration") {
|
||||
// Expand to the nearest dedent or blank line for statements and declarations.
|
||||
let tab_size = snapshot.settings_at(node_range.start, cx).tab_size.get();
|
||||
let indent_level = snapshot
|
||||
.line_indent_for_row(node_range.start.row)
|
||||
.len(tab_size);
|
||||
let rows_remaining = max_row_count.saturating_sub(row_count);
|
||||
let Some(start_row) = (node_range.start.row.saturating_sub(rows_remaining)
|
||||
..node_range.start.row)
|
||||
.rev()
|
||||
.find(|row| is_line_blank_or_indented_less(indent_level, *row, tab_size, snapshot))
|
||||
else {
|
||||
return Some(node_row_range);
|
||||
};
|
||||
let rows_remaining = max_row_count.saturating_sub(node_range.end.row - start_row);
|
||||
let Some(end_row) = (node_range.end.row + 1
|
||||
..cmp::min(
|
||||
node_range.end.row + rows_remaining + 1,
|
||||
snapshot.row_count(),
|
||||
))
|
||||
.find(|row| is_line_blank_or_indented_less(indent_level, *row, tab_size, snapshot))
|
||||
else {
|
||||
return Some(node_row_range);
|
||||
};
|
||||
return Some(RangeInclusive::new(start_row, end_row));
|
||||
}
|
||||
|
||||
// TODO: doing this instead of walking a cursor as that doesn't work - why?
|
||||
let Some(parent) = node.parent() else {
|
||||
log::info!(
|
||||
"Expanding to ancestor reached the top node, so using default context line count.",
|
||||
);
|
||||
return None;
|
||||
};
|
||||
node = parent;
|
||||
}
|
||||
}
|
||||
|
||||
fn is_line_blank_or_indented_less(
|
||||
indent_level: u32,
|
||||
row: u32,
|
||||
tab_size: u32,
|
||||
snapshot: &BufferSnapshot,
|
||||
) -> bool {
|
||||
let line_indent = snapshot.line_indent_for_row(row);
|
||||
line_indent.is_line_blank() || line_indent.len(tab_size) < indent_level
|
||||
}
|
||||
|
||||
@@ -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,11 +1,8 @@
|
||||
use std::cell::RefCell;
|
||||
use std::{cell::Cell, cmp::Reverse, ops::Range, rc::Rc};
|
||||
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
div, px, uniform_list, AnyElement, BackgroundExecutor, Div, FontWeight, ListSizingBehavior,
|
||||
Model, ScrollStrategy, SharedString, StrikethroughStyle, StyledText, UniformListScrollHandle,
|
||||
ViewContext, WeakView,
|
||||
Model, ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText,
|
||||
UniformListScrollHandle, ViewContext, WeakView,
|
||||
};
|
||||
use language::Buffer;
|
||||
use language::{CodeLabel, Documentation};
|
||||
@@ -13,13 +10,16 @@ use lsp::LanguageServerId;
|
||||
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 std::{
|
||||
cell::RefCell,
|
||||
cmp::{min, Reverse},
|
||||
iter,
|
||||
ops::Range,
|
||||
rc::Rc,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use task::ResolvedTask;
|
||||
use ui::{prelude::*, Color, IntoElement, ListItem, Pixels, Popover, Styled};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{
|
||||
@@ -30,6 +30,11 @@ use crate::{
|
||||
};
|
||||
use crate::{AcceptInlineCompletion, InlineCompletionMenuHint, InlineCompletionText};
|
||||
|
||||
pub const MENU_GAP: Pixels = px(4.);
|
||||
pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
|
||||
pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
|
||||
pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
|
||||
|
||||
pub enum CodeContextMenu {
|
||||
Completions(CompletionsMenu),
|
||||
CodeActions(CodeActionsMenu),
|
||||
@@ -113,20 +118,31 @@ 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_size: Size<Pixels>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<AnyElement> {
|
||||
match self {
|
||||
CodeContextMenu::Completions(menu) => menu.render_aside(style, max_size, workspace, cx),
|
||||
CodeContextMenu::CodeActions(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ContextMenuOrigin {
|
||||
@@ -146,8 +162,8 @@ pub struct CompletionsMenu {
|
||||
pub selected_item: usize,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
resolve_completions: bool,
|
||||
pub aside_was_displayed: Cell<bool>,
|
||||
show_completion_documentation: bool,
|
||||
last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -164,7 +180,6 @@ impl CompletionsMenu {
|
||||
initial_position: Anchor,
|
||||
buffer: Model<Buffer>,
|
||||
completions: Box<[Completion]>,
|
||||
aside_was_displayed: bool,
|
||||
) -> Self {
|
||||
let match_candidates = completions
|
||||
.iter()
|
||||
@@ -184,7 +199,7 @@ impl CompletionsMenu {
|
||||
selected_item: 0,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
resolve_completions: true,
|
||||
aside_was_displayed: Cell::new(aside_was_displayed),
|
||||
last_rendered_range: RefCell::new(None).into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,8 +255,8 @@ impl CompletionsMenu {
|
||||
selected_item: 0,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
resolve_completions: false,
|
||||
aside_was_displayed: Cell::new(false),
|
||||
show_completion_documentation: false,
|
||||
last_rendered_range: RefCell::new(None).into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,11 +265,7 @@ impl CompletionsMenu {
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.selected_item = 0;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_selected_completion(provider, cx);
|
||||
cx.notify();
|
||||
self.update_selection_index(0, provider, cx);
|
||||
}
|
||||
|
||||
fn select_prev(
|
||||
@@ -262,15 +273,7 @@ impl CompletionsMenu {
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
if self.selected_item > 0 {
|
||||
self.selected_item -= 1;
|
||||
} else {
|
||||
self.selected_item = self.entries.len() - 1;
|
||||
}
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_selected_completion(provider, cx);
|
||||
cx.notify();
|
||||
self.update_selection_index(self.prev_match_index(), provider, cx);
|
||||
}
|
||||
|
||||
fn select_next(
|
||||
@@ -278,15 +281,7 @@ impl CompletionsMenu {
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
if self.selected_item + 1 < self.entries.len() {
|
||||
self.selected_item += 1;
|
||||
} else {
|
||||
self.selected_item = 0;
|
||||
}
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_selected_completion(provider, cx);
|
||||
cx.notify();
|
||||
self.update_selection_index(self.next_match_index(), provider, cx);
|
||||
}
|
||||
|
||||
fn select_last(
|
||||
@@ -294,11 +289,38 @@ impl CompletionsMenu {
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.selected_item = self.entries.len() - 1;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_selected_completion(provider, cx);
|
||||
cx.notify();
|
||||
self.update_selection_index(self.entries.len() - 1, provider, cx);
|
||||
}
|
||||
|
||||
fn update_selection_index(
|
||||
&mut self,
|
||||
match_index: usize,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
if self.selected_item != match_index {
|
||||
self.selected_item = match_index;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_visible_completions(provider, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn prev_match_index(&self) -> usize {
|
||||
if self.selected_item > 0 {
|
||||
self.selected_item - 1
|
||||
} else {
|
||||
self.entries.len() - 1
|
||||
}
|
||||
}
|
||||
|
||||
fn next_match_index(&self) -> usize {
|
||||
if self.selected_item + 1 < self.entries.len() {
|
||||
self.selected_item + 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_inline_completion_hint(&mut self, hint: InlineCompletionMenuHint) {
|
||||
@@ -318,10 +340,12 @@ 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(
|
||||
pub fn resolve_visible_completions(
|
||||
&mut self,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
@@ -333,24 +357,76 @@ impl CompletionsMenu {
|
||||
return;
|
||||
};
|
||||
|
||||
match &self.entries[self.selected_item] {
|
||||
CompletionEntry::Match(entry) => {
|
||||
let completion_index = entry.candidate_id;
|
||||
let resolve_task = provider.resolve_completions(
|
||||
self.buffer.clone(),
|
||||
vec![completion_index],
|
||||
self.completions.clone(),
|
||||
cx,
|
||||
);
|
||||
// Attempt to resolve completions for every item that will be displayed. This matters
|
||||
// because single line documentation may be displayed inline with the completion.
|
||||
//
|
||||
// When navigating to the very beginning or end of completions, `last_rendered_range` may
|
||||
// have no overlap with the completions that will be displayed, so instead use a range based
|
||||
// on the last rendered count.
|
||||
const APPROXIMATE_VISIBLE_COUNT: usize = 12;
|
||||
let last_rendered_range = self.last_rendered_range.borrow().clone();
|
||||
let visible_count = last_rendered_range
|
||||
.clone()
|
||||
.map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
|
||||
let entry_range = if self.selected_item == 0 {
|
||||
0..min(visible_count, self.entries.len())
|
||||
} else if self.selected_item == self.entries.len() - 1 {
|
||||
self.entries.len().saturating_sub(visible_count)..self.entries.len()
|
||||
} else {
|
||||
last_rendered_range.map_or(0..0, |range| {
|
||||
min(range.start, self.entries.len())..min(range.end, self.entries.len())
|
||||
})
|
||||
};
|
||||
|
||||
cx.spawn(move |editor, mut cx| async move {
|
||||
if let Some(true) = resolve_task.await.log_err() {
|
||||
editor.update(&mut cx, |_, cx| cx.notify()).ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
// Expand the range to resolve more completions than are predicted to be visible, to reduce
|
||||
// jank on navigation.
|
||||
const EXTRA_TO_RESOLVE: usize = 4;
|
||||
let entry_indices = util::iterate_expanded_and_wrapped_usize_range(
|
||||
entry_range.clone(),
|
||||
EXTRA_TO_RESOLVE,
|
||||
EXTRA_TO_RESOLVE,
|
||||
self.entries.len(),
|
||||
);
|
||||
|
||||
// Avoid work by sometimes filtering out completions that already have documentation.
|
||||
// This filtering doesn't happen if the completions are currently being updated.
|
||||
let completions = self.completions.borrow();
|
||||
let candidate_ids = entry_indices
|
||||
.flat_map(|i| Self::entry_candidate_id(&self.entries[i]))
|
||||
.filter(|i| completions[*i].documentation.is_none());
|
||||
|
||||
// Current selection is always resolved even if it already has documentation, to handle
|
||||
// out-of-spec language servers that return more results later.
|
||||
let candidate_ids = match Self::entry_candidate_id(&self.entries[self.selected_item]) {
|
||||
None => candidate_ids.collect::<Vec<usize>>(),
|
||||
Some(selected_candidate_id) => iter::once(selected_candidate_id)
|
||||
.chain(candidate_ids.filter(|id| *id != selected_candidate_id))
|
||||
.collect::<Vec<usize>>(),
|
||||
};
|
||||
|
||||
if candidate_ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let resolve_task = provider.resolve_completions(
|
||||
self.buffer.clone(),
|
||||
candidate_ids,
|
||||
self.completions.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn(move |editor, mut cx| async move {
|
||||
if let Some(true) = resolve_task.await.log_err() {
|
||||
editor.update(&mut cx, |_, cx| cx.notify()).ok();
|
||||
}
|
||||
CompletionEntry::InlineCompletionHint { .. } => {}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn entry_candidate_id(entry: &CompletionEntry) -> Option<usize> {
|
||||
match entry {
|
||||
CompletionEntry::Match(entry) => Some(entry.candidate_id),
|
||||
CompletionEntry::InlineCompletionHint { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,11 +442,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,75 +470,19 @@ 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 last_rendered_range = self.last_rendered_range.clone();
|
||||
let style = style.clone();
|
||||
let list = uniform_list(
|
||||
cx.view().clone(),
|
||||
"completions",
|
||||
matches.len(),
|
||||
move |_editor, range, cx| {
|
||||
last_rendered_range.borrow_mut().replace(range.clone());
|
||||
let start_ix = range.start;
|
||||
let completions_guard = completions.borrow_mut();
|
||||
|
||||
@@ -513,6 +530,7 @@ impl CompletionsMenu {
|
||||
(range, highlight)
|
||||
}),
|
||||
);
|
||||
|
||||
let completion_label =
|
||||
StyledText::new(completion.label.text.clone())
|
||||
.with_highlights(&style.text, highlights);
|
||||
@@ -563,17 +581,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 +608,70 @@ 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_size: Size<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")
|
||||
.px(MENU_ASIDE_X_PADDING / 2.)
|
||||
.max_w(max_size.width)
|
||||
.max_h(max_size.height)
|
||||
.overflow_y_scroll()
|
||||
.occlude(),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
|
||||
|
||||
@@ -128,6 +128,7 @@ use multi_buffer::{
|
||||
ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16,
|
||||
};
|
||||
use project::{
|
||||
buffer_store::BufferChangeSet,
|
||||
lsp_store::{FormatTarget, FormatTrigger, OpenLspBufferHandle},
|
||||
project_settings::{GitGutterSetting, ProjectSettings},
|
||||
CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink,
|
||||
@@ -605,6 +606,7 @@ pub struct Editor {
|
||||
mode: EditorMode,
|
||||
show_breadcrumbs: bool,
|
||||
show_gutter: bool,
|
||||
show_scrollbars: bool,
|
||||
show_line_numbers: Option<bool>,
|
||||
use_relative_line_numbers: Option<bool>,
|
||||
show_git_diff_gutter: Option<bool>,
|
||||
@@ -1234,6 +1236,7 @@ impl Editor {
|
||||
project,
|
||||
blink_manager: blink_manager.clone(),
|
||||
show_local_selections: true,
|
||||
show_scrollbars: true,
|
||||
mode,
|
||||
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
|
||||
show_gutter: mode == EditorMode::Full,
|
||||
@@ -1370,7 +1373,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
this.report_editor_event("open", None, cx);
|
||||
this.report_editor_event("Editor Opened", None, cx);
|
||||
this
|
||||
}
|
||||
|
||||
@@ -2473,7 +2476,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 +2859,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 +2880,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 +3688,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 +3722,6 @@ impl Editor {
|
||||
position,
|
||||
buffer.clone(),
|
||||
completions.into(),
|
||||
aside_was_displayed,
|
||||
);
|
||||
|
||||
menu.filter(query.as_deref(), cx.background_executor().clone())
|
||||
@@ -3744,11 +3745,14 @@ impl Editor {
|
||||
|
||||
if editor.focus_handle.is_focused(cx) && menu.is_some() {
|
||||
let mut menu = menu.unwrap();
|
||||
menu.resolve_selected_completion(editor.completion_provider.as_deref(), cx);
|
||||
menu.resolve_visible_completions(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 +3761,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 +3822,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 +4562,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 +4713,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 +4720,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 +4748,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 +4782,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 +4827,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 +4853,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 +4878,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 +5123,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_size: Size<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_size,
|
||||
self.workspace.as_ref().map(|(w, _)| w.clone()),
|
||||
cx,
|
||||
))
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -5133,7 +5153,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 +5446,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())
|
||||
@@ -8752,9 +8777,10 @@ impl Editor {
|
||||
.map(|selection| {
|
||||
let old_range = selection.start..selection.end;
|
||||
let mut new_range = old_range.clone();
|
||||
while let Some(containing_range) =
|
||||
buffer.range_for_syntax_ancestor(new_range.clone())
|
||||
let mut new_node = None;
|
||||
while let Some((node, containing_range)) = buffer.syntax_ancestor(new_range.clone())
|
||||
{
|
||||
new_node = Some(node);
|
||||
new_range = containing_range;
|
||||
if !display_map.intersects_fold(new_range.start)
|
||||
&& !display_map.intersects_fold(new_range.end)
|
||||
@@ -8763,6 +8789,17 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(node) = new_node {
|
||||
// Log the ancestor, to support using this action as a way to explore TreeSitter
|
||||
// nodes. Parent and grandparent are also logged because this operation will not
|
||||
// visit nodes that have the same range as their parent.
|
||||
log::info!("Node: {node:?}");
|
||||
let parent = node.parent();
|
||||
log::info!("Parent: {parent:?}");
|
||||
let grandparent = parent.and_then(|x| x.parent());
|
||||
log::info!("Grandparent: {grandparent:?}");
|
||||
}
|
||||
|
||||
selected_larger_node |= new_range != old_range;
|
||||
Selection {
|
||||
id: selection.id,
|
||||
@@ -10377,7 +10414,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 +10428,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn end_transaction_at(
|
||||
pub fn end_transaction_at(
|
||||
&mut self,
|
||||
now: Instant,
|
||||
cx: &mut ViewContext<Self>,
|
||||
@@ -11225,6 +11262,11 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_show_scrollbars(&mut self, show_scrollbars: bool, cx: &mut ViewContext<Self>) {
|
||||
self.show_scrollbars = show_scrollbars;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut ViewContext<Self>) {
|
||||
self.show_line_numbers = Some(show_line_numbers);
|
||||
cx.notify();
|
||||
@@ -12568,7 +12610,7 @@ impl Editor {
|
||||
|
||||
fn report_editor_event(
|
||||
&self,
|
||||
operation: &'static str,
|
||||
event_type: &'static str,
|
||||
file_extension: Option<String>,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
@@ -12605,15 +12647,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,
|
||||
@@ -12917,6 +12958,14 @@ impl Editor {
|
||||
.and_then(|item| item.to_any().downcast_ref::<T>())
|
||||
}
|
||||
|
||||
pub fn add_change_set(
|
||||
&mut self,
|
||||
change_set: Model<BufferChangeSet>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.diff_map.add_change_set(change_set, cx);
|
||||
}
|
||||
|
||||
fn character_size(&self, cx: &mut ViewContext<Self>) -> gpui::Point<Pixels> {
|
||||
let text_layout_details = self.text_layout_details(cx);
|
||||
let style = &text_layout_details.editor_style;
|
||||
@@ -14595,6 +14644,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 +14668,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
|
||||
|
||||
@@ -25,14 +25,18 @@ use language::{
|
||||
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
|
||||
use multi_buffer::MultiBufferIndentGuide;
|
||||
use parking_lot::Mutex;
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
use project::{buffer_store::BufferChangeSet, FakeFs};
|
||||
use project::{
|
||||
lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT,
|
||||
project_settings::{LspSettings, ProjectSettings},
|
||||
};
|
||||
use serde_json::{self, json};
|
||||
use std::sync::atomic::{self, AtomicBool, AtomicUsize};
|
||||
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
|
||||
use std::{
|
||||
iter,
|
||||
sync::atomic::{self, AtomicUsize},
|
||||
};
|
||||
use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
|
||||
use unindent::Unindent;
|
||||
use util::{
|
||||
@@ -10787,6 +10791,62 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
|
||||
async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let item_0 = lsp::CompletionItem {
|
||||
label: "abs".into(),
|
||||
insert_text: Some("abs".into()),
|
||||
data: Some(json!({ "very": "special"})),
|
||||
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
|
||||
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
|
||||
lsp::InsertReplaceEdit {
|
||||
new_text: "abs".to_string(),
|
||||
insert: lsp::Range::default(),
|
||||
replace: lsp::Range::default(),
|
||||
},
|
||||
)),
|
||||
..lsp::CompletionItem::default()
|
||||
};
|
||||
let items = iter::once(item_0.clone())
|
||||
.chain((11..51).map(|i| lsp::CompletionItem {
|
||||
label: format!("item_{}", i),
|
||||
insert_text: Some(format!("item_{}", i)),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
|
||||
..lsp::CompletionItem::default()
|
||||
}))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let default_commit_characters = vec!["?".to_string()];
|
||||
let default_data = json!({ "default": "data"});
|
||||
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
|
||||
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
|
||||
let default_edit_range = lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: 0,
|
||||
character: 5,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: 0,
|
||||
character: 5,
|
||||
},
|
||||
};
|
||||
|
||||
let item_0_out = lsp::CompletionItem {
|
||||
commit_characters: Some(default_commit_characters.clone()),
|
||||
insert_text_format: Some(default_insert_text_format),
|
||||
..item_0
|
||||
};
|
||||
let items_out = iter::once(item_0_out)
|
||||
.chain(items[1..].iter().map(|item| lsp::CompletionItem {
|
||||
commit_characters: Some(default_commit_characters.clone()),
|
||||
data: Some(default_data.clone()),
|
||||
insert_text_mode: Some(default_insert_text_mode),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: default_edit_range,
|
||||
new_text: item.label.clone(),
|
||||
})),
|
||||
..item.clone()
|
||||
}))
|
||||
.collect::<Vec<lsp::CompletionItem>>();
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
@@ -10803,138 +10863,15 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
|
||||
cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
|
||||
cx.simulate_keystroke(".");
|
||||
|
||||
let default_commit_characters = vec!["?".to_string()];
|
||||
let default_data = json!({ "very": "special"});
|
||||
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
|
||||
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
|
||||
let default_edit_range = lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: 0,
|
||||
character: 5,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: 0,
|
||||
character: 5,
|
||||
},
|
||||
};
|
||||
|
||||
let resolve_requests_number = Arc::new(AtomicUsize::new(0));
|
||||
let expect_first_item = Arc::new(AtomicBool::new(true));
|
||||
cx.lsp
|
||||
.server
|
||||
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
|
||||
let closure_default_data = default_data.clone();
|
||||
let closure_resolve_requests_number = resolve_requests_number.clone();
|
||||
let closure_expect_first_item = expect_first_item.clone();
|
||||
let closure_default_commit_characters = default_commit_characters.clone();
|
||||
move |item_to_resolve, _| {
|
||||
closure_resolve_requests_number.fetch_add(1, atomic::Ordering::Release);
|
||||
let default_data = closure_default_data.clone();
|
||||
let default_commit_characters = closure_default_commit_characters.clone();
|
||||
let expect_first_item = closure_expect_first_item.clone();
|
||||
async move {
|
||||
if expect_first_item.load(atomic::Ordering::Acquire) {
|
||||
assert_eq!(
|
||||
item_to_resolve.label, "Some(2)",
|
||||
"Should have selected the first item"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.data,
|
||||
Some(json!({ "very": "special"})),
|
||||
"First item should bring its own data for resolving"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.commit_characters,
|
||||
Some(default_commit_characters),
|
||||
"First item had no own commit characters and should inherit the default ones"
|
||||
);
|
||||
assert!(
|
||||
matches!(
|
||||
item_to_resolve.text_edit,
|
||||
Some(lsp::CompletionTextEdit::InsertAndReplace { .. })
|
||||
),
|
||||
"First item should bring its own edit range for resolving"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.insert_text_format,
|
||||
Some(default_insert_text_format),
|
||||
"First item had no own insert text format and should inherit the default one"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.insert_text_mode,
|
||||
Some(lsp::InsertTextMode::ADJUST_INDENTATION),
|
||||
"First item should bring its own insert text mode for resolving"
|
||||
);
|
||||
Ok(item_to_resolve)
|
||||
} else {
|
||||
assert_eq!(
|
||||
item_to_resolve.label, "vec![2]",
|
||||
"Should have selected the last item"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.data,
|
||||
Some(default_data),
|
||||
"Last item has no own resolve data and should inherit the default one"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.commit_characters,
|
||||
Some(default_commit_characters),
|
||||
"Last item had no own commit characters and should inherit the default ones"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.text_edit,
|
||||
Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: default_edit_range,
|
||||
new_text: "vec![2]".to_string()
|
||||
})),
|
||||
"Last item had no own edit range and should inherit the default one"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.insert_text_format,
|
||||
Some(lsp::InsertTextFormat::PLAIN_TEXT),
|
||||
"Last item should bring its own insert text format for resolving"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.insert_text_mode,
|
||||
Some(default_insert_text_mode),
|
||||
"Last item had no own insert text mode and should inherit the default one"
|
||||
);
|
||||
|
||||
Ok(item_to_resolve)
|
||||
}
|
||||
}
|
||||
}
|
||||
}).detach();
|
||||
|
||||
let completion_data = default_data.clone();
|
||||
let completion_characters = default_commit_characters.clone();
|
||||
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
|
||||
let default_data = completion_data.clone();
|
||||
let default_commit_characters = completion_characters.clone();
|
||||
let items = items.clone();
|
||||
async move {
|
||||
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
||||
items: vec![
|
||||
lsp::CompletionItem {
|
||||
label: "Some(2)".into(),
|
||||
insert_text: Some("Some(2)".into()),
|
||||
data: Some(json!({ "very": "special"})),
|
||||
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
|
||||
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
|
||||
lsp::InsertReplaceEdit {
|
||||
new_text: "Some(2)".to_string(),
|
||||
insert: lsp::Range::default(),
|
||||
replace: lsp::Range::default(),
|
||||
},
|
||||
)),
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "vec![2]".into(),
|
||||
insert_text: Some("vec![2]".into()),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
],
|
||||
items,
|
||||
item_defaults: Some(lsp::CompletionListItemDefaults {
|
||||
data: Some(default_data.clone()),
|
||||
commit_characters: Some(default_commit_characters.clone()),
|
||||
@@ -10951,6 +10888,21 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
|
||||
.next()
|
||||
.await;
|
||||
|
||||
let resolved_items = Arc::new(Mutex::new(Vec::new()));
|
||||
cx.lsp
|
||||
.server
|
||||
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
|
||||
let closure_resolved_items = resolved_items.clone();
|
||||
move |item_to_resolve, _| {
|
||||
let closure_resolved_items = closure_resolved_items.clone();
|
||||
async move {
|
||||
closure_resolved_items.lock().push(item_to_resolve.clone());
|
||||
Ok(item_to_resolve)
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
@@ -10959,39 +10911,56 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
|
||||
match menu.as_ref().expect("should have the completions menu") {
|
||||
CodeContextMenu::Completions(completions_menu) => {
|
||||
assert_eq!(
|
||||
completion_menu_entries(&completions_menu.entries),
|
||||
vec!["Some(2)", "vec![2]"]
|
||||
completions_menu
|
||||
.entries
|
||||
.iter()
|
||||
.flat_map(|c| match c {
|
||||
CompletionEntry::Match(mat) => Some(mat.string.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<String>>(),
|
||||
items_out
|
||||
.iter()
|
||||
.map(|completion| completion.label.clone())
|
||||
.collect::<Vec<String>>()
|
||||
);
|
||||
}
|
||||
CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
|
||||
}
|
||||
});
|
||||
// Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
|
||||
// with 4 from the end.
|
||||
assert_eq!(
|
||||
resolve_requests_number.load(atomic::Ordering::Acquire),
|
||||
1,
|
||||
"While there are 2 items in the completion list, only 1 resolve request should have been sent, for the selected item"
|
||||
*resolved_items.lock(),
|
||||
[
|
||||
&items_out[0..16],
|
||||
&items_out[items_out.len() - 4..items_out.len()]
|
||||
]
|
||||
.concat()
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<lsp::CompletionItem>>()
|
||||
);
|
||||
resolved_items.lock().clear();
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.context_menu_first(&ContextMenuFirst, cx);
|
||||
editor.context_menu_prev(&ContextMenuPrev, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
// Completions that have already been resolved are skipped.
|
||||
assert_eq!(
|
||||
resolve_requests_number.load(atomic::Ordering::Acquire),
|
||||
2,
|
||||
"After re-selecting the first item, another resolve request should have been sent"
|
||||
);
|
||||
|
||||
expect_first_item.store(false, atomic::Ordering::Release);
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.context_menu_last(&ContextMenuLast, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
resolve_requests_number.load(atomic::Ordering::Acquire),
|
||||
3,
|
||||
"After selecting the other item, another resolve request should have been sent"
|
||||
*resolved_items.lock(),
|
||||
[
|
||||
// Selected item is always resolved even if it was resolved before.
|
||||
&items_out[items_out.len() - 1..items_out.len()],
|
||||
&items_out[items_out.len() - 16..items_out.len() - 4]
|
||||
]
|
||||
.concat()
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<lsp::CompletionItem>>()
|
||||
);
|
||||
resolved_items.lock().clear();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -14367,7 +14336,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 +14352,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 +14368,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 +14386,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 +14402,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 +14427,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 +14448,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 +14479,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 +14501,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
@@ -429,7 +429,7 @@ fn show_hover(
|
||||
})
|
||||
.or_else(|| {
|
||||
let snapshot = &snapshot.buffer_snapshot;
|
||||
let offset_range = snapshot.range_for_syntax_ancestor(anchor..anchor)?;
|
||||
let offset_range = snapshot.syntax_ancestor(anchor..anchor)?.1;
|
||||
Some(
|
||||
snapshot.anchor_before(offset_range.start)
|
||||
..snapshot.anchor_after(offset_range.end),
|
||||
|
||||
@@ -1155,6 +1155,11 @@ fn editor_with_deleted_text(
|
||||
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.set_show_line_numbers(false, cx);
|
||||
editor.set_show_scrollbars(false, cx);
|
||||
editor.set_show_runnables(false, cx);
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.scroll_manager.set_forbid_vertical_scroll(true);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_inline_completions(Some(false), cx);
|
||||
@@ -1166,7 +1171,7 @@ fn editor_with_deleted_text(
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None)); //
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
|
||||
editor
|
||||
._subscriptions
|
||||
.extend([cx.on_blur(&editor.focus_handle, |editor, cx| {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::Editor;
|
||||
use collections::HashMap;
|
||||
use gpui::{Model, WindowContext};
|
||||
use language::Buffer;
|
||||
use language::Language;
|
||||
@@ -20,6 +22,7 @@ where
|
||||
return None;
|
||||
};
|
||||
let multibuffer = editor.buffer().read(cx);
|
||||
let mut language_servers_for = HashMap::default();
|
||||
editor
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
@@ -28,27 +31,36 @@ where
|
||||
.filter_map(|selection| Some((selection.start.buffer_id?, selection.start)))
|
||||
.filter_map(|(buffer_id, trigger_anchor)| {
|
||||
let buffer = multibuffer.buffer(buffer_id)?;
|
||||
let server_id = *match language_servers_for.entry(buffer_id) {
|
||||
Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
|
||||
Entry::Vacant(vacant_entry) => {
|
||||
let language_server_id = project
|
||||
.read(cx)
|
||||
.language_servers_for_local_buffer(buffer.read(cx), cx)
|
||||
.find_map(|(adapter, server)| {
|
||||
if adapter.name.0.as_ref() == language_server_name {
|
||||
Some(server.server_id())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
vacant_entry.insert(language_server_id)
|
||||
}
|
||||
}
|
||||
.as_ref()?;
|
||||
|
||||
Some((buffer, trigger_anchor, server_id))
|
||||
})
|
||||
.find_map(|(buffer, trigger_anchor, server_id)| {
|
||||
let language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?;
|
||||
if !filter_language(&language) {
|
||||
return None;
|
||||
}
|
||||
Some((trigger_anchor, language, buffer))
|
||||
})
|
||||
.find_map(|(trigger_anchor, language, buffer)| {
|
||||
project
|
||||
.read(cx)
|
||||
.language_servers_for_local_buffer(buffer.read(cx), cx)
|
||||
.find_map(|(adapter, server)| {
|
||||
if adapter.name.0.as_ref() == language_server_name {
|
||||
Some((
|
||||
trigger_anchor,
|
||||
Arc::clone(&language),
|
||||
server.server_id(),
|
||||
buffer.clone(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
Some((
|
||||
trigger_anchor,
|
||||
Arc::clone(&language),
|
||||
server_id,
|
||||
buffer.clone(),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -51,8 +51,8 @@ impl FileIcons {
|
||||
pub fn get_icon(path: &Path, cx: &AppContext) -> Option<SharedString> {
|
||||
let this = cx.try_global::<Self>()?;
|
||||
|
||||
// FIXME: Associate a type with the languages and have the file's language
|
||||
// override these associations
|
||||
// TODO: Associate a type with the languages and have the file's language
|
||||
// override these associations
|
||||
maybe!({
|
||||
let suffix = path.icon_stem_or_suffix()?;
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -15,7 +15,11 @@ path = "src/git_ui.rs"
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
menu.workspace = true
|
||||
project.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -1,28 +1,42 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{
|
||||
scroll::{Autoscroll, AutoscrollStrategy},
|
||||
Editor, MultiBuffer, DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
};
|
||||
use git::{diff::DiffHunk, repository::GitFileStatus};
|
||||
use gpui::{
|
||||
actions, prelude::*, uniform_list, Action, AppContext, AsyncWindowContext, ClickEvent,
|
||||
CursorStyle, EventEmitter, FocusHandle, FocusableView, KeyContext,
|
||||
ListHorizontalSizingBehavior, ListSizingBehavior, Model, Modifiers, ModifiersChangedEvent,
|
||||
MouseButton, ScrollStrategy, Stateful, Task, UniformListScrollHandle, View, WeakView,
|
||||
};
|
||||
use language::{Buffer, BufferRow, OffsetRangeExt};
|
||||
use menu::{SelectNext, SelectPrev};
|
||||
use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, WorktreeId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings as _;
|
||||
use std::{
|
||||
cell::OnceCell,
|
||||
collections::HashSet,
|
||||
ffi::OsStr,
|
||||
ops::Range,
|
||||
ops::{Deref, Range},
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
usize,
|
||||
};
|
||||
|
||||
use git::repository::GitFileStatus;
|
||||
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::*;
|
||||
use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, WorktreeId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings as _;
|
||||
use ui::{
|
||||
prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, Scrollbar, ScrollbarState, Tooltip,
|
||||
prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, ListItem, Scrollbar,
|
||||
ScrollbarState, Tooltip,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
ItemHandle, Workspace,
|
||||
};
|
||||
use workspace::dock::{DockPosition, Panel, PanelEvent};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{git_status_icon, settings::GitPanelSettings};
|
||||
use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll};
|
||||
@@ -31,6 +45,8 @@ actions!(git_panel, [ToggleFocus]);
|
||||
|
||||
const GIT_PANEL_KEY: &str = "GitPanel";
|
||||
|
||||
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(
|
||||
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
|
||||
@@ -58,6 +74,8 @@ struct EntryDetails {
|
||||
depth: usize,
|
||||
is_expanded: bool,
|
||||
status: Option<GitFileStatus>,
|
||||
hunks: Rc<OnceCell<Vec<DiffHunk>>>,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl EntryDetails {
|
||||
@@ -72,7 +90,7 @@ struct SerializedGitPanel {
|
||||
}
|
||||
|
||||
pub struct GitPanel {
|
||||
_workspace: WeakView<Workspace>,
|
||||
workspace: WeakView<Workspace>,
|
||||
current_modifiers: Modifiers,
|
||||
focus_handle: FocusHandle,
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -87,8 +105,43 @@ pub struct GitPanel {
|
||||
|
||||
// The entries that are currently shown in the panel, aka
|
||||
// not hidden by folding or such
|
||||
visible_entries: Vec<(WorktreeId, Vec<Entry>, OnceCell<HashSet<Arc<Path>>>)>,
|
||||
visible_entries: Vec<WorktreeEntries>,
|
||||
width: Option<Pixels>,
|
||||
git_diff_editor: View<Editor>,
|
||||
git_diff_editor_updates: Task<()>,
|
||||
reveal_in_editor: Task<()>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct WorktreeEntries {
|
||||
worktree_id: WorktreeId,
|
||||
visible_entries: Vec<GitPanelEntry>,
|
||||
paths: Rc<OnceCell<HashSet<Arc<Path>>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct GitPanelEntry {
|
||||
entry: Entry,
|
||||
hunks: Rc<OnceCell<Vec<DiffHunk>>>,
|
||||
}
|
||||
|
||||
impl Deref for GitPanelEntry {
|
||||
type Target = Entry;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.entry
|
||||
}
|
||||
}
|
||||
|
||||
impl WorktreeEntries {
|
||||
fn paths(&self) -> &HashSet<Arc<Path>> {
|
||||
self.paths.get_or_init(|| {
|
||||
self.visible_entries
|
||||
.iter()
|
||||
.map(|e| (e.entry.path.clone()))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl GitPanel {
|
||||
@@ -115,18 +168,28 @@ impl GitPanel {
|
||||
this.hide_scrollbar(cx);
|
||||
})
|
||||
.detach();
|
||||
cx.subscribe(&project, |this, _project, event, cx| match event {
|
||||
cx.subscribe(&project, |this, project, event, cx| match event {
|
||||
project::Event::WorktreeRemoved(id) => {
|
||||
this.expanded_dir_ids.remove(id);
|
||||
this.update_visible_entries(None, cx);
|
||||
this.update_visible_entries(None, None, cx);
|
||||
cx.notify();
|
||||
}
|
||||
project::Event::WorktreeUpdatedEntries(_, _)
|
||||
| project::Event::WorktreeAdded(_)
|
||||
| project::Event::WorktreeOrderChanged => {
|
||||
this.update_visible_entries(None, cx);
|
||||
project::Event::WorktreeOrderChanged => {
|
||||
this.update_visible_entries(None, None, cx);
|
||||
cx.notify();
|
||||
}
|
||||
project::Event::WorktreeUpdatedEntries(id, _)
|
||||
| project::Event::WorktreeAdded(id)
|
||||
| project::Event::WorktreeUpdatedGitRepositories(id) => {
|
||||
this.update_visible_entries(Some(*id), None, cx);
|
||||
cx.notify();
|
||||
}
|
||||
project::Event::Closed => {
|
||||
this.git_diff_editor_updates = Task::ready(());
|
||||
this.expanded_dir_ids.clear();
|
||||
this.visible_entries.clear();
|
||||
this.git_diff_editor = diff_display_editor(project.clone(), cx);
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
@@ -134,11 +197,10 @@ impl GitPanel {
|
||||
let scroll_handle = UniformListScrollHandle::new();
|
||||
|
||||
let mut this = Self {
|
||||
_workspace: weak_workspace,
|
||||
workspace: weak_workspace,
|
||||
focus_handle: cx.focus_handle(),
|
||||
fs,
|
||||
pending_serialization: Task::ready(None),
|
||||
project,
|
||||
visible_entries: Vec::new(),
|
||||
current_modifiers: cx.modifiers(),
|
||||
expanded_dir_ids: Default::default(),
|
||||
@@ -149,8 +211,12 @@ impl GitPanel {
|
||||
selected_item: None,
|
||||
show_scrollbar: !Self::should_autohide_scrollbar(cx),
|
||||
hide_scrollbar_task: None,
|
||||
git_diff_editor: diff_display_editor(project.clone(), cx),
|
||||
git_diff_editor_updates: Task::ready(()),
|
||||
reveal_in_editor: Task::ready(()),
|
||||
project,
|
||||
};
|
||||
this.update_visible_entries(None, cx);
|
||||
this.update_visible_entries(None, None, cx);
|
||||
this
|
||||
});
|
||||
|
||||
@@ -251,6 +317,82 @@ impl GitPanel {
|
||||
|
||||
(depth, difference)
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
|
||||
let item_count = self
|
||||
.visible_entries
|
||||
.iter()
|
||||
.map(|worktree_entries| worktree_entries.visible_entries.len())
|
||||
.sum::<usize>();
|
||||
if item_count == 0 {
|
||||
return;
|
||||
}
|
||||
let selection = match self.selected_item {
|
||||
Some(i) => {
|
||||
if i < item_count - 1 {
|
||||
self.selected_item = Some(i + 1);
|
||||
i + 1
|
||||
} else {
|
||||
self.selected_item = Some(0);
|
||||
0
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.selected_item = Some(0);
|
||||
0
|
||||
}
|
||||
};
|
||||
self.scroll_handle
|
||||
.scroll_to_item(selection, ScrollStrategy::Center);
|
||||
|
||||
let mut hunks = None;
|
||||
self.for_each_visible_entry(selection..selection + 1, cx, |_, entry, _| {
|
||||
hunks = Some(entry.hunks.clone());
|
||||
});
|
||||
if let Some(hunks) = hunks {
|
||||
self.reveal_entry_in_git_editor(hunks, false, Some(UPDATE_DEBOUNCE), cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
|
||||
let item_count = self
|
||||
.visible_entries
|
||||
.iter()
|
||||
.map(|worktree_entries| worktree_entries.visible_entries.len())
|
||||
.sum::<usize>();
|
||||
if item_count == 0 {
|
||||
return;
|
||||
}
|
||||
let selection = match self.selected_item {
|
||||
Some(i) => {
|
||||
if i > 0 {
|
||||
self.selected_item = Some(i - 1);
|
||||
i - 1
|
||||
} else {
|
||||
self.selected_item = Some(item_count - 1);
|
||||
item_count - 1
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.selected_item = Some(0);
|
||||
0
|
||||
}
|
||||
};
|
||||
self.scroll_handle
|
||||
.scroll_to_item(selection, ScrollStrategy::Center);
|
||||
|
||||
let mut hunks = None;
|
||||
self.for_each_visible_entry(selection..selection + 1, cx, |_, entry, _| {
|
||||
hunks = Some(entry.hunks.clone());
|
||||
});
|
||||
if let Some(hunks) = hunks {
|
||||
self.reveal_entry_in_git_editor(hunks, false, Some(UPDATE_DEBOUNCE), cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl GitPanel {
|
||||
@@ -293,8 +435,9 @@ impl GitPanel {
|
||||
fn entry_count(&self) -> usize {
|
||||
self.visible_entries
|
||||
.iter()
|
||||
.map(|(_, entries, _)| {
|
||||
entries
|
||||
.map(|worktree_entries| {
|
||||
worktree_entries
|
||||
.visible_entries
|
||||
.iter()
|
||||
.filter(|entry| entry.git_status.is_some())
|
||||
.count()
|
||||
@@ -309,19 +452,23 @@ impl GitPanel {
|
||||
mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<Self>),
|
||||
) {
|
||||
let mut ix = 0;
|
||||
for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries {
|
||||
for worktree_entries in &self.visible_entries {
|
||||
if ix >= range.end {
|
||||
return;
|
||||
}
|
||||
|
||||
if ix + visible_worktree_entries.len() <= range.start {
|
||||
ix += visible_worktree_entries.len();
|
||||
if ix + worktree_entries.visible_entries.len() <= range.start {
|
||||
ix += worktree_entries.visible_entries.len();
|
||||
continue;
|
||||
}
|
||||
|
||||
let end_ix = range.end.min(ix + visible_worktree_entries.len());
|
||||
let end_ix = range.end.min(ix + worktree_entries.visible_entries.len());
|
||||
// let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
|
||||
if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
|
||||
if let Some(worktree) = self
|
||||
.project
|
||||
.read(cx)
|
||||
.worktree_for_id(worktree_entries.worktree_id, cx)
|
||||
{
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
let root_name = OsStr::new(snapshot.root_name());
|
||||
let expanded_entry_ids = self
|
||||
@@ -331,14 +478,14 @@ impl GitPanel {
|
||||
.unwrap_or(&[]);
|
||||
|
||||
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
|
||||
let entries = entries_paths.get_or_init(|| {
|
||||
visible_worktree_entries
|
||||
.iter()
|
||||
.map(|e| (e.path.clone()))
|
||||
.collect()
|
||||
});
|
||||
let entries = worktree_entries.paths();
|
||||
|
||||
for entry in visible_worktree_entries[entry_range].iter() {
|
||||
let index_start = entry_range.start;
|
||||
for (i, entry) in worktree_entries.visible_entries[entry_range]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let index = index_start + i;
|
||||
let status = entry.git_status;
|
||||
let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
|
||||
|
||||
@@ -360,16 +507,16 @@ impl GitPanel {
|
||||
.unwrap_or_else(|| root_name.to_string_lossy().to_string()),
|
||||
};
|
||||
|
||||
let display_name = entry.path.to_string_lossy().into_owned();
|
||||
|
||||
let details = EntryDetails {
|
||||
filename,
|
||||
display_name,
|
||||
display_name: entry.path.to_string_lossy().into_owned(),
|
||||
kind: entry.kind,
|
||||
is_expanded,
|
||||
path: entry.path.clone(),
|
||||
status,
|
||||
hunks: entry.hunks.clone(),
|
||||
depth,
|
||||
index,
|
||||
};
|
||||
callback(entry.id, details, cx);
|
||||
}
|
||||
@@ -379,44 +526,75 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
// TODO: Update expanded directory state
|
||||
// TODO: Updates happen in the main loop, could be long for large workspaces
|
||||
fn update_visible_entries(
|
||||
&mut self,
|
||||
for_worktree: Option<WorktreeId>,
|
||||
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let project = self.project.read(cx);
|
||||
self.visible_entries.clear();
|
||||
for worktree in project.visible_worktrees(cx) {
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
let worktree_id = snapshot.id();
|
||||
|
||||
let mut visible_worktree_entries = Vec::new();
|
||||
let mut entry_iter = snapshot.entries(true, 0);
|
||||
while let Some(entry) = entry_iter.entry() {
|
||||
// Only include entries with a git status
|
||||
if entry.git_status.is_some() {
|
||||
visible_worktree_entries.push(entry.clone());
|
||||
let mut old_entries_removed = false;
|
||||
let mut after_update = Vec::new();
|
||||
self.visible_entries
|
||||
.retain(|worktree_entries| match for_worktree {
|
||||
Some(for_worktree) => {
|
||||
if worktree_entries.worktree_id == for_worktree {
|
||||
old_entries_removed = true;
|
||||
false
|
||||
} else if old_entries_removed {
|
||||
after_update.push(worktree_entries.clone());
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
entry_iter.advance();
|
||||
None => false,
|
||||
});
|
||||
for worktree in project.visible_worktrees(cx) {
|
||||
let worktree_id = worktree.read(cx).id();
|
||||
if for_worktree.is_some() && for_worktree != Some(worktree_id) {
|
||||
continue;
|
||||
}
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
|
||||
let mut visible_worktree_entries = snapshot
|
||||
.entries(false, 0)
|
||||
.filter(|entry| !entry.is_external)
|
||||
.filter(|entry| entry.git_status.is_some())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
snapshot.propagate_git_statuses(&mut visible_worktree_entries);
|
||||
project::sort_worktree_entries(&mut visible_worktree_entries);
|
||||
|
||||
if !visible_worktree_entries.is_empty() {
|
||||
self.visible_entries
|
||||
.push((worktree_id, visible_worktree_entries, OnceCell::new()));
|
||||
self.visible_entries.push(WorktreeEntries {
|
||||
worktree_id,
|
||||
visible_entries: visible_worktree_entries
|
||||
.into_iter()
|
||||
.map(|entry| GitPanelEntry {
|
||||
entry,
|
||||
hunks: Rc::default(),
|
||||
})
|
||||
.collect(),
|
||||
paths: Rc::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
self.visible_entries.extend(after_update);
|
||||
|
||||
if let Some((worktree_id, entry_id)) = new_selected_entry {
|
||||
self.selected_item = self.visible_entries.iter().enumerate().find_map(
|
||||
|(worktree_index, (id, entries, _))| {
|
||||
if *id == worktree_id {
|
||||
entries
|
||||
|(worktree_index, worktree_entries)| {
|
||||
if worktree_entries.worktree_id == worktree_id {
|
||||
worktree_entries
|
||||
.visible_entries
|
||||
.iter()
|
||||
.position(|entry| entry.id == entry_id)
|
||||
.map(|entry_index| worktree_index * entries.len() + entry_index)
|
||||
.map(|entry_index| {
|
||||
worktree_index * worktree_entries.visible_entries.len()
|
||||
+ entry_index
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -424,6 +602,163 @@ impl GitPanel {
|
||||
);
|
||||
}
|
||||
|
||||
let project = self.project.clone();
|
||||
self.git_diff_editor_updates = cx.spawn(|git_panel, mut cx| async move {
|
||||
cx.background_executor()
|
||||
.timer(UPDATE_DEBOUNCE)
|
||||
.await;
|
||||
let Some(project_buffers) = git_panel
|
||||
.update(&mut cx, |git_panel, cx| {
|
||||
futures::future::join_all(git_panel.visible_entries.iter_mut().flat_map(
|
||||
move |worktree_entries| {
|
||||
worktree_entries
|
||||
.visible_entries
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
let git_status = entry.git_status()?;
|
||||
let entry_hunks = entry.hunks.clone();
|
||||
let (entry_path, unstaged_changes_task) =
|
||||
project.update(cx, |project, cx| {
|
||||
let entry_path =
|
||||
project.path_for_entry(entry.id, cx)?;
|
||||
let open_task =
|
||||
project.open_path(entry_path.clone(), cx);
|
||||
let unstaged_changes_task =
|
||||
cx.spawn(|project, mut cx| async move {
|
||||
let (_, opened_model) = open_task
|
||||
.await
|
||||
.context("opening buffer")?;
|
||||
let buffer = opened_model
|
||||
.downcast::<Buffer>()
|
||||
.map_err(|_| {
|
||||
anyhow::anyhow!(
|
||||
"accessing buffer for entry"
|
||||
)
|
||||
})?;
|
||||
// TODO added files have noop changes and those are not expanded properly in the multi buffer
|
||||
let unstaged_changes = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.open_unstaged_changes(
|
||||
buffer.clone(),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await
|
||||
.context("opening unstaged changes")?;
|
||||
|
||||
let hunks = cx.update(|cx| {
|
||||
entry_hunks
|
||||
.get_or_init(|| {
|
||||
match git_status {
|
||||
GitFileStatus::Added => {
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let entire_buffer_range =
|
||||
buffer_snapshot.anchor_after(0)
|
||||
..buffer_snapshot
|
||||
.anchor_before(
|
||||
buffer_snapshot.len(),
|
||||
);
|
||||
let entire_buffer_point_range =
|
||||
entire_buffer_range
|
||||
.clone()
|
||||
.to_point(&buffer_snapshot);
|
||||
|
||||
vec![DiffHunk {
|
||||
row_range: entire_buffer_point_range
|
||||
.start
|
||||
.row
|
||||
..entire_buffer_point_range
|
||||
.end
|
||||
.row,
|
||||
buffer_range: entire_buffer_range,
|
||||
diff_base_byte_range: 0..0,
|
||||
}]
|
||||
}
|
||||
GitFileStatus::Modified => {
|
||||
let buffer_snapshot =
|
||||
buffer.read(cx).snapshot();
|
||||
unstaged_changes.read(cx)
|
||||
.diff_to_buffer
|
||||
.hunks_in_row_range(
|
||||
0..BufferRow::MAX,
|
||||
&buffer_snapshot,
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
// TODO support conflicts display
|
||||
GitFileStatus::Conflict => Vec::new(),
|
||||
}
|
||||
}).clone()
|
||||
})?;
|
||||
|
||||
anyhow::Ok((buffer, unstaged_changes, hunks))
|
||||
});
|
||||
Some((entry_path, unstaged_changes_task))
|
||||
})?;
|
||||
Some((entry_path, unstaged_changes_task))
|
||||
})
|
||||
.map(|(entry_path, open_task)| async move {
|
||||
(entry_path, open_task.await)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
},
|
||||
))
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project_buffers = project_buffers.await;
|
||||
if project_buffers.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut change_sets = Vec::with_capacity(project_buffers.len());
|
||||
if let Some(buffer_update_task) = git_panel
|
||||
.update(&mut cx, |git_panel, cx| {
|
||||
let editor = git_panel.git_diff_editor.clone();
|
||||
let multi_buffer = editor.read(cx).buffer().clone();
|
||||
let mut buffers_with_ranges = Vec::with_capacity(project_buffers.len());
|
||||
for (buffer_path, open_result) in project_buffers {
|
||||
if let Some((buffer, unstaged_changes, diff_hunks)) = open_result
|
||||
.with_context(|| format!("opening buffer {buffer_path:?}"))
|
||||
.log_err()
|
||||
{
|
||||
change_sets.push(unstaged_changes);
|
||||
buffers_with_ranges.push((
|
||||
buffer,
|
||||
diff_hunks
|
||||
.into_iter()
|
||||
.map(|hunk| hunk.buffer_range)
|
||||
.collect(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
multi_buffer.update(cx, |multi_buffer, cx| {
|
||||
multi_buffer.clear(cx);
|
||||
multi_buffer.push_multiple_excerpts_with_context_lines(
|
||||
buffers_with_ranges,
|
||||
DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
{
|
||||
buffer_update_task.await;
|
||||
git_panel
|
||||
.update(&mut cx, |git_panel, cx| {
|
||||
git_panel.git_diff_editor.update(cx, |editor, cx| {
|
||||
for change_set in change_sets {
|
||||
editor.add_change_set(change_set, cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -626,17 +961,23 @@ impl GitPanel {
|
||||
let item_count = self
|
||||
.visible_entries
|
||||
.iter()
|
||||
.map(|(_, worktree_entries, _)| worktree_entries.len())
|
||||
.map(|worktree_entries| worktree_entries.visible_entries.len())
|
||||
.sum();
|
||||
let selected_entry = self.selected_item;
|
||||
h_flex()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
uniform_list(cx.view().clone(), "entries", item_count, {
|
||||
|this, range, cx| {
|
||||
move |git_panel, range, cx| {
|
||||
let mut items = Vec::with_capacity(range.end - range.start);
|
||||
this.for_each_visible_entry(range, cx, |id, details, cx| {
|
||||
items.push(this.render_entry(id, details, cx));
|
||||
git_panel.for_each_visible_entry(range, cx, |id, details, cx| {
|
||||
items.push(git_panel.render_entry(
|
||||
id,
|
||||
Some(details.index) == selected_entry,
|
||||
details,
|
||||
cx,
|
||||
));
|
||||
});
|
||||
items
|
||||
}
|
||||
@@ -653,12 +994,14 @@ impl GitPanel {
|
||||
fn render_entry(
|
||||
&self,
|
||||
id: ProjectEntryId,
|
||||
selected: bool,
|
||||
details: EntryDetails,
|
||||
cx: &ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let id = id.to_proto() as usize;
|
||||
let checkbox_id = ElementId::Name(format!("checkbox_{}", id).into());
|
||||
let is_staged = ToggleState::Selected;
|
||||
let handle = cx.view().clone();
|
||||
|
||||
h_flex()
|
||||
.id(id)
|
||||
@@ -676,7 +1019,113 @@ impl GitPanel {
|
||||
.when_some(details.status, |this, status| {
|
||||
this.child(git_status_icon(status))
|
||||
})
|
||||
.child(h_flex().gap_1p5().child(details.display_name.clone()))
|
||||
.child(
|
||||
ListItem::new(("label", id))
|
||||
.toggle_state(selected)
|
||||
.child(h_flex().gap_1p5().child(details.display_name.clone()))
|
||||
.on_click(move |e, cx| {
|
||||
handle.update(cx, |git_panel, cx| {
|
||||
git_panel.selected_item = Some(details.index);
|
||||
let change_focus = e.down.click_count > 1;
|
||||
git_panel.reveal_entry_in_git_editor(
|
||||
details.hunks.clone(),
|
||||
change_focus,
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn reveal_entry_in_git_editor(
|
||||
&mut self,
|
||||
hunks: Rc<OnceCell<Vec<DiffHunk>>>,
|
||||
change_focus: bool,
|
||||
debounce: Option<Duration>,
|
||||
cx: &mut ViewContext<'_, Self>,
|
||||
) {
|
||||
let workspace = self.workspace.clone();
|
||||
let diff_editor = self.git_diff_editor.clone();
|
||||
self.reveal_in_editor = cx.spawn(|_, mut cx| async move {
|
||||
if let Some(debounce) = debounce {
|
||||
cx.background_executor().timer(debounce).await;
|
||||
}
|
||||
|
||||
let Some(editor) = workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
let git_diff_editor = workspace
|
||||
.items_of_type::<Editor>(cx)
|
||||
.find(|editor| &diff_editor == editor);
|
||||
match git_diff_editor {
|
||||
Some(existing_editor) => {
|
||||
workspace.activate_item(&existing_editor, true, change_focus, cx);
|
||||
existing_editor
|
||||
}
|
||||
None => {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.add_item(
|
||||
diff_editor.boxed_clone(),
|
||||
true,
|
||||
change_focus,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
diff_editor.clone()
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(first_hunk) = hunks.get().and_then(|hunks| hunks.first()) {
|
||||
let hunk_buffer_range = &first_hunk.buffer_range;
|
||||
if let Some(buffer_id) = hunk_buffer_range
|
||||
.start
|
||||
.buffer_id
|
||||
.or_else(|| first_hunk.buffer_range.end.buffer_id)
|
||||
{
|
||||
editor
|
||||
.update(&mut cx, |editor, cx| {
|
||||
let multi_buffer = editor.buffer().read(cx);
|
||||
let buffer = multi_buffer.buffer(buffer_id)?;
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let (excerpt_id, _) = multi_buffer
|
||||
.excerpts_for_buffer(&buffer, cx)
|
||||
.into_iter()
|
||||
.find(|(_, excerpt)| {
|
||||
hunk_buffer_range
|
||||
.start
|
||||
.cmp(&excerpt.context.start, &buffer_snapshot)
|
||||
.is_ge()
|
||||
&& hunk_buffer_range
|
||||
.end
|
||||
.cmp(&excerpt.context.end, &buffer_snapshot)
|
||||
.is_le()
|
||||
})?;
|
||||
let multi_buffer_hunk_start = multi_buffer
|
||||
.snapshot(cx)
|
||||
.anchor_in_excerpt(excerpt_id, hunk_buffer_range.start)?;
|
||||
editor.change_selections(
|
||||
Some(Autoscroll::Strategy(AutoscrollStrategy::Center)),
|
||||
cx,
|
||||
|s| {
|
||||
s.select_ranges(Some(
|
||||
multi_buffer_hunk_start..multi_buffer_hunk_start,
|
||||
))
|
||||
},
|
||||
);
|
||||
cx.notify();
|
||||
Some(())
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -704,6 +1153,8 @@ impl Render for GitPanel {
|
||||
this.commit_all_changes(&CommitAllChanges, cx)
|
||||
}))
|
||||
})
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_prev))
|
||||
.on_hover(cx.listener(|this, hovered, cx| {
|
||||
if *hovered {
|
||||
this.show_scrollbar = true;
|
||||
@@ -784,3 +1235,14 @@ impl Panel for GitPanel {
|
||||
Box::new(ToggleFocus)
|
||||
}
|
||||
}
|
||||
|
||||
fn diff_display_editor(project: Model<Project>, cx: &mut WindowContext) -> View<Editor> {
|
||||
cx.new_view(|cx| {
|
||||
let multi_buffer = cx.new_model(|cx| {
|
||||
MultiBuffer::new(project.read(cx).capability()).with_title("Project diff".to_string())
|
||||
});
|
||||
let mut editor = Editor::for_multibuffer(multi_buffer, Some(project), true, cx);
|
||||
editor.set_expand_all_diff_hunks();
|
||||
editor
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::*;
|
||||
use anyhow::Result;
|
||||
use gpui::{
|
||||
black, bounce, div, ease_in_out, percentage, prelude::*, px, rgb, size, svg, Animation,
|
||||
AnimationExt as _, App, AppContext, AssetSource, Bounds, SharedString, Transformation,
|
||||
ViewContext, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
struct Assets {}
|
||||
|
||||
@@ -37,7 +42,7 @@ impl Render for AnimationExample {
|
||||
div()
|
||||
.flex()
|
||||
.bg(rgb(0x2e7d32))
|
||||
.size(Length::Definite(Pixels(300.0).into()))
|
||||
.size(px(300.0))
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.shadow_lg()
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use gpui::*;
|
||||
use gpui::{
|
||||
div, prelude::*, px, rgb, size, App, AppContext, Bounds, SharedString, ViewContext,
|
||||
WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
struct HelloWorld {
|
||||
text: SharedString,
|
||||
@@ -11,7 +14,7 @@ impl Render for HelloWorld {
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.bg(rgb(0x505050))
|
||||
.size(Length::Definite(Pixels(500.0).into()))
|
||||
.size(px(500.0))
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.shadow_lg()
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::*;
|
||||
use std::fs;
|
||||
use anyhow::Result;
|
||||
use gpui::{
|
||||
actions, div, img, prelude::*, px, rgb, size, App, AppContext, AssetSource, Bounds,
|
||||
ImageSource, KeyBinding, Menu, MenuItem, Point, SharedString, SharedUri, TitlebarOptions,
|
||||
ViewContext, WindowBounds, WindowContext, WindowOptions,
|
||||
};
|
||||
|
||||
struct Assets {
|
||||
base: PathBuf,
|
||||
@@ -55,7 +60,7 @@ impl RenderOnce for ImageContainer {
|
||||
.size_full()
|
||||
.gap_4()
|
||||
.child(self.text)
|
||||
.child(img(self.src).w(px(256.0)).h(px(256.0))),
|
||||
.child(img(self.src).size(px(256.0))),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -75,7 +80,7 @@ impl Render for ImageShowcase {
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.gap_8()
|
||||
.bg(rgb(0xFFFFFF))
|
||||
.bg(rgb(0xffffff))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::*;
|
||||
use gpui::{
|
||||
actions, black, div, fill, hsla, opaque_grey, point, prelude::*, px, relative, rgb, rgba, size,
|
||||
white, yellow, App, AppContext, Bounds, ClipboardItem, CursorStyle, ElementId,
|
||||
ElementInputHandler, FocusHandle, FocusableView, GlobalElementId, KeyBinding, Keystroke,
|
||||
LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
|
||||
ShapedLine, SharedString, Style, TextRun, UTF16Selection, UnderlineStyle, View, ViewContext,
|
||||
ViewInputHandler, WindowBounds, WindowContext, WindowOptions,
|
||||
};
|
||||
use unicode_segmentation::*;
|
||||
|
||||
actions!(
|
||||
@@ -463,7 +470,7 @@ impl Element for TextElement {
|
||||
bounds.bottom(),
|
||||
),
|
||||
),
|
||||
rgba(0x3311FF30),
|
||||
rgba(0x3311ff30),
|
||||
)),
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use std::{fs, path::PathBuf, time::Duration};
|
||||
|
||||
use gpui::*;
|
||||
use anyhow::Result;
|
||||
use gpui::{
|
||||
div, hsla, img, point, prelude::*, px, rgb, size, svg, App, AppContext, AssetSource, Bounds,
|
||||
BoxShadow, ClickEvent, SharedString, Task, Timer, ViewContext, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
struct Assets {
|
||||
base: PathBuf,
|
||||
@@ -76,7 +80,7 @@ impl Render for HelloWorld {
|
||||
.flex()
|
||||
.flex_row()
|
||||
.size_full()
|
||||
.bg(rgb(0xE0E0E0))
|
||||
.bg(rgb(0xe0e0e0))
|
||||
.text_xl()
|
||||
.child(
|
||||
div()
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use gpui::*;
|
||||
use gpui::{
|
||||
actions, div, prelude::*, rgb, App, AppContext, Menu, MenuItem, ViewContext, WindowOptions,
|
||||
};
|
||||
|
||||
struct SetMenus;
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use gpui::*;
|
||||
use gpui::{
|
||||
div, prelude::*, px, rgb, size, App, AppContext, Bounds, ViewContext, WindowBounds,
|
||||
WindowOptions,
|
||||
};
|
||||
|
||||
struct Shadow {}
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use gpui::*;
|
||||
use std::fs;
|
||||
use anyhow::Result;
|
||||
use gpui::{
|
||||
div, prelude::*, px, rgb, size, svg, App, AppContext, AssetSource, Bounds, SharedString,
|
||||
ViewContext, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
struct Assets {
|
||||
base: PathBuf,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use gpui::*;
|
||||
use gpui::{
|
||||
div, prelude::*, px, size, App, AppContext, Bounds, ViewContext, WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
struct HelloWorld {}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use gpui::*;
|
||||
use gpui::{
|
||||
div, prelude::*, px, rgb, size, uniform_list, App, AppContext, Bounds, ViewContext,
|
||||
WindowBounds, WindowOptions,
|
||||
};
|
||||
|
||||
struct UniformListExample {}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use gpui::*;
|
||||
use prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, prelude::*, px, rgb, size, App, AppContext, Bounds, SharedString, Timer, ViewContext,
|
||||
WindowBounds, WindowContext, WindowKind, WindowOptions,
|
||||
};
|
||||
|
||||
struct SubWindow {
|
||||
custom_titlebar: bool,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use gpui::*;
|
||||
use gpui::{
|
||||
div, point, prelude::*, px, rgb, App, AppContext, Bounds, DisplayId, Hsla, Pixels,
|
||||
SharedString, Size, ViewContext, WindowBackgroundAppearance, WindowBounds, WindowKind,
|
||||
WindowOptions,
|
||||
};
|
||||
|
||||
struct WindowContent {
|
||||
text: SharedString,
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use gpui::*;
|
||||
use prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
black, canvas, div, green, point, prelude::*, px, rgb, size, transparent_black, white, App,
|
||||
AppContext, Bounds, CursorStyle, Decorations, Hsla, MouseButton, Pixels, Point, ResizeEdge,
|
||||
Size, ViewContext, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowOptions,
|
||||
};
|
||||
|
||||
struct WindowShadow {}
|
||||
|
||||
/*
|
||||
Things to do:
|
||||
1. We need a way of calculating which edge or corner the mouse is on,
|
||||
and then dispatch on that
|
||||
2. We need to improve the shadow rendering significantly
|
||||
3. We need to implement the techniques in here in Zed
|
||||
*/
|
||||
// Things to do:
|
||||
// 1. We need a way of calculating which edge or corner the mouse is on,
|
||||
// and then dispatch on that
|
||||
// 2. We need to improve the shadow rendering significantly
|
||||
// 3. We need to implement the techniques in here in Zed
|
||||
|
||||
impl Render for WindowShadow {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
@@ -128,7 +129,7 @@ impl Render for WindowShadow {
|
||||
div()
|
||||
.flex()
|
||||
.bg(white())
|
||||
.size(Length::Definite(Pixels(300.0).into()))
|
||||
.size(px(300.0))
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.shadow_lg()
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user