Compare commits
264 Commits
compl-menu
...
remove-con
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b4d9ffd96 | ||
|
|
9626de34f7 | ||
|
|
764af2fd87 | ||
|
|
a5fa81586e | ||
|
|
4d4ea61b12 | ||
|
|
9f56b9208a | ||
|
|
1cedbc04db | ||
|
|
643f2e1e45 | ||
|
|
6f6e995603 | ||
|
|
b43fcb7b09 | ||
|
|
de6abee965 | ||
|
|
93ad5c38f0 | ||
|
|
16e9304af6 | ||
|
|
71a6e7e677 | ||
|
|
f92da24fae | ||
|
|
4b3ed1152f | ||
|
|
fc43ec805d | ||
|
|
3738d9c34a | ||
|
|
2776d382c3 | ||
|
|
8e002b1e5a | ||
|
|
898bc8cb8c | ||
|
|
991345ed4e | ||
|
|
55d2e26fa5 | ||
|
|
2813cc077f | ||
|
|
696530b1e5 | ||
|
|
1c6b2b9dd0 | ||
|
|
f4d76abf39 | ||
|
|
4034691669 | ||
|
|
904e1ef8f7 | ||
|
|
9f73fb6ddc | ||
|
|
339ed1bad2 | ||
|
|
59fce2924d | ||
|
|
8d42bc011d | ||
|
|
47f46fe786 | ||
|
|
56d9e60ad7 | ||
|
|
b94279da00 | ||
|
|
f971c9eb5f | ||
|
|
be4ecd9e21 | ||
|
|
6a3834fc91 | ||
|
|
952d10239d | ||
|
|
8fdc5fa6ad | ||
|
|
79ec03cd14 | ||
|
|
df29eaf897 | ||
|
|
e3bccd7f0d | ||
|
|
4737c81c44 | ||
|
|
603b398d32 | ||
|
|
db387de6b3 | ||
|
|
a5fbde69ac | ||
|
|
cc5c668744 | ||
|
|
953586f532 | ||
|
|
b1485631cd | ||
|
|
55c46bbddf | ||
|
|
9f409de5fe | ||
|
|
426ae3339a | ||
|
|
fb05ab846b | ||
|
|
591352cd9e | ||
|
|
54945b922a | ||
|
|
16fa413d2d | ||
|
|
0de4094cc1 | ||
|
|
60ecf8345e | ||
|
|
2a3a9d06ad | ||
|
|
2aa3bd7099 | ||
|
|
bdc85d780e | ||
|
|
df4c51e9ea | ||
|
|
cb3bd8d442 | ||
|
|
d219bfc10f | ||
|
|
d768bfdce1 | ||
|
|
6c9d544341 | ||
|
|
ab1359b315 | ||
|
|
c641bbf0b1 | ||
|
|
6864b3e86d | ||
|
|
25416ff303 | ||
|
|
5e3ec9d81c | ||
|
|
5a3b9d8820 | ||
|
|
ec72020cb1 | ||
|
|
f2d41ca066 | ||
|
|
7510afccb8 | ||
|
|
ab8f8dde43 | ||
|
|
41de86f87f | ||
|
|
0ebd08871b | ||
|
|
dbb74ca28d | ||
|
|
8e987c273f | ||
|
|
859da6b13a | ||
|
|
54fe194f7b | ||
|
|
d07e1d2141 | ||
|
|
c9a477350f | ||
|
|
c49c33bf61 | ||
|
|
772489499c | ||
|
|
1d73e6646c | ||
|
|
efba1cb141 | ||
|
|
19ac46a252 | ||
|
|
cc95b79913 | ||
|
|
267df69b44 | ||
|
|
1de66771d6 | ||
|
|
d7b266ce62 | ||
|
|
61aa1d8f08 | ||
|
|
d4428c7c4b | ||
|
|
021e8a17f6 | ||
|
|
296fe144cb | ||
|
|
6ef5d8f748 | ||
|
|
f912c545e7 | ||
|
|
ddc469ca3e | ||
|
|
e9bd4b2890 | ||
|
|
a119688a50 | ||
|
|
dcbff982ad | ||
|
|
ad51df7644 | ||
|
|
3f33ca01a8 | ||
|
|
5f4f3a9c87 | ||
|
|
a7e5d5a298 | ||
|
|
344284e013 | ||
|
|
838ebe90cd | ||
|
|
dcd2d85127 | ||
|
|
b05f723e95 | ||
|
|
30ffadfe32 | ||
|
|
04ffcab87d | ||
|
|
5a3233ebe4 | ||
|
|
f6dabadaf7 | ||
|
|
a037f71432 | ||
|
|
d625a3e6ba | ||
|
|
ca6825066f | ||
|
|
a15360bcc8 | ||
|
|
016b5d60e1 | ||
|
|
9815358bdd | ||
|
|
ac60dcd67a | ||
|
|
3e3a5f04a2 | ||
|
|
7903f4ea58 | ||
|
|
a3dec643a1 | ||
|
|
02cc0b9afa | ||
|
|
787466383e | ||
|
|
34f8bb246a | ||
|
|
56b425fced | ||
|
|
ed61abb8b8 | ||
|
|
d71180abc2 | ||
|
|
af2ac1a4a0 | ||
|
|
c742cda8e8 | ||
|
|
52614482bf | ||
|
|
8b0a0dfb11 | ||
|
|
f5935d0cec | ||
|
|
d32fe0216f | ||
|
|
2957263b23 | ||
|
|
95911aaa14 | ||
|
|
1a9f0a647a | ||
|
|
45c714110e | ||
|
|
4c84600630 | ||
|
|
564936e1fe | ||
|
|
3d4e0780c4 | ||
|
|
44a46e3713 | ||
|
|
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@11bd71901bbe5b1630ceea73d27597364c9af683 # 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:
|
||||
|
||||
566
Cargo.lock
generated
566
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
22
Cargo.toml
@@ -117,6 +117,7 @@ members = [
|
||||
"crates/tab_switcher",
|
||||
"crates/task",
|
||||
"crates/tasks_ui",
|
||||
"crates/telemetry",
|
||||
"crates/telemetry_events",
|
||||
"crates/terminal",
|
||||
"crates/terminal_view",
|
||||
@@ -305,6 +306,7 @@ supermaven_api = { path = "crates/supermaven_api" }
|
||||
tab_switcher = { path = "crates/tab_switcher" }
|
||||
task = { path = "crates/task" }
|
||||
tasks_ui = { path = "crates/tasks_ui" }
|
||||
telemetry = { path = "crates/telemetry" }
|
||||
telemetry_events = { path = "crates/telemetry_events" }
|
||||
terminal = { path = "crates/terminal" }
|
||||
terminal_view = { path = "crates/terminal_view" }
|
||||
@@ -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 = "091a8401033847bb9b6ace3fcf70448d069621c5" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
|
||||
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"
|
||||
@@ -399,14 +401,13 @@ jupyter-websocket-client = { version = "0.8.0" }
|
||||
libc = "0.2"
|
||||
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
|
||||
linkify = "0.10.0"
|
||||
livekit = { git = "https://github.com/zed-industries/rust-sdks", rev="799f10133d93ba2a88642cd480d01ec4da53408c", features = ["dispatcher", "services-dispatcher", "rustls-tls-native-roots"], default-features = false }
|
||||
livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev="060964da10574cd9bf06463a53bf6e0769c5c45e", features = ["dispatcher", "services-dispatcher", "rustls-tls-native-roots"], default-features = false }
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
nanoid = "0.4"
|
||||
nbformat = { version = "0.9.0" }
|
||||
nix = "0.29"
|
||||
num-format = "0.4.4"
|
||||
once_cell = "1.19.0"
|
||||
ordered-float = "2.1.1"
|
||||
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
||||
parking_lot = "0.12.1"
|
||||
@@ -470,7 +471,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 +497,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 +524,7 @@ wasmtime-wasi = "24"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.201"
|
||||
zstd = "0.11"
|
||||
metal = "0.30"
|
||||
|
||||
[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 |
@@ -260,6 +260,8 @@
|
||||
"ctrl-f4": "pane::CloseActiveItem",
|
||||
"alt-ctrl-t": ["pane::CloseInactiveItems", { "close_pinned": false }],
|
||||
"alt-ctrl-shift-w": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }],
|
||||
"ctrl-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }],
|
||||
"ctrl-k u": ["pane::CloseCleanItems", { "close_pinned": false }],
|
||||
"ctrl-k w": ["pane::CloseAllItems", { "close_pinned": false }],
|
||||
"ctrl-shift-f": "project_search::ToggleFocus",
|
||||
|
||||
@@ -327,6 +327,8 @@
|
||||
"cmd-w": "pane::CloseActiveItem",
|
||||
"alt-cmd-t": ["pane::CloseInactiveItems", { "close_pinned": false }],
|
||||
"ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes",
|
||||
"cmd-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }],
|
||||
"cmd-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }],
|
||||
"cmd-k u": ["pane::CloseCleanItems", { "close_pinned": false }],
|
||||
"cmd-k cmd-w": ["pane::CloseAllItems", { "close_pinned": false }],
|
||||
"cmd-f": "project_search::ToggleFocus",
|
||||
@@ -448,7 +450,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 +496,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 +763,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",
|
||||
|
||||
@@ -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.
|
||||
@@ -1089,6 +1103,9 @@
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"Zig": {
|
||||
"language_servers": ["zls", "..."]
|
||||
}
|
||||
},
|
||||
// Different settings for specific language models.
|
||||
|
||||
@@ -4,8 +4,8 @@ use extension_host::ExtensionStore;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter,
|
||||
InteractiveElement as _, Model, ParentElement as _, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Transformation, View, ViewContext, VisualContext as _,
|
||||
InteractiveElement as _, Model, ModelContext, ParentElement as _, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Transformation, Window,
|
||||
};
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId};
|
||||
use lsp::LanguageServerName;
|
||||
@@ -46,22 +46,25 @@ struct PendingWork<'a> {
|
||||
struct Content {
|
||||
icon: Option<gpui::AnyElement>,
|
||||
message: String,
|
||||
on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
|
||||
on_click: Option<
|
||||
Arc<dyn Fn(&mut ActivityIndicator, &mut Window, &mut ModelContext<ActivityIndicator>)>,
|
||||
>,
|
||||
}
|
||||
|
||||
impl ActivityIndicator {
|
||||
pub fn new(
|
||||
workspace: &mut Workspace,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> View<ActivityIndicator> {
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Workspace>,
|
||||
) -> Model<ActivityIndicator> {
|
||||
let project = workspace.project().clone();
|
||||
let auto_updater = AutoUpdater::get(cx);
|
||||
let this = cx.new_view(|cx: &mut ViewContext<Self>| {
|
||||
let this = cx.new_model(|cx| {
|
||||
let mut status_events = languages.language_server_binary_statuses();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
while let Some((name, status)) = status_events.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this: &mut ActivityIndicator, cx| {
|
||||
this.statuses.retain(|s| s.name != name);
|
||||
this.statuses.push(LspStatus { name, status });
|
||||
cx.notify();
|
||||
@@ -70,6 +73,7 @@ impl ActivityIndicator {
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.observe(&project, |_, _, cx| cx.notify()).detach();
|
||||
|
||||
if let Some(auto_updater) = auto_updater.as_ref() {
|
||||
@@ -84,13 +88,13 @@ impl ActivityIndicator {
|
||||
}
|
||||
});
|
||||
|
||||
cx.subscribe(&this, move |_, _, event, cx| match event {
|
||||
cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event {
|
||||
Event::ShowError { lsp_name, error } => {
|
||||
let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
|
||||
let project = project.clone();
|
||||
let error = error.clone();
|
||||
let lsp_name = lsp_name.clone();
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
cx.spawn_in(window, |workspace, mut cx| async move {
|
||||
let buffer = create_buffer.await?;
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
@@ -103,13 +107,14 @@ impl ActivityIndicator {
|
||||
);
|
||||
buffer.set_capability(language::Capability::ReadOnly, cx);
|
||||
})?;
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(cx.new_view(|cx| {
|
||||
Editor::for_buffer(buffer, Some(project.clone()), cx)
|
||||
Box::new(cx.new_model(|cx| {
|
||||
Editor::for_buffer(buffer, Some(project.clone()), window, cx)
|
||||
})),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
@@ -123,7 +128,12 @@ impl ActivityIndicator {
|
||||
this
|
||||
}
|
||||
|
||||
fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext<Self>) {
|
||||
fn show_error_message(
|
||||
&mut self,
|
||||
_: &ShowErrorMessage,
|
||||
_: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.statuses.retain(|status| {
|
||||
if let LanguageServerBinaryStatus::Failed { error } = &status.status {
|
||||
cx.emit(Event::ShowError {
|
||||
@@ -139,7 +149,12 @@ impl ActivityIndicator {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
|
||||
fn dismiss_error_message(
|
||||
&mut self,
|
||||
_: &DismissErrorMessage,
|
||||
_: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
updater.update(cx, |updater, cx| {
|
||||
updater.dismiss_error(cx);
|
||||
@@ -183,7 +198,7 @@ impl ActivityIndicator {
|
||||
self.project.read(cx).shell_environment_errors(cx)
|
||||
}
|
||||
|
||||
fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Option<Content> {
|
||||
fn content_to_render(&mut self, cx: &mut ModelContext<Self>) -> Option<Content> {
|
||||
// Show if any direnv calls failed
|
||||
if let Some((&worktree_id, error)) = self.pending_environment_errors(cx).next() {
|
||||
return Some(Content {
|
||||
@@ -193,11 +208,11 @@ impl ActivityIndicator {
|
||||
.into_any_element(),
|
||||
),
|
||||
message: error.0.clone(),
|
||||
on_click: Some(Arc::new(move |this, cx| {
|
||||
on_click: Some(Arc::new(move |this, window, cx| {
|
||||
this.project.update(cx, |project, cx| {
|
||||
project.remove_environment_error(cx, worktree_id);
|
||||
});
|
||||
cx.dispatch_action(Box::new(workspace::OpenLog));
|
||||
window.dispatch_action(Box::new(workspace::OpenLog), cx);
|
||||
})),
|
||||
});
|
||||
}
|
||||
@@ -280,10 +295,10 @@ impl ActivityIndicator {
|
||||
}
|
||||
)
|
||||
),
|
||||
on_click: Some(Arc::new(move |this, cx| {
|
||||
on_click: Some(Arc::new(move |this, window, cx| {
|
||||
this.statuses
|
||||
.retain(|status| !downloading.contains(&status.name));
|
||||
this.dismiss_error_message(&DismissErrorMessage, cx)
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
});
|
||||
}
|
||||
@@ -308,10 +323,10 @@ impl ActivityIndicator {
|
||||
}
|
||||
),
|
||||
),
|
||||
on_click: Some(Arc::new(move |this, cx| {
|
||||
on_click: Some(Arc::new(move |this, window, cx| {
|
||||
this.statuses
|
||||
.retain(|status| !checking_for_update.contains(&status.name));
|
||||
this.dismiss_error_message(&DismissErrorMessage, cx)
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
});
|
||||
}
|
||||
@@ -336,8 +351,8 @@ impl ActivityIndicator {
|
||||
acc
|
||||
}),
|
||||
),
|
||||
on_click: Some(Arc::new(|this, cx| {
|
||||
this.show_error_message(&Default::default(), cx)
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.show_error_message(&Default::default(), window, cx)
|
||||
})),
|
||||
});
|
||||
}
|
||||
@@ -351,11 +366,11 @@ impl ActivityIndicator {
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!("Formatting failed: {}. Click to see logs.", failure),
|
||||
on_click: Some(Arc::new(|indicator, cx| {
|
||||
on_click: Some(Arc::new(|indicator, window, cx| {
|
||||
indicator.project.update(cx, |project, cx| {
|
||||
project.reset_last_formatting_failure(cx);
|
||||
});
|
||||
cx.dispatch_action(Box::new(workspace::OpenLog));
|
||||
window.dispatch_action(Box::new(workspace::OpenLog), cx);
|
||||
})),
|
||||
});
|
||||
}
|
||||
@@ -370,8 +385,8 @@ impl ActivityIndicator {
|
||||
.into_any_element(),
|
||||
),
|
||||
message: "Checking for Zed updates…".to_string(),
|
||||
on_click: Some(Arc::new(|this, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, cx)
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
}),
|
||||
AutoUpdateStatus::Downloading => Some(Content {
|
||||
@@ -381,8 +396,8 @@ impl ActivityIndicator {
|
||||
.into_any_element(),
|
||||
),
|
||||
message: "Downloading Zed update…".to_string(),
|
||||
on_click: Some(Arc::new(|this, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, cx)
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
}),
|
||||
AutoUpdateStatus::Installing => Some(Content {
|
||||
@@ -392,8 +407,8 @@ impl ActivityIndicator {
|
||||
.into_any_element(),
|
||||
),
|
||||
message: "Installing Zed update…".to_string(),
|
||||
on_click: Some(Arc::new(|this, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, cx)
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
}),
|
||||
AutoUpdateStatus::Updated { binary_path } => Some(Content {
|
||||
@@ -403,7 +418,7 @@ impl ActivityIndicator {
|
||||
let reload = workspace::Reload {
|
||||
binary_path: Some(binary_path.clone()),
|
||||
};
|
||||
move |_, cx| workspace::reload(&reload, cx)
|
||||
move |_, _, cx| workspace::reload(&reload, cx)
|
||||
})),
|
||||
}),
|
||||
AutoUpdateStatus::Errored => Some(Content {
|
||||
@@ -413,8 +428,8 @@ impl ActivityIndicator {
|
||||
.into_any_element(),
|
||||
),
|
||||
message: "Auto update failed".to_string(),
|
||||
on_click: Some(Arc::new(|this, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, cx)
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
}),
|
||||
AutoUpdateStatus::Idle => None,
|
||||
@@ -432,8 +447,8 @@ impl ActivityIndicator {
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!("Updating {extension_id} extension…"),
|
||||
on_click: Some(Arc::new(|this, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, cx)
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
});
|
||||
}
|
||||
@@ -442,8 +457,12 @@ impl ActivityIndicator {
|
||||
None
|
||||
}
|
||||
|
||||
fn toggle_language_server_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.context_menu_handle.toggle(cx);
|
||||
fn toggle_language_server_work_context_menu(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.context_menu_handle.toggle(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,7 +471,7 @@ impl EventEmitter<Event> for ActivityIndicator {}
|
||||
const MAX_MESSAGE_LEN: usize = 50;
|
||||
|
||||
impl Render for ActivityIndicator {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
|
||||
let result = h_flex()
|
||||
.id("activity-indicator")
|
||||
.on_action(cx.listener(Self::show_error_message))
|
||||
@@ -460,7 +479,7 @@ impl Render for ActivityIndicator {
|
||||
let Some(content) = self.content_to_render(cx) else {
|
||||
return result;
|
||||
};
|
||||
let this = cx.view().downgrade();
|
||||
let this = cx.model().downgrade();
|
||||
let truncate_content = content.message.len() > MAX_MESSAGE_LEN;
|
||||
result.gap_2().child(
|
||||
PopoverMenu::new("activity-indicator-popover")
|
||||
@@ -480,24 +499,24 @@ impl Render for ActivityIndicator {
|
||||
))
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::text(&content.message, cx))
|
||||
.tooltip(Tooltip::text(content.message))
|
||||
} else {
|
||||
button.child(Label::new(content.message).size(LabelSize::Small))
|
||||
}
|
||||
})
|
||||
.when_some(content.on_click, |this, handler| {
|
||||
this.on_click(cx.listener(move |this, _, cx| {
|
||||
handler(this, cx);
|
||||
this.on_click(cx.listener(move |this, _, window, cx| {
|
||||
handler(this, window, cx);
|
||||
}))
|
||||
.cursor(CursorStyle::PointingHand)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.menu(move |cx| {
|
||||
.menu(move |window, cx| {
|
||||
let strong_this = this.upgrade()?;
|
||||
let mut has_work = false;
|
||||
let menu = ContextMenu::build(cx, |mut menu, cx| {
|
||||
let menu = ContextMenu::build(window, cx, |mut menu, _, cx| {
|
||||
for work in strong_this.read(cx).pending_language_server_work(cx) {
|
||||
has_work = true;
|
||||
let this = this.clone();
|
||||
@@ -513,7 +532,7 @@ impl Render for ActivityIndicator {
|
||||
let token = work.progress_token.to_string();
|
||||
let title = SharedString::from(title);
|
||||
menu = menu.custom_entry(
|
||||
move |_| {
|
||||
move |_, _| {
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
@@ -521,7 +540,7 @@ impl Render for ActivityIndicator {
|
||||
.child(Icon::new(IconName::XCircle))
|
||||
.into_any_element()
|
||||
},
|
||||
move |cx| {
|
||||
move |_, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.project.update(cx, |project, cx| {
|
||||
project.cancel_language_server_work(
|
||||
@@ -554,5 +573,11 @@ impl Render for ActivityIndicator {
|
||||
}
|
||||
|
||||
impl StatusItemView for ActivityIndicator {
|
||||
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
_: Option<&dyn ItemHandle>,
|
||||
_window: &mut Window,
|
||||
_: &mut ModelContext<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@ use futures::{
|
||||
channel::mpsc,
|
||||
stream::{self, StreamExt},
|
||||
};
|
||||
use gpui::{prelude::*, AppContext, Model, SharedString, Task, TestAppContext, WeakView};
|
||||
use gpui::{prelude::*, AppContext, Model, SharedString, Task, TestAppContext, WeakModel};
|
||||
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
|
||||
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
|
||||
use parking_lot::Mutex;
|
||||
@@ -35,7 +35,7 @@ use std::{
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToOffset};
|
||||
use ui::{IconName, WindowContext};
|
||||
use ui::{IconName, Window};
|
||||
use unindent::Unindent;
|
||||
use util::{
|
||||
test::{generate_marked_text, marked_text_ranges},
|
||||
@@ -1642,8 +1642,9 @@ impl SlashCommand for FakeSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(vec![]))
|
||||
}
|
||||
@@ -1657,9 +1658,10 @@ impl SlashCommand for FakeSlashCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_cx: &mut WindowContext,
|
||||
_window: &mut Window,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
Task::ready(Ok(SlashCommandOutput {
|
||||
text: format!("Executed fake command: {}", self.0),
|
||||
|
||||
@@ -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,
|
||||
@@ -319,7 +320,7 @@ impl ContextStore {
|
||||
.client
|
||||
.subscribe_to_entity(remote_id)
|
||||
.log_err()
|
||||
.map(|subscription| subscription.set_model(&cx.handle(), &mut cx.to_async()));
|
||||
.map(|subscription| subscription.set_model(&cx.model(), &mut cx.to_async()));
|
||||
self.advertise_contexts(cx);
|
||||
} else {
|
||||
self.client_subscription = None;
|
||||
@@ -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,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,8 +11,8 @@ use futures::{
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
actions, point, size, transparent_black, Action, AppContext, BackgroundExecutor, Bounds,
|
||||
EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TextStyle, TitlebarOptions,
|
||||
UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
|
||||
EventEmitter, Focusable, Global, Model, PromptLevel, ReadGlobal, Subscription, Task, TextStyle,
|
||||
TitlebarOptions, UpdateGlobal, WindowBounds, WindowHandle, WindowOptions,
|
||||
};
|
||||
use heed::{
|
||||
types::{SerdeBincode, SerdeJson, Str},
|
||||
@@ -38,8 +38,8 @@ use std::{
|
||||
use text::LineEnding;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
div, prelude::*, IconButtonShape, KeyBinding, ListItem, ListItemSpacing, ParentElement, Render,
|
||||
SharedString, Styled, Tooltip, ViewContext, VisualContext,
|
||||
div, prelude::*, IconButtonShape, KeyBinding, ListItem, ListItemSpacing, ModelContext,
|
||||
ParentElement, Render, SharedString, Styled, Tooltip, Window,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
@@ -88,7 +88,7 @@ pub fn open_prompt_library(
|
||||
.find_map(|window| window.downcast::<PromptLibrary>());
|
||||
if let Some(existing_window) = existing_window {
|
||||
existing_window
|
||||
.update(cx, |_, cx| cx.activate_window())
|
||||
.update(cx, |_, window, _| window.activate_window())
|
||||
.ok();
|
||||
Task::ready(Ok(existing_window))
|
||||
} else {
|
||||
@@ -109,7 +109,9 @@ pub fn open_prompt_library(
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|cx| cx.new_view(|cx| PromptLibrary::new(store, language_registry, cx)),
|
||||
|window, cx| {
|
||||
cx.new_model(|cx| PromptLibrary::new(store, language_registry, window, cx))
|
||||
},
|
||||
)
|
||||
})?
|
||||
})
|
||||
@@ -121,14 +123,14 @@ pub struct PromptLibrary {
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
prompt_editors: HashMap<PromptId, PromptEditor>,
|
||||
active_prompt_id: Option<PromptId>,
|
||||
picker: View<Picker<PromptPickerDelegate>>,
|
||||
picker: Model<Picker<PromptPickerDelegate>>,
|
||||
pending_load: Task<()>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
struct PromptEditor {
|
||||
title_editor: View<Editor>,
|
||||
body_editor: View<Editor>,
|
||||
title_editor: Model<Editor>,
|
||||
body_editor: Model<Editor>,
|
||||
token_count: Option<usize>,
|
||||
pending_token_count: Task<Option<()>>,
|
||||
next_title_and_body_to_save: Option<(String, Rope)>,
|
||||
@@ -158,7 +160,7 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut AppContext) -> SharedString {
|
||||
if self.store.prompt_count() == 0 {
|
||||
"No prompts.".into()
|
||||
} else {
|
||||
@@ -170,7 +172,12 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_: &mut Window,
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
if let Some(prompt) = self.matches.get(self.selected_index) {
|
||||
cx.emit(PromptPickerEvent::Selected {
|
||||
@@ -179,14 +186,19 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut AppContext) -> Arc<str> {
|
||||
"Search...".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let search = self.store.search(query);
|
||||
let prev_prompt_id = self.matches.get(self.selected_index).map(|mat| mat.id);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.spawn_in(window, |this, mut cx| async move {
|
||||
let (matches, selected_index) = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
@@ -201,16 +213,16 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update_in(&mut cx, |this, window, cx| {
|
||||
this.delegate.matches = matches;
|
||||
this.delegate.set_selected_index(selected_index, cx);
|
||||
this.delegate.set_selected_index(selected_index, window, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn confirm(&mut self, _secondary: bool, _: &mut Window, cx: &mut ModelContext<Picker<Self>>) {
|
||||
if let Some(prompt) = self.matches.get(self.selected_index) {
|
||||
cx.emit(PromptPickerEvent::Confirmed {
|
||||
prompt_id: prompt.id,
|
||||
@@ -218,13 +230,14 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
fn dismissed(&mut self, _window: &mut Window, _cx: &mut ModelContext<Picker<Self>>) {}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
_: &mut Window,
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let prompt = self.matches.get(ix)?;
|
||||
let default = prompt.default;
|
||||
@@ -241,8 +254,8 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
.toggle_state(true)
|
||||
.icon_color(Color::Accent)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Remove from Default Prompt", cx))
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
.tooltip(Tooltip::text("Remove from Default Prompt"))
|
||||
.on_click(cx.listener(move |_, _, _, cx| {
|
||||
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
|
||||
}))
|
||||
}))
|
||||
@@ -253,11 +266,12 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
div()
|
||||
.id("built-in-prompt")
|
||||
.child(Icon::new(IconName::FileLock).color(Color::Muted))
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Built-in prompt",
|
||||
None,
|
||||
BUILT_IN_TOOLTIP_TEXT,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -266,8 +280,8 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
IconButton::new("delete-prompt", IconName::Trash)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
.tooltip(Tooltip::text("Delete Prompt"))
|
||||
.on_click(cx.listener(move |_, _, _, cx| {
|
||||
cx.emit(PromptPickerEvent::Deleted { prompt_id })
|
||||
}))
|
||||
.into_any_element()
|
||||
@@ -278,17 +292,12 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
.selected_icon(IconName::SparkleFilled)
|
||||
.icon_color(if default { Color::Accent } else { Color::Muted })
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(
|
||||
if default {
|
||||
"Remove from Default Prompt"
|
||||
} else {
|
||||
"Add to Default Prompt"
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
.tooltip(Tooltip::text(if default {
|
||||
"Remove from Default Prompt"
|
||||
} else {
|
||||
"Add to Default Prompt"
|
||||
}))
|
||||
.on_click(cx.listener(move |_, _, _, cx| {
|
||||
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
|
||||
})),
|
||||
),
|
||||
@@ -296,7 +305,12 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
Some(element)
|
||||
}
|
||||
|
||||
fn render_editor(&self, editor: &View<Editor>, cx: &mut ViewContext<Picker<Self>>) -> Div {
|
||||
fn render_editor(
|
||||
&self,
|
||||
editor: &Model<Editor>,
|
||||
_: &mut Window,
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) -> Div {
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_md()
|
||||
@@ -313,7 +327,8 @@ impl PromptLibrary {
|
||||
fn new(
|
||||
store: Arc<PromptStore>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let delegate = PromptPickerDelegate {
|
||||
store: store.clone(),
|
||||
@@ -321,11 +336,11 @@ impl PromptLibrary {
|
||||
matches: Vec::new(),
|
||||
};
|
||||
|
||||
let picker = cx.new_view(|cx| {
|
||||
let picker = Picker::uniform_list(delegate, cx)
|
||||
let picker = cx.new_model(|cx| {
|
||||
let picker = Picker::uniform_list(delegate, window, cx)
|
||||
.modal(false)
|
||||
.max_height(None);
|
||||
picker.focus(cx);
|
||||
picker.focus(window, cx);
|
||||
picker
|
||||
});
|
||||
Self {
|
||||
@@ -334,54 +349,63 @@ impl PromptLibrary {
|
||||
prompt_editors: HashMap::default(),
|
||||
active_prompt_id: None,
|
||||
pending_load: Task::ready(()),
|
||||
_subscriptions: vec![cx.subscribe(&picker, Self::handle_picker_event)],
|
||||
_subscriptions: vec![cx.subscribe_in(&picker, window, Self::handle_picker_event)],
|
||||
picker,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_picker_event(
|
||||
&mut self,
|
||||
_: View<Picker<PromptPickerDelegate>>,
|
||||
_: &Model<Picker<PromptPickerDelegate>>,
|
||||
event: &PromptPickerEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
PromptPickerEvent::Selected { prompt_id } => {
|
||||
self.load_prompt(*prompt_id, false, cx);
|
||||
self.load_prompt(*prompt_id, false, window, cx);
|
||||
}
|
||||
PromptPickerEvent::Confirmed { prompt_id } => {
|
||||
self.load_prompt(*prompt_id, true, cx);
|
||||
self.load_prompt(*prompt_id, true, window, cx);
|
||||
}
|
||||
PromptPickerEvent::ToggledDefault { prompt_id } => {
|
||||
self.toggle_default_for_prompt(*prompt_id, cx);
|
||||
self.toggle_default_for_prompt(*prompt_id, window, cx);
|
||||
}
|
||||
PromptPickerEvent::Deleted { prompt_id } => {
|
||||
self.delete_prompt(*prompt_id, cx);
|
||||
self.delete_prompt(*prompt_id, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_prompt(&mut self, cx: &mut ViewContext<Self>) {
|
||||
pub fn new_prompt(&mut self, window: &mut Window, cx: &mut ModelContext<Self>) {
|
||||
// If we already have an untitled prompt, use that instead
|
||||
// of creating a new one.
|
||||
if let Some(metadata) = self.store.first() {
|
||||
if metadata.title.is_none() {
|
||||
self.load_prompt(metadata.id, true, cx);
|
||||
self.load_prompt(metadata.id, true, window, cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let prompt_id = PromptId::new();
|
||||
let save = self.store.save(prompt_id, None, false, "".into());
|
||||
self.picker.update(cx, |picker, cx| picker.refresh(cx));
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
self.picker
|
||||
.update(cx, |picker, cx| picker.refresh(window, cx));
|
||||
cx.spawn_in(window, |this, mut cx| async move {
|
||||
save.await?;
|
||||
this.update(&mut cx, |this, cx| this.load_prompt(prompt_id, true, cx))
|
||||
this.update_in(&mut cx, |this, window, cx| {
|
||||
this.load_prompt(prompt_id, true, window, cx)
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
pub fn save_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||
pub fn save_prompt(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
const SAVE_THROTTLE: Duration = Duration::from_millis(500);
|
||||
|
||||
if prompt_id.is_built_in() {
|
||||
@@ -407,7 +431,7 @@ impl PromptLibrary {
|
||||
|
||||
prompt_editor.next_title_and_body_to_save = Some((title, body));
|
||||
if prompt_editor.pending_save.is_none() {
|
||||
prompt_editor.pending_save = Some(cx.spawn(|this, mut cx| {
|
||||
prompt_editor.pending_save = Some(cx.spawn_in(window, |this, mut cx| {
|
||||
async move {
|
||||
loop {
|
||||
let title_and_body = this.update(&mut cx, |this, _| {
|
||||
@@ -427,8 +451,9 @@ impl PromptLibrary {
|
||||
.save(prompt_id, title, prompt_metadata.default, body)
|
||||
.await
|
||||
.log_err();
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.picker.update(cx, |picker, cx| picker.refresh(cx));
|
||||
this.update_in(&mut cx, |this, window, cx| {
|
||||
this.picker
|
||||
.update(cx, |picker, cx| picker.refresh(window, cx));
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
@@ -449,61 +474,77 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
|
||||
pub fn delete_active_prompt(&mut self, window: &mut Window, cx: &mut ModelContext<Self>) {
|
||||
if let Some(active_prompt_id) = self.active_prompt_id {
|
||||
self.delete_prompt(active_prompt_id, cx);
|
||||
self.delete_prompt(active_prompt_id, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn duplicate_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
|
||||
pub fn duplicate_active_prompt(&mut self, window: &mut Window, cx: &mut ModelContext<Self>) {
|
||||
if let Some(active_prompt_id) = self.active_prompt_id {
|
||||
self.duplicate_prompt(active_prompt_id, cx);
|
||||
self.duplicate_prompt(active_prompt_id, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_default_for_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
|
||||
pub fn toggle_default_for_active_prompt(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let Some(active_prompt_id) = self.active_prompt_id {
|
||||
self.toggle_default_for_prompt(active_prompt_id, cx);
|
||||
self.toggle_default_for_prompt(active_prompt_id, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_default_for_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||
pub fn toggle_default_for_prompt(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let Some(prompt_metadata) = self.store.metadata(prompt_id) {
|
||||
self.store
|
||||
.save_metadata(prompt_id, prompt_metadata.title, !prompt_metadata.default)
|
||||
.detach_and_log_err(cx);
|
||||
self.picker.update(cx, |picker, cx| picker.refresh(cx));
|
||||
self.picker
|
||||
.update(cx, |picker, cx| picker.refresh(window, cx));
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_prompt(&mut self, prompt_id: PromptId, focus: bool, cx: &mut ViewContext<Self>) {
|
||||
pub fn load_prompt(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
focus: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
|
||||
if focus {
|
||||
prompt_editor
|
||||
.body_editor
|
||||
.update(cx, |editor, cx| editor.focus(cx));
|
||||
.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)));
|
||||
}
|
||||
self.set_active_prompt(Some(prompt_id), cx);
|
||||
self.set_active_prompt(Some(prompt_id), window, cx);
|
||||
} else if let Some(prompt_metadata) = self.store.metadata(prompt_id) {
|
||||
let language_registry = self.language_registry.clone();
|
||||
let prompt = self.store.load(prompt_id);
|
||||
self.pending_load = cx.spawn(|this, mut cx| async move {
|
||||
self.pending_load = cx.spawn_in(window, |this, mut cx| async move {
|
||||
let prompt = prompt.await;
|
||||
let markdown = language_registry.language_for_name("Markdown").await;
|
||||
this.update(&mut cx, |this, cx| match prompt {
|
||||
this.update_in(&mut cx, |this, window, cx| match prompt {
|
||||
Ok(prompt) => {
|
||||
let title_editor = cx.new_view(|cx| {
|
||||
let mut editor = Editor::auto_width(cx);
|
||||
let title_editor = cx.new_model(|cx| {
|
||||
let mut editor = Editor::auto_width(window, cx);
|
||||
editor.set_placeholder_text("Untitled", cx);
|
||||
editor.set_text(prompt_metadata.title.unwrap_or_default(), cx);
|
||||
editor.set_text(prompt_metadata.title.unwrap_or_default(), window, cx);
|
||||
if prompt_id.is_built_in() {
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_inline_completions(Some(false), cx);
|
||||
editor.set_show_inline_completions(Some(false), window, cx);
|
||||
}
|
||||
editor
|
||||
});
|
||||
let body_editor = cx.new_view(|cx| {
|
||||
let body_editor = cx.new_model(|cx| {
|
||||
let buffer = cx.new_model(|cx| {
|
||||
let mut buffer = Buffer::local(prompt, cx);
|
||||
buffer.set_language(markdown.log_err(), cx);
|
||||
@@ -511,10 +552,10 @@ impl PromptLibrary {
|
||||
buffer
|
||||
});
|
||||
|
||||
let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
let mut editor = Editor::for_buffer(buffer, None, window, cx);
|
||||
if prompt_id.is_built_in() {
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_inline_completions(Some(false), cx);
|
||||
editor.set_show_inline_completions(Some(false), window, cx);
|
||||
}
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
@@ -530,17 +571,29 @@ impl PromptLibrary {
|
||||
),
|
||||
)));
|
||||
if focus {
|
||||
editor.focus(cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
}
|
||||
editor
|
||||
});
|
||||
let _subscriptions = vec![
|
||||
cx.subscribe(&title_editor, move |this, editor, event, cx| {
|
||||
this.handle_prompt_title_editor_event(prompt_id, editor, event, cx)
|
||||
}),
|
||||
cx.subscribe(&body_editor, move |this, editor, event, cx| {
|
||||
this.handle_prompt_body_editor_event(prompt_id, editor, event, cx)
|
||||
}),
|
||||
cx.subscribe_in(
|
||||
&title_editor,
|
||||
window,
|
||||
move |this, editor, event, window, cx| {
|
||||
this.handle_prompt_title_editor_event(
|
||||
prompt_id, editor, event, window, cx,
|
||||
)
|
||||
},
|
||||
),
|
||||
cx.subscribe_in(
|
||||
&body_editor,
|
||||
window,
|
||||
move |this, editor, event, window, cx| {
|
||||
this.handle_prompt_body_editor_event(
|
||||
prompt_id, editor, event, window, cx,
|
||||
)
|
||||
},
|
||||
),
|
||||
];
|
||||
this.prompt_editors.insert(
|
||||
prompt_id,
|
||||
@@ -554,8 +607,8 @@ impl PromptLibrary {
|
||||
_subscriptions,
|
||||
},
|
||||
);
|
||||
this.set_active_prompt(Some(prompt_id), cx);
|
||||
this.count_tokens(prompt_id, cx);
|
||||
this.set_active_prompt(Some(prompt_id), window, cx);
|
||||
this.count_tokens(prompt_id, window, cx);
|
||||
}
|
||||
Err(error) => {
|
||||
// TODO: we should show the error in the UI.
|
||||
@@ -567,7 +620,12 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_active_prompt(&mut self, prompt_id: Option<PromptId>, cx: &mut ViewContext<Self>) {
|
||||
fn set_active_prompt(
|
||||
&mut self,
|
||||
prompt_id: Option<PromptId>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.active_prompt_id = prompt_id;
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
if let Some(prompt_id) = prompt_id {
|
||||
@@ -585,19 +643,24 @@ impl PromptLibrary {
|
||||
.iter()
|
||||
.position(|mat| mat.id == prompt_id)
|
||||
{
|
||||
picker.set_selected_index(ix, true, cx);
|
||||
picker.set_selected_index(ix, true, window, cx);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
picker.focus(cx);
|
||||
picker.focus(window, cx);
|
||||
}
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn delete_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||
pub fn delete_prompt(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let Some(metadata) = self.store.metadata(prompt_id) {
|
||||
let confirmation = cx.prompt(
|
||||
let confirmation = window.prompt(
|
||||
PromptLevel::Warning,
|
||||
&format!(
|
||||
"Are you sure you want to delete {}",
|
||||
@@ -605,17 +668,19 @@ impl PromptLibrary {
|
||||
),
|
||||
None,
|
||||
&["Delete", "Cancel"],
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.spawn_in(window, |this, mut cx| async move {
|
||||
if confirmation.await.ok() == Some(0) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update_in(&mut cx, |this, window, cx| {
|
||||
if this.active_prompt_id == Some(prompt_id) {
|
||||
this.set_active_prompt(None, cx);
|
||||
this.set_active_prompt(None, window, cx);
|
||||
}
|
||||
this.prompt_editors.remove(&prompt_id);
|
||||
this.store.delete(prompt_id).detach_and_log_err(cx);
|
||||
this.picker.update(cx, |picker, cx| picker.refresh(cx));
|
||||
this.picker
|
||||
.update(cx, |picker, cx| picker.refresh(window, cx));
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
@@ -625,7 +690,12 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn duplicate_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||
pub fn duplicate_prompt(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let Some(prompt) = self.prompt_editors.get(&prompt_id) {
|
||||
const DUPLICATE_SUFFIX: &str = " copy";
|
||||
let title_to_duplicate = prompt.title_editor.read(cx).text(cx);
|
||||
@@ -655,31 +725,38 @@ impl PromptLibrary {
|
||||
let save = self
|
||||
.store
|
||||
.save(new_id, Some(title.into()), false, body.into());
|
||||
self.picker.update(cx, |picker, cx| picker.refresh(cx));
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
self.picker
|
||||
.update(cx, |picker, cx| picker.refresh(window, cx));
|
||||
cx.spawn_in(window, |this, mut cx| async move {
|
||||
save.await?;
|
||||
this.update(&mut cx, |prompt_library, cx| {
|
||||
prompt_library.load_prompt(new_id, true, cx)
|
||||
this.update_in(&mut cx, |prompt_library, window, cx| {
|
||||
prompt_library.load_prompt(new_id, true, window, cx)
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_active_prompt(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
|
||||
fn focus_active_prompt(&mut self, _: &Tab, window: &mut Window, cx: &mut ModelContext<Self>) {
|
||||
if let Some(active_prompt) = self.active_prompt_id {
|
||||
self.prompt_editors[&active_prompt]
|
||||
.body_editor
|
||||
.update(cx, |editor, cx| editor.focus(cx));
|
||||
.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)));
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_picker(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| picker.focus(cx));
|
||||
fn focus_picker(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut ModelContext<Self>) {
|
||||
self.picker
|
||||
.update(cx, |picker, cx| picker.focus(window, cx));
|
||||
}
|
||||
|
||||
pub fn inline_assist(&mut self, action: &InlineAssist, cx: &mut ViewContext<Self>) {
|
||||
pub fn inline_assist(
|
||||
&mut self,
|
||||
action: &InlineAssist,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let Some(active_prompt_id) = self.active_prompt_id else {
|
||||
cx.propagate();
|
||||
return;
|
||||
@@ -693,15 +770,15 @@ impl PromptLibrary {
|
||||
let initial_prompt = action.prompt.clone();
|
||||
if provider.is_authenticated(cx) {
|
||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.assist(&prompt_editor, None, None, initial_prompt, cx)
|
||||
assistant.assist(&prompt_editor, None, None, initial_prompt, window, cx)
|
||||
})
|
||||
} else {
|
||||
for window in cx.windows() {
|
||||
if let Some(workspace) = window.downcast::<Workspace>() {
|
||||
let panel = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
cx.activate_window();
|
||||
workspace.focus_panel::<AssistantPanel>(cx)
|
||||
.update(cx, |workspace, window, cx| {
|
||||
window.activate_window();
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx)
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
@@ -713,18 +790,28 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
fn move_down_from_title(&mut self, _: &editor::actions::MoveDown, cx: &mut ViewContext<Self>) {
|
||||
fn move_down_from_title(
|
||||
&mut self,
|
||||
_: &editor::actions::MoveDown,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let Some(prompt_id) = self.active_prompt_id {
|
||||
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
|
||||
cx.focus_view(&prompt_editor.body_editor);
|
||||
window.focus(&prompt_editor.body_editor.focus_handle(cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn move_up_from_body(&mut self, _: &editor::actions::MoveUp, cx: &mut ViewContext<Self>) {
|
||||
fn move_up_from_body(
|
||||
&mut self,
|
||||
_: &editor::actions::MoveUp,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let Some(prompt_id) = self.active_prompt_id {
|
||||
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
|
||||
cx.focus_view(&prompt_editor.title_editor);
|
||||
window.focus(&prompt_editor.title_editor.focus_handle(cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -732,18 +819,19 @@ impl PromptLibrary {
|
||||
fn handle_prompt_title_editor_event(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
title_editor: View<Editor>,
|
||||
title_editor: &Model<Editor>,
|
||||
event: &EditorEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
EditorEvent::BufferEdited => {
|
||||
self.save_prompt(prompt_id, cx);
|
||||
self.count_tokens(prompt_id, cx);
|
||||
self.save_prompt(prompt_id, window, cx);
|
||||
self.count_tokens(prompt_id, window, cx);
|
||||
}
|
||||
EditorEvent::Blurred => {
|
||||
title_editor.update(cx, |title_editor, cx| {
|
||||
title_editor.change_selections(None, cx, |selections| {
|
||||
title_editor.change_selections(None, window, cx, |selections| {
|
||||
let cursor = selections.oldest_anchor().head();
|
||||
selections.select_anchor_ranges([cursor..cursor]);
|
||||
});
|
||||
@@ -756,18 +844,19 @@ impl PromptLibrary {
|
||||
fn handle_prompt_body_editor_event(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
body_editor: View<Editor>,
|
||||
body_editor: &Model<Editor>,
|
||||
event: &EditorEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
EditorEvent::BufferEdited => {
|
||||
self.save_prompt(prompt_id, cx);
|
||||
self.count_tokens(prompt_id, cx);
|
||||
self.save_prompt(prompt_id, window, cx);
|
||||
self.count_tokens(prompt_id, window, cx);
|
||||
}
|
||||
EditorEvent::Blurred => {
|
||||
body_editor.update(cx, |body_editor, cx| {
|
||||
body_editor.change_selections(None, cx, |selections| {
|
||||
body_editor.change_selections(None, window, cx, |selections| {
|
||||
let cursor = selections.oldest_anchor().head();
|
||||
selections.select_anchor_ranges([cursor..cursor]);
|
||||
});
|
||||
@@ -777,7 +866,12 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
fn count_tokens(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||
fn count_tokens(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
|
||||
return;
|
||||
};
|
||||
@@ -785,13 +879,13 @@ impl PromptLibrary {
|
||||
let editor = &prompt.body_editor.read(cx);
|
||||
let buffer = &editor.buffer().read(cx).as_singleton().unwrap().read(cx);
|
||||
let body = buffer.as_rope().clone();
|
||||
prompt.pending_token_count = cx.spawn(|this, mut cx| {
|
||||
prompt.pending_token_count = cx.spawn_in(window, |this, mut cx| {
|
||||
async move {
|
||||
const DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
|
||||
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
|
||||
let token_count = cx
|
||||
.update(|cx| {
|
||||
.update(|_, cx| {
|
||||
model.count_tokens(
|
||||
LanguageModelRequest {
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
@@ -819,7 +913,7 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_prompt_list(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render_prompt_list(&mut self, cx: &mut ModelContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.id("prompt-list")
|
||||
.capture_action(cx.listener(Self::focus_active_prompt))
|
||||
@@ -839,16 +933,21 @@ impl PromptLibrary {
|
||||
IconButton::new("new-prompt", IconName::Plus)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::for_action("New Prompt", &NewPrompt, cx))
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(NewPrompt));
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action("New Prompt", &NewPrompt, window, cx)
|
||||
})
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(Box::new(NewPrompt), cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(div().flex_grow().child(self.picker.clone()))
|
||||
}
|
||||
|
||||
fn render_active_prompt(&mut self, cx: &mut ViewContext<PromptLibrary>) -> gpui::Stateful<Div> {
|
||||
fn render_active_prompt(
|
||||
&mut self,
|
||||
cx: &mut ModelContext<PromptLibrary>,
|
||||
) -> gpui::Stateful<Div> {
|
||||
div()
|
||||
.w_2_3()
|
||||
.h_full()
|
||||
@@ -873,8 +972,8 @@ impl PromptLibrary {
|
||||
.overflow_hidden()
|
||||
.pl(DynamicSpacing::Base16.rems(cx))
|
||||
.pt(DynamicSpacing::Base08.rems(cx))
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.focus(&focus_handle);
|
||||
.on_click(cx.listener(move |_, _, window, _| {
|
||||
window.focus(&focus_handle);
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -957,7 +1056,7 @@ impl PromptLibrary {
|
||||
|
||||
h_flex()
|
||||
.id("token_count")
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
let token_count =
|
||||
token_count.clone();
|
||||
|
||||
@@ -976,6 +1075,7 @@ impl PromptLibrary {
|
||||
.0)
|
||||
.unwrap_or_default()
|
||||
),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -995,11 +1095,12 @@ impl PromptLibrary {
|
||||
Icon::new(IconName::FileLock)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Built-in prompt",
|
||||
None,
|
||||
BUILT_IN_TOOLTIP_TEXT,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -1013,15 +1114,19 @@ impl PromptLibrary {
|
||||
.style(ButtonStyle::Transparent)
|
||||
.shape(IconButtonShape::Square)
|
||||
.size(ButtonSize::Large)
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Delete Prompt",
|
||||
&DeletePrompt,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(DeletePrompt));
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(DeletePrompt),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.into_any_element()
|
||||
})
|
||||
@@ -1034,17 +1139,19 @@ impl PromptLibrary {
|
||||
.style(ButtonStyle::Transparent)
|
||||
.shape(IconButtonShape::Square)
|
||||
.size(ButtonSize::Large)
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Duplicate Prompt",
|
||||
&DuplicatePrompt,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(
|
||||
DuplicatePrompt,
|
||||
));
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(DuplicatePrompt),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
@@ -1062,20 +1169,18 @@ impl PromptLibrary {
|
||||
})
|
||||
.shape(IconButtonShape::Square)
|
||||
.size(ButtonSize::Large)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(
|
||||
if prompt_metadata.default {
|
||||
"Remove from Default Prompt"
|
||||
} else {
|
||||
"Add to Default Prompt"
|
||||
},
|
||||
.tooltip(Tooltip::text(
|
||||
if prompt_metadata.default {
|
||||
"Remove from Default Prompt"
|
||||
} else {
|
||||
"Add to Default Prompt"
|
||||
},
|
||||
))
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(ToggleDefaultPrompt),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(
|
||||
ToggleDefaultPrompt,
|
||||
));
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
@@ -1096,18 +1201,24 @@ impl PromptLibrary {
|
||||
}
|
||||
|
||||
impl Render for PromptLibrary {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let ui_font = theme::setup_ui_font(cx);
|
||||
fn render(&mut self, window: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
|
||||
let ui_font = theme::setup_ui_font(window, cx);
|
||||
let theme = cx.theme().clone();
|
||||
|
||||
h_flex()
|
||||
.id("prompt-manager")
|
||||
.key_context("PromptLibrary")
|
||||
.on_action(cx.listener(|this, &NewPrompt, cx| this.new_prompt(cx)))
|
||||
.on_action(cx.listener(|this, &DeletePrompt, cx| this.delete_active_prompt(cx)))
|
||||
.on_action(cx.listener(|this, &DuplicatePrompt, cx| this.duplicate_active_prompt(cx)))
|
||||
.on_action(cx.listener(|this, &ToggleDefaultPrompt, cx| {
|
||||
this.toggle_default_for_active_prompt(cx)
|
||||
.on_action(cx.listener(|this, &NewPrompt, window, cx| this.new_prompt(window, cx)))
|
||||
.on_action(
|
||||
cx.listener(|this, &DeletePrompt, window, cx| {
|
||||
this.delete_active_prompt(window, cx)
|
||||
}),
|
||||
)
|
||||
.on_action(cx.listener(|this, &DuplicatePrompt, window, cx| {
|
||||
this.duplicate_active_prompt(window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|this, &ToggleDefaultPrompt, window, cx| {
|
||||
this.toggle_default_for_active_prompt(window, cx)
|
||||
}))
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
@@ -1149,10 +1260,13 @@ impl Render for PromptLibrary {
|
||||
Button::new("create-prompt", "New Prompt")
|
||||
.full_width()
|
||||
.key_binding(KeyBinding::for_action(
|
||||
&NewPrompt, cx,
|
||||
&NewPrompt, window,
|
||||
))
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(NewPrompt.boxed_clone())
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(
|
||||
NewPrompt.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ use assistant_slash_command::AfterCompletion;
|
||||
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput};
|
||||
use editor::{CompletionProvider, Editor};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext};
|
||||
use gpui::{AppContext, Model, ModelContext, Task, WeakModel, Window};
|
||||
use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
|
||||
use parking_lot::Mutex;
|
||||
use project::CompletionIntent;
|
||||
@@ -43,8 +43,8 @@ pub mod terminal_command;
|
||||
pub(crate) struct SlashCommandCompletionProvider {
|
||||
cancel_flag: Mutex<Arc<AtomicBool>>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
editor: Option<WeakView<ContextEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
editor: Option<WeakModel<ContextEditor>>,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
}
|
||||
|
||||
pub(crate) struct SlashCommandLine {
|
||||
@@ -57,8 +57,8 @@ pub(crate) struct SlashCommandLine {
|
||||
impl SlashCommandCompletionProvider {
|
||||
pub fn new(
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
editor: Option<WeakView<ContextEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
editor: Option<WeakModel<ContextEditor>>,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
|
||||
@@ -73,7 +73,8 @@ impl SlashCommandCompletionProvider {
|
||||
command_name: &str,
|
||||
command_range: Range<Anchor>,
|
||||
name_range: Range<Anchor>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
let slash_commands = self.slash_commands.clone();
|
||||
let candidates = slash_commands
|
||||
@@ -85,7 +86,7 @@ impl SlashCommandCompletionProvider {
|
||||
let command_name = command_name.to_string();
|
||||
let editor = self.editor.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
window.spawn(cx, |mut cx| async move {
|
||||
let matches = match_strings(
|
||||
&candidates,
|
||||
&command_name,
|
||||
@@ -96,7 +97,7 @@ impl SlashCommandCompletionProvider {
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.update(|_, cx| {
|
||||
matches
|
||||
.into_iter()
|
||||
.filter_map(|mat| {
|
||||
@@ -118,28 +119,31 @@ impl SlashCommandCompletionProvider {
|
||||
let editor = editor.clone();
|
||||
let workspace = workspace.clone();
|
||||
Arc::new(
|
||||
move |intent: CompletionIntent, cx: &mut WindowContext| {
|
||||
if !requires_argument
|
||||
&& (!accepts_arguments || intent.is_complete())
|
||||
{
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
&[],
|
||||
true,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
false
|
||||
} else {
|
||||
requires_argument || accepts_arguments
|
||||
}
|
||||
},
|
||||
) as Arc<_>
|
||||
move |intent: CompletionIntent,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext| {
|
||||
if !requires_argument
|
||||
&& (!accepts_arguments || intent.is_complete())
|
||||
{
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
&[],
|
||||
true,
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
false
|
||||
} else {
|
||||
requires_argument || accepts_arguments
|
||||
}
|
||||
},
|
||||
) as Arc<_>
|
||||
});
|
||||
Some(project::Completion {
|
||||
old_range: name_range.clone(),
|
||||
@@ -149,6 +153,7 @@ impl SlashCommandCompletionProvider {
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
confirm,
|
||||
resolved: true,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
@@ -163,7 +168,8 @@ impl SlashCommandCompletionProvider {
|
||||
command_range: Range<Anchor>,
|
||||
argument_range: Range<Anchor>,
|
||||
last_argument_range: Range<Anchor>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
let new_cancel_flag = Arc::new(AtomicBool::new(false));
|
||||
let mut flag = self.cancel_flag.lock();
|
||||
@@ -174,6 +180,7 @@ impl SlashCommandCompletionProvider {
|
||||
arguments,
|
||||
new_cancel_flag.clone(),
|
||||
self.workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let command_name: Arc<str> = command_name.into();
|
||||
@@ -201,27 +208,30 @@ impl SlashCommandCompletionProvider {
|
||||
|
||||
let command_range = command_range.clone();
|
||||
let command_name = command_name.clone();
|
||||
move |intent: CompletionIntent, cx: &mut WindowContext| {
|
||||
if new_argument.after_completion.run()
|
||||
|| intent.is_complete()
|
||||
{
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
&completed_arguments,
|
||||
true,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
false
|
||||
} else {
|
||||
!new_argument.after_completion.run()
|
||||
}
|
||||
move |intent: CompletionIntent,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext| {
|
||||
if new_argument.after_completion.run()
|
||||
|| intent.is_complete()
|
||||
{
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
&completed_arguments,
|
||||
true,
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
false
|
||||
} else {
|
||||
!new_argument.after_completion.run()
|
||||
}
|
||||
}
|
||||
}) as Arc<_>
|
||||
});
|
||||
|
||||
@@ -242,6 +252,7 @@ impl SlashCommandCompletionProvider {
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
confirm,
|
||||
resolved: true,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
@@ -258,7 +269,8 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_position: Anchor,
|
||||
_: editor::CompletionContext,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Editor>,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
let Some((name, arguments, command_range, last_argument_range)) =
|
||||
buffer.update(cx, |buffer, _cx| {
|
||||
@@ -313,10 +325,11 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
command_range,
|
||||
argument_range,
|
||||
last_argument_range,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
self.complete_command_name(&name, command_range, last_argument_range, cx)
|
||||
self.complete_command_name(&name, command_range, last_argument_range, window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,28 +338,20 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
_: Model<Buffer>,
|
||||
_: Vec<usize>,
|
||||
_: Rc<RefCell<Box<[project::Completion]>>>,
|
||||
_: &mut ViewContext<Editor>,
|
||||
_window: &mut Window,
|
||||
_: &mut ModelContext<Editor>,
|
||||
) -> Task<Result<bool>> {
|
||||
Task::ready(Ok(true))
|
||||
}
|
||||
|
||||
fn apply_additional_edits_for_completion(
|
||||
&self,
|
||||
_: Model<Buffer>,
|
||||
_: project::Completion,
|
||||
_: bool,
|
||||
_: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<Option<language::Transaction>>> {
|
||||
Task::ready(Ok(None))
|
||||
}
|
||||
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
position: language::Anchor,
|
||||
_text: &str,
|
||||
_trigger_in_words: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
_: &mut Window,
|
||||
cx: &mut ModelContext<Editor>,
|
||||
) -> bool {
|
||||
let buffer = buffer.read(cx);
|
||||
let position = position.to_point(buffer);
|
||||
|
||||
@@ -5,7 +5,7 @@ use assistant_slash_command::{
|
||||
};
|
||||
use feature_flags::FeatureFlag;
|
||||
use futures::StreamExt;
|
||||
use gpui::{AppContext, AsyncAppContext, Task, WeakView};
|
||||
use gpui::{AppContext, AsyncAppContext, Task, WeakModel, Window};
|
||||
use language::{CodeLabel, LspAdapterDelegate};
|
||||
use language_model::{
|
||||
LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
|
||||
@@ -14,7 +14,7 @@ use language_model::{
|
||||
use semantic_index::{FileSummary, SemanticDb};
|
||||
use smol::channel;
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use ui::{prelude::*, BorrowAppContext, WindowContext};
|
||||
use ui::{prelude::*, BorrowAppContext};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -53,8 +53,9 @@ impl SlashCommand for AutoCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
// There's no autocomplete for a prompt, since it's arbitrary text.
|
||||
// However, we can use this opportunity to kick off a drain of the backlog.
|
||||
@@ -96,9 +97,10 @@ impl SlashCommand for AutoCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: language::BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
||||
@@ -115,7 +117,7 @@ impl SlashCommand for AutoCommand {
|
||||
return Task::ready(Err(anyhow!("no project indexer")));
|
||||
};
|
||||
|
||||
let task = cx.spawn(|cx: gpui::AsyncWindowContext| async move {
|
||||
let task = window.spawn(cx, |cx| async move {
|
||||
let summaries = project_index
|
||||
.read_with(&cx, |project_index, cx| project_index.all_summaries(cx))?
|
||||
.await?;
|
||||
|
||||
@@ -4,7 +4,7 @@ use assistant_slash_command::{
|
||||
SlashCommandResult,
|
||||
};
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, Model, Task, WeakView};
|
||||
use gpui::{AppContext, Model, Task, WeakModel};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use project::{Project, ProjectPath};
|
||||
use std::{
|
||||
@@ -107,8 +107,9 @@ impl SlashCommand for CargoWorkspaceSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
@@ -122,9 +123,10 @@ impl SlashCommand for CargoWorkspaceSlashCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
_window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let output = workspace.update(cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
|
||||
@@ -8,7 +8,7 @@ use context_server::{
|
||||
manager::{ContextServer, ContextServerManager},
|
||||
types::Prompt,
|
||||
};
|
||||
use gpui::{AppContext, Model, Task, WeakView, WindowContext};
|
||||
use gpui::{AppContext, Model, Task, WeakModel, Window};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
@@ -77,8 +77,9 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let Ok((arg_name, arg_value)) = completion_argument(&self.prompt, arguments) else {
|
||||
return Task::ready(Err(anyhow!("Failed to complete argument")));
|
||||
@@ -128,9 +129,10 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
_window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let server_id = self.server_id.clone();
|
||||
let prompt_name = self.prompt.name.clone();
|
||||
|
||||
@@ -4,7 +4,7 @@ use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
};
|
||||
use gpui::{Task, WeakView};
|
||||
use gpui::{Task, WeakModel};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use std::{
|
||||
fmt::Write,
|
||||
@@ -36,8 +36,9 @@ impl SlashCommand for DefaultSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
@@ -47,9 +48,10 @@ impl SlashCommand for DefaultSlashCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
_window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let store = PromptStore::global(cx);
|
||||
cx.background_executor().spawn(async move {
|
||||
|
||||
@@ -6,7 +6,7 @@ use assistant_slash_command::{
|
||||
};
|
||||
use collections::HashSet;
|
||||
use futures::future;
|
||||
use gpui::{Task, WeakView, WindowContext};
|
||||
use gpui::{AppContext, Task, WeakModel, Window};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use text::OffsetRangeExt;
|
||||
@@ -40,8 +40,9 @@ impl SlashCommand for DeltaSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
@@ -51,9 +52,10 @@ impl SlashCommand for DeltaSlashCommand {
|
||||
_arguments: &[String],
|
||||
context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let mut paths = HashSet::default();
|
||||
let mut file_command_old_outputs = Vec::new();
|
||||
@@ -77,6 +79,7 @@ impl SlashCommand for DeltaSlashCommand {
|
||||
context_buffer.clone(),
|
||||
workspace.clone(),
|
||||
delegate.clone(),
|
||||
window,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use assistant_slash_command::{
|
||||
SlashCommandResult,
|
||||
};
|
||||
use fuzzy::{PathMatch, StringMatchCandidate};
|
||||
use gpui::{AppContext, Model, Task, View, WeakView};
|
||||
use gpui::{AppContext, Model, Task, WeakModel};
|
||||
use language::{
|
||||
Anchor, BufferSnapshot, DiagnosticEntry, DiagnosticSeverity, LspAdapterDelegate,
|
||||
OffsetRangeExt, ToOffset,
|
||||
@@ -30,7 +30,7 @@ impl DiagnosticsSlashCommand {
|
||||
&self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &View<Workspace>,
|
||||
workspace: &Model<Workspace>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
if query.is_empty() {
|
||||
@@ -118,8 +118,9 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
_: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
@@ -172,9 +173,10 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
@@ -184,7 +186,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
||||
|
||||
let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
|
||||
|
||||
cx.spawn(move |_| async move {
|
||||
window.spawn(cx, move |_| async move {
|
||||
task.await?
|
||||
.map(|output| output.to_event_stream())
|
||||
.ok_or_else(|| anyhow!("No diagnostics found"))
|
||||
|
||||
@@ -8,7 +8,7 @@ use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
};
|
||||
use gpui::{AppContext, BackgroundExecutor, Model, Task, WeakView};
|
||||
use gpui::{AppContext, BackgroundExecutor, Model, Task, WeakModel};
|
||||
use indexed_docs::{
|
||||
DocsDotRsProvider, IndexedDocsRegistry, IndexedDocsStore, LocalRustdocProvider, PackageName,
|
||||
ProviderId,
|
||||
@@ -43,7 +43,7 @@ impl DocsSlashCommand {
|
||||
/// access the workspace so we can read the project.
|
||||
fn ensure_rust_doc_providers_are_registered(
|
||||
&self,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let indexed_docs_registry = IndexedDocsRegistry::global(cx);
|
||||
@@ -164,8 +164,9 @@ impl SlashCommand for DocsSlashCommand {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
_: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
self.ensure_rust_doc_providers_are_registered(workspace, cx);
|
||||
|
||||
@@ -272,9 +273,10 @@ impl SlashCommand for DocsSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
_: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
if arguments.is_empty() {
|
||||
return Task::ready(Err(anyhow!("missing an argument")));
|
||||
|
||||
@@ -9,7 +9,7 @@ use assistant_slash_command::{
|
||||
SlashCommandResult,
|
||||
};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{Task, WeakView};
|
||||
use gpui::{Task, WeakModel};
|
||||
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
|
||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
@@ -124,8 +124,9 @@ impl SlashCommand for FetchSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
@@ -135,9 +136,10 @@ impl SlashCommand for FetchSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
_: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(argument) = arguments.first() else {
|
||||
return Task::ready(Err(anyhow!("missing URL")));
|
||||
|
||||
@@ -6,7 +6,7 @@ use assistant_slash_command::{
|
||||
use futures::channel::mpsc;
|
||||
use futures::Stream;
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{AppContext, Model, Task, View, WeakView};
|
||||
use gpui::{AppContext, Model, Task, WeakModel};
|
||||
use language::{BufferSnapshot, CodeLabel, HighlightId, LineEnding, LspAdapterDelegate};
|
||||
use project::{PathMatchCandidateSet, Project};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -28,7 +28,7 @@ impl FileSlashCommand {
|
||||
&self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &View<Workspace>,
|
||||
workspace: &Model<Workspace>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
if query.is_empty() {
|
||||
@@ -134,8 +134,9 @@ impl SlashCommand for FileSlashCommand {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
_: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
@@ -187,9 +188,10 @@ impl SlashCommand for FileSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
_: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
|
||||
@@ -7,7 +7,7 @@ use assistant_slash_command::{
|
||||
SlashCommandResult,
|
||||
};
|
||||
use chrono::Local;
|
||||
use gpui::{Task, WeakView};
|
||||
use gpui::{Task, WeakModel};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
@@ -35,8 +35,9 @@ impl SlashCommand for NowSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
@@ -46,9 +47,10 @@ impl SlashCommand for NowSlashCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_cx: &mut WindowContext,
|
||||
_window: &mut Window,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let now = Local::now();
|
||||
let text = format!("Today is {now}.", now = now.to_rfc2822());
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::PromptBuilder;
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection, SlashCommandResult};
|
||||
use feature_flags::FeatureFlag;
|
||||
use gpui::{AppContext, Task, WeakView, WindowContext};
|
||||
use gpui::{AppContext, Task, WeakModel, Window};
|
||||
use language::{Anchor, CodeLabel, LspAdapterDelegate};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelTool};
|
||||
use schemars::JsonSchema;
|
||||
@@ -67,8 +67,9 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
@@ -78,9 +79,10 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<Anchor>],
|
||||
context_buffer: language::BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let current_model = model_registry.active_model();
|
||||
@@ -97,7 +99,7 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
return Task::ready(Err(anyhow::anyhow!("no project indexer")));
|
||||
};
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
window.spawn(cx, |mut cx| async move {
|
||||
let current_model = current_model.ok_or_else(|| anyhow!("no model selected"))?;
|
||||
|
||||
let prompt =
|
||||
|
||||
@@ -4,7 +4,7 @@ use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
};
|
||||
use gpui::{Task, WeakView};
|
||||
use gpui::{Task, WeakModel};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use ui::prelude::*;
|
||||
@@ -37,8 +37,9 @@ impl SlashCommand for PromptSlashCommand {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let store = PromptStore::global(cx);
|
||||
let query = arguments.to_owned().join(" ");
|
||||
@@ -64,9 +65,10 @@ impl SlashCommand for PromptSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
_: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let title = arguments.to_owned().join(" ");
|
||||
if title.trim().is_empty() {
|
||||
|
||||
@@ -4,7 +4,7 @@ use assistant_slash_command::{
|
||||
SlashCommandResult,
|
||||
};
|
||||
use feature_flags::FeatureFlag;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use gpui::{AppContext, Task, WeakModel};
|
||||
use language::{CodeLabel, LspAdapterDelegate};
|
||||
use semantic_index::{LoadedSearchResult, SemanticDb};
|
||||
use std::{
|
||||
@@ -58,8 +58,9 @@ impl SlashCommand for SearchSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
@@ -69,9 +70,10 @@ impl SlashCommand for SearchSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: language::BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
||||
@@ -107,7 +109,7 @@ impl SlashCommand for SearchSlashCommand {
|
||||
return Task::ready(Err(anyhow::anyhow!("no project indexer")));
|
||||
};
|
||||
|
||||
cx.spawn(|cx| async move {
|
||||
window.spawn(cx, |cx| async move {
|
||||
let results = project_index
|
||||
.read_with(&cx, |project_index, cx| {
|
||||
project_index.search(vec![query.clone()], limit.unwrap_or(5), cx)
|
||||
|
||||
@@ -5,11 +5,11 @@ use assistant_slash_command::{
|
||||
SlashCommandOutputSection, SlashCommandResult,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use gpui::{AppContext, Task, WeakModel};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use ui::{IconName, SharedString, WindowContext};
|
||||
use ui::{IconName, SharedString, Window};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct SelectionCommand;
|
||||
@@ -47,8 +47,9 @@ impl SlashCommand for SelectionCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
@@ -58,9 +59,10 @@ impl SlashCommand for SelectionCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
_: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let mut events = vec![];
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use assistant_slash_command::{
|
||||
};
|
||||
use feature_flags::FeatureFlag;
|
||||
use futures::channel::mpsc;
|
||||
use gpui::{Task, WeakView};
|
||||
use gpui::{Task, WeakModel};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use smol::stream::StreamExt;
|
||||
use smol::Timer;
|
||||
@@ -45,8 +45,9 @@ impl SlashCommand for StreamingExampleSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
@@ -56,9 +57,10 @@ impl SlashCommand for StreamingExampleSlashCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
_: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let (events_tx, events_rx) = mpsc::unbounded();
|
||||
cx.background_executor()
|
||||
|
||||
@@ -4,11 +4,11 @@ use assistant_slash_command::{
|
||||
SlashCommandResult,
|
||||
};
|
||||
use editor::Editor;
|
||||
use gpui::{Task, WeakView};
|
||||
use gpui::{Task, WeakModel};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use std::sync::Arc;
|
||||
use std::{path::Path, sync::atomic::AtomicBool};
|
||||
use ui::{IconName, WindowContext};
|
||||
use ui::{AppContext, IconName, Window};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct OutlineSlashCommand;
|
||||
@@ -34,8 +34,9 @@ impl SlashCommand for OutlineSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
@@ -49,9 +50,10 @@ impl SlashCommand for OutlineSlashCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
_: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let output = workspace.update(cx, |workspace, cx| {
|
||||
let Some(active_item) = workspace.active_item(cx) else {
|
||||
|
||||
@@ -6,13 +6,13 @@ use assistant_slash_command::{
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::Editor;
|
||||
use futures::future::join_all;
|
||||
use gpui::{Entity, Task, WeakView};
|
||||
use gpui::{Entity, Task, WeakModel};
|
||||
use language::{BufferSnapshot, CodeLabel, HighlightId, LspAdapterDelegate};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::{prelude::*, ActiveTheme, WindowContext};
|
||||
use ui::{prelude::*, ActiveTheme, AppContext, Window};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -51,8 +51,9 @@ impl SlashCommand for TabSlashCommand {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
cancel: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let mut has_all_tabs_completion_item = false;
|
||||
let argument_set = arguments
|
||||
@@ -82,10 +83,10 @@ impl SlashCommand for TabSlashCommand {
|
||||
});
|
||||
let current_query = arguments.last().cloned().unwrap_or_default();
|
||||
let tab_items_search =
|
||||
tab_items_for_queries(workspace, &[current_query], cancel, false, cx);
|
||||
tab_items_for_queries(workspace, &[current_query], cancel, false, window, cx);
|
||||
|
||||
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
|
||||
cx.spawn(|_| async move {
|
||||
window.spawn(cx, |_| async move {
|
||||
let tab_items = tab_items_search.await?;
|
||||
let run_command = tab_items.len() == 1;
|
||||
let tab_completion_items = tab_items.into_iter().filter_map(|(path, ..)| {
|
||||
@@ -137,15 +138,17 @@ impl SlashCommand for TabSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let tab_items_search = tab_items_for_queries(
|
||||
Some(workspace),
|
||||
arguments,
|
||||
Arc::new(AtomicBool::new(false)),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -160,15 +163,16 @@ impl SlashCommand for TabSlashCommand {
|
||||
}
|
||||
|
||||
fn tab_items_for_queries(
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
queries: &[String],
|
||||
cancel: Arc<AtomicBool>,
|
||||
strict_match: bool,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<anyhow::Result<Vec<(Option<PathBuf>, BufferSnapshot, usize)>>> {
|
||||
let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty());
|
||||
let queries = queries.to_owned();
|
||||
cx.spawn(|mut cx| async move {
|
||||
window.spawn(cx, |mut cx| async move {
|
||||
let mut open_buffers =
|
||||
workspace
|
||||
.context("no workspace")?
|
||||
@@ -281,7 +285,7 @@ fn tab_items_for_queries(
|
||||
|
||||
fn active_item_buffer(
|
||||
workspace: &mut Workspace,
|
||||
cx: &mut ui::ViewContext<Workspace>,
|
||||
cx: &mut ModelContext<Workspace>,
|
||||
) -> anyhow::Result<BufferSnapshot> {
|
||||
let active_editor = workspace
|
||||
.active_item(cx)
|
||||
|
||||
@@ -6,7 +6,7 @@ use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
};
|
||||
use gpui::{AppContext, Task, View, WeakView};
|
||||
use gpui::{AppContext, Model, Task, WeakModel};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
||||
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
|
||||
use ui::prelude::*;
|
||||
@@ -53,8 +53,9 @@ impl SlashCommand for TerminalSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
@@ -64,9 +65,10 @@ impl SlashCommand for TerminalSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
_: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
||||
@@ -107,9 +109,9 @@ impl SlashCommand for TerminalSlashCommand {
|
||||
}
|
||||
|
||||
fn resolve_active_terminal(
|
||||
workspace: &View<Workspace>,
|
||||
cx: &WindowContext,
|
||||
) -> Option<View<TerminalView>> {
|
||||
workspace: &Model<Workspace>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<Model<TerminalView>> {
|
||||
if let Some(terminal_view) = workspace
|
||||
.read(cx)
|
||||
.active_item(cx)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakView};
|
||||
use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakModel};
|
||||
use picker::{Picker, PickerDelegate, PickerEditorPosition};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::SlashCommandWorkingSet;
|
||||
#[derive(IntoElement)]
|
||||
pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
|
||||
working_set: Arc<SlashCommandWorkingSet>,
|
||||
active_context_editor: WeakView<ContextEditor>,
|
||||
active_context_editor: WeakModel<ContextEditor>,
|
||||
trigger: T,
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ enum SlashCommandEntry {
|
||||
Info(SlashCommandInfo),
|
||||
Advert {
|
||||
name: SharedString,
|
||||
renderer: fn(&mut WindowContext<'_>) -> AnyElement,
|
||||
on_confirm: fn(&mut WindowContext<'_>),
|
||||
renderer: fn(&mut Window, &mut AppContext) -> AnyElement,
|
||||
on_confirm: fn(&mut Window, &mut AppContext),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -44,14 +44,14 @@ impl AsRef<str> for SlashCommandEntry {
|
||||
pub(crate) struct SlashCommandDelegate {
|
||||
all_commands: Vec<SlashCommandEntry>,
|
||||
filtered_commands: Vec<SlashCommandEntry>,
|
||||
active_context_editor: WeakView<ContextEditor>,
|
||||
active_context_editor: WeakModel<ContextEditor>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> SlashCommandSelector<T> {
|
||||
pub(crate) fn new(
|
||||
working_set: Arc<SlashCommandWorkingSet>,
|
||||
active_context_editor: WeakView<ContextEditor>,
|
||||
active_context_editor: WeakModel<ContextEditor>,
|
||||
trigger: T,
|
||||
) -> Self {
|
||||
SlashCommandSelector {
|
||||
@@ -73,18 +73,28 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_: &mut Window,
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) {
|
||||
self.selected_index = ix.min(self.filtered_commands.len().saturating_sub(1));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut AppContext) -> Arc<str> {
|
||||
"Select a command...".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let all_commands = self.all_commands.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.spawn_in(window, |this, mut cx| async move {
|
||||
let filtered_commands = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
@@ -104,9 +114,9 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update_in(&mut cx, |this, window, cx| {
|
||||
this.delegate.filtered_commands = filtered_commands;
|
||||
this.delegate.set_selected_index(0, cx);
|
||||
this.delegate.set_selected_index(0, window, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
@@ -139,25 +149,30 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
ret
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn confirm(
|
||||
&mut self,
|
||||
_secondary: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) {
|
||||
if let Some(command) = self.filtered_commands.get(self.selected_index) {
|
||||
match command {
|
||||
SlashCommandEntry::Info(info) => {
|
||||
self.active_context_editor
|
||||
.update(cx, |context_editor, cx| {
|
||||
context_editor.insert_command(&info.name, cx)
|
||||
context_editor.insert_command(&info.name, window, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
SlashCommandEntry::Advert { on_confirm, .. } => {
|
||||
on_confirm(cx);
|
||||
on_confirm(window, cx);
|
||||
}
|
||||
}
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
fn dismissed(&mut self, _window: &mut Window, _cx: &mut ModelContext<Picker<Self>>) {}
|
||||
|
||||
fn editor_position(&self) -> PickerEditorPosition {
|
||||
PickerEditorPosition::End
|
||||
@@ -167,7 +182,8 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let command_info = self.filtered_commands.get(ix)?;
|
||||
|
||||
@@ -179,7 +195,7 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
.toggle_state(selected)
|
||||
.tooltip({
|
||||
let description = info.description.clone();
|
||||
move |cx| cx.new_view(|_| Tooltip::new(description.clone())).into()
|
||||
move |_, cx| cx.new_model(|_| Tooltip::new(description.clone())).into()
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
@@ -229,14 +245,14 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Dense)
|
||||
.toggle_state(selected)
|
||||
.child(renderer(cx)),
|
||||
.child(renderer(window, cx)),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
fn render(self, window: &mut Window, cx: &mut AppContext) -> impl IntoElement {
|
||||
let all_models = self
|
||||
.working_set
|
||||
.featured_command_names(cx)
|
||||
@@ -259,7 +275,7 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
|
||||
})
|
||||
.chain([SlashCommandEntry::Advert {
|
||||
name: "create-your-command".into(),
|
||||
renderer: |cx| {
|
||||
renderer: |_, cx| {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.child(
|
||||
@@ -293,7 +309,7 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
|
||||
)
|
||||
.into_any_element()
|
||||
},
|
||||
on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
|
||||
on_confirm: |_, cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
|
||||
}])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -304,8 +320,9 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
|
||||
selected_index: 0,
|
||||
};
|
||||
|
||||
let picker_view = cx.new_view(|cx| {
|
||||
let picker = Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into()));
|
||||
let picker_view = cx.new_model(|cx| {
|
||||
let picker =
|
||||
Picker::uniform_list(delegate, window, cx).max_height(Some(rems(20.).into()));
|
||||
picker
|
||||
});
|
||||
|
||||
@@ -314,7 +331,7 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
|
||||
.update(cx, |this, _| this.slash_menu_handle.clone())
|
||||
.ok();
|
||||
PopoverMenu::new("model-switcher")
|
||||
.menu(move |_cx| Some(picker_view.clone()))
|
||||
.menu(move |_window, _cx| Some(picker_view.clone()))
|
||||
.trigger(self.trigger)
|
||||
.attach(gpui::Corner::TopLeft)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
|
||||
@@ -13,8 +13,8 @@ use editor::{
|
||||
use fs::Fs;
|
||||
use futures::{channel::mpsc, SinkExt, StreamExt};
|
||||
use gpui::{
|
||||
AppContext, Context, EventEmitter, FocusHandle, FocusableView, Global, Model, ModelContext,
|
||||
Subscription, Task, TextStyle, UpdateGlobal, View, WeakView,
|
||||
AppContext, Context, EventEmitter, FocusHandle, Focusable, Global, Model, ModelContext,
|
||||
Subscription, Task, TextStyle, UpdateGlobal, WeakModel,
|
||||
};
|
||||
use language::Buffer;
|
||||
use language_model::{
|
||||
@@ -87,11 +87,12 @@ impl TerminalInlineAssistant {
|
||||
|
||||
pub fn assist(
|
||||
&mut self,
|
||||
terminal_view: &View<TerminalView>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
assistant_panel: Option<&View<AssistantPanel>>,
|
||||
terminal_view: &Model<TerminalView>,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
assistant_panel: Option<&Model<AssistantPanel>>,
|
||||
initial_prompt: Option<String>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let terminal = terminal_view.read(cx).terminal().clone();
|
||||
let assist_id = self.next_assist_id.post_inc();
|
||||
@@ -100,7 +101,7 @@ impl TerminalInlineAssistant {
|
||||
let prompt_buffer = cx.new_model(|cx| MultiBuffer::singleton(prompt_buffer, cx));
|
||||
let codegen = cx.new_model(|_| Codegen::new(terminal, self.telemetry.clone()));
|
||||
|
||||
let prompt_editor = cx.new_view(|cx| {
|
||||
let prompt_editor = cx.new_model(|cx| {
|
||||
PromptEditor::new(
|
||||
assist_id,
|
||||
self.prompt_history.clone(),
|
||||
@@ -109,6 +110,7 @@ impl TerminalInlineAssistant {
|
||||
assistant_panel,
|
||||
workspace.clone(),
|
||||
self.fs.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -118,7 +120,7 @@ impl TerminalInlineAssistant {
|
||||
render: Box::new(move |_| prompt_editor_render.clone().into_any_element()),
|
||||
};
|
||||
terminal_view.update(cx, |terminal_view, cx| {
|
||||
terminal_view.set_block_below_cursor(block, cx);
|
||||
terminal_view.set_block_below_cursor(block, window, cx);
|
||||
});
|
||||
|
||||
let terminal_assistant = TerminalInlineAssist::new(
|
||||
@@ -127,21 +129,27 @@ impl TerminalInlineAssistant {
|
||||
assistant_panel.is_some(),
|
||||
prompt_editor,
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
self.assists.insert(assist_id, terminal_assistant);
|
||||
|
||||
self.focus_assist(assist_id, cx);
|
||||
self.focus_assist(assist_id, window, cx);
|
||||
}
|
||||
|
||||
fn focus_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) {
|
||||
fn focus_assist(
|
||||
&mut self,
|
||||
assist_id: TerminalInlineAssistId,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let assist = &self.assists[&assist_id];
|
||||
if let Some(prompt_editor) = assist.prompt_editor.as_ref() {
|
||||
prompt_editor.update(cx, |this, cx| {
|
||||
this.editor.update(cx, |editor, cx| {
|
||||
editor.focus(cx);
|
||||
editor.select_all(&SelectAll, cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.select_all(&SelectAll, window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -149,9 +157,10 @@ impl TerminalInlineAssistant {
|
||||
|
||||
fn handle_prompt_editor_event(
|
||||
&mut self,
|
||||
prompt_editor: View<PromptEditor>,
|
||||
prompt_editor: Model<PromptEditor>,
|
||||
event: &PromptEditorEvent,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let assist_id = prompt_editor.read(cx).id;
|
||||
match event {
|
||||
@@ -162,21 +171,21 @@ impl TerminalInlineAssistant {
|
||||
self.stop_assist(assist_id, cx);
|
||||
}
|
||||
PromptEditorEvent::ConfirmRequested { execute } => {
|
||||
self.finish_assist(assist_id, false, *execute, cx);
|
||||
self.finish_assist(assist_id, false, *execute, window, cx);
|
||||
}
|
||||
PromptEditorEvent::CancelRequested => {
|
||||
self.finish_assist(assist_id, true, false, cx);
|
||||
self.finish_assist(assist_id, true, false, window, cx);
|
||||
}
|
||||
PromptEditorEvent::DismissRequested => {
|
||||
self.dismiss_assist(assist_id, cx);
|
||||
self.dismiss_assist(assist_id, window, cx);
|
||||
}
|
||||
PromptEditorEvent::Resized { height_in_lines } => {
|
||||
self.insert_prompt_editor_into_terminal(assist_id, *height_in_lines, cx);
|
||||
self.insert_prompt_editor_into_terminal(assist_id, *height_in_lines, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) {
|
||||
fn start_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut AppContext) {
|
||||
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
|
||||
assist
|
||||
} else {
|
||||
@@ -214,7 +223,7 @@ impl TerminalInlineAssistant {
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
}
|
||||
|
||||
fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) {
|
||||
fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut AppContext) {
|
||||
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
|
||||
assist
|
||||
} else {
|
||||
@@ -227,7 +236,7 @@ impl TerminalInlineAssistant {
|
||||
fn request_for_inline_assist(
|
||||
&self,
|
||||
assist_id: TerminalInlineAssistId,
|
||||
cx: &mut WindowContext,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<LanguageModelRequest> {
|
||||
let assist = self.assists.get(&assist_id).context("invalid assist")?;
|
||||
|
||||
@@ -297,16 +306,17 @@ impl TerminalInlineAssistant {
|
||||
assist_id: TerminalInlineAssistId,
|
||||
undo: bool,
|
||||
execute: bool,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.dismiss_assist(assist_id, cx);
|
||||
self.dismiss_assist(assist_id, window, cx);
|
||||
|
||||
if let Some(assist) = self.assists.remove(&assist_id) {
|
||||
assist
|
||||
.terminal
|
||||
.update(cx, |this, cx| {
|
||||
this.clear_block_below_cursor(cx);
|
||||
this.focus_handle(cx).focus(cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
@@ -349,7 +359,8 @@ impl TerminalInlineAssistant {
|
||||
fn dismiss_assist(
|
||||
&mut self,
|
||||
assist_id: TerminalInlineAssistId,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> bool {
|
||||
let Some(assist) = self.assists.get_mut(&assist_id) else {
|
||||
return false;
|
||||
@@ -362,7 +373,7 @@ impl TerminalInlineAssistant {
|
||||
.terminal
|
||||
.update(cx, |this, cx| {
|
||||
this.clear_block_below_cursor(cx);
|
||||
this.focus_handle(cx).focus(cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
})
|
||||
.is_ok()
|
||||
}
|
||||
@@ -371,7 +382,8 @@ impl TerminalInlineAssistant {
|
||||
&mut self,
|
||||
assist_id: TerminalInlineAssistId,
|
||||
height: u8,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if let Some(assist) = self.assists.get_mut(&assist_id) {
|
||||
if let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned() {
|
||||
@@ -383,7 +395,7 @@ impl TerminalInlineAssistant {
|
||||
height,
|
||||
render: Box::new(move |_| prompt_editor.clone().into_any_element()),
|
||||
};
|
||||
terminal.set_block_below_cursor(block, cx);
|
||||
terminal.set_block_below_cursor(block, window, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
@@ -392,10 +404,10 @@ impl TerminalInlineAssistant {
|
||||
}
|
||||
|
||||
struct TerminalInlineAssist {
|
||||
terminal: WeakView<TerminalView>,
|
||||
prompt_editor: Option<View<PromptEditor>>,
|
||||
terminal: WeakModel<TerminalView>,
|
||||
prompt_editor: Option<Model<PromptEditor>>,
|
||||
codegen: Model<Codegen>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
include_context: bool,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
@@ -403,11 +415,12 @@ struct TerminalInlineAssist {
|
||||
impl TerminalInlineAssist {
|
||||
pub fn new(
|
||||
assist_id: TerminalInlineAssistId,
|
||||
terminal: &View<TerminalView>,
|
||||
terminal: &Model<TerminalView>,
|
||||
include_context: bool,
|
||||
prompt_editor: View<PromptEditor>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
prompt_editor: Model<PromptEditor>,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Self {
|
||||
let codegen = prompt_editor.read(cx).codegen.clone();
|
||||
Self {
|
||||
@@ -417,12 +430,12 @@ impl TerminalInlineAssist {
|
||||
workspace: workspace.clone(),
|
||||
include_context,
|
||||
_subscriptions: vec![
|
||||
cx.subscribe(&prompt_editor, |prompt_editor, event, cx| {
|
||||
window.subscribe(&prompt_editor, cx, |prompt_editor, event, window, cx| {
|
||||
TerminalInlineAssistant::update_global(cx, |this, cx| {
|
||||
this.handle_prompt_editor_event(prompt_editor, event, cx)
|
||||
this.handle_prompt_editor_event(prompt_editor, event, window, cx)
|
||||
})
|
||||
}),
|
||||
cx.subscribe(&codegen, move |codegen, event, cx| {
|
||||
window.subscribe(&codegen, cx, move |codegen, event, window, cx| {
|
||||
TerminalInlineAssistant::update_global(cx, |this, cx| match event {
|
||||
CodegenEvent::Finished => {
|
||||
let assist = if let Some(assist) = this.assists.get(&assist_id) {
|
||||
@@ -455,7 +468,7 @@ impl TerminalInlineAssist {
|
||||
}
|
||||
|
||||
if assist.prompt_editor.is_none() {
|
||||
this.finish_assist(assist_id, false, false, cx);
|
||||
this.finish_assist(assist_id, false, false, window, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -477,8 +490,8 @@ enum PromptEditorEvent {
|
||||
struct PromptEditor {
|
||||
id: TerminalInlineAssistId,
|
||||
height_in_lines: u8,
|
||||
editor: View<Editor>,
|
||||
language_model_selector: View<LanguageModelSelector>,
|
||||
editor: Model<Editor>,
|
||||
language_model_selector: Model<LanguageModelSelector>,
|
||||
edited_since_done: bool,
|
||||
prompt_history: VecDeque<String>,
|
||||
prompt_history_ix: Option<usize>,
|
||||
@@ -489,13 +502,13 @@ struct PromptEditor {
|
||||
pending_token_count: Task<Result<()>>,
|
||||
token_count: Option<usize>,
|
||||
_token_count_subscriptions: Vec<Subscription>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
}
|
||||
|
||||
impl EventEmitter<PromptEditorEvent> for PromptEditor {}
|
||||
|
||||
impl Render for PromptEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
|
||||
let status = &self.codegen.read(cx).status;
|
||||
let buttons = match status {
|
||||
CodegenStatus::Idle => {
|
||||
@@ -503,16 +516,20 @@ impl Render for PromptEditor {
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
|
||||
.tooltip(|window, cx| {
|
||||
Tooltip::for_action("Cancel Assist", &menu::Cancel, window, cx)
|
||||
})
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
|
||||
cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
|
||||
),
|
||||
IconButton::new("start", IconName::SparkleAlt)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::for_action("Generate", &menu::Confirm, cx))
|
||||
.tooltip(|window, cx| {
|
||||
Tooltip::for_action("Generate", &menu::Confirm, window, cx)
|
||||
})
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
|
||||
cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
|
||||
),
|
||||
]
|
||||
}
|
||||
@@ -521,23 +538,24 @@ impl Render for PromptEditor {
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::text("Cancel Assist", cx))
|
||||
.tooltip(Tooltip::text("Cancel Assist"))
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
|
||||
cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
|
||||
),
|
||||
IconButton::new("stop", IconName::Stop)
|
||||
.icon_color(Color::Error)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
.tooltip(|window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Interrupt Generation",
|
||||
Some(&menu::Cancel),
|
||||
"Changes won't be discarded",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested)),
|
||||
cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)),
|
||||
),
|
||||
]
|
||||
}
|
||||
@@ -545,8 +563,12 @@ impl Render for PromptEditor {
|
||||
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)));
|
||||
.tooltip(|window, cx| {
|
||||
Tooltip::for_action("Cancel Assist", &menu::Cancel, window, cx)
|
||||
})
|
||||
.on_click(
|
||||
cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
|
||||
);
|
||||
|
||||
let has_error = matches!(status, CodegenStatus::Error(_));
|
||||
if has_error || self.edited_since_done {
|
||||
@@ -555,15 +577,16 @@ impl Render for PromptEditor {
|
||||
IconButton::new("restart", IconName::RotateCw)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
.tooltip(|window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Restart Generation",
|
||||
Some(&menu::Confirm),
|
||||
"Changes will be discarded",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
.on_click(cx.listener(|_, _, _, cx| {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
})),
|
||||
]
|
||||
@@ -573,23 +596,29 @@ impl Render for PromptEditor {
|
||||
IconButton::new("accept", IconName::Check)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::for_action("Accept Generated Command", &menu::Confirm, cx)
|
||||
.tooltip(|window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Accept Generated Command",
|
||||
&menu::Confirm,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
.on_click(cx.listener(|_, _, _, cx| {
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
|
||||
})),
|
||||
IconButton::new("confirm", IconName::Play)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
.tooltip(|window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Execute Generated Command",
|
||||
&menu::SecondaryConfirm,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
.on_click(cx.listener(|_, _, _, cx| {
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
|
||||
})),
|
||||
]
|
||||
@@ -620,7 +649,7 @@ impl Render for PromptEditor {
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"Using {}",
|
||||
@@ -631,6 +660,7 @@ impl Render for PromptEditor {
|
||||
),
|
||||
None,
|
||||
"Change Model",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
@@ -641,7 +671,7 @@ impl Render for PromptEditor {
|
||||
Some(
|
||||
div()
|
||||
.id("error")
|
||||
.tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
|
||||
.tooltip(Tooltip::text(error_message))
|
||||
.child(
|
||||
Icon::new(IconName::XCircle)
|
||||
.size(IconSize::Small)
|
||||
@@ -664,7 +694,7 @@ impl Render for PromptEditor {
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for PromptEditor {
|
||||
impl Focusable for PromptEditor {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.editor.focus_handle(cx)
|
||||
}
|
||||
@@ -679,12 +709,13 @@ impl PromptEditor {
|
||||
prompt_history: VecDeque<String>,
|
||||
prompt_buffer: Model<MultiBuffer>,
|
||||
codegen: Model<Codegen>,
|
||||
assistant_panel: Option<&View<AssistantPanel>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
assistant_panel: Option<&Model<AssistantPanel>>,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let prompt_editor = cx.new_view(|cx| {
|
||||
let prompt_editor = cx.new_model(|cx| {
|
||||
let mut editor = Editor::new(
|
||||
EditorMode::AutoHeight {
|
||||
max_lines: Self::MAX_LINES as usize,
|
||||
@@ -692,24 +723,28 @@ impl PromptEditor {
|
||||
prompt_buffer,
|
||||
None,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
|
||||
editor.set_placeholder_text(Self::placeholder_text(cx), cx);
|
||||
editor.set_placeholder_text(Self::placeholder_text(window), cx);
|
||||
editor
|
||||
});
|
||||
|
||||
let mut token_count_subscriptions = Vec::new();
|
||||
if let Some(assistant_panel) = assistant_panel {
|
||||
token_count_subscriptions
|
||||
.push(cx.subscribe(assistant_panel, Self::handle_assistant_panel_event));
|
||||
token_count_subscriptions.push(cx.subscribe_in(
|
||||
assistant_panel,
|
||||
window,
|
||||
Self::handle_assistant_panel_event,
|
||||
));
|
||||
}
|
||||
|
||||
let mut this = Self {
|
||||
id,
|
||||
height_in_lines: 1,
|
||||
editor: prompt_editor,
|
||||
language_model_selector: cx.new_view(|cx| {
|
||||
language_model_selector: cx.new_model(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
@@ -719,6 +754,7 @@ impl PromptEditor {
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
@@ -726,7 +762,7 @@ impl PromptEditor {
|
||||
prompt_history,
|
||||
prompt_history_ix: None,
|
||||
pending_prompt: String::new(),
|
||||
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
|
||||
_codegen_subscription: cx.observe_in(&codegen, window, Self::handle_codegen_changed),
|
||||
editor_subscriptions: Vec::new(),
|
||||
codegen,
|
||||
pending_token_count: Task::ready(Ok(())),
|
||||
@@ -740,15 +776,15 @@ impl PromptEditor {
|
||||
this
|
||||
}
|
||||
|
||||
fn placeholder_text(cx: &WindowContext) -> String {
|
||||
let context_keybinding = text_for_action(&crate::ToggleFocus, cx)
|
||||
fn placeholder_text(window: &mut Window) -> String {
|
||||
let context_keybinding = text_for_action(&crate::ToggleFocus, window)
|
||||
.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>) {
|
||||
fn subscribe_to_editor(&mut self, cx: &mut ModelContext<Self>) {
|
||||
self.editor_subscriptions.clear();
|
||||
self.editor_subscriptions
|
||||
.push(cx.observe(&self.editor, Self::handle_prompt_editor_changed));
|
||||
@@ -760,7 +796,7 @@ impl PromptEditor {
|
||||
self.editor.read(cx).text(cx)
|
||||
}
|
||||
|
||||
fn count_lines(&mut self, cx: &mut ViewContext<Self>) {
|
||||
fn count_lines(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let height_in_lines = cmp::max(
|
||||
2, // Make the editor at least two lines tall, to account for padding and buttons.
|
||||
cmp::min(
|
||||
@@ -778,15 +814,16 @@ impl PromptEditor {
|
||||
|
||||
fn handle_assistant_panel_event(
|
||||
&mut self,
|
||||
_: View<AssistantPanel>,
|
||||
_: &Model<AssistantPanel>,
|
||||
event: &AssistantPanelEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
_: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let AssistantPanelEvent::ContextEdited { .. } = event;
|
||||
self.count_tokens(cx);
|
||||
}
|
||||
|
||||
fn count_tokens(&mut self, cx: &mut ViewContext<Self>) {
|
||||
fn count_tokens(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let assist_id = self.id;
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
|
||||
return;
|
||||
@@ -806,15 +843,15 @@ impl PromptEditor {
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_prompt_editor_changed(&mut self, _: View<Editor>, cx: &mut ViewContext<Self>) {
|
||||
fn handle_prompt_editor_changed(&mut self, _: Model<Editor>, cx: &mut ModelContext<Self>) {
|
||||
self.count_lines(cx);
|
||||
}
|
||||
|
||||
fn handle_prompt_editor_events(
|
||||
&mut self,
|
||||
_: View<Editor>,
|
||||
_: Model<Editor>,
|
||||
event: &EditorEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
EditorEvent::Edited { .. } => {
|
||||
@@ -837,7 +874,12 @@ impl PromptEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
|
||||
fn handle_codegen_changed(
|
||||
&mut self,
|
||||
_: Model<Codegen>,
|
||||
_: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
match &self.codegen.read(cx).status {
|
||||
CodegenStatus::Idle => {
|
||||
self.editor
|
||||
@@ -855,7 +897,7 @@ impl PromptEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
|
||||
fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut ModelContext<Self>) {
|
||||
match &self.codegen.read(cx).status {
|
||||
CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
|
||||
cx.emit(PromptEditorEvent::CancelRequested);
|
||||
@@ -866,7 +908,7 @@ impl PromptEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||
fn confirm(&mut self, _: &menu::Confirm, _: &mut Window, cx: &mut ModelContext<Self>) {
|
||||
match &self.codegen.read(cx).status {
|
||||
CodegenStatus::Idle => {
|
||||
if !self.editor.read(cx).text(cx).trim().is_empty() {
|
||||
@@ -889,53 +931,58 @@ impl PromptEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
|
||||
fn secondary_confirm(
|
||||
&mut self,
|
||||
_: &menu::SecondaryConfirm,
|
||||
_: &mut Window,
|
||||
cx: &mut ModelContext<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>) {
|
||||
fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut ModelContext<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);
|
||||
editor.set_text(prompt, window, cx);
|
||||
editor.move_to_beginning(&Default::default(), window, 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);
|
||||
editor.set_text(prompt, window, cx);
|
||||
editor.move_to_beginning(&Default::default(), window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
|
||||
fn move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut ModelContext<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)
|
||||
editor.set_text(prompt, window, cx);
|
||||
editor.move_to_end(&Default::default(), window, 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)
|
||||
editor.set_text(prompt, window, cx);
|
||||
editor.move_to_end(&Default::default(), window, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_token_count(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
|
||||
fn render_token_count(&self, cx: &mut ModelContext<Self>) -> Option<impl IntoElement> {
|
||||
let model = LanguageModelRegistry::read_global(cx).active_model()?;
|
||||
let token_count = self.token_count?;
|
||||
let max_token_count = model.max_token_count();
|
||||
@@ -965,34 +1012,35 @@ impl PromptEditor {
|
||||
);
|
||||
if let Some(workspace) = self.workspace.clone() {
|
||||
token_count = token_count
|
||||
.tooltip(|cx| {
|
||||
.tooltip(|window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Tokens Used by Inline Assistant",
|
||||
None,
|
||||
"Click to Open Assistant Panel",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.cursor_pointer()
|
||||
.on_mouse_down(gpui::MouseButton::Left, |_, cx| cx.stop_propagation())
|
||||
.on_click(move |_, cx| {
|
||||
.on_mouse_down(gpui::MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.on_click(move |_, window, cx| {
|
||||
cx.stop_propagation();
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.focus_panel::<AssistantPanel>(cx)
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx)
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
} else {
|
||||
token_count = token_count
|
||||
.cursor_default()
|
||||
.tooltip(|cx| Tooltip::text("Tokens Used by Inline Assistant", cx));
|
||||
.tooltip(Tooltip::text("Tokens Used by Inline Assistant"));
|
||||
}
|
||||
|
||||
Some(token_count)
|
||||
}
|
||||
|
||||
fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render_prompt_editor(&self, cx: &mut ModelContext<Self>) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: if self.editor.read(cx).read_only(cx) {
|
||||
|
||||
@@ -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, DefiniteLength, EdgesRefinement, Empty, Length,
|
||||
ListAlignment, ListOffset, ListState, Model, StyleRefinement, Subscription,
|
||||
TextStyleRefinement, WeakModel,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::Role;
|
||||
@@ -18,13 +19,13 @@ use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent};
|
||||
use crate::ui::ContextPill;
|
||||
|
||||
pub struct ActiveThread {
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
thread: Model<Thread>,
|
||||
messages: Vec<MessageId>,
|
||||
list_state: ListState,
|
||||
rendered_messages_by_id: HashMap<MessageId, View<Markdown>>,
|
||||
rendered_messages_by_id: HashMap<MessageId, Model<Markdown>>,
|
||||
last_error: Option<ThreadError>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
@@ -32,14 +33,15 @@ pub struct ActiveThread {
|
||||
impl ActiveThread {
|
||||
pub fn new(
|
||||
thread: Model<Thread>,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let subscriptions = vec![
|
||||
cx.observe(&thread, |_, _, cx| cx.notify()),
|
||||
cx.subscribe(&thread, Self::handle_thread_event),
|
||||
cx.subscribe_in(&thread, window, Self::handle_thread_event),
|
||||
];
|
||||
|
||||
let mut this = Self {
|
||||
@@ -50,8 +52,8 @@ impl ActiveThread {
|
||||
messages: Vec::new(),
|
||||
rendered_messages_by_id: HashMap::default(),
|
||||
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
|
||||
let this = cx.view().downgrade();
|
||||
move |ix, cx: &mut WindowContext| {
|
||||
let this = cx.model().downgrade();
|
||||
move |ix, _: &mut Window, cx: &mut AppContext| {
|
||||
this.update(cx, |this, cx| this.render_message(ix, cx))
|
||||
.unwrap()
|
||||
}
|
||||
@@ -61,7 +63,7 @@ impl ActiveThread {
|
||||
};
|
||||
|
||||
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
|
||||
this.push_message(&message.id, message.text.clone(), cx);
|
||||
this.push_message(&message.id, message.text.clone(), window, cx);
|
||||
}
|
||||
|
||||
this
|
||||
@@ -83,16 +85,23 @@ impl ActiveThread {
|
||||
self.last_error.take();
|
||||
}
|
||||
|
||||
fn push_message(&mut self, id: &MessageId, text: String, cx: &mut ViewContext<Self>) {
|
||||
fn push_message(
|
||||
&mut self,
|
||||
id: &MessageId,
|
||||
text: String,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let old_len = self.messages.len();
|
||||
self.messages.push(*id);
|
||||
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 = window.text_style();
|
||||
|
||||
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 +114,26 @@ 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(1.0).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.3)),
|
||||
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.))),
|
||||
},
|
||||
text: Some(TextStyleRefinement {
|
||||
font_family: Some(theme_settings.buffer_font.family.clone()),
|
||||
font_size: Some(buffer_font_size.into()),
|
||||
@@ -114,30 +143,36 @@ 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()
|
||||
};
|
||||
|
||||
let markdown = cx.new_view(|cx| {
|
||||
let markdown = cx.new_model(|cx| {
|
||||
Markdown::new(
|
||||
text,
|
||||
markdown_style,
|
||||
Some(self.language_registry.clone()),
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.rendered_messages_by_id.insert(*id, markdown);
|
||||
self.list_state.scroll_to(ListOffset {
|
||||
item_ix: old_len,
|
||||
offset_in_item: Pixels(0.0),
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
&mut self,
|
||||
_: Model<Thread>,
|
||||
_: &Model<Thread>,
|
||||
event: &ThreadEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
ThreadEvent::ShowError(error) => {
|
||||
@@ -148,7 +183,7 @@ impl ActiveThread {
|
||||
ThreadEvent::StreamedAssistantText(message_id, text) => {
|
||||
if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) {
|
||||
markdown.update(cx, |markdown, cx| {
|
||||
markdown.append(text, cx);
|
||||
markdown.append(text, window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -159,7 +194,7 @@ impl ActiveThread {
|
||||
.message(*message_id)
|
||||
.map(|message| message.text.clone())
|
||||
{
|
||||
self.push_message(message_id, message_text, cx);
|
||||
self.push_message(message_id, message_text, window, cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
@@ -176,7 +211,7 @@ impl ActiveThread {
|
||||
|
||||
for tool_use in pending_tool_uses {
|
||||
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
|
||||
let task = tool.run(tool_use.input, self.workspace.clone(), cx);
|
||||
let task = tool.run(tool_use.input, self.workspace.clone(), window, cx);
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.insert_tool_output(
|
||||
@@ -193,7 +228,7 @@ impl ActiveThread {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
fn render_message(&self, ix: usize, cx: &mut ModelContext<Self>) -> AnyElement {
|
||||
let message_id = self.messages[ix];
|
||||
let Some(message) = self.thread.read(cx).message(message_id) else {
|
||||
return Empty.into_any();
|
||||
@@ -204,11 +239,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,36 +254,45 @@ 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(div().p_2p5().text_ui(cx).child(markdown.clone()))
|
||||
.when_some(context, |parent, context| {
|
||||
parent.child(
|
||||
h_flex().flex_wrap().gap_2().p_1p5().children(
|
||||
context
|
||||
.iter()
|
||||
.map(|context| ContextPill::new(context.clone())),
|
||||
),
|
||||
)
|
||||
if !context.is_empty() {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.flex_wrap()
|
||||
.gap_1()
|
||||
.px_1p5()
|
||||
.pb_1p5()
|
||||
.children(context.iter().map(|c| ContextPill::new(c.clone()))),
|
||||
)
|
||||
} else {
|
||||
parent
|
||||
}
|
||||
}),
|
||||
)
|
||||
.into_any()
|
||||
@@ -255,7 +300,7 @@ impl ActiveThread {
|
||||
}
|
||||
|
||||
impl Render for ActiveThread {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut ModelContext<Self>) -> impl IntoElement {
|
||||
list(self.list_state.clone()).flex_1().py_1()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
93
crates/assistant2/src/assistant_model_selector.rs
Normal file
93
crates/assistant2/src/assistant_model_selector.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use fs::Fs;
|
||||
use gpui::{Focusable, Model};
|
||||
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: Model<LanguageModelSelector>,
|
||||
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
}
|
||||
|
||||
impl AssistantModelSelector {
|
||||
pub(crate) fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Self {
|
||||
Self {
|
||||
selector: cx.new_model(|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()),
|
||||
);
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
menu_handle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AssistantModelSelector {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut ModelContext<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 |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Change Model",
|
||||
&ToggleModelSelector,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.with_handle(self.menu_handle.clone())
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,7 @@ use client::zed_urls;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter,
|
||||
FocusHandle, FocusableView, FontWeight, Model, Pixels, Task, View, ViewContext, WeakView,
|
||||
WindowContext,
|
||||
FocusHandle, Focusable, FontWeight, Model, ModelContext, Pixels, Task, WeakModel, Window,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use settings::Settings;
|
||||
@@ -25,11 +24,24 @@ use crate::thread_store::ThreadStore;
|
||||
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);
|
||||
});
|
||||
cx.observe_new_models(
|
||||
|workspace: &mut Workspace, _window, _cx: &mut ModelContext<Workspace>| {
|
||||
workspace
|
||||
.register_action(|workspace, _: &ToggleFocus, window, cx| {
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
|
||||
})
|
||||
.register_action(|workspace, _: &NewThread, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| panel.new_thread(window, cx));
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &OpenHistory, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| panel.open_history(window, cx));
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
@@ -41,25 +53,25 @@ enum ActiveView {
|
||||
}
|
||||
|
||||
pub struct AssistantPanel {
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
fs: Arc<dyn Fs>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
thread_store: Model<ThreadStore>,
|
||||
thread: View<ActiveThread>,
|
||||
message_editor: View<MessageEditor>,
|
||||
thread: Model<ActiveThread>,
|
||||
message_editor: Model<MessageEditor>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
local_timezone: UtcOffset,
|
||||
active_view: ActiveView,
|
||||
history: View<ThreadHistory>,
|
||||
history: Model<ThreadHistory>,
|
||||
width: Option<Pixels>,
|
||||
height: Option<Pixels>,
|
||||
}
|
||||
|
||||
impl AssistantPanel {
|
||||
pub fn load(
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
) -> Task<Result<Model<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let tools = Arc::new(ToolWorkingSet::default());
|
||||
let thread_store = workspace
|
||||
@@ -69,8 +81,8 @@ impl AssistantPanel {
|
||||
})?
|
||||
.await?;
|
||||
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
cx.new_view(|cx| Self::new(workspace, thread_store, tools, cx))
|
||||
workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||
cx.new_model(|cx| Self::new(workspace, thread_store, tools, window, cx))
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -79,13 +91,14 @@ impl AssistantPanel {
|
||||
workspace: &Workspace,
|
||||
thread_store: Model<ThreadStore>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let language_registry = workspace.project().read(cx).languages().clone();
|
||||
let workspace = workspace.weak_handle();
|
||||
let weak_self = cx.view().downgrade();
|
||||
let weak_self = cx.model().downgrade();
|
||||
|
||||
Self {
|
||||
active_view: ActiveView::Thread,
|
||||
@@ -93,21 +106,23 @@ impl AssistantPanel {
|
||||
fs: fs.clone(),
|
||||
language_registry: language_registry.clone(),
|
||||
thread_store: thread_store.clone(),
|
||||
thread: cx.new_view(|cx| {
|
||||
thread: cx.new_model(|cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
workspace.clone(),
|
||||
language_registry,
|
||||
tools.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
message_editor: cx.new_view(|cx| {
|
||||
message_editor: cx.new_model(|cx| {
|
||||
MessageEditor::new(
|
||||
fs.clone(),
|
||||
workspace,
|
||||
thread_store.downgrade(),
|
||||
thread.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
@@ -116,7 +131,7 @@ impl AssistantPanel {
|
||||
chrono::Local::now().offset().local_minus_utc(),
|
||||
)
|
||||
.unwrap(),
|
||||
history: cx.new_view(|cx| ThreadHistory::new(weak_self, thread_store, cx)),
|
||||
history: cx.new_model(|cx| ThreadHistory::new(weak_self, thread_store, cx)),
|
||||
width: None,
|
||||
height: None,
|
||||
}
|
||||
@@ -130,34 +145,47 @@ impl AssistantPanel {
|
||||
&self.thread_store
|
||||
}
|
||||
|
||||
fn new_thread(&mut self, cx: &mut ViewContext<Self>) {
|
||||
fn new_thread(&mut self, window: &mut Window, cx: &mut ModelContext<Self>) {
|
||||
let thread = self
|
||||
.thread_store
|
||||
.update(cx, |this, cx| this.create_thread(cx));
|
||||
|
||||
self.active_view = ActiveView::Thread;
|
||||
self.thread = cx.new_view(|cx| {
|
||||
self.thread = cx.new_model(|cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
self.workspace.clone(),
|
||||
self.language_registry.clone(),
|
||||
self.tools.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor = cx.new_view(|cx| {
|
||||
self.message_editor = cx.new_model(|cx| {
|
||||
MessageEditor::new(
|
||||
self.fs.clone(),
|
||||
self.workspace.clone(),
|
||||
self.thread_store.downgrade(),
|
||||
thread,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor.focus_handle(cx).focus(cx);
|
||||
self.message_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
pub(crate) fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
|
||||
fn open_history(&mut self, window: &mut Window, cx: &mut ModelContext<Self>) {
|
||||
self.active_view = ActiveView::History;
|
||||
self.history.focus_handle(cx).focus(window);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn open_thread(
|
||||
&mut self,
|
||||
thread_id: &ThreadId,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let Some(thread) = self
|
||||
.thread_store
|
||||
.update(cx, |this, cx| this.open_thread(thread_id, cx))
|
||||
@@ -166,34 +194,36 @@ impl AssistantPanel {
|
||||
};
|
||||
|
||||
self.active_view = ActiveView::Thread;
|
||||
self.thread = cx.new_view(|cx| {
|
||||
self.thread = cx.new_model(|cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
self.workspace.clone(),
|
||||
self.language_registry.clone(),
|
||||
self.tools.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor = cx.new_view(|cx| {
|
||||
self.message_editor = cx.new_model(|cx| {
|
||||
MessageEditor::new(
|
||||
self.fs.clone(),
|
||||
self.workspace.clone(),
|
||||
self.thread_store.downgrade(),
|
||||
thread,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor.focus_handle(cx).focus(cx);
|
||||
self.message_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
|
||||
pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut ModelContext<Self>) {
|
||||
self.thread_store
|
||||
.update(cx, |this, cx| this.delete_thread(thread_id, cx));
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for AssistantPanel {
|
||||
impl Focusable for AssistantPanel {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
match self.active_view {
|
||||
ActiveView::Thread => self.message_editor.focus_handle(cx),
|
||||
@@ -209,7 +239,7 @@ impl Panel for AssistantPanel {
|
||||
"AssistantPanel2"
|
||||
}
|
||||
|
||||
fn position(&self, _cx: &WindowContext) -> DockPosition {
|
||||
fn position(&self, _window: &Window, _cx: &AppContext) -> DockPosition {
|
||||
DockPosition::Right
|
||||
}
|
||||
|
||||
@@ -217,7 +247,12 @@ impl Panel for AssistantPanel {
|
||||
true
|
||||
}
|
||||
|
||||
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
|
||||
fn set_position(
|
||||
&mut self,
|
||||
position: DockPosition,
|
||||
_: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
settings::update_settings_file::<AssistantSettings>(
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
@@ -232,9 +267,9 @@ impl Panel for AssistantPanel {
|
||||
);
|
||||
}
|
||||
|
||||
fn size(&self, cx: &WindowContext) -> Pixels {
|
||||
fn size(&self, window: &Window, cx: &AppContext) -> Pixels {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
match self.position(cx) {
|
||||
match self.position(window, cx) {
|
||||
DockPosition::Left | DockPosition::Right => {
|
||||
self.width.unwrap_or(settings.default_width)
|
||||
}
|
||||
@@ -242,35 +277,39 @@ impl Panel for AssistantPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
|
||||
match self.position(cx) {
|
||||
fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut ModelContext<Self>) {
|
||||
match self.position(window, cx) {
|
||||
DockPosition::Left | DockPosition::Right => self.width = size,
|
||||
DockPosition::Bottom => self.height = size,
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
|
||||
fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut ModelContext<Self>) {}
|
||||
|
||||
fn remote_id() -> Option<proto::PanelId> {
|
||||
Some(proto::PanelId::AssistantPanel)
|
||||
}
|
||||
|
||||
fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
|
||||
Some(IconName::ZedAssistant)
|
||||
fn icon(&self, _window: &Window, _cx: &AppContext) -> Option<IconName> {
|
||||
Some(IconName::ZedAssistant2)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||
fn icon_tooltip(&self, _window: &Window, _cx: &AppContext) -> Option<&'static str> {
|
||||
Some("Assistant Panel")
|
||||
}
|
||||
|
||||
fn toggle_action(&self) -> Box<dyn Action> {
|
||||
Box::new(ToggleFocus)
|
||||
}
|
||||
|
||||
fn activation_priority(&self) -> u32 {
|
||||
3
|
||||
}
|
||||
}
|
||||
|
||||
impl AssistantPanel {
|
||||
fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render_toolbar(&self, cx: &mut ModelContext<Self>) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
h_flex()
|
||||
@@ -296,17 +335,18 @@ impl AssistantPanel {
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |cx| {
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"New Thread",
|
||||
&NewThread,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(move |_event, cx| {
|
||||
cx.dispatch_action(NewThread.boxed_clone());
|
||||
.on_click(move |_event, window, cx| {
|
||||
window.dispatch_action(NewThread.boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
@@ -315,40 +355,51 @@ impl AssistantPanel {
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |cx| {
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Open History",
|
||||
&OpenHistory,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(move |_event, cx| {
|
||||
cx.dispatch_action(OpenHistory.boxed_clone());
|
||||
.on_click(move |_event, window, cx| {
|
||||
window.dispatch_action(OpenHistory.boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("configure-assistant", IconName::Settings)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
|
||||
.on_click(move |_event, _cx| {
|
||||
.tooltip(Tooltip::text("Configure Assistant"))
|
||||
.on_click(move |_event, _window, _cx| {
|
||||
println!("Configure Assistant");
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
fn render_active_thread_or_empty_state(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> AnyElement {
|
||||
if self.thread.read(cx).is_empty() {
|
||||
return self.render_thread_empty_state(cx).into_any_element();
|
||||
return self
|
||||
.render_thread_empty_state(window, cx)
|
||||
.into_any_element();
|
||||
}
|
||||
|
||||
self.thread.clone().into_any()
|
||||
self.thread.clone().into_any_element()
|
||||
}
|
||||
|
||||
fn render_thread_empty_state(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render_thread_empty_state(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let recent_threads = self
|
||||
.thread_store
|
||||
.update(cx, |this, cx| this.recent_threads(3, cx));
|
||||
@@ -379,7 +430,7 @@ impl AssistantPanel {
|
||||
v_flex().mx_auto().w_4_5().gap_2().children(
|
||||
recent_threads
|
||||
.into_iter()
|
||||
.map(|thread| PastThread::new(thread, cx.view().downgrade())),
|
||||
.map(|thread| PastThread::new(thread, cx.model().downgrade())),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -390,17 +441,17 @@ impl AssistantPanel {
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&OpenHistory,
|
||||
&self.focus_handle(cx),
|
||||
cx,
|
||||
window,
|
||||
))
|
||||
.on_click(move |_event, cx| {
|
||||
cx.dispatch_action(OpenHistory.boxed_clone());
|
||||
.on_click(move |_event, window, cx| {
|
||||
window.dispatch_action(OpenHistory.boxed_clone(), cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
|
||||
fn render_last_error(&self, cx: &mut ModelContext<Self>) -> Option<AnyElement> {
|
||||
let last_error = self.thread.read(cx).last_error()?;
|
||||
|
||||
Some(
|
||||
@@ -426,7 +477,7 @@ impl AssistantPanel {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
fn render_payment_required_error(&self, cx: &mut ModelContext<Self>) -> AnyElement {
|
||||
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
|
||||
|
||||
v_flex()
|
||||
@@ -450,7 +501,7 @@ impl AssistantPanel {
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
|this, _, _, cx| {
|
||||
this.thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
@@ -460,7 +511,7 @@ impl AssistantPanel {
|
||||
},
|
||||
)))
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
|this, _, _, cx| {
|
||||
this.thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
@@ -472,7 +523,7 @@ impl AssistantPanel {
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
fn render_max_monthly_spend_reached_error(&self, cx: &mut ModelContext<Self>) -> AnyElement {
|
||||
const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
|
||||
|
||||
v_flex()
|
||||
@@ -497,7 +548,7 @@ impl AssistantPanel {
|
||||
.mt_1()
|
||||
.child(
|
||||
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
|
||||
cx.listener(|this, _, cx| {
|
||||
cx.listener(|this, _, _, cx| {
|
||||
this.thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
@@ -508,7 +559,7 @@ impl AssistantPanel {
|
||||
),
|
||||
)
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
|this, _, _, cx| {
|
||||
this.thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
@@ -523,7 +574,7 @@ impl AssistantPanel {
|
||||
fn render_error_message(
|
||||
&self,
|
||||
error_message: &SharedString,
|
||||
cx: &mut ViewContext<Self>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> AnyElement {
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
@@ -549,7 +600,7 @@ impl AssistantPanel {
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
|this, _, _, cx| {
|
||||
this.thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
@@ -563,23 +614,21 @@ impl AssistantPanel {
|
||||
}
|
||||
|
||||
impl Render for AssistantPanel {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("AssistantPanel2")
|
||||
.justify_between()
|
||||
.size_full()
|
||||
.on_action(cx.listener(|this, _: &NewThread, cx| {
|
||||
this.new_thread(cx);
|
||||
.on_action(cx.listener(|this, _: &NewThread, window, cx| {
|
||||
this.new_thread(window, cx);
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &OpenHistory, cx| {
|
||||
this.active_view = ActiveView::History;
|
||||
this.history.focus_handle(cx).focus(cx);
|
||||
cx.notify();
|
||||
.on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
|
||||
this.open_history(window, cx);
|
||||
}))
|
||||
.child(self.render_toolbar(cx))
|
||||
.map(|parent| match self.active_view {
|
||||
ActiveView::Thread => parent
|
||||
.child(self.render_active_thread_or_empty_state(cx))
|
||||
.child(self.render_active_thread_or_empty_state(window, cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.border_t_1()
|
||||
|
||||
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
@@ -21,9 +21,10 @@ pub struct Context {
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, Copy, 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;
|
||||
@@ -5,73 +6,95 @@ mod thread_context_picker;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{
|
||||
AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, SharedString, Task, View,
|
||||
WeakModel, WeakView,
|
||||
AppContext, DismissEvent, EventEmitter, FocusHandle, Focusable, Model, SharedString, Task,
|
||||
WeakModel,
|
||||
};
|
||||
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>),
|
||||
Fetch(View<FetchContextPicker>),
|
||||
Thread(View<ThreadContextPicker>),
|
||||
File(Model<FileContextPicker>),
|
||||
Directory(Model<DirectoryContextPicker>),
|
||||
Fetch(Model<FetchContextPicker>),
|
||||
Thread(Model<ThreadContextPicker>),
|
||||
}
|
||||
|
||||
pub(super) struct ContextPicker {
|
||||
mode: ContextPickerMode,
|
||||
picker: View<Picker<ContextPickerDelegate>>,
|
||||
picker: Model<Picker<ContextPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl ContextPicker {
|
||||
pub fn new(
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
thread_store: Option<WeakModel<ThreadStore>>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<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,
|
||||
});
|
||||
}
|
||||
|
||||
let delegate = ContextPickerDelegate {
|
||||
context_picker: cx.view().downgrade(),
|
||||
context_picker: cx.model().downgrade(),
|
||||
workspace,
|
||||
thread_store,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
entries,
|
||||
selected_ix: 0,
|
||||
};
|
||||
|
||||
let picker = cx.new_view(|cx| {
|
||||
Picker::nonsearchable_uniform_list(delegate, cx).max_height(Some(rems(20.).into()))
|
||||
let picker = cx.new_model(|cx| {
|
||||
Picker::nonsearchable_uniform_list(delegate, window, cx)
|
||||
.max_height(Some(rems(20.).into()))
|
||||
});
|
||||
|
||||
ContextPicker {
|
||||
@@ -87,11 +110,12 @@ impl ContextPicker {
|
||||
|
||||
impl EventEmitter<DismissEvent> for ContextPicker {}
|
||||
|
||||
impl FocusableView for ContextPicker {
|
||||
impl Focusable for ContextPicker {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
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),
|
||||
}
|
||||
@@ -99,13 +123,16 @@ impl FocusableView for ContextPicker {
|
||||
}
|
||||
|
||||
impl Render for ContextPicker {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut ModelContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w(px(400.))
|
||||
.min_w(px(400.))
|
||||
.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,14 +142,16 @@ impl Render for ContextPicker {
|
||||
#[derive(Clone)]
|
||||
struct ContextPickerEntry {
|
||||
name: SharedString,
|
||||
kind: ContextKind,
|
||||
icon: IconName,
|
||||
}
|
||||
|
||||
pub(crate) struct ContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_picker: WeakModel<ContextPicker>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
thread_store: Option<WeakModel<ThreadStore>>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
entries: Vec<ContextPickerEntry>,
|
||||
selected_ix: usize,
|
||||
}
|
||||
@@ -138,70 +167,103 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||
self.selected_ix
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_: &mut Window,
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) {
|
||||
self.selected_ix = ix.min(self.entries.len().saturating_sub(1));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut AppContext) -> Arc<str> {
|
||||
"Select a context source…".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, _query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
_query: String,
|
||||
_window: &mut Window,
|
||||
_cx: &mut ModelContext<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn confirm(
|
||||
&mut self,
|
||||
_secondary: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) {
|
||||
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" => {
|
||||
this.mode = ContextPickerMode::File(cx.new_view(|cx| {
|
||||
match entry.kind {
|
||||
ContextKind::File => {
|
||||
this.mode = ContextPickerMode::File(cx.new_model(|cx| {
|
||||
FileContextPicker::new(
|
||||
self.context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
self.confirm_behavior,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
"Fetch" => {
|
||||
this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
|
||||
ContextKind::Directory => {
|
||||
this.mode = ContextPickerMode::Directory(cx.new_model(|cx| {
|
||||
DirectoryContextPicker::new(
|
||||
self.context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
self.confirm_behavior,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
ContextKind::FetchedUrl => {
|
||||
this.mode = ContextPickerMode::Fetch(cx.new_model(|cx| {
|
||||
FetchContextPicker::new(
|
||||
self.context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
self.confirm_behavior,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
"Thread" => {
|
||||
ContextKind::Thread => {
|
||||
if let Some(thread_store) = self.thread_store.as_ref() {
|
||||
this.mode = ContextPickerMode::Thread(cx.new_view(|cx| {
|
||||
this.mode = ContextPickerMode::Thread(cx.new_model(|cx| {
|
||||
ThreadContextPicker::new(
|
||||
thread_store.clone(),
|
||||
self.context_picker.clone(),
|
||||
self.context_store.clone(),
|
||||
self.confirm_behavior,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cx.focus_self();
|
||||
cx.focus_self(window);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn dismissed(&mut self, _: &mut Window, cx: &mut ModelContext<Picker<Self>>) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| match this.mode {
|
||||
ContextPickerMode::Default => cx.emit(DismissEvent),
|
||||
ContextPickerMode::File(_)
|
||||
| ContextPickerMode::Directory(_)
|
||||
| ContextPickerMode::Fetch(_)
|
||||
| ContextPickerMode::Thread(_) => {}
|
||||
})
|
||||
@@ -212,7 +274,8 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut ModelContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let entry = &self.entries[ix];
|
||||
|
||||
|
||||
267
crates/assistant2/src/context_picker/directory_context_picker.rs
Normal file
267
crates/assistant2/src/context_picker/directory_context_picker.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
// 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, Focusable, Model, Task, WeakModel};
|
||||
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: Model<Picker<DirectoryContextPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl DirectoryContextPicker {
|
||||
pub fn new(
|
||||
context_picker: WeakModel<ContextPicker>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let delegate = DirectoryContextPickerDelegate::new(
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
);
|
||||
let picker = cx.new_model(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for DirectoryContextPicker {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DirectoryContextPicker {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut ModelContext<Self>) -> impl IntoElement {
|
||||
self.picker.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DirectoryContextPickerDelegate {
|
||||
context_picker: WeakModel<ContextPicker>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
matches: Vec<PathMatch>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl DirectoryContextPickerDelegate {
|
||||
pub fn new(
|
||||
context_picker: WeakModel<ContextPicker>,
|
||||
workspace: WeakModel<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: &Model<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<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,
|
||||
_window: &mut Window,
|
||||
_cx: &mut ModelContext<Picker<Self>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut AppContext) -> Arc<str> {
|
||||
"Search folders…".into()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
let search_task = self.search(query, Arc::<AtomicBool>::default(), &workspace, window, cx);
|
||||
|
||||
cx.spawn_in(window, |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,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<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_in(window, |this, mut cx| async move {
|
||||
this.update_in(&mut cx, |this, window, 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(window, cx),
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, window: &mut Window, cx: &mut ModelContext<Picker<Self>>) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| {
|
||||
this.reset_mode();
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_window: &mut Window,
|
||||
_cx: &mut ModelContext<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))),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,43 +4,50 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
|
||||
use gpui::{AppContext, DismissEvent, FocusHandle, Focusable, Model, Task, WeakModel};
|
||||
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
|
||||
use http_client::{AsyncBody, HttpClientWithUrl};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{prelude::*, ListItem, ViewContext};
|
||||
use ui::{prelude::*, ListItem, ModelContext, Window};
|
||||
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 {
|
||||
picker: View<Picker<FetchContextPickerDelegate>>,
|
||||
picker: Model<Picker<FetchContextPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl FetchContextPicker {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_picker: WeakModel<ContextPicker>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let delegate = FetchContextPickerDelegate::new(context_picker, workspace, context_store);
|
||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
||||
let delegate = FetchContextPickerDelegate::new(
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
);
|
||||
let picker = cx.new_model(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for FetchContextPicker {
|
||||
impl Focusable for FetchContextPicker {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for FetchContextPicker {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut ModelContext<Self>) -> impl IntoElement {
|
||||
self.picker.clone()
|
||||
}
|
||||
}
|
||||
@@ -53,22 +60,25 @@ enum ContentType {
|
||||
}
|
||||
|
||||
pub struct FetchContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_picker: WeakModel<ContextPicker>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl FetchContextPickerDelegate {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_picker: WeakModel<ContextPicker>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
) -> Self {
|
||||
FetchContextPickerDelegate {
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
url: String::new(),
|
||||
}
|
||||
}
|
||||
@@ -157,7 +167,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut AppContext) -> SharedString {
|
||||
"Enter the URL that you would like to fetch".into()
|
||||
}
|
||||
|
||||
@@ -165,34 +175,58 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
0
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, _ix: usize, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
_ix: usize,
|
||||
_window: &mut Window,
|
||||
_cx: &mut ModelContext<Picker<Self>>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut ui::WindowContext) -> Arc<str> {
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut AppContext) -> Arc<str> {
|
||||
"Enter a URL…".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
_window: &mut Window,
|
||||
_cx: &mut ModelContext<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
self.url = query;
|
||||
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn confirm(
|
||||
&mut self,
|
||||
_secondary: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let http_client = workspace.read(cx).client().http_client().clone();
|
||||
let url = self.url.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let confirm_behavior = self.confirm_behavior;
|
||||
cx.spawn_in(window, |this, mut cx| async move {
|
||||
let text = Self::build_message(http_client, &url).await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update_in(&mut cx, |this, window, cx| {
|
||||
this.delegate
|
||||
.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(window, cx),
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
|
||||
anyhow::Ok(())
|
||||
@@ -200,7 +234,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn dismissed(&mut self, _: &mut Window, cx: &mut ModelContext<Picker<Self>>) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| {
|
||||
this.reset_mode();
|
||||
@@ -213,7 +247,8 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut ModelContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
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;
|
||||
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
|
||||
use gpui::{AppContext, DismissEvent, FocusHandle, Focusable, Model, Task, WeakModel};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, WorktreeId};
|
||||
use ui::{prelude::*, ListItem};
|
||||
@@ -13,57 +13,67 @@ 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 {
|
||||
picker: View<Picker<FileContextPickerDelegate>>,
|
||||
picker: Model<Picker<FileContextPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl FileContextPicker {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_picker: WeakModel<ContextPicker>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let delegate = FileContextPickerDelegate::new(context_picker, workspace, context_store);
|
||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
||||
let delegate = FileContextPickerDelegate::new(
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
);
|
||||
let picker = cx.new_model(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for FileContextPicker {
|
||||
impl Focusable for FileContextPicker {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for FileContextPicker {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut ModelContext<Self>) -> impl IntoElement {
|
||||
self.picker.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FileContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_picker: WeakModel<ContextPicker>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
matches: Vec<PathMatch>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl FileContextPickerDelegate {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_picker: WeakModel<ContextPicker>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
) -> Self {
|
||||
Self {
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
@@ -73,50 +83,44 @@ impl FileContextPickerDelegate {
|
||||
&mut self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &View<Workspace>,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
workspace: &Model<Workspace>,
|
||||
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
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
|
||||
@@ -163,22 +167,32 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_window: &mut Window,
|
||||
_cx: &mut ModelContext<Picker<Self>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut AppContext) -> Arc<str> {
|
||||
"Search files…".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<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 {
|
||||
cx.spawn_in(window, |this, mut cx| async move {
|
||||
// TODO: This should be probably be run in the background.
|
||||
let paths = search_task.await;
|
||||
|
||||
@@ -189,8 +203,15 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let mat = &self.matches[self.selected_index];
|
||||
fn confirm(
|
||||
&mut self,
|
||||
_secondary: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) {
|
||||
let Some(mat) = self.matches.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(project) = workspace
|
||||
@@ -201,7 +222,8 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
};
|
||||
let path = mat.path.clone();
|
||||
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let confirm_behavior = self.confirm_behavior;
|
||||
cx.spawn_in(window, |this, mut cx| async move {
|
||||
let Some(open_buffer_task) = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.open_buffer((worktree_id, path.clone()), cx)
|
||||
@@ -213,23 +235,32 @@ 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.update_in(&mut cx, |this, window, 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');
|
||||
}
|
||||
|
||||
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(window, cx),
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
|
||||
anyhow::Ok(())
|
||||
@@ -237,7 +268,7 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn dismissed(&mut self, _: &mut Window, cx: &mut ModelContext<Picker<Self>>) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| {
|
||||
this.reset_mode();
|
||||
@@ -250,19 +281,34 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut ModelContext<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(
|
||||
|
||||
@@ -1,43 +1,49 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
|
||||
use gpui::{AppContext, DismissEvent, FocusHandle, Focusable, Model, Task, WeakModel};
|
||||
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;
|
||||
|
||||
pub struct ThreadContextPicker {
|
||||
picker: View<Picker<ThreadContextPickerDelegate>>,
|
||||
picker: Model<Picker<ThreadContextPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl ThreadContextPicker {
|
||||
pub fn new(
|
||||
thread_store: WeakModel<ThreadStore>,
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
context_picker: WeakModel<ContextPicker>,
|
||||
context_store: WeakModel<context_store::ContextStore>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let delegate =
|
||||
ThreadContextPickerDelegate::new(thread_store, context_picker, context_store);
|
||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
||||
let delegate = ThreadContextPickerDelegate::new(
|
||||
thread_store,
|
||||
context_picker,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
);
|
||||
let picker = cx.new_model(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
|
||||
ThreadContextPicker { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for ThreadContextPicker {
|
||||
impl Focusable for ThreadContextPicker {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ThreadContextPicker {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut ModelContext<Self>) -> impl IntoElement {
|
||||
self.picker.clone()
|
||||
}
|
||||
}
|
||||
@@ -50,8 +56,9 @@ struct ThreadContextEntry {
|
||||
|
||||
pub struct ThreadContextPickerDelegate {
|
||||
thread_store: WeakModel<ThreadStore>,
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
context_picker: WeakModel<ContextPicker>,
|
||||
context_store: WeakModel<context_store::ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
matches: Vec<ThreadContextEntry>,
|
||||
selected_index: usize,
|
||||
}
|
||||
@@ -59,13 +66,15 @@ pub struct ThreadContextPickerDelegate {
|
||||
impl ThreadContextPickerDelegate {
|
||||
pub fn new(
|
||||
thread_store: WeakModel<ThreadStore>,
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
context_picker: WeakModel<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,
|
||||
}
|
||||
@@ -83,15 +92,25 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_window: &mut Window,
|
||||
_cx: &mut ModelContext<Picker<Self>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut AppContext) -> Arc<str> {
|
||||
"Search threads…".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let Ok(threads) = self.thread_store.update(cx, |this, cx| {
|
||||
this.threads(cx)
|
||||
.into_iter()
|
||||
@@ -134,7 +153,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.spawn_in(window, |this, mut cx| async move {
|
||||
let matches = search_task.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate.matches = matches;
|
||||
@@ -145,8 +164,15 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let entry = &self.matches[self.selected_index];
|
||||
fn confirm(
|
||||
&mut self,
|
||||
_secondary: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) {
|
||||
let Some(entry) = self.matches.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(thread_store) = self.thread_store.upgrade() else {
|
||||
return;
|
||||
@@ -180,9 +206,14 @@ 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(window, cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn dismissed(&mut self, _: &mut Window, cx: &mut ModelContext<Picker<Self>>) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| {
|
||||
this.reset_mode();
|
||||
@@ -195,7 +226,8 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut ModelContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let thread = &self.matches[ix];
|
||||
|
||||
@@ -203,7 +235,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.child(thread.summary.clone()),
|
||||
.child(Label::new(thread.summary.clone())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{FocusHandle, Model, View, WeakModel, WeakView};
|
||||
use gpui::{FocusHandle, Model, WeakModel};
|
||||
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;
|
||||
use crate::ToggleContextPicker;
|
||||
use settings::Settings;
|
||||
|
||||
pub struct ContextStrip {
|
||||
context_store: Model<ContextStore>,
|
||||
context_picker: View<ContextPicker>,
|
||||
context_picker: Model<ContextPicker>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
@@ -20,19 +21,22 @@ pub struct ContextStrip {
|
||||
impl ContextStrip {
|
||||
pub fn new(
|
||||
context_store: Model<ContextStore>,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
thread_store: Option<WeakModel<ThreadStore>>,
|
||||
focus_handle: FocusHandle,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
context_store: context_store.clone(),
|
||||
context_picker: cx.new_view(|cx| {
|
||||
context_picker: cx.new_model(|cx| {
|
||||
ContextPicker::new(
|
||||
workspace.clone(),
|
||||
thread_store.clone(),
|
||||
context_store.downgrade(),
|
||||
ConfirmBehavior::KeepOpen,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
@@ -43,8 +47,8 @@ impl ContextStrip {
|
||||
}
|
||||
|
||||
impl Render for ContextStrip {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let context = self.context_store.read(cx).context();
|
||||
fn render(&mut self, window: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
|
||||
let context = self.context_store.read(cx).context().clone();
|
||||
let context_picker = self.context_picker.clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
@@ -53,16 +57,17 @@ impl Render for ContextStrip {
|
||||
.gap_1()
|
||||
.child(
|
||||
PopoverMenu::new("context-picker")
|
||||
.menu(move |_cx| Some(context_picker.clone()))
|
||||
.menu(move |_window, _cx| Some(context_picker.clone()))
|
||||
.trigger(
|
||||
IconButton::new("add-context", IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ui::ButtonStyle::Filled)
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Add Context",
|
||||
&ToggleContextPicker,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
@@ -75,11 +80,34 @@ impl Render for ContextStrip {
|
||||
})
|
||||
.with_handle(self.context_picker_menu_handle.clone()),
|
||||
)
|
||||
.when(context.is_empty(), {
|
||||
|parent| {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.id("no-content-info")
|
||||
.ml_1p5()
|
||||
.gap_2()
|
||||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||
.text_size(TextSize::Small.rems(cx))
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child("Add Context")
|
||||
.children(
|
||||
ui::KeyBinding::for_action_in(
|
||||
&ToggleContextPicker,
|
||||
&self.focus_handle,
|
||||
window,
|
||||
)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
)
|
||||
.opacity(0.5),
|
||||
)
|
||||
}
|
||||
})
|
||||
.children(context.iter().map(|context| {
|
||||
ContextPill::new(context.clone()).on_remove({
|
||||
let context = context.clone();
|
||||
let context_store = self.context_store.clone();
|
||||
Rc::new(cx.listener(move |_this, _event, cx| {
|
||||
Rc::new(cx.listener(move |_this, _event, _, cx| {
|
||||
context_store.update(cx, |this, _cx| {
|
||||
this.remove_context(&context.id);
|
||||
});
|
||||
@@ -87,19 +115,21 @@ impl Render for ContextStrip {
|
||||
}))
|
||||
})
|
||||
}))
|
||||
.when(!context.is_empty(), |parent| {
|
||||
parent.child(
|
||||
IconButton::new("remove-all-context", IconName::Eraser)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(move |cx| Tooltip::text("Remove All Context", cx))
|
||||
.on_click({
|
||||
let context_store = self.context_store.clone();
|
||||
cx.listener(move |_this, _event, cx| {
|
||||
context_store.update(cx, |this, _cx| this.clear());
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
)
|
||||
.when(!context.is_empty(), {
|
||||
move |parent| {
|
||||
parent.child(
|
||||
IconButton::new("remove-all-context", IconName::Eraser)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Remove All Context"))
|
||||
.on_click({
|
||||
let context_store = self.context_store.clone();
|
||||
cx.listener(move |_this, _event, _, cx| {
|
||||
context_store.update(cx, |this, _cx| this.clear());
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1148
crates/assistant2/src/inline_prompt_editor.rs
Normal file
1148
crates/assistant2/src/inline_prompt_editor.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,21 @@
|
||||
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, Focusable, Model, Subscription, TextStyle, WeakModel};
|
||||
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};
|
||||
@@ -23,83 +24,112 @@ use crate::{Chat, ToggleContextPicker, ToggleModelSelector};
|
||||
|
||||
pub struct MessageEditor {
|
||||
thread: Model<Thread>,
|
||||
editor: View<Editor>,
|
||||
editor: Model<Editor>,
|
||||
context_store: Model<ContextStore>,
|
||||
context_strip: View<ContextStrip>,
|
||||
context_strip: Model<ContextStrip>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
language_model_selector: View<LanguageModelSelector>,
|
||||
language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
inline_context_picker: Model<ContextPicker>,
|
||||
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
model_selector: Model<AssistantModelSelector>,
|
||||
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
use_tools: bool,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl MessageEditor {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
thread_store: WeakModel<ThreadStore>,
|
||||
thread: Model<Thread>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> 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);
|
||||
editor.set_placeholder_text("Ask anything, @ to add context", cx);
|
||||
let editor = cx.new_model(|cx| {
|
||||
let mut editor = Editor::auto_height(10, window, cx);
|
||||
editor.set_placeholder_text("Ask anything…", cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
|
||||
editor
|
||||
});
|
||||
let inline_context_picker = cx.new_model(|cx| {
|
||||
ContextPicker::new(
|
||||
workspace.clone(),
|
||||
Some(thread_store.clone()),
|
||||
context_store.downgrade(),
|
||||
ConfirmBehavior::Close,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let subscriptions = vec![
|
||||
cx.subscribe_in(&editor, window, Self::handle_editor_event),
|
||||
cx.subscribe_in(
|
||||
&inline_context_picker,
|
||||
window,
|
||||
Self::handle_inline_context_picker_event,
|
||||
),
|
||||
];
|
||||
|
||||
Self {
|
||||
thread,
|
||||
editor: editor.clone(),
|
||||
context_store: context_store.clone(),
|
||||
context_strip: cx.new_view(|cx| {
|
||||
context_strip: cx.new_model(|cx| {
|
||||
ContextStrip::new(
|
||||
context_store,
|
||||
workspace.clone(),
|
||||
Some(thread_store.clone()),
|
||||
editor.focus_handle(cx),
|
||||
context_picker_menu_handle.clone(),
|
||||
window,
|
||||
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, _cx| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
inline_context_picker,
|
||||
inline_context_picker_menu_handle,
|
||||
model_selector: cx.new_model(|cx| {
|
||||
AssistantModelSelector::new(fs, model_selector_menu_handle.clone(), window, 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);
|
||||
fn toggle_model_selector(
|
||||
&mut self,
|
||||
_: &ToggleModelSelector,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.model_selector_menu_handle.toggle(window, cx)
|
||||
}
|
||||
|
||||
fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
|
||||
self.context_picker_menu_handle.toggle(cx);
|
||||
fn toggle_context_picker(
|
||||
&mut self,
|
||||
_: &ToggleContextPicker,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.context_picker_menu_handle.toggle(window, cx);
|
||||
}
|
||||
|
||||
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
|
||||
self.send_to_model(RequestKind::Chat, cx);
|
||||
fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut ModelContext<Self>) {
|
||||
self.send_to_model(RequestKind::Chat, window, cx);
|
||||
}
|
||||
|
||||
fn send_to_model(
|
||||
&mut self,
|
||||
request_kind: RequestKind,
|
||||
cx: &mut ViewContext<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<()> {
|
||||
let provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
if provider
|
||||
@@ -115,7 +145,7 @@ impl MessageEditor {
|
||||
|
||||
let user_message = self.editor.update(cx, |editor, cx| {
|
||||
let text = editor.text(cx);
|
||||
editor.clear(cx);
|
||||
editor.clear(window, cx);
|
||||
text
|
||||
});
|
||||
let context = self.context_store.update(cx, |this, _cx| this.drain());
|
||||
@@ -143,62 +173,55 @@ 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: &Model<Editor>,
|
||||
event: &EditorEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<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(window, 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: &Model<ContextPicker>,
|
||||
_event: &DismissEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let editor_focus_handle = self.editor.focus_handle(cx);
|
||||
window.focus(&editor_focus_handle);
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for MessageEditor {
|
||||
impl Focusable for MessageEditor {
|
||||
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for MessageEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
|
||||
let font_size = TextSize::Default.rems(cx);
|
||||
let line_height = font_size.to_pixels(cx.rem_size()) * 1.5;
|
||||
let line_height = font_size.to_pixels(window.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,58 +234,72 @@ 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({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: line_height.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
background: bg_color,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(CheckboxWithLabel::new(
|
||||
"use-tools",
|
||||
Label::new("Tools"),
|
||||
self.use_tools.into(),
|
||||
cx.listener(|this, selection, _cx| {
|
||||
this.use_tools = match selection {
|
||||
ToggleState::Selected => true,
|
||||
ToggleState::Unselected | ToggleState::Indeterminate => false,
|
||||
};
|
||||
}),
|
||||
))
|
||||
v_flex()
|
||||
.gap_4()
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: line_height.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
background: bg_color,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.child(
|
||||
PopoverMenu::new("inline-context-picker")
|
||||
.menu(move |_window, _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()
|
||||
.gap_1()
|
||||
.child(self.render_language_model_selector(cx))
|
||||
.justify_between()
|
||||
.child(SwitchWithLabel::new(
|
||||
"use-tools",
|
||||
Label::new("Tools"),
|
||||
self.use_tools.into(),
|
||||
cx.listener(|this, selection, _window, _cx| {
|
||||
this.use_tools = match selection {
|
||||
ToggleState::Selected => true,
|
||||
ToggleState::Unselected | ToggleState::Indeterminate => {
|
||||
false
|
||||
}
|
||||
};
|
||||
}),
|
||||
))
|
||||
.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, window)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(&Chat, window, 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,26 @@
|
||||
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,
|
||||
};
|
||||
use gpui::{AppContext, Context, Focusable, Global, Model, Subscription, UpdateGlobal, WeakModel};
|
||||
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 +36,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>,
|
||||
@@ -87,10 +65,11 @@ impl TerminalInlineAssistant {
|
||||
|
||||
pub fn assist(
|
||||
&mut self,
|
||||
terminal_view: &View<TerminalView>,
|
||||
workspace: WeakView<Workspace>,
|
||||
terminal_view: &Model<TerminalView>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
thread_store: Option<WeakModel<ThreadStore>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let terminal = terminal_view.read(cx).terminal().clone();
|
||||
let assist_id = self.next_assist_id.post_inc();
|
||||
@@ -98,10 +77,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(
|
||||
let prompt_editor = cx.new_model(|cx| {
|
||||
PromptEditor::new_terminal(
|
||||
assist_id,
|
||||
self.prompt_history.clone(),
|
||||
prompt_buffer.clone(),
|
||||
@@ -110,6 +89,7 @@ impl TerminalInlineAssistant {
|
||||
context_store.clone(),
|
||||
workspace.clone(),
|
||||
thread_store.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -119,7 +99,7 @@ impl TerminalInlineAssistant {
|
||||
render: Box::new(move |_| prompt_editor_render.clone().into_any_element()),
|
||||
};
|
||||
terminal_view.update(cx, |terminal_view, cx| {
|
||||
terminal_view.set_block_below_cursor(block, cx);
|
||||
terminal_view.set_block_below_cursor(block, window, cx);
|
||||
});
|
||||
|
||||
let terminal_assistant = TerminalInlineAssist::new(
|
||||
@@ -128,21 +108,27 @@ impl TerminalInlineAssistant {
|
||||
prompt_editor,
|
||||
workspace.clone(),
|
||||
context_store,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
self.assists.insert(assist_id, terminal_assistant);
|
||||
|
||||
self.focus_assist(assist_id, cx);
|
||||
self.focus_assist(assist_id, window, cx);
|
||||
}
|
||||
|
||||
fn focus_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) {
|
||||
fn focus_assist(
|
||||
&mut self,
|
||||
assist_id: TerminalInlineAssistId,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let assist = &self.assists[&assist_id];
|
||||
if let Some(prompt_editor) = assist.prompt_editor.as_ref() {
|
||||
prompt_editor.update(cx, |this, cx| {
|
||||
this.editor.update(cx, |editor, cx| {
|
||||
editor.focus(cx);
|
||||
editor.select_all(&SelectAll, cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.select_all(&SelectAll, window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -150,11 +136,12 @@ impl TerminalInlineAssistant {
|
||||
|
||||
fn handle_prompt_editor_event(
|
||||
&mut self,
|
||||
prompt_editor: View<PromptEditor>,
|
||||
prompt_editor: Model<PromptEditor<TerminalCodegen>>,
|
||||
event: &PromptEditorEvent,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
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);
|
||||
@@ -163,21 +150,21 @@ impl TerminalInlineAssistant {
|
||||
self.stop_assist(assist_id, cx);
|
||||
}
|
||||
PromptEditorEvent::ConfirmRequested { execute } => {
|
||||
self.finish_assist(assist_id, false, *execute, cx);
|
||||
self.finish_assist(assist_id, false, *execute, window, cx);
|
||||
}
|
||||
PromptEditorEvent::CancelRequested => {
|
||||
self.finish_assist(assist_id, true, false, cx);
|
||||
self.finish_assist(assist_id, true, false, window, cx);
|
||||
}
|
||||
PromptEditorEvent::DismissRequested => {
|
||||
self.dismiss_assist(assist_id, cx);
|
||||
self.dismiss_assist(assist_id, window, cx);
|
||||
}
|
||||
PromptEditorEvent::Resized { height_in_lines } => {
|
||||
self.insert_prompt_editor_into_terminal(assist_id, *height_in_lines, cx);
|
||||
self.insert_prompt_editor_into_terminal(assist_id, *height_in_lines, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) {
|
||||
fn start_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut AppContext) {
|
||||
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
|
||||
assist
|
||||
} else {
|
||||
@@ -215,7 +202,7 @@ impl TerminalInlineAssistant {
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
}
|
||||
|
||||
fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) {
|
||||
fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut AppContext) {
|
||||
let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
|
||||
assist
|
||||
} else {
|
||||
@@ -228,7 +215,7 @@ impl TerminalInlineAssistant {
|
||||
fn request_for_inline_assist(
|
||||
&self,
|
||||
assist_id: TerminalInlineAssistId,
|
||||
cx: &mut WindowContext,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<LanguageModelRequest> {
|
||||
let assist = self.assists.get(&assist_id).context("invalid assist")?;
|
||||
|
||||
@@ -284,16 +271,17 @@ impl TerminalInlineAssistant {
|
||||
assist_id: TerminalInlineAssistId,
|
||||
undo: bool,
|
||||
execute: bool,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.dismiss_assist(assist_id, cx);
|
||||
self.dismiss_assist(assist_id, window, cx);
|
||||
|
||||
if let Some(assist) = self.assists.remove(&assist_id) {
|
||||
assist
|
||||
.terminal
|
||||
.update(cx, |this, cx| {
|
||||
this.clear_block_below_cursor(cx);
|
||||
this.focus_handle(cx).focus(cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
@@ -336,7 +324,8 @@ impl TerminalInlineAssistant {
|
||||
fn dismiss_assist(
|
||||
&mut self,
|
||||
assist_id: TerminalInlineAssistId,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> bool {
|
||||
let Some(assist) = self.assists.get_mut(&assist_id) else {
|
||||
return false;
|
||||
@@ -349,7 +338,7 @@ impl TerminalInlineAssistant {
|
||||
.terminal
|
||||
.update(cx, |this, cx| {
|
||||
this.clear_block_below_cursor(cx);
|
||||
this.focus_handle(cx).focus(cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
})
|
||||
.is_ok()
|
||||
}
|
||||
@@ -358,7 +347,8 @@ impl TerminalInlineAssistant {
|
||||
&mut self,
|
||||
assist_id: TerminalInlineAssistId,
|
||||
height: u8,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if let Some(assist) = self.assists.get_mut(&assist_id) {
|
||||
if let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned() {
|
||||
@@ -370,7 +360,7 @@ impl TerminalInlineAssistant {
|
||||
height,
|
||||
render: Box::new(move |_| prompt_editor.clone().into_any_element()),
|
||||
};
|
||||
terminal.set_block_below_cursor(block, cx);
|
||||
terminal.set_block_below_cursor(block, window, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
@@ -379,10 +369,10 @@ impl TerminalInlineAssistant {
|
||||
}
|
||||
|
||||
struct TerminalInlineAssist {
|
||||
terminal: WeakView<TerminalView>,
|
||||
prompt_editor: Option<View<PromptEditor>>,
|
||||
codegen: Model<Codegen>,
|
||||
workspace: WeakView<Workspace>,
|
||||
terminal: WeakModel<TerminalView>,
|
||||
prompt_editor: Option<Model<PromptEditor<TerminalCodegen>>>,
|
||||
codegen: Model<TerminalCodegen>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
context_store: Model<ContextStore>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
@@ -390,13 +380,14 @@ struct TerminalInlineAssist {
|
||||
impl TerminalInlineAssist {
|
||||
pub fn new(
|
||||
assist_id: TerminalInlineAssistId,
|
||||
terminal: &View<TerminalView>,
|
||||
prompt_editor: View<PromptEditor>,
|
||||
workspace: WeakView<Workspace>,
|
||||
terminal: &Model<TerminalView>,
|
||||
prompt_editor: Model<PromptEditor<TerminalCodegen>>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
context_store: Model<ContextStore>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> 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()),
|
||||
@@ -404,12 +395,12 @@ impl TerminalInlineAssist {
|
||||
workspace: workspace.clone(),
|
||||
context_store,
|
||||
_subscriptions: vec![
|
||||
cx.subscribe(&prompt_editor, |prompt_editor, event, cx| {
|
||||
window.subscribe(&prompt_editor, cx, |prompt_editor, event, window, cx| {
|
||||
TerminalInlineAssistant::update_global(cx, |this, cx| {
|
||||
this.handle_prompt_editor_event(prompt_editor, event, cx)
|
||||
this.handle_prompt_editor_event(prompt_editor, event, window, cx)
|
||||
})
|
||||
}),
|
||||
cx.subscribe(&codegen, move |codegen, event, cx| {
|
||||
window.subscribe(&codegen, cx, move |codegen, event, window, cx| {
|
||||
TerminalInlineAssistant::update_global(cx, |this, cx| match event {
|
||||
CodegenEvent::Finished => {
|
||||
let assist = if let Some(assist) = this.assists.get(&assist_id) {
|
||||
@@ -438,7 +429,7 @@ impl TerminalInlineAssist {
|
||||
}
|
||||
|
||||
if assist.prompt_editor.is_none() {
|
||||
this.finish_assist(assist_id, false, false, cx);
|
||||
this.finish_assist(assist_id, false, false, window, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -447,660 +438,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),
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use gpui::{
|
||||
uniform_list, AppContext, FocusHandle, FocusableView, Model, UniformListScrollHandle, WeakView,
|
||||
uniform_list, AppContext, FocusHandle, Focusable, Model, UniformListScrollHandle, WeakModel,
|
||||
};
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip};
|
||||
@@ -10,16 +10,17 @@ use crate::AssistantPanel;
|
||||
|
||||
pub struct ThreadHistory {
|
||||
focus_handle: FocusHandle,
|
||||
assistant_panel: WeakView<AssistantPanel>,
|
||||
assistant_panel: WeakModel<AssistantPanel>,
|
||||
thread_store: Model<ThreadStore>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
}
|
||||
|
||||
impl ThreadHistory {
|
||||
pub(crate) fn new(
|
||||
assistant_panel: WeakView<AssistantPanel>,
|
||||
assistant_panel: WeakModel<AssistantPanel>,
|
||||
thread_store: Model<ThreadStore>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
@@ -30,14 +31,14 @@ impl ThreadHistory {
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for ThreadHistory {
|
||||
impl Focusable for ThreadHistory {
|
||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ThreadHistory {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
|
||||
let threads = self.thread_store.update(cx, |this, cx| this.threads(cx));
|
||||
|
||||
v_flex()
|
||||
@@ -59,17 +60,17 @@ impl Render for ThreadHistory {
|
||||
} else {
|
||||
history.child(
|
||||
uniform_list(
|
||||
cx.view().clone(),
|
||||
cx.model().clone(),
|
||||
"thread-history",
|
||||
threads.len(),
|
||||
move |history, range, _cx| {
|
||||
move |history, range, _window, _cx| {
|
||||
threads[range]
|
||||
.iter()
|
||||
.map(|thread| {
|
||||
PastThread::new(
|
||||
h_flex().w_full().pb_1().child(PastThread::new(
|
||||
thread.clone(),
|
||||
history.assistant_panel.clone(),
|
||||
)
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
@@ -85,11 +86,11 @@ impl Render for ThreadHistory {
|
||||
#[derive(IntoElement)]
|
||||
pub struct PastThread {
|
||||
thread: Model<Thread>,
|
||||
assistant_panel: WeakView<AssistantPanel>,
|
||||
assistant_panel: WeakModel<AssistantPanel>,
|
||||
}
|
||||
|
||||
impl PastThread {
|
||||
pub fn new(thread: Model<Thread>, assistant_panel: WeakView<AssistantPanel>) -> Self {
|
||||
pub fn new(thread: Model<Thread>, assistant_panel: WeakModel<AssistantPanel>) -> Self {
|
||||
Self {
|
||||
thread,
|
||||
assistant_panel,
|
||||
@@ -98,7 +99,7 @@ impl PastThread {
|
||||
}
|
||||
|
||||
impl RenderOnce for PastThread {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
fn render(self, _window: &mut Window, cx: &mut AppContext) -> impl IntoElement {
|
||||
let (id, summary) = {
|
||||
const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
|
||||
let thread = self.thread.read(cx);
|
||||
@@ -139,11 +140,11 @@ impl RenderOnce for PastThread {
|
||||
IconButton::new("delete", IconName::TrashAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(|cx| Tooltip::text("Delete Thread", cx))
|
||||
.tooltip(Tooltip::text("Delete Thread"))
|
||||
.on_click({
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
let id = id.clone();
|
||||
move |_event, cx| {
|
||||
move |_event, _, cx| {
|
||||
assistant_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.delete_thread(&id, cx);
|
||||
@@ -156,10 +157,10 @@ impl RenderOnce for PastThread {
|
||||
.on_click({
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
let id = id.clone();
|
||||
move |_event, cx| {
|
||||
move |_event, window, cx| {
|
||||
assistant_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.open_thread(&id, cx);
|
||||
this.open_thread(&id, window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ use std::rc::Rc;
|
||||
use gpui::ClickEvent;
|
||||
use ui::{prelude::*, IconButtonShape};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::context::{Context, ContextKind};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ContextPill {
|
||||
context: Context,
|
||||
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
|
||||
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut AppContext)>>,
|
||||
}
|
||||
|
||||
impl ContextPill {
|
||||
@@ -19,23 +19,39 @@ impl ContextPill {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_remove(mut self, on_remove: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>) -> Self {
|
||||
pub fn on_remove(
|
||||
mut self,
|
||||
on_remove: Rc<dyn Fn(&ClickEvent, &mut Window, &mut AppContext)>,
|
||||
) -> Self {
|
||||
self.on_remove = Some(on_remove);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ContextPill {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
fn render(self, _window: &mut Window, cx: &mut AppContext) -> impl IntoElement {
|
||||
let padding_right = if self.on_remove.is_some() {
|
||||
px(2.)
|
||||
} else {
|
||||
px(4.)
|
||||
};
|
||||
let icon = match self.context.kind {
|
||||
ContextKind::File => IconName::File,
|
||||
ContextKind::Directory => IconName::Folder,
|
||||
ContextKind::FetchedUrl => IconName::Globe,
|
||||
ContextKind::Thread => IconName::MessageCircle,
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.pl_1p5()
|
||||
.pr_0p5()
|
||||
.pl_1()
|
||||
.pr(padding_right)
|
||||
.pb(px(1.))
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.5))
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.rounded_md()
|
||||
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
|
||||
.child(Label::new(self.context.name.clone()).size(LabelSize::Small))
|
||||
.when_some(self.on_remove, |parent, on_remove| {
|
||||
parent.child(
|
||||
@@ -44,7 +60,7 @@ impl RenderOnce for ContextPill {
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click({
|
||||
let on_remove = on_remove.clone();
|
||||
move |event, cx| on_remove(event, cx)
|
||||
move |event, window, cx| on_remove(event, window, cx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ pub use crate::slash_command_registry::*;
|
||||
use anyhow::Result;
|
||||
use futures::stream::{self, BoxStream};
|
||||
use futures::StreamExt;
|
||||
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext};
|
||||
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakModel, Window};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
|
||||
pub use language_model::Role;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -78,8 +78,9 @@ pub trait SlashCommand: 'static + Send + Sync {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
cancel: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>>;
|
||||
fn requires_argument(&self) -> bool;
|
||||
fn accepts_arguments(&self) -> bool {
|
||||
@@ -90,21 +91,27 @@ pub trait SlashCommand: 'static + Send + Sync {
|
||||
arguments: &[String],
|
||||
context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
// TODO: We're just using the `LspAdapterDelegate` here because that is
|
||||
// what the extension API is already expecting.
|
||||
//
|
||||
// It may be that `LspAdapterDelegate` needs a more general name, or
|
||||
// perhaps another kind of delegate is needed here.
|
||||
delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<SlashCommandResult>;
|
||||
}
|
||||
|
||||
pub type RenderFoldPlaceholder = Arc<
|
||||
dyn Send
|
||||
+ Sync
|
||||
+ Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
|
||||
+ Fn(
|
||||
ElementId,
|
||||
Arc<dyn Fn(&mut Window, &mut AppContext)>,
|
||||
&mut Window,
|
||||
&mut AppContext,
|
||||
) -> AnyElement,
|
||||
>;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::sync::{atomic::AtomicBool, Arc};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use extension::{Extension, ExtensionHostProxy, ExtensionSlashCommandProxy, WorktreeDelegate};
|
||||
use gpui::{AppContext, Task, WeakView, WindowContext};
|
||||
use gpui::{AppContext, Task, WeakModel, Window};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
@@ -97,8 +97,9 @@ impl SlashCommand for ExtensionSlashCommand {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let command = self.command.clone();
|
||||
let arguments = arguments.to_owned();
|
||||
@@ -127,9 +128,10 @@ impl SlashCommand for ExtensionSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
_window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let command = self.command.clone();
|
||||
let arguments = arguments.to_owned();
|
||||
|
||||
@@ -4,7 +4,7 @@ mod tool_working_set;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::{AppContext, Task, WeakView, WindowContext};
|
||||
use gpui::{AppContext, Task, WeakModel, Window};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub use crate::tool_registry::*;
|
||||
@@ -31,7 +31,8 @@ pub trait Tool: 'static + Send + Sync {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
workspace: WeakModel<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<String>>;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::Tool;
|
||||
use chrono::{Local, Utc};
|
||||
use gpui::{Task, WeakView, WindowContext};
|
||||
use gpui::{AppContext, Task, WeakModel, Window};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -41,8 +41,9 @@ impl Tool for NowTool {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_workspace: WeakView<workspace::Workspace>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: WeakModel<workspace::Workspace>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<String>> {
|
||||
let input: FileToolInput = match serde_json::from_value(input) {
|
||||
Ok(input) => input,
|
||||
|
||||
@@ -4,7 +4,7 @@ use db::kvp::KEY_VALUE_STORE;
|
||||
use db::RELEASE_CHANNEL;
|
||||
use gpui::{
|
||||
actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext,
|
||||
SemanticVersion, Task, WindowContext,
|
||||
SemanticVersion, Task, Window,
|
||||
};
|
||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use paths::remote_servers_dir;
|
||||
@@ -130,10 +130,10 @@ impl Global for GlobalAutoUpdate {}
|
||||
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
AutoUpdateSetting::register(cx);
|
||||
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
|
||||
workspace.register_action(|_, action: &Check, cx| check(action, cx));
|
||||
cx.observe_new_models(|workspace: &mut Workspace, _window, _cx| {
|
||||
workspace.register_action(|_, action: &Check, window, cx| check(action, window, cx));
|
||||
|
||||
workspace.register_action(|_, action, cx| {
|
||||
workspace.register_action(|_, action, _, cx| {
|
||||
view_release_notes(action, cx);
|
||||
});
|
||||
})
|
||||
@@ -172,23 +172,25 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
cx.set_global(GlobalAutoUpdate(Some(auto_updater)));
|
||||
}
|
||||
|
||||
pub fn check(_: &Check, cx: &mut WindowContext) {
|
||||
pub fn check(_: &Check, window: &mut Window, cx: &mut AppContext) {
|
||||
if let Some(message) = option_env!("ZED_UPDATE_EXPLANATION") {
|
||||
drop(cx.prompt(
|
||||
drop(window.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
"Zed was installed via a package manager.",
|
||||
Some(message),
|
||||
&["Ok"],
|
||||
cx,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(message) = env::var("ZED_UPDATE_EXPLANATION") {
|
||||
drop(cx.prompt(
|
||||
drop(window.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
"Zed was installed via a package manager.",
|
||||
Some(&message),
|
||||
&["Ok"],
|
||||
cx,
|
||||
));
|
||||
return;
|
||||
}
|
||||
@@ -203,11 +205,12 @@ pub fn check(_: &Check, cx: &mut WindowContext) {
|
||||
if let Some(updater) = AutoUpdater::get(cx) {
|
||||
updater.update(cx, |updater, cx| updater.poll(cx));
|
||||
} else {
|
||||
drop(cx.prompt(
|
||||
drop(window.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
"Could not check for updates",
|
||||
Some("Auto-updates disabled for non-bundled app."),
|
||||
&["Ok"],
|
||||
cx,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ mod update_notification;
|
||||
|
||||
use auto_update::AutoUpdater;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{actions, prelude::*, AppContext, SharedString, View, ViewContext};
|
||||
use gpui::{actions, prelude::*, AppContext, Model, ModelContext, SharedString, Window};
|
||||
use http_client::HttpClient;
|
||||
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
|
||||
use release_channel::{AppVersion, ReleaseChannel};
|
||||
@@ -17,9 +17,9 @@ use crate::update_notification::UpdateNotification;
|
||||
actions!(auto_update, [ViewReleaseNotesLocally]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
|
||||
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| {
|
||||
view_release_notes_locally(workspace, cx);
|
||||
cx.observe_new_models(|workspace: &mut Workspace, _window, _cx| {
|
||||
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, window, cx| {
|
||||
view_release_notes_locally(workspace, window, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
@@ -31,7 +31,11 @@ struct ReleaseNotesBody {
|
||||
release_notes: String,
|
||||
}
|
||||
|
||||
fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
fn view_release_notes_locally(
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Workspace>,
|
||||
) {
|
||||
let release_channel = ReleaseChannel::global(cx);
|
||||
|
||||
let url = match release_channel {
|
||||
@@ -60,8 +64,8 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
|
||||
.language_for_name("Markdown");
|
||||
|
||||
workspace
|
||||
.with_local_workspace(cx, move |_, cx| {
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
.with_local_workspace(window, cx, move |_, window, cx| {
|
||||
cx.spawn_in(window, |workspace, mut cx| async move {
|
||||
let markdown = markdown.await.log_err();
|
||||
let response = client.get(&url, Default::default(), true).await;
|
||||
let Some(mut response) = response.log_err() else {
|
||||
@@ -76,7 +80,7 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
|
||||
|
||||
if let Ok(body) = body {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
.update_in(&mut cx, |workspace, window, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
project.create_local_buffer("", markdown, cx)
|
||||
@@ -89,22 +93,24 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
|
||||
let tab_description = SharedString::from(body.title.to_string());
|
||||
let editor = cx.new_view(|cx| {
|
||||
Editor::for_multibuffer(buffer, Some(project), true, cx)
|
||||
let editor = cx.new_model(|cx| {
|
||||
Editor::for_multibuffer(buffer, Some(project), true, window, cx)
|
||||
});
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
|
||||
let view: Model<MarkdownPreviewView> = MarkdownPreviewView::new(
|
||||
MarkdownPreviewMode::Default,
|
||||
editor,
|
||||
workspace_handle,
|
||||
language_registry,
|
||||
Some(tab_description),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(view.clone()),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
@@ -117,12 +123,15 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
|
||||
pub fn notify_of_any_new_update(
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Workspace>,
|
||||
) -> Option<()> {
|
||||
let updater = AutoUpdater::get(cx)?;
|
||||
let version = updater.read(cx).current_version();
|
||||
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
|
||||
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
cx.spawn_in(window, |workspace, mut cx| async move {
|
||||
let should_show_notification = should_show_notification.await?;
|
||||
if should_show_notification {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
@@ -130,7 +139,7 @@ pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
|
||||
workspace.show_notification(
|
||||
NotificationId::unique::<UpdateNotification>(),
|
||||
cx,
|
||||
|cx| cx.new_view(|_| UpdateNotification::new(version, workspace_handle)),
|
||||
|cx| cx.new_model(|_| UpdateNotification::new(version, workspace_handle)),
|
||||
);
|
||||
updater.update(cx, |updater, cx| {
|
||||
updater
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
div, DismissEvent, EventEmitter, InteractiveElement, IntoElement, ParentElement, Render,
|
||||
SemanticVersion, StatefulInteractiveElement, Styled, ViewContext, WeakView,
|
||||
div, DismissEvent, EventEmitter, InteractiveElement, IntoElement, ModelContext, ParentElement,
|
||||
Render, SemanticVersion, StatefulInteractiveElement, Styled, WeakModel, Window,
|
||||
};
|
||||
use menu::Cancel;
|
||||
use release_channel::ReleaseChannel;
|
||||
@@ -12,13 +12,13 @@ use workspace::{
|
||||
|
||||
pub struct UpdateNotification {
|
||||
version: SemanticVersion,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for UpdateNotification {}
|
||||
|
||||
impl Render for UpdateNotification {
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
|
||||
let app_name = ReleaseChannel::global(cx).display_name();
|
||||
|
||||
v_flex()
|
||||
@@ -37,7 +37,9 @@ impl Render for UpdateNotification {
|
||||
.id("cancel")
|
||||
.child(Icon::new(IconName::Close))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))),
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dismiss(&menu::Cancel, window, cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -45,24 +47,24 @@ impl Render for UpdateNotification {
|
||||
.id("notes")
|
||||
.child(Label::new("View the release notes"))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
crate::view_release_notes_locally(workspace, cx);
|
||||
crate::view_release_notes_locally(workspace, window, cx);
|
||||
})
|
||||
.log_err();
|
||||
this.dismiss(&menu::Cancel, cx)
|
||||
this.dismiss(&menu::Cancel, window, cx)
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl UpdateNotification {
|
||||
pub fn new(version: SemanticVersion, workspace: WeakView<Workspace>) -> Self {
|
||||
pub fn new(version: SemanticVersion, workspace: WeakModel<Workspace>) -> Self {
|
||||
Self { version, workspace }
|
||||
}
|
||||
|
||||
pub fn dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
pub fn dismiss(&mut self, _: &Cancel, _: &mut Window, cx: &mut ModelContext<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@ doctest = false
|
||||
editor.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
outline.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
Element, EventEmitter, FocusableView, IntoElement, ParentElement, Render, StyledText,
|
||||
Subscription, ViewContext,
|
||||
Element, EventEmitter, Focusable, IntoElement, ModelContext, ParentElement, Render, StyledText,
|
||||
Subscription, Window,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use std::cmp;
|
||||
@@ -37,12 +37,19 @@ impl Breadcrumbs {
|
||||
impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
|
||||
|
||||
impl Render for Breadcrumbs {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut ModelContext<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,
|
||||
@@ -64,7 +72,7 @@ impl Render for Breadcrumbs {
|
||||
}
|
||||
|
||||
let highlighted_segments = segments.into_iter().map(|segment| {
|
||||
let mut text_style = cx.text_style();
|
||||
let mut text_style = window.text_style();
|
||||
if let Some(font) = segment.font {
|
||||
text_style.font_family = font.family;
|
||||
text_style.font_features = font.features;
|
||||
@@ -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())
|
||||
@@ -92,25 +101,30 @@ impl Render for Breadcrumbs {
|
||||
.style(ButtonStyle::Transparent)
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
move |_, cx| {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
outline::toggle(editor, &editor::actions::ToggleOutline, cx)
|
||||
move |_, window, cx| {
|
||||
if let Some((editor, callback)) = editor
|
||||
.upgrade()
|
||||
.zip(zed_actions::outline::TOGGLE_OUTLINE.get())
|
||||
{
|
||||
callback(editor.to_any(), window, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
let focus_handle = editor.read(cx).focus_handle(cx);
|
||||
Tooltip::for_action_in(
|
||||
"Show symbol outline",
|
||||
&editor::actions::ToggleOutline,
|
||||
"Show Symbol Outline",
|
||||
&zed_actions::outline::ToggleOutline,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
Tooltip::for_action(
|
||||
"Show symbol outline",
|
||||
&editor::actions::ToggleOutline,
|
||||
"Show Symbol Outline",
|
||||
&zed_actions::outline::ToggleOutline,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
@@ -128,7 +142,8 @@ impl ToolbarItemView for Breadcrumbs {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> ToolbarItemLocation {
|
||||
cx.notify();
|
||||
self.active_item = None;
|
||||
@@ -137,10 +152,11 @@ impl ToolbarItemView for Breadcrumbs {
|
||||
return ToolbarItemLocation::Hidden;
|
||||
};
|
||||
|
||||
let this = cx.view().downgrade();
|
||||
let this = cx.model().downgrade();
|
||||
self.subscription = Some(item.subscribe_to_item_events(
|
||||
window,
|
||||
cx,
|
||||
Box::new(move |event, cx| {
|
||||
Box::new(move |event, _, cx| {
|
||||
if let ItemEvent::UpdateBreadcrumbs = event {
|
||||
this.update(cx, |this, cx| {
|
||||
cx.notify();
|
||||
@@ -158,7 +174,12 @@ impl ToolbarItemView for Breadcrumbs {
|
||||
item.breadcrumb_location(cx)
|
||||
}
|
||||
|
||||
fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
|
||||
fn pane_focus_update(
|
||||
&mut self,
|
||||
pane_focused: bool,
|
||||
_window: &mut Window,
|
||||
_: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.pane_focused = pane_focused;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -81,7 +81,7 @@ impl ChannelBuffer {
|
||||
collaborators: Default::default(),
|
||||
acknowledge_task: None,
|
||||
channel_id: channel.id,
|
||||
subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())),
|
||||
subscription: Some(subscription.set_model(&cx.model(), &mut cx.to_async())),
|
||||
user_store,
|
||||
channel_store,
|
||||
};
|
||||
|
||||
@@ -134,7 +134,7 @@ impl ChannelChat {
|
||||
last_acknowledged_id: None,
|
||||
rng: StdRng::from_entropy(),
|
||||
first_loaded_message_id: None,
|
||||
_subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()),
|
||||
_subscription: subscription.set_model(&cx.model(), &mut cx.to_async()),
|
||||
}
|
||||
})?;
|
||||
Self::handle_loaded_messages(
|
||||
|
||||
@@ -311,7 +311,7 @@ impl ChannelStore {
|
||||
) -> Task<Result<Model<ChannelBuffer>>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let channel_store = cx.handle();
|
||||
let channel_store = cx.model();
|
||||
self.open_channel_resource(
|
||||
channel_id,
|
||||
|this| &mut this.opened_buffers,
|
||||
@@ -441,7 +441,7 @@ impl ChannelStore {
|
||||
) -> Task<Result<Model<ChannelChat>>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let this = cx.handle();
|
||||
let this = cx.model();
|
||||
self.open_channel_resource(
|
||||
channel_id,
|
||||
|this| &mut this.opened_chats,
|
||||
|
||||
@@ -25,7 +25,6 @@ anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
collections.workspace = true
|
||||
ipc-channel = "0.19"
|
||||
once_cell.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
release_channel.workspace = true
|
||||
|
||||
@@ -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![];
|
||||
@@ -242,6 +257,7 @@ fn main() -> Result<()> {
|
||||
if args.foreground {
|
||||
app.run_foreground(url)?;
|
||||
} else {
|
||||
eprintln!("Logs are written to {:?}", paths::log_file());
|
||||
app.launch(url)?;
|
||||
sender.join().unwrap()?;
|
||||
pipe_handle.join().unwrap()?;
|
||||
@@ -262,6 +278,7 @@ mod linux {
|
||||
os::unix::net::{SocketAddr, UnixDatagram},
|
||||
path::{Path, PathBuf},
|
||||
process::{self, ExitStatus},
|
||||
sync::LazyLock,
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
@@ -269,12 +286,11 @@ mod linux {
|
||||
use anyhow::anyhow;
|
||||
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
|
||||
use fork::Fork;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::{Detect, InstalledApp};
|
||||
|
||||
static RELEASE_CHANNEL: Lazy<String> =
|
||||
Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string());
|
||||
static RELEASE_CHANNEL: LazyLock<String> =
|
||||
LazyLock::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string());
|
||||
|
||||
struct App(PathBuf);
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
log.workspace = true
|
||||
once_cell.workspace = true
|
||||
paths.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
@@ -51,6 +50,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,10 +4,10 @@ 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;
|
||||
use parking_lot::Mutex;
|
||||
use release_channel::ReleaseChannel;
|
||||
use settings::{Settings, SettingsStore};
|
||||
@@ -15,11 +15,15 @@ use sha2::{Digest, Sha256};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::time::Instant;
|
||||
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
|
||||
use std::{
|
||||
env, mem,
|
||||
path::PathBuf,
|
||||
sync::{Arc, LazyLock},
|
||||
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};
|
||||
@@ -84,7 +88,7 @@ const FLUSH_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
const FLUSH_INTERVAL: Duration = Duration::from_secs(60 * 5);
|
||||
static ZED_CLIENT_CHECKSUM_SEED: Lazy<Option<Vec<u8>>> = Lazy::new(|| {
|
||||
static ZED_CLIENT_CHECKSUM_SEED: LazyLock<Option<Vec<u8>>> = LazyLock::new(|| {
|
||||
option_env!("ZED_CLIENT_CHECKSUM_SEED")
|
||||
.map(|s| s.as_bytes().into())
|
||||
.or_else(|| {
|
||||
@@ -245,7 +249,6 @@ impl Telemetry {
|
||||
})
|
||||
.detach();
|
||||
|
||||
// TODO: Replace all hardware stuff with nested SystemSpecs json
|
||||
let this = Arc::new(Self {
|
||||
clock,
|
||||
http_client: client,
|
||||
@@ -253,6 +256,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 +338,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 +353,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 +380,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 +399,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 +445,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 {
|
||||
|
||||
@@ -2,7 +2,11 @@ use collab::env::get_dotenv_vars;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
for (key, value) in get_dotenv_vars(".")? {
|
||||
println!("export {}=\"{}\"", key, value);
|
||||
if option_env!("POWERSHELL").is_some() {
|
||||
println!("$Env:{} = \"{}\"", key, value);
|
||||
} else {
|
||||
println!("export {}=\"{}\"", key, value);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use axum::{
|
||||
routing::get,
|
||||
Extension, Router,
|
||||
};
|
||||
|
||||
use collab::api::billing::sync_llm_usage_with_stripe_periodically;
|
||||
use collab::api::CloudflareIpCountryHeader;
|
||||
use collab::llm::{db::LlmDatabase, log_usage_periodically};
|
||||
|
||||
@@ -9,7 +9,7 @@ use collab_ui::channel_view::ChannelView;
|
||||
use collections::HashMap;
|
||||
use editor::{Anchor, Editor, ToOffset};
|
||||
use futures::future;
|
||||
use gpui::{BackgroundExecutor, Model, TestAppContext, ViewContext};
|
||||
use gpui::{BackgroundExecutor, Model, ModelContext, TestAppContext, Window};
|
||||
use rpc::{proto::PeerId, RECEIVE_TIMEOUT};
|
||||
use serde_json::json;
|
||||
use std::ops::Range;
|
||||
@@ -161,43 +161,43 @@ async fn test_channel_notes_participant_indices(
|
||||
|
||||
// Clients A, B, and C open the channel notes
|
||||
let channel_view_a = cx_a
|
||||
.update(|cx| ChannelView::open(channel_id, None, workspace_a.clone(), cx))
|
||||
.update(|window, cx| ChannelView::open(channel_id, None, workspace_a.clone(), window, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_view_b = cx_b
|
||||
.update(|cx| ChannelView::open(channel_id, None, workspace_b.clone(), cx))
|
||||
.update(|window, cx| ChannelView::open(channel_id, None, workspace_b.clone(), window, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_view_c = cx_c
|
||||
.update(|cx| ChannelView::open(channel_id, None, workspace_c.clone(), cx))
|
||||
.update(|window, cx| ChannelView::open(channel_id, None, workspace_c.clone(), window, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Clients A, B, and C all insert and select some text
|
||||
channel_view_a.update(cx_a, |notes, cx| {
|
||||
channel_view_a.update_in(cx_a, |notes, window, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
editor.insert("a", cx);
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
editor.insert("a", window, cx);
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
selections.select_ranges(vec![0..1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
executor.run_until_parked();
|
||||
channel_view_b.update(cx_b, |notes, cx| {
|
||||
channel_view_b.update_in(cx_b, |notes, window, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
editor.move_down(&Default::default(), cx);
|
||||
editor.insert("b", cx);
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
editor.move_down(&Default::default(), window, cx);
|
||||
editor.insert("b", window, cx);
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
selections.select_ranges(vec![1..2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
executor.run_until_parked();
|
||||
channel_view_c.update(cx_c, |notes, cx| {
|
||||
channel_view_c.update_in(cx_c, |notes, window, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
editor.move_down(&Default::default(), cx);
|
||||
editor.insert("c", cx);
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
editor.move_down(&Default::default(), window, cx);
|
||||
editor.insert("c", window, cx);
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
selections.select_ranges(vec![2..3]);
|
||||
});
|
||||
});
|
||||
@@ -206,9 +206,9 @@ async fn test_channel_notes_participant_indices(
|
||||
// Client A sees clients B and C without assigned colors, because they aren't
|
||||
// in a call together.
|
||||
executor.run_until_parked();
|
||||
channel_view_a.update(cx_a, |notes, cx| {
|
||||
channel_view_a.update_in(cx_a, |notes, window, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx);
|
||||
assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], window, cx);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -222,20 +222,22 @@ async fn test_channel_notes_participant_indices(
|
||||
// Clients A and B see each other with two different assigned colors. Client C
|
||||
// still doesn't have a color.
|
||||
executor.run_until_parked();
|
||||
channel_view_a.update(cx_a, |notes, cx| {
|
||||
channel_view_a.update_in(cx_a, |notes, window, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
assert_remote_selections(
|
||||
editor,
|
||||
&[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)],
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
channel_view_b.update(cx_b, |notes, cx| {
|
||||
channel_view_b.update_in(cx_b, |notes, window, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
assert_remote_selections(
|
||||
editor,
|
||||
&[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)],
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -252,8 +254,8 @@ async fn test_channel_notes_participant_indices(
|
||||
// Clients A and B open the same file.
|
||||
executor.start_waiting();
|
||||
let editor_a = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
|
||||
.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id_a, "file.txt"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -261,32 +263,32 @@ async fn test_channel_notes_participant_indices(
|
||||
.unwrap();
|
||||
executor.start_waiting();
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
|
||||
.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id_a, "file.txt"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
selections.select_ranges(vec![0..1]);
|
||||
});
|
||||
});
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
selections.select_ranges(vec![2..3]);
|
||||
});
|
||||
});
|
||||
executor.run_until_parked();
|
||||
|
||||
// Clients A and B see each other with the same colors as in the channel notes.
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx);
|
||||
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||
assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], window, cx);
|
||||
});
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx);
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -294,9 +296,10 @@ async fn test_channel_notes_participant_indices(
|
||||
fn assert_remote_selections(
|
||||
editor: &mut Editor,
|
||||
expected_selections: &[(Option<ParticipantIndex>, Range<usize>)],
|
||||
cx: &mut ViewContext<Editor>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Editor>,
|
||||
) {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let range = Anchor::min()..Anchor::max();
|
||||
let remote_selections = snapshot
|
||||
.remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx)
|
||||
@@ -641,9 +644,9 @@ async fn test_channel_buffer_changes(
|
||||
});
|
||||
|
||||
// Closing the buffer should re-enable change tracking
|
||||
cx_b.update(|cx| {
|
||||
cx_b.update(|window, cx| {
|
||||
workspace_b.update(cx, |workspace, cx| {
|
||||
workspace.close_all_items_and_panes(&Default::default(), cx)
|
||||
workspace.close_all_items_and_panes(&Default::default(), window, cx)
|
||||
});
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
|
||||
@@ -107,7 +107,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
|
||||
});
|
||||
assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)));
|
||||
assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx)));
|
||||
cx_b.update(|cx_b| {
|
||||
cx_b.update(|_, cx_b| {
|
||||
assert!(room_b.read_with(cx_b, |room, cx| !room.can_use_microphone(cx)));
|
||||
});
|
||||
assert!(room_b
|
||||
@@ -135,7 +135,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
|
||||
assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx)));
|
||||
|
||||
// B sees themselves as muted, and can unmute.
|
||||
cx_b.update(|cx_b| {
|
||||
cx_b.update(|_, cx_b| {
|
||||
assert!(room_b.read_with(cx_b, |room, cx| room.can_use_microphone(cx)));
|
||||
});
|
||||
room_b.read_with(cx_b, |room, _| assert!(room.is_muted()));
|
||||
|
||||
@@ -356,10 +356,10 @@ async fn test_channel_message_changes(
|
||||
let project_b = client_b.build_empty_local_project(cx_b);
|
||||
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||
|
||||
let chat_panel_b = workspace_b.update(cx_b, ChatPanel::new);
|
||||
let chat_panel_b = workspace_b.update_in(cx_b, ChatPanel::new);
|
||||
chat_panel_b
|
||||
.update(cx_b, |chat_panel, cx| {
|
||||
chat_panel.set_active(true, cx);
|
||||
.update_in(cx_b, |chat_panel, window, cx| {
|
||||
chat_panel.set_active(true, window, cx);
|
||||
chat_panel.select_channel(channel_id, None, cx)
|
||||
})
|
||||
.await
|
||||
@@ -367,7 +367,7 @@ async fn test_channel_message_changes(
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
let b_has_messages = cx_b.update(|cx| {
|
||||
let b_has_messages = cx_b.update(|_, cx| {
|
||||
client_b
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
@@ -384,7 +384,7 @@ async fn test_channel_message_changes(
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
let b_has_messages = cx_b.update(|cx| {
|
||||
let b_has_messages = cx_b.update(|_, cx| {
|
||||
client_b
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
@@ -394,8 +394,8 @@ async fn test_channel_message_changes(
|
||||
assert!(!b_has_messages);
|
||||
|
||||
// Sending a message while the chat is closed should change the flag.
|
||||
chat_panel_b.update(cx_b, |chat_panel, cx| {
|
||||
chat_panel.set_active(false, cx);
|
||||
chat_panel_b.update_in(cx_b, |chat_panel, window, cx| {
|
||||
chat_panel.set_active(false, window, cx);
|
||||
});
|
||||
|
||||
// Sending a message while the chat is open should not change the flag.
|
||||
@@ -406,7 +406,7 @@ async fn test_channel_message_changes(
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
let b_has_messages = cx_b.update(|cx| {
|
||||
let b_has_messages = cx_b.update(|_, cx| {
|
||||
client_b
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
@@ -416,7 +416,7 @@ async fn test_channel_message_changes(
|
||||
assert!(b_has_messages);
|
||||
|
||||
// Closing the chat should re-enable change tracking
|
||||
cx_b.update(|_| drop(chat_panel_b));
|
||||
cx_b.update(|_, _| drop(chat_panel_b));
|
||||
|
||||
channel_chat_a
|
||||
.update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap())
|
||||
@@ -425,7 +425,7 @@ async fn test_channel_message_changes(
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
let b_has_messages = cx_b.update(|cx| {
|
||||
let b_has_messages = cx_b.update(|_, cx| {
|
||||
client_b
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
|
||||
@@ -82,14 +82,21 @@ async fn test_host_disconnect(
|
||||
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
|
||||
|
||||
let workspace_b = cx_b
|
||||
.add_window(|cx| Workspace::new(None, project_b.clone(), client_b.app_state.clone(), cx));
|
||||
let workspace_b = cx_b.add_window(|window, cx| {
|
||||
Workspace::new(
|
||||
None,
|
||||
project_b.clone(),
|
||||
client_b.app_state.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
|
||||
let workspace_b_view = workspace_b.root_view(cx_b).unwrap();
|
||||
let workspace_b_view = workspace_b.root_model(cx_b).unwrap();
|
||||
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "b.txt"), None, true, cx)
|
||||
.update(cx_b, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "b.txt"), None, true, window, cx)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
@@ -98,10 +105,10 @@ async fn test_host_disconnect(
|
||||
.unwrap();
|
||||
|
||||
//TODO: focus
|
||||
assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx)));
|
||||
editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
|
||||
assert!(cx_b.update_window_model(&editor_b, |editor, window, _| editor.is_focused(window)));
|
||||
editor_b.update_in(cx_b, |editor, window, cx| editor.insert("X", window, cx));
|
||||
|
||||
cx_b.update(|cx| {
|
||||
cx_b.update(|_, cx| {
|
||||
assert!(workspace_b_view.read(cx).is_edited());
|
||||
});
|
||||
|
||||
@@ -121,7 +128,7 @@ async fn test_host_disconnect(
|
||||
|
||||
// Ensure client B's edited state is reset and that the whole window is blurred.
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
.update(cx_b, |workspace, _, cx| {
|
||||
assert!(workspace.active_modal::<DisconnectedOverlay>(cx).is_some());
|
||||
assert!(!workspace.is_edited());
|
||||
})
|
||||
@@ -129,8 +136,8 @@ async fn test_host_disconnect(
|
||||
|
||||
// Ensure client B is not prompted to save edits when closing window after disconnecting.
|
||||
let can_close = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.prepare_to_close(CloseIntent::Quit, cx)
|
||||
.update(cx_b, |workspace, window, cx| {
|
||||
workspace.prepare_to_close(CloseIntent::Quit, window, cx)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
@@ -201,11 +208,12 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
|
||||
.await
|
||||
.unwrap();
|
||||
let cx_a = cx_a.add_empty_window();
|
||||
let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
|
||||
let editor_a = cx_a
|
||||
.new_window_model(|window, cx| Editor::for_buffer(buffer_a, Some(project_a), window, cx));
|
||||
|
||||
let mut editor_cx_a = EditorTestContext {
|
||||
cx: cx_a.clone(),
|
||||
window: cx_a.handle(),
|
||||
window: cx_a.window_handle(),
|
||||
editor: editor_a,
|
||||
assertion_cx: AssertionContextManager::new(),
|
||||
};
|
||||
@@ -216,11 +224,12 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
|
||||
let editor_b = cx_b
|
||||
.new_window_model(|window, cx| Editor::for_buffer(buffer_b, Some(project_b), window, cx));
|
||||
|
||||
let mut editor_cx_b = EditorTestContext {
|
||||
cx: cx_b.clone(),
|
||||
window: cx_b.handle(),
|
||||
window: cx_b.window_handle(),
|
||||
editor: editor_b,
|
||||
assertion_cx: AssertionContextManager::new(),
|
||||
};
|
||||
@@ -232,8 +241,9 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
|
||||
editor_cx_b.set_selections_state(indoc! {"
|
||||
Some textˇ
|
||||
"});
|
||||
editor_cx_a
|
||||
.update_editor(|editor, cx| editor.newline_above(&editor::actions::NewlineAbove, cx));
|
||||
editor_cx_a.update_editor(|editor, window, cx| {
|
||||
editor.newline_above(&editor::actions::NewlineAbove, window, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
editor_cx_a.assert_editor_state(indoc! {"
|
||||
ˇ
|
||||
@@ -253,8 +263,9 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
|
||||
|
||||
Some textˇ
|
||||
"});
|
||||
editor_cx_a
|
||||
.update_editor(|editor, cx| editor.newline_below(&editor::actions::NewlineBelow, cx));
|
||||
editor_cx_a.update_editor(|editor, window, cx| {
|
||||
editor.newline_below(&editor::actions::NewlineBelow, window, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
editor_cx_a.assert_editor_state(indoc! {"
|
||||
|
||||
@@ -317,8 +328,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
.await
|
||||
.unwrap();
|
||||
let cx_b = cx_b.add_empty_window();
|
||||
let editor_b =
|
||||
cx_b.new_view(|cx| Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx));
|
||||
let editor_b = cx_b.new_window_model(|window, cx| {
|
||||
Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), window, cx)
|
||||
});
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
cx_a.background_executor.run_until_parked();
|
||||
@@ -328,11 +340,11 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
});
|
||||
|
||||
// Type a completion trigger character as the guest.
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
|
||||
editor.handle_input(".", cx);
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
|
||||
editor.handle_input(".", window, cx);
|
||||
});
|
||||
cx_b.focus_view(&editor_b);
|
||||
cx_b.focus(&editor_b);
|
||||
|
||||
// Receive a completion request as the host's language server.
|
||||
// Return some completions from the host's language server.
|
||||
@@ -394,9 +406,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
});
|
||||
|
||||
// Confirm a completion on the guest.
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
|
||||
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx);
|
||||
assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
|
||||
});
|
||||
|
||||
@@ -441,10 +453,10 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
|
||||
// Now we do a second completion, this time to ensure that documentation/snippets are
|
||||
// resolved
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([46..46]));
|
||||
editor.handle_input("; a", cx);
|
||||
editor.handle_input(".", cx);
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([46..46]));
|
||||
editor.handle_input("; a", window, cx);
|
||||
editor.handle_input(".", window, cx);
|
||||
});
|
||||
|
||||
buffer_b.read_with(cx_b, |buffer, _| {
|
||||
@@ -508,18 +520,18 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
|
||||
completion_response.next().await.unwrap();
|
||||
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
editor.context_menu_first(&ContextMenuFirst {}, cx);
|
||||
editor.context_menu_first(&ContextMenuFirst {}, window, cx);
|
||||
});
|
||||
|
||||
resolve_completion_response.next().await.unwrap();
|
||||
cx_b.executor().run_until_parked();
|
||||
|
||||
// When accepting the completion, the snippet is insert.
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
|
||||
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx);
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ) }"
|
||||
@@ -569,8 +581,8 @@ async fn test_collaborating_with_code_actions(
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||
.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -593,12 +605,12 @@ async fn test_collaborating_with_code_actions(
|
||||
requests.next().await;
|
||||
|
||||
// Move cursor to a location that contains code actions.
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
|
||||
});
|
||||
});
|
||||
cx_b.focus_view(&editor_b);
|
||||
cx_b.focus(&editor_b);
|
||||
|
||||
let mut requests = fake_language_server
|
||||
.handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
|
||||
@@ -658,11 +670,12 @@ async fn test_collaborating_with_code_actions(
|
||||
requests.next().await;
|
||||
|
||||
// Toggle code actions and wait for them to display.
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.toggle_code_actions(
|
||||
&ToggleCodeActions {
|
||||
deployed_from_indicator: None,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -674,8 +687,8 @@ async fn test_collaborating_with_code_actions(
|
||||
|
||||
// Confirming the code action will trigger a resolve request.
|
||||
let confirm_action = editor_b
|
||||
.update(cx_b, |editor, cx| {
|
||||
Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, cx)
|
||||
.update_in(cx_b, |editor, window, cx| {
|
||||
Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, window, cx)
|
||||
})
|
||||
.unwrap();
|
||||
fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
|
||||
@@ -726,14 +739,14 @@ async fn test_collaborating_with_code_actions(
|
||||
.downcast::<Editor>()
|
||||
.unwrap()
|
||||
});
|
||||
code_action_editor.update(cx_b, |editor, cx| {
|
||||
code_action_editor.update_in(cx_b, |editor, window, cx| {
|
||||
assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
|
||||
editor.undo(&Undo, cx);
|
||||
editor.undo(&Undo, window, cx);
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
|
||||
);
|
||||
editor.redo(&Redo, cx);
|
||||
editor.redo(&Redo, window, cx);
|
||||
assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
|
||||
});
|
||||
}
|
||||
@@ -785,8 +798,8 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
|
||||
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "one.rs"), None, true, cx)
|
||||
.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "one.rs"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -795,9 +808,9 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
|
||||
// Move cursor to a location that can be renamed.
|
||||
let prepare_rename = editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
|
||||
editor.rename(&Rename, cx).unwrap()
|
||||
let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([7..7]));
|
||||
editor.rename(&Rename, window, cx).unwrap()
|
||||
});
|
||||
|
||||
fake_language_server
|
||||
@@ -835,12 +848,12 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
});
|
||||
|
||||
// Cancel the rename, and repeat the same, but use selections instead of cursor movement
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.cancel(&editor::actions::Cancel, cx);
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.cancel(&editor::actions::Cancel, window, cx);
|
||||
});
|
||||
let prepare_rename = editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([7..8]));
|
||||
editor.rename(&Rename, cx).unwrap()
|
||||
let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([7..8]));
|
||||
editor.rename(&Rename, window, cx).unwrap()
|
||||
});
|
||||
|
||||
fake_language_server
|
||||
@@ -876,8 +889,8 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
});
|
||||
});
|
||||
|
||||
let confirm_rename = editor_b.update(cx_b, |editor, cx| {
|
||||
Editor::confirm_rename(editor, &ConfirmRename, cx).unwrap()
|
||||
let confirm_rename = editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
Editor::confirm_rename(editor, &ConfirmRename, window, cx).unwrap()
|
||||
});
|
||||
fake_language_server
|
||||
.handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
|
||||
@@ -935,17 +948,17 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
workspace.active_item_as::<Editor>(cx).unwrap()
|
||||
});
|
||||
|
||||
rename_editor.update(cx_b, |editor, cx| {
|
||||
rename_editor.update_in(cx_b, |editor, window, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
|
||||
);
|
||||
editor.undo(&Undo, cx);
|
||||
editor.undo(&Undo, window, cx);
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
|
||||
);
|
||||
editor.redo(&Redo, cx);
|
||||
editor.redo(&Redo, window, cx);
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
|
||||
@@ -953,12 +966,12 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
});
|
||||
|
||||
// Ensure temporary rename edits cannot be undone/redone.
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.undo(&Undo, cx);
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.undo(&Undo, window, cx);
|
||||
assert_eq!(editor.text(cx), "const ONE: usize = 1;");
|
||||
editor.undo(&Undo, cx);
|
||||
editor.undo(&Undo, window, cx);
|
||||
assert_eq!(editor.text(cx), "const ONE: usize = 1;");
|
||||
editor.redo(&Redo, cx);
|
||||
editor.redo(&Redo, window, cx);
|
||||
assert_eq!(editor.text(cx), "const THREE: usize = 1;");
|
||||
})
|
||||
}
|
||||
@@ -1193,7 +1206,8 @@ async fn test_share_project(
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, None, cx));
|
||||
let editor_b =
|
||||
cx_b.new_window_model(|window, cx| Editor::for_buffer(buffer_b, None, window, cx));
|
||||
|
||||
// Client A sees client B's selection
|
||||
executor.run_until_parked();
|
||||
@@ -1207,7 +1221,9 @@ async fn test_share_project(
|
||||
});
|
||||
|
||||
// Edit the buffer as client B and see that edit as client A.
|
||||
editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.handle_input("ok, ", window, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
|
||||
buffer_a.read_with(cx_a, |buffer, _| {
|
||||
@@ -1234,7 +1250,7 @@ async fn test_share_project(
|
||||
let _project_c = client_c.join_remote_project(initial_project.id, cx_c).await;
|
||||
|
||||
// Client B closes the editor, and client A sees client B's selections removed.
|
||||
cx_b.update(move |_| drop(editor_b));
|
||||
cx_b.update(move |_, _| drop(editor_b));
|
||||
executor.run_until_parked();
|
||||
|
||||
buffer_a.read_with(cx_a, |buffer, _| {
|
||||
@@ -1298,7 +1314,9 @@ async fn test_on_input_format_from_host_to_guest(
|
||||
.await
|
||||
.unwrap();
|
||||
let cx_a = cx_a.add_empty_window();
|
||||
let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx));
|
||||
let editor_a = cx_a.new_window_model(|window, cx| {
|
||||
Editor::for_buffer(buffer_a, Some(project_a.clone()), window, cx)
|
||||
});
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
executor.run_until_parked();
|
||||
@@ -1330,10 +1348,10 @@ async fn test_on_input_format_from_host_to_guest(
|
||||
.unwrap();
|
||||
|
||||
// Type a on type formatting trigger character as the guest.
|
||||
cx_a.focus_view(&editor_a);
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
|
||||
editor.handle_input(">", cx);
|
||||
cx_a.focus(&editor_a);
|
||||
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
|
||||
editor.handle_input(">", window, cx);
|
||||
});
|
||||
|
||||
executor.run_until_parked();
|
||||
@@ -1343,9 +1361,9 @@ async fn test_on_input_format_from_host_to_guest(
|
||||
});
|
||||
|
||||
// Undo should remove LSP edits first
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||
assert_eq!(editor.text(cx), "fn main() { a>~< }");
|
||||
editor.undo(&Undo, cx);
|
||||
editor.undo(&Undo, window, cx);
|
||||
assert_eq!(editor.text(cx), "fn main() { a> }");
|
||||
});
|
||||
executor.run_until_parked();
|
||||
@@ -1354,9 +1372,9 @@ async fn test_on_input_format_from_host_to_guest(
|
||||
assert_eq!(buffer.text(), "fn main() { a> }")
|
||||
});
|
||||
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||
assert_eq!(editor.text(cx), "fn main() { a> }");
|
||||
editor.undo(&Undo, cx);
|
||||
editor.undo(&Undo, window, cx);
|
||||
assert_eq!(editor.text(cx), "fn main() { a }");
|
||||
});
|
||||
executor.run_until_parked();
|
||||
@@ -1418,16 +1436,18 @@ async fn test_on_input_format_from_guest_to_host(
|
||||
.await
|
||||
.unwrap();
|
||||
let cx_b = cx_b.add_empty_window();
|
||||
let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b.clone()), cx));
|
||||
let editor_b = cx_b.new_window_model(|window, cx| {
|
||||
Editor::for_buffer(buffer_b, Some(project_b.clone()), window, cx)
|
||||
});
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
executor.run_until_parked();
|
||||
|
||||
// Type a on type formatting trigger character as the guest.
|
||||
cx_b.focus_view(&editor_b);
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
|
||||
editor.handle_input(":", cx);
|
||||
cx_b.focus(&editor_b);
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
|
||||
editor.handle_input(":", window, cx);
|
||||
});
|
||||
|
||||
// Receive an OnTypeFormatting request as the host's language server.
|
||||
@@ -1466,9 +1486,9 @@ async fn test_on_input_format_from_guest_to_host(
|
||||
});
|
||||
|
||||
// Undo should remove LSP edits first
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
assert_eq!(editor.text(cx), "fn main() { a:~: }");
|
||||
editor.undo(&Undo, cx);
|
||||
editor.undo(&Undo, window, cx);
|
||||
assert_eq!(editor.text(cx), "fn main() { a: }");
|
||||
});
|
||||
executor.run_until_parked();
|
||||
@@ -1477,9 +1497,9 @@ async fn test_on_input_format_from_guest_to_host(
|
||||
assert_eq!(buffer.text(), "fn main() { a: }")
|
||||
});
|
||||
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
assert_eq!(editor.text(cx), "fn main() { a: }");
|
||||
editor.undo(&Undo, cx);
|
||||
editor.undo(&Undo, window, cx);
|
||||
assert_eq!(editor.text(cx), "fn main() { a }");
|
||||
});
|
||||
executor.run_until_parked();
|
||||
@@ -1590,8 +1610,8 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
.await
|
||||
.unwrap();
|
||||
let editor_a = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||
.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -1646,8 +1666,8 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
});
|
||||
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||
.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -1670,11 +1690,11 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
});
|
||||
|
||||
let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
|
||||
editor.handle_input(":", cx);
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13].clone()));
|
||||
editor.handle_input(":", window, cx);
|
||||
});
|
||||
cx_b.focus_view(&editor_b);
|
||||
cx_b.focus(&editor_b);
|
||||
|
||||
executor.run_until_parked();
|
||||
editor_a.update(cx_a, |editor, _| {
|
||||
@@ -1695,11 +1715,11 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
});
|
||||
|
||||
let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
|
||||
editor.handle_input("a change to increment both buffers' versions", cx);
|
||||
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
|
||||
editor.handle_input("a change to increment both buffers' versions", window, cx);
|
||||
});
|
||||
cx_a.focus_view(&editor_a);
|
||||
cx_a.focus(&editor_a);
|
||||
|
||||
executor.run_until_parked();
|
||||
editor_a.update(cx_a, |editor, _| {
|
||||
@@ -1848,8 +1868,8 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
||||
cx_a.background_executor.start_waiting();
|
||||
|
||||
let editor_a = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||
.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -1857,8 +1877,8 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
||||
.unwrap();
|
||||
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||
.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -2042,8 +2062,8 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
// Create editor_a
|
||||
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||
let editor_a = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "file.txt"), None, true, cx)
|
||||
.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -2054,8 +2074,8 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "file.txt"), None, true, cx)
|
||||
.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -2063,9 +2083,9 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
.unwrap();
|
||||
|
||||
// client_b now requests git blame for the open buffer
|
||||
editor_b.update(cx_b, |editor_b, cx| {
|
||||
editor_b.update_in(cx_b, |editor_b, window, cx| {
|
||||
assert!(editor_b.blame().is_none());
|
||||
editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, cx);
|
||||
editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, window, cx);
|
||||
});
|
||||
|
||||
cx_a.executor().run_until_parked();
|
||||
@@ -2103,7 +2123,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
|
||||
// editor_b updates the file, which gets sent to client_a, which updates git blame,
|
||||
// which gets back to client_b.
|
||||
editor_b.update(cx_b, |editor_b, cx| {
|
||||
editor_b.update_in(cx_b, |editor_b, _, cx| {
|
||||
editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
|
||||
});
|
||||
|
||||
@@ -2130,7 +2150,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
});
|
||||
|
||||
// Now editor_a also updates the file
|
||||
editor_a.update(cx_a, |editor_a, cx| {
|
||||
editor_a.update_in(cx_a, |editor_a, _, cx| {
|
||||
editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
|
||||
});
|
||||
|
||||
@@ -2208,19 +2228,21 @@ async fn test_collaborating_with_editorconfig(
|
||||
.await
|
||||
.unwrap();
|
||||
let cx_a = cx_a.add_empty_window();
|
||||
let main_editor_a =
|
||||
cx_a.new_view(|cx| Editor::for_buffer(main_buffer_a, Some(project_a.clone()), cx));
|
||||
let other_editor_a =
|
||||
cx_a.new_view(|cx| Editor::for_buffer(other_buffer_a, Some(project_a), cx));
|
||||
let main_editor_a = cx_a.new_window_model(|window, cx| {
|
||||
Editor::for_buffer(main_buffer_a, Some(project_a.clone()), window, cx)
|
||||
});
|
||||
let other_editor_a = cx_a.new_window_model(|window, cx| {
|
||||
Editor::for_buffer(other_buffer_a, Some(project_a), window, cx)
|
||||
});
|
||||
let mut main_editor_cx_a = EditorTestContext {
|
||||
cx: cx_a.clone(),
|
||||
window: cx_a.handle(),
|
||||
window: cx_a.window_handle(),
|
||||
editor: main_editor_a,
|
||||
assertion_cx: AssertionContextManager::new(),
|
||||
};
|
||||
let mut other_editor_cx_a = EditorTestContext {
|
||||
cx: cx_a.clone(),
|
||||
window: cx_a.handle(),
|
||||
window: cx_a.window_handle(),
|
||||
editor: other_editor_a,
|
||||
assertion_cx: AssertionContextManager::new(),
|
||||
};
|
||||
@@ -2240,19 +2262,21 @@ async fn test_collaborating_with_editorconfig(
|
||||
.await
|
||||
.unwrap();
|
||||
let cx_b = cx_b.add_empty_window();
|
||||
let main_editor_b =
|
||||
cx_b.new_view(|cx| Editor::for_buffer(main_buffer_b, Some(project_b.clone()), cx));
|
||||
let other_editor_b =
|
||||
cx_b.new_view(|cx| Editor::for_buffer(other_buffer_b, Some(project_b.clone()), cx));
|
||||
let main_editor_b = cx_b.new_window_model(|window, cx| {
|
||||
Editor::for_buffer(main_buffer_b, Some(project_b.clone()), window, cx)
|
||||
});
|
||||
let other_editor_b = cx_b.new_window_model(|window, cx| {
|
||||
Editor::for_buffer(other_buffer_b, Some(project_b.clone()), window, cx)
|
||||
});
|
||||
let mut main_editor_cx_b = EditorTestContext {
|
||||
cx: cx_b.clone(),
|
||||
window: cx_b.handle(),
|
||||
window: cx_b.window_handle(),
|
||||
editor: main_editor_b,
|
||||
assertion_cx: AssertionContextManager::new(),
|
||||
};
|
||||
let mut other_editor_cx_b = EditorTestContext {
|
||||
cx: cx_b.clone(),
|
||||
window: cx_b.handle(),
|
||||
window: cx_b.window_handle(),
|
||||
editor: other_editor_b,
|
||||
assertion_cx: AssertionContextManager::new(),
|
||||
};
|
||||
@@ -2416,12 +2440,12 @@ fn tab_undo_assert(
|
||||
cx_b.assert_editor_state(expected_initial);
|
||||
|
||||
if a_tabs {
|
||||
cx_a.update_editor(|editor, cx| {
|
||||
editor.tab(&editor::actions::Tab, cx);
|
||||
cx_a.update_editor(|editor, window, cx| {
|
||||
editor.tab(&editor::actions::Tab, window, cx);
|
||||
});
|
||||
} else {
|
||||
cx_b.update_editor(|editor, cx| {
|
||||
editor.tab(&editor::actions::Tab, cx);
|
||||
cx_b.update_editor(|editor, window, cx| {
|
||||
editor.tab(&editor::actions::Tab, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2432,12 +2456,12 @@ fn tab_undo_assert(
|
||||
cx_b.assert_editor_state(expected_tabbed);
|
||||
|
||||
if a_tabs {
|
||||
cx_a.update_editor(|editor, cx| {
|
||||
editor.undo(&editor::actions::Undo, cx);
|
||||
cx_a.update_editor(|editor, window, cx| {
|
||||
editor.undo(&editor::actions::Undo, window, cx);
|
||||
});
|
||||
} else {
|
||||
cx_b.update_editor(|editor, cx| {
|
||||
editor.undo(&editor::actions::Undo, cx);
|
||||
cx_b.update_editor(|editor, window, cx| {
|
||||
editor.undo(&editor::actions::Undo, window, cx);
|
||||
});
|
||||
}
|
||||
cx_a.run_until_parked();
|
||||
|
||||
@@ -8,8 +8,8 @@ use collab_ui::{
|
||||
};
|
||||
use editor::{Editor, ExcerptRange, MultiBuffer};
|
||||
use gpui::{
|
||||
point, BackgroundExecutor, BorrowAppContext, Context, Entity, SharedString, TestAppContext,
|
||||
View, VisualContext, VisualTestContext,
|
||||
point, BackgroundExecutor, BorrowAppContext, Context, Entity, Model, SharedString,
|
||||
TestAppContext, VisualTestContext,
|
||||
};
|
||||
use language::Capability;
|
||||
use project::WorktreeSettings;
|
||||
@@ -77,23 +77,23 @@ async fn test_basic_following(
|
||||
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||
|
||||
cx_b.update(|cx| {
|
||||
assert!(cx.is_window_active());
|
||||
cx_b.update(|window, _| {
|
||||
assert!(window.is_window_active());
|
||||
});
|
||||
|
||||
// Client A opens some editors.
|
||||
let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
let editor_a1 = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
let editor_a2 = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
|
||||
.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -102,8 +102,8 @@ async fn test_basic_following(
|
||||
|
||||
// Client B opens an editor.
|
||||
let editor_b1 = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -116,22 +116,24 @@ async fn test_basic_following(
|
||||
let peer_id_d = client_d.peer_id().unwrap();
|
||||
|
||||
// Client A updates their selections in those editors
|
||||
editor_a1.update(cx_a, |editor, cx| {
|
||||
editor.handle_input("a", cx);
|
||||
editor.handle_input("b", cx);
|
||||
editor.handle_input("c", cx);
|
||||
editor.select_left(&Default::default(), cx);
|
||||
editor_a1.update_in(cx_a, |editor, window, cx| {
|
||||
editor.handle_input("a", window, cx);
|
||||
editor.handle_input("b", window, cx);
|
||||
editor.handle_input("c", window, cx);
|
||||
editor.select_left(&Default::default(), window, cx);
|
||||
assert_eq!(editor.selections.ranges(cx), vec![3..2]);
|
||||
});
|
||||
editor_a2.update(cx_a, |editor, cx| {
|
||||
editor.handle_input("d", cx);
|
||||
editor.handle_input("e", cx);
|
||||
editor.select_left(&Default::default(), cx);
|
||||
editor_a2.update_in(cx_a, |editor, window, cx| {
|
||||
editor.handle_input("d", window, cx);
|
||||
editor.handle_input("e", window, cx);
|
||||
editor.select_left(&Default::default(), window, cx);
|
||||
assert_eq!(editor.selections.ranges(cx), vec![2..1]);
|
||||
});
|
||||
|
||||
// When client B starts following client A, only the active view state is replicated to client B.
|
||||
workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx));
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.follow(peer_id_a, window, cx)
|
||||
});
|
||||
|
||||
cx_c.executor().run_until_parked();
|
||||
let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
|
||||
@@ -165,7 +167,9 @@ async fn test_basic_following(
|
||||
drop(project_c);
|
||||
|
||||
// Client C also follows client A.
|
||||
workspace_c.update(cx_c, |workspace, cx| workspace.follow(peer_id_a, cx));
|
||||
workspace_c.update_in(cx_c, |workspace, window, cx| {
|
||||
workspace.follow(peer_id_a, window, cx)
|
||||
});
|
||||
|
||||
cx_d.executor().run_until_parked();
|
||||
let active_call_d = cx_d.read(ActiveCall::global);
|
||||
@@ -188,8 +192,8 @@ async fn test_basic_following(
|
||||
}
|
||||
|
||||
// Client C unfollows client A.
|
||||
workspace_c.update(cx_c, |workspace, cx| {
|
||||
workspace.unfollow(peer_id_a, cx).unwrap();
|
||||
workspace_c.update_in(cx_c, |workspace, window, cx| {
|
||||
workspace.unfollow(peer_id_a, window, cx).unwrap();
|
||||
});
|
||||
|
||||
// All clients see that clients B is following client A.
|
||||
@@ -203,7 +207,9 @@ async fn test_basic_following(
|
||||
}
|
||||
|
||||
// Client C re-follows client A.
|
||||
workspace_c.update(cx_c, |workspace, cx| workspace.follow(peer_id_a, cx));
|
||||
workspace_c.update_in(cx_c, |workspace, window, cx| {
|
||||
workspace.follow(peer_id_a, window, cx)
|
||||
});
|
||||
|
||||
// All clients see that clients B and C are following client A.
|
||||
cx_c.executor().run_until_parked();
|
||||
@@ -216,9 +222,13 @@ async fn test_basic_following(
|
||||
}
|
||||
|
||||
// Client D follows client B, then switches to following client C.
|
||||
workspace_d.update(cx_d, |workspace, cx| workspace.follow(peer_id_b, cx));
|
||||
workspace_d.update_in(cx_d, |workspace, window, cx| {
|
||||
workspace.follow(peer_id_b, window, cx)
|
||||
});
|
||||
cx_a.executor().run_until_parked();
|
||||
workspace_d.update(cx_d, |workspace, cx| workspace.follow(peer_id_c, cx));
|
||||
workspace_d.update_in(cx_d, |workspace, window, cx| {
|
||||
workspace.follow(peer_id_c, window, cx)
|
||||
});
|
||||
|
||||
// All clients see that D is following C
|
||||
cx_a.executor().run_until_parked();
|
||||
@@ -235,8 +245,8 @@ async fn test_basic_following(
|
||||
|
||||
// Client C closes the project.
|
||||
let weak_workspace_c = workspace_c.downgrade();
|
||||
workspace_c.update(cx_c, |workspace, cx| {
|
||||
workspace.close_window(&Default::default(), cx);
|
||||
workspace_c.update_in(cx_c, |workspace, window, cx| {
|
||||
workspace.close_window(&Default::default(), window, cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
// are you sure you want to leave the call?
|
||||
@@ -260,8 +270,8 @@ async fn test_basic_following(
|
||||
}
|
||||
|
||||
// When client A activates a different editor, client B does so as well.
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.activate_item(&editor_a1, true, true, cx)
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.activate_item(&editor_a1, true, true, window, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
@@ -302,11 +312,11 @@ async fn test_basic_following(
|
||||
);
|
||||
result
|
||||
});
|
||||
let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
|
||||
let editor = cx.new_view(|cx| {
|
||||
Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), true, cx)
|
||||
let multibuffer_editor_a = workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
let editor = cx.new_model(|cx| {
|
||||
Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), true, window, cx)
|
||||
});
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx);
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
|
||||
editor
|
||||
});
|
||||
executor.run_until_parked();
|
||||
@@ -324,8 +334,8 @@ async fn test_basic_following(
|
||||
|
||||
// When client A navigates back and forth, client B does so as well.
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.go_back(workspace.active_pane().downgrade(), window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -338,8 +348,8 @@ async fn test_basic_following(
|
||||
});
|
||||
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.go_back(workspace.active_pane().downgrade(), window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -352,8 +362,8 @@ async fn test_basic_following(
|
||||
});
|
||||
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.go_forward(workspace.active_pane().downgrade(), cx)
|
||||
.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.go_forward(workspace.active_pane().downgrade(), window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -366,8 +376,8 @@ async fn test_basic_following(
|
||||
});
|
||||
|
||||
// Changes to client A's editor are reflected on client B.
|
||||
editor_a1.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
|
||||
editor_a1.update_in(cx_a, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2]));
|
||||
});
|
||||
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
|
||||
executor.run_until_parked();
|
||||
@@ -377,13 +387,15 @@ async fn test_basic_following(
|
||||
assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
|
||||
});
|
||||
|
||||
editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
|
||||
editor_a1.update_in(cx_a, |editor, window, cx| {
|
||||
editor.set_text("TWO", window, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
|
||||
|
||||
editor_a1.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
|
||||
editor.set_scroll_position(point(0., 100.), cx);
|
||||
editor_a1.update_in(cx_a, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
|
||||
editor.set_scroll_position(point(0., 100.), window, cx);
|
||||
});
|
||||
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
|
||||
executor.run_until_parked();
|
||||
@@ -392,11 +404,11 @@ async fn test_basic_following(
|
||||
});
|
||||
|
||||
// After unfollowing, client B stops receiving updates from client A.
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.unfollow(peer_id_a, cx).unwrap()
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.unfollow(peer_id_a, window, cx).unwrap()
|
||||
});
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.activate_item(&editor_a2, true, true, cx)
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.activate_item(&editor_a2, true, true, window, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
@@ -408,14 +420,16 @@ async fn test_basic_following(
|
||||
);
|
||||
|
||||
// Client A starts following client B.
|
||||
workspace_a.update(cx_a, |workspace, cx| workspace.follow(peer_id_b, cx));
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.follow(peer_id_b, window, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
Some(peer_id_b)
|
||||
);
|
||||
assert_eq!(
|
||||
workspace_a.update(cx_a, |workspace, cx| workspace
|
||||
workspace_a.update_in(cx_a, |workspace, _, cx| workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.item_id()),
|
||||
@@ -593,19 +607,19 @@ async fn test_following_tab_order(
|
||||
|
||||
//Open 1, 3 in that order on client A
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "3.txt"), None, true, cx)
|
||||
.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "3.txt"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let pane_paths = |pane: &View<workspace::Pane>, cx: &mut VisualTestContext| {
|
||||
let pane_paths = |pane: &Model<workspace::Pane>, cx: &mut VisualTestContext| {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.items()
|
||||
.map(|item| {
|
||||
@@ -624,13 +638,15 @@ async fn test_following_tab_order(
|
||||
assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
|
||||
|
||||
//Follow client B as client A
|
||||
workspace_a.update(cx_a, |workspace, cx| workspace.follow(client_b_id, cx));
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.follow(client_b_id, window, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
|
||||
//Open just 2 on client B
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
|
||||
.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -641,8 +657,8 @@ async fn test_following_tab_order(
|
||||
|
||||
//Open just 1 on client B
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -701,8 +717,8 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
// Client A opens a file.
|
||||
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -712,8 +728,8 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
// Client B opens a different file.
|
||||
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
|
||||
.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -721,24 +737,38 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
.unwrap();
|
||||
|
||||
// Clients A and B follow each other in split panes
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.split_and_clone(
|
||||
workspace.active_pane().clone(),
|
||||
SplitDirection::Right,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.follow(client_b.peer_id().unwrap(), cx)
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.follow(client_b.peer_id().unwrap(), window, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.split_and_clone(
|
||||
workspace.active_pane().clone(),
|
||||
SplitDirection::Right,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.follow(client_a.peer_id().unwrap(), cx)
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.follow(client_a.peer_id().unwrap(), window, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
|
||||
// Clients A and B return focus to the original files they had open
|
||||
workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
|
||||
workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.activate_next_pane(window, cx)
|
||||
});
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.activate_next_pane(window, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
|
||||
// Both clients see the other client's focused file in their right pane.
|
||||
@@ -775,15 +805,15 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
|
||||
// Clients A and B each open a new file.
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "3.txt"), None, true, cx)
|
||||
.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "3.txt"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "4.txt"), None, true, cx)
|
||||
.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "4.txt"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -831,7 +861,9 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
);
|
||||
|
||||
// Client A focuses their right pane, in which they're following client B.
|
||||
workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.activate_next_pane(window, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
|
||||
// Client B sees that client A is now looking at the same file as them.
|
||||
@@ -877,7 +909,9 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
|
||||
// Client B focuses their right pane, in which they're following client A,
|
||||
// who is following them.
|
||||
workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.activate_next_pane(window, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
|
||||
// Client A sees that client B is now looking at the same file as them.
|
||||
@@ -923,9 +957,9 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
|
||||
// Client B focuses a file that they previously followed A to, breaking
|
||||
// the follow.
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, cx);
|
||||
pane.activate_prev_item(true, window, cx);
|
||||
});
|
||||
});
|
||||
executor.run_until_parked();
|
||||
@@ -974,9 +1008,9 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
|
||||
// Client B closes tabs, some of which were originally opened by client A,
|
||||
// and some of which were originally opened by client B.
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.close_inactive_items(&Default::default(), cx)
|
||||
pane.close_inactive_items(&Default::default(), window, cx)
|
||||
.unwrap()
|
||||
.detach();
|
||||
});
|
||||
@@ -1022,14 +1056,14 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
);
|
||||
|
||||
// Client B follows client A again.
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.follow(client_a.peer_id().unwrap(), cx)
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.follow(client_a.peer_id().unwrap(), window, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
// Client A cycles through some tabs.
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, cx);
|
||||
pane.activate_prev_item(true, window, cx);
|
||||
});
|
||||
});
|
||||
executor.run_until_parked();
|
||||
@@ -1071,9 +1105,9 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
]
|
||||
);
|
||||
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, cx);
|
||||
pane.activate_prev_item(true, window, cx);
|
||||
});
|
||||
});
|
||||
executor.run_until_parked();
|
||||
@@ -1118,9 +1152,9 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
]
|
||||
);
|
||||
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, cx);
|
||||
pane.activate_prev_item(true, window, cx);
|
||||
});
|
||||
});
|
||||
executor.run_until_parked();
|
||||
@@ -1215,8 +1249,8 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
||||
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||
|
||||
let _editor_a1 = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -1228,7 +1262,9 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
||||
let leader_id = project_b.update(cx_b, |project, _| {
|
||||
project.collaborators().values().next().unwrap().peer_id
|
||||
});
|
||||
workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx));
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.follow(leader_id, window, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
@@ -1243,15 +1279,17 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
||||
});
|
||||
|
||||
// When client B moves, it automatically stops following client A.
|
||||
editor_b2.update(cx_b, |editor, cx| {
|
||||
editor.move_right(&editor::actions::MoveRight, cx)
|
||||
editor_b2.update_in(cx_b, |editor, window, cx| {
|
||||
editor.move_right(&editor::actions::MoveRight, window, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
None
|
||||
);
|
||||
|
||||
workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx));
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.follow(leader_id, window, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
@@ -1259,13 +1297,15 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
||||
);
|
||||
|
||||
// When client B edits, it automatically stops following client A.
|
||||
editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
|
||||
editor_b2.update_in(cx_b, |editor, window, cx| editor.insert("X", window, cx));
|
||||
assert_eq!(
|
||||
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
workspace_b.update_in(cx_b, |workspace, _, _| workspace.leader_for_pane(&pane_b)),
|
||||
None
|
||||
);
|
||||
|
||||
workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx));
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.follow(leader_id, window, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
@@ -1273,15 +1313,17 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
||||
);
|
||||
|
||||
// When client B scrolls, it automatically stops following client A.
|
||||
editor_b2.update(cx_b, |editor, cx| {
|
||||
editor.set_scroll_position(point(0., 3.), cx)
|
||||
editor_b2.update_in(cx_b, |editor, window, cx| {
|
||||
editor.set_scroll_position(point(0., 3.), window, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
None
|
||||
);
|
||||
|
||||
workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx));
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.follow(leader_id, window, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
@@ -1289,15 +1331,17 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
||||
);
|
||||
|
||||
// When client B activates a different pane, it continues following client A in the original pane.
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, window, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
Some(leader_id)
|
||||
);
|
||||
|
||||
workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.activate_next_pane(window, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
Some(leader_id)
|
||||
@@ -1305,8 +1349,8 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
||||
|
||||
// When client B activates a different item in the original pane, it automatically stops following client A.
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
|
||||
.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1352,8 +1396,12 @@ async fn test_peers_simultaneously_following_each_other(
|
||||
project.collaborators().values().next().unwrap().peer_id
|
||||
});
|
||||
|
||||
workspace_a.update(cx_a, |workspace, cx| workspace.follow(client_b_id, cx));
|
||||
workspace_b.update(cx_b, |workspace, cx| workspace.follow(client_a_id, cx));
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.follow(client_b_id, window, cx)
|
||||
});
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.follow(client_a_id, window, cx)
|
||||
});
|
||||
executor.run_until_parked();
|
||||
|
||||
workspace_a.update(cx_a, |workspace, _| {
|
||||
@@ -1434,8 +1482,8 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
.unwrap();
|
||||
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id_a, "w.rs"), None, true, cx)
|
||||
.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id_a, "w.rs"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1443,8 +1491,8 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
executor.run_until_parked();
|
||||
assert_eq!(visible_push_notifications(cx_b).len(), 1);
|
||||
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.follow(client_a.peer_id().unwrap(), cx)
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.follow(client_a.peer_id().unwrap(), window, cx)
|
||||
});
|
||||
|
||||
executor.run_until_parked();
|
||||
@@ -1490,8 +1538,8 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
|
||||
// b moves to x.rs in a's project, and a follows
|
||||
workspace_b_project_a
|
||||
.update(&mut cx_b2, |workspace, cx| {
|
||||
workspace.open_path((worktree_id_a, "x.rs"), None, true, cx)
|
||||
.update_in(&mut cx_b2, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id_a, "x.rs"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1505,8 +1553,8 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
);
|
||||
});
|
||||
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.follow(client_b.peer_id().unwrap(), cx)
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.follow(client_b.peer_id().unwrap(), window, cx)
|
||||
});
|
||||
|
||||
executor.run_until_parked();
|
||||
@@ -1522,8 +1570,8 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
|
||||
// b moves to y.rs in b's project, a is still following but can't yet see
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id_b, "y.rs"), None, true, cx)
|
||||
.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id_b, "y.rs"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1544,7 +1592,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
|
||||
executor.run_until_parked();
|
||||
assert_eq!(visible_push_notifications(cx_a).len(), 1);
|
||||
cx_a.update(|cx| {
|
||||
cx_a.update(|_, cx| {
|
||||
workspace::join_in_room_project(
|
||||
project_b_id,
|
||||
client_b.user_id().unwrap(),
|
||||
@@ -1607,8 +1655,8 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
});
|
||||
|
||||
// b should follow a to position 1
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([1..1]))
|
||||
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]))
|
||||
});
|
||||
cx_a.executor()
|
||||
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
|
||||
@@ -1618,7 +1666,7 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
});
|
||||
|
||||
// a unshares the project
|
||||
cx_a.update(|cx| {
|
||||
cx_a.update(|_, cx| {
|
||||
let project = workspace_a.read(cx).project().clone();
|
||||
ActiveCall::global(cx).update(cx, |call, cx| {
|
||||
call.unshare_project(project, cx).unwrap();
|
||||
@@ -1627,8 +1675,8 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
cx_a.run_until_parked();
|
||||
|
||||
// b should not follow a to position 2
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([2..2]))
|
||||
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]))
|
||||
});
|
||||
cx_a.executor()
|
||||
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
|
||||
@@ -1636,7 +1684,7 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert_eq!(editor.selections.ranges(cx), vec![1..1])
|
||||
});
|
||||
cx_b.update(|cx| {
|
||||
cx_b.update(|_, cx| {
|
||||
let room = ActiveCall::global(cx).read(cx).room().unwrap().read(cx);
|
||||
let participant = room.remote_participants().get(&client_a.id()).unwrap();
|
||||
assert_eq!(participant.location, ParticipantLocation::UnsharedProject)
|
||||
@@ -1703,16 +1751,16 @@ async fn test_following_into_excluded_file(
|
||||
|
||||
// Client A opens editors for a regular file and an excluded file.
|
||||
let editor_for_regular = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
let editor_for_excluded_a = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
|
||||
.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -1720,22 +1768,24 @@ async fn test_following_into_excluded_file(
|
||||
.unwrap();
|
||||
|
||||
// Client A updates their selections in those editors
|
||||
editor_for_regular.update(cx_a, |editor, cx| {
|
||||
editor.handle_input("a", cx);
|
||||
editor.handle_input("b", cx);
|
||||
editor.handle_input("c", cx);
|
||||
editor.select_left(&Default::default(), cx);
|
||||
editor_for_regular.update_in(cx_a, |editor, window, cx| {
|
||||
editor.handle_input("a", window, cx);
|
||||
editor.handle_input("b", window, cx);
|
||||
editor.handle_input("c", window, cx);
|
||||
editor.select_left(&Default::default(), window, cx);
|
||||
assert_eq!(editor.selections.ranges(cx), vec![3..2]);
|
||||
});
|
||||
editor_for_excluded_a.update(cx_a, |editor, cx| {
|
||||
editor.select_all(&Default::default(), cx);
|
||||
editor.handle_input("new commit message", cx);
|
||||
editor.select_left(&Default::default(), cx);
|
||||
editor_for_excluded_a.update_in(cx_a, |editor, window, cx| {
|
||||
editor.select_all(&Default::default(), window, cx);
|
||||
editor.handle_input("new commit message", window, cx);
|
||||
editor.select_left(&Default::default(), window, cx);
|
||||
assert_eq!(editor.selections.ranges(cx), vec![18..17]);
|
||||
});
|
||||
|
||||
// When client B starts following client A, currently visible file is replicated
|
||||
workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx));
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.follow(peer_id_a, window, cx)
|
||||
});
|
||||
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
|
||||
executor.run_until_parked();
|
||||
|
||||
@@ -1755,15 +1805,15 @@ async fn test_following_into_excluded_file(
|
||||
vec![18..17]
|
||||
);
|
||||
|
||||
editor_for_excluded_a.update(cx_a, |editor, cx| {
|
||||
editor.select_right(&Default::default(), cx);
|
||||
editor_for_excluded_a.update_in(cx_a, |editor, window, cx| {
|
||||
editor.select_right(&Default::default(), window, cx);
|
||||
});
|
||||
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
|
||||
executor.run_until_parked();
|
||||
|
||||
// Changes from B to the excluded file are replicated in A's editor
|
||||
editor_for_excluded_b.update(cx_b, |editor, cx| {
|
||||
editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx);
|
||||
editor_for_excluded_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.handle_input("\nCo-Authored-By: B <b@b.b>", window, cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
editor_for_excluded_a.update(cx_a, |editor, cx| {
|
||||
@@ -1774,13 +1824,11 @@ async fn test_following_into_excluded_file(
|
||||
});
|
||||
}
|
||||
|
||||
fn visible_push_notifications(
|
||||
cx: &mut TestAppContext,
|
||||
) -> Vec<gpui::View<ProjectSharedNotification>> {
|
||||
fn visible_push_notifications(cx: &mut TestAppContext) -> Vec<Model<ProjectSharedNotification>> {
|
||||
let mut ret = Vec::new();
|
||||
for window in cx.windows() {
|
||||
window
|
||||
.update(cx, |window, _| {
|
||||
.update(cx, |window, _, _| {
|
||||
if let Ok(handle) = window.downcast::<ProjectSharedNotification>() {
|
||||
ret.push(handle)
|
||||
}
|
||||
@@ -1821,7 +1869,7 @@ fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec
|
||||
})
|
||||
}
|
||||
|
||||
fn pane_summaries(workspace: &View<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
|
||||
fn pane_summaries(workspace: &Model<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let active_pane = workspace.active_pane();
|
||||
workspace
|
||||
@@ -1924,14 +1972,14 @@ async fn test_following_to_channel_notes_without_a_shared_project(
|
||||
|
||||
// Client A opens the notes for channel 1.
|
||||
let channel_notes_1_a = cx_a
|
||||
.update(|cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), cx))
|
||||
.update(|window, cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), window, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
channel_notes_1_a.update(cx_a, |notes, cx| {
|
||||
channel_notes_1_a.update_in(cx_a, |notes, window, cx| {
|
||||
assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
editor.insert("Hello from A.", cx);
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
editor.insert("Hello from A.", window, cx);
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
selections.select_ranges(vec![3..4]);
|
||||
});
|
||||
});
|
||||
@@ -1939,9 +1987,9 @@ async fn test_following_to_channel_notes_without_a_shared_project(
|
||||
|
||||
// Client B follows client A.
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace
|
||||
.start_following(client_a.peer_id().unwrap(), cx)
|
||||
.start_following(client_a.peer_id().unwrap(), window, cx)
|
||||
.unwrap()
|
||||
})
|
||||
.await
|
||||
@@ -1971,7 +2019,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
|
||||
|
||||
// Client A opens the notes for channel 2.
|
||||
let channel_notes_2_a = cx_a
|
||||
.update(|cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), cx))
|
||||
.update(|window, cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), window, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
channel_notes_2_a.update(cx_a, |notes, cx| {
|
||||
@@ -1997,8 +2045,8 @@ async fn test_following_to_channel_notes_without_a_shared_project(
|
||||
|
||||
// Client A opens a local buffer in their unshared project.
|
||||
let _unshared_editor_a1 = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -2027,7 +2075,7 @@ pub(crate) async fn join_channel(
|
||||
}
|
||||
|
||||
async fn share_workspace(
|
||||
workspace: &View<Workspace>,
|
||||
workspace: &Model<Workspace>,
|
||||
cx: &mut VisualTestContext,
|
||||
) -> anyhow::Result<u64> {
|
||||
let project = workspace.update(cx, |workspace, _| workspace.project().clone());
|
||||
@@ -2069,9 +2117,9 @@ async fn test_following_to_channel_notes_other_workspace(
|
||||
|
||||
// a opens a second workspace and the channel notes
|
||||
let (workspace_a2, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
|
||||
cx_a2.update(|cx| cx.activate_window());
|
||||
cx_a2.update(|window, _| window.activate_window());
|
||||
cx_a2
|
||||
.update(|cx| ChannelView::open(channel, None, workspace_a2, cx))
|
||||
.update(|window, cx| ChannelView::open(channel, None, workspace_a2, window, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a2.run_until_parked();
|
||||
@@ -2083,7 +2131,7 @@ async fn test_following_to_channel_notes_other_workspace(
|
||||
});
|
||||
|
||||
// a returns to the shared project
|
||||
cx_a.update(|cx| cx.activate_window());
|
||||
cx_a.update(|window, _| window.activate_window());
|
||||
cx_a.run_until_parked();
|
||||
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
@@ -2141,7 +2189,7 @@ async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
|
||||
// a opens a file in a new window
|
||||
let (_, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
|
||||
cx_a2.update(|cx| cx.activate_window());
|
||||
cx_a2.update(|window, _| window.activate_window());
|
||||
cx_a2.simulate_keystrokes("cmd-p");
|
||||
cx_a2.run_until_parked();
|
||||
cx_a2.simulate_keystrokes("3 enter");
|
||||
@@ -2152,7 +2200,7 @@ async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
cx_a.run_until_parked();
|
||||
|
||||
// a returns to the shared project
|
||||
cx_a.update(|cx| cx.activate_window());
|
||||
cx_a.update(|window, _| window.activate_window());
|
||||
cx_a.run_until_parked();
|
||||
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
|
||||
@@ -6174,7 +6174,7 @@ async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) {
|
||||
cx.simulate_resize(size(px(300.), px(300.)));
|
||||
|
||||
cx.simulate_keystrokes("cmd-n cmd-n cmd-n");
|
||||
cx.update(|cx| cx.refresh());
|
||||
cx.update(|window, _cx| window.refresh());
|
||||
|
||||
let tab_bounds = cx.debug_bounds("TAB-2").unwrap();
|
||||
let new_tab_button_bounds = cx.debug_bounds("ICON-Plus").unwrap();
|
||||
@@ -6273,8 +6273,8 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
|
||||
// Opening item 3 as a "permanent" tab
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_path(path_3.clone(), None, false, cx)
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
workspace.open_path(path_3.clone(), None, false, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -6290,8 +6290,8 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
|
||||
// Open item 1 as preview
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_path_preview(path_1.clone(), None, true, true, cx)
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
workspace.open_path_preview(path_1.clone(), None, true, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -6311,8 +6311,8 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
|
||||
// Open item 2 as preview
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_path_preview(path_2.clone(), None, true, true, cx)
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
workspace.open_path_preview(path_2.clone(), None, true, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -6332,7 +6332,9 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
|
||||
// Going back should show item 1 as preview
|
||||
workspace
|
||||
.update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
workspace.go_back(pane.downgrade(), window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -6350,10 +6352,11 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
// Closing item 1
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
pane.close_item_by_id(
|
||||
pane.active_item().unwrap().item_id(),
|
||||
workspace::SaveIntent::Skip,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -6371,7 +6374,9 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
|
||||
// Going back should show item 1 as preview
|
||||
workspace
|
||||
.update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
workspace.go_back(pane.downgrade(), window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -6389,9 +6394,9 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
// Close permanent tab
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
let id = pane.items().next().unwrap().item_id();
|
||||
pane.close_item_by_id(id, workspace::SaveIntent::Skip, cx)
|
||||
pane.close_item_by_id(id, workspace::SaveIntent::Skip, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -6438,8 +6443,8 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
|
||||
// Open item 2 as preview in right pane
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_path_preview(path_2.clone(), None, true, true, cx)
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
workspace.open_path_preview(path_2.clone(), None, true, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -6470,14 +6475,14 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
// Focus left pane
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.activate_pane_in_direction(workspace::SplitDirection::Left, cx)
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
workspace.activate_pane_in_direction(workspace::SplitDirection::Left, window, cx)
|
||||
});
|
||||
|
||||
// Open item 2 as preview in left pane
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_path_preview(path_2.clone(), None, true, true, cx)
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
workspace.open_path_preview(path_2.clone(), None, true, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -21,14 +21,14 @@ async fn test_notifications(
|
||||
let notification_events_b = Arc::new(Mutex::new(Vec::new()));
|
||||
client_a.notification_store().update(cx_a, |_, cx| {
|
||||
let events = notification_events_a.clone();
|
||||
cx.subscribe(&cx.handle(), move |_, _, event, _| {
|
||||
cx.subscribe(&cx.model(), move |_, _, event, _| {
|
||||
events.lock().push(event.clone());
|
||||
})
|
||||
.detach()
|
||||
});
|
||||
client_b.notification_store().update(cx_b, |_, cx| {
|
||||
let events = notification_events_b.clone();
|
||||
cx.subscribe(&cx.handle(), move |_, _, event, _| {
|
||||
cx.subscribe(&cx.model(), move |_, _, event, _| {
|
||||
events.lock().push(event.clone());
|
||||
})
|
||||
.detach()
|
||||
|
||||
@@ -17,7 +17,7 @@ use collections::{HashMap, HashSet};
|
||||
use fs::FakeFs;
|
||||
use futures::{channel::oneshot, StreamExt as _};
|
||||
use git::GitHostingProviderRegistry;
|
||||
use gpui::{BackgroundExecutor, Context, Model, Task, TestAppContext, View, VisualTestContext};
|
||||
use gpui::{BackgroundExecutor, Context, Model, Task, TestAppContext, VisualTestContext};
|
||||
use http_client::FakeHttpClient;
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::NodeRuntime;
|
||||
@@ -752,17 +752,17 @@ impl TestClient {
|
||||
|
||||
pub async fn host_workspace(
|
||||
&self,
|
||||
workspace: &View<Workspace>,
|
||||
workspace: &Model<Workspace>,
|
||||
channel_id: ChannelId,
|
||||
cx: &mut VisualTestContext,
|
||||
) {
|
||||
cx.update(|cx| {
|
||||
cx.update(|_, cx| {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
active_call.update(cx, |call, cx| call.join_channel(channel_id, cx))
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| {
|
||||
cx.update(|_, cx| {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
let project = workspace.read(cx).project().clone();
|
||||
active_call.update(cx, |call, cx| call.share_project(project, cx))
|
||||
@@ -776,7 +776,7 @@ impl TestClient {
|
||||
&'a self,
|
||||
channel_id: ChannelId,
|
||||
cx: &'a mut TestAppContext,
|
||||
) -> (View<Workspace>, &'a mut VisualTestContext) {
|
||||
) -> (Model<Workspace>, &'a mut VisualTestContext) {
|
||||
cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -822,31 +822,31 @@ impl TestClient {
|
||||
&'a self,
|
||||
project: &Model<Project>,
|
||||
cx: &'a mut TestAppContext,
|
||||
) -> (View<Workspace>, &'a mut VisualTestContext) {
|
||||
cx.add_window_view(|cx| {
|
||||
cx.activate_window();
|
||||
Workspace::new(None, project.clone(), self.app_state.clone(), cx)
|
||||
) -> (Model<Workspace>, &'a mut VisualTestContext) {
|
||||
cx.add_window_view(|window, cx| {
|
||||
window.activate_window();
|
||||
Workspace::new(None, project.clone(), self.app_state.clone(), window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn build_test_workspace<'a>(
|
||||
&'a self,
|
||||
cx: &'a mut TestAppContext,
|
||||
) -> (View<Workspace>, &'a mut VisualTestContext) {
|
||||
) -> (Model<Workspace>, &'a mut VisualTestContext) {
|
||||
let project = self.build_test_project(cx).await;
|
||||
cx.add_window_view(|cx| {
|
||||
cx.activate_window();
|
||||
Workspace::new(None, project.clone(), self.app_state.clone(), cx)
|
||||
cx.add_window_view(|window, cx| {
|
||||
window.activate_window();
|
||||
Workspace::new(None, project.clone(), self.app_state.clone(), window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn active_workspace<'a>(
|
||||
&'a self,
|
||||
cx: &'a mut TestAppContext,
|
||||
) -> (View<Workspace>, &'a mut VisualTestContext) {
|
||||
) -> (Model<Workspace>, &'a mut VisualTestContext) {
|
||||
let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
|
||||
|
||||
let view = window.root_view(cx).unwrap();
|
||||
let view = window.root_model(cx).unwrap();
|
||||
let cx = VisualTestContext::from_window(*window.deref(), cx).as_mut();
|
||||
// it might be nice to try and cleanup these at the end of each test.
|
||||
(view, cx)
|
||||
@@ -856,11 +856,11 @@ impl TestClient {
|
||||
pub fn open_channel_notes(
|
||||
channel_id: ChannelId,
|
||||
cx: &mut VisualTestContext,
|
||||
) -> Task<anyhow::Result<View<ChannelView>>> {
|
||||
let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
|
||||
let view = window.root_view(cx).unwrap();
|
||||
) -> Task<anyhow::Result<Model<ChannelView>>> {
|
||||
let window = cx.update(|_, cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
|
||||
let view = window.root_model(cx).unwrap();
|
||||
|
||||
cx.update(|cx| ChannelView::open(channel_id, None, view.clone(), cx))
|
||||
cx.update(|window, cx| ChannelView::open(channel_id, None, view.clone(), window, cx))
|
||||
}
|
||||
|
||||
impl Drop for TestClient {
|
||||
|
||||
@@ -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},
|
||||
@@ -11,9 +11,8 @@ use editor::{
|
||||
EditorEvent,
|
||||
};
|
||||
use gpui::{
|
||||
actions, AnyView, AppContext, ClipboardItem, Entity as _, EventEmitter, FocusableView, Model,
|
||||
Pixels, Point, Render, Subscription, Task, View, ViewContext, VisualContext as _, WeakView,
|
||||
WindowContext,
|
||||
actions, AnyView, AppContext, ClipboardItem, Entity as _, EventEmitter, Focusable, Model,
|
||||
ModelContext, Pixels, Point, Render, Subscription, Task, VisualContext as _, WeakModel, Window,
|
||||
};
|
||||
use project::Project;
|
||||
use rpc::proto::ChannelVisibility;
|
||||
@@ -38,8 +37,8 @@ pub fn init(cx: &mut AppContext) {
|
||||
}
|
||||
|
||||
pub struct ChannelView {
|
||||
pub editor: View<Editor>,
|
||||
workspace: WeakView<Workspace>,
|
||||
pub editor: Model<Editor>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
project: Model<Project>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_buffer: Model<ChannelBuffer>,
|
||||
@@ -52,27 +51,31 @@ impl ChannelView {
|
||||
pub fn open(
|
||||
channel_id: ChannelId,
|
||||
link_position: Option<String>,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
workspace: Model<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Model<Self>>> {
|
||||
let pane = workspace.read(cx).active_pane().clone();
|
||||
let channel_view = Self::open_in_pane(
|
||||
channel_id,
|
||||
link_position,
|
||||
pane.clone(),
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
cx.spawn(|mut cx| async move {
|
||||
window.spawn(cx, |mut cx| async move {
|
||||
let channel_view = channel_view.await?;
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
report_call_event_for_channel(
|
||||
"open channel notes",
|
||||
pane.update_in(&mut cx, |pane, window, cx| {
|
||||
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);
|
||||
pane.add_item(Box::new(channel_view.clone()), true, true, None, window, cx);
|
||||
})?;
|
||||
anyhow::Ok(channel_view)
|
||||
})
|
||||
@@ -81,15 +84,16 @@ impl ChannelView {
|
||||
pub fn open_in_pane(
|
||||
channel_id: ChannelId,
|
||||
link_position: Option<String>,
|
||||
pane: View<Pane>,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
let channel_view = Self::load(channel_id, workspace, cx);
|
||||
cx.spawn(|mut cx| async move {
|
||||
pane: Model<Pane>,
|
||||
workspace: Model<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Model<Self>>> {
|
||||
let channel_view = Self::load(channel_id, workspace, window, cx);
|
||||
window.spawn(cx, |mut cx| async move {
|
||||
let channel_view = channel_view.await?;
|
||||
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
pane.update_in(&mut cx, |pane, window, cx| {
|
||||
let buffer_id = channel_view.read(cx).channel_buffer.read(cx).remote_id(cx);
|
||||
|
||||
let existing_view = pane
|
||||
@@ -102,7 +106,12 @@ impl ChannelView {
|
||||
{
|
||||
if let Some(link_position) = link_position {
|
||||
existing_view.update(cx, |channel_view, cx| {
|
||||
channel_view.focus_position_from_link(link_position, true, cx)
|
||||
channel_view.focus_position_from_link(
|
||||
link_position,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
return existing_view;
|
||||
@@ -113,15 +122,27 @@ impl ChannelView {
|
||||
// replace that.
|
||||
if let Some(existing_item) = existing_view {
|
||||
if let Some(ix) = pane.index_for_item(&existing_item) {
|
||||
pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx)
|
||||
.detach();
|
||||
pane.add_item(Box::new(channel_view.clone()), true, true, Some(ix), cx);
|
||||
pane.close_item_by_id(
|
||||
existing_item.entity_id(),
|
||||
SaveIntent::Skip,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
pane.add_item(
|
||||
Box::new(channel_view.clone()),
|
||||
true,
|
||||
true,
|
||||
Some(ix),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(link_position) = link_position {
|
||||
channel_view.update(cx, |channel_view, cx| {
|
||||
channel_view.focus_position_from_link(link_position, true, cx)
|
||||
channel_view.focus_position_from_link(link_position, true, window, cx)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -132,9 +153,10 @@ impl ChannelView {
|
||||
|
||||
pub fn load(
|
||||
channel_id: ChannelId,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
workspace: Model<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Model<Self>>> {
|
||||
let weak_workspace = workspace.downgrade();
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().to_owned();
|
||||
@@ -144,7 +166,7 @@ impl ChannelView {
|
||||
let channel_buffer =
|
||||
channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
window.spawn(cx, |mut cx| async move {
|
||||
let channel_buffer = channel_buffer.await?;
|
||||
let markdown = markdown.await.log_err();
|
||||
|
||||
@@ -158,9 +180,15 @@ impl ChannelView {
|
||||
})
|
||||
})?;
|
||||
|
||||
cx.new_view(|cx| {
|
||||
let mut this =
|
||||
Self::new(project, weak_workspace, channel_store, channel_buffer, cx);
|
||||
cx.new_window_model(|window, cx| {
|
||||
let mut this = Self::new(
|
||||
project,
|
||||
weak_workspace,
|
||||
channel_store,
|
||||
channel_buffer,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
this.acknowledge_buffer_version(cx);
|
||||
this
|
||||
})
|
||||
@@ -169,24 +197,27 @@ impl ChannelView {
|
||||
|
||||
pub fn new(
|
||||
project: Model<Project>,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_buffer: Model<ChannelBuffer>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let buffer = channel_buffer.read(cx).buffer();
|
||||
let this = cx.view().downgrade();
|
||||
let editor = cx.new_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
let this = cx.model().downgrade();
|
||||
let editor = cx.new_model(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, None, window, cx);
|
||||
editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
|
||||
channel_buffer.clone(),
|
||||
)));
|
||||
editor.set_custom_context_menu(move |_, position, cx| {
|
||||
editor.set_custom_context_menu(move |_, position, window, cx| {
|
||||
let this = this.clone();
|
||||
Some(ui::ContextMenu::build(cx, move |menu, _| {
|
||||
menu.entry("Copy link to section", None, move |cx| {
|
||||
this.update(cx, |this, cx| this.copy_link_for_position(position, cx))
|
||||
.ok();
|
||||
Some(ui::ContextMenu::build(window, cx, move |menu, _, _| {
|
||||
menu.entry("Copy link to section", None, move |window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.copy_link_for_position(position, window, cx)
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}))
|
||||
});
|
||||
@@ -195,7 +226,7 @@ impl ChannelView {
|
||||
let _editor_event_subscription =
|
||||
cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone()));
|
||||
|
||||
cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
|
||||
cx.subscribe_in(&channel_buffer, window, Self::handle_channel_buffer_event)
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
@@ -214,10 +245,13 @@ impl ChannelView {
|
||||
&mut self,
|
||||
position: String,
|
||||
first_attempt: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let position = Channel::slug(&position).to_lowercase();
|
||||
let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
|
||||
let snapshot = self
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.snapshot(window, cx));
|
||||
|
||||
if let Some(outline) = snapshot.buffer_snapshot.outline(None) {
|
||||
if let Some(item) = outline
|
||||
@@ -226,7 +260,7 @@ impl ChannelView {
|
||||
.find(|item| &Channel::slug(&item.text).to_lowercase() == &position)
|
||||
{
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::focused()), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::focused()), window, cx, |s| {
|
||||
s.replace_cursors_with(|map| vec![item.range.start.to_display_point(map)])
|
||||
})
|
||||
});
|
||||
@@ -237,12 +271,13 @@ impl ChannelView {
|
||||
if !first_attempt {
|
||||
return;
|
||||
}
|
||||
self._reparse_subscription = Some(cx.subscribe(
|
||||
self._reparse_subscription = Some(cx.subscribe_in(
|
||||
&self.editor,
|
||||
move |this, _, e: &EditorEvent, cx| {
|
||||
window,
|
||||
move |this, _, e: &EditorEvent, window, cx| {
|
||||
match e {
|
||||
EditorEvent::Reparsed(_) => {
|
||||
this.focus_position_from_link(position.clone(), false, cx);
|
||||
this.focus_position_from_link(position.clone(), false, window, cx);
|
||||
this._reparse_subscription.take();
|
||||
}
|
||||
EditorEvent::Edited { .. } | EditorEvent::SelectionsChanged { local: true } => {
|
||||
@@ -254,15 +289,22 @@ impl ChannelView {
|
||||
));
|
||||
}
|
||||
|
||||
fn copy_link(&mut self, _: &CopyLink, cx: &mut ViewContext<Self>) {
|
||||
fn copy_link(&mut self, _: &CopyLink, window: &mut Window, cx: &mut ModelContext<Self>) {
|
||||
let position = self
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.selections.newest_display(cx).start);
|
||||
self.copy_link_for_position(position, cx)
|
||||
self.copy_link_for_position(position, window, cx)
|
||||
}
|
||||
|
||||
fn copy_link_for_position(&self, position: DisplayPoint, cx: &mut ViewContext<Self>) {
|
||||
let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
|
||||
fn copy_link_for_position(
|
||||
&self,
|
||||
position: DisplayPoint,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let snapshot = self
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.snapshot(window, cx));
|
||||
|
||||
let mut closest_heading = None;
|
||||
|
||||
@@ -302,9 +344,10 @@ impl ChannelView {
|
||||
|
||||
fn handle_channel_buffer_event(
|
||||
&mut self,
|
||||
_: Model<ChannelBuffer>,
|
||||
_: &Model<ChannelBuffer>,
|
||||
event: &ChannelBufferEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
|
||||
@@ -318,7 +361,7 @@ impl ChannelView {
|
||||
});
|
||||
}
|
||||
ChannelBufferEvent::BufferEdited => {
|
||||
if self.editor.read(cx).is_focused(cx) {
|
||||
if self.editor.read(cx).is_focused(window) {
|
||||
self.acknowledge_buffer_version(cx);
|
||||
} else {
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
@@ -336,7 +379,7 @@ impl ChannelView {
|
||||
}
|
||||
}
|
||||
|
||||
fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<ChannelView>) {
|
||||
fn acknowledge_buffer_version(&mut self, cx: &mut ModelContext<ChannelView>) {
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
store.acknowledge_notes_version(
|
||||
@@ -355,7 +398,7 @@ impl ChannelView {
|
||||
impl EventEmitter<EditorEvent> for ChannelView {}
|
||||
|
||||
impl Render for ChannelView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_full()
|
||||
.on_action(cx.listener(Self::copy_link))
|
||||
@@ -363,7 +406,7 @@ impl Render for ChannelView {
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for ChannelView {
|
||||
impl Focusable for ChannelView {
|
||||
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.editor.read(cx).focus_handle(cx)
|
||||
}
|
||||
@@ -375,7 +418,7 @@ impl Item for ChannelView {
|
||||
fn act_as_type<'a>(
|
||||
&'a self,
|
||||
type_id: TypeId,
|
||||
self_handle: &'a View<Self>,
|
||||
self_handle: &'a Model<Self>,
|
||||
_: &'a AppContext,
|
||||
) -> Option<AnyView> {
|
||||
if type_id == TypeId::of::<Self>() {
|
||||
@@ -387,7 +430,7 @@ impl Item for ChannelView {
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_icon(&self, cx: &WindowContext) -> Option<Icon> {
|
||||
fn tab_icon(&self, _: &Window, cx: &AppContext) -> Option<Icon> {
|
||||
let channel = self.channel(cx)?;
|
||||
let icon = match channel.visibility {
|
||||
ChannelVisibility::Public => IconName::Public,
|
||||
@@ -397,7 +440,12 @@ impl Item for ChannelView {
|
||||
Some(Icon::new(icon))
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> gpui::AnyElement {
|
||||
fn tab_content(
|
||||
&self,
|
||||
params: TabContentParams,
|
||||
_: &Window,
|
||||
cx: &AppContext,
|
||||
) -> gpui::AnyElement {
|
||||
let (channel_name, status) = if let Some(channel) = self.channel(cx) {
|
||||
let status = match (
|
||||
self.channel_buffer.read(cx).buffer().read(cx).read_only(),
|
||||
@@ -437,14 +485,16 @@ impl Item for ChannelView {
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_: Option<WorkspaceId>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<View<Self>> {
|
||||
Some(cx.new_view(|cx| {
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<Model<Self>> {
|
||||
Some(cx.new_model(|cx| {
|
||||
Self::new(
|
||||
self.project.clone(),
|
||||
self.workspace.clone(),
|
||||
self.channel_store.clone(),
|
||||
self.channel_buffer.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
@@ -454,21 +504,33 @@ impl Item for ChannelView {
|
||||
false
|
||||
}
|
||||
|
||||
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
fn navigate(
|
||||
&mut self,
|
||||
data: Box<dyn Any>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> bool {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.navigate(data, cx))
|
||||
.update(cx, |editor, cx| editor.navigate(data, window, cx))
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, Item::deactivated)
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||
fn deactivated(&mut self, window: &mut Window, cx: &mut ModelContext<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
|
||||
.update(cx, |item, cx| item.deactivated(window, cx))
|
||||
}
|
||||
|
||||
fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
fn set_nav_history(
|
||||
&mut self,
|
||||
history: ItemNavHistory,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
Item::set_nav_history(editor, history, window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn as_searchable(&self, _: &Model<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(self.editor.clone()))
|
||||
}
|
||||
|
||||
@@ -490,7 +552,7 @@ impl FollowableItem for ChannelView {
|
||||
self.remote_id
|
||||
}
|
||||
|
||||
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
|
||||
fn to_state_proto(&self, window: &Window, cx: &AppContext) -> Option<proto::view::Variant> {
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
if !channel_buffer.is_connected() {
|
||||
return None;
|
||||
@@ -500,7 +562,7 @@ impl FollowableItem for ChannelView {
|
||||
proto::view::ChannelView {
|
||||
channel_id: channel_buffer.channel_id.0,
|
||||
editor: if let Some(proto::view::Variant::Editor(proto)) =
|
||||
self.editor.read(cx).to_state_proto(cx)
|
||||
self.editor.read(cx).to_state_proto(window, cx)
|
||||
{
|
||||
Some(proto)
|
||||
} else {
|
||||
@@ -511,11 +573,12 @@ impl FollowableItem for ChannelView {
|
||||
}
|
||||
|
||||
fn from_state_proto(
|
||||
workspace: View<workspace::Workspace>,
|
||||
workspace: Model<workspace::Workspace>,
|
||||
remote_id: workspace::ViewId,
|
||||
state: &mut Option<proto::view::Variant>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<gpui::Task<anyhow::Result<View<Self>>>> {
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<gpui::Task<anyhow::Result<Model<Self>>>> {
|
||||
let Some(proto::view::Variant::ChannelView(_)) = state else {
|
||||
return None;
|
||||
};
|
||||
@@ -523,12 +586,12 @@ impl FollowableItem for ChannelView {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let open = ChannelView::load(ChannelId(state.channel_id), workspace, cx);
|
||||
let open = ChannelView::load(ChannelId(state.channel_id), workspace, window, cx);
|
||||
|
||||
Some(cx.spawn(|mut cx| async move {
|
||||
Some(window.spawn(cx, |mut cx| async move {
|
||||
let this = open.await?;
|
||||
|
||||
let task = this.update(&mut cx, |this, cx| {
|
||||
let task = this.update_in(&mut cx, |this, window, cx| {
|
||||
this.remote_id = Some(remote_id);
|
||||
|
||||
if let Some(state) = state.editor {
|
||||
@@ -543,6 +606,7 @@ impl FollowableItem for ChannelView {
|
||||
scroll_y: state.scroll_y,
|
||||
..Default::default()
|
||||
}),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
@@ -563,31 +627,38 @@ impl FollowableItem for ChannelView {
|
||||
&self,
|
||||
event: &EditorEvent,
|
||||
update: &mut Option<proto::update_view::Variant>,
|
||||
cx: &WindowContext,
|
||||
window: &Window,
|
||||
cx: &AppContext,
|
||||
) -> bool {
|
||||
self.editor
|
||||
.read(cx)
|
||||
.add_event_to_update_proto(event, update, cx)
|
||||
.add_event_to_update_proto(event, update, window, cx)
|
||||
}
|
||||
|
||||
fn apply_update_proto(
|
||||
&mut self,
|
||||
project: &Model<Project>,
|
||||
message: proto::update_view::Variant,
|
||||
cx: &mut ViewContext<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> gpui::Task<anyhow::Result<()>> {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.apply_update_proto(project, message, cx)
|
||||
editor.apply_update_proto(project, message, window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
|
||||
fn set_leader_peer_id(
|
||||
&mut self,
|
||||
leader_peer_id: Option<PeerId>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.set_leader_peer_id(leader_peer_id, cx)
|
||||
editor.set_leader_peer_id(leader_peer_id, window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn is_project_item(&self, _cx: &WindowContext) -> bool {
|
||||
fn is_project_item(&self, _window: &Window, _cx: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -595,7 +666,7 @@ impl FollowableItem for ChannelView {
|
||||
Editor::to_follow_event(event)
|
||||
}
|
||||
|
||||
fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<Dedup> {
|
||||
fn dedup(&self, existing: &Self, _: &Window, cx: &AppContext) -> Option<Dedup> {
|
||||
let existing = existing.channel_buffer.read(cx);
|
||||
if self.channel_buffer.read(cx).channel_id == existing.channel_id {
|
||||
if existing.is_connected() {
|
||||
|
||||
@@ -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};
|
||||
@@ -8,9 +8,9 @@ use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{actions, Editor};
|
||||
use gpui::{
|
||||
actions, div, list, prelude::*, px, Action, AppContext, AsyncWindowContext, ClipboardItem,
|
||||
CursorStyle, DismissEvent, ElementId, EventEmitter, FocusHandle, FocusableView, FontWeight,
|
||||
HighlightStyle, ListOffset, ListScrollEvent, ListState, Model, Render, Stateful, Subscription,
|
||||
Task, View, ViewContext, VisualContext, WeakView,
|
||||
CursorStyle, DismissEvent, ElementId, EventEmitter, FocusHandle, Focusable, FontWeight,
|
||||
HighlightStyle, ListOffset, ListScrollEvent, ListState, Model, ModelContext, Render, Stateful,
|
||||
Subscription, Task, WeakModel, Window,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use menu::Confirm;
|
||||
@@ -37,9 +37,9 @@ const MESSAGE_LOADING_THRESHOLD: usize = 50;
|
||||
const CHAT_PANEL_KEY: &str = "ChatPanel";
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _| {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|
||||
workspace.toggle_panel_focus::<ChatPanel>(cx);
|
||||
cx.observe_new_models(|workspace: &mut Workspace, _, _| {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
|
||||
workspace.toggle_panel_focus::<ChatPanel>(window, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
@@ -51,7 +51,7 @@ pub struct ChatPanel {
|
||||
languages: Arc<LanguageRegistry>,
|
||||
message_list: ListState,
|
||||
active_chat: Option<(Model<ChannelChat>, Subscription)>,
|
||||
message_editor: View<MessageEditor>,
|
||||
message_editor: Model<MessageEditor>,
|
||||
local_timezone: UtcOffset,
|
||||
fs: Arc<dyn Fs>,
|
||||
width: Option<Pixels>,
|
||||
@@ -74,37 +74,46 @@ struct SerializedChatPanel {
|
||||
actions!(chat_panel, [ToggleFocus]);
|
||||
|
||||
impl ChatPanel {
|
||||
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
|
||||
pub fn new(
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Workspace>,
|
||||
) -> Model<Self> {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let client = workspace.app_state().client.clone();
|
||||
let channel_store = ChannelStore::global(cx);
|
||||
let user_store = workspace.app_state().user_store.clone();
|
||||
let languages = workspace.app_state().languages.clone();
|
||||
|
||||
let input_editor = cx.new_view(|cx| {
|
||||
let input_editor = cx.new_model(|cx| {
|
||||
MessageEditor::new(
|
||||
languages.clone(),
|
||||
user_store.clone(),
|
||||
None,
|
||||
cx.new_view(|cx| Editor::auto_height(4, cx)),
|
||||
cx.new_model(|cx| Editor::auto_height(4, window, cx)),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.new_view(|cx: &mut ViewContext<Self>| {
|
||||
let view = cx.view().downgrade();
|
||||
let message_list =
|
||||
ListState::new(0, gpui::ListAlignment::Bottom, px(1000.), move |ix, cx| {
|
||||
cx.new_model(|cx| {
|
||||
let view = cx.model().downgrade();
|
||||
let message_list = ListState::new(
|
||||
0,
|
||||
gpui::ListAlignment::Bottom,
|
||||
px(1000.),
|
||||
move |ix, window, cx| {
|
||||
if let Some(view) = view.upgrade() {
|
||||
view.update(cx, |view, cx| {
|
||||
view.render_message(ix, cx).into_any_element()
|
||||
view.update(cx, |view: &mut Self, cx| {
|
||||
view.render_message(ix, window, cx).into_any_element()
|
||||
})
|
||||
} else {
|
||||
div().into_any()
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, cx| {
|
||||
message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, _, cx| {
|
||||
if event.visible_range.start < MESSAGE_LOADING_THRESHOLD {
|
||||
this.load_more_messages(cx);
|
||||
}
|
||||
@@ -187,9 +196,9 @@ impl ChatPanel {
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
) -> Task<Result<Model<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let serialized_panel = if let Some(panel) = cx
|
||||
.background_executor()
|
||||
@@ -203,8 +212,8 @@ impl ChatPanel {
|
||||
None
|
||||
};
|
||||
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let panel = Self::new(workspace, cx);
|
||||
workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||
let panel = Self::new(workspace, window, cx);
|
||||
if let Some(serialized_panel) = serialized_panel {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.width = serialized_panel.width.map(|r| r.round());
|
||||
@@ -216,7 +225,7 @@ impl ChatPanel {
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
|
||||
fn serialize(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let width = self.width;
|
||||
self.pending_serialization = cx.background_executor().spawn(
|
||||
async move {
|
||||
@@ -232,7 +241,7 @@ impl ChatPanel {
|
||||
);
|
||||
}
|
||||
|
||||
fn set_active_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) {
|
||||
fn set_active_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ModelContext<Self>) {
|
||||
if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
|
||||
self.markdown_data.clear();
|
||||
self.message_list.reset(chat.read(cx).message_count());
|
||||
@@ -251,7 +260,7 @@ impl ChatPanel {
|
||||
&mut self,
|
||||
_: Model<ChannelChat>,
|
||||
event: &ChannelChatEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
ChannelChatEvent::MessagesUpdated {
|
||||
@@ -284,7 +293,7 @@ impl ChatPanel {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn acknowledge_last_message(&mut self, cx: &mut ViewContext<Self>) {
|
||||
fn acknowledge_last_message(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if self.active && self.is_scrolled_to_bottom {
|
||||
if let Some((chat, _)) = &self.active_chat {
|
||||
if let Some(channel_id) = self.channel_id(cx) {
|
||||
@@ -305,7 +314,7 @@ impl ChatPanel {
|
||||
&mut self,
|
||||
message_id: Option<ChannelMessageId>,
|
||||
reply_to_message: &Option<ChannelMessage>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let reply_to_message = match reply_to_message {
|
||||
None => {
|
||||
@@ -369,8 +378,8 @@ impl ChatPanel {
|
||||
),
|
||||
)
|
||||
.cursor(CursorStyle::PointingHand)
|
||||
.tooltip(|cx| Tooltip::text("Go to message", cx))
|
||||
.on_click(cx.listener(move |chat_panel, _, cx| {
|
||||
.tooltip(Tooltip::text("Go to message"))
|
||||
.on_click(cx.listener(move |chat_panel, _, _, cx| {
|
||||
if let Some(channel_id) = current_channel_id {
|
||||
chat_panel
|
||||
.select_channel(channel_id, reply_to_message_id.into(), cx)
|
||||
@@ -380,7 +389,12 @@ impl ChatPanel {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render_message(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let active_chat = &self.active_chat.as_ref().unwrap().0;
|
||||
let (message, is_continuation_from_previous, is_admin) =
|
||||
active_chat.update(cx, |active_chat, cx| {
|
||||
@@ -530,7 +544,7 @@ impl ChatPanel {
|
||||
.w_full()
|
||||
.text_ui_sm(cx)
|
||||
.id(element_id)
|
||||
.child(text.element("body".into(), cx)),
|
||||
.child(text.element("body".into(), window, cx)),
|
||||
)
|
||||
.when(self.has_open_menu(message_id), |el| {
|
||||
el.bg(cx.theme().colors().element_selected)
|
||||
@@ -560,7 +574,7 @@ impl ChatPanel {
|
||||
},
|
||||
)
|
||||
.child(
|
||||
self.render_popover_buttons(cx, message_id, can_delete_message, can_edit_message)
|
||||
self.render_popover_buttons(message_id, can_delete_message, can_edit_message, cx)
|
||||
.mt_neg_2p5(),
|
||||
)
|
||||
}
|
||||
@@ -572,7 +586,7 @@ impl ChatPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_popover_button(&self, cx: &ViewContext<Self>, child: Stateful<Div>) -> Div {
|
||||
fn render_popover_button(&self, cx: &mut ModelContext<Self>, child: Stateful<Div>) -> Div {
|
||||
div()
|
||||
.w_6()
|
||||
.bg(cx.theme().colors().element_background)
|
||||
@@ -582,10 +596,10 @@ impl ChatPanel {
|
||||
|
||||
fn render_popover_buttons(
|
||||
&self,
|
||||
cx: &ViewContext<Self>,
|
||||
message_id: Option<u64>,
|
||||
can_delete_message: bool,
|
||||
can_edit_message: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Div {
|
||||
h_flex()
|
||||
.absolute()
|
||||
@@ -606,16 +620,16 @@ impl ChatPanel {
|
||||
.id("reply")
|
||||
.child(
|
||||
IconButton::new(("reply", message_id), IconName::ReplyArrowRight)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.cancel_edit_message(cx);
|
||||
|
||||
this.message_editor.update(cx, |editor, cx| {
|
||||
editor.set_reply_to_message_id(message_id);
|
||||
editor.focus_handle(cx).focus(cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
})
|
||||
})),
|
||||
)
|
||||
.tooltip(|cx| Tooltip::text("Reply", cx)),
|
||||
.tooltip(Tooltip::text("Reply")),
|
||||
),
|
||||
)
|
||||
})
|
||||
@@ -628,7 +642,7 @@ impl ChatPanel {
|
||||
.id("edit")
|
||||
.child(
|
||||
IconButton::new(("edit", message_id), IconName::Pencil)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.message_editor.update(cx, |editor, cx| {
|
||||
editor.clear_reply_to_message_id();
|
||||
|
||||
@@ -655,18 +669,18 @@ impl ChatPanel {
|
||||
});
|
||||
|
||||
editor.set_edit_message_id(message_id);
|
||||
editor.focus_handle(cx).focus(cx);
|
||||
editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
})
|
||||
})),
|
||||
)
|
||||
.tooltip(|cx| Tooltip::text("Edit", cx)),
|
||||
.tooltip(Tooltip::text("Edit")),
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
.when_some(message_id, |el, message_id| {
|
||||
let this = cx.view().clone();
|
||||
let this = cx.model().clone();
|
||||
|
||||
el.child(
|
||||
self.render_popover_button(
|
||||
@@ -678,34 +692,36 @@ impl ChatPanel {
|
||||
("trigger", message_id),
|
||||
IconName::Ellipsis,
|
||||
))
|
||||
.menu(move |cx| {
|
||||
.menu(move |window, cx| {
|
||||
Some(Self::render_message_menu(
|
||||
&this,
|
||||
message_id,
|
||||
can_delete_message,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
}),
|
||||
)
|
||||
.id("more")
|
||||
.tooltip(|cx| Tooltip::text("More", cx)),
|
||||
.tooltip(Tooltip::text("More")),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn render_message_menu(
|
||||
this: &View<Self>,
|
||||
this: &Model<Self>,
|
||||
message_id: u64,
|
||||
can_delete_message: bool,
|
||||
cx: &mut WindowContext,
|
||||
) -> View<ContextMenu> {
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) -> Model<ContextMenu> {
|
||||
let menu = {
|
||||
ContextMenu::build(cx, move |menu, cx| {
|
||||
ContextMenu::build(window, cx, move |menu, window, _| {
|
||||
menu.entry(
|
||||
"Copy message text",
|
||||
None,
|
||||
cx.handler_for(this, move |this, cx| {
|
||||
window.handler_for(this, move |this, _, cx| {
|
||||
if let Some(message) = this.active_chat().and_then(|active_chat| {
|
||||
active_chat.read(cx).find_loaded_message(message_id)
|
||||
}) {
|
||||
@@ -718,15 +734,21 @@ impl ChatPanel {
|
||||
menu.entry(
|
||||
"Delete message",
|
||||
None,
|
||||
cx.handler_for(this, move |this, cx| this.remove_message(message_id, cx)),
|
||||
window.handler_for(this, move |this, _, cx| {
|
||||
this.remove_message(message_id, cx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
};
|
||||
this.update(cx, |this, cx| {
|
||||
let subscription = cx.subscribe(&menu, |this: &mut Self, _, _: &DismissEvent, _| {
|
||||
this.open_context_menu = None;
|
||||
});
|
||||
let subscription = cx.subscribe_in(
|
||||
&menu,
|
||||
window,
|
||||
|this: &mut Self, _, _: &DismissEvent, _, _| {
|
||||
this.open_context_menu = None;
|
||||
},
|
||||
);
|
||||
this.open_context_menu = Some((message_id, subscription));
|
||||
});
|
||||
menu
|
||||
@@ -777,19 +799,19 @@ impl ChatPanel {
|
||||
);
|
||||
|
||||
rich_text.custom_ranges.push(range);
|
||||
rich_text.set_tooltip_builder_for_custom_ranges(move |_, _, cx| {
|
||||
Some(Tooltip::text(edit_timestamp_text.clone(), cx))
|
||||
rich_text.set_tooltip_builder_for_custom_ranges(move |_, _, _, cx| {
|
||||
Some(Tooltip::simple(edit_timestamp_text.clone(), cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
rich_text
|
||||
}
|
||||
|
||||
fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||
fn send(&mut self, _: &Confirm, window: &mut Window, cx: &mut ModelContext<Self>) {
|
||||
if let Some((chat, _)) = self.active_chat.as_ref() {
|
||||
let message = self
|
||||
.message_editor
|
||||
.update(cx, |editor, cx| editor.take_message(cx));
|
||||
.update(cx, |editor, cx| editor.take_message(window, cx));
|
||||
|
||||
if let Some(id) = self.message_editor.read(cx).edit_message_id() {
|
||||
self.message_editor.update(cx, |editor, _| {
|
||||
@@ -811,13 +833,13 @@ impl ChatPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_message(&mut self, id: u64, cx: &mut ViewContext<Self>) {
|
||||
fn remove_message(&mut self, id: u64, cx: &mut ModelContext<Self>) {
|
||||
if let Some((chat, _)) = self.active_chat.as_ref() {
|
||||
chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
|
||||
}
|
||||
}
|
||||
|
||||
fn load_more_messages(&mut self, cx: &mut ViewContext<Self>) {
|
||||
fn load_more_messages(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if let Some((chat, _)) = self.active_chat.as_ref() {
|
||||
chat.update(cx, |channel, cx| {
|
||||
if let Some(task) = channel.load_more_messages(cx) {
|
||||
@@ -831,7 +853,7 @@ impl ChatPanel {
|
||||
&mut self,
|
||||
selected_channel_id: ChannelId,
|
||||
scroll_to_message_id: Option<u64>,
|
||||
cx: &mut ViewContext<ChatPanel>,
|
||||
cx: &mut ModelContext<ChatPanel>,
|
||||
) -> Task<Result<()>> {
|
||||
let open_chat = self
|
||||
.active_chat
|
||||
@@ -857,20 +879,18 @@ impl ChatPanel {
|
||||
|
||||
if let Some(message_id) = scroll_to_message_id {
|
||||
if let Some(item_ix) =
|
||||
ChannelChat::load_history_since_message(chat.clone(), message_id, (*cx).clone())
|
||||
ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone())
|
||||
.await
|
||||
{
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(highlight_message_id) = highlight_message_id {
|
||||
let task = cx.spawn({
|
||||
|this, mut cx| async move {
|
||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.highlighted_message.take();
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
let task = cx.spawn(|this, mut cx| async move {
|
||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.highlighted_message.take();
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
|
||||
this.highlighted_message = Some((highlight_message_id, task));
|
||||
@@ -891,12 +911,12 @@ impl ChatPanel {
|
||||
})
|
||||
}
|
||||
|
||||
fn close_reply_preview(&mut self, cx: &mut ViewContext<Self>) {
|
||||
fn close_reply_preview(&mut self, cx: &mut ModelContext<Self>) {
|
||||
self.message_editor
|
||||
.update(cx, |editor, _| editor.clear_reply_to_message_id());
|
||||
}
|
||||
|
||||
fn cancel_edit_message(&mut self, cx: &mut ViewContext<Self>) {
|
||||
fn cancel_edit_message(&mut self, cx: &mut ModelContext<Self>) {
|
||||
self.message_editor.update(cx, |editor, cx| {
|
||||
// only clear the editor input if we were editing a message
|
||||
if editor.edit_message_id().is_none() {
|
||||
@@ -919,7 +939,7 @@ impl ChatPanel {
|
||||
}
|
||||
|
||||
impl Render for ChatPanel {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
|
||||
let channel_id = self
|
||||
.active_chat
|
||||
.as_ref()
|
||||
@@ -971,11 +991,12 @@ impl Render for ChatPanel {
|
||||
.full_width()
|
||||
.key_binding(KeyBinding::for_action(
|
||||
&collab_panel::ToggleFocus,
|
||||
cx,
|
||||
window,
|
||||
))
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(
|
||||
collab_panel::ToggleFocus.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
),
|
||||
@@ -999,8 +1020,8 @@ impl Render for ChatPanel {
|
||||
.child(
|
||||
IconButton::new("cancel-edit-message", IconName::Close)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::text("Cancel edit message", cx))
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
.tooltip(Tooltip::text("Cancel edit message"))
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.cancel_edit_message(cx);
|
||||
})),
|
||||
),
|
||||
@@ -1045,7 +1066,7 @@ impl Render for ChatPanel {
|
||||
)
|
||||
.when_some(channel_id, |this, channel_id| {
|
||||
this.cursor_pointer().on_click(cx.listener(
|
||||
move |chat_panel, _, cx| {
|
||||
move |chat_panel, _, _, cx| {
|
||||
chat_panel
|
||||
.select_channel(
|
||||
channel_id,
|
||||
@@ -1061,8 +1082,8 @@ impl Render for ChatPanel {
|
||||
.child(
|
||||
IconButton::new("close-reply-preview", IconName::Close)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::text("Close reply", cx))
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
.tooltip(Tooltip::text("Close reply"))
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.close_reply_preview(cx);
|
||||
})),
|
||||
),
|
||||
@@ -1073,7 +1094,7 @@ impl Render for ChatPanel {
|
||||
Some(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.on_action(cx.listener(|this, _: &actions::Cancel, cx| {
|
||||
.on_action(cx.listener(|this, _: &actions::Cancel, _, cx| {
|
||||
this.cancel_edit_message(cx);
|
||||
this.close_reply_preview(cx);
|
||||
}))
|
||||
@@ -1085,7 +1106,7 @@ impl Render for ChatPanel {
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for ChatPanel {
|
||||
impl Focusable for ChatPanel {
|
||||
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||
if self.active_chat.is_some() {
|
||||
self.message_editor.read(cx).focus_handle(cx)
|
||||
@@ -1096,7 +1117,7 @@ impl FocusableView for ChatPanel {
|
||||
}
|
||||
|
||||
impl Panel for ChatPanel {
|
||||
fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
|
||||
fn position(&self, _: &Window, cx: &AppContext) -> DockPosition {
|
||||
ChatPanelSettings::get_global(cx).dock
|
||||
}
|
||||
|
||||
@@ -1104,7 +1125,12 @@ impl Panel for ChatPanel {
|
||||
matches!(position, DockPosition::Left | DockPosition::Right)
|
||||
}
|
||||
|
||||
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
|
||||
fn set_position(
|
||||
&mut self,
|
||||
position: DockPosition,
|
||||
_: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
settings::update_settings_file::<ChatPanelSettings>(
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
@@ -1112,18 +1138,18 @@ impl Panel for ChatPanel {
|
||||
);
|
||||
}
|
||||
|
||||
fn size(&self, cx: &gpui::WindowContext) -> Pixels {
|
||||
fn size(&self, _: &Window, cx: &AppContext) -> Pixels {
|
||||
self.width
|
||||
.unwrap_or_else(|| ChatPanelSettings::get_global(cx).default_width)
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
|
||||
fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut ModelContext<Self>) {
|
||||
self.width = size;
|
||||
self.serialize(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
fn set_active(&mut self, active: bool, _: &mut Window, cx: &mut ModelContext<Self>) {
|
||||
self.active = active;
|
||||
if active {
|
||||
self.acknowledge_last_message(cx);
|
||||
@@ -1134,11 +1160,19 @@ impl Panel for ChatPanel {
|
||||
"ChatPanel"
|
||||
}
|
||||
|
||||
fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
|
||||
Some(ui::IconName::MessageBubbles).filter(|_| ChatPanelSettings::get_global(cx).button)
|
||||
fn icon(&self, _: &Window, cx: &AppContext) -> Option<ui::IconName> {
|
||||
match ChatPanelSettings::get_global(cx).button {
|
||||
ChatPanelButton::Never => None,
|
||||
ChatPanelButton::Always => Some(ui::IconName::MessageBubbles),
|
||||
ChatPanelButton::WhenInCall => ActiveCall::global(cx)
|
||||
.read(cx)
|
||||
.room()
|
||||
.filter(|room| room.read(cx).contains_guests())
|
||||
.map(|_| ui::IconName::MessageBubbles),
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||
fn icon_tooltip(&self, _: &Window, _: &AppContext) -> Option<&'static str> {
|
||||
Some("Chat Panel")
|
||||
}
|
||||
|
||||
@@ -1146,12 +1180,16 @@ impl Panel for ChatPanel {
|
||||
Box::new(ToggleFocus)
|
||||
}
|
||||
|
||||
fn starts_open(&self, cx: &WindowContext) -> bool {
|
||||
fn starts_open(&self, _: &Window, cx: &AppContext) -> bool {
|
||||
ActiveCall::global(cx)
|
||||
.read(cx)
|
||||
.room()
|
||||
.is_some_and(|room| room.read(cx).contains_guests())
|
||||
}
|
||||
|
||||
fn activation_priority(&self) -> u32 {
|
||||
7
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for ChatPanel {}
|
||||
|
||||
@@ -5,8 +5,8 @@ use collections::HashSet;
|
||||
use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
|
||||
Render, Task, TextStyle, View, ViewContext, WeakView,
|
||||
AsyncWindowContext, Focusable, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
|
||||
ModelContext, Render, Task, TextStyle, WeakModel, Window,
|
||||
};
|
||||
use language::{
|
||||
language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry,
|
||||
@@ -42,7 +42,7 @@ static MENTIONS_SEARCH: LazyLock<SearchQuery> = LazyLock::new(|| {
|
||||
});
|
||||
|
||||
pub struct MessageEditor {
|
||||
pub editor: View<Editor>,
|
||||
pub editor: Model<Editor>,
|
||||
user_store: Model<UserStore>,
|
||||
channel_chat: Option<Model<ChannelChat>>,
|
||||
mentions: Vec<UserId>,
|
||||
@@ -51,7 +51,7 @@ pub struct MessageEditor {
|
||||
edit_message_id: Option<u64>,
|
||||
}
|
||||
|
||||
struct MessageEditorCompletionProvider(WeakView<MessageEditor>);
|
||||
struct MessageEditorCompletionProvider(WeakModel<MessageEditor>);
|
||||
|
||||
impl CompletionProvider for MessageEditorCompletionProvider {
|
||||
fn completions(
|
||||
@@ -59,13 +59,14 @@ impl CompletionProvider for MessageEditorCompletionProvider {
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_position: language::Anchor,
|
||||
_: editor::CompletionContext,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Editor>,
|
||||
) -> Task<anyhow::Result<Vec<Completion>>> {
|
||||
let Some(handle) = self.0.upgrade() else {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
};
|
||||
handle.update(cx, |message_editor, cx| {
|
||||
message_editor.completions(buffer, buffer_position, cx)
|
||||
message_editor.completions(buffer, buffer_position, window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -74,28 +75,20 @@ impl CompletionProvider for MessageEditorCompletionProvider {
|
||||
_buffer: Model<Buffer>,
|
||||
_completion_indices: Vec<usize>,
|
||||
_completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
_cx: &mut ViewContext<Editor>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut ModelContext<Editor>,
|
||||
) -> Task<anyhow::Result<bool>> {
|
||||
Task::ready(Ok(false))
|
||||
}
|
||||
|
||||
fn apply_additional_edits_for_completion(
|
||||
&self,
|
||||
_buffer: Model<Buffer>,
|
||||
_completion: Completion,
|
||||
_push_to_history: bool,
|
||||
_cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<Option<language::Transaction>>> {
|
||||
Task::ready(Ok(None))
|
||||
}
|
||||
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
_buffer: &Model<Buffer>,
|
||||
_position: language::Anchor,
|
||||
text: &str,
|
||||
_trigger_in_words: bool,
|
||||
_cx: &mut ViewContext<Editor>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut ModelContext<Editor>,
|
||||
) -> bool {
|
||||
text == "@"
|
||||
}
|
||||
@@ -106,10 +99,11 @@ impl MessageEditor {
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
user_store: Model<UserStore>,
|
||||
channel_chat: Option<Model<ChannelChat>>,
|
||||
editor: View<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
editor: Model<Editor>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let this = cx.view().downgrade();
|
||||
let this = cx.model().downgrade();
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor.set_use_autoclose(false);
|
||||
@@ -131,7 +125,8 @@ impl MessageEditor {
|
||||
.as_singleton()
|
||||
.expect("message editor must be singleton");
|
||||
|
||||
cx.subscribe(&buffer, Self::on_buffer_event).detach();
|
||||
cx.subscribe_in(&buffer, window, Self::on_buffer_event)
|
||||
.detach();
|
||||
cx.observe_global::<settings::SettingsStore>(|view, cx| {
|
||||
view.editor.update(cx, |editor, cx| {
|
||||
editor.set_auto_replace_emoji_shortcode(
|
||||
@@ -144,7 +139,7 @@ impl MessageEditor {
|
||||
.detach();
|
||||
|
||||
let markdown = language_registry.language_for_name("Markdown");
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
cx.spawn_in(window, |_, mut cx| async move {
|
||||
let markdown = markdown.await.context("failed to load Markdown language")?;
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language(Some(markdown), cx)
|
||||
@@ -187,7 +182,7 @@ impl MessageEditor {
|
||||
self.edit_message_id = None;
|
||||
}
|
||||
|
||||
pub fn set_channel_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) {
|
||||
pub fn set_channel_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ModelContext<Self>) {
|
||||
let channel_id = chat.read(cx).channel_id;
|
||||
self.channel_chat = Some(chat);
|
||||
let channel_name = ChannelStore::global(cx)
|
||||
@@ -203,7 +198,11 @@ impl MessageEditor {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn take_message(&mut self, cx: &mut ViewContext<Self>) -> MessageParams {
|
||||
pub fn take_message(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> MessageParams {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let highlights = editor.text_highlights::<Self>(cx);
|
||||
let text = editor.text(cx);
|
||||
@@ -218,7 +217,7 @@ impl MessageEditor {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
editor.clear(cx);
|
||||
editor.clear(window, cx);
|
||||
self.mentions.clear();
|
||||
let reply_to_message_id = std::mem::take(&mut self.reply_to_message_id);
|
||||
|
||||
@@ -232,13 +231,14 @@ impl MessageEditor {
|
||||
|
||||
fn on_buffer_event(
|
||||
&mut self,
|
||||
buffer: Model<Buffer>,
|
||||
buffer: &Model<Buffer>,
|
||||
event: &language::BufferEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let language::BufferEvent::Reparsed | language::BufferEvent::Edited = event {
|
||||
let buffer = buffer.read(cx).snapshot();
|
||||
self.mentions_task = Some(cx.spawn(|this, cx| async move {
|
||||
self.mentions_task = Some(cx.spawn_in(window, |this, cx| async move {
|
||||
cx.background_executor()
|
||||
.timer(MENTIONS_DEBOUNCE_INTERVAL)
|
||||
.await;
|
||||
@@ -251,13 +251,14 @@ impl MessageEditor {
|
||||
&mut self,
|
||||
buffer: &Model<Buffer>,
|
||||
end_anchor: Anchor,
|
||||
cx: &mut ViewContext<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<Completion>>> {
|
||||
if let Some((start_anchor, query, candidates)) =
|
||||
self.collect_mention_candidates(buffer, end_anchor, cx)
|
||||
{
|
||||
if !candidates.is_empty() {
|
||||
return cx.spawn(|_, cx| async move {
|
||||
return cx.spawn_in(window, |_, cx| async move {
|
||||
Ok(Self::resolve_completions_for_candidates(
|
||||
&cx,
|
||||
query.as_str(),
|
||||
@@ -274,7 +275,7 @@ impl MessageEditor {
|
||||
self.collect_emoji_candidates(buffer, end_anchor, cx)
|
||||
{
|
||||
if !candidates.is_empty() {
|
||||
return cx.spawn(|_, cx| async move {
|
||||
return cx.spawn_in(window, |_, cx| async move {
|
||||
Ok(Self::resolve_completions_for_candidates(
|
||||
&cx,
|
||||
query.as_str(),
|
||||
@@ -319,6 +320,7 @@ impl MessageEditor {
|
||||
server_id: LanguageServerId(0), // TODO: Make this optional or something?
|
||||
lsp_completion: Default::default(), // TODO: Make this optional or something?
|
||||
confirm: None,
|
||||
resolved: true,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
@@ -347,7 +349,7 @@ impl MessageEditor {
|
||||
&mut self,
|
||||
buffer: &Model<Buffer>,
|
||||
end_anchor: Anchor,
|
||||
cx: &mut ViewContext<Self>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<(Anchor, String, Vec<StringMatchCandidate>)> {
|
||||
let end_offset = end_anchor.to_offset(buffer.read(cx));
|
||||
|
||||
@@ -396,7 +398,7 @@ impl MessageEditor {
|
||||
&mut self,
|
||||
buffer: &Model<Buffer>,
|
||||
end_anchor: Anchor,
|
||||
cx: &mut ViewContext<Self>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<(Anchor, String, &'static [StringMatchCandidate])> {
|
||||
static EMOJI_FUZZY_MATCH_CANDIDATES: LazyLock<Vec<StringMatchCandidate>> =
|
||||
LazyLock::new(|| {
|
||||
@@ -454,7 +456,7 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
async fn find_mentions(
|
||||
this: WeakView<MessageEditor>,
|
||||
this: WeakModel<MessageEditor>,
|
||||
buffer: BufferSnapshot,
|
||||
mut cx: AsyncWindowContext,
|
||||
) {
|
||||
@@ -514,7 +516,7 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
impl Render for MessageEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: if self.editor.read(cx).read_only(cx) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,8 @@ use client::{
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, anchored, deferred, div, AppContext, ClipboardItem, DismissEvent, EventEmitter,
|
||||
FocusableView, Model, ParentElement, Render, Styled, Subscription, Task, View, ViewContext,
|
||||
VisualContext, WeakView,
|
||||
Focusable, Model, ModelContext, ParentElement, Render, Styled, Subscription, Task, WeakModel,
|
||||
Window,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
@@ -26,7 +26,7 @@ actions!(
|
||||
);
|
||||
|
||||
pub struct ChannelModal {
|
||||
picker: View<Picker<ChannelModalDelegate>>,
|
||||
picker: Model<Picker<ChannelModalDelegate>>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
}
|
||||
@@ -37,11 +37,12 @@ impl ChannelModal {
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
mode: Mode,
|
||||
cx: &mut ViewContext<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
|
||||
let channel_modal = cx.view().downgrade();
|
||||
let picker = cx.new_view(|cx| {
|
||||
let channel_modal = cx.model().downgrade();
|
||||
let picker = cx.new_model(|cx| {
|
||||
Picker::uniform_list(
|
||||
ChannelModalDelegate {
|
||||
channel_modal,
|
||||
@@ -57,6 +58,7 @@ impl ChannelModal {
|
||||
has_all_members: false,
|
||||
mode,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.modal(false)
|
||||
@@ -69,27 +71,32 @@ impl ChannelModal {
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
|
||||
fn toggle_mode(&mut self, _: &ToggleMode, window: &mut Window, cx: &mut ModelContext<Self>) {
|
||||
let mode = match self.picker.read(cx).delegate.mode {
|
||||
Mode::ManageMembers => Mode::InviteMembers,
|
||||
Mode::InviteMembers => Mode::ManageMembers,
|
||||
};
|
||||
self.set_mode(mode, cx);
|
||||
self.set_mode(mode, window, cx);
|
||||
}
|
||||
|
||||
fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
|
||||
fn set_mode(&mut self, mode: Mode, window: &mut Window, cx: &mut ModelContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
let delegate = &mut picker.delegate;
|
||||
delegate.mode = mode;
|
||||
delegate.selected_index = 0;
|
||||
picker.set_query("", cx);
|
||||
picker.update_matches(picker.query(cx), cx);
|
||||
picker.set_query("", window, cx);
|
||||
picker.update_matches(picker.query(cx), window, cx);
|
||||
cx.notify()
|
||||
});
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn set_channel_visibility(&mut self, selection: &ToggleState, cx: &mut ViewContext<Self>) {
|
||||
fn set_channel_visibility(
|
||||
&mut self,
|
||||
selection: &ToggleState,
|
||||
_: &mut Window,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.channel_store.update(cx, |channel_store, cx| {
|
||||
channel_store
|
||||
.set_channel_visibility(
|
||||
@@ -105,7 +112,7 @@ impl ChannelModal {
|
||||
});
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut ModelContext<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
@@ -113,14 +120,14 @@ impl ChannelModal {
|
||||
impl EventEmitter<DismissEvent> for ChannelModal {}
|
||||
impl ModalView for ChannelModal {}
|
||||
|
||||
impl FocusableView for ChannelModal {
|
||||
impl Focusable for ChannelModal {
|
||||
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ChannelModal {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut ModelContext<Self>) -> impl IntoElement {
|
||||
let channel_store = self.channel_store.read(cx);
|
||||
let Some(channel) = channel_store.channel_for_id(self.channel_id) else {
|
||||
return div();
|
||||
@@ -169,7 +176,7 @@ impl Render for ChannelModal {
|
||||
Some(
|
||||
Button::new("copy-link", "Copy Link")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
if let Some(channel) = this
|
||||
.channel_store
|
||||
.read(cx)
|
||||
@@ -197,8 +204,8 @@ impl Render for ChannelModal {
|
||||
this.border_color(cx.theme().colors().border)
|
||||
})
|
||||
.child(Label::new("Manage Members"))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.set_mode(Mode::ManageMembers, cx);
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.set_mode(Mode::ManageMembers, window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
@@ -212,8 +219,8 @@ impl Render for ChannelModal {
|
||||
this.border_color(cx.theme().colors().border)
|
||||
})
|
||||
.child(Label::new("Invite Members"))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.set_mode(Mode::InviteMembers, cx);
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.set_mode(Mode::InviteMembers, window, cx);
|
||||
})),
|
||||
),
|
||||
),
|
||||
@@ -229,7 +236,7 @@ pub enum Mode {
|
||||
}
|
||||
|
||||
pub struct ChannelModalDelegate {
|
||||
channel_modal: WeakView<ChannelModal>,
|
||||
channel_modal: WeakModel<ChannelModal>,
|
||||
matching_users: Vec<Arc<User>>,
|
||||
matching_member_indices: Vec<usize>,
|
||||
user_store: Model<UserStore>,
|
||||
@@ -240,13 +247,13 @@ pub struct ChannelModalDelegate {
|
||||
match_candidates: Vec<StringMatchCandidate>,
|
||||
members: Vec<ChannelMembership>,
|
||||
has_all_members: bool,
|
||||
context_menu: Option<(View<ContextMenu>, Subscription)>,
|
||||
context_menu: Option<(Model<ContextMenu>, Subscription)>,
|
||||
}
|
||||
|
||||
impl PickerDelegate for ChannelModalDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut AppContext) -> Arc<str> {
|
||||
"Search collaborator by username...".into()
|
||||
}
|
||||
|
||||
@@ -261,11 +268,21 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_window: &mut Window,
|
||||
_: &mut ModelContext<Picker<Self>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => {
|
||||
if self.has_all_members {
|
||||
@@ -284,7 +301,7 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
cx.spawn_in(window, |picker, mut cx| async move {
|
||||
picker
|
||||
.update(&mut cx, |picker, cx| {
|
||||
let delegate = &mut picker.delegate;
|
||||
@@ -300,7 +317,7 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
let search_members = self.channel_store.update(cx, |store, cx| {
|
||||
store.fuzzy_search_members(self.channel_id, query.clone(), 100, cx)
|
||||
});
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
cx.spawn_in(window, |picker, mut cx| async move {
|
||||
async {
|
||||
let members = search_members.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
@@ -322,7 +339,7 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
let search_users = self
|
||||
.user_store
|
||||
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
cx.spawn_in(window, |picker, mut cx| async move {
|
||||
async {
|
||||
let users = search_users.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
@@ -338,26 +355,26 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut ModelContext<Picker<Self>>) {
|
||||
if let Some(selected_user) = self.user_at_index(self.selected_index) {
|
||||
if Some(selected_user.id) == self.user_store.read(cx).current_user().map(|user| user.id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
match self.mode {
|
||||
Mode::ManageMembers => self.show_context_menu(self.selected_index, cx),
|
||||
Mode::ManageMembers => self.show_context_menu(self.selected_index, window, cx),
|
||||
Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
|
||||
Some(proto::channel_member::Kind::Invitee) => {
|
||||
self.remove_member(selected_user.id, cx);
|
||||
self.remove_member(selected_user.id, window, cx);
|
||||
}
|
||||
Some(proto::channel_member::Kind::Member) => {}
|
||||
None => self.invite_member(selected_user, cx),
|
||||
None => self.invite_member(selected_user, window, cx),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn dismissed(&mut self, _: &mut Window, cx: &mut ModelContext<Picker<Self>>) {
|
||||
if self.context_menu.is_none() {
|
||||
self.channel_modal
|
||||
.update(cx, |_, cx| {
|
||||
@@ -371,7 +388,8 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
_: &mut Window,
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let user = self.user_at_index(ix)?;
|
||||
let membership = self.member_at_index(ix);
|
||||
@@ -470,33 +488,39 @@ impl ChannelModalDelegate {
|
||||
&mut self,
|
||||
user_id: UserId,
|
||||
new_role: ChannelRole,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) -> Option<()> {
|
||||
let update = self.channel_store.update(cx, |store, cx| {
|
||||
store.set_member_role(self.channel_id, user_id, new_role, cx)
|
||||
});
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
cx.spawn_in(window, |picker, mut cx| async move {
|
||||
update.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
picker.update_in(&mut cx, |picker, window, cx| {
|
||||
let this = &mut picker.delegate;
|
||||
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user_id) {
|
||||
member.role = new_role;
|
||||
}
|
||||
cx.focus_self();
|
||||
cx.focus_self(window);
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_prompt_err("Failed to update role", cx, |_, _| None);
|
||||
.detach_and_prompt_err("Failed to update role", window, cx, |_, _, _| None);
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn remove_member(&mut self, user_id: UserId, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
|
||||
fn remove_member(
|
||||
&mut self,
|
||||
user_id: UserId,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) -> Option<()> {
|
||||
let update = self.channel_store.update(cx, |store, cx| {
|
||||
store.remove_member(self.channel_id, user_id, cx)
|
||||
});
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
cx.spawn_in(window, |picker, mut cx| async move {
|
||||
update.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
picker.update_in(&mut cx, |picker, window, cx| {
|
||||
let this = &mut picker.delegate;
|
||||
if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
|
||||
this.members.remove(ix);
|
||||
@@ -514,20 +538,25 @@ impl ChannelModalDelegate {
|
||||
.selected_index
|
||||
.min(this.matching_member_indices.len().saturating_sub(1));
|
||||
|
||||
picker.focus(cx);
|
||||
picker.focus(window, cx);
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_prompt_err("Failed to remove member", cx, |_, _| None);
|
||||
.detach_and_prompt_err("Failed to remove member", window, cx, |_, _, _| None);
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn invite_member(
|
||||
&mut self,
|
||||
user: Arc<User>,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) {
|
||||
let invite_member = self.channel_store.update(cx, |store, cx| {
|
||||
store.invite_member(self.channel_id, user.id, ChannelRole::Member, cx)
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.spawn_in(window, |this, mut cx| async move {
|
||||
invite_member.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
@@ -544,25 +573,30 @@ impl ChannelModalDelegate {
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_prompt_err("Failed to invite member", cx, |_, _| None);
|
||||
.detach_and_prompt_err("Failed to invite member", window, cx, |_, _, _| None);
|
||||
}
|
||||
|
||||
fn show_context_menu(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn show_context_menu(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut ModelContext<Picker<Self>>,
|
||||
) {
|
||||
let Some(membership) = self.member_at_index(ix) else {
|
||||
return;
|
||||
};
|
||||
let user_id = membership.user.id;
|
||||
let picker = cx.view().clone();
|
||||
let context_menu = ContextMenu::build(cx, |mut menu, _cx| {
|
||||
let picker = cx.model().clone();
|
||||
let context_menu = ContextMenu::build(window, cx, |mut menu, _window, _cx| {
|
||||
let role = membership.role;
|
||||
|
||||
if role == ChannelRole::Admin || role == ChannelRole::Member {
|
||||
let picker = picker.clone();
|
||||
menu = menu.entry("Demote to Guest", None, move |cx| {
|
||||
menu = menu.entry("Demote to Guest", None, move |window, cx| {
|
||||
picker.update(cx, |picker, cx| {
|
||||
picker
|
||||
.delegate
|
||||
.set_user_role(user_id, ChannelRole::Guest, cx);
|
||||
.set_user_role(user_id, ChannelRole::Guest, window, cx);
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -575,22 +609,22 @@ impl ChannelModalDelegate {
|
||||
"Demote to Member"
|
||||
};
|
||||
|
||||
menu = menu.entry(label, None, move |cx| {
|
||||
menu = menu.entry(label, None, move |window, cx| {
|
||||
picker.update(cx, |picker, cx| {
|
||||
picker
|
||||
.delegate
|
||||
.set_user_role(user_id, ChannelRole::Member, cx);
|
||||
.set_user_role(user_id, ChannelRole::Member, window, cx);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if role == ChannelRole::Member || role == ChannelRole::Guest {
|
||||
let picker = picker.clone();
|
||||
menu = menu.entry("Promote to Admin", None, move |cx| {
|
||||
menu = menu.entry("Promote to Admin", None, move |window, cx| {
|
||||
picker.update(cx, |picker, cx| {
|
||||
picker
|
||||
.delegate
|
||||
.set_user_role(user_id, ChannelRole::Admin, cx);
|
||||
.set_user_role(user_id, ChannelRole::Admin, window, cx);
|
||||
})
|
||||
});
|
||||
};
|
||||
@@ -598,20 +632,24 @@ impl ChannelModalDelegate {
|
||||
menu = menu.separator();
|
||||
menu = menu.entry("Remove from Channel", None, {
|
||||
let picker = picker.clone();
|
||||
move |cx| {
|
||||
move |window, cx| {
|
||||
picker.update(cx, |picker, cx| {
|
||||
picker.delegate.remove_member(user_id, cx);
|
||||
picker.delegate.remove_member(user_id, window, cx);
|
||||
})
|
||||
}
|
||||
});
|
||||
menu
|
||||
});
|
||||
cx.focus_view(&context_menu);
|
||||
let subscription = cx.subscribe(&context_menu, |picker, _, _: &DismissEvent, cx| {
|
||||
picker.delegate.context_menu = None;
|
||||
picker.focus(cx);
|
||||
cx.notify();
|
||||
});
|
||||
window.focus(&context_menu.focus_handle(cx));
|
||||
let subscription = cx.subscribe_in(
|
||||
&context_menu,
|
||||
window,
|
||||
|picker, _, _: &DismissEvent, window, cx| {
|
||||
picker.delegate.context_menu = None;
|
||||
picker.focus(window, cx);
|
||||
cx.notify();
|
||||
},
|
||||
);
|
||||
self.context_menu = Some((context_menu, subscription));
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user