Compare commits
365 Commits
git-panel-
...
git-panel-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8566ed816 | ||
|
|
556fe6c14f | ||
|
|
72cb786e5b | ||
|
|
73dceb5cf2 | ||
|
|
9fad819749 | ||
|
|
312534ffe7 | ||
|
|
1d176f1a6b | ||
|
|
e301b7bbf5 | ||
|
|
a047c02bcc | ||
|
|
c82a27e4f9 | ||
|
|
d44a1a4474 | ||
|
|
6852bf5488 | ||
|
|
ea678027e0 | ||
|
|
232e0883f8 | ||
|
|
8a6c6174f9 | ||
|
|
5146520df8 | ||
|
|
93007d42a6 | ||
|
|
4438c4c1c3 | ||
|
|
2a152ddcdc | ||
|
|
c62b10792c | ||
|
|
ae50babfb4 | ||
|
|
c25079adb6 | ||
|
|
197591f557 | ||
|
|
1ced12fbaf | ||
|
|
68e670bf54 | ||
|
|
8317c9215a | ||
|
|
f9ee28db5e | ||
|
|
dffdf99228 | ||
|
|
0b361e5b7c | ||
|
|
222b04548d | ||
|
|
811b872f4e | ||
|
|
d67f2d3eab | ||
|
|
eae88a5c47 | ||
|
|
a331497367 | ||
|
|
c2971819b4 | ||
|
|
a653e8adda | ||
|
|
c4b470685d | ||
|
|
5a170ad359 | ||
|
|
7a66c764b4 | ||
|
|
9d5ae516fd | ||
|
|
bb6e8053d3 | ||
|
|
c53615ff61 | ||
|
|
fffa40f973 | ||
|
|
76a8b55f77 | ||
|
|
deeccd2c63 | ||
|
|
d3fc00d5a0 | ||
|
|
d58f006498 | ||
|
|
f3e75d8ff6 | ||
|
|
d2e44ab87d | ||
|
|
56017022c4 | ||
|
|
3d8625f25c | ||
|
|
f53a17b044 | ||
|
|
57dfaa63ca | ||
|
|
4deab8a0b9 | ||
|
|
677868ba1a | ||
|
|
6af9e8ded8 | ||
|
|
f439ee0d55 | ||
|
|
44c492b3c0 | ||
|
|
0a8e9c0fe2 | ||
|
|
aa0eaea4e9 | ||
|
|
fb272c0edc | ||
|
|
bcc6d95529 | ||
|
|
a827f54022 | ||
|
|
4c47728d6f | ||
|
|
e56b692036 | ||
|
|
810b37c129 | ||
|
|
2ba91609c9 | ||
|
|
410b4bded1 | ||
|
|
dc0075b8e6 | ||
|
|
e08eba8129 | ||
|
|
92b9d80549 | ||
|
|
6fce53651b | ||
|
|
e7ca39dfe9 | ||
|
|
7d0c571a8f | ||
|
|
d2d1779e0d | ||
|
|
76d18f3cd2 | ||
|
|
7fa30f411d | ||
|
|
7075f34b47 | ||
|
|
847596af6e | ||
|
|
e36ae0465c | ||
|
|
5f7de2eb5d | ||
|
|
3c430af31a | ||
|
|
5ec924828e | ||
|
|
c968225b61 | ||
|
|
141393232e | ||
|
|
1c223d8940 | ||
|
|
80acecc91a | ||
|
|
bbce1c19d1 | ||
|
|
2856d0a661 | ||
|
|
799e81ffe5 | ||
|
|
0d30bda740 | ||
|
|
ec2506b2e7 | ||
|
|
3a061a91e7 | ||
|
|
c74e5f5de2 | ||
|
|
2023c43681 | ||
|
|
84fdcbbe7d | ||
|
|
2dec4c2c91 | ||
|
|
d02bfe1e95 | ||
|
|
154a3915a6 | ||
|
|
0f548c6add | ||
|
|
033726cf87 | ||
|
|
d83f1e8f8f | ||
|
|
1ef638d802 | ||
|
|
fcb03989d2 | ||
|
|
2a9fa0e2dc | ||
|
|
3ae6aa0e4d | ||
|
|
7506c0385b | ||
|
|
570e6c80a8 | ||
|
|
94ee2e1811 | ||
|
|
299ae92ffb | ||
|
|
de08e47e5b | ||
|
|
8151dc7696 | ||
|
|
5f1eee3c66 | ||
|
|
9613084f59 | ||
|
|
72057e5716 | ||
|
|
e25789893d | ||
|
|
b46b261f11 | ||
|
|
71a0eb3b13 | ||
|
|
dd75f85ecf | ||
|
|
b1a6e2427f | ||
|
|
bbe6bf9caf | ||
|
|
53cfb578e8 | ||
|
|
e6fe12d0e1 | ||
|
|
04cf19d49a | ||
|
|
4bd5f0d355 | ||
|
|
fdbf3d0f25 | ||
|
|
a1ef1d3f76 | ||
|
|
04518b11bc | ||
|
|
e4eef725de | ||
|
|
7c7eb98e64 | ||
|
|
7826d79a40 | ||
|
|
bb24c085be | ||
|
|
11ec25aedb | ||
|
|
39af06085a | ||
|
|
a49e394e51 | ||
|
|
e5c3d5d626 | ||
|
|
1aba459a0a | ||
|
|
8c253af451 | ||
|
|
3c207209cb | ||
|
|
663f5244ca | ||
|
|
0599f0fcb6 | ||
|
|
6e2b6258b1 | ||
|
|
82492d74a8 | ||
|
|
16ead69052 | ||
|
|
a53be7b4be | ||
|
|
a79def005d | ||
|
|
2d431e9b51 | ||
|
|
f9df8c1729 | ||
|
|
898064e6b4 | ||
|
|
8cb397cf6c | ||
|
|
374c298bd5 | ||
|
|
0e75ca8603 | ||
|
|
2c2ca9e370 | ||
|
|
3cf5ab16a9 | ||
|
|
53e1ab3c64 | ||
|
|
d0c4c0c240 | ||
|
|
20c0d72fe4 | ||
|
|
d5f058d6e2 | ||
|
|
fbef8c2b3b | ||
|
|
b009e72121 | ||
|
|
f55a3629b0 | ||
|
|
3ac0aef211 | ||
|
|
59b5b9af90 | ||
|
|
b3e36c93b4 | ||
|
|
5447821715 | ||
|
|
665717da9a | ||
|
|
28d1d2d939 | ||
|
|
44af405fb0 | ||
|
|
c11bde7bf4 | ||
|
|
0d423a7b37 | ||
|
|
a6f95a14b7 | ||
|
|
642dab82e5 | ||
|
|
38938ece49 | ||
|
|
8e12c679fc | ||
|
|
4ce1ccfc40 | ||
|
|
eedbede939 | ||
|
|
30668589ed | ||
|
|
51ac2d3667 | ||
|
|
6ef5d8f748 | ||
|
|
f912c545e7 | ||
|
|
ddc469ca3e | ||
|
|
e9bd4b2890 | ||
|
|
a119688a50 | ||
|
|
dcbff982ad | ||
|
|
ad51df7644 | ||
|
|
3f33ca01a8 | ||
|
|
5f4f3a9c87 | ||
|
|
344284e013 | ||
|
|
f6dabadaf7 | ||
|
|
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 | ||
|
|
228c89a78a | ||
|
|
3978937457 | ||
|
|
95334cb0ad | ||
|
|
cc56ed7a88 | ||
|
|
4878b9bbed | ||
|
|
e1bc48c554 | ||
|
|
9082a006d6 | ||
|
|
ebf6804afd | ||
|
|
a062c0f1bc | ||
|
|
fc5a810408 | ||
|
|
3052fc2565 | ||
|
|
80431e5518 | ||
|
|
28087934d1 | ||
|
|
5558b04223 | ||
|
|
0ca0433912 | ||
|
|
d11deff3c2 | ||
|
|
8e71e46867 | ||
|
|
ac24f074df | ||
|
|
ccf2a60039 | ||
|
|
db2aa0bca5 | ||
|
|
373854be46 | ||
|
|
eb74332e96 | ||
|
|
1932c04b84 | ||
|
|
97d9567188 | ||
|
|
53c8b48647 | ||
|
|
92fb38acb6 | ||
|
|
84392fbc2f | ||
|
|
91fdb5d2a9 | ||
|
|
8127decd2d | ||
|
|
4bf005ef52 | ||
|
|
082469e173 |
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
@@ -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
|
||||
|
||||
79
.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,31 @@ 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 [ "${{ github.event_name }}" == "merge_group" ]; then
|
||||
# When we're running in a merge queue, never assume that the changes
|
||||
# are docs-only, as there could be other PRs in the group that
|
||||
# contain non-docs changes.
|
||||
echo "Running in the merge queue"
|
||||
echo "docs_only=false" >> $GITHUB_OUTPUT
|
||||
elif git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep -qvE '^docs/'; then
|
||||
echo "Detected non-docs changes"
|
||||
echo "docs_only=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Docs-only change"
|
||||
echo "docs_only=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
migration_checks:
|
||||
name: Check Postgres and Protobuf migrations, mergability
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
@@ -81,6 +102,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 +121,7 @@ jobs:
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: check_docs_only
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -103,29 +129,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 +171,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 +182,26 @@ jobs:
|
||||
clean: false
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # 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 +212,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 +223,18 @@ jobs:
|
||||
clean: false
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # 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 +243,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 +254,19 @@ jobs:
|
||||
clean: false
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # 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:
|
||||
@@ -276,9 +322,6 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create macOS app bundle
|
||||
run: script/bundle-mac
|
||||
|
||||
@@ -289,14 +332,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 +369,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 +390,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 +417,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 +438,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
@@ -7,6 +7,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
check_formatting:
|
||||
|
||||
2
.github/workflows/publish_extension_cli.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
clean: false
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
|
||||
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-provider: "github"
|
||||
|
||||
3
.github/workflows/release_nightly.yml
vendored
@@ -86,9 +86,6 @@ jobs:
|
||||
echo "Publishing version: ${version} on release channel nightly"
|
||||
echo "nightly" > crates/zed/RELEASE_CHANNEL
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create macOS app bundle
|
||||
run: script/bundle-mac
|
||||
|
||||
|
||||
775
Cargo.lock
generated
45
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",
|
||||
@@ -148,12 +149,9 @@ members = [
|
||||
# Extensions
|
||||
#
|
||||
|
||||
"extensions/astro",
|
||||
"extensions/clojure",
|
||||
"extensions/csharp",
|
||||
"extensions/deno",
|
||||
"extensions/elixir",
|
||||
"extensions/elm",
|
||||
"extensions/emmet",
|
||||
"extensions/erlang",
|
||||
"extensions/glsl",
|
||||
@@ -305,6 +303,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 +334,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 +352,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,14 +382,14 @@ 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"
|
||||
http = "1.1"
|
||||
ignore = "0.4.22"
|
||||
image = "0.25.1"
|
||||
indexmap = { version = "1.6.2", features = ["serde"] }
|
||||
indexmap = { version = "2.7.0", features = ["serde"] }
|
||||
indoc = "2"
|
||||
itertools = "0.13.0"
|
||||
jsonwebtoken = "9.3"
|
||||
@@ -399,24 +398,24 @@ 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"
|
||||
pathdiff = "0.2"
|
||||
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
|
||||
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
|
||||
pet-pixi = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
|
||||
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
|
||||
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
|
||||
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
|
||||
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
|
||||
postage = { version = "0.5", features = ["futures-traits"] }
|
||||
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
|
||||
profiling = "1"
|
||||
@@ -442,9 +441,10 @@ runtimelib = { version = "0.24.0", default-features = false, features = [
|
||||
] }
|
||||
rustc-demangle = "0.1.23"
|
||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||
rustc-hash = "2.1.0"
|
||||
rustls = "0.21.12"
|
||||
rustls-native-certs = "0.8.0"
|
||||
schemars = { version = "0.8", features = ["impl_json_schema"] }
|
||||
schemars = { version = "0.8", features = ["impl_json_schema", "indexmap2"] }
|
||||
semver = "1.0"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
|
||||
@@ -464,13 +464,13 @@ smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
sqlformat = "0.2"
|
||||
strsim = "0.11"
|
||||
strum = { version = "0.25.0", features = ["derive"] }
|
||||
strum = { version = "0.26.0", features = ["derive"] }
|
||||
subtle = "2.5.0"
|
||||
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 +496,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 +523,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 |
@@ -1 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-search"><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M4.268 21a2 2 0 0 0 1.727 1H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v3"/><path d="m9 18-1.5-1.5"/><circle cx="5" cy="14" r="3"/></svg>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.2345 20.1C5.38772 20.373 5.60794 20.5998 5.87313 20.7577C6.13832 20.9157 6.43919 20.9992 6.74562 21H17.25C17.7141 21 18.1592 20.8104 18.4874 20.4728C18.8156 20.1352 19 19.6774 19 19.2V7.5L14.625 3H6.75C6.28587 3 5.84075 3.18964 5.51256 3.52721C5.18437 3.86477 5 4.32261 5 4.8V6.5" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 16.8182L8.5 15.3182" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 15.8182C7.65685 15.8182 9 14.475 9 12.8182C9 11.1613 7.65685 9.81818 6 9.81818C4.34315 9.81818 3 11.1613 3 12.8182C3 14.475 4.34315 15.8182 6 15.8182Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 393 B After Width: | Height: | Size: 837 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.6749 2.40608C11.8058 2.24239 11.6893 1.99991 11.4796 1.99991H2.51996C2.31033 1.99991 2.19379 2.24239 2.32474 2.40608L5.14583 5.93246C5.34148 6.17701 5.44808 6.48087 5.44808 6.79412C5.44808 7.46881 5.44808 10.334 5.44808 11.5016C5.44808 11.7778 5.67194 11.9999 5.94808 11.9999H8.05153C8.32767 11.9999 8.55153 11.7778 8.55153 11.5016C8.55153 10.334 8.55153 7.46881 8.55153 6.79412C8.55153 6.48087 8.65815 6.17701 8.8538 5.93246L11.6749 2.40608Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.9416 2.99643C13.08 2.79636 12.9568 2.5 12.7352 2.5H3.26475C3.04317 2.5 2.91999 2.79636 3.0584 2.99643L6.04033 7.30646C6.24713 7.60535 6.35981 7.97674 6.35981 8.3596C6.35981 9.18422 6.35981 11.4639 6.35981 12.891C6.35981 13.2285 6.59643 13.5 6.88831 13.5H9.11168C9.40357 13.5 9.64019 13.2285 9.64019 12.891C9.64019 11.4639 9.64019 9.18422 9.64019 8.3596C9.64019 7.97674 9.75289 7.60535 9.95969 7.30646L12.9416 2.99643Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 644 B After Width: | Height: | Size: 618 B |
@@ -1,5 +1,5 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.5 6C1.5 6.89002 1.76392 7.76004 2.25839 8.50007C2.75285 9.24009 3.45566 9.81686 4.27792 10.1575C5.10019 10.4981 6.00499 10.5872 6.87791 10.4135C7.75082 10.2399 8.55264 9.81132 9.18198 9.18198C9.81132 8.55264 10.2399 7.75082 10.4135 6.87791C10.5872 6.00499 10.4981 5.10019 10.1575 4.27792C9.81686 3.45566 9.24009 2.75285 8.50007 2.25839C7.76004 1.76392 6.89002 1.5 6 1.5C4.74198 1.50473 3.53448 1.99561 2.63 2.87L1.5 4" stroke="#919081" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M1.5 1.5V4H4" stroke="#919081" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 3.5V6L8 7" stroke="#919081" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 8C2 9.18669 2.35189 10.3467 3.01118 11.3334C3.67047 12.3201 4.60754 13.0892 5.7039 13.5433C6.80026 13.9974 8.00666 14.1162 9.17054 13.8847C10.3344 13.6532 11.4035 13.0818 12.2426 12.2426C13.0818 11.4035 13.6532 10.3344 13.8847 9.17054C14.1162 8.00666 13.9974 6.80026 13.5433 5.7039C13.0892 4.60754 12.3201 3.67047 11.3334 3.01118C10.3467 2.35189 9.18669 2 8 2C6.32263 2.00631 4.71265 2.66082 3.50667 3.82667L2 5.33333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2 2V5.33333H5.33333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 5V8.5L10 9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 778 B After Width: | Height: | Size: 840 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 12L9.41379 9.41379M2 6.31034C2 3.92981 3.92981 2 6.31034 2C8.6909 2 10.6207 3.92981 10.6207 6.31034C10.6207 8.6909 8.6909 10.6207 6.31034 10.6207C3.92981 10.6207 2 8.6909 2 6.31034Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 13L10.4138 10.4138M3 7.31034C3 4.92981 4.92981 3 7.31034 3C9.6909 3 11.6207 4.92981 11.6207 7.31034C11.6207 9.6909 9.6909 11.6207 7.31034 11.6207C4.92981 11.6207 3 9.6909 3 7.31034Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 383 B After Width: | Height: | Size: 382 B |
1
assets/icons/message_circle.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle-more"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/><path d="M8 12h.01"/><path d="M12 12h.01"/><path d="M16 12h.01"/></svg>
|
||||
|
After Width: | Height: | Size: 337 B |
@@ -1,3 +1,4 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5 0.875C5.49797 0.875 3.875 2.49797 3.875 4.5C3.875 6.15288 4.98124 7.54738 6.49373 7.98351C5.2997 8.12901 4.27557 8.55134 3.50407 9.31167C2.52216 10.2794 2.02502 11.72 2.02502 13.5999C2.02502 13.8623 2.23769 14.0749 2.50002 14.0749C2.76236 14.0749 2.97502 13.8623 2.97502 13.5999C2.97502 11.8799 3.42786 10.7206 4.17091 9.9883C4.91536 9.25463 6.02674 8.87499 7.49995 8.87499C8.97317 8.87499 10.0846 9.25463 10.8291 9.98831C11.5721 10.7206 12.025 11.8799 12.025 13.5999C12.025 13.8623 12.2376 14.0749 12.5 14.0749C12.7623 14.075 12.975 13.8623 12.975 13.6C12.975 11.72 12.4778 10.2794 11.4959 9.31166C10.7244 8.55135 9.70025 8.12903 8.50625 7.98352C10.0187 7.5474 11.125 6.15289 11.125 4.5C11.125 2.49797 9.50203 0.875 7.5 0.875ZM4.825 4.5C4.825 3.02264 6.02264 1.825 7.5 1.825C8.97736 1.825 10.175 3.02264 10.175 4.5C10.175 5.97736 8.97736 7.175 7.5 7.175C6.02264 7.175 4.825 5.97736 4.825 4.5Z" fill="black"/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.6666 14V12.6667C12.6666 11.9594 12.3856 11.2811 11.8855 10.781C11.3854 10.281 10.7072 10 9.99992 10H5.99992C5.29267 10 4.6144 10.281 4.1143 10.781C3.6142 11.2811 3.33325 11.9594 3.33325 12.6667V14" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.99992 7.33333C9.47268 7.33333 10.6666 6.13943 10.6666 4.66667C10.6666 3.19391 9.47268 2 7.99992 2C6.52716 2 5.33325 3.19391 5.33325 4.66667C5.33325 6.13943 6.52716 7.33333 7.99992 7.33333Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 690 B |
@@ -1,8 +1,4 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M8 2.75C8 2.47386 7.77614 2.25 7.5 2.25C7.22386 2.25 7 2.47386 7 2.75V7H2.75C2.47386 7 2.25 7.22386 2.25 7.5C2.25 7.77614 2.47386 8 2.75 8H7V12.25C7 12.5261 7.22386 12.75 7.5 12.75C7.77614 12.75 8 12.5261 8 12.25V8H12.25C12.5261 8 12.75 7.77614 12.75 7.5C12.75 7.22386 12.5261 7 12.25 7H8V2.75Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.33325 8H12.6666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 3.33333V12.6667" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 491 B After Width: | Height: | Size: 327 B |
@@ -1 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-cw"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 6.5L9.99556 4.21778C9.27778 3.5 8.12 3 7 3C6.20888 3 5.43552 3.2346 4.77772 3.67412C4.11992 4.11365 3.60723 4.73836 3.30448 5.46927C3.00173 6.20017 2.92252 7.00444 3.07686 7.78036C3.2312 8.55628 3.61216 9.26902 4.17157 9.82842C4.73098 10.3878 5.44372 10.7688 6.21964 10.9231C6.99556 11.0775 7.79983 10.9983 8.53073 10.6955C8.88113 10.5504 9.20712 10.357 9.5 10.1225" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 4V6.5H9.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 303 B After Width: | Height: | Size: 673 B |
@@ -1,4 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.16089 10.2476L3.99598 10.3784C4.61244 10.4749 5.05269 11.0395 5.00728 11.6755L4.94576 12.5377C4.92784 12.789 5.06165 13.0255 5.28326 13.1348L5.90091 13.4391C6.12253 13.5485 6.38717 13.5075 6.56817 13.3371L7.1888 12.7505C7.64641 12.3178 8.35245 12.3178 8.81059 12.7505L9.43121 13.3371C9.61222 13.5081 9.87629 13.5485 10.0985 13.4391L10.7173 13.1341C10.9384 13.0255 11.0716 12.7895 11.0537 12.539L10.9921 11.6755C10.9467 11.0395 11.3869 10.4749 12.0033 10.3784L12.8385 10.2476C13.0817 10.2097 13.2776 10.0233 13.3325 9.77768L13.4848 9.09455C13.5398 8.8489 13.4425 8.59408 13.2393 8.45229L12.5422 7.96404C12.0279 7.60355 11.8708 6.89963 12.1814 6.34659L12.6025 5.59745C12.7249 5.3793 12.7047 5.10616 12.5511 4.9094L12.1241 4.36128C11.9706 4.16451 11.7149 4.08325 11.4795 4.15719L10.6719 4.41016C10.0752 4.59714 9.43903 4.28367 9.20962 3.69035L8.90017 2.88803C8.80937 2.65339 8.58777 2.4994 8.34108 2.5L7.65649 2.50184C7.40979 2.50244 7.1888 2.65766 7.09921 2.89291L6.79751 3.68607C6.57053 4.28307 5.93138 4.59898 5.33284 4.41077L4.49178 4.1468C4.25583 4.07225 3.99897 4.15413 3.84545 4.35212L3.42133 4.90084C3.26781 5.09943 3.2493 5.37319 3.37414 5.59133L3.80483 6.34232C4.12201 6.89591 3.96671 7.60659 3.44941 7.96897L2.76065 8.45169C2.55756 8.59408 2.4602 8.84891 2.51516 9.09393L2.66747 9.77708C2.72184 10.0233 2.91777 10.2097 3.16089 10.2476Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.41432 6.83576C8.63332 6.05481 7.36676 6.05476 6.58575 6.83571C5.8048 7.61672 5.80476 8.88327 6.58571 9.66427C7.36671 10.4452 8.63326 10.4452 9.41426 9.66432C10.1952 8.88332 10.1952 7.61676 9.41432 6.83576Z" fill="black"/>
|
||||
<path d="M8.14667 1.33334H7.85333C7.49971 1.33334 7.16057 1.47382 6.91053 1.72387C6.66048 1.97392 6.52 2.31305 6.52 2.66668V2.78668C6.51976 3.02049 6.45804 3.25014 6.34103 3.45257C6.22401 3.655 6.05583 3.8231 5.85333 3.94001L5.56667 4.10668C5.36398 4.2237 5.13405 4.28531 4.9 4.28531C4.66595 4.28531 4.43603 4.2237 4.23333 4.10668L4.13333 4.05334C3.82738 3.87685 3.46389 3.82897 3.12267 3.92022C2.78145 4.01146 2.49037 4.23437 2.31333 4.54001L2.16667 4.79334C1.99018 5.0993 1.9423 5.46279 2.03354 5.80401C2.12478 6.14523 2.34769 6.43631 2.65333 6.61334L2.75333 6.68001C2.95485 6.79635 3.12241 6.9634 3.23937 7.16456C3.35632 7.36573 3.4186 7.59399 3.42 7.82668V8.16668C3.42093 8.40162 3.35977 8.63265 3.2427 8.83635C3.12563 9.04005 2.95681 9.2092 2.75333 9.32668L2.65333 9.38668C2.34769 9.56371 2.12478 9.85479 2.03354 10.196C1.9423 10.5372 1.99018 10.9007 2.16667 11.2067L2.31333 11.46C2.49037 11.7657 2.78145 11.9886 3.12267 12.0798C3.46389 12.171 3.82738 12.1232 4.13333 11.9467L4.23333 11.8933C4.43603 11.7763 4.66595 11.7147 4.9 11.7147C5.13405 11.7147 5.36398 11.7763 5.56667 11.8933L5.85333 12.06C6.05583 12.1769 6.22401 12.345 6.34103 12.5475C6.45804 12.7499 6.51976 12.9795 6.52 13.2133V13.3333C6.52 13.687 6.66048 14.0261 6.91053 14.2762C7.16057 14.5262 7.49971 14.6667 7.85333 14.6667H8.14667C8.50029 14.6667 8.83943 14.5262 9.08948 14.2762C9.33953 14.0261 9.48 13.687 9.48 13.3333V13.2133C9.48024 12.9795 9.54196 12.7499 9.65898 12.5475C9.77599 12.345 9.94418 12.1769 10.1467 12.06L10.4333 11.8933C10.636 11.7763 10.866 11.7147 11.1 11.7147C11.3341 11.7147 11.564 11.7763 11.7667 11.8933L11.8667 11.9467C12.1726 12.1232 12.5361 12.171 12.8773 12.0798C13.2186 11.9886 13.5096 11.7657 13.6867 11.46L13.8333 11.2C14.0098 10.8941 14.0577 10.5306 13.9665 10.1893C13.8752 9.84812 13.6523 9.55704 13.3467 9.38001L13.2467 9.32668C13.0432 9.2092 12.8744 9.04005 12.7573 8.83635C12.6402 8.63265 12.5791 8.40162 12.58 8.16668V7.83334C12.5791 7.5984 12.6402 7.36738 12.7573 7.16367C12.8744 6.95997 13.0432 6.79082 13.2467 6.67334L13.3467 6.61334C13.6523 6.43631 13.8752 6.14523 13.9665 5.80401C14.0577 5.46279 14.0098 5.0993 13.8333 4.79334L13.6867 4.54001C13.5096 4.23437 13.2186 4.01146 12.8773 3.92022C12.5361 3.82897 12.1726 3.87685 11.8667 4.05334L11.7667 4.10668C11.564 4.2237 11.3341 4.28531 11.1 4.28531C10.866 4.28531 10.636 4.2237 10.4333 4.10668L10.1467 3.94001C9.94418 3.8231 9.77599 3.655 9.65898 3.45257C9.54196 3.25014 9.48024 3.02049 9.48 2.78668V2.66668C9.48 2.31305 9.33953 1.97392 9.08948 1.72387C8.83943 1.47382 8.50029 1.33334 8.14667 1.33334Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z" fill="black"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.8 KiB |
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 |
@@ -17,6 +17,7 @@
|
||||
"escape": "menu::Cancel",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"alt-shift-enter": "menu::Restart",
|
||||
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
|
||||
"ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
|
||||
"ctrl-shift-w": "workspace::CloseWindow",
|
||||
@@ -171,6 +172,7 @@
|
||||
"context": "AssistantPanel",
|
||||
"bindings": {
|
||||
"ctrl-k c": "assistant::CopyCode",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPrevMatch",
|
||||
"ctrl-shift-m": "assistant::ToggleModelSelector",
|
||||
@@ -259,9 +261,11 @@
|
||||
"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",
|
||||
"ctrl-shift-f": "pane::DeploySearch",
|
||||
"ctrl-alt-g": "search::SelectNextMatch",
|
||||
"ctrl-alt-shift-g": "search::SelectPrevMatch",
|
||||
"ctrl-alt-shift-h": "search::ToggleReplace",
|
||||
@@ -374,6 +378,7 @@
|
||||
// Change the default action on `menu::Confirm` by setting the parameter
|
||||
// "alt-ctrl-o": ["projects::OpenRecent", { "create_new_window": true }],
|
||||
"alt-ctrl-o": "projects::OpenRecent",
|
||||
"alt-ctrl-shift-o": "projects::OpenRemote",
|
||||
"alt-ctrl-shift-b": "branches::OpenRecent",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
"ctrl-s": "workspace::Save",
|
||||
@@ -407,7 +412,7 @@
|
||||
"ctrl-shift-p": "command_palette::Toggle",
|
||||
"f1": "command_palette::Toggle",
|
||||
"ctrl-shift-m": "diagnostics::Deploy",
|
||||
"ctrl-shift-e": "pane::RevealInProjectPanel",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-b": "outline_panel::ToggleFocus",
|
||||
"ctrl-?": "assistant::ToggleFocus",
|
||||
"ctrl-alt-s": "workspace::SaveAll",
|
||||
@@ -425,7 +430,17 @@
|
||||
"ctrl-shift-r": "task::Rerun",
|
||||
"ctrl-alt-r": "task::Rerun",
|
||||
"alt-t": "task::Rerun",
|
||||
"alt-shift-t": "task::Spawn"
|
||||
"alt-shift-t": "task::Spawn",
|
||||
"alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
|
||||
// also possible to spawn tasks by name:
|
||||
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ApplicationMenu",
|
||||
"bindings": {
|
||||
"left": ["app_menu::NavigateApplicationMenuInDirection", "Left"],
|
||||
"right": ["app_menu::NavigateApplicationMenuInDirection", "Right"]
|
||||
}
|
||||
},
|
||||
// Bindings from Sublime Text
|
||||
@@ -470,18 +485,12 @@
|
||||
"context": "Editor && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !inline_completion && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion",
|
||||
"tab": "editor::ComposeCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_completion",
|
||||
"context": "Editor && inline_completion && !showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"tab": "editor::AcceptInlineCompletion"
|
||||
@@ -524,6 +533,7 @@
|
||||
"alt-enter": "editor::OpenExcerpts",
|
||||
"shift-enter": "editor::ExpandExcerpts",
|
||||
"ctrl-k enter": "editor::OpenExcerptsSplit",
|
||||
"ctrl-shift-e": "pane::RevealInProjectPanel",
|
||||
"ctrl-f8": "editor::GoToHunk",
|
||||
"ctrl-shift-f8": "editor::GoToPrevHunk",
|
||||
"ctrl-enter": "assistant::InlineAssist"
|
||||
@@ -604,7 +614,6 @@
|
||||
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-ctrl-r": "project_panel::RevealInFileManager",
|
||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrev",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"cmd-escape": "menu::Cancel",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"alt-shift-enter": "menu::Restart",
|
||||
"cmd-shift-w": "workspace::CloseWindow",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"cmd-o": "workspace::Open",
|
||||
@@ -195,6 +196,7 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-k c": "assistant::CopyCode",
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
"cmd-g": "search::SelectNextMatch",
|
||||
"cmd-shift-g": "search::SelectPrevMatch",
|
||||
"cmd-shift-m": "assistant::ToggleModelSelector",
|
||||
@@ -223,7 +225,10 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "assistant2::NewThread",
|
||||
"cmd-shift-h": "assistant2::OpenHistory"
|
||||
"cmd-shift-h": "assistant2::OpenHistory",
|
||||
"cmd-shift-m": "assistant2::ToggleModelSelector",
|
||||
"cmd-shift-a": "assistant2::ToggleContextPicker",
|
||||
"cmd-alt-e": "assistant2::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -324,6 +329,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",
|
||||
@@ -429,7 +436,7 @@
|
||||
"ctrl--": "pane::GoBack",
|
||||
"ctrl-shift--": "pane::GoForward",
|
||||
"cmd-shift-t": "pane::ReopenClosedItem",
|
||||
"cmd-shift-f": "project_search::ToggleFocus"
|
||||
"cmd-shift-f": "pane::DeploySearch"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -445,7 +452,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],
|
||||
@@ -471,7 +477,7 @@
|
||||
"ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }],
|
||||
"cmd-shift-p": "command_palette::Toggle",
|
||||
"cmd-shift-m": "diagnostics::Deploy",
|
||||
"cmd-shift-e": "pane::RevealInProjectPanel",
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
"cmd-shift-b": "outline_panel::ToggleFocus",
|
||||
"cmd-?": "assistant::ToggleFocus",
|
||||
"cmd-alt-s": "workspace::SaveAll",
|
||||
@@ -492,10 +498,12 @@
|
||||
"context": "Workspace && !Terminal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "workspace::NewFile",
|
||||
"cmd-shift-r": "task::Spawn",
|
||||
"cmd-alt-r": "task::Rerun",
|
||||
"alt-t": "task::Spawn",
|
||||
"alt-shift-t": "task::Spawn"
|
||||
"ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
|
||||
// also possible to spawn tasks by name:
|
||||
// "foo-bar": ["task_name::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
|
||||
}
|
||||
},
|
||||
// Bindings from Sublime Text
|
||||
@@ -541,18 +549,12 @@
|
||||
"context": "Editor && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !inline_completion && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion",
|
||||
"tab": "editor::ComposeCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_completion",
|
||||
"context": "Editor && inline_completion && !showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"tab": "editor::AcceptInlineCompletion"
|
||||
@@ -595,6 +597,7 @@
|
||||
"alt-enter": "editor::OpenExcerpts",
|
||||
"shift-enter": "editor::ExpandExcerpts",
|
||||
"cmd-k enter": "editor::OpenExcerptsSplit",
|
||||
"cmd-shift-e": "pane::RevealInProjectPanel",
|
||||
"cmd-f8": "editor::GoToHunk",
|
||||
"cmd-shift-f8": "editor::GoToPrevHunk",
|
||||
"ctrl-enter": "assistant::InlineAssist"
|
||||
@@ -612,6 +615,8 @@
|
||||
"context": "PromptEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-a": "assistant2::ToggleContextPicker",
|
||||
"cmd-alt-e": "assistant2::RemoveAllContext",
|
||||
"ctrl-[": "assistant::CyclePreviousInlineAssist",
|
||||
"ctrl-]": "assistant::CycleNextInlineAssist"
|
||||
}
|
||||
@@ -662,7 +667,6 @@
|
||||
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-cmd-r": "project_panel::RevealInFileManager",
|
||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"cmd-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
@@ -677,6 +681,38 @@
|
||||
"space": "project_panel::Open"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel && !CommitEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "git_panel::Close"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel && ChangesList",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "menu::SelectPrev",
|
||||
"down": "menu::SelectNext",
|
||||
"cmd-up": "menu::SelectFirst",
|
||||
"cmd-down": "menu::SelectLast",
|
||||
"enter": "menu::Confirm",
|
||||
"space": "git::ToggleStaged",
|
||||
"cmd-shift-space": "git::StageAll",
|
||||
"ctrl-shift-space": "git::UnstageAll",
|
||||
"alt-down": "git_panel::FocusEditor"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel && CommitEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"alt-up": "git_panel::FocusChanges",
|
||||
"escape": "git_panel::FocusChanges",
|
||||
"cmd-enter": "git::CommitChanges",
|
||||
"cmd-alt-enter": "git::CommitAllChanges"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel && not_editing",
|
||||
"use_key_equivalents": true,
|
||||
@@ -762,6 +798,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"],
|
||||
|
||||
@@ -4,55 +4,72 @@
|
||||
// from the command palette.
|
||||
[
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-g": "editor::Cancel",
|
||||
"ctrl-shift-g": "go_to_line::Toggle",
|
||||
//"ctrl-space": "editor::SetMark",
|
||||
"ctrl-x u": "editor::Undo",
|
||||
"ctrl-x ctrl-u": "editor::Redo",
|
||||
"ctrl-f": "editor::MoveRight",
|
||||
"ctrl-b": "editor::MoveLeft",
|
||||
"ctrl-n": "editor::MoveDown",
|
||||
"ctrl-p": "editor::MoveUp",
|
||||
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
|
||||
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
|
||||
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
|
||||
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
|
||||
"alt-f": "editor::MoveToNextSubwordEnd",
|
||||
"alt-b": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-d": "editor::Delete",
|
||||
"alt-d": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-k": "editor::CutToEndOfLine",
|
||||
"ctrl-w": "editor::Cut",
|
||||
"alt-w": "editor::Copy",
|
||||
"ctrl-y": "editor::Paste",
|
||||
"ctrl-_": "editor::Undo",
|
||||
"ctrl-v": "editor::MovePageDown",
|
||||
"alt-v": "editor::MovePageUp",
|
||||
"ctrl-x ]": "editor::MoveToEnd",
|
||||
"ctrl-x [": "editor::MoveToBeginning",
|
||||
"ctrl-l": "editor::ScrollCursorCenterTopBottom",
|
||||
"ctrl-s": "buffer_search::Deploy",
|
||||
"ctrl-x ctrl-f": "file_finder::Toggle",
|
||||
"ctrl-shift-r": "editor::Rename"
|
||||
"ctrl-g": "menu::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-x k": "pane::CloseActiveItem",
|
||||
"ctrl-x ctrl-c": "workspace::CloseWindow",
|
||||
"ctrl-x o": "workspace::ActivateNextPane",
|
||||
"ctrl-x b": "tab_switcher::Toggle",
|
||||
"ctrl-x 0": "pane::CloseActiveItem",
|
||||
"ctrl-x 1": "pane::CloseInactiveItems",
|
||||
"ctrl-x 2": "pane::SplitVertical",
|
||||
"ctrl-x ctrl-f": "file_finder::Toggle",
|
||||
"ctrl-x ctrl-s": "workspace::Save",
|
||||
"ctrl-x ctrl-w": "workspace::SaveAs",
|
||||
"ctrl-x s": "workspace::SaveAll",
|
||||
"shift shift": "file_finder::Toggle"
|
||||
"ctrl-g": "editor::Cancel",
|
||||
"ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer
|
||||
"alt-g g": "go_to_line::Toggle", // goto-line
|
||||
"alt-g alt-g": "go_to_line::Toggle", // goto-line
|
||||
//"ctrl-space": "editor::SetMark",
|
||||
"ctrl-f": "editor::MoveRight", // forward-char
|
||||
"ctrl-b": "editor::MoveLeft", // backward-char
|
||||
"ctrl-n": "editor::MoveDown", // next-line
|
||||
"ctrl-p": "editor::MoveUp", // previous-line
|
||||
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line
|
||||
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line
|
||||
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line
|
||||
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line
|
||||
"alt-f": "editor::MoveToNextSubwordEnd", // forward-word
|
||||
"alt-b": "editor::MoveToPreviousSubwordStart", // backward-word
|
||||
"alt-u": "editor::ConvertToUpperCase", // upcase-word
|
||||
"alt-l": "editor::ConvertToLowerCase", // downcase-word
|
||||
"alt-c": "editor::ConvertToUpperCamelCase", // capitalize-word
|
||||
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"ctrl-x ctrl-;": "editor::ToggleComments",
|
||||
"alt-.": "editor::GoToDefinition", // xref-find-definitions
|
||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||
"ctrl-d": "editor::Delete", // delete-char
|
||||
"alt-d": "editor::DeleteToNextWordEnd", // kill-word
|
||||
"ctrl-k": "editor::KillRingCut", // kill-line
|
||||
"ctrl-w": "editor::Cut", // kill-region
|
||||
"alt-w": "editor::Copy", // kill-ring-save
|
||||
"ctrl-y": "editor::KillRingYank", // yank
|
||||
"ctrl-_": "editor::Undo", // undo
|
||||
"ctrl-/": "editor::Undo", // undo
|
||||
"ctrl-x u": "editor::Undo", // undo
|
||||
"ctrl-v": "editor::MovePageDown", // scroll-up
|
||||
"alt-v": "editor::MovePageUp", // scroll-down
|
||||
"ctrl-x [": "editor::MoveToBeginning", // beginning-of-buffer
|
||||
"ctrl-x ]": "editor::MoveToEnd", // end-of-buffer
|
||||
"alt-<": "editor::MoveToBeginning", // beginning-of-buffer
|
||||
"alt->": "editor::MoveToEnd", // end-of-buffer
|
||||
"ctrl-l": "editor::ScrollCursorCenterTopBottom", // recenter-top-bottom
|
||||
"ctrl-s": "buffer_search::Deploy", // isearch-forward
|
||||
"alt-^": "editor::JoinLines" // join-line
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace && !Terminal",
|
||||
"bindings": {
|
||||
"ctrl-x ctrl-c": "zed::Quit", // save-buffers-kill-terminal
|
||||
"ctrl-x 5 0": "workspace::CloseWindow", // delete-frame
|
||||
"ctrl-x 5 2": "workspace::NewWindow", // make-frame-command
|
||||
"ctrl-x o": "workspace::ActivateNextPane", // other-window
|
||||
"ctrl-x k": "pane::CloseActiveItem", // kill-buffer
|
||||
"ctrl-x 0": "pane::CloseActiveItem", // delete-window
|
||||
"ctrl-x 1": "pane::CloseInactiveItems", // delete-other-windows
|
||||
"ctrl-x 2": "pane::SplitDown", // split-window-below
|
||||
"ctrl-x 3": "pane::SplitRight", // split-window-right
|
||||
"ctrl-x ctrl-f": "file_finder::Toggle", // find-file
|
||||
"ctrl-x ctrl-s": "workspace::Save", // save-buffer
|
||||
"ctrl-x ctrl-w": "workspace::SaveAs", // write-file
|
||||
"ctrl-x s": "workspace::SaveAll" // save-some-buffers
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,12 +4,32 @@
|
||||
"ctrl-shift-[": "pane::ActivatePrevItem",
|
||||
"ctrl-shift-]": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem"
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-1": ["workspace::ActivatePane", 0],
|
||||
"ctrl-2": ["workspace::ActivatePane", 1],
|
||||
"ctrl-3": ["workspace::ActivatePane", 2],
|
||||
"ctrl-4": ["workspace::ActivatePane", 3],
|
||||
"ctrl-5": ["workspace::ActivatePane", 4],
|
||||
"ctrl-6": ["workspace::ActivatePane", 5],
|
||||
"ctrl-7": ["workspace::ActivatePane", 6],
|
||||
"ctrl-8": ["workspace::ActivatePane", 7],
|
||||
"ctrl-9": ["workspace::ActivatePane", 8],
|
||||
"ctrl-shift-1": ["workspace::MoveItemToPane", { "destination": 0, "focus": true }],
|
||||
"ctrl-shift-2": ["workspace::MoveItemToPane", { "destination": 1 }],
|
||||
"ctrl-shift-3": ["workspace::MoveItemToPane", { "destination": 2 }],
|
||||
"ctrl-shift-4": ["workspace::MoveItemToPane", { "destination": 3 }],
|
||||
"ctrl-shift-5": ["workspace::MoveItemToPane", { "destination": 4 }],
|
||||
"ctrl-shift-6": ["workspace::MoveItemToPane", { "destination": 5 }],
|
||||
"ctrl-shift-7": ["workspace::MoveItemToPane", { "destination": 6 }],
|
||||
"ctrl-shift-8": ["workspace::MoveItemToPane", { "destination": 7 }],
|
||||
"ctrl-shift-9": ["workspace::MoveItemToPane", { "destination": 8 }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-alt-up": "editor::AddSelectionAbove",
|
||||
"ctrl-alt-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-up": "editor::MoveLineUp",
|
||||
"ctrl-shift-down": "editor::MoveLineDown",
|
||||
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
|
||||
@@ -17,6 +37,8 @@
|
||||
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-d": "editor::DuplicateSelection",
|
||||
"alt-f3": "editor::SelectAllMatches", // find_all_under
|
||||
"f9": "editor::SortLinesCaseSensitive",
|
||||
"ctrl-f9": "editor::SortLinesCaseInsensitive",
|
||||
"f12": "editor::GoToDefinition",
|
||||
"ctrl-f12": "editor::GoToDefinitionSplit",
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
|
||||
@@ -3,56 +3,85 @@
|
||||
// To see the default key bindings run `zed: open default keymap`
|
||||
// from the command palette.
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-g": "menu::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-g": "editor::Cancel",
|
||||
"ctrl-shift-g": "go_to_line::Toggle",
|
||||
"ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer
|
||||
"alt-g g": "go_to_line::Toggle", // goto-line
|
||||
"alt-g alt-g": "go_to_line::Toggle", // goto-line
|
||||
//"ctrl-space": "editor::SetMark",
|
||||
"ctrl-x u": "editor::Undo",
|
||||
"ctrl-x ctrl-u": "editor::Redo",
|
||||
"ctrl-f": "editor::MoveRight",
|
||||
"ctrl-b": "editor::MoveLeft",
|
||||
"ctrl-n": "editor::MoveDown",
|
||||
"ctrl-p": "editor::MoveUp",
|
||||
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
|
||||
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
|
||||
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
|
||||
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
|
||||
"alt-f": "editor::MoveToNextSubwordEnd",
|
||||
"alt-b": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-d": "editor::Delete",
|
||||
"alt-d": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-k": "editor::CutToEndOfLine",
|
||||
"ctrl-w": "editor::Cut",
|
||||
"alt-w": "editor::Copy",
|
||||
"ctrl-y": "editor::Paste",
|
||||
"ctrl-_": "editor::Undo",
|
||||
"ctrl-v": "editor::MovePageDown",
|
||||
"alt-v": "editor::MovePageUp",
|
||||
"ctrl-x ]": "editor::MoveToEnd",
|
||||
"ctrl-x [": "editor::MoveToBeginning",
|
||||
"ctrl-l": "editor::ScrollCursorCenterTopBottom",
|
||||
"ctrl-s": "buffer_search::Deploy",
|
||||
"ctrl-x ctrl-f": "file_finder::Toggle",
|
||||
"ctrl-shift-r": "editor::Rename"
|
||||
"ctrl-f": "editor::MoveRight", // forward-char
|
||||
"ctrl-b": "editor::MoveLeft", // backward-char
|
||||
"ctrl-n": "editor::MoveDown", // next-line
|
||||
"ctrl-p": "editor::MoveUp", // previous-line
|
||||
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line
|
||||
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line
|
||||
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line
|
||||
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line
|
||||
"alt-f": "editor::MoveToNextSubwordEnd", // forward-word
|
||||
"alt-b": "editor::MoveToPreviousSubwordStart", // backward-word
|
||||
"alt-u": "editor::ConvertToUpperCase", // upcase-word
|
||||
"alt-l": "editor::ConvertToLowerCase", // downcase-word
|
||||
"alt-c": "editor::ConvertToUpperCamelCase", // capitalize-word
|
||||
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"ctrl-x ctrl-;": "editor::ToggleComments",
|
||||
"alt-.": "editor::GoToDefinition", // xref-find-definitions
|
||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||
"ctrl-d": "editor::Delete", // delete-char
|
||||
"alt-d": "editor::DeleteToNextWordEnd", // kill-word
|
||||
"ctrl-k": "editor::KillRingCut", // kill-line
|
||||
"ctrl-w": "editor::Cut", // kill-region
|
||||
"alt-w": "editor::Copy", // kill-ring-save
|
||||
"ctrl-y": "editor::KillRingYank", // yank
|
||||
"ctrl-_": "editor::Undo", // undo
|
||||
"ctrl-/": "editor::Undo", // undo
|
||||
"ctrl-x u": "editor::Undo", // undo
|
||||
"ctrl-v": "editor::MovePageDown", // scroll-up
|
||||
"alt-v": "editor::MovePageUp", // scroll-down
|
||||
"ctrl-x [": "editor::MoveToBeginning", // beginning-of-buffer
|
||||
"ctrl-x ]": "editor::MoveToEnd", // end-of-buffer
|
||||
"alt-<": "editor::MoveToBeginning", // beginning-of-buffer
|
||||
"alt->": "editor::MoveToEnd", // end-of-buffer
|
||||
"ctrl-l": "editor::ScrollCursorCenterTopBottom", // recenter-top-bottom
|
||||
"ctrl-s": "buffer_search::Deploy", // isearch-forward
|
||||
"alt-^": "editor::JoinLines" // join-line
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"ctrl-x k": "pane::CloseActiveItem",
|
||||
"ctrl-x ctrl-c": "workspace::CloseWindow",
|
||||
"ctrl-x o": "workspace::ActivateNextPane",
|
||||
"ctrl-x b": "tab_switcher::Toggle",
|
||||
"ctrl-x 0": "pane::CloseActiveItem",
|
||||
"ctrl-x 1": "pane::CloseInactiveItems",
|
||||
"ctrl-x 2": "pane::SplitVertical",
|
||||
"ctrl-x ctrl-f": "file_finder::Toggle",
|
||||
"ctrl-x ctrl-s": "workspace::Save",
|
||||
"ctrl-x ctrl-w": "workspace::SaveAs",
|
||||
"ctrl-x s": "workspace::SaveAll",
|
||||
"shift shift": "file_finder::Toggle"
|
||||
"ctrl-x ctrl-c": "zed::Quit", // save-buffers-kill-terminal
|
||||
"ctrl-x 5 0": "workspace::CloseWindow", // delete-frame
|
||||
"ctrl-x 5 2": "workspace::NewWindow", // make-frame-command
|
||||
"ctrl-x o": "workspace::ActivateNextPane", // other-window
|
||||
"ctrl-x k": "pane::CloseActiveItem", // kill-buffer
|
||||
"ctrl-x 0": "pane::CloseActiveItem", // delete-window
|
||||
"ctrl-x 1": "pane::CloseInactiveItems", // delete-other-windows
|
||||
"ctrl-x 2": "pane::SplitDown", // split-window-below
|
||||
"ctrl-x 3": "pane::SplitRight", // split-window-right
|
||||
"ctrl-x ctrl-f": "file_finder::Toggle", // find-file
|
||||
"ctrl-x ctrl-s": "workspace::Save", // save-buffer
|
||||
"ctrl-x ctrl-w": "workspace::SaveAs", // write-file
|
||||
"ctrl-x s": "workspace::SaveAll" // save-some-buffers
|
||||
}
|
||||
},
|
||||
{
|
||||
// Workaround to enable using emacs in the Zed terminal.
|
||||
// Unbind so Zed ignores these keys and lets emacs handle them.
|
||||
"context": "Terminal",
|
||||
"bindings": {
|
||||
"ctrl-x ctrl-c": null, // save-buffers-kill-terminal
|
||||
"ctrl-x ctrl-f": null, // find-file
|
||||
"ctrl-x ctrl-s": null, // save-buffer
|
||||
"ctrl-x ctrl-w": null, // write-file
|
||||
"ctrl-x s": null // save-some-buffers
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,7 +4,25 @@
|
||||
"cmd-shift-[": "pane::ActivatePrevItem",
|
||||
"cmd-shift-]": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem"
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-1": ["workspace::ActivatePane", 0],
|
||||
"ctrl-2": ["workspace::ActivatePane", 1],
|
||||
"ctrl-3": ["workspace::ActivatePane", 2],
|
||||
"ctrl-4": ["workspace::ActivatePane", 3],
|
||||
"ctrl-5": ["workspace::ActivatePane", 4],
|
||||
"ctrl-6": ["workspace::ActivatePane", 5],
|
||||
"ctrl-7": ["workspace::ActivatePane", 6],
|
||||
"ctrl-8": ["workspace::ActivatePane", 7],
|
||||
"ctrl-9": ["workspace::ActivatePane", 8],
|
||||
"ctrl-shift-1": ["workspace::MoveItemToPane", { "destination": 0, "focus": true }],
|
||||
"ctrl-shift-2": ["workspace::MoveItemToPane", { "destination": 1 }],
|
||||
"ctrl-shift-3": ["workspace::MoveItemToPane", { "destination": 2 }],
|
||||
"ctrl-shift-4": ["workspace::MoveItemToPane", { "destination": 3 }],
|
||||
"ctrl-shift-5": ["workspace::MoveItemToPane", { "destination": 4 }],
|
||||
"ctrl-shift-6": ["workspace::MoveItemToPane", { "destination": 5 }],
|
||||
"ctrl-shift-7": ["workspace::MoveItemToPane", { "destination": 6 }],
|
||||
"ctrl-shift-8": ["workspace::MoveItemToPane", { "destination": 7 }],
|
||||
"ctrl-shift-9": ["workspace::MoveItemToPane", { "destination": 8 }]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -20,6 +38,8 @@
|
||||
"cmd-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
"cmd-shift-d": "editor::DuplicateSelection",
|
||||
"ctrl-cmd-g": "editor::SelectAllMatches", // find_all_under
|
||||
"f5": "editor::SortLinesCaseSensitive",
|
||||
"ctrl-f5": "editor::SortLinesCaseInsensitive",
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
"alt-cmd-down": "editor::GoToDefinition",
|
||||
"ctrl-alt-cmd-down": "editor::GoToDefinitionSplit",
|
||||
|
||||
@@ -197,6 +197,7 @@
|
||||
"d": ["vim::PushOperator", "Delete"],
|
||||
"shift-d": "vim::DeleteToEndOfLine",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"g shift-j": "vim::JoinLinesNoWhitespace",
|
||||
"y": ["vim::PushOperator", "Yank"],
|
||||
"shift-y": "vim::YankLine",
|
||||
"i": "vim::InsertBefore",
|
||||
@@ -230,8 +231,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",
|
||||
@@ -259,7 +260,7 @@
|
||||
"shift-d": "vim::VisualDeleteLine",
|
||||
"shift-x": "vim::VisualDeleteLine",
|
||||
"y": "vim::VisualYank",
|
||||
"shift-y": "vim::VisualYank",
|
||||
"shift-y": "vim::VisualYankLine",
|
||||
"p": "vim::Paste",
|
||||
"shift-p": ["vim::Paste", { "preserveClipboard": true }],
|
||||
"s": "vim::Substitute",
|
||||
@@ -278,6 +279,7 @@
|
||||
"g shift-i": "vim::VisualInsertFirstNonWhiteSpace",
|
||||
"g shift-a": "vim::VisualInsertEndOfLine",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"g shift-j": "vim::JoinLinesNoWhitespace",
|
||||
"r": ["vim::PushOperator", "Replace"],
|
||||
"ctrl-c": ["vim::SwitchMode", "Normal"],
|
||||
"escape": ["vim::SwitchMode", "Normal"],
|
||||
@@ -395,6 +397,7 @@
|
||||
"'": "vim::Quotes",
|
||||
"`": "vim::BackQuotes",
|
||||
"\"": "vim::DoubleQuotes",
|
||||
"q": "vim::AnyQuotes",
|
||||
"|": "vim::VerticalBars",
|
||||
"(": "vim::Parentheses",
|
||||
")": "vim::Parentheses",
|
||||
|
||||
@@ -101,6 +101,8 @@
|
||||
// Whether to show the informational hover box when moving the mouse
|
||||
// over symbols in the editor.
|
||||
"hover_popover_enabled": true,
|
||||
// Time to wait before showing the informational hover box
|
||||
"hover_popover_delay": 350,
|
||||
// Whether to confirm before quitting Zed.
|
||||
"confirm_quit": false,
|
||||
// Whether to restore last closed project when fresh Zed instance is opened.
|
||||
@@ -158,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
|
||||
@@ -251,8 +256,20 @@
|
||||
"search_results": true,
|
||||
// Whether to show selected symbol occurrences in the scrollbar.
|
||||
"selected_symbol": true,
|
||||
// Whether to show diagnostic indicators in the scrollbar.
|
||||
"diagnostics": true
|
||||
// Which diagnostic indicators to show in the scrollbar:
|
||||
// - "none" or false: do not show diagnostics
|
||||
// - "error": show only errors
|
||||
// - "warning": show only errors and warnings
|
||||
// - "information": show only errors, warnings, and information
|
||||
// - "all" or true: show all diagnostics
|
||||
"diagnostics": "all",
|
||||
/// 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,
|
||||
@@ -302,6 +319,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,
|
||||
@@ -467,8 +486,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.
|
||||
@@ -480,7 +501,11 @@
|
||||
// Where to the git panel. Can be 'left' or 'right'.
|
||||
"dock": "left",
|
||||
// Default width of the git panel.
|
||||
"default_width": 360
|
||||
"default_width": 360,
|
||||
"status_style": "icon",
|
||||
"scrollbar": {
|
||||
"show": "auto"
|
||||
}
|
||||
},
|
||||
"message_editor": {
|
||||
// Whether to automatically replace emoji shortcodes with emoji characters.
|
||||
@@ -552,6 +577,8 @@
|
||||
// 4. Save when idle for a certain amount of time:
|
||||
// "autosave": { "after_delay": {"milliseconds": 500} },
|
||||
"autosave": "off",
|
||||
// Maximum number of tabs per pane. Unset for unlimited.
|
||||
"max_tabs": null,
|
||||
// Settings related to the editor's tab bar.
|
||||
"tab_bar": {
|
||||
// Whether or not to show the tab bar in the editor
|
||||
@@ -723,7 +750,7 @@
|
||||
// Delay is restarted with every cursor movement.
|
||||
// "delay_ms": 600
|
||||
//
|
||||
// Whether or not do display the git commit summary on the same line.
|
||||
// Whether or not to display the git commit summary on the same line.
|
||||
// "show_commit_summary": false
|
||||
//
|
||||
// The minimum column number to show the inline blame information at
|
||||
@@ -1085,6 +1112,9 @@
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"Zig": {
|
||||
"language_servers": ["zls", "..."]
|
||||
}
|
||||
},
|
||||
// Different settings for specific language models.
|
||||
|
||||
@@ -15,10 +15,14 @@
|
||||
// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`.
|
||||
"allow_concurrent_runs": false,
|
||||
// What to do with the terminal pane and tab, after the command was started:
|
||||
// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
|
||||
// * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it
|
||||
// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
|
||||
// * `always` — always show the task's pane, and focus the corresponding tab in it (default)
|
||||
// * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it
|
||||
// * `never` — do not alter focus, but still add/reuse the task's tab in its pane
|
||||
"reveal": "always",
|
||||
// Where to place the task's terminal item after starting the task:
|
||||
// * `dock` — in the terminal dock, "regular" terminal items' place (default)
|
||||
// * `center` — in the central pane group, "main" editor area
|
||||
"reveal_target": "dock",
|
||||
// What to do with the terminal pane and tab, after the command had finished:
|
||||
// * `never` — Do nothing when the command finishes (default)
|
||||
// * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"style": {
|
||||
"border": "#464b57ff",
|
||||
"border.variant": "#363c46ff",
|
||||
"border.focused": "#293b5bff",
|
||||
"border.focused": "#47679eff",
|
||||
"border.selected": "#293b5bff",
|
||||
"border.transparent": "#00000000",
|
||||
"border.disabled": "#414754ff",
|
||||
@@ -384,7 +384,7 @@
|
||||
"style": {
|
||||
"border": "#c9c9caff",
|
||||
"border.variant": "#dfdfe0ff",
|
||||
"border.focused": "#cbcdf6ff",
|
||||
"border.focused": "#7d82e8ff",
|
||||
"border.selected": "#cbcdf6ff",
|
||||
"border.transparent": "#00000000",
|
||||
"border.disabled": "#d3d3d4ff",
|
||||
|
||||
@@ -493,7 +493,7 @@ impl Render for ActivityIndicator {
|
||||
}),
|
||||
),
|
||||
)
|
||||
.anchor(gpui::AnchorCorner::BottomLeft)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.menu(move |cx| {
|
||||
let strong_this = this.upgrade()?;
|
||||
let mut has_work = false;
|
||||
|
||||
@@ -30,6 +30,8 @@ pub enum Model {
|
||||
#[default]
|
||||
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
|
||||
Claude3_5Sonnet,
|
||||
#[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
|
||||
Claude3_5Haiku,
|
||||
#[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
|
||||
Claude3Opus,
|
||||
#[serde(rename = "claude-3-sonnet", alias = "claude-3-sonnet-latest")]
|
||||
@@ -48,6 +50,8 @@ pub enum Model {
|
||||
cache_configuration: Option<AnthropicModelCacheConfiguration>,
|
||||
max_output_tokens: Option<u32>,
|
||||
default_temperature: Option<f32>,
|
||||
#[serde(default)]
|
||||
extra_beta_headers: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -55,6 +59,8 @@ impl Model {
|
||||
pub fn from_id(id: &str) -> Result<Self> {
|
||||
if id.starts_with("claude-3-5-sonnet") {
|
||||
Ok(Self::Claude3_5Sonnet)
|
||||
} else if id.starts_with("claude-3-5-haiku") {
|
||||
Ok(Self::Claude3_5Haiku)
|
||||
} else if id.starts_with("claude-3-opus") {
|
||||
Ok(Self::Claude3Opus)
|
||||
} else if id.starts_with("claude-3-sonnet") {
|
||||
@@ -69,6 +75,7 @@ impl Model {
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
|
||||
Model::Claude3_5Haiku => "claude-3-5-haiku-latest",
|
||||
Model::Claude3Opus => "claude-3-opus-latest",
|
||||
Model::Claude3Sonnet => "claude-3-sonnet-latest",
|
||||
Model::Claude3Haiku => "claude-3-haiku-latest",
|
||||
@@ -79,6 +86,7 @@ impl Model {
|
||||
pub fn display_name(&self) -> &str {
|
||||
match self {
|
||||
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
||||
Self::Claude3_5Haiku => "Claude 3.5 Haiku",
|
||||
Self::Claude3Opus => "Claude 3 Opus",
|
||||
Self::Claude3Sonnet => "Claude 3 Sonnet",
|
||||
Self::Claude3Haiku => "Claude 3 Haiku",
|
||||
@@ -90,11 +98,13 @@ impl Model {
|
||||
|
||||
pub fn cache_configuration(&self) -> Option<AnthropicModelCacheConfiguration> {
|
||||
match self {
|
||||
Self::Claude3_5Sonnet | Self::Claude3Haiku => Some(AnthropicModelCacheConfiguration {
|
||||
min_total_token: 2_048,
|
||||
should_speculate: true,
|
||||
max_cache_anchors: 4,
|
||||
}),
|
||||
Self::Claude3_5Sonnet | Self::Claude3_5Haiku | Self::Claude3Haiku => {
|
||||
Some(AnthropicModelCacheConfiguration {
|
||||
min_total_token: 2_048,
|
||||
should_speculate: true,
|
||||
max_cache_anchors: 4,
|
||||
})
|
||||
}
|
||||
Self::Custom {
|
||||
cache_configuration,
|
||||
..
|
||||
@@ -106,6 +116,7 @@ impl Model {
|
||||
pub fn max_token_count(&self) -> usize {
|
||||
match self {
|
||||
Self::Claude3_5Sonnet
|
||||
| Self::Claude3_5Haiku
|
||||
| Self::Claude3Opus
|
||||
| Self::Claude3Sonnet
|
||||
| Self::Claude3Haiku => 200_000,
|
||||
@@ -116,7 +127,7 @@ impl Model {
|
||||
pub fn max_output_tokens(&self) -> u32 {
|
||||
match self {
|
||||
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 4_096,
|
||||
Self::Claude3_5Sonnet => 8_192,
|
||||
Self::Claude3_5Sonnet | Self::Claude3_5Haiku => 8_192,
|
||||
Self::Custom {
|
||||
max_output_tokens, ..
|
||||
} => max_output_tokens.unwrap_or(4_096),
|
||||
@@ -126,6 +137,7 @@ impl Model {
|
||||
pub fn default_temperature(&self) -> f32 {
|
||||
match self {
|
||||
Self::Claude3_5Sonnet
|
||||
| Self::Claude3_5Haiku
|
||||
| Self::Claude3Opus
|
||||
| Self::Claude3Sonnet
|
||||
| Self::Claude3Haiku => 1.0,
|
||||
@@ -136,6 +148,24 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn beta_headers(&self) -> String {
|
||||
let mut headers = vec!["prompt-caching-2024-07-31".to_string()];
|
||||
|
||||
if let Self::Custom {
|
||||
extra_beta_headers, ..
|
||||
} = self
|
||||
{
|
||||
headers.extend(
|
||||
extra_beta_headers
|
||||
.iter()
|
||||
.filter(|header| !header.trim().is_empty())
|
||||
.cloned(),
|
||||
);
|
||||
}
|
||||
|
||||
headers.join(",")
|
||||
}
|
||||
|
||||
pub fn tool_model_id(&self) -> &str {
|
||||
if let Self::Custom {
|
||||
tool_override: Some(tool_override),
|
||||
@@ -156,11 +186,12 @@ pub async fn complete(
|
||||
request: Request,
|
||||
) -> Result<Response, AnthropicError> {
|
||||
let uri = format!("{api_url}/v1/messages");
|
||||
let model = Model::from_id(&request.model)?;
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("Anthropic-Version", "2023-06-01")
|
||||
.header("Anthropic-Beta", "prompt-caching-2024-07-31")
|
||||
.header("Anthropic-Beta", model.beta_headers())
|
||||
.header("X-Api-Key", api_key)
|
||||
.header("Content-Type", "application/json");
|
||||
|
||||
@@ -271,14 +302,12 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
stream: true,
|
||||
};
|
||||
let uri = format!("{api_url}/v1/messages");
|
||||
let model = Model::from_id(&request.base.model)?;
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("Anthropic-Version", "2023-06-01")
|
||||
.header(
|
||||
"Anthropic-Beta",
|
||||
"tools-2024-04-04,prompt-caching-2024-07-31,max-tokens-3-5-sonnet-2024-07-15",
|
||||
)
|
||||
.header("Anthropic-Beta", model.beta_headers())
|
||||
.header("X-Api-Key", api_key)
|
||||
.header("Content-Type", "application/json");
|
||||
let serialized_request =
|
||||
|
||||
@@ -103,6 +103,7 @@ pretty_assertions.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
terminal_view = { workspace = true, features = ["test-support"] }
|
||||
text = { workspace = true, features = ["test-support"] }
|
||||
tree-sitter-md.workspace = true
|
||||
unindent.workspace = true
|
||||
|
||||
@@ -37,7 +37,7 @@ pub use prompts::PromptBuilder;
|
||||
use prompts::PromptLoadingParams;
|
||||
use semantic_index::{CloudEmbeddingProvider, SemanticDb};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use slash_command::search_command::SearchSlashCommandFeatureFlag;
|
||||
use slash_command::{
|
||||
auto_command, cargo_workspace_command, default_command, delta_command, diagnostics_command,
|
||||
@@ -199,16 +199,6 @@ pub fn init(
|
||||
AssistantSettings::register(cx);
|
||||
SlashCommandSettings::register(cx);
|
||||
|
||||
// TODO: remove this when 0.148.0 is released.
|
||||
if AssistantSettings::get_global(cx).using_outdated_settings_version {
|
||||
update_settings_file::<AssistantSettings>(fs.clone(), cx, {
|
||||
let fs = fs.clone();
|
||||
|content, cx| {
|
||||
content.update_file(fs, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cx.spawn(|mut cx| {
|
||||
let client = client.clone();
|
||||
async move {
|
||||
|
||||
@@ -108,7 +108,6 @@ pub fn init(cx: &mut AppContext) {
|
||||
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(cx);
|
||||
})
|
||||
.register_action(AssistantPanel::inline_assist)
|
||||
.register_action(ContextEditor::quote_selection)
|
||||
.register_action(ContextEditor::insert_selection)
|
||||
.register_action(ContextEditor::copy_code)
|
||||
@@ -123,7 +122,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(
|
||||
|terminal_panel: &mut TerminalPanel, cx: &mut ViewContext<TerminalPanel>| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
terminal_panel.asssistant_enabled(settings.enabled, cx);
|
||||
terminal_panel.set_assistant_enabled(settings.enabled, cx);
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
@@ -1459,6 +1458,10 @@ impl Panel for AssistantPanel {
|
||||
fn toggle_action(&self) -> Box<dyn Action> {
|
||||
Box::new(ToggleFocus)
|
||||
}
|
||||
|
||||
fn activation_priority(&self) -> u32 {
|
||||
4
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for AssistantPanel {}
|
||||
@@ -1557,6 +1560,7 @@ impl ContextEditor {
|
||||
let mut editor = Editor::for_buffer(context.read(cx).buffer().clone(), None, cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor.set_show_line_numbers(false, cx);
|
||||
editor.set_show_scrollbars(false, cx);
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.set_show_runnables(false, cx);
|
||||
@@ -4966,8 +4970,8 @@ fn fold_toggle(
|
||||
) -> impl Fn(
|
||||
MultiBufferRow,
|
||||
bool,
|
||||
Arc<dyn Fn(bool, &mut WindowContext<'_>) + Send + Sync>,
|
||||
&mut WindowContext<'_>,
|
||||
Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>,
|
||||
&mut WindowContext,
|
||||
) -> AnyElement {
|
||||
move |row, is_folded, fold, _cx| {
|
||||
Disclosure::new((name, row.0 as u64), !is_folded)
|
||||
|
||||
@@ -3,18 +3,12 @@ use std::sync::Arc;
|
||||
use ::open_ai::Model as OpenAiModel;
|
||||
use anthropic::Model as AnthropicModel;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, Pixels};
|
||||
use language_model::{CloudModel, LanguageModel};
|
||||
use language_models::{
|
||||
provider::open_ai, AllLanguageModelSettings, AnthropicSettingsContent,
|
||||
AnthropicSettingsContentV1, OllamaSettingsContent, OpenAiSettingsContent,
|
||||
OpenAiSettingsContentV1, VersionedAnthropicSettingsContent, VersionedOpenAiSettingsContent,
|
||||
};
|
||||
use ollama::Model as OllamaModel;
|
||||
use schemars::{schema::Schema, JsonSchema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{update_settings_file, Settings, SettingsSources};
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -106,96 +100,6 @@ impl AssistantSettingsContent {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_file(&mut self, fs: Arc<dyn Fs>, cx: &AppContext) {
|
||||
if let AssistantSettingsContent::Versioned(settings) = self {
|
||||
if let VersionedAssistantSettingsContent::V1(settings) = settings {
|
||||
if let Some(provider) = settings.provider.clone() {
|
||||
match provider {
|
||||
AssistantProviderContentV1::Anthropic { api_url, .. } => {
|
||||
update_settings_file::<AllLanguageModelSettings>(
|
||||
fs,
|
||||
cx,
|
||||
move |content, _| {
|
||||
if content.anthropic.is_none() {
|
||||
content.anthropic =
|
||||
Some(AnthropicSettingsContent::Versioned(
|
||||
VersionedAnthropicSettingsContent::V1(
|
||||
AnthropicSettingsContentV1 {
|
||||
api_url,
|
||||
available_models: None,
|
||||
},
|
||||
),
|
||||
));
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
AssistantProviderContentV1::Ollama { api_url, .. } => {
|
||||
update_settings_file::<AllLanguageModelSettings>(
|
||||
fs,
|
||||
cx,
|
||||
move |content, _| {
|
||||
if content.ollama.is_none() {
|
||||
content.ollama = Some(OllamaSettingsContent {
|
||||
api_url,
|
||||
available_models: None,
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
AssistantProviderContentV1::OpenAi {
|
||||
api_url,
|
||||
available_models,
|
||||
..
|
||||
} => update_settings_file::<AllLanguageModelSettings>(
|
||||
fs,
|
||||
cx,
|
||||
move |content, _| {
|
||||
if content.openai.is_none() {
|
||||
let available_models = available_models.map(|models| {
|
||||
models
|
||||
.into_iter()
|
||||
.filter_map(|model| match model {
|
||||
OpenAiModel::Custom {
|
||||
name,
|
||||
display_name,
|
||||
max_tokens,
|
||||
max_output_tokens,
|
||||
max_completion_tokens: None,
|
||||
} => Some(open_ai::AvailableModel {
|
||||
name,
|
||||
display_name,
|
||||
max_tokens,
|
||||
max_output_tokens,
|
||||
max_completion_tokens: None,
|
||||
}),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
content.openai = Some(OpenAiSettingsContent::Versioned(
|
||||
VersionedOpenAiSettingsContent::V1(
|
||||
OpenAiSettingsContentV1 {
|
||||
api_url,
|
||||
available_models,
|
||||
},
|
||||
),
|
||||
));
|
||||
}
|
||||
},
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*self = AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(
|
||||
self.upgrade(),
|
||||
));
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> AssistantSettingsContentV2 {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
@@ -534,6 +438,7 @@ fn merge<T>(target: &mut T, value: Option<T>) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fs::Fs;
|
||||
use gpui::{ReadGlobal, TestAppContext};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -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,
|
||||
@@ -716,7 +717,7 @@ impl ContextStore {
|
||||
let candidates = metadata
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, metadata)| StringMatchCandidate::new(id, metadata.title.clone()))
|
||||
.map(|(id, metadata)| StringMatchCandidate::new(id, &metadata.title))
|
||||
.collect::<Vec<_>>();
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
@@ -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,
|
||||
|
||||
@@ -47,6 +47,7 @@ use std::{
|
||||
iter, mem,
|
||||
ops::{Range, RangeInclusive},
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
task::{self, Poll},
|
||||
time::{Duration, Instant},
|
||||
@@ -132,7 +133,7 @@ impl InlineAssistant {
|
||||
};
|
||||
let enabled = AssistantSettings::get_global(cx).enabled;
|
||||
terminal_panel.update(cx, |terminal_panel, cx| {
|
||||
terminal_panel.asssistant_enabled(enabled, cx)
|
||||
terminal_panel.set_assistant_enabled(enabled, cx)
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
@@ -174,7 +175,7 @@ impl InlineAssistant {
|
||||
if let Some(editor) = item.act_as::<Editor>(cx) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.push_code_action_provider(
|
||||
Arc::new(AssistantCodeActionProvider {
|
||||
Rc::new(AssistantCodeActionProvider {
|
||||
editor: cx.view().downgrade(),
|
||||
workspace: workspace.downgrade(),
|
||||
}),
|
||||
@@ -796,10 +797,11 @@ impl InlineAssistant {
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||
let language_name = assist.editor.upgrade().and_then(|editor| {
|
||||
let multibuffer = editor.read(cx).buffer().read(cx);
|
||||
let ranges = multibuffer.range_to_buffer_ranges(assist.range.clone(), cx);
|
||||
let multibuffer_snapshot = multibuffer.snapshot(cx);
|
||||
let ranges = multibuffer_snapshot.range_to_buffer_ranges(assist.range.clone());
|
||||
ranges
|
||||
.first()
|
||||
.and_then(|(buffer, _, _)| buffer.read(cx).language())
|
||||
.and_then(|(excerpt, _)| excerpt.buffer().language())
|
||||
.map(|language| language.name())
|
||||
});
|
||||
report_assistant_event(
|
||||
@@ -1441,6 +1443,15 @@ impl Render for PromptEditor {
|
||||
]
|
||||
}
|
||||
CodegenStatus::Error(_) | CodegenStatus::Done => {
|
||||
let must_rerun =
|
||||
self.edited_since_done || matches!(status, CodegenStatus::Error(_));
|
||||
// when accept button isn't visible, then restart maps to confirm
|
||||
// when accept button is visible, then restart must be mapped to an alternate keyboard shortcut
|
||||
let restart_key: &dyn gpui::Action = if must_rerun {
|
||||
&menu::Confirm
|
||||
} else {
|
||||
&menu::Restart
|
||||
};
|
||||
vec![
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
@@ -1450,23 +1461,22 @@ impl Render for PromptEditor {
|
||||
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
|
||||
)
|
||||
.into_any_element(),
|
||||
if self.edited_since_done || matches!(status, CodegenStatus::Error(_)) {
|
||||
IconButton::new("restart", IconName::RotateCw)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::with_meta(
|
||||
"Restart Transformation",
|
||||
Some(&menu::Confirm),
|
||||
"Changes will be discarded",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}))
|
||||
.into_any_element()
|
||||
} else {
|
||||
IconButton::new("restart", IconName::RotateCw)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::with_meta(
|
||||
"Regenerate Transformation",
|
||||
Some(restart_key),
|
||||
"Current change will be discarded",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}))
|
||||
.into_any_element(),
|
||||
if !must_rerun {
|
||||
IconButton::new("confirm", IconName::Check)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
@@ -1475,6 +1485,8 @@ impl Render for PromptEditor {
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested);
|
||||
}))
|
||||
.into_any_element()
|
||||
} else {
|
||||
div().into_any_element()
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -1491,6 +1503,7 @@ impl Render for PromptEditor {
|
||||
.py(cx.line_height() / 2.5)
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::restart))
|
||||
.on_action(cx.listener(Self::move_up))
|
||||
.on_action(cx.listener(Self::move_down))
|
||||
.capture_action(cx.listener(Self::cycle_prev))
|
||||
@@ -1544,7 +1557,7 @@ impl Render for PromptEditor {
|
||||
anchored()
|
||||
.position_mode(gpui::AnchoredPositionMode::Local)
|
||||
.position(point(px(0.), px(24.)))
|
||||
.anchor(gpui::AnchorCorner::TopLeft)
|
||||
.anchor(gpui::Corner::TopLeft)
|
||||
.child(self.render_rate_limit_notice(cx)),
|
||||
)
|
||||
})),
|
||||
@@ -1837,6 +1850,10 @@ impl PromptEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn restart(&mut self, _: &menu::Restart, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
|
||||
match self.codegen.read(cx).status(cx) {
|
||||
CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
|
||||
@@ -2599,26 +2616,29 @@ impl EventEmitter<CodegenEvent> for CodegenAlternative {}
|
||||
|
||||
impl CodegenAlternative {
|
||||
pub fn new(
|
||||
buffer: Model<MultiBuffer>,
|
||||
multi_buffer: Model<MultiBuffer>,
|
||||
range: Range<Anchor>,
|
||||
active: bool,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
builder: Arc<PromptBuilder>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
let snapshot = multi_buffer.read(cx).snapshot(cx);
|
||||
|
||||
let (old_buffer, _, _) = buffer
|
||||
.read(cx)
|
||||
.range_to_buffer_ranges(range.clone(), cx)
|
||||
let (old_excerpt, _) = snapshot
|
||||
.range_to_buffer_ranges(range.clone())
|
||||
.pop()
|
||||
.unwrap();
|
||||
let old_buffer = cx.new_model(|cx| {
|
||||
let old_buffer = old_buffer.read(cx);
|
||||
let text = old_buffer.as_rope().clone();
|
||||
let line_ending = old_buffer.line_ending();
|
||||
let language = old_buffer.language().cloned();
|
||||
let language_registry = old_buffer.language_registry();
|
||||
let text = old_excerpt.buffer().as_rope().clone();
|
||||
let line_ending = old_excerpt.buffer().line_ending();
|
||||
let language = old_excerpt.buffer().language().cloned();
|
||||
let language_registry = multi_buffer
|
||||
.read(cx)
|
||||
.buffer(old_excerpt.buffer_id())
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.language_registry();
|
||||
|
||||
let mut buffer = Buffer::local_normalized(text, line_ending, cx);
|
||||
buffer.set_language(language, cx);
|
||||
@@ -2629,7 +2649,7 @@ impl CodegenAlternative {
|
||||
});
|
||||
|
||||
Self {
|
||||
buffer: buffer.clone(),
|
||||
buffer: multi_buffer.clone(),
|
||||
old_buffer,
|
||||
edit_position: None,
|
||||
message_id: None,
|
||||
@@ -2640,7 +2660,7 @@ impl CodegenAlternative {
|
||||
generation: Task::ready(()),
|
||||
diff: Diff::default(),
|
||||
telemetry,
|
||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||
_subscription: cx.subscribe(&multi_buffer, Self::handle_buffer_event),
|
||||
builder,
|
||||
active,
|
||||
edits: Vec::new(),
|
||||
@@ -2851,10 +2871,11 @@ impl CodegenAlternative {
|
||||
let telemetry = self.telemetry.clone();
|
||||
let language_name = {
|
||||
let multibuffer = self.buffer.read(cx);
|
||||
let ranges = multibuffer.range_to_buffer_ranges(self.range.clone(), cx);
|
||||
let snapshot = multibuffer.snapshot(cx);
|
||||
let ranges = snapshot.range_to_buffer_ranges(self.range.clone());
|
||||
ranges
|
||||
.first()
|
||||
.and_then(|(buffer, _, _)| buffer.read(cx).language())
|
||||
.and_then(|(excerpt, _)| excerpt.buffer().language())
|
||||
.map(|language| language.name())
|
||||
};
|
||||
|
||||
|
||||
@@ -1439,10 +1439,7 @@ impl PromptStore {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(ix, metadata)| {
|
||||
Some(StringMatchCandidate::new(
|
||||
ix,
|
||||
metadata.title.as_ref()?.to_string(),
|
||||
))
|
||||
Some(StringMatchCandidate::new(ix, metadata.title.as_ref()?))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let matches = fuzzy::match_strings(
|
||||
|
||||
@@ -7,11 +7,13 @@ use editor::{CompletionProvider, Editor};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext};
|
||||
use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use parking_lot::Mutex;
|
||||
use project::CompletionIntent;
|
||||
use rope::Point;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
ops::Range,
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
Arc,
|
||||
@@ -78,11 +80,7 @@ impl SlashCommandCompletionProvider {
|
||||
.command_names(cx)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, def)| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: def.to_string(),
|
||||
char_bag: def.as_ref().into(),
|
||||
})
|
||||
.map(|(ix, def)| StringMatchCandidate::new(ix, &def))
|
||||
.collect::<Vec<_>>();
|
||||
let command_name = command_name.to_string();
|
||||
let editor = self.editor.clone();
|
||||
@@ -151,6 +149,7 @@ impl SlashCommandCompletionProvider {
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
confirm,
|
||||
resolved: true,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
@@ -244,6 +243,7 @@ impl SlashCommandCompletionProvider {
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
confirm,
|
||||
resolved: true,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
@@ -326,22 +326,12 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
&self,
|
||||
_: Model<Buffer>,
|
||||
_: Vec<usize>,
|
||||
_: Arc<RwLock<Box<[project::Completion]>>>,
|
||||
_: Rc<RefCell<Box<[project::Completion]>>>,
|
||||
_: &mut ViewContext<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>,
|
||||
|
||||
@@ -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, AsyncWindowContext, Task, WeakView, WindowContext};
|
||||
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;
|
||||
|
||||
@@ -115,7 +115,7 @@ impl SlashCommand for AutoCommand {
|
||||
return Task::ready(Err(anyhow!("no project indexer")));
|
||||
};
|
||||
|
||||
let task = cx.spawn(|cx: gpui::AsyncWindowContext| async move {
|
||||
let task = cx.spawn(|cx: AsyncWindowContext| async move {
|
||||
let summaries = project_index
|
||||
.read_with(&cx, |project_index, cx| project_index.all_summaries(cx))?
|
||||
.await?;
|
||||
|
||||
@@ -218,10 +218,7 @@ impl Options {
|
||||
}
|
||||
|
||||
fn match_candidates_for_args() -> [StringMatchCandidate; 1] {
|
||||
[StringMatchCandidate::new(
|
||||
0,
|
||||
INCLUDE_WARNINGS_ARGUMENT.to_string(),
|
||||
)]
|
||||
[StringMatchCandidate::new(0, INCLUDE_WARNINGS_ARGUMENT)]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -249,11 +249,7 @@ fn tab_items_for_queries(
|
||||
.enumerate()
|
||||
.filter_map(|(id, (full_path, ..))| {
|
||||
let path_string = full_path.as_deref()?.to_string_lossy().to_string();
|
||||
Some(fuzzy::StringMatchCandidate {
|
||||
id,
|
||||
char_bag: path_string.as_str().into(),
|
||||
string: path_string,
|
||||
})
|
||||
Some(fuzzy::StringMatchCandidate::new(id, &path_string))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut processed_matches = HashSet::default();
|
||||
@@ -285,7 +281,7 @@ fn tab_items_for_queries(
|
||||
|
||||
fn active_item_buffer(
|
||||
workspace: &mut Workspace,
|
||||
cx: &mut ui::ViewContext<Workspace>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> anyhow::Result<BufferSnapshot> {
|
||||
let active_editor = workspace
|
||||
.active_item(cx)
|
||||
|
||||
@@ -27,8 +27,8 @@ enum SlashCommandEntry {
|
||||
Info(SlashCommandInfo),
|
||||
Advert {
|
||||
name: SharedString,
|
||||
renderer: fn(&mut WindowContext<'_>) -> AnyElement,
|
||||
on_confirm: fn(&mut WindowContext<'_>),
|
||||
renderer: fn(&mut WindowContext) -> AnyElement,
|
||||
on_confirm: fn(&mut WindowContext),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -217,11 +217,10 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
div().overflow_hidden().text_ellipsis().child(
|
||||
Label::new(info.description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
Label::new(info.description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.text_ellipsis(),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -317,8 +316,8 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
|
||||
PopoverMenu::new("model-switcher")
|
||||
.menu(move |_cx| Some(picker_view.clone()))
|
||||
.trigger(self.trigger)
|
||||
.attach(gpui::AnchorCorner::TopLeft)
|
||||
.anchor(gpui::AnchorCorner::BottomLeft)
|
||||
.attach(gpui::Corner::TopLeft)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-16.0),
|
||||
|
||||
@@ -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, UnderlineStyle, View, WeakView,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::Role;
|
||||
@@ -21,7 +22,7 @@ pub struct ActiveThread {
|
||||
workspace: WeakView<Workspace>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
thread: Model<Thread>,
|
||||
pub(crate) thread: Model<Thread>,
|
||||
messages: Vec<MessageId>,
|
||||
list_state: ListState,
|
||||
rendered_messages_by_id: HashMap<MessageId, View<Markdown>>,
|
||||
@@ -89,10 +90,11 @@ impl ActiveThread {
|
||||
self.list_state.splice(old_len..old_len, 1);
|
||||
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
let colors = cx.theme().colors();
|
||||
let ui_font_size = TextSize::Default.rems(cx);
|
||||
let buffer_font_size = theme_settings.buffer_font_size;
|
||||
|
||||
let buffer_font_size = TextSize::Small.rems(cx);
|
||||
let mut text_style = cx.text_style();
|
||||
|
||||
text_style.refine(&TextStyleRefinement {
|
||||
font_family: Some(theme_settings.ui_font.family.clone()),
|
||||
font_size: Some(ui_font_size.into()),
|
||||
@@ -105,6 +107,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,8 +136,17 @@ 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.1)),
|
||||
..Default::default()
|
||||
},
|
||||
link: TextStyleRefinement {
|
||||
background_color: Some(colors.editor_foreground.opacity(0.025)),
|
||||
underline: Some(UnderlineStyle {
|
||||
color: Some(colors.text_accent.opacity(0.5)),
|
||||
thickness: px(1.),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
@@ -131,6 +162,10 @@ impl ActiveThread {
|
||||
)
|
||||
});
|
||||
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(
|
||||
@@ -204,43 +239,57 @@ 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()
|
||||
.id(("message-container", ix))
|
||||
.p_2()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.child(
|
||||
v_flex()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.border_color(colors.border_variant)
|
||||
.bg(colors.editor_background)
|
||||
.rounded_md()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.p_1p5()
|
||||
.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_2()
|
||||
.child(Icon::new(role_icon).size(IconSize::Small))
|
||||
.child(Label::new(role_name).size(LabelSize::Small)),
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(role_icon)
|
||||
.size(IconSize::XSmall)
|
||||
.color(role_color),
|
||||
)
|
||||
.child(
|
||||
Label::new(role_name)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(role_color),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(v_flex().p_1p5().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(|context| {
|
||||
ContextPill::new_added(context.clone(), false, None)
|
||||
}),
|
||||
))
|
||||
} else {
|
||||
parent
|
||||
}
|
||||
}),
|
||||
)
|
||||
.into_any()
|
||||
@@ -249,6 +298,6 @@ impl ActiveThread {
|
||||
|
||||
impl Render for ActiveThread {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
list(self.list_state.clone()).flex_1()
|
||||
list(self.list_state.clone()).flex_1().py_1()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
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;
|
||||
@@ -15,7 +21,6 @@ mod ui;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::AssistantSettings;
|
||||
use client::Client;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
|
||||
@@ -26,16 +31,19 @@ use settings::Settings as _;
|
||||
use util::ResultExt;
|
||||
|
||||
pub use crate::assistant_panel::AssistantPanel;
|
||||
use crate::assistant_settings::AssistantSettings;
|
||||
pub use crate::inline_assistant::InlineAssistant;
|
||||
|
||||
actions!(
|
||||
assistant2,
|
||||
[
|
||||
ToggleFocus,
|
||||
NewThread,
|
||||
ToggleContextPicker,
|
||||
ToggleModelSelector,
|
||||
RemoveAllContext,
|
||||
OpenHistory,
|
||||
Chat,
|
||||
ToggleInlineAssist,
|
||||
CycleNextInlineAssist,
|
||||
CyclePreviousInlineAssist
|
||||
]
|
||||
@@ -75,8 +83,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";
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(NAMESPACE);
|
||||
});
|
||||
@@ -85,12 +91,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);
|
||||
});
|
||||
} else {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(NAMESPACE);
|
||||
filter.show_namespace(ASSISTANT1_NAMESPACE);
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
85
crates/assistant2/src/assistant_model_selector.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use fs::Fs;
|
||||
use gpui::View;
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, ButtonLike, PopoverMenuHandle, Tooltip};
|
||||
|
||||
use crate::{assistant_settings::AssistantSettings, ToggleModelSelector};
|
||||
|
||||
pub struct AssistantModelSelector {
|
||||
selector: View<LanguageModelSelector>,
|
||||
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
}
|
||||
|
||||
impl AssistantModelSelector {
|
||||
pub(crate) fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self {
|
||||
Self {
|
||||
selector: cx.new_view(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _cx| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
menu_handle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AssistantModelSelector {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
let focus_handle = self.selector.focus_handle(cx).clone();
|
||||
|
||||
LanguageModelSelectorPopoverMenu::new(
|
||||
self.selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.overflow_x_hidden()
|
||||
.flex_grow()
|
||||
.whitespace_nowrap()
|
||||
.child(match active_model {
|
||||
Some(model) => h_flex()
|
||||
.child(
|
||||
Label::new(model.name().0)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element(),
|
||||
_ => Label::new("No model selected")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
|
||||
}),
|
||||
)
|
||||
.with_handle(self.menu_handle.clone())
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,23 @@ use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
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,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use settings::Settings;
|
||||
use time::UtcOffset;
|
||||
use ui::{prelude::*, Divider, IconButtonShape, KeyBinding, Tab, Tooltip};
|
||||
use ui::{prelude::*, KeyBinding, Tab, Tooltip};
|
||||
use workspace::dock::{DockPosition, Panel, PanelEvent};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::active_thread::ActiveThread;
|
||||
use crate::assistant_settings::{AssistantDockPosition, AssistantSettings};
|
||||
use crate::message_editor::MessageEditor;
|
||||
use crate::thread::{ThreadError, ThreadId};
|
||||
use crate::thread::{Thread, ThreadError, ThreadId};
|
||||
use crate::thread_history::{PastThread, ThreadHistory};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::{NewThread, OpenHistory, ToggleFocus};
|
||||
@@ -24,9 +27,22 @@ use crate::{NewThread, OpenHistory, ToggleFocus};
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(
|
||||
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(cx);
|
||||
});
|
||||
workspace
|
||||
.register_action(|workspace, _: &ToggleFocus, cx| {
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(cx);
|
||||
})
|
||||
.register_action(|workspace, _: &NewThread, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| panel.new_thread(cx));
|
||||
workspace.focus_panel::<AssistantPanel>(cx);
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &OpenHistory, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(cx);
|
||||
panel.update(cx, |panel, cx| panel.open_history(cx));
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
@@ -39,6 +55,7 @@ enum ActiveView {
|
||||
|
||||
pub struct AssistantPanel {
|
||||
workspace: WeakView<Workspace>,
|
||||
fs: Arc<dyn Fs>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
thread_store: Model<ThreadStore>,
|
||||
thread: View<ActiveThread>,
|
||||
@@ -47,6 +64,8 @@ pub struct AssistantPanel {
|
||||
local_timezone: UtcOffset,
|
||||
active_view: ActiveView,
|
||||
history: View<ThreadHistory>,
|
||||
width: Option<Pixels>,
|
||||
height: Option<Pixels>,
|
||||
}
|
||||
|
||||
impl AssistantPanel {
|
||||
@@ -76,6 +95,7 @@ impl AssistantPanel {
|
||||
cx: &mut ViewContext<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();
|
||||
@@ -83,6 +103,7 @@ impl AssistantPanel {
|
||||
Self {
|
||||
active_view: ActiveView::Thread,
|
||||
workspace: workspace.clone(),
|
||||
fs: fs.clone(),
|
||||
language_registry: language_registry.clone(),
|
||||
thread_store: thread_store.clone(),
|
||||
thread: cx.new_view(|cx| {
|
||||
@@ -94,13 +115,23 @@ impl AssistantPanel {
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
message_editor: cx.new_view(|cx| MessageEditor::new(workspace, thread.clone(), cx)),
|
||||
message_editor: cx.new_view(|cx| {
|
||||
MessageEditor::new(
|
||||
fs.clone(),
|
||||
workspace,
|
||||
thread_store.downgrade(),
|
||||
thread.clone(),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
tools,
|
||||
local_timezone: UtcOffset::from_whole_seconds(
|
||||
chrono::Local::now().offset().local_minus_utc(),
|
||||
)
|
||||
.unwrap(),
|
||||
history: cx.new_view(|cx| ThreadHistory::new(weak_self, thread_store, cx)),
|
||||
width: None,
|
||||
height: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +139,10 @@ impl AssistantPanel {
|
||||
self.local_timezone
|
||||
}
|
||||
|
||||
pub(crate) fn thread_store(&self) -> &Model<ThreadStore> {
|
||||
&self.thread_store
|
||||
}
|
||||
|
||||
fn new_thread(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let thread = self
|
||||
.thread_store
|
||||
@@ -123,11 +158,24 @@ impl AssistantPanel {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor =
|
||||
cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
|
||||
self.message_editor = cx.new_view(|cx| {
|
||||
MessageEditor::new(
|
||||
self.fs.clone(),
|
||||
self.workspace.clone(),
|
||||
self.thread_store.downgrade(),
|
||||
thread,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor.focus_handle(cx).focus(cx);
|
||||
}
|
||||
|
||||
fn open_history(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.active_view = ActiveView::History;
|
||||
self.history.focus_handle(cx).focus(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
|
||||
let Some(thread) = self
|
||||
.thread_store
|
||||
@@ -146,11 +194,22 @@ impl AssistantPanel {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor =
|
||||
cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
|
||||
self.message_editor = cx.new_view(|cx| {
|
||||
MessageEditor::new(
|
||||
self.fs.clone(),
|
||||
self.workspace.clone(),
|
||||
self.thread_store.downgrade(),
|
||||
thread,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor.focus_handle(cx).focus(cx);
|
||||
}
|
||||
|
||||
pub(crate) fn active_thread(&self, cx: &AppContext) -> Model<Thread> {
|
||||
self.thread.read(cx).thread.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
|
||||
self.thread_store
|
||||
.update(cx, |this, cx| this.delete_thread(thread_id, cx));
|
||||
@@ -181,13 +240,38 @@ impl Panel for AssistantPanel {
|
||||
true
|
||||
}
|
||||
|
||||
fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
|
||||
|
||||
fn size(&self, _cx: &WindowContext) -> Pixels {
|
||||
px(640.)
|
||||
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
|
||||
settings::update_settings_file::<AssistantSettings>(
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
move |settings, _| {
|
||||
let dock = match position {
|
||||
DockPosition::Left => AssistantDockPosition::Left,
|
||||
DockPosition::Bottom => AssistantDockPosition::Bottom,
|
||||
DockPosition::Right => AssistantDockPosition::Right,
|
||||
};
|
||||
settings.set_dock(dock);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn set_size(&mut self, _size: Option<Pixels>, _cx: &mut ViewContext<Self>) {}
|
||||
fn size(&self, cx: &WindowContext) -> Pixels {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
match self.position(cx) {
|
||||
DockPosition::Left | DockPosition::Right => {
|
||||
self.width.unwrap_or(settings.default_width)
|
||||
}
|
||||
DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
|
||||
match self.position(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>) {}
|
||||
|
||||
@@ -196,7 +280,7 @@ impl Panel for AssistantPanel {
|
||||
}
|
||||
|
||||
fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
|
||||
Some(IconName::ZedAssistant)
|
||||
Some(IconName::ZedAssistant2)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||
@@ -206,29 +290,45 @@ impl Panel for AssistantPanel {
|
||||
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 {
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
let title = if self.thread.read(cx).is_empty() {
|
||||
SharedString::from("New Thread")
|
||||
} else {
|
||||
self.thread
|
||||
.read(cx)
|
||||
.summary(cx)
|
||||
.unwrap_or_else(|| SharedString::from("Loading Summary…"))
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id("assistant-toolbar")
|
||||
.px(DynamicSpacing::Base08.rems(cx))
|
||||
.h(Tab::container_height(cx))
|
||||
.flex_none()
|
||||
.justify_between()
|
||||
.gap(DynamicSpacing::Base08.rems(cx))
|
||||
.h(Tab::container_height(cx))
|
||||
.px(DynamicSpacing::Base08.rems(cx))
|
||||
.bg(cx.theme().colors().tab_bar_background)
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(h_flex().children(self.thread.read(cx).summary(cx).map(Label::new)))
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(h_flex().child(Label::new(title)))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap(DynamicSpacing::Base08.rems(cx))
|
||||
.child(Divider::vertical())
|
||||
.h_full()
|
||||
.pl_1p5()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.gap(DynamicSpacing::Base02.rems(cx))
|
||||
.child(
|
||||
IconButton::new("new-thread", IconName::Plus)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip({
|
||||
@@ -248,7 +348,6 @@ impl AssistantPanel {
|
||||
)
|
||||
.child(
|
||||
IconButton::new("open-history", IconName::HistoryRerun)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip({
|
||||
@@ -268,7 +367,6 @@ impl AssistantPanel {
|
||||
)
|
||||
.child(
|
||||
IconButton::new("configure-assistant", IconName::Settings)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
|
||||
@@ -294,7 +392,6 @@ impl AssistantPanel {
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.mx_auto()
|
||||
.child(
|
||||
v_flex().w_full().child(
|
||||
svg()
|
||||
@@ -309,13 +406,14 @@ impl AssistantPanel {
|
||||
.when(!recent_threads.is_empty(), |parent| {
|
||||
parent
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_center()
|
||||
.child(Label::new("Recent Threads:").size(LabelSize::Small)),
|
||||
h_flex().w_full().justify_center().child(
|
||||
Label::new("Recent Threads:")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex().gap_2().children(
|
||||
v_flex().mx_auto().w_4_5().gap_2().children(
|
||||
recent_threads
|
||||
.into_iter()
|
||||
.map(|thread| PastThread::new(thread, cx.view().downgrade())),
|
||||
@@ -511,9 +609,7 @@ impl Render for AssistantPanel {
|
||||
this.new_thread(cx);
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &OpenHistory, cx| {
|
||||
this.active_view = ActiveView::History;
|
||||
this.history.focus_handle(cx).focus(cx);
|
||||
cx.notify();
|
||||
this.open_history(cx);
|
||||
}))
|
||||
.child(self.render_toolbar(cx))
|
||||
.map(|parent| match self.active_view {
|
||||
@@ -522,7 +618,7 @@ impl Render for AssistantPanel {
|
||||
.child(
|
||||
h_flex()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(self.message_editor.clone()),
|
||||
)
|
||||
.children(self.render_last_error(cx)),
|
||||
|
||||
@@ -157,6 +157,22 @@ impl AssistantSettingsContent {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_dock(&mut self, dock: AssistantDockPosition) {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
VersionedAssistantSettingsContent::V2(settings) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
|
||||
let model = language_model.id().0.to_string();
|
||||
let provider = language_model.provider_id().0.to_string();
|
||||
|
||||
1479
crates/assistant2/src/buffer_codegen.rs
Normal file
@@ -1,4 +1,5 @@
|
||||
use gpui::SharedString;
|
||||
use language_model::{LanguageModelRequestMessage, MessageContent};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::post_inc;
|
||||
|
||||
@@ -16,6 +17,8 @@ impl ContextId {
|
||||
pub struct Context {
|
||||
pub id: ContextId,
|
||||
pub name: SharedString,
|
||||
pub parent: Option<SharedString>,
|
||||
pub tooltip: Option<SharedString>,
|
||||
pub kind: ContextKind,
|
||||
pub text: SharedString,
|
||||
}
|
||||
@@ -23,5 +26,67 @@ pub struct Context {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum ContextKind {
|
||||
File,
|
||||
Directory,
|
||||
FetchedUrl,
|
||||
Thread,
|
||||
}
|
||||
|
||||
pub fn attach_context_to_message(
|
||||
message: &mut LanguageModelRequestMessage,
|
||||
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();
|
||||
|
||||
for context in context.into_iter() {
|
||||
match context.kind {
|
||||
ContextKind::File { .. } => {
|
||||
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');
|
||||
fetch_context.push_str(&context.text);
|
||||
fetch_context.push('\n');
|
||||
}
|
||||
ContextKind::Thread => {
|
||||
thread_context.push_str(&context.name);
|
||||
thread_context.push('\n');
|
||||
thread_context.push_str(&context.text);
|
||||
thread_context.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut context_text = String::new();
|
||||
if !file_context.is_empty() {
|
||||
context_text.push_str("The following files are available:\n");
|
||||
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);
|
||||
}
|
||||
|
||||
if !thread_context.is_empty() {
|
||||
context_text.push_str("The following previous conversation threads are available\n");
|
||||
context_text.push_str(&thread_context);
|
||||
}
|
||||
|
||||
if !context_text.is_empty() {
|
||||
message.content.push(MessageContent::Text(context_text));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,40 @@
|
||||
mod directory_context_picker;
|
||||
mod fetch_context_picker;
|
||||
mod file_context_picker;
|
||||
mod thread_context_picker;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{
|
||||
AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, SharedString, Task, View,
|
||||
WeakView,
|
||||
WeakModel, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing, Tooltip};
|
||||
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::message_editor::MessageEditor;
|
||||
use crate::context_picker::thread_context_picker::ThreadContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::thread_store::ThreadStore;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ConfirmBehavior {
|
||||
KeepOpen,
|
||||
Close,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum ContextPickerMode {
|
||||
Default,
|
||||
File(View<FileContextPicker>),
|
||||
Directory(View<DirectoryContextPicker>),
|
||||
Fetch(View<FetchContextPicker>),
|
||||
Thread(View<ThreadContextPicker>),
|
||||
}
|
||||
|
||||
pub(super) struct ContextPicker {
|
||||
@@ -31,30 +45,43 @@ pub(super) struct ContextPicker {
|
||||
impl ContextPicker {
|
||||
pub fn new(
|
||||
workspace: WeakView<Workspace>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
thread_store: Option<WeakModel<ThreadStore>>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let mut entries = Vec::new();
|
||||
entries.push(ContextPickerEntry {
|
||||
name: "File".into(),
|
||||
kind: ContextKind::File,
|
||||
icon: IconName::File,
|
||||
});
|
||||
entries.push(ContextPickerEntry {
|
||||
name: "Folder".into(),
|
||||
kind: ContextKind::Directory,
|
||||
icon: IconName::Folder,
|
||||
});
|
||||
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(),
|
||||
workspace: workspace.clone(),
|
||||
message_editor: message_editor.clone(),
|
||||
entries: vec![
|
||||
ContextPickerEntry {
|
||||
name: "directory".into(),
|
||||
description: "Insert any directory".into(),
|
||||
icon: IconName::Folder,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "file".into(),
|
||||
description: "Insert any file".into(),
|
||||
icon: IconName::File,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "fetch".into(),
|
||||
description: "Fetch content from URL".into(),
|
||||
icon: IconName::Globe,
|
||||
},
|
||||
],
|
||||
workspace,
|
||||
thread_store,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
entries,
|
||||
selected_ix: 0,
|
||||
};
|
||||
|
||||
@@ -80,7 +107,9 @@ impl FocusableView for ContextPicker {
|
||||
match &self.mode {
|
||||
ContextPickerMode::Default => self.picker.focus_handle(cx),
|
||||
ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx),
|
||||
ContextPickerMode::Directory(directory_picker) => directory_picker.focus_handle(cx),
|
||||
ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
|
||||
ContextPickerMode::Thread(thread_picker) => thread_picker.focus_handle(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,7 +122,11 @@ impl Render for ContextPicker {
|
||||
.map(|parent| match &self.mode {
|
||||
ContextPickerMode::Default => parent.child(self.picker.clone()),
|
||||
ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
|
||||
ContextPickerMode::Directory(directory_picker) => {
|
||||
parent.child(directory_picker.clone())
|
||||
}
|
||||
ContextPickerMode::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
|
||||
ContextPickerMode::Thread(thread_picker) => parent.child(thread_picker.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -101,14 +134,16 @@ impl Render for ContextPicker {
|
||||
#[derive(Clone)]
|
||||
struct ContextPickerEntry {
|
||||
name: SharedString,
|
||||
description: SharedString,
|
||||
kind: ContextKind,
|
||||
icon: IconName,
|
||||
}
|
||||
|
||||
pub(crate) struct ContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
thread_store: Option<WeakModel<ThreadStore>>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
entries: Vec<ContextPickerEntry>,
|
||||
selected_ix: usize,
|
||||
}
|
||||
@@ -141,28 +176,53 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||
if let Some(entry) = self.entries.get(self.selected_ix) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| {
|
||||
match entry.name.to_string().as_str() {
|
||||
"file" => {
|
||||
match entry.kind {
|
||||
ContextKind::File => {
|
||||
this.mode = ContextPickerMode::File(cx.new_view(|cx| {
|
||||
FileContextPicker::new(
|
||||
self.context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.message_editor.clone(),
|
||||
self.context_store.clone(),
|
||||
self.confirm_behavior,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
"fetch" => {
|
||||
ContextKind::Directory => {
|
||||
this.mode = ContextPickerMode::Directory(cx.new_view(|cx| {
|
||||
DirectoryContextPicker::new(
|
||||
self.context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
self.confirm_behavior,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
ContextKind::FetchedUrl => {
|
||||
this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
|
||||
FetchContextPicker::new(
|
||||
self.context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.message_editor.clone(),
|
||||
self.context_store.clone(),
|
||||
self.confirm_behavior,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
_ => {}
|
||||
ContextKind::Thread => {
|
||||
if let Some(thread_store) = self.thread_store.as_ref() {
|
||||
this.mode = ContextPickerMode::Thread(cx.new_view(|cx| {
|
||||
ThreadContextPicker::new(
|
||||
thread_store.clone(),
|
||||
self.context_picker.clone(),
|
||||
self.context_store.clone(),
|
||||
self.confirm_behavior,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cx.focus_self();
|
||||
@@ -175,7 +235,10 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| match this.mode {
|
||||
ContextPickerMode::Default => cx.emit(DismissEvent),
|
||||
ContextPickerMode::File(_) | ContextPickerMode::Fetch(_) => {}
|
||||
ContextPickerMode::File(_)
|
||||
| ContextPickerMode::Directory(_)
|
||||
| ContextPickerMode::Fetch(_)
|
||||
| ContextPickerMode::Thread(_) => {}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
@@ -193,34 +256,13 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Dense)
|
||||
.toggle_state(selected)
|
||||
.tooltip({
|
||||
let description = entry.description.clone();
|
||||
move |cx| cx.new_view(|_cx| Tooltip::new(description.clone())).into()
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.group(format!("context-entry-label-{ix}"))
|
||||
.w_full()
|
||||
.py_0p5()
|
||||
h_flex()
|
||||
.min_w(px(250.))
|
||||
.max_w(px(400.))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(entry.icon).size(IconSize::XSmall))
|
||||
.child(
|
||||
Label::new(entry.name.clone())
|
||||
.single_line()
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().overflow_hidden().text_ellipsis().child(
|
||||
Label::new(entry.description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
.gap_2()
|
||||
.child(Icon::new(entry.icon).size(IconSize::Small))
|
||||
.child(Label::new(entry.name.clone()).single_line()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
334
crates/assistant2/src/context_picker/directory_context_picker.rs
Normal file
@@ -0,0 +1,334 @@
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, ProjectPath, Worktree, WorktreeId};
|
||||
use ui::{prelude::*, ListItem};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_store::{push_fenced_codeblock, ContextStore};
|
||||
|
||||
pub struct DirectoryContextPicker {
|
||||
picker: View<Picker<DirectoryContextPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl DirectoryContextPicker {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let delegate = DirectoryContextPickerDelegate::new(
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
);
|
||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
||||
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for DirectoryContextPicker {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DirectoryContextPicker {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
self.picker.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DirectoryContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
matches: Vec<PathMatch>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl DirectoryContextPickerDelegate {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
) -> Self {
|
||||
Self {
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn search(
|
||||
&mut self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &View<Workspace>,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
if query.is_empty() {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().read(cx);
|
||||
let directory_matches = project.worktrees(cx).flat_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let path_prefix: Arc<str> = worktree.root_name().into();
|
||||
worktree.directories(false, 0).map(move |entry| PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: worktree.id().to_usize(),
|
||||
path: entry.path.clone(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: true,
|
||||
})
|
||||
});
|
||||
|
||||
Task::ready(directory_matches.collect())
|
||||
} else {
|
||||
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
|
||||
let candidate_sets = worktrees
|
||||
.into_iter()
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
|
||||
PathMatchCandidateSet {
|
||||
snapshot: worktree.snapshot(),
|
||||
include_ignored: worktree
|
||||
.root_entry()
|
||||
.map_or(false, |entry| entry.is_ignored),
|
||||
include_root_name: true,
|
||||
candidates: project::Candidates::Directories,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
fuzzy::match_path_sets(
|
||||
candidate_sets.as_slice(),
|
||||
query.as_str(),
|
||||
None,
|
||||
false,
|
||||
100,
|
||||
&cancellation_flag,
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for DirectoryContextPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
"Search folders…".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
let search_task = self.search(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let mut paths = search_task.await;
|
||||
let empty_path = Path::new("");
|
||||
paths.retain(|path_match| path_match.path.as_ref() != empty_path);
|
||||
|
||||
this.update(&mut cx, |this, _cx| {
|
||||
this.delegate.matches = paths;
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let Some(mat) = self.matches.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(project) = workspace
|
||||
.upgrade()
|
||||
.map(|workspace| workspace.read(cx).project().clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let path = mat.path.clone();
|
||||
|
||||
let already_included = self
|
||||
.context_store
|
||||
.update(cx, |context_store, _cx| {
|
||||
if let Some(context_id) = context_store.included_directory(&path) {
|
||||
context_store.remove_context(&context_id);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.unwrap_or(true);
|
||||
if already_included {
|
||||
return;
|
||||
}
|
||||
|
||||
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
|
||||
let confirm_behavior = self.confirm_behavior;
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let worktree = project.update(&mut cx, |project, cx| {
|
||||
project
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
.ok_or_else(|| anyhow!("no worktree found for {worktree_id:?}"))
|
||||
})??;
|
||||
|
||||
let files = worktree.update(&mut cx, |worktree, _cx| {
|
||||
collect_files_in_path(worktree, &path)
|
||||
})?;
|
||||
|
||||
let open_buffer_tasks = project.update(&mut cx, |project, cx| {
|
||||
files
|
||||
.into_iter()
|
||||
.map(|file_path| {
|
||||
project.open_buffer(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: file_path.clone(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})?;
|
||||
|
||||
let open_all_buffers_tasks = cx.background_executor().spawn(async move {
|
||||
let mut buffers = Vec::with_capacity(open_buffer_tasks.len());
|
||||
|
||||
for open_buffer_task in open_buffer_tasks {
|
||||
let buffer = open_buffer_task.await?;
|
||||
|
||||
buffers.push(buffer);
|
||||
}
|
||||
|
||||
anyhow::Ok(buffers)
|
||||
});
|
||||
|
||||
let buffers = open_all_buffers_tasks.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let mut text = String::new();
|
||||
|
||||
for buffer in buffers {
|
||||
let buffer = buffer.read(cx);
|
||||
let path = buffer.file().map_or(&path, |file| file.path());
|
||||
push_fenced_codeblock(&path, buffer.text(), &mut text);
|
||||
}
|
||||
|
||||
this.delegate
|
||||
.context_store
|
||||
.update(cx, |context_store, _cx| {
|
||||
context_store.insert_directory(&path, text);
|
||||
})?;
|
||||
|
||||
match confirm_behavior {
|
||||
ConfirmBehavior::KeepOpen => {}
|
||||
ConfirmBehavior::Close => this.delegate.dismissed(cx),
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| {
|
||||
this.reset_mode();
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let path_match = &self.matches[ix];
|
||||
let directory_name = path_match.path.to_string_lossy().to_string();
|
||||
|
||||
let added = self.context_store.upgrade().map_or(false, |context_store| {
|
||||
context_store
|
||||
.read(cx)
|
||||
.included_directory(&path_match.path)
|
||||
.is_some()
|
||||
});
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.child(h_flex().gap_2().child(Label::new(directory_name)))
|
||||
.when(added, |el| {
|
||||
el.end_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Success),
|
||||
)
|
||||
.child(Label::new("Added").size(LabelSize::Small)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<Arc<Path>> {
|
||||
let mut files = Vec::new();
|
||||
|
||||
for entry in worktree.child_entries(path) {
|
||||
if entry.is_dir() {
|
||||
files.extend(collect_files_in_path(worktree, &entry.path));
|
||||
} else if entry.is_file() {
|
||||
files.push(entry.path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
files
|
||||
}
|
||||
@@ -4,16 +4,15 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakView};
|
||||
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
|
||||
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
|
||||
use http_client::{AsyncBody, HttpClientWithUrl};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing, ViewContext};
|
||||
use ui::{prelude::*, ListItem, ViewContext};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::ContextKind;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::message_editor::MessageEditor;
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_store::ContextStore;
|
||||
|
||||
pub struct FetchContextPicker {
|
||||
picker: View<Picker<FetchContextPickerDelegate>>,
|
||||
@@ -23,10 +22,16 @@ impl FetchContextPicker {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let delegate = FetchContextPickerDelegate::new(context_picker, workspace, message_editor);
|
||||
let delegate = FetchContextPickerDelegate::new(
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
);
|
||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
||||
|
||||
Self { picker }
|
||||
@@ -55,7 +60,8 @@ enum ContentType {
|
||||
pub struct FetchContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
url: String,
|
||||
}
|
||||
|
||||
@@ -63,12 +69,14 @@ impl FetchContextPickerDelegate {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
) -> Self {
|
||||
FetchContextPickerDelegate {
|
||||
context_picker,
|
||||
workspace,
|
||||
message_editor,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
url: String::new(),
|
||||
}
|
||||
}
|
||||
@@ -150,7 +158,15 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
1
|
||||
if self.url.is_empty() {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
|
||||
"Enter the URL that you would like to fetch".into()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
@@ -159,7 +175,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
|
||||
fn set_selected_index(&mut self, _ix: usize, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut ui::WindowContext) -> Arc<str> {
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
"Enter a URL…".into()
|
||||
}
|
||||
|
||||
@@ -176,15 +192,25 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
|
||||
let http_client = workspace.read(cx).client().http_client().clone();
|
||||
let url = self.url.clone();
|
||||
let confirm_behavior = self.confirm_behavior;
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let text = Self::build_message(http_client, &url).await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate
|
||||
.message_editor
|
||||
.update(cx, |message_editor, _cx| {
|
||||
message_editor.insert_context(ContextKind::FetchedUrl, url, text);
|
||||
})
|
||||
.context_store
|
||||
.update(cx, |context_store, _cx| {
|
||||
if context_store.included_url(&url).is_none() {
|
||||
context_store.insert_fetched_url(url, text);
|
||||
}
|
||||
})?;
|
||||
|
||||
match confirm_behavior {
|
||||
ConfirmBehavior::KeepOpen => {}
|
||||
ConfirmBehavior::Close => this.delegate.dismissed(cx),
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
|
||||
anyhow::Ok(())
|
||||
@@ -205,14 +231,29 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let added = self.context_store.upgrade().map_or(false, |context_store| {
|
||||
context_store.read(cx).included_url(&self.url).is_some()
|
||||
});
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(self.url.clone()),
|
||||
.child(Label::new(self.url.clone()))
|
||||
.when(added, |child| {
|
||||
child.disabled(true).end_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Success),
|
||||
)
|
||||
.child(Label::new("Added").size(LabelSize::Small)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
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, WeakView};
|
||||
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, WorktreeId};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing};
|
||||
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
|
||||
use ui::{prelude::*, ListItem, Tooltip};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::ContextKind;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::message_editor::MessageEditor;
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_store::{ContextStore, IncludedFile};
|
||||
|
||||
pub struct FileContextPicker {
|
||||
picker: View<Picker<FileContextPickerDelegate>>,
|
||||
@@ -24,10 +21,16 @@ impl FileContextPicker {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let delegate = FileContextPickerDelegate::new(context_picker, workspace, message_editor);
|
||||
let delegate = FileContextPickerDelegate::new(
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
);
|
||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
||||
|
||||
Self { picker }
|
||||
@@ -49,7 +52,8 @@ impl Render for FileContextPicker {
|
||||
pub struct FileContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
matches: Vec<PathMatch>,
|
||||
selected_index: usize,
|
||||
}
|
||||
@@ -58,12 +62,14 @@ impl FileContextPickerDelegate {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
) -> Self {
|
||||
Self {
|
||||
context_picker,
|
||||
workspace,
|
||||
message_editor,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
@@ -79,44 +85,37 @@ impl FileContextPickerDelegate {
|
||||
if query.is_empty() {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().read(cx);
|
||||
let entries = workspace.recent_navigation_history(Some(10), cx);
|
||||
|
||||
let entries = entries
|
||||
let recent_matches = workspace
|
||||
.recent_navigation_history(Some(10), cx)
|
||||
.into_iter()
|
||||
.map(|entries| entries.0)
|
||||
.chain(project.worktrees(cx).flat_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let id = worktree.id();
|
||||
worktree
|
||||
.child_entries(Path::new(""))
|
||||
.filter(|entry| entry.kind.is_file())
|
||||
.map(move |entry| project::ProjectPath {
|
||||
worktree_id: id,
|
||||
path: entry.path.clone(),
|
||||
})
|
||||
}))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let path_prefix: Arc<str> = Arc::default();
|
||||
Task::ready(
|
||||
entries
|
||||
.into_iter()
|
||||
.filter_map(|entry| {
|
||||
let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
|
||||
let mut full_path = PathBuf::from(worktree.read(cx).root_name());
|
||||
full_path.push(&entry.path);
|
||||
Some(PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: entry.worktree_id.to_usize(),
|
||||
path: full_path.into(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
})
|
||||
.filter_map(|(project_path, _)| {
|
||||
let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
|
||||
Some(PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: project_path.worktree_id.to_usize(),
|
||||
path: project_path.path,
|
||||
path_prefix: worktree.read(cx).root_name().into(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
});
|
||||
|
||||
let file_matches = project.worktrees(cx).flat_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let path_prefix: Arc<str> = worktree.root_name().into();
|
||||
worktree.files(true, 0).map(move |entry| PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: worktree.id().to_usize(),
|
||||
path: entry.path.clone(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
})
|
||||
});
|
||||
|
||||
Task::ready(recent_matches.chain(file_matches).collect())
|
||||
} else {
|
||||
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
|
||||
let candidate_sets = worktrees
|
||||
@@ -190,7 +189,9 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let mat = &self.matches[self.selected_index];
|
||||
let Some(mat) = self.matches.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(project) = workspace
|
||||
@@ -200,13 +201,40 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
return;
|
||||
};
|
||||
let path = mat.path.clone();
|
||||
|
||||
let already_included = self
|
||||
.context_store
|
||||
.update(cx, |context_store, _cx| {
|
||||
match context_store.included_file(&path) {
|
||||
Some(IncludedFile::Direct(context_id)) => {
|
||||
context_store.remove_context(&context_id);
|
||||
true
|
||||
}
|
||||
Some(IncludedFile::InDirectory(_)) => true,
|
||||
None => false,
|
||||
}
|
||||
})
|
||||
.unwrap_or(true);
|
||||
if already_included {
|
||||
return;
|
||||
}
|
||||
|
||||
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
|
||||
let confirm_behavior = self.confirm_behavior;
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let Some(open_buffer_task) = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.open_buffer((worktree_id, path.clone()), cx)
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: path.clone(),
|
||||
};
|
||||
|
||||
let task = project.open_buffer(project_path, cx);
|
||||
|
||||
Some(task)
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
else {
|
||||
return anyhow::Ok(());
|
||||
};
|
||||
@@ -215,23 +243,17 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate
|
||||
.message_editor
|
||||
.update(cx, |message_editor, 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');
|
||||
}
|
||||
.context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
context_store.insert_file(buffer.read(cx));
|
||||
})?;
|
||||
|
||||
text.push_str("```\n");
|
||||
match confirm_behavior {
|
||||
ConfirmBehavior::KeepOpen => {}
|
||||
ConfirmBehavior::Close => this.delegate.dismissed(cx),
|
||||
}
|
||||
|
||||
message_editor.insert_context(
|
||||
ContextKind::File,
|
||||
path.to_string_lossy().to_string(),
|
||||
text,
|
||||
);
|
||||
})
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
|
||||
anyhow::Ok(())
|
||||
@@ -252,38 +274,80 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let mat = &self.matches[ix];
|
||||
let path_match = &self.matches[ix];
|
||||
|
||||
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))
|
||||
};
|
||||
|
||||
let added = self
|
||||
.context_store
|
||||
.upgrade()
|
||||
.and_then(|context_store| context_store.read(cx).included_file(&path_match.path));
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(mat.path.to_string_lossy().to_string()),
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Label::new(file_name))
|
||||
.children(directory.map(|directory| {
|
||||
Label::new(directory)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
})),
|
||||
)
|
||||
.when_some(added, |el, added| match added {
|
||||
IncludedFile::Direct(_) => el.end_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Success),
|
||||
)
|
||||
.child(Label::new("Added").size(LabelSize::Small)),
|
||||
),
|
||||
IncludedFile::InDirectory(dir_name) => {
|
||||
let dir_name = dir_name.to_string_lossy().into_owned();
|
||||
|
||||
el.end_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Success),
|
||||
)
|
||||
.child(Label::new("Included").size(LabelSize::Small)),
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::text(format!("in {dir_name}"), cx))
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn codeblock_fence_for_path(path: Option<&Path>, row_range: Option<RangeInclusive<u32>>) -> String {
|
||||
let mut text = String::new();
|
||||
write!(text, "```").unwrap();
|
||||
|
||||
if let Some(path) = path {
|
||||
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
|
||||
write!(text, "{} ", extension).unwrap();
|
||||
}
|
||||
|
||||
write!(text, "{}", path.display()).unwrap();
|
||||
} else {
|
||||
write!(text, "untitled").unwrap();
|
||||
}
|
||||
|
||||
if let Some(row_range) = row_range {
|
||||
write!(text, ":{}-{}", row_range.start() + 1, row_range.end() + 1).unwrap();
|
||||
}
|
||||
|
||||
text.push('\n');
|
||||
text
|
||||
}
|
||||
|
||||
225
crates/assistant2/src/context_picker/thread_context_picker.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{prelude::*, ListItem};
|
||||
|
||||
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>>,
|
||||
}
|
||||
|
||||
impl ThreadContextPicker {
|
||||
pub fn new(
|
||||
thread_store: WeakModel<ThreadStore>,
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
context_store: WeakModel<context_store::ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let delegate = ThreadContextPickerDelegate::new(
|
||||
thread_store,
|
||||
context_picker,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
);
|
||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
||||
|
||||
ThreadContextPicker { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView 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 {
|
||||
self.picker.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ThreadContextEntry {
|
||||
id: ThreadId,
|
||||
summary: SharedString,
|
||||
}
|
||||
|
||||
pub struct ThreadContextPickerDelegate {
|
||||
thread_store: WeakModel<ThreadStore>,
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
context_store: WeakModel<context_store::ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
matches: Vec<ThreadContextEntry>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl ThreadContextPickerDelegate {
|
||||
pub fn new(
|
||||
thread_store: WeakModel<ThreadStore>,
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
context_store: WeakModel<context_store::ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
) -> Self {
|
||||
ThreadContextPickerDelegate {
|
||||
thread_store,
|
||||
context_picker,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
"Search threads…".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
let Ok(threads) = self.thread_store.update(cx, |this, cx| {
|
||||
this.threads(cx)
|
||||
.into_iter()
|
||||
.map(|thread| {
|
||||
const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
|
||||
|
||||
let id = thread.read(cx).id().clone();
|
||||
let summary = thread.read(cx).summary().unwrap_or(DEFAULT_SUMMARY);
|
||||
ThreadContextEntry { id, summary }
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}) else {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
let executor = cx.background_executor().clone();
|
||||
let search_task = cx.background_executor().spawn(async move {
|
||||
if query.is_empty() {
|
||||
threads
|
||||
} else {
|
||||
let candidates = threads
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, thread)| StringMatchCandidate::new(id, &thread.summary))
|
||||
.collect::<Vec<_>>();
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| threads[mat.candidate_id].clone())
|
||||
.collect()
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let matches = search_task.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate.matches = matches;
|
||||
this.delegate.selected_index = 0;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let Some(entry) = self.matches.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(thread_store) = self.thread_store.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(thread) = thread_store.update(cx, |this, cx| this.open_thread(&entry.id, cx))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
if let Some(context_id) = context_store.included_thread(&entry.id) {
|
||||
context_store.remove_context(&context_id);
|
||||
} else {
|
||||
context_store.insert_thread(thread.read(cx));
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
match self.confirm_behavior {
|
||||
ConfirmBehavior::KeepOpen => {}
|
||||
ConfirmBehavior::Close => self.dismissed(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| {
|
||||
this.reset_mode();
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let thread = &self.matches[ix];
|
||||
|
||||
let added = self.context_store.upgrade().map_or(false, |ctx_store| {
|
||||
ctx_store.read(cx).included_thread(&thread.id).is_some()
|
||||
});
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.child(Label::new(thread.summary.clone()))
|
||||
.when(added, |el| {
|
||||
el.end_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Success),
|
||||
)
|
||||
.child(Label::new("Added").size(LabelSize::Small)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
227
crates/assistant2/src/context_store.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
use std::fmt::Write as _;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use collections::{HashMap, HashSet};
|
||||
use gpui::SharedString;
|
||||
use language::Buffer;
|
||||
|
||||
use crate::thread::Thread;
|
||||
use crate::{
|
||||
context::{Context, ContextId, ContextKind},
|
||||
thread::ThreadId,
|
||||
};
|
||||
|
||||
pub struct ContextStore {
|
||||
context: Vec<Context>,
|
||||
next_context_id: ContextId,
|
||||
files: HashMap<PathBuf, ContextId>,
|
||||
directories: HashMap<PathBuf, ContextId>,
|
||||
threads: HashMap<ThreadId, ContextId>,
|
||||
fetched_urls: HashMap<String, ContextId>,
|
||||
}
|
||||
|
||||
impl ContextStore {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
context: Vec::new(),
|
||||
next_context_id: ContextId(0),
|
||||
files: HashMap::default(),
|
||||
directories: HashMap::default(),
|
||||
threads: HashMap::default(),
|
||||
fetched_urls: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn context(&self) -> &Vec<Context> {
|
||||
&self.context
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.context.clear();
|
||||
self.files.clear();
|
||||
self.directories.clear();
|
||||
self.threads.clear();
|
||||
self.fetched_urls.clear();
|
||||
}
|
||||
|
||||
pub fn insert_file(&mut self, buffer: &Buffer) {
|
||||
let Some(file) = buffer.file() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let path = file.path();
|
||||
|
||||
let id = self.next_context_id.post_inc();
|
||||
self.files.insert(path.to_path_buf(), id);
|
||||
|
||||
let full_path: SharedString = path.to_string_lossy().into_owned().into();
|
||||
|
||||
let name = match path.file_name() {
|
||||
Some(name) => name.to_string_lossy().into_owned().into(),
|
||||
None => full_path.clone(),
|
||||
};
|
||||
|
||||
let parent = path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|p| p.to_string_lossy().into_owned().into());
|
||||
|
||||
let mut text = String::new();
|
||||
push_fenced_codeblock(path, buffer.text(), &mut text);
|
||||
|
||||
self.context.push(Context {
|
||||
id,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path),
|
||||
kind: ContextKind::File,
|
||||
text: text.into(),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn insert_directory(&mut self, path: &Path, text: impl Into<SharedString>) {
|
||||
let id = self.next_context_id.post_inc();
|
||||
self.directories.insert(path.to_path_buf(), id);
|
||||
|
||||
let full_path: SharedString = path.to_string_lossy().into_owned().into();
|
||||
|
||||
let name = match path.file_name() {
|
||||
Some(name) => name.to_string_lossy().into_owned().into(),
|
||||
None => full_path.clone(),
|
||||
};
|
||||
|
||||
let parent = path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|p| p.to_string_lossy().into_owned().into());
|
||||
|
||||
self.context.push(Context {
|
||||
id,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path),
|
||||
kind: ContextKind::Directory,
|
||||
text: text.into(),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn insert_thread(&mut self, thread: &Thread) {
|
||||
let context_id = self.next_context_id.post_inc();
|
||||
self.threads.insert(thread.id().clone(), context_id);
|
||||
|
||||
self.context.push(Context {
|
||||
id: context_id,
|
||||
name: thread.summary().unwrap_or("New thread".into()),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
kind: ContextKind::Thread,
|
||||
text: thread.text().into(),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn insert_fetched_url(&mut self, url: String, text: impl Into<SharedString>) {
|
||||
let context_id = self.next_context_id.post_inc();
|
||||
self.fetched_urls.insert(url.clone(), context_id);
|
||||
|
||||
self.context.push(Context {
|
||||
id: context_id,
|
||||
name: url.into(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
kind: ContextKind::FetchedUrl,
|
||||
text: text.into(),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn remove_context(&mut self, id: &ContextId) {
|
||||
let Some(ix) = self.context.iter().position(|context| context.id == *id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match self.context.remove(ix).kind {
|
||||
ContextKind::File => {
|
||||
self.files.retain(|_, context_id| context_id != id);
|
||||
}
|
||||
ContextKind::Directory => {
|
||||
self.directories.retain(|_, context_id| context_id != id);
|
||||
}
|
||||
ContextKind::FetchedUrl => {
|
||||
self.fetched_urls.retain(|_, context_id| context_id != id);
|
||||
}
|
||||
ContextKind::Thread => {
|
||||
self.threads.retain(|_, context_id| context_id != id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn included_file(&self, path: &Path) -> Option<IncludedFile> {
|
||||
if let Some(id) = self.files.get(path) {
|
||||
return Some(IncludedFile::Direct(*id));
|
||||
}
|
||||
|
||||
if self.directories.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut buf = path.to_path_buf();
|
||||
|
||||
while buf.pop() {
|
||||
if let Some(_) = self.directories.get(&buf) {
|
||||
return Some(IncludedFile::InDirectory(buf));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn included_directory(&self, path: &Path) -> Option<ContextId> {
|
||||
self.directories.get(path).copied()
|
||||
}
|
||||
|
||||
pub fn included_thread(&self, thread_id: &ThreadId) -> Option<ContextId> {
|
||||
self.threads.get(thread_id).copied()
|
||||
}
|
||||
|
||||
pub fn included_url(&self, url: &str) -> Option<ContextId> {
|
||||
self.fetched_urls.get(url).copied()
|
||||
}
|
||||
|
||||
pub fn duplicated_names(&self) -> HashSet<SharedString> {
|
||||
let mut seen = HashSet::default();
|
||||
let mut dupes = HashSet::default();
|
||||
|
||||
for context in self.context().iter() {
|
||||
if !seen.insert(&context.name) {
|
||||
dupes.insert(context.name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
dupes
|
||||
}
|
||||
}
|
||||
|
||||
pub enum IncludedFile {
|
||||
Direct(ContextId),
|
||||
InDirectory(PathBuf),
|
||||
}
|
||||
|
||||
pub(crate) fn push_fenced_codeblock(path: &Path, content: String, buffer: &mut String) {
|
||||
buffer.reserve(content.len() + 64);
|
||||
|
||||
write!(buffer, "```").unwrap();
|
||||
|
||||
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
|
||||
write!(buffer, "{} ", extension).unwrap();
|
||||
}
|
||||
|
||||
write!(buffer, "{}", path.display()).unwrap();
|
||||
|
||||
buffer.push('\n');
|
||||
buffer.push_str(&content);
|
||||
|
||||
if !buffer.ends_with('\n') {
|
||||
buffer.push('\n');
|
||||
}
|
||||
|
||||
buffer.push_str("```\n");
|
||||
}
|
||||
304
crates/assistant2/src/context_strip.rs
Normal file
@@ -0,0 +1,304 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
AppContext, DismissEvent, EventEmitter, FocusHandle, Model, Subscription, View, WeakModel,
|
||||
WeakView,
|
||||
};
|
||||
use language::Buffer;
|
||||
use ui::{prelude::*, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::ContextKind;
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::thread::Thread;
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::ui::ContextPill;
|
||||
use crate::{AssistantPanel, RemoveAllContext, ToggleContextPicker};
|
||||
|
||||
pub struct ContextStrip {
|
||||
context_store: Model<ContextStore>,
|
||||
context_picker: View<ContextPicker>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
focus_handle: FocusHandle,
|
||||
suggest_context_kind: SuggestContextKind,
|
||||
workspace: WeakView<Workspace>,
|
||||
_context_picker_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl ContextStrip {
|
||||
pub fn new(
|
||||
context_store: Model<ContextStore>,
|
||||
workspace: WeakView<Workspace>,
|
||||
thread_store: Option<WeakModel<ThreadStore>>,
|
||||
focus_handle: FocusHandle,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
suggest_context_kind: SuggestContextKind,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let context_picker = cx.new_view(|cx| {
|
||||
ContextPicker::new(
|
||||
workspace.clone(),
|
||||
thread_store.clone(),
|
||||
context_store.downgrade(),
|
||||
ConfirmBehavior::KeepOpen,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let context_picker_subscription =
|
||||
cx.subscribe(&context_picker, Self::handle_context_picker_event);
|
||||
|
||||
Self {
|
||||
context_store: context_store.clone(),
|
||||
context_picker,
|
||||
context_picker_menu_handle,
|
||||
focus_handle,
|
||||
suggest_context_kind,
|
||||
workspace,
|
||||
_context_picker_subscription: context_picker_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
fn suggested_context(&self, cx: &ViewContext<Self>) -> Option<SuggestedContext> {
|
||||
match self.suggest_context_kind {
|
||||
SuggestContextKind::File => self.suggested_file(cx),
|
||||
SuggestContextKind::Thread => self.suggested_thread(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn suggested_file(&self, cx: &ViewContext<Self>) -> Option<SuggestedContext> {
|
||||
let workspace = self.workspace.upgrade()?;
|
||||
let active_item = workspace.read(cx).active_item(cx)?;
|
||||
|
||||
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
|
||||
let active_buffer = editor.buffer().read(cx).as_singleton()?;
|
||||
|
||||
let path = active_buffer.read(cx).file()?.path();
|
||||
|
||||
if self.context_store.read(cx).included_file(path).is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let name = match path.file_name() {
|
||||
Some(name) => name.to_string_lossy().into_owned().into(),
|
||||
None => path.to_string_lossy().into_owned().into(),
|
||||
};
|
||||
|
||||
Some(SuggestedContext::File {
|
||||
name,
|
||||
buffer: active_buffer.downgrade(),
|
||||
})
|
||||
}
|
||||
|
||||
fn suggested_thread(&self, cx: &ViewContext<Self>) -> Option<SuggestedContext> {
|
||||
let workspace = self.workspace.upgrade()?;
|
||||
let active_thread = workspace
|
||||
.read(cx)
|
||||
.panel::<AssistantPanel>(cx)?
|
||||
.read(cx)
|
||||
.active_thread(cx);
|
||||
let weak_active_thread = active_thread.downgrade();
|
||||
|
||||
let active_thread = active_thread.read(cx);
|
||||
|
||||
if self
|
||||
.context_store
|
||||
.read(cx)
|
||||
.included_thread(active_thread.id())
|
||||
.is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(SuggestedContext::Thread {
|
||||
name: active_thread.summary().unwrap_or("New Thread".into()),
|
||||
thread: weak_active_thread,
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_context_picker_event(
|
||||
&mut self,
|
||||
_picker: View<ContextPicker>,
|
||||
_event: &DismissEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
cx.emit(ContextStripEvent::PickerDismissed);
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ContextStrip {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let context_store = self.context_store.read(cx);
|
||||
let context = context_store.context().clone();
|
||||
let context_picker = self.context_picker.clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let suggested_context = self.suggested_context(cx);
|
||||
|
||||
let dupe_names = context_store.duplicated_names();
|
||||
|
||||
h_flex()
|
||||
.flex_wrap()
|
||||
.gap_1()
|
||||
.child(
|
||||
PopoverMenu::new("context-picker")
|
||||
.menu(move |_cx| Some(context_picker.clone()))
|
||||
.trigger(
|
||||
IconButton::new("add-context", IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ui::ButtonStyle::Filled)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
|
||||
move |cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Add Context",
|
||||
&ToggleContextPicker,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.attach(gpui::Corner::TopLeft)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-16.0),
|
||||
})
|
||||
.with_handle(self.context_picker_menu_handle.clone()),
|
||||
)
|
||||
.when(context.is_empty() && suggested_context.is_none(), {
|
||||
|parent| {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.ml_1p5()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new("Add Context")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.opacity(0.5)
|
||||
.children(
|
||||
KeyBinding::for_action_in(&ToggleContextPicker, &focus_handle, cx)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
.children(context.iter().map(|context| {
|
||||
ContextPill::new_added(
|
||||
context.clone(),
|
||||
dupe_names.contains(&context.name),
|
||||
Some({
|
||||
let context = context.clone();
|
||||
let context_store = self.context_store.clone();
|
||||
Rc::new(cx.listener(move |_this, _event, cx| {
|
||||
context_store.update(cx, |this, _cx| {
|
||||
this.remove_context(&context.id);
|
||||
});
|
||||
cx.notify();
|
||||
}))
|
||||
}),
|
||||
)
|
||||
}))
|
||||
.when_some(suggested_context, |el, suggested| {
|
||||
el.child(ContextPill::new_suggested(
|
||||
suggested.name().clone(),
|
||||
suggested.kind(),
|
||||
{
|
||||
let context_store = self.context_store.clone();
|
||||
Rc::new(cx.listener(move |_this, _event, cx| {
|
||||
context_store.update(cx, |context_store, cx| {
|
||||
suggested.accept(context_store, cx);
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}))
|
||||
},
|
||||
))
|
||||
})
|
||||
.when(!context.is_empty(), {
|
||||
move |parent| {
|
||||
parent.child(
|
||||
IconButton::new("remove-all-context", IconName::Eraser)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Remove All Context",
|
||||
&RemoveAllContext,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |_this, _event, cx| {
|
||||
focus_handle.dispatch_action(&RemoveAllContext, cx);
|
||||
}
|
||||
})),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ContextStripEvent {
|
||||
PickerDismissed,
|
||||
}
|
||||
|
||||
impl EventEmitter<ContextStripEvent> for ContextStrip {}
|
||||
|
||||
pub enum SuggestContextKind {
|
||||
File,
|
||||
Thread,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum SuggestedContext {
|
||||
File {
|
||||
name: SharedString,
|
||||
buffer: WeakModel<Buffer>,
|
||||
},
|
||||
Thread {
|
||||
name: SharedString,
|
||||
thread: WeakModel<Thread>,
|
||||
},
|
||||
}
|
||||
|
||||
impl SuggestedContext {
|
||||
pub fn name(&self) -> &SharedString {
|
||||
match self {
|
||||
Self::File { name, .. } => name,
|
||||
Self::Thread { name, .. } => name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn accept(&self, context_store: &mut ContextStore, cx: &mut AppContext) {
|
||||
match self {
|
||||
Self::File { buffer, name: _ } => {
|
||||
if let Some(buffer) = buffer.upgrade() {
|
||||
context_store.insert_file(buffer.read(cx));
|
||||
};
|
||||
}
|
||||
Self::Thread { thread, name: _ } => {
|
||||
if let Some(thread) = thread.upgrade() {
|
||||
context_store.insert_thread(thread.read(cx));
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> ContextKind {
|
||||
match self {
|
||||
Self::File { .. } => ContextKind::File,
|
||||
Self::Thread { .. } => ContextKind::Thread,
|
||||
}
|
||||
}
|
||||
}
|
||||
1151
crates/assistant2/src/inline_prompt_editor.rs
Normal file
@@ -1,77 +1,124 @@
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakView};
|
||||
use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
AppContext, DismissEvent, FocusableView, Model, Subscription, TextStyle, View, WeakModel,
|
||||
WeakView,
|
||||
};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use language_model_selector::LanguageModelSelector;
|
||||
use rope::Point;
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding,
|
||||
PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||
prelude::*, ButtonLike, ElevationIndex, KeyBinding, PopoverMenu, PopoverMenuHandle,
|
||||
SwitchWithLabel,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::{Context, ContextId, ContextKind};
|
||||
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, ContextStripEvent, SuggestContextKind};
|
||||
use crate::thread::{RequestKind, Thread};
|
||||
use crate::ui::ContextPill;
|
||||
use crate::{Chat, ToggleModelSelector};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::{Chat, RemoveAllContext, ToggleContextPicker, ToggleModelSelector};
|
||||
|
||||
pub struct MessageEditor {
|
||||
thread: Model<Thread>,
|
||||
editor: View<Editor>,
|
||||
context: Vec<Context>,
|
||||
next_context_id: ContextId,
|
||||
context_picker: View<ContextPicker>,
|
||||
pub(crate) context_picker_handle: PopoverMenuHandle<ContextPicker>,
|
||||
language_model_selector: View<LanguageModelSelector>,
|
||||
context_store: Model<ContextStore>,
|
||||
context_strip: View<ContextStrip>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
inline_context_picker: View<ContextPicker>,
|
||||
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
model_selector: View<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>,
|
||||
thread_store: WeakModel<ThreadStore>,
|
||||
thread: Model<Thread>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let weak_self = cx.view().downgrade();
|
||||
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(10, cx);
|
||||
editor.set_placeholder_text("Ask anything…", cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
|
||||
editor
|
||||
});
|
||||
|
||||
let inline_context_picker = cx.new_view(|cx| {
|
||||
ContextPicker::new(
|
||||
workspace.clone(),
|
||||
Some(thread_store.clone()),
|
||||
context_store.downgrade(),
|
||||
ConfirmBehavior::Close,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let context_strip = cx.new_view(|cx| {
|
||||
ContextStrip::new(
|
||||
context_store.clone(),
|
||||
workspace.clone(),
|
||||
Some(thread_store.clone()),
|
||||
editor.focus_handle(cx),
|
||||
context_picker_menu_handle.clone(),
|
||||
SuggestContextKind::File,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let subscriptions = vec![
|
||||
cx.subscribe(&editor, Self::handle_editor_event),
|
||||
cx.subscribe(
|
||||
&inline_context_picker,
|
||||
Self::handle_inline_context_picker_event,
|
||||
),
|
||||
cx.subscribe(&context_strip, Self::handle_context_strip_event),
|
||||
];
|
||||
|
||||
Self {
|
||||
thread,
|
||||
editor: cx.new_view(|cx| {
|
||||
let mut editor = Editor::auto_height(80, cx);
|
||||
editor.set_placeholder_text("Ask anything or type @ to add context", cx);
|
||||
|
||||
editor
|
||||
}),
|
||||
context: Vec::new(),
|
||||
next_context_id: ContextId(0),
|
||||
context_picker: cx.new_view(|cx| ContextPicker::new(workspace.clone(), weak_self, cx)),
|
||||
context_picker_handle: PopoverMenuHandle::default(),
|
||||
language_model_selector: cx.new_view(|cx| {
|
||||
LanguageModelSelector::new(
|
||||
|model, _cx| {
|
||||
println!("Selected {:?}", model.name());
|
||||
},
|
||||
cx,
|
||||
)
|
||||
editor: editor.clone(),
|
||||
context_store,
|
||||
context_strip,
|
||||
context_picker_menu_handle,
|
||||
inline_context_picker,
|
||||
inline_context_picker_menu_handle,
|
||||
model_selector: cx.new_view(|cx| {
|
||||
AssistantModelSelector::new(fs, model_selector_menu_handle.clone(), cx)
|
||||
}),
|
||||
model_selector_menu_handle,
|
||||
use_tools: false,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_context(
|
||||
&mut self,
|
||||
kind: ContextKind,
|
||||
name: impl Into<SharedString>,
|
||||
text: impl Into<SharedString>,
|
||||
) {
|
||||
self.context.push(Context {
|
||||
id: self.next_context_id.post_inc(),
|
||||
name: name.into(),
|
||||
kind,
|
||||
text: text.into(),
|
||||
});
|
||||
fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
|
||||
self.model_selector_menu_handle.toggle(cx)
|
||||
}
|
||||
|
||||
fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
|
||||
self.context_picker_menu_handle.toggle(cx);
|
||||
}
|
||||
|
||||
pub fn remove_all_context(&mut self, _: &RemoveAllContext, cx: &mut ViewContext<Self>) {
|
||||
self.context_store.update(cx, |store, _cx| store.clear());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
|
||||
@@ -100,7 +147,9 @@ impl MessageEditor {
|
||||
editor.clear(cx);
|
||||
text
|
||||
});
|
||||
let context = self.context.drain(..).collect::<Vec<_>>();
|
||||
let context = self
|
||||
.context_store
|
||||
.update(cx, |this, _cx| this.context().clone());
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message(user_message, context, cx);
|
||||
@@ -125,53 +174,48 @@ impl MessageEditor {
|
||||
None
|
||||
}
|
||||
|
||||
fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
fn handle_editor_event(
|
||||
&mut self,
|
||||
editor: View<Editor>,
|
||||
event: &EditorEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
EditorEvent::SelectionsChanged { .. } => {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let newest_cursor = editor.selections.newest::<Point>(cx).head();
|
||||
if newest_cursor.column > 0 {
|
||||
let behind_cursor = Point::new(newest_cursor.row, newest_cursor.column - 1);
|
||||
let char_behind_cursor = snapshot.chars_at(behind_cursor).next();
|
||||
if char_behind_cursor == Some('@') {
|
||||
self.inline_context_picker_menu_handle.show(cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
LanguageModelSelectorPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.overflow_x_hidden()
|
||||
.flex_grow()
|
||||
.whitespace_nowrap()
|
||||
.child(match (active_provider, active_model) {
|
||||
(Some(provider), Some(model)) => h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(
|
||||
model.icon().unwrap_or_else(|| provider.icon()),
|
||||
)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.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("Change Model", &ToggleModelSelector, cx)),
|
||||
)
|
||||
fn handle_inline_context_picker_event(
|
||||
&mut self,
|
||||
_inline_context_picker: View<ContextPicker>,
|
||||
_event: &DismissEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let editor_focus_handle = self.editor.focus_handle(cx);
|
||||
cx.focus(&editor_focus_handle);
|
||||
}
|
||||
|
||||
fn handle_context_strip_event(
|
||||
&mut self,
|
||||
_context_strip: View<ContextStrip>,
|
||||
ContextStripEvent::PickerDismissed: &ContextStripEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let editor_focus_handle = self.editor.focus_handle(cx);
|
||||
cx.focus(&editor_focus_handle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,111 +228,88 @@ impl FocusableView for MessageEditor {
|
||||
impl Render for MessageEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let font_size = TextSize::Default.rems(cx);
|
||||
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
|
||||
let line_height = font_size.to_pixels(cx.rem_size()) * 1.5;
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
let context_picker = self.context_picker.clone();
|
||||
let inline_context_picker = self.inline_context_picker.clone();
|
||||
let bg_color = cx.theme().colors().editor_background;
|
||||
|
||||
v_flex()
|
||||
.key_context("MessageEditor")
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(Self::toggle_model_selector))
|
||||
.on_action(cx.listener(Self::toggle_context_picker))
|
||||
.on_action(cx.listener(Self::remove_all_context))
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.bg(bg_color)
|
||||
.child(self.context_strip.clone())
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_wrap()
|
||||
.gap_2()
|
||||
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("context-picker")
|
||||
.menu(move |_cx| Some(context_picker.clone()))
|
||||
.trigger(
|
||||
IconButton::new("add-context", IconName::Plus)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small),
|
||||
)
|
||||
.attach(gpui::AnchorCorner::TopLeft)
|
||||
.anchor(gpui::AnchorCorner::BottomLeft)
|
||||
PopoverMenu::new("inline-context-picker")
|
||||
.menu(move |_cx| Some(inline_context_picker.clone()))
|
||||
.attach(gpui::Corner::TopLeft)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-16.0),
|
||||
})
|
||||
.with_handle(self.context_picker_handle.clone()),
|
||||
.with_handle(self.inline_context_picker_menu_handle.clone()),
|
||||
)
|
||||
.children(self.context.iter().map(|context| {
|
||||
ContextPill::new(context.clone()).on_remove({
|
||||
let context = context.clone();
|
||||
Rc::new(cx.listener(move |this, _event, cx| {
|
||||
this.context.retain(|other| other.id != context.id);
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
}))
|
||||
.when(!self.context.is_empty(), |parent| {
|
||||
parent.child(
|
||||
IconButton::new("remove-all-context", IconName::Eraser)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(move |cx| Tooltip::text("Remove All Context", cx))
|
||||
.on_click(cx.listener(|this, _event, cx| {
|
||||
this.context.clear();
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.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: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(h_flex().gap_2().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,
|
||||
};
|
||||
}),
|
||||
)))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(self.render_language_model_selector(cx))
|
||||
.justify_between()
|
||||
.child(SwitchWithLabel::new(
|
||||
"use-tools",
|
||||
Label::new("Tools").size(LabelSize::Small),
|
||||
self.use_tools.into(),
|
||||
cx.listener(|this, selection, _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").size(LabelSize::Small))
|
||||
.children(
|
||||
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
)
|
||||
.on_click(move |_event, cx| {
|
||||
focus_handle.dispatch_action(&Chat, cx);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
192
crates/assistant2/src/terminal_codegen.rs
Normal file
@@ -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,31 +1,29 @@
|
||||
use crate::assistant_settings::AssistantSettings;
|
||||
use crate::context::attach_context_to_message;
|
||||
use crate::context_store::ContextStore;
|
||||
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 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, WeakView,
|
||||
AppContext, Context, FocusableView, Global, Model, Subscription, UpdateGlobal, View, WeakModel,
|
||||
WeakView,
|
||||
};
|
||||
use language::Buffer;
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use language_models::report_assistant_event;
|
||||
use settings::{update_settings_file, Settings};
|
||||
use std::{cmp, sync::Arc, time::Instant};
|
||||
use std::sync::Arc;
|
||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use terminal::Terminal;
|
||||
use terminal_view::TerminalView;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, text_for_action, IconButtonShape, Tooltip};
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
use workspace::{notifications::NotificationId, Toast, Workspace};
|
||||
|
||||
@@ -41,17 +39,6 @@ pub fn init(
|
||||
const DEFAULT_CONTEXT_LINES: usize = 50;
|
||||
const PROMPT_HISTORY_MAX_LEN: usize = 20;
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
|
||||
struct TerminalInlineAssistId(usize);
|
||||
|
||||
impl TerminalInlineAssistId {
|
||||
fn post_inc(&mut self) -> TerminalInlineAssistId {
|
||||
let id = *self;
|
||||
self.0 += 1;
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TerminalInlineAssistant {
|
||||
next_assist_id: TerminalInlineAssistId,
|
||||
assists: HashMap<TerminalInlineAssistId, TerminalInlineAssist>,
|
||||
@@ -82,7 +69,8 @@ impl TerminalInlineAssistant {
|
||||
pub fn assist(
|
||||
&mut self,
|
||||
terminal_view: &View<TerminalView>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
workspace: WeakView<Workspace>,
|
||||
thread_store: Option<WeakModel<ThreadStore>>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let terminal = terminal_view.read(cx).terminal().clone();
|
||||
@@ -90,15 +78,19 @@ impl TerminalInlineAssistant {
|
||||
let prompt_buffer = cx.new_model(|cx| {
|
||||
MultiBuffer::singleton(cx.new_model(|cx| Buffer::local(String::new(), cx)), cx)
|
||||
});
|
||||
let codegen = cx.new_model(|_| Codegen::new(terminal, self.telemetry.clone()));
|
||||
let context_store = cx.new_model(|_cx| ContextStore::new());
|
||||
let codegen = cx.new_model(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
|
||||
|
||||
let prompt_editor = cx.new_view(|cx| {
|
||||
PromptEditor::new(
|
||||
PromptEditor::new_terminal(
|
||||
assist_id,
|
||||
self.prompt_history.clone(),
|
||||
prompt_buffer.clone(),
|
||||
codegen,
|
||||
self.fs.clone(),
|
||||
context_store.clone(),
|
||||
workspace.clone(),
|
||||
thread_store.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -116,6 +108,7 @@ impl TerminalInlineAssistant {
|
||||
terminal_view,
|
||||
prompt_editor,
|
||||
workspace.clone(),
|
||||
context_store,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -138,11 +131,11 @@ impl TerminalInlineAssistant {
|
||||
|
||||
fn handle_prompt_editor_event(
|
||||
&mut self,
|
||||
prompt_editor: View<PromptEditor>,
|
||||
prompt_editor: View<PromptEditor<TerminalCodegen>>,
|
||||
event: &PromptEditorEvent,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let assist_id = prompt_editor.read(cx).id;
|
||||
let assist_id = prompt_editor.read(cx).id();
|
||||
match event {
|
||||
PromptEditorEvent::StartRequested => {
|
||||
self.start_assist(assist_id, cx);
|
||||
@@ -246,12 +239,21 @@ impl TerminalInlineAssistant {
|
||||
&latest_output,
|
||||
)?;
|
||||
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![],
|
||||
cache: false,
|
||||
};
|
||||
|
||||
let context = assist
|
||||
.context_store
|
||||
.update(cx, |this, _cx| this.context().clone());
|
||||
attach_context_to_message(&mut request_message, context);
|
||||
|
||||
request_message.content.push(prompt.into());
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![prompt.into()],
|
||||
cache: false,
|
||||
}],
|
||||
messages: vec![request_message],
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
@@ -359,9 +361,10 @@ impl TerminalInlineAssistant {
|
||||
|
||||
struct TerminalInlineAssist {
|
||||
terminal: WeakView<TerminalView>,
|
||||
prompt_editor: Option<View<PromptEditor>>,
|
||||
codegen: Model<Codegen>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
prompt_editor: Option<View<PromptEditor<TerminalCodegen>>>,
|
||||
codegen: Model<TerminalCodegen>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: Model<ContextStore>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -369,16 +372,18 @@ impl TerminalInlineAssist {
|
||||
pub fn new(
|
||||
assist_id: TerminalInlineAssistId,
|
||||
terminal: &View<TerminalView>,
|
||||
prompt_editor: View<PromptEditor>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
prompt_editor: View<PromptEditor<TerminalCodegen>>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: Model<ContextStore>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self {
|
||||
let codegen = prompt_editor.read(cx).codegen.clone();
|
||||
let codegen = prompt_editor.read(cx).codegen().clone();
|
||||
Self {
|
||||
terminal: terminal.downgrade(),
|
||||
prompt_editor: Some(prompt_editor.clone()),
|
||||
codegen: codegen.clone(),
|
||||
workspace: workspace.clone(),
|
||||
context_store,
|
||||
_subscriptions: vec![
|
||||
cx.subscribe(&prompt_editor, |prompt_editor, event, cx| {
|
||||
TerminalInlineAssistant::update_global(cx, |this, cx| {
|
||||
@@ -396,11 +401,7 @@ impl TerminalInlineAssist {
|
||||
|
||||
if let CodegenStatus::Error(error) = &codegen.read(cx).status {
|
||||
if assist.prompt_editor.is_none() {
|
||||
if let Some(workspace) = assist
|
||||
.workspace
|
||||
.as_ref()
|
||||
.and_then(|workspace| workspace.upgrade())
|
||||
{
|
||||
if let Some(workspace) = assist.workspace.upgrade() {
|
||||
let error =
|
||||
format!("Terminal inline assistant error: {}", error);
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
@@ -427,636 +428,3 @@ impl TerminalInlineAssist {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PromptEditorEvent {
|
||||
StartRequested,
|
||||
StopRequested,
|
||||
ConfirmRequested { execute: bool },
|
||||
CancelRequested,
|
||||
DismissRequested,
|
||||
Resized { height_in_lines: u8 },
|
||||
}
|
||||
|
||||
struct PromptEditor {
|
||||
id: TerminalInlineAssistId,
|
||||
height_in_lines: u8,
|
||||
editor: View<Editor>,
|
||||
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![Button::new("add-context", "Add Context")
|
||||
.style(ButtonStyle::Filled)
|
||||
.icon(IconName::Plus)
|
||||
.icon_position(IconPosition::Start)
|
||||
.into_any_element()];
|
||||
|
||||
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(),
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_y_1()
|
||||
.border_color(cx.theme().status().info_border)
|
||||
.py_2()
|
||||
.h_full()
|
||||
.w_full()
|
||||
.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))
|
||||
}
|
||||
}
|
||||
|
||||
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>,
|
||||
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 mut this = Self {
|
||||
id,
|
||||
height_in_lines: 1,
|
||||
editor: prompt_editor,
|
||||
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 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),
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::future::Shared;
|
||||
use futures::{FutureExt as _, StreamExt as _};
|
||||
use gpui::{AppContext, EventEmitter, ModelContext, SharedString, Task};
|
||||
@@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize};
|
||||
use util::{post_inc, TryFutureExt as _};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::context::{Context, ContextKind};
|
||||
use crate::context::{attach_context_to_message, Context, ContextId};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum RequestKind {
|
||||
@@ -64,7 +64,8 @@ pub struct Thread {
|
||||
pending_summary: Task<Option<()>>,
|
||||
messages: Vec<Message>,
|
||||
next_message_id: MessageId,
|
||||
context_by_message: HashMap<MessageId, Vec<Context>>,
|
||||
context: HashMap<ContextId, Context>,
|
||||
context_by_message: HashMap<MessageId, Vec<ContextId>>,
|
||||
completion_count: usize,
|
||||
pending_completions: Vec<PendingCompletion>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
@@ -82,6 +83,7 @@ impl Thread {
|
||||
pending_summary: Task::ready(None),
|
||||
messages: Vec::new(),
|
||||
next_message_id: MessageId(0),
|
||||
context: HashMap::default(),
|
||||
context_by_message: HashMap::default(),
|
||||
completion_count: 0,
|
||||
pending_completions: Vec::new(),
|
||||
@@ -129,8 +131,15 @@ impl Thread {
|
||||
&self.tools
|
||||
}
|
||||
|
||||
pub fn context_for_message(&self, id: MessageId) -> Option<&Vec<Context>> {
|
||||
self.context_by_message.get(&id)
|
||||
pub fn context_for_message(&self, id: MessageId) -> Option<Vec<Context>> {
|
||||
let context = self.context_by_message.get(&id)?;
|
||||
Some(
|
||||
context
|
||||
.into_iter()
|
||||
.filter_map(|context_id| self.context.get(&context_id))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
|
||||
@@ -144,7 +153,10 @@ impl Thread {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let message_id = self.insert_message(Role::User, text, cx);
|
||||
self.context_by_message.insert(message_id, context);
|
||||
let context_ids = context.iter().map(|context| context.id).collect::<Vec<_>>();
|
||||
self.context
|
||||
.extend(context.into_iter().map(|context| (context.id, context)));
|
||||
self.context_by_message.insert(message_id, context_ids);
|
||||
}
|
||||
|
||||
pub fn insert_message(
|
||||
@@ -164,6 +176,27 @@ impl Thread {
|
||||
id
|
||||
}
|
||||
|
||||
/// Returns the representation of this [`Thread`] in a textual form.
|
||||
///
|
||||
/// This is the representation we use when attaching a thread as context to another thread.
|
||||
pub fn text(&self) -> String {
|
||||
let mut text = String::new();
|
||||
|
||||
for message in &self.messages {
|
||||
text.push_str(match message.role {
|
||||
language_model::Role::User => "User:",
|
||||
language_model::Role::Assistant => "Assistant:",
|
||||
language_model::Role::System => "System:",
|
||||
});
|
||||
text.push('\n');
|
||||
|
||||
text.push_str(&message.text);
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
text
|
||||
}
|
||||
|
||||
pub fn to_completion_request(
|
||||
&self,
|
||||
_request_kind: RequestKind,
|
||||
@@ -176,7 +209,13 @@ impl Thread {
|
||||
temperature: None,
|
||||
};
|
||||
|
||||
let mut referenced_context_ids = HashSet::default();
|
||||
|
||||
for message in &self.messages {
|
||||
if let Some(context_ids) = self.context_by_message.get(&message.id) {
|
||||
referenced_context_ids.extend(context_ids);
|
||||
}
|
||||
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: message.role,
|
||||
content: Vec::new(),
|
||||
@@ -191,41 +230,6 @@ impl Thread {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(context) = self.context_for_message(message.id) {
|
||||
let mut file_context = String::new();
|
||||
let mut fetch_context = String::new();
|
||||
|
||||
for context in context.iter() {
|
||||
match context.kind {
|
||||
ContextKind::File => {
|
||||
file_context.push_str(&context.text);
|
||||
file_context.push('\n');
|
||||
}
|
||||
ContextKind::FetchedUrl => {
|
||||
fetch_context.push_str(&context.name);
|
||||
fetch_context.push('\n');
|
||||
fetch_context.push_str(&context.text);
|
||||
fetch_context.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut context_text = String::new();
|
||||
if !file_context.is_empty() {
|
||||
context_text.push_str("The following files are available:\n");
|
||||
context_text.push_str(&file_context);
|
||||
}
|
||||
|
||||
if !fetch_context.is_empty() {
|
||||
context_text.push_str("The following fetched results are available\n");
|
||||
context_text.push_str(&fetch_context);
|
||||
}
|
||||
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::Text(context_text))
|
||||
}
|
||||
|
||||
if !message.text.is_empty() {
|
||||
request_message
|
||||
.content
|
||||
@@ -243,6 +247,22 @@ impl Thread {
|
||||
request.messages.push(request_message);
|
||||
}
|
||||
|
||||
if !referenced_context_ids.is_empty() {
|
||||
let mut context_message = LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: Vec::new(),
|
||||
cache: false,
|
||||
};
|
||||
|
||||
let referenced_context = referenced_context_ids
|
||||
.into_iter()
|
||||
.filter_map(|context_id| self.context.get(context_id))
|
||||
.cloned();
|
||||
attach_context_to_message(&mut context_message, referenced_context);
|
||||
|
||||
request.messages.push(context_message);
|
||||
}
|
||||
|
||||
request
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use gpui::{
|
||||
uniform_list, AppContext, FocusHandle, FocusableView, Model, UniformListScrollHandle, WeakView,
|
||||
};
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{prelude::*, IconButtonShape, ListItem};
|
||||
use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip};
|
||||
|
||||
use crate::thread::Thread;
|
||||
use crate::thread_store::ThreadStore;
|
||||
@@ -66,10 +66,10 @@ impl Render for ThreadHistory {
|
||||
threads[range]
|
||||
.iter()
|
||||
.map(|thread| {
|
||||
PastThread::new(
|
||||
h_flex().w_full().pb_1().child(PastThread::new(
|
||||
thread.clone(),
|
||||
history.assistant_panel.clone(),
|
||||
)
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
@@ -117,17 +117,29 @@ impl RenderOnce for PastThread {
|
||||
.unwrap_or(UtcOffset::UTC),
|
||||
time_format::TimestampFormat::EnhancedAbsolute,
|
||||
);
|
||||
|
||||
ListItem::new(("past-thread", self.thread.entity_id()))
|
||||
.start_slot(Icon::new(IconName::MessageBubbles))
|
||||
.child(Label::new(summary))
|
||||
.outlined()
|
||||
.start_slot(
|
||||
Icon::new(IconName::MessageCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis())
|
||||
.end_slot(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Label::new(thread_timestamp).color(Color::Disabled))
|
||||
.child(
|
||||
Label::new(thread_timestamp)
|
||||
.color(Color::Disabled)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("delete", IconName::TrashAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(|cx| Tooltip::text("Delete Thread", cx))
|
||||
.on_click({
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
let id = id.clone();
|
||||
|
||||
@@ -238,5 +238,46 @@ impl ThreadStore {
|
||||
Async programming in Rust provides a powerful way to write concurrent code that's both safe and efficient. It's particularly useful for servers, network programming, and any application that deals with many concurrent operations.".unindent(), cx);
|
||||
thread
|
||||
}));
|
||||
|
||||
self.threads.push(cx.new_model(|cx| {
|
||||
let mut thread = Thread::new(self.tools.clone(), cx);
|
||||
thread.set_summary("Rust code with long lines", cx);
|
||||
thread.insert_user_message("Could you write me some Rust code with long lines?", Vec::new(), cx);
|
||||
thread.insert_message(Role::Assistant, r#"Here's some Rust code with some intentionally long lines:
|
||||
```rust
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
fn main() {
|
||||
let very_long_vector = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25];
|
||||
|
||||
let complicated_hashmap: HashMap<String, Vec<(i32, f64, String)>> = [("key1".to_string(), vec![(1, 1.1, "value1".to_string()), (2, 2.2, "value2".to_string())]), ("key2".to_string(), vec![(3, 3.3, "value3".to_string()), (4, 4.4, "value4".to_string())])].iter().cloned().collect();
|
||||
|
||||
let nested_structure = Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
let long_closure = |x: i32, y: i32, z: i32| -> i32 { let result = x * y + z; println!("The result of the long closure calculation is: {}", result); result };
|
||||
|
||||
let thread_handles: Vec<_> = (0..10).map(|i| {
|
||||
let nested_structure_clone = Arc::clone(&nested_structure);
|
||||
thread::spawn(move || {
|
||||
let mut lock = nested_structure_clone.lock().unwrap();
|
||||
lock.entry(format!("thread_{}", i)).or_insert_with(|| HashSet::new()).insert(i * i);
|
||||
})
|
||||
}).collect();
|
||||
|
||||
for handle in thread_handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
println!("The final state of the nested structure is: {:?}", nested_structure.lock().unwrap());
|
||||
|
||||
let complex_expression = very_long_vector.iter().filter(|&&x| x % 2 == 0).map(|&x| x * x).fold(0, |acc, x| acc + x) + long_closure(5, 10, 15);
|
||||
|
||||
println!("The result of the complex expression is: {}", complex_expression);
|
||||
}
|
||||
```"#.unindent(), cx);
|
||||
thread
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,153 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::ClickEvent;
|
||||
use ui::{prelude::*, IconButtonShape};
|
||||
use ui::{prelude::*, IconButtonShape, Tooltip};
|
||||
|
||||
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)>>,
|
||||
pub enum ContextPill {
|
||||
Added {
|
||||
context: Context,
|
||||
dupe_name: bool,
|
||||
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
|
||||
},
|
||||
Suggested {
|
||||
name: SharedString,
|
||||
kind: ContextKind,
|
||||
on_add: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ContextPill {
|
||||
pub fn new(context: Context) -> Self {
|
||||
Self {
|
||||
pub fn new_added(
|
||||
context: Context,
|
||||
dupe_name: bool,
|
||||
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
|
||||
) -> Self {
|
||||
Self::Added {
|
||||
context,
|
||||
on_remove: None,
|
||||
dupe_name,
|
||||
on_remove,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_remove(mut self, on_remove: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>) -> Self {
|
||||
self.on_remove = Some(on_remove);
|
||||
self
|
||||
pub fn new_suggested(
|
||||
name: SharedString,
|
||||
kind: ContextKind,
|
||||
on_add: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>,
|
||||
) -> Self {
|
||||
Self::Suggested { name, kind, on_add }
|
||||
}
|
||||
|
||||
pub fn id(&self) -> ElementId {
|
||||
match self {
|
||||
Self::Added { context, .. } => {
|
||||
ElementId::NamedInteger("context-pill".into(), context.id.0)
|
||||
}
|
||||
Self::Suggested { .. } => "suggested-context-pill".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> &ContextKind {
|
||||
match self {
|
||||
Self::Added { context, .. } => &context.kind,
|
||||
Self::Suggested { kind, .. } => kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ContextPill {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.px_1()
|
||||
let icon = match &self.kind() {
|
||||
ContextKind::File => IconName::File,
|
||||
ContextKind::Directory => IconName::Folder,
|
||||
ContextKind::FetchedUrl => IconName::Globe,
|
||||
ContextKind::Thread => IconName::MessageCircle,
|
||||
};
|
||||
|
||||
let color = cx.theme().colors();
|
||||
|
||||
let base_pill = h_flex()
|
||||
.id(self.id())
|
||||
.pl_1()
|
||||
.pb(px(1.))
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_md()
|
||||
.child(Label::new(self.context.name.clone()).size(LabelSize::Small))
|
||||
.when_some(self.on_remove, |parent, on_remove| {
|
||||
parent.child(
|
||||
IconButton::new("remove", IconName::Close)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click({
|
||||
let on_remove = on_remove.clone();
|
||||
move |event, cx| on_remove(event, cx)
|
||||
.gap_1()
|
||||
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted));
|
||||
|
||||
match &self {
|
||||
ContextPill::Added {
|
||||
context,
|
||||
dupe_name,
|
||||
on_remove,
|
||||
} => base_pill
|
||||
.bg(color.element_background)
|
||||
.border_color(color.border.opacity(0.5))
|
||||
.pr(if on_remove.is_some() { px(2.) } else { px(4.) })
|
||||
.child(
|
||||
h_flex()
|
||||
.id("context-data")
|
||||
.gap_1()
|
||||
.child(Label::new(context.name.clone()).size(LabelSize::Small))
|
||||
.when_some(context.parent.as_ref(), |element, parent_name| {
|
||||
if *dupe_name {
|
||||
element.child(
|
||||
Label::new(parent_name.clone())
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
} else {
|
||||
element
|
||||
}
|
||||
})
|
||||
.when_some(context.tooltip.clone(), |element, tooltip| {
|
||||
element.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when_some(on_remove.as_ref(), |element, on_remove| {
|
||||
element.child(
|
||||
IconButton::new(("remove", context.id.0), IconName::Close)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.tooltip(|cx| Tooltip::text("Remove Context", cx))
|
||||
.on_click({
|
||||
let on_remove = on_remove.clone();
|
||||
move |event, cx| on_remove(event, cx)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
ContextPill::Suggested { name, kind, on_add } => base_pill
|
||||
.cursor_pointer()
|
||||
.pr_1()
|
||||
.border_color(color.border_variant.opacity(0.5))
|
||||
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
|
||||
.child(
|
||||
Label::new(name.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(match kind {
|
||||
ContextKind::File => "Open File",
|
||||
ContextKind::Thread | ContextKind::Directory | ContextKind::FetchedUrl => {
|
||||
"Active"
|
||||
}
|
||||
})
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::Plus)
|
||||
.size(IconSize::XSmall)
|
||||
.into_any_element(),
|
||||
)
|
||||
.tooltip(|cx| Tooltip::with_meta("Suggested Context", None, "Click to add it", cx))
|
||||
.on_click({
|
||||
let on_add = on_add.clone();
|
||||
move |event, cx| on_add(event, cx)
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ pub struct UpdateNotification {
|
||||
impl EventEmitter<DismissEvent> for UpdateNotification {}
|
||||
|
||||
impl Render for UpdateNotification {
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let app_name = ReleaseChannel::global(cx).display_name();
|
||||
|
||||
v_flex()
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -39,10 +39,17 @@ impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
|
||||
impl Render for Breadcrumbs {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
const MAX_SEGMENTS: usize = 12;
|
||||
let element = h_flex().text_ui(cx);
|
||||
|
||||
let element = h_flex()
|
||||
.id("breadcrumb-container")
|
||||
.flex_grow()
|
||||
.overflow_x_scroll()
|
||||
.text_ui(cx);
|
||||
|
||||
let Some(active_item) = self.active_item.as_ref() else {
|
||||
return element;
|
||||
};
|
||||
|
||||
let Some(mut segments) = active_item.breadcrumbs(cx.theme(), cx) else {
|
||||
return element;
|
||||
};
|
||||
@@ -52,6 +59,7 @@ impl Render for Breadcrumbs {
|
||||
prefix_end_ix,
|
||||
segments.len().saturating_sub(MAX_SEGMENTS / 2),
|
||||
);
|
||||
|
||||
if suffix_start_ix > prefix_end_ix {
|
||||
segments.splice(
|
||||
prefix_end_ix..suffix_start_ix,
|
||||
@@ -82,6 +90,7 @@ impl Render for Breadcrumbs {
|
||||
});
|
||||
|
||||
let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
|
||||
|
||||
match active_item
|
||||
.downcast::<Editor>()
|
||||
.map(|editor| editor.downgrade())
|
||||
@@ -93,8 +102,11 @@ impl Render for Breadcrumbs {
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
move |_, cx| {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
outline::toggle(editor, &editor::actions::ToggleOutline, cx)
|
||||
if let Some((editor, callback)) = editor
|
||||
.upgrade()
|
||||
.zip(zed_actions::outline::TOGGLE_OUTLINE.get())
|
||||
{
|
||||
callback(editor.to_any(), cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -102,15 +114,15 @@ impl Render for Breadcrumbs {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
let focus_handle = editor.read(cx).focus_handle(cx);
|
||||
Tooltip::for_action_in(
|
||||
"Show symbol outline",
|
||||
&editor::actions::ToggleOutline,
|
||||
"Show Symbol Outline",
|
||||
&zed_actions::outline::ToggleOutline,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
Tooltip::for_action(
|
||||
"Show symbol outline",
|
||||
&editor::actions::ToggleOutline,
|
||||
"Show Symbol Outline",
|
||||
&zed_actions::outline::ToggleOutline,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
settings.workspace = true
|
||||
telemetry.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
|
||||
@@ -250,7 +250,9 @@ impl ActiveCall {
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let result = invite.await;
|
||||
if result.is_ok() {
|
||||
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("Participant Invited", cx)
|
||||
})?;
|
||||
} else {
|
||||
//TODO: report collaboration error
|
||||
log::error!("invite failed: {:?}", result);
|
||||
@@ -318,7 +320,7 @@ impl ActiveCall {
|
||||
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("accept incoming", cx)
|
||||
this.report_call_event("Incoming Call Accepted", cx)
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
@@ -331,7 +333,7 @@ impl ActiveCall {
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("no incoming call"))?;
|
||||
report_call_event_for_room("decline incoming", call.room_id, None, &self.client);
|
||||
telemetry::event!("Incoming Call Declined", room_id = call.room_id);
|
||||
self.client.send(proto::DeclineCall {
|
||||
room_id: call.room_id,
|
||||
})?;
|
||||
@@ -366,7 +368,7 @@ impl ActiveCall {
|
||||
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("join channel", cx)
|
||||
this.report_call_event("Channel Joined", cx)
|
||||
})?;
|
||||
Ok(room)
|
||||
})
|
||||
@@ -374,7 +376,7 @@ impl ActiveCall {
|
||||
|
||||
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.notify();
|
||||
self.report_call_event("hang up", cx);
|
||||
self.report_call_event("Call Ended", cx);
|
||||
|
||||
Audio::end_call(cx);
|
||||
|
||||
@@ -393,7 +395,7 @@ impl ActiveCall {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<u64>> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("share project", cx);
|
||||
self.report_call_event("Project Shared", cx);
|
||||
room.update(cx, |room, cx| room.share_project(project, cx))
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("no active call")))
|
||||
@@ -406,7 +408,7 @@ impl ActiveCall {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("unshare project", cx);
|
||||
self.report_call_event("Project Unshared", cx);
|
||||
room.update(cx, |room, cx| room.unshare_project(project, cx))
|
||||
} else {
|
||||
Err(anyhow!("no active call"))
|
||||
@@ -486,35 +488,15 @@ impl ActiveCall {
|
||||
pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) {
|
||||
if let Some(room) = self.room() {
|
||||
let room = room.read(cx);
|
||||
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client);
|
||||
telemetry::event!(
|
||||
operation,
|
||||
room_id = room.id(),
|
||||
channel_id = room.channel_id()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_room(
|
||||
operation: &'static str,
|
||||
room_id: u64,
|
||||
channel_id: Option<ChannelId>,
|
||||
client: &Arc<Client>,
|
||||
) {
|
||||
let telemetry = client.telemetry();
|
||||
|
||||
telemetry.report_call_event(operation, Some(room_id), channel_id)
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_channel(
|
||||
operation: &'static str,
|
||||
channel_id: ChannelId,
|
||||
client: &Arc<Client>,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
let room = ActiveCall::global(cx).read(cx).room();
|
||||
|
||||
let telemetry = client.telemetry();
|
||||
|
||||
telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::TestAppContext;
|
||||
|
||||
@@ -243,7 +243,9 @@ impl ActiveCall {
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let result = invite.await;
|
||||
if result.is_ok() {
|
||||
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("Participant Invited", cx)
|
||||
})?;
|
||||
} else {
|
||||
//TODO: report collaboration error
|
||||
log::error!("invite failed: {:?}", result);
|
||||
@@ -311,7 +313,7 @@ impl ActiveCall {
|
||||
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("accept incoming", cx)
|
||||
this.report_call_event("Incoming Call Accepted", cx)
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
@@ -324,7 +326,7 @@ impl ActiveCall {
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("no incoming call"))?;
|
||||
report_call_event_for_room("decline incoming", call.room_id, None, &self.client);
|
||||
telemetry::event!("Incoming Call Declined", room_id = call.room_id);
|
||||
self.client.send(proto::DeclineCall {
|
||||
room_id: call.room_id,
|
||||
})?;
|
||||
@@ -359,7 +361,7 @@ impl ActiveCall {
|
||||
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("join channel", cx)
|
||||
this.report_call_event("Channel Joined", cx)
|
||||
})?;
|
||||
Ok(room)
|
||||
})
|
||||
@@ -367,7 +369,7 @@ impl ActiveCall {
|
||||
|
||||
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.notify();
|
||||
self.report_call_event("hang up", cx);
|
||||
self.report_call_event("Call Ended", cx);
|
||||
|
||||
Audio::end_call(cx);
|
||||
|
||||
@@ -386,7 +388,7 @@ impl ActiveCall {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<u64>> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("share project", cx);
|
||||
self.report_call_event("Project Shared", cx);
|
||||
room.update(cx, |room, cx| room.share_project(project, cx))
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("no active call")))
|
||||
@@ -399,7 +401,7 @@ impl ActiveCall {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("unshare project", cx);
|
||||
self.report_call_event("Project Unshared", cx);
|
||||
room.update(cx, |room, cx| room.unshare_project(project, cx))
|
||||
} else {
|
||||
Err(anyhow!("no active call"))
|
||||
@@ -479,35 +481,15 @@ impl ActiveCall {
|
||||
pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) {
|
||||
if let Some(room) = self.room() {
|
||||
let room = room.read(cx);
|
||||
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client);
|
||||
telemetry::event!(
|
||||
operation,
|
||||
room_id = room.id(),
|
||||
channel_id = room.channel_id()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_room(
|
||||
operation: &'static str,
|
||||
room_id: u64,
|
||||
channel_id: Option<ChannelId>,
|
||||
client: &Arc<Client>,
|
||||
) {
|
||||
let telemetry = client.telemetry();
|
||||
|
||||
telemetry.report_call_event(operation, Some(room_id), channel_id)
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_channel(
|
||||
operation: &'static str,
|
||||
channel_id: ChannelId,
|
||||
client: &Arc<Client>,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
let room = ActiveCall::global(cx).read(cx).room();
|
||||
|
||||
let telemetry = client.telemetry();
|
||||
|
||||
telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::TestAppContext;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,4 +2,10 @@ fn main() {
|
||||
if std::env::var("ZED_UPDATE_EXPLANATION").is_ok() {
|
||||
println!(r#"cargo:rustc-cfg=feature="no-bundled-uninstall""#);
|
||||
}
|
||||
|
||||
if cfg!(target_os = "macos") {
|
||||
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7");
|
||||
// Weakly link ScreenCaptureKit to ensure can be used on macOS 10.15+.
|
||||
println!("cargo:rustc-link-arg=-Wl,-weak_framework,ScreenCaptureKit");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -73,7 +79,7 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
|
||||
Ok(existing_path) => PathWithPosition::from_path(existing_path),
|
||||
Err(_) => {
|
||||
let path = PathWithPosition::parse_str(argument_str);
|
||||
let curdir = env::current_dir().context("reteiving current directory")?;
|
||||
let curdir = env::current_dir().context("retrieving current directory")?;
|
||||
path.map_path(|path| match fs::canonicalize(&path) {
|
||||
Ok(path) => Ok(path),
|
||||
Err(e) => {
|
||||
@@ -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![];
|
||||
@@ -262,6 +277,7 @@ mod linux {
|
||||
os::unix::net::{SocketAddr, UnixDatagram},
|
||||
path::{Path, PathBuf},
|
||||
process::{self, ExitStatus},
|
||||
sync::LazyLock,
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
@@ -269,12 +285,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
|
||||
|
||||
@@ -106,6 +106,22 @@ CREATE TABLE "worktree_repositories" (
|
||||
CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id");
|
||||
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");
|
||||
|
||||
CREATE TABLE "worktree_repository_statuses" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INT8 NOT NULL,
|
||||
"work_directory_id" INT8 NOT NULL,
|
||||
"repo_path" VARCHAR NOT NULL,
|
||||
"status" INT8 NOT NULL,
|
||||
"scan_id" INT8 NOT NULL,
|
||||
"is_deleted" BOOL NOT NULL,
|
||||
PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_wt_repos_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id");
|
||||
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
|
||||
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id_and_wd_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");
|
||||
|
||||
CREATE TABLE "worktree_settings_files" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INTEGER NOT NULL,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::Context as _;
|
||||
|
||||
use util::ResultExt;
|
||||
|
||||
use super::*;
|
||||
@@ -274,8 +275,8 @@ impl Database {
|
||||
mtime_nanos: ActiveValue::set(mtime.nanos as i32),
|
||||
canonical_path: ActiveValue::set(entry.canonical_path.clone()),
|
||||
is_ignored: ActiveValue::set(entry.is_ignored),
|
||||
git_status: ActiveValue::set(None),
|
||||
is_external: ActiveValue::set(entry.is_external),
|
||||
git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)),
|
||||
is_deleted: ActiveValue::set(false),
|
||||
scan_id: ActiveValue::set(update.scan_id as i64),
|
||||
is_fifo: ActiveValue::set(entry.is_fifo),
|
||||
@@ -295,7 +296,6 @@ impl Database {
|
||||
worktree_entry::Column::MtimeNanos,
|
||||
worktree_entry::Column::CanonicalPath,
|
||||
worktree_entry::Column::IsIgnored,
|
||||
worktree_entry::Column::GitStatus,
|
||||
worktree_entry::Column::ScanId,
|
||||
])
|
||||
.to_owned(),
|
||||
@@ -349,6 +349,79 @@ impl Database {
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
let has_any_statuses = update
|
||||
.updated_repositories
|
||||
.iter()
|
||||
.any(|repository| !repository.updated_statuses.is_empty());
|
||||
|
||||
if has_any_statuses {
|
||||
worktree_repository_statuses::Entity::insert_many(
|
||||
update.updated_repositories.iter().flat_map(
|
||||
|repository: &proto::RepositoryEntry| {
|
||||
repository.updated_statuses.iter().map(|status_entry| {
|
||||
worktree_repository_statuses::ActiveModel {
|
||||
project_id: ActiveValue::set(project_id),
|
||||
worktree_id: ActiveValue::set(worktree_id),
|
||||
work_directory_id: ActiveValue::set(
|
||||
repository.work_directory_id as i64,
|
||||
),
|
||||
scan_id: ActiveValue::set(update.scan_id as i64),
|
||||
is_deleted: ActiveValue::set(false),
|
||||
repo_path: ActiveValue::set(status_entry.repo_path.clone()),
|
||||
status: ActiveValue::set(status_entry.status as i64),
|
||||
}
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
worktree_repository_statuses::Column::ProjectId,
|
||||
worktree_repository_statuses::Column::WorktreeId,
|
||||
worktree_repository_statuses::Column::WorkDirectoryId,
|
||||
worktree_repository_statuses::Column::RepoPath,
|
||||
])
|
||||
.update_columns([
|
||||
worktree_repository_statuses::Column::ScanId,
|
||||
worktree_repository_statuses::Column::Status,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let has_any_removed_statuses = update
|
||||
.updated_repositories
|
||||
.iter()
|
||||
.any(|repository| !repository.removed_statuses.is_empty());
|
||||
|
||||
if has_any_removed_statuses {
|
||||
worktree_repository_statuses::Entity::update_many()
|
||||
.filter(
|
||||
worktree_repository_statuses::Column::ProjectId
|
||||
.eq(project_id)
|
||||
.and(
|
||||
worktree_repository_statuses::Column::WorktreeId
|
||||
.eq(worktree_id),
|
||||
)
|
||||
.and(
|
||||
worktree_repository_statuses::Column::RepoPath.is_in(
|
||||
update.updated_repositories.iter().flat_map(|repository| {
|
||||
repository.removed_statuses.iter()
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.set(worktree_repository_statuses::ActiveModel {
|
||||
is_deleted: ActiveValue::Set(true),
|
||||
scan_id: ActiveValue::Set(update.scan_id as i64),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
if !update.removed_repositories.is_empty() {
|
||||
@@ -643,7 +716,6 @@ impl Database {
|
||||
canonical_path: db_entry.canonical_path,
|
||||
is_ignored: db_entry.is_ignored,
|
||||
is_external: db_entry.is_external,
|
||||
git_status: db_entry.git_status.map(|status| status as i32),
|
||||
// This is only used in the summarization backlog, so if it's None,
|
||||
// that just means we won't be able to detect when to resummarize
|
||||
// based on total number of backlogged bytes - instead, we'd go
|
||||
@@ -657,23 +729,49 @@ impl Database {
|
||||
|
||||
// Populate repository entries.
|
||||
{
|
||||
let mut db_repository_entries = worktree_repository::Entity::find()
|
||||
let db_repository_entries = worktree_repository::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_repository::Column::ProjectId.eq(project.id))
|
||||
.add(worktree_repository::Column::IsDeleted.eq(false)),
|
||||
)
|
||||
.stream(tx)
|
||||
.all(tx)
|
||||
.await?;
|
||||
while let Some(db_repository_entry) = db_repository_entries.next().await {
|
||||
let db_repository_entry = db_repository_entry?;
|
||||
for db_repository_entry in db_repository_entries {
|
||||
if let Some(worktree) = worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
|
||||
{
|
||||
let mut repository_statuses = worktree_repository_statuses::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_repository_statuses::Column::ProjectId.eq(project.id))
|
||||
.add(
|
||||
worktree_repository_statuses::Column::WorktreeId
|
||||
.eq(worktree.id),
|
||||
)
|
||||
.add(
|
||||
worktree_repository_statuses::Column::WorkDirectoryId
|
||||
.eq(db_repository_entry.work_directory_id),
|
||||
)
|
||||
.add(worktree_repository_statuses::Column::IsDeleted.eq(false)),
|
||||
)
|
||||
.stream(tx)
|
||||
.await?;
|
||||
let mut updated_statuses = Vec::new();
|
||||
while let Some(status_entry) = repository_statuses.next().await {
|
||||
let status_entry: worktree_repository_statuses::Model = status_entry?;
|
||||
updated_statuses.push(proto::StatusEntry {
|
||||
repo_path: status_entry.repo_path,
|
||||
status: status_entry.status as i32,
|
||||
});
|
||||
}
|
||||
|
||||
worktree.repository_entries.insert(
|
||||
db_repository_entry.work_directory_id as u64,
|
||||
proto::RepositoryEntry {
|
||||
work_directory_id: db_repository_entry.work_directory_id as u64,
|
||||
branch: db_repository_entry.branch,
|
||||
updated_statuses,
|
||||
removed_statuses: Vec::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -662,7 +662,6 @@ impl Database {
|
||||
canonical_path: db_entry.canonical_path,
|
||||
is_ignored: db_entry.is_ignored,
|
||||
is_external: db_entry.is_external,
|
||||
git_status: db_entry.git_status.map(|status| status as i32),
|
||||
// This is only used in the summarization backlog, so if it's None,
|
||||
// that just means we won't be able to detect when to resummarize
|
||||
// based on total number of backlogged bytes - instead, we'd go
|
||||
@@ -682,26 +681,69 @@ impl Database {
|
||||
worktree_repository::Column::IsDeleted.eq(false)
|
||||
};
|
||||
|
||||
let mut db_repositories = worktree_repository::Entity::find()
|
||||
let db_repositories = worktree_repository::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_repository::Column::ProjectId.eq(project.id))
|
||||
.add(worktree_repository::Column::WorktreeId.eq(worktree.id))
|
||||
.add(repository_entry_filter),
|
||||
)
|
||||
.stream(tx)
|
||||
.all(tx)
|
||||
.await?;
|
||||
|
||||
while let Some(db_repository) = db_repositories.next().await {
|
||||
let db_repository = db_repository?;
|
||||
for db_repository in db_repositories.into_iter() {
|
||||
if db_repository.is_deleted {
|
||||
worktree
|
||||
.removed_repositories
|
||||
.push(db_repository.work_directory_id as u64);
|
||||
} else {
|
||||
let status_entry_filter = if let Some(rejoined_worktree) = rejoined_worktree
|
||||
{
|
||||
worktree_repository_statuses::Column::ScanId
|
||||
.gt(rejoined_worktree.scan_id)
|
||||
} else {
|
||||
worktree_repository_statuses::Column::IsDeleted.eq(false)
|
||||
};
|
||||
|
||||
let mut db_statuses = worktree_repository_statuses::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(
|
||||
worktree_repository_statuses::Column::ProjectId
|
||||
.eq(project.id),
|
||||
)
|
||||
.add(
|
||||
worktree_repository_statuses::Column::WorktreeId
|
||||
.eq(worktree.id),
|
||||
)
|
||||
.add(
|
||||
worktree_repository_statuses::Column::WorkDirectoryId
|
||||
.eq(db_repository.work_directory_id),
|
||||
)
|
||||
.add(status_entry_filter),
|
||||
)
|
||||
.stream(tx)
|
||||
.await?;
|
||||
let mut removed_statuses = Vec::new();
|
||||
let mut updated_statuses = Vec::new();
|
||||
|
||||
while let Some(db_status) = db_statuses.next().await {
|
||||
let db_status: worktree_repository_statuses::Model = db_status?;
|
||||
if db_status.is_deleted {
|
||||
removed_statuses.push(db_status.repo_path);
|
||||
} else {
|
||||
updated_statuses.push(proto::StatusEntry {
|
||||
repo_path: db_status.repo_path,
|
||||
status: db_status.status as i32,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
worktree.updated_repositories.push(proto::RepositoryEntry {
|
||||
work_directory_id: db_repository.work_directory_id as u64,
|
||||
branch: db_repository.branch,
|
||||
updated_statuses,
|
||||
removed_statuses,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2925,8 +2925,6 @@ async fn test_git_status_sync(
|
||||
assert_eq!(snapshot.status_for_file(file), status);
|
||||
}
|
||||
|
||||
// Smoke test status reading
|
||||
|
||||
project_local.read_with(cx_a, |project, cx| {
|
||||
assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
|
||||
assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
|
||||
@@ -6669,6 +6667,10 @@ async fn test_remote_git_branches(
|
||||
client_a
|
||||
.fs()
|
||||
.insert_branches(Path::new("/project/.git"), &branches);
|
||||
let branches_set = branches
|
||||
.into_iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/project", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
@@ -6690,10 +6692,10 @@ async fn test_remote_git_branches(
|
||||
|
||||
let branches_b = branches_b
|
||||
.into_iter()
|
||||
.map(|branch| branch.name)
|
||||
.collect::<Vec<_>>();
|
||||
.map(|branch| branch.name.to_string())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
assert_eq!(&branches_b, &branches);
|
||||
assert_eq!(branches_b, branches_set);
|
||||
|
||||
cx_b.update(|cx| {
|
||||
project_b.update(cx, |project, cx| {
|
||||
|
||||
@@ -1134,7 +1134,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
let end = PointUtf16::new(end_row, end_column);
|
||||
let range = if start > end { end..start } else { start..end };
|
||||
highlights.push(lsp::DocumentHighlight {
|
||||
range: range_to_lsp(range.clone()),
|
||||
range: range_to_lsp(range.clone()).unwrap(),
|
||||
kind: Some(lsp::DocumentHighlightKind::READ),
|
||||
});
|
||||
}
|
||||
@@ -1222,7 +1222,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
id,
|
||||
guest_project.remote_id(),
|
||||
);
|
||||
assert_eq!(guest_snapshot.repositories().collect::<Vec<_>>(), host_snapshot.repositories().collect::<Vec<_>>(),
|
||||
assert_eq!(guest_snapshot.repositories().iter().collect::<Vec<_>>(), host_snapshot.repositories().iter().collect::<Vec<_>>(),
|
||||
"{} has different repositories than the host for worktree {:?} and project {:?}",
|
||||
client.username,
|
||||
host_snapshot.abs_path(),
|
||||
|
||||
@@ -229,6 +229,10 @@ async fn test_ssh_collaboration_git_branches(
|
||||
.await;
|
||||
|
||||
let branches = ["main", "dev", "feature-1"];
|
||||
let branches_set = branches
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<HashSet<_>>();
|
||||
remote_fs.insert_branches(Path::new("/project/.git"), &branches);
|
||||
|
||||
// User A connects to the remote project via SSH.
|
||||
@@ -281,10 +285,10 @@ async fn test_ssh_collaboration_git_branches(
|
||||
|
||||
let branches_b = branches_b
|
||||
.into_iter()
|
||||
.map(|branch| branch.name)
|
||||
.collect::<Vec<_>>();
|
||||
.map(|branch| branch.name.to_string())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
assert_eq!(&branches_b, &branches);
|
||||
assert_eq!(&branches_b, &branches_set);
|
||||
|
||||
cx_b.update(|cx| {
|
||||
project_b.update(cx, |project, cx| {
|
||||
|
||||
@@ -44,7 +44,6 @@ gpui.workspace = true
|
||||
language.workspace = true
|
||||
menu.workspace = true
|
||||
notifications.workspace = true
|
||||
parking_lot.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
release_channel.workspace = true
|
||||
@@ -57,6 +56,7 @@ serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
story = { workspace = true, optional = true }
|
||||
telemetry.workspace = true
|
||||
theme.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use call::report_call_event_for_channel;
|
||||
use call::ActiveCall;
|
||||
use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelStore};
|
||||
use client::{
|
||||
proto::{self, PeerId},
|
||||
@@ -66,11 +66,13 @@ impl ChannelView {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let channel_view = channel_view.await?;
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
report_call_event_for_channel(
|
||||
"open channel notes",
|
||||
telemetry::event!(
|
||||
"Channel Notes Opened",
|
||||
channel_id,
|
||||
&workspace.read(cx).app_state().client,
|
||||
cx,
|
||||
room_id = ActiveCall::global(cx)
|
||||
.read(cx)
|
||||
.room()
|
||||
.map(|r| r.read(cx).id())
|
||||
);
|
||||
pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
|
||||
})?;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{collab_panel, ChatPanelSettings};
|
||||
use crate::{collab_panel, ChatPanelButton, ChatPanelSettings};
|
||||
use anyhow::Result;
|
||||
use call::{room, ActiveCall};
|
||||
use channel::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, ChannelStore};
|
||||
@@ -1096,7 +1096,7 @@ impl FocusableView for ChatPanel {
|
||||
}
|
||||
|
||||
impl Panel for ChatPanel {
|
||||
fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
|
||||
fn position(&self, cx: &WindowContext) -> DockPosition {
|
||||
ChatPanelSettings::get_global(cx).dock
|
||||
}
|
||||
|
||||
@@ -1112,7 +1112,7 @@ impl Panel for ChatPanel {
|
||||
);
|
||||
}
|
||||
|
||||
fn size(&self, cx: &gpui::WindowContext) -> Pixels {
|
||||
fn size(&self, cx: &WindowContext) -> Pixels {
|
||||
self.width
|
||||
.unwrap_or_else(|| ChatPanelSettings::get_global(cx).default_width)
|
||||
}
|
||||
@@ -1135,7 +1135,20 @@ impl Panel for ChatPanel {
|
||||
}
|
||||
|
||||
fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
|
||||
Some(ui::IconName::MessageBubbles).filter(|_| ChatPanelSettings::get_global(cx).button)
|
||||
let show_icon = match ChatPanelSettings::get_global(cx).button {
|
||||
ChatPanelButton::Never => false,
|
||||
ChatPanelButton::Always => true,
|
||||
ChatPanelButton::WhenInCall => {
|
||||
let is_in_call = ActiveCall::global(cx)
|
||||
.read(cx)
|
||||
.room()
|
||||
.map_or(false, |room| room.read(cx).contains_guests());
|
||||
|
||||
self.active || is_in_call
|
||||
}
|
||||
};
|
||||
|
||||
show_icon.then(|| ui::IconName::MessageBubbles)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||
@@ -1152,6 +1165,10 @@ impl Panel for ChatPanel {
|
||||
.room()
|
||||
.is_some_and(|room| room.read(cx).contains_guests())
|
||||
}
|
||||
|
||||
fn activation_priority(&self) -> u32 {
|
||||
7
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for ChatPanel {}
|
||||
|
||||
@@ -12,10 +12,15 @@ use language::{
|
||||
language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry,
|
||||
LanguageServerId, ToOffset,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use project::{search::SearchQuery, Completion};
|
||||
use settings::Settings;
|
||||
use std::{ops::Range, sync::Arc, sync::LazyLock, time::Duration};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
ops::Range,
|
||||
rc::Rc,
|
||||
sync::{Arc, LazyLock},
|
||||
time::Duration,
|
||||
};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, TextSize};
|
||||
|
||||
@@ -68,22 +73,12 @@ impl CompletionProvider for MessageEditorCompletionProvider {
|
||||
&self,
|
||||
_buffer: Model<Buffer>,
|
||||
_completion_indices: Vec<usize>,
|
||||
_completions: Arc<RwLock<Box<[Completion]>>>,
|
||||
_completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
_cx: &mut ViewContext<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>,
|
||||
@@ -314,6 +309,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()
|
||||
@@ -381,11 +377,7 @@ impl MessageEditor {
|
||||
|
||||
let candidates = names
|
||||
.into_iter()
|
||||
.map(|user| StringMatchCandidate {
|
||||
id: 0,
|
||||
string: user.clone(),
|
||||
char_bag: user.chars().collect(),
|
||||
})
|
||||
.map(|user| StringMatchCandidate::new(0, &user))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Some((start_anchor, query, candidates))
|
||||
@@ -401,11 +393,7 @@ impl MessageEditor {
|
||||
LazyLock::new(|| {
|
||||
let emojis = emojis::iter()
|
||||
.flat_map(|s| s.shortcodes())
|
||||
.map(|emoji| StringMatchCandidate {
|
||||
id: 0,
|
||||
string: emoji.to_string(),
|
||||
char_bag: emoji.chars().collect(),
|
||||
})
|
||||
.map(|emoji| StringMatchCandidate::new(0, emoji))
|
||||
.collect::<Vec<_>>();
|
||||
emojis
|
||||
});
|
||||
|
||||
@@ -393,11 +393,8 @@ impl CollabPanel {
|
||||
// Populate the active user.
|
||||
if let Some(user) = user_store.current_user() {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates.push(StringMatchCandidate {
|
||||
id: 0,
|
||||
string: user.github_login.clone(),
|
||||
char_bag: user.github_login.chars().collect(),
|
||||
});
|
||||
self.match_candidates
|
||||
.push(StringMatchCandidate::new(0, &user.github_login));
|
||||
let matches = executor.block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
@@ -436,11 +433,10 @@ impl CollabPanel {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates
|
||||
.extend(room.remote_participants().values().map(|participant| {
|
||||
StringMatchCandidate {
|
||||
id: participant.user.id as usize,
|
||||
string: participant.user.github_login.clone(),
|
||||
char_bag: participant.user.github_login.chars().collect(),
|
||||
}
|
||||
StringMatchCandidate::new(
|
||||
participant.user.id as usize,
|
||||
&participant.user.github_login,
|
||||
)
|
||||
}));
|
||||
let mut matches = executor.block(match_strings(
|
||||
&self.match_candidates,
|
||||
@@ -489,10 +485,8 @@ impl CollabPanel {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates
|
||||
.extend(room.pending_participants().iter().enumerate().map(
|
||||
|(id, participant)| StringMatchCandidate {
|
||||
id,
|
||||
string: participant.github_login.clone(),
|
||||
char_bag: participant.github_login.chars().collect(),
|
||||
|(id, participant)| {
|
||||
StringMatchCandidate::new(id, &participant.github_login)
|
||||
},
|
||||
));
|
||||
let matches = executor.block(match_strings(
|
||||
@@ -519,17 +513,12 @@ impl CollabPanel {
|
||||
|
||||
if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates
|
||||
.extend(
|
||||
channel_store
|
||||
.ordered_channels()
|
||||
.enumerate()
|
||||
.map(|(ix, (_, channel))| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: channel.name.clone().into(),
|
||||
char_bag: channel.name.chars().collect(),
|
||||
}),
|
||||
);
|
||||
self.match_candidates.extend(
|
||||
channel_store
|
||||
.ordered_channels()
|
||||
.enumerate()
|
||||
.map(|(ix, (_, channel))| StringMatchCandidate::new(ix, &channel.name)),
|
||||
);
|
||||
let matches = executor.block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
@@ -600,14 +589,12 @@ impl CollabPanel {
|
||||
let channel_invites = channel_store.channel_invitations();
|
||||
if !channel_invites.is_empty() {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates
|
||||
.extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
|
||||
StringMatchCandidate {
|
||||
id: ix,
|
||||
string: channel.name.clone().into(),
|
||||
char_bag: channel.name.chars().collect(),
|
||||
}
|
||||
}));
|
||||
self.match_candidates.extend(
|
||||
channel_invites
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, channel)| StringMatchCandidate::new(ix, &channel.name)),
|
||||
);
|
||||
let matches = executor.block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
@@ -637,17 +624,12 @@ impl CollabPanel {
|
||||
let incoming = user_store.incoming_contact_requests();
|
||||
if !incoming.is_empty() {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates
|
||||
.extend(
|
||||
incoming
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, user)| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: user.github_login.clone(),
|
||||
char_bag: user.github_login.chars().collect(),
|
||||
}),
|
||||
);
|
||||
self.match_candidates.extend(
|
||||
incoming
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, user)| StringMatchCandidate::new(ix, &user.github_login)),
|
||||
);
|
||||
let matches = executor.block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
@@ -666,17 +648,12 @@ impl CollabPanel {
|
||||
let outgoing = user_store.outgoing_contact_requests();
|
||||
if !outgoing.is_empty() {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates
|
||||
.extend(
|
||||
outgoing
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, user)| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: user.github_login.clone(),
|
||||
char_bag: user.github_login.chars().collect(),
|
||||
}),
|
||||
);
|
||||
self.match_candidates.extend(
|
||||
outgoing
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, user)| StringMatchCandidate::new(ix, &user.github_login)),
|
||||
);
|
||||
let matches = executor.block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
@@ -703,17 +680,12 @@ impl CollabPanel {
|
||||
let contacts = user_store.contacts();
|
||||
if !contacts.is_empty() {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates
|
||||
.extend(
|
||||
contacts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, contact)| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: contact.user.github_login.clone(),
|
||||
char_bag: contact.user.github_login.chars().collect(),
|
||||
}),
|
||||
);
|
||||
self.match_candidates.extend(
|
||||
contacts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, contact)| StringMatchCandidate::new(ix, &contact.user.github_login)),
|
||||
);
|
||||
|
||||
let matches = executor.block(match_strings(
|
||||
&self.match_candidates,
|
||||
@@ -2736,7 +2708,7 @@ impl Render for CollabPanel {
|
||||
deferred(
|
||||
anchored()
|
||||
.position(*position)
|
||||
.anchor(gpui::AnchorCorner::TopLeft)
|
||||
.anchor(gpui::Corner::TopLeft)
|
||||
.child(menu.clone()),
|
||||
)
|
||||
.with_priority(1)
|
||||
@@ -2747,7 +2719,7 @@ impl Render for CollabPanel {
|
||||
impl EventEmitter<PanelEvent> for CollabPanel {}
|
||||
|
||||
impl Panel for CollabPanel {
|
||||
fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
|
||||
fn position(&self, cx: &WindowContext) -> DockPosition {
|
||||
CollaborationPanelSettings::get_global(cx).dock
|
||||
}
|
||||
|
||||
@@ -2763,7 +2735,7 @@ impl Panel for CollabPanel {
|
||||
);
|
||||
}
|
||||
|
||||
fn size(&self, cx: &gpui::WindowContext) -> Pixels {
|
||||
fn size(&self, cx: &WindowContext) -> Pixels {
|
||||
self.width
|
||||
.unwrap_or_else(|| CollaborationPanelSettings::get_global(cx).default_width)
|
||||
}
|
||||
@@ -2774,7 +2746,7 @@ impl Panel for CollabPanel {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::IconName> {
|
||||
fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
|
||||
CollaborationPanelSettings::get_global(cx)
|
||||
.button
|
||||
.then_some(ui::IconName::UserGroup)
|
||||
@@ -2791,6 +2763,10 @@ impl Panel for CollabPanel {
|
||||
fn persistent_name() -> &'static str {
|
||||
"CollabPanel"
|
||||
}
|
||||
|
||||
fn activation_priority(&self) -> u32 {
|
||||
6
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for CollabPanel {
|
||||
|
||||
@@ -272,11 +272,7 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates
|
||||
.extend(self.members.iter().enumerate().map(|(id, member)| {
|
||||
StringMatchCandidate {
|
||||
id,
|
||||
string: member.user.github_login.clone(),
|
||||
char_bag: member.user.github_login.chars().collect(),
|
||||
}
|
||||
StringMatchCandidate::new(id, &member.user.github_login)
|
||||
}));
|
||||
|
||||
let matches = cx.background_executor().block(match_strings(
|
||||
@@ -413,7 +409,7 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
Some(
|
||||
deferred(
|
||||
anchored()
|
||||
.anchor(gpui::AnchorCorner::TopRight)
|
||||
.anchor(gpui::Corner::TopRight)
|
||||
.child(menu.clone()),
|
||||
)
|
||||
.with_priority(1),
|
||||
|
||||
@@ -14,7 +14,7 @@ use gpui::{
|
||||
};
|
||||
use panel_settings::MessageEditorSettings;
|
||||
pub use panel_settings::{
|
||||
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
|
||||
ChatPanelButton, ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
|
||||
};
|
||||
use release_channel::ReleaseChannel;
|
||||
use settings::Settings;
|
||||
@@ -44,7 +44,7 @@ fn notification_window_options(
|
||||
let notification_margin_height = px(-48.);
|
||||
|
||||
let bounds = gpui::Bounds::<Pixels> {
|
||||
origin: screen.bounds().upper_right()
|
||||
origin: screen.bounds().top_right()
|
||||
- point(
|
||||
size.width + notification_margin_width,
|
||||
notification_margin_height,
|
||||
|
||||
@@ -662,7 +662,7 @@ impl Panel for NotificationPanel {
|
||||
"NotificationPanel"
|
||||
}
|
||||
|
||||
fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
|
||||
fn position(&self, cx: &WindowContext) -> DockPosition {
|
||||
NotificationPanelSettings::get_global(cx).dock
|
||||
}
|
||||
|
||||
@@ -678,7 +678,7 @@ impl Panel for NotificationPanel {
|
||||
);
|
||||
}
|
||||
|
||||
fn size(&self, cx: &gpui::WindowContext) -> Pixels {
|
||||
fn size(&self, cx: &WindowContext) -> Pixels {
|
||||
self.width
|
||||
.unwrap_or_else(|| NotificationPanelSettings::get_global(cx).default_width)
|
||||
}
|
||||
@@ -702,7 +702,7 @@ impl Panel for NotificationPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn icon(&self, cx: &gpui::WindowContext) -> Option<IconName> {
|
||||
fn icon(&self, cx: &WindowContext) -> Option<IconName> {
|
||||
let show_button = NotificationPanelSettings::get_global(cx).button;
|
||||
if !show_button {
|
||||
return None;
|
||||
@@ -731,6 +731,10 @@ impl Panel for NotificationPanel {
|
||||
fn toggle_action(&self) -> Box<dyn gpui::Action> {
|
||||
Box::new(ToggleFocus)
|
||||
}
|
||||
|
||||
fn activation_priority(&self) -> u32 {
|
||||
8
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NotificationToast {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::Pixels;
|
||||
use schemars::JsonSchema;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use workspace::dock::DockPosition;
|
||||
|
||||
@@ -11,13 +11,82 @@ pub struct CollaborationPanelSettings {
|
||||
pub default_width: Pixels,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, Serialize, JsonSchema, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ChatPanelButton {
|
||||
Never,
|
||||
Always,
|
||||
#[default]
|
||||
WhenInCall,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ChatPanelButton {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct Visitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for Visitor {
|
||||
type Value = ChatPanelButton;
|
||||
|
||||
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
r#"a boolean or one of "never", "always", "when_in_call""#
|
||||
)
|
||||
}
|
||||
|
||||
fn visit_bool<E>(self, b: bool) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
match b {
|
||||
false => Ok(ChatPanelButton::Never),
|
||||
true => Ok(ChatPanelButton::Always),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
match s {
|
||||
"never" => Ok(ChatPanelButton::Never),
|
||||
"always" => Ok(ChatPanelButton::Always),
|
||||
"when_in_call" => Ok(ChatPanelButton::WhenInCall),
|
||||
_ => Err(E::unknown_variant(s, &["never", "always", "when_in_call"])),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(Visitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ChatPanelSettings {
|
||||
pub button: bool,
|
||||
pub button: ChatPanelButton,
|
||||
pub dock: DockPosition,
|
||||
pub default_width: Pixels,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct ChatPanelSettingsContent {
|
||||
/// When to show the panel button in the status bar.
|
||||
///
|
||||
/// Default: only when in a call
|
||||
pub button: Option<ChatPanelButton>,
|
||||
/// Where to dock the panel.
|
||||
///
|
||||
/// Default: right
|
||||
pub dock: Option<DockPosition>,
|
||||
/// Default width of the panel in pixels.
|
||||
///
|
||||
/// Default: 240
|
||||
pub default_width: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct NotificationPanelSettings {
|
||||
pub button: bool,
|
||||
@@ -66,7 +135,7 @@ impl Settings for CollaborationPanelSettings {
|
||||
impl Settings for ChatPanelSettings {
|
||||
const KEY: Option<&'static str> = Some("chat_panel");
|
||||
|
||||
type FileContent = PanelSettingsContent;
|
||||
type FileContent = ChatPanelSettingsContent;
|
||||
|
||||
fn load(
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
|
||||
@@ -16,4 +16,5 @@ doctest = false
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
rustc-hash = "1.1"
|
||||
indexmap.workspace = true
|
||||
rustc-hash.workspace = true
|
||||
|
||||
@@ -4,12 +4,24 @@ pub type HashMap<K, V> = FxHashMap<K, V>;
|
||||
#[cfg(feature = "test-support")]
|
||||
pub type HashSet<T> = FxHashSet<T>;
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub type IndexMap<K, V> = indexmap::IndexMap<K, V, rustc_hash::FxBuildHasher>;
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub type IndexSet<T> = indexmap::IndexSet<T, rustc_hash::FxBuildHasher>;
|
||||
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub type HashMap<K, V> = std::collections::HashMap<K, V>;
|
||||
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub type HashSet<T> = std::collections::HashSet<T>;
|
||||
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub type IndexMap<K, V> = indexmap::IndexMap<K, V>;
|
||||
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub type IndexSet<T> = indexmap::IndexSet<T>;
|
||||
|
||||
pub use rustc_hash::FxHasher;
|
||||
pub use rustc_hash::{FxHashMap, FxHashSet};
|
||||
pub use std::collections::*;
|
||||
|
||||