Compare commits
283 Commits
fix-depend
...
debugger-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdc5999d79 | ||
|
|
74931bd472 | ||
|
|
7f8c28877f | ||
|
|
1ff23477de | ||
|
|
d28950c633 | ||
|
|
6ff5e00740 | ||
|
|
b70acdfa4a | ||
|
|
403ae10087 | ||
|
|
9a8a54109e | ||
|
|
11c740b47a | ||
|
|
b7c6f3e98e | ||
|
|
7146087b44 | ||
|
|
6d3eaa055f | ||
|
|
f31c55a76f | ||
|
|
97750529fe | ||
|
|
cd9dd5ccf7 | ||
|
|
3ce864e69e | ||
|
|
9eeb564c5c | ||
|
|
847bd35bd9 | ||
|
|
b8e5ddf456 | ||
|
|
6998c03c59 | ||
|
|
8631180e43 | ||
|
|
cd9a42e8da | ||
|
|
3246a932ca | ||
|
|
8ba392bba6 | ||
|
|
856a8ef5e8 | ||
|
|
6dd9ce1376 | ||
|
|
fbbea7ab01 | ||
|
|
aded3dfb05 | ||
|
|
3053f98652 | ||
|
|
ebd407deb6 | ||
|
|
5e44f5fa44 | ||
|
|
a07122d78f | ||
|
|
6a079cbdc3 | ||
|
|
c7e2d5bd89 | ||
|
|
6bff8ecb73 | ||
|
|
e2113e4895 | ||
|
|
b56e4ff2af | ||
|
|
88f68101d4 | ||
|
|
d852a32ef1 | ||
|
|
b53c3b84e2 | ||
|
|
3d6b07d78c | ||
|
|
4c0d0ad4f4 | ||
|
|
55575ca830 | ||
|
|
ea44af4a85 | ||
|
|
5865c5e80f | ||
|
|
51f8013616 | ||
|
|
b9570218b6 | ||
|
|
fb4d77c008 | ||
|
|
b14bb6bda4 | ||
|
|
8501ae6a19 | ||
|
|
4e88a08ed4 | ||
|
|
659f34bf21 | ||
|
|
001376fd6d | ||
|
|
298ca5ff1b | ||
|
|
596ee58be8 | ||
|
|
fd4a4127eb | ||
|
|
0297a42735 | ||
|
|
9c9a0bd24f | ||
|
|
740c444089 | ||
|
|
325e6b9fef | ||
|
|
65c63defcc | ||
|
|
274e56b086 | ||
|
|
7fb906d774 | ||
|
|
d107d22c2d | ||
|
|
23dac9cfce | ||
|
|
777ddefa73 | ||
|
|
77a2f2490e | ||
|
|
846fc67519 | ||
|
|
0843da0f81 | ||
|
|
9e528dbb16 | ||
|
|
c6d6c44810 | ||
|
|
9de6d318e9 | ||
|
|
d540c95c81 | ||
|
|
f7d6818c97 | ||
|
|
06d3dc010c | ||
|
|
64b15d3a0e | ||
|
|
23c5fc1b03 | ||
|
|
c43eafbf9a | ||
|
|
62a890fb7f | ||
|
|
5ad3c6b6dc | ||
|
|
e004a573fd | ||
|
|
babf8923ee | ||
|
|
1a421db92d | ||
|
|
faf5ab7873 | ||
|
|
9c60884771 | ||
|
|
912b396e58 | ||
|
|
ee3323d12a | ||
|
|
87d93033d1 | ||
|
|
b88fd3e0c5 | ||
|
|
af4b9805c9 | ||
|
|
ba6c36f370 | ||
|
|
7644605f0f | ||
|
|
b4a8f14a76 | ||
|
|
c84da37030 | ||
|
|
edf7f6defe | ||
|
|
1307a80e07 | ||
|
|
9d11a6ff78 | ||
|
|
6769e55ce0 | ||
|
|
855048041d | ||
|
|
d36ebc8c74 | ||
|
|
5062bf0b4d | ||
|
|
4d65f7eea3 | ||
|
|
7ae305ac0d | ||
|
|
ba4ff1df59 | ||
|
|
d034d73af9 | ||
|
|
5f7881fc1e | ||
|
|
b0c525af5f | ||
|
|
41a3e78b1e | ||
|
|
5021397c01 | ||
|
|
b2b9d4ccb6 | ||
|
|
4a43084cb7 | ||
|
|
fa76d8edcf | ||
|
|
5f8e799d60 | ||
|
|
38e3182bef | ||
|
|
ec093c390f | ||
|
|
3d1bf09299 | ||
|
|
53b711c2b4 | ||
|
|
d0f52e90e6 | ||
|
|
17ef9a367f | ||
|
|
53f828df7d | ||
|
|
d9a00b6f8b | ||
|
|
7d0386eff9 | ||
|
|
bdf1d4edea | ||
|
|
c262c81e52 | ||
|
|
a5cb66f0e1 | ||
|
|
a0d687c24a | ||
|
|
5f57efb266 | ||
|
|
6398b45084 | ||
|
|
728650f94a | ||
|
|
2e10853b34 | ||
|
|
dde9d37cf9 | ||
|
|
5d77a7dc6c | ||
|
|
1fae99a7c4 | ||
|
|
eb210ca248 | ||
|
|
ddea18d546 | ||
|
|
b85dba106b | ||
|
|
4ba430b16c | ||
|
|
fe1f55cbfd | ||
|
|
01392c1329 | ||
|
|
a9397834eb | ||
|
|
4227a3d3e0 | ||
|
|
2a69420c42 | ||
|
|
d8a42bbf63 | ||
|
|
8f20ea1093 | ||
|
|
a20e92a8c1 | ||
|
|
c703e20a06 | ||
|
|
a955968de3 | ||
|
|
f597c29432 | ||
|
|
d08e28f4e0 | ||
|
|
ef67321ff2 | ||
|
|
4c777ad140 | ||
|
|
923ae5473a | ||
|
|
a48166e5a0 | ||
|
|
ef098c028b | ||
|
|
7ce1b8dc76 | ||
|
|
e350417a33 | ||
|
|
ea9e0755df | ||
|
|
6aced1b3aa | ||
|
|
99e01fc608 | ||
|
|
fe899c9164 | ||
|
|
f8b9937e51 | ||
|
|
dc5928374e | ||
|
|
0deb3cc606 | ||
|
|
ba4a70d7ae | ||
|
|
65a790e4ca | ||
|
|
b6e677eb06 | ||
|
|
ffa0609f8d | ||
|
|
77a314350f | ||
|
|
737b03c928 | ||
|
|
1b42dd5865 | ||
|
|
c9074b1c25 | ||
|
|
00379280f3 | ||
|
|
4e2d0351cc | ||
|
|
a583efd9b9 | ||
|
|
6237c29a42 | ||
|
|
9ea9b41e73 | ||
|
|
8d99f9b7d2 | ||
|
|
014ffbce2e | ||
|
|
68dd3c90c2 | ||
|
|
4a6f6151f0 | ||
|
|
f108d4c705 | ||
|
|
5ab95f1e1a | ||
|
|
49da08ffa4 | ||
|
|
f287c897a4 | ||
|
|
d15ff2d06f | ||
|
|
648daa3237 | ||
|
|
33e127de09 | ||
|
|
5a9b279039 | ||
|
|
cce58570dc | ||
|
|
361bbec3a0 | ||
|
|
a87409813c | ||
|
|
da84aa1ac2 | ||
|
|
817760688a | ||
|
|
13e56010c1 | ||
|
|
b827a35e44 | ||
|
|
9006e8fdff | ||
|
|
ce8ec033f4 | ||
|
|
9678cc9bc3 | ||
|
|
e87c4ddadc | ||
|
|
3f8581a2fb | ||
|
|
00c5b83384 | ||
|
|
4aedc1cd0b | ||
|
|
d238675c1a | ||
|
|
dcf6f6ca30 | ||
|
|
f4606bd951 | ||
|
|
12bef0830a | ||
|
|
953a2b376c | ||
|
|
ac3b9f7a4c | ||
|
|
ef5990d427 | ||
|
|
23a81d5d70 | ||
|
|
515122c54d | ||
|
|
f4eacca987 | ||
|
|
003fb7c81e | ||
|
|
7d2f63ebbd | ||
|
|
93e0bbb833 | ||
|
|
8c5f6a0be7 | ||
|
|
d6cafb8315 | ||
|
|
a24b76b30b | ||
|
|
11d74ea4ec | ||
|
|
93f4775cf6 | ||
|
|
a93913b9a3 | ||
|
|
08afbc6b58 | ||
|
|
b869465f00 | ||
|
|
2debea8115 | ||
|
|
cae295ff65 | ||
|
|
c51206e980 | ||
|
|
1baa5aea94 | ||
|
|
3e022a5565 | ||
|
|
3dd769be94 | ||
|
|
7936a4bee3 | ||
|
|
09aabe481c | ||
|
|
2ea1e4fa85 | ||
|
|
8699dad0e3 | ||
|
|
47a5f0c620 | ||
|
|
854ff68bac | ||
|
|
01d384e676 | ||
|
|
ddd893a795 | ||
|
|
3d7cd5dac7 | ||
|
|
a6fdfb5191 | ||
|
|
73a68d560f | ||
|
|
b4eeb25f55 | ||
|
|
fc991ab273 | ||
|
|
ab58d14559 | ||
|
|
3a0b311378 | ||
|
|
b81065fe63 | ||
|
|
a67f28dba2 | ||
|
|
99b2472e83 | ||
|
|
be45d5aa73 | ||
|
|
0508df9e7b | ||
|
|
153efab377 | ||
|
|
8015fb70e3 | ||
|
|
79d23aa4fe | ||
|
|
d5dae425fc | ||
|
|
d9e09c4a66 | ||
|
|
331625e876 | ||
|
|
61949fb348 | ||
|
|
c7f4e09496 | ||
|
|
5442e116ce | ||
|
|
11a4fc8b02 | ||
|
|
d303ebd46e | ||
|
|
e1de8dc50e | ||
|
|
5fe110c1dd | ||
|
|
89b203d03a | ||
|
|
0f4f8abbaa | ||
|
|
0d97e9e579 | ||
|
|
14b913fb4b | ||
|
|
9f1cd2bdb5 | ||
|
|
547c40e332 | ||
|
|
9cff6d5aa5 | ||
|
|
7e438bc1f3 | ||
|
|
944a52ce91 | ||
|
|
95a814ed41 | ||
|
|
6b9295b6c4 | ||
|
|
1128fce61a | ||
|
|
7c355fdb0f | ||
|
|
0e2a0b9edc | ||
|
|
c130f9c2f2 | ||
|
|
08300c6e90 | ||
|
|
7b71119094 | ||
|
|
c18db76862 | ||
|
|
c0dd152509 | ||
|
|
f402a4e5ce |
2
.github/actions/run_tests/action.yml
vendored
@@ -10,7 +10,7 @@ runs:
|
||||
cargo install cargo-nextest
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
2
.github/workflows/bump_patch_version.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
ssh-key: ${{ secrets.ZED_BOT_DEPLOY_KEY }}
|
||||
|
||||
14
.github/workflows/check_deps.yml
vendored
@@ -1,14 +0,0 @@
|
||||
name: "Dependency Review"
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@v4
|
||||
47
.github/workflows/ci.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
fetch-depth: 0
|
||||
@@ -40,10 +40,15 @@ jobs:
|
||||
|
||||
- name: Check spelling
|
||||
run: |
|
||||
if ! which typos > /dev/null; then
|
||||
cargo install typos-cli
|
||||
if ! cargo install --list | grep "typos-cli v$TYPOS_CLI_VERSION" > /dev/null; then
|
||||
echo "Installing typos-cli@$TYPOS_CLI_VERSION..."
|
||||
cargo install "typos-cli@$TYPOS_CLI_VERSION"
|
||||
else
|
||||
echo "typos-cli@$TYPOS_CLI_VERSION is already installed."
|
||||
fi
|
||||
typos
|
||||
env:
|
||||
TYPOS_CLI_VERSION: "1.23.3"
|
||||
|
||||
- name: Run style checks
|
||||
uses: ./.github/actions/check_style
|
||||
@@ -85,7 +90,7 @@ jobs:
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
@@ -112,7 +117,7 @@ jobs:
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
@@ -132,12 +137,12 @@ jobs:
|
||||
runs-on: hosted-windows-1
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@v2
|
||||
uses: swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
@@ -165,12 +170,12 @@ jobs:
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
# We need to fetch more than one commit so that `script/draft-release-notes`
|
||||
# is able to diff between the current and previous tag.
|
||||
@@ -225,26 +230,26 @@ jobs:
|
||||
mv target/x86_64-apple-darwin/release/Zed.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
||||
|
||||
- name: Upload app bundle (universal) to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # 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 }}.dmg
|
||||
path: target/release/Zed.dmg
|
||||
- name: Upload app bundle (aarch64) to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # 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@v4
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # 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
|
||||
path: target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
- uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||
name: Upload app bundle to release
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||
with:
|
||||
@@ -275,7 +280,7 @@ jobs:
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
@@ -313,14 +318,14 @@ jobs:
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Linux bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # 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
|
||||
path: target/release/zed-*.tar.gz
|
||||
|
||||
- name: Upload app bundle to release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
@@ -342,11 +347,11 @@ jobs:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
- name: "Setup jq"
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
uses: dcarbone/install-jq-action@8867ddb4788346d7c22b72ea2e2ffe4d514c7bcb # v2
|
||||
|
||||
- name: Set up Clang
|
||||
run: |
|
||||
@@ -354,7 +359,7 @@ jobs:
|
||||
sudo apt-get install -y llvm-10 clang-10 build-essential cmake pkg-config libasound2-dev libfontconfig-dev libwayland-dev libxkbcommon-x11-dev libssl-dev libsqlite3-dev libzstd-dev libvulkan1 libgit2-dev
|
||||
echo "/usr/lib/llvm-10/bin" >> $GITHUB_PATH
|
||||
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: rui314/setup-mold@2e332a0b602c2fc65d2d3995941b1b29a5f554a0 # v1
|
||||
with:
|
||||
mold-version: 2.32.0
|
||||
|
||||
@@ -397,14 +402,14 @@ jobs:
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Linux bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # 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
|
||||
path: target/release/zed-*.tar.gz
|
||||
|
||||
- name: Upload app bundle to release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||
with:
|
||||
draft: true
|
||||
|
||||
4
.github/workflows/danger.yml
vendored
@@ -14,14 +14,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "pnpm"
|
||||
|
||||
12
.github/workflows/deploy_cloudflare.yml
vendored
@@ -12,12 +12,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Setup mdBook
|
||||
uses: peaceiris/actions-mdbook@v2
|
||||
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # v2
|
||||
with:
|
||||
mdbook-version: "0.4.37"
|
||||
|
||||
@@ -28,28 +28,28 @@ jobs:
|
||||
mdbook build ./docs --dest-dir=../target/deploy/docs/
|
||||
|
||||
- name: Deploy Docs
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
uses: cloudflare/wrangler-action@f84a562284fc78278ff9052435d9526f9c718361 # v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: pages deploy target/deploy --project-name=docs
|
||||
|
||||
- name: Deploy Install
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
uses: cloudflare/wrangler-action@f84a562284fc78278ff9052435d9526f9c718361 # v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: r2 object put -f script/install.sh zed-open-source-website-assets/install.sh
|
||||
|
||||
- name: Deploy Docs Workers
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
uses: cloudflare/wrangler-action@f84a562284fc78278ff9052435d9526f9c718361 # v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: deploy .cloudflare/docs-proxy/src/worker.js
|
||||
|
||||
- name: Deploy Install Workers
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
uses: cloudflare/wrangler-action@f84a562284fc78278ff9052435d9526f9c718361 # v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
|
||||
6
.github/workflows/deploy_collab.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
fetch-depth: 0
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
needs: style
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
fetch-depth: 0
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
run: doctl registry login
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
|
||||
4
.github/workflows/publish_extension_cli.yml
vendored
@@ -16,12 +16,12 @@ jobs:
|
||||
- ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@v2
|
||||
uses: swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
|
||||
4
.github/workflows/randomized_tests.yml
vendored
@@ -23,12 +23,12 @@ jobs:
|
||||
- randomized-tests
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
|
||||
4
.github/workflows/release_actions.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
fi
|
||||
echo "::set-output name=URL::$URL"
|
||||
- name: Get content
|
||||
uses: 2428392/gh-truncate-string-action@v1.3.0
|
||||
uses: 2428392/gh-truncate-string-action@67b1b814955634208b103cff064be3cb1c7a19be # v1.3.0
|
||||
id: get-content
|
||||
with:
|
||||
stringToTruncate: |
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
maxLength: 2000
|
||||
truncationSymbol: "..."
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v5.3.0
|
||||
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 # v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: ${{ steps.get-content.outputs.string }}
|
||||
|
||||
16
.github/workflows/release_nightly.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
fetch-depth: 0
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
needs: style
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
@@ -69,12 +69,12 @@ jobs:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
@@ -141,12 +141,12 @@ jobs:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: "Setup jq"
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
uses: dcarbone/install-jq-action@8867ddb4788346d7c22b72ea2e2ffe4d514c7bcb # v2
|
||||
|
||||
- name: Set up Clang
|
||||
run: |
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
sudo apt-get install -y llvm-10 clang-10 build-essential cmake pkg-config libasound2-dev libfontconfig-dev libwayland-dev libxkbcommon-x11-dev libssl-dev libsqlite3-dev libzstd-dev libvulkan1 libgit2-dev
|
||||
echo "/usr/lib/llvm-10/bin" >> $GITHUB_PATH
|
||||
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: rui314/setup-mold@2e332a0b602c2fc65d2d3995941b1b29a5f554a0 # v1
|
||||
with:
|
||||
mold-version: 2.32.0
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
- uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
architecture: "x64"
|
||||
|
||||
@@ -8,8 +8,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
- uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
architecture: "x64"
|
||||
|
||||
1
.gitignore
vendored
@@ -29,3 +29,4 @@ DerivedData/
|
||||
.vscode
|
||||
.wrangler
|
||||
.flatpak-builder
|
||||
.zed/debug.json
|
||||
|
||||
1015
Cargo.lock
generated
86
Cargo.toml
@@ -21,6 +21,8 @@ members = [
|
||||
"crates/command_palette_hooks",
|
||||
"crates/completion",
|
||||
"crates/copilot",
|
||||
"crates/dap",
|
||||
"crates/debugger_ui",
|
||||
"crates/db",
|
||||
"crates/dev_server_projects",
|
||||
"crates/diagnostics",
|
||||
@@ -44,7 +46,7 @@ members = [
|
||||
"crates/gpui_macros",
|
||||
"crates/headless",
|
||||
"crates/html_to_markdown",
|
||||
"crates/http",
|
||||
"crates/http_client",
|
||||
"crates/image_viewer",
|
||||
"crates/indexed_docs",
|
||||
"crates/inline_completion_button",
|
||||
@@ -90,7 +92,9 @@ members = [
|
||||
"crates/search",
|
||||
"crates/semantic_index",
|
||||
"crates/semantic_version",
|
||||
"crates/session",
|
||||
"crates/settings",
|
||||
"crates/settings_ui",
|
||||
"crates/snippet",
|
||||
"crates/snippet_provider",
|
||||
"crates/sqlez",
|
||||
@@ -159,13 +163,13 @@ resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
activity_indicator = { path = "crates/activity_indicator" }
|
||||
aho-corasick = "1.1"
|
||||
ai = { path = "crates/ai" }
|
||||
anthropic = { path = "crates/anthropic" }
|
||||
assets = { path = "crates/assets" }
|
||||
assistant = { path = "crates/assistant" }
|
||||
assistant_slash_command = { path = "crates/assistant_slash_command" }
|
||||
assistant_tooling = { path = "crates/assistant_tooling" }
|
||||
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
|
||||
audio = { path = "crates/audio" }
|
||||
auto_update = { path = "crates/auto_update" }
|
||||
breadcrumbs = { path = "crates/breadcrumbs" }
|
||||
@@ -181,7 +185,9 @@ command_palette = { path = "crates/command_palette" }
|
||||
command_palette_hooks = { path = "crates/command_palette_hooks" }
|
||||
completion = { path = "crates/completion" }
|
||||
copilot = { path = "crates/copilot" }
|
||||
dap = { path = "crates/dap" }
|
||||
db = { path = "crates/db" }
|
||||
debugger_ui = { path = "crates/debugger_ui" }
|
||||
dev_server_projects = { path = "crates/dev_server_projects" }
|
||||
diagnostics = { path = "crates/diagnostics" }
|
||||
editor = { path = "crates/editor" }
|
||||
@@ -202,7 +208,7 @@ gpui = { path = "crates/gpui" }
|
||||
gpui_macros = { path = "crates/gpui_macros" }
|
||||
headless = { path = "crates/headless" }
|
||||
html_to_markdown = { path = "crates/html_to_markdown" }
|
||||
http = { path = "crates/http" }
|
||||
http_client = { path = "crates/http_client" }
|
||||
image_viewer = { path = "crates/image_viewer" }
|
||||
indexed_docs = { path = "crates/indexed_docs" }
|
||||
inline_completion_button = { path = "crates/inline_completion_button" }
|
||||
@@ -248,7 +254,9 @@ rpc = { path = "crates/rpc" }
|
||||
search = { path = "crates/search" }
|
||||
semantic_index = { path = "crates/semantic_index" }
|
||||
semantic_version = { path = "crates/semantic_version" }
|
||||
session = { path = "crates/session" }
|
||||
settings = { path = "crates/settings" }
|
||||
settings_ui = { path = "crates/settings_ui" }
|
||||
snippet = { path = "crates/snippet" }
|
||||
snippet_provider = { path = "crates/snippet_provider" }
|
||||
sqlez = { path = "crates/sqlez" }
|
||||
@@ -282,22 +290,24 @@ zed = { path = "crates/zed" }
|
||||
zed_actions = { path = "crates/zed_actions" }
|
||||
|
||||
alacritty_terminal = "0.23"
|
||||
any_vec = "0.13"
|
||||
anyhow = "1.0.57"
|
||||
any_vec = "0.14"
|
||||
anyhow = "1.0.86"
|
||||
ashpd = "0.9.1"
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
async-dispatcher = { version = "0.1" }
|
||||
async-fs = "1.6"
|
||||
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
|
||||
async-recursion = "1.0.0"
|
||||
async-tar = "0.4.2"
|
||||
async-trait = "0.1"
|
||||
async-tungstenite = { version = "0.16" }
|
||||
async-watch = "0.3.1"
|
||||
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
||||
base64 = "0.13"
|
||||
bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" }
|
||||
blade-macros = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" }
|
||||
blade-util = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" }
|
||||
blade-graphics = { git = "https://github.com/zed-industries/blade", rev = "7e497c534d5d4a30c18d9eb182cf39eaf0aaa25e" }
|
||||
blade-macros = { git = "https://github.com/zed-industries/blade", rev = "7e497c534d5d4a30c18d9eb182cf39eaf0aaa25e" }
|
||||
blade-util = { git = "https://github.com/zed-industries/blade", rev = "7e497c534d5d4a30c18d9eb182cf39eaf0aaa25e" }
|
||||
cap-std = "3.0"
|
||||
cargo_toml = "0.20"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
@@ -311,7 +321,7 @@ dashmap = "5.5.3"
|
||||
derive_more = "0.99.17"
|
||||
dirs = "4.0"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.9"
|
||||
env_logger = "0.10"
|
||||
exec = "0.3.1"
|
||||
fork = "0.1.23"
|
||||
futures = "0.3"
|
||||
@@ -334,7 +344,7 @@ itertools = "0.11.0"
|
||||
lazy_static = "1.4.0"
|
||||
libc = "0.2"
|
||||
linkify = "0.10.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
nanoid = "0.4"
|
||||
nix = "0.28"
|
||||
@@ -355,12 +365,14 @@ rand = "0.8.5"
|
||||
refineable = { path = "./crates/refineable" }
|
||||
regex = "1.5"
|
||||
repair_json = "0.1.0"
|
||||
rsa = "0.9.6"
|
||||
runtimelib = { version = "0.12", default-features = false, features = [
|
||||
"async-dispatcher-runtime",
|
||||
] }
|
||||
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
|
||||
rustc-demangle = "0.1.23"
|
||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||
schemars = "0.8"
|
||||
schemars = {version = "0.8", features = ["impl_json_schema"]}
|
||||
semver = "1.0"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
|
||||
@@ -395,29 +407,28 @@ tiny_http = "0.8"
|
||||
toml = "0.8"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tower-http = "0.4.4"
|
||||
tree-sitter = { version = "0.20", features = ["wasm"] }
|
||||
tree-sitter-bash = "0.20.5"
|
||||
tree-sitter-c = "0.20.1"
|
||||
tree-sitter-cpp = "0.20.5"
|
||||
tree-sitter-css = "0.20"
|
||||
tree-sitter-elixir = "0.1.1"
|
||||
tree-sitter = { version = "0.22", features = ["wasm"] }
|
||||
tree-sitter-bash = "0.21"
|
||||
tree-sitter-c = "0.21"
|
||||
tree-sitter-cpp = "0.22"
|
||||
tree-sitter-css = "0.21"
|
||||
tree-sitter-elixir = "0.2"
|
||||
tree-sitter-embedded-template = "0.20.0"
|
||||
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "b82ab803d887002a0af11f6ce63d72884580bf33" }
|
||||
tree-sitter-gomod = "1.0.1"
|
||||
tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work" }
|
||||
rustc-demangle = "0.1.23"
|
||||
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
|
||||
tree-sitter-html = "0.19.0"
|
||||
tree-sitter-jsdoc = { git = "https://github.com/tree-sitter/tree-sitter-jsdoc", rev = "6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" }
|
||||
tree-sitter-json = "0.20.2"
|
||||
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
|
||||
tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" }
|
||||
tree-sitter-python = "0.20.2"
|
||||
tree-sitter-regex = "0.20.0"
|
||||
tree-sitter-ruby = "0.20.0"
|
||||
tree-sitter-rust = "0.20.3"
|
||||
tree-sitter-typescript = "0.20.5"
|
||||
tree-sitter-yaml = "0.0.1"
|
||||
tree-sitter-go = "0.21"
|
||||
tree-sitter-go-mod = { git = "https://github.com/SomeoneToIgnore/tree-sitter-go-mod", rev = "8c1f54f12bb4c846336b634bc817645d6f35d641", package = "tree-sitter-gomod"}
|
||||
tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work", rev = "dcbabff454703c3a4bc98a23cf8778d4be46fd22" }
|
||||
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "6dd0303acf7138dd2b9b432a229e16539581c701" }
|
||||
tree-sitter-html = "0.20"
|
||||
tree-sitter-jsdoc = "0.21"
|
||||
tree-sitter-json = "0.21"
|
||||
tree-sitter-md = { git = "https://github.com/zed-industries/tree-sitter-markdown", rev = "e3855e37f8f2c71aa7513c18a9c95fb7461b1b10" }
|
||||
protols-tree-sitter-proto = "0.2"
|
||||
tree-sitter-python = "0.21"
|
||||
tree-sitter-regex = "0.21"
|
||||
tree-sitter-ruby = "0.21"
|
||||
tree-sitter-rust = "0.21"
|
||||
tree-sitter-typescript = "0.21"
|
||||
tree-sitter-yaml = "0.6"
|
||||
unindent = "0.1.7"
|
||||
unicase = "2.6"
|
||||
unicode-segmentation = "1.10"
|
||||
@@ -425,14 +436,14 @@ url = "2.2"
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] }
|
||||
wasmparser = "0.201"
|
||||
wasm-encoder = "0.201"
|
||||
wasmtime = { version = "19.0.0", default-features = false, features = [
|
||||
wasmtime = { version = "19.0.2", default-features = false, features = [
|
||||
"async",
|
||||
"demangle",
|
||||
"runtime",
|
||||
"cranelift",
|
||||
"component-model",
|
||||
] }
|
||||
wasmtime-wasi = "19.0.0"
|
||||
wasmtime-wasi = "19.0.2"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.201"
|
||||
sys-locale = "0.3.1"
|
||||
@@ -477,11 +488,6 @@ features = [
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
]
|
||||
|
||||
[patch.crates-io]
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "7b4894ba2ae81b988846676f54c0988d4027ef4f" }
|
||||
# Workaround for a broken nightly build of gpui: See #7644 and revisit once 0.5.3 is released.
|
||||
pathfinder_simd = { git = "https://github.com/servo/pathfinder.git", rev = "4968e819c0d9b015437ffc694511e175801a17c7" }
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
debug = "limited"
|
||||
|
||||
1
assets/icons/debug-continue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M2.5 2H4v12H2.5V2zm4.936.39L6.25 3v10l1.186.61 7-5V7.39l-7-5zM12.71 8l-4.96 3.543V4.457L12.71 8z"/></svg>
|
||||
|
After Width: | Height: | Size: 257 B |
1
assets/icons/debug-pause.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M4.5 3H6v10H4.5V3zm7 0v10H10V3h1.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 156 B |
1
assets/icons/debug-restart.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.75 8a4.5 4.5 0 0 1-8.61 1.834l-1.391.565A6.001 6.001 0 0 0 14.25 8 6 6 0 0 0 3.5 4.334V2.5H2v4l.75.75h3.5v-1.5H4.352A4.5 4.5 0 0 1 12.75 8z"/></svg>
|
||||
|
After Width: | Height: | Size: 304 B |
1
assets/icons/debug-step-into.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M8 9.532h.542l3.905-3.905-1.061-1.06-2.637 2.61V1H7.251v6.177l-2.637-2.61-1.061 1.06 3.905 3.905H8zm1.956 3.481a2 2 0 1 1-4 0 2 2 0 0 1 4 0z"/></svg>
|
||||
|
After Width: | Height: | Size: 301 B |
1
assets/icons/debug-step-out.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M8 1h-.542L3.553 4.905l1.061 1.06 2.637-2.61v6.177h1.498V3.355l2.637 2.61 1.061-1.06L8.542 1H8zm1.956 12.013a2 2 0 1 1-4 0 2 2 0 0 1 4 0z"/></svg>
|
||||
|
After Width: | Height: | Size: 298 B |
1
assets/icons/debug-step-over.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M14.25 5.75v-4h-1.5v2.542c-1.145-1.359-2.911-2.209-4.84-2.209-3.177 0-5.92 2.307-6.16 5.398l-.02.269h1.501l.022-.226c.212-2.195 2.202-3.94 4.656-3.94 1.736 0 3.244.875 4.05 2.166h-2.83v1.5h4.163l.962-.975V5.75h-.004zM8 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></svg>
|
||||
|
After Width: | Height: | Size: 411 B |
1
assets/icons/debug-stop.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M13 1.99976L14 2.99976V12.9998L13 13.9998H3L2 12.9998L2 2.99976L3 1.99976H13ZM12.7461 3.25057L3.25469 3.25057L3.25469 12.7504H12.7461V3.25057Z"/></svg>
|
||||
|
After Width: | Height: | Size: 303 B |
3
assets/icons/sparkle_alt.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 6C5.69062 6.30938 4.56159 6.55977 3.51192 6.73263C3.27345 6.7719 3.27345 7.2281 3.51192 7.26737C4.56159 7.44023 5.69062 7.69062 6 8C6.30938 8.30938 6.55977 9.43841 6.73263 10.4881C6.7719 10.7266 7.2281 10.7266 7.26737 10.4881C7.44023 9.43841 7.69062 8.30938 8 8C8.30938 7.69062 9.43841 7.44023 10.4881 7.26737C10.7266 7.2281 10.7266 6.7719 10.4881 6.73263C9.43841 6.55977 8.30938 6.30938 8 6C7.69062 5.69062 7.44023 4.56159 7.26737 3.51192C7.2281 3.27345 6.7719 3.27345 6.73263 3.51192C6.55977 4.56159 6.30938 5.69062 6 6Z" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 700 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.88889 1H2.11111C1.49746 1 1 1.49746 1 2.11111V9.88889C1 10.5025 1.49746 11 2.11111 11H9.88889C10.5025 11 11 10.5025 11 9.88889V2.11111C11 1.49746 10.5025 1 9.88889 1Z" stroke="#C56757" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 9.8V4.2C4 4.08954 4.08954 4 4.2 4H9.8C9.91046 4 10 4.08954 10 4.2V9.8C10 9.91046 9.91046 10 9.8 10H4.2C4.08954 10 4 9.91046 4 9.8Z" stroke="#C56757" stroke-width="1.25" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 369 B After Width: | Height: | Size: 310 B |
@@ -106,6 +106,7 @@
|
||||
"bindings": {
|
||||
"enter": "editor::Newline",
|
||||
"shift-enter": "editor::Newline",
|
||||
"ctrl-enter": "editor::NewlineAbove",
|
||||
"ctrl-shift-enter": "editor::NewlineBelow",
|
||||
"alt-z": "editor::ToggleSoftWrap",
|
||||
"ctrl-f": "buffer_search::Deploy",
|
||||
@@ -116,12 +117,6 @@
|
||||
"ctrl-alt-e": "editor::SelectEnclosingSymbol"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full && !jupyter",
|
||||
"bindings": {
|
||||
"ctrl-enter": "editor::NewlineAbove"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full && inline_completion",
|
||||
"bindings": {
|
||||
@@ -485,7 +480,7 @@
|
||||
{
|
||||
"context": "Editor && jupyter && !ContextEditor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "repl::Run"
|
||||
"ctrl-shift-enter": "repl::Run"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -135,6 +135,7 @@
|
||||
"bindings": {
|
||||
"enter": "editor::Newline",
|
||||
"shift-enter": "editor::Newline",
|
||||
"cmd-enter": "editor::NewlineBelow",
|
||||
"cmd-shift-enter": "editor::NewlineAbove",
|
||||
"alt-z": "editor::ToggleSoftWrap",
|
||||
"cmd-f": "buffer_search::Deploy",
|
||||
@@ -146,12 +147,6 @@
|
||||
"cmd-alt-e": "editor::SelectEnclosingSymbol"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full && !jupyter",
|
||||
"bindings": {
|
||||
"cmd-enter": "editor::NewlineBelow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full && inline_completion",
|
||||
"bindings": {
|
||||
@@ -183,7 +178,7 @@
|
||||
{
|
||||
"context": "Editor && jupyter && !ContextEditor",
|
||||
"bindings": {
|
||||
"cmd-enter": "repl::Run"
|
||||
"ctrl-shift-enter": "repl::Run"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -298,7 +293,6 @@
|
||||
"alt-cmd-c": "search::ToggleCaseSensitive",
|
||||
"alt-cmd-w": "search::ToggleWholeWord",
|
||||
"alt-cmd-f": "project_search::ToggleFilters",
|
||||
"alt-cmd-g": "search::ToggleRegex",
|
||||
"alt-cmd-x": "search::ToggleRegex"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-up": "editor::MoveLineUp",
|
||||
"ctrl-shift-down": "editor::MoveLineDown",
|
||||
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-l": "editor::SplitSelectionIntoLines",
|
||||
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"bindings": {
|
||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
"cmd-ctrl-up": "editor::MoveLineUp",
|
||||
"cmd-ctrl-down": "editor::MoveLineDown",
|
||||
"cmd-shift-space": "editor::SelectAll",
|
||||
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
|
||||
"cmd-shift-l": "editor::SplitSelectionIntoLines",
|
||||
|
||||
@@ -253,7 +253,7 @@
|
||||
"[ d": "editor::GoToPrevDiagnostic",
|
||||
"] c": "editor::GoToHunk",
|
||||
"[ c": "editor::GoToPrevHunk",
|
||||
"g c c": "vim::ToggleComments"
|
||||
"g c": ["vim::PushOperator", "ToggleComments"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -434,6 +434,12 @@
|
||||
"<": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gc",
|
||||
"bindings": {
|
||||
"c": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar && !in_replace",
|
||||
"bindings": {
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
// Whether to confirm before quitting Zed.
|
||||
"confirm_quit": false,
|
||||
// Whether to restore last closed project when fresh Zed instance is opened.
|
||||
"restore_on_startup": "last_workspace",
|
||||
"restore_on_startup": "last_session",
|
||||
// Size of the drop target in the editor.
|
||||
"drop_target_size": 0.2,
|
||||
// Whether the window should be closed when using 'close active item' on a window with no tabs.
|
||||
@@ -375,7 +375,7 @@
|
||||
},
|
||||
"assistant": {
|
||||
// Version of this setting.
|
||||
"version": "1",
|
||||
"version": "2",
|
||||
// Whether the assistant is enabled.
|
||||
"enabled": true,
|
||||
// Whether to show the assistant panel button in the status bar.
|
||||
@@ -386,18 +386,12 @@
|
||||
"default_width": 640,
|
||||
// Default height when the assistant is docked to the bottom.
|
||||
"default_height": 320,
|
||||
// AI provider.
|
||||
"provider": {
|
||||
"name": "openai",
|
||||
// The default model to use when creating new contexts. This
|
||||
// setting can take three values:
|
||||
//
|
||||
// 1. "gpt-3.5-turbo"
|
||||
// 2. "gpt-4"
|
||||
// 3. "gpt-4-turbo-preview"
|
||||
// 4. "gpt-4o"
|
||||
// 5. "gpt-4o-mini"
|
||||
"default_model": "gpt-4o"
|
||||
// The default model to use when creating new contexts.
|
||||
"default_model": {
|
||||
// The provider to use.
|
||||
"provider": "openai",
|
||||
// The model to use.
|
||||
"model": "gpt-4o"
|
||||
}
|
||||
},
|
||||
// Whether the screen sharing icon is shown in the os status bar.
|
||||
@@ -858,6 +852,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
// Different settings for specific language models.
|
||||
"language_models": {
|
||||
"anthropic": {
|
||||
"api_url": "https://api.anthropic.com"
|
||||
},
|
||||
"openai": {
|
||||
"api_url": "https://api.openai.com/v1"
|
||||
},
|
||||
"ollama": {
|
||||
"api_url": "http://localhost:11434"
|
||||
}
|
||||
},
|
||||
// Zed's Prettier integration settings.
|
||||
// Allows to enable/disable formatting with Prettier
|
||||
// and configure default Prettier, used when no project-level Prettier installation is found.
|
||||
@@ -890,6 +896,15 @@
|
||||
// }
|
||||
// }
|
||||
},
|
||||
// Jupyter settings
|
||||
"jupyter": {
|
||||
"enabled": true
|
||||
// Specify the language name as the key and the kernel name as the value.
|
||||
// "kernel_selections": {
|
||||
// "python": "conda-base"
|
||||
// "typescript": "deno"
|
||||
// }
|
||||
},
|
||||
// Vim settings
|
||||
"vim": {
|
||||
"use_system_clipboard": "always",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Folder-specific settings
|
||||
//
|
||||
// For a full list of overridable settings, and general information on folder-specific settings,
|
||||
// see the documentation: https://zed.dev/docs/configuring-zed#folder-specific-settings
|
||||
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
|
||||
{}
|
||||
|
||||
@@ -17,6 +17,27 @@
|
||||
// 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)
|
||||
// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
|
||||
"reveal": "always"
|
||||
"reveal": "always",
|
||||
// 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
|
||||
// * `on_success` — hide the terminal tab on task success only, otherwise behaves similar to `always`
|
||||
"hide": "never",
|
||||
// Which shell to use when running a task inside the terminal.
|
||||
// May take 3 values:
|
||||
// 1. (default) Use the system's default terminal configuration in /etc/passwd
|
||||
// "shell": "system"
|
||||
// 2. A program:
|
||||
// "shell": {
|
||||
// "program": "sh"
|
||||
// }
|
||||
// 3. A program with arguments:
|
||||
// "shell": {
|
||||
// "with_arguments": {
|
||||
// "program": "/bin/bash",
|
||||
// "arguments": ["--login"]
|
||||
// }
|
||||
// }
|
||||
"shell": "system"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -18,7 +18,7 @@ path = "src/anthropic.rs"
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
http.workspace = true
|
||||
http_client.workspace = true
|
||||
isahc.workspace = true
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
|
||||
use http::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use isahc::config::Configurable;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{convert::TryFrom, time::Duration};
|
||||
@@ -21,11 +21,7 @@ pub enum Model {
|
||||
#[serde(alias = "claude-3-haiku", rename = "claude-3-haiku-20240307")]
|
||||
Claude3Haiku,
|
||||
#[serde(rename = "custom")]
|
||||
Custom {
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
max_tokens: Option<usize>,
|
||||
},
|
||||
Custom { name: String, max_tokens: usize },
|
||||
}
|
||||
|
||||
impl Model {
|
||||
@@ -39,10 +35,7 @@ impl Model {
|
||||
} else if id.starts_with("claude-3-haiku") {
|
||||
Ok(Self::Claude3Haiku)
|
||||
} else {
|
||||
Ok(Self::Custom {
|
||||
name: id.to_string(),
|
||||
max_tokens: None,
|
||||
})
|
||||
Err(anyhow!("invalid model id"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +45,7 @@ impl Model {
|
||||
Model::Claude3Opus => "claude-3-opus-20240229",
|
||||
Model::Claude3Sonnet => "claude-3-sonnet-20240229",
|
||||
Model::Claude3Haiku => "claude-3-opus-20240307",
|
||||
Model::Custom { name, .. } => name,
|
||||
Self::Custom { name, .. } => name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +65,7 @@ impl Model {
|
||||
| Self::Claude3Opus
|
||||
| Self::Claude3Sonnet
|
||||
| Self::Claude3Haiku => 200_000,
|
||||
Self::Custom { max_tokens, .. } => max_tokens.unwrap_or(200_000),
|
||||
Self::Custom { max_tokens, .. } => *max_tokens,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ anyhow.workspace = true
|
||||
assets.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
async-watch.workspace = true
|
||||
breadcrumbs.workspace = true
|
||||
cargo_toml.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
@@ -42,7 +41,7 @@ fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
heed.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
http.workspace = true
|
||||
http_client.workspace = true
|
||||
indexed_docs.workspace = true
|
||||
indoc.workspace = true
|
||||
language.workspace = true
|
||||
|
||||
@@ -15,20 +15,20 @@ use assistant_settings::AssistantSettings;
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::{proto, Client};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use completion::CompletionProvider;
|
||||
use completion::LanguageModelCompletionProvider;
|
||||
pub use context::*;
|
||||
pub use context_store::*;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
actions, impl_actions, AppContext, BorrowAppContext, Global, SharedString, UpdateGlobal,
|
||||
};
|
||||
use gpui::{actions, impl_actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
use indexed_docs::IndexedDocsRegistry;
|
||||
pub(crate) use inline_assistant::*;
|
||||
use language_model::LanguageModelResponseMessage;
|
||||
use language_model::{
|
||||
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
|
||||
};
|
||||
pub(crate) use model_selector::*;
|
||||
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use slash_command::{
|
||||
active_command, default_command, diagnostics_command, docs_command, fetch_command,
|
||||
file_command, now_command, project_command, prompt_command, search_command, symbols_command,
|
||||
@@ -165,6 +165,16 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) {
|
||||
cx.set_global(Assistant::default());
|
||||
AssistantSettings::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 {
|
||||
@@ -182,7 +192,7 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) {
|
||||
|
||||
context_store::init(&client);
|
||||
prompt_library::init(cx);
|
||||
init_completion_provider(Arc::clone(&client), cx);
|
||||
init_completion_provider(cx);
|
||||
assistant_slash_command::init(cx);
|
||||
register_slash_commands(cx);
|
||||
assistant_panel::init(cx);
|
||||
@@ -207,20 +217,38 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn init_completion_provider(client: Arc<Client>, cx: &mut AppContext) {
|
||||
let provider = assistant_settings::create_provider_from_settings(client.clone(), 0, cx);
|
||||
cx.set_global(CompletionProvider::new(provider, Some(client)));
|
||||
fn init_completion_provider(cx: &mut AppContext) {
|
||||
completion::init(cx);
|
||||
update_active_language_model_from_settings(cx);
|
||||
|
||||
let mut settings_version = 0;
|
||||
cx.observe_global::<SettingsStore>(move |cx| {
|
||||
settings_version += 1;
|
||||
cx.update_global::<CompletionProvider, _>(|provider, cx| {
|
||||
assistant_settings::update_completion_provider_settings(provider, settings_version, cx);
|
||||
})
|
||||
cx.observe_global::<SettingsStore>(update_active_language_model_from_settings)
|
||||
.detach();
|
||||
cx.observe(&LanguageModelRegistry::global(cx), |_, cx| {
|
||||
update_active_language_model_from_settings(cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn update_active_language_model_from_settings(cx: &mut AppContext) {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let provider_name = LanguageModelProviderId::from(settings.default_model.provider.clone());
|
||||
let model_id = LanguageModelId::from(settings.default_model.model.clone());
|
||||
|
||||
let Some(provider) = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.provider(&provider_name)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let models = provider.provided_models(cx);
|
||||
if let Some(model) = models.iter().find(|model| model.id() == model_id).cloned() {
|
||||
LanguageModelCompletionProvider::global(cx).update(cx, |completion_provider, cx| {
|
||||
completion_provider.set_active_model(model, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn register_slash_commands(cx: &mut AppContext) {
|
||||
let slash_command_registry = SlashCommandRegistry::global(cx);
|
||||
slash_command_registry.register_command(file_command::FileSlashCommand, true);
|
||||
|
||||
@@ -16,10 +16,9 @@ use crate::{
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
|
||||
use breadcrumbs::Breadcrumbs;
|
||||
use client::proto;
|
||||
use collections::{BTreeSet, HashMap, HashSet};
|
||||
use completion::CompletionProvider;
|
||||
use completion::LanguageModelCompletionProvider;
|
||||
use editor::{
|
||||
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
|
||||
display_map::{
|
||||
@@ -50,6 +49,7 @@ use project::{Project, ProjectLspAdapterDelegate};
|
||||
use search::{buffer_search::DivRegistrar, BufferSearchBar};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::{self, Ordering},
|
||||
fmt::Write,
|
||||
ops::Range,
|
||||
@@ -58,7 +58,6 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*,
|
||||
utils::{format_distance_from_now, DateTimeType},
|
||||
@@ -68,7 +67,7 @@ use ui::{
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
item::{self, BreadcrumbText, FollowableItem, Item, ItemHandle},
|
||||
item::{self, FollowableItem, Item, ItemHandle},
|
||||
notifications::NotifyTaskExt,
|
||||
pane::{self, SaveIntent},
|
||||
searchable::{SearchEvent, SearchableItem},
|
||||
@@ -113,6 +112,7 @@ pub struct AssistantPanel {
|
||||
subscriptions: Vec<Subscription>,
|
||||
authentication_prompt: Option<AnyView>,
|
||||
model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
model_summary_editor: View<Editor>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -300,13 +300,21 @@ impl AssistantPanel {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||
let model_summary_editor = cx.new_view(|cx| Editor::single_line(cx));
|
||||
let context_editor_toolbar = cx.new_view(|_| {
|
||||
ContextEditorToolbarItem::new(
|
||||
workspace,
|
||||
model_selector_menu_handle.clone(),
|
||||
model_summary_editor.clone(),
|
||||
)
|
||||
});
|
||||
let pane = cx.new_view(|cx| {
|
||||
let mut pane = Pane::new(
|
||||
workspace.weak_handle(),
|
||||
workspace.project().clone(),
|
||||
Default::default(),
|
||||
None,
|
||||
NewFile.boxed_clone(),
|
||||
Some(NewFile.boxed_clone()),
|
||||
cx,
|
||||
);
|
||||
pane.set_can_split(false, cx);
|
||||
@@ -345,13 +353,7 @@ impl AssistantPanel {
|
||||
.into_any_element()
|
||||
});
|
||||
pane.toolbar().update(cx, |toolbar, cx| {
|
||||
toolbar.add_item(cx.new_view(|_| Breadcrumbs::new()), cx);
|
||||
toolbar.add_item(
|
||||
cx.new_view(|_| {
|
||||
ContextEditorToolbarItem::new(workspace, model_selector_menu_handle.clone())
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
toolbar.add_item(context_editor_toolbar.clone(), cx);
|
||||
toolbar.add_item(cx.new_view(BufferSearchBar::new), cx)
|
||||
});
|
||||
pane
|
||||
@@ -360,13 +362,14 @@ impl AssistantPanel {
|
||||
let subscriptions = vec![
|
||||
cx.observe(&pane, |_, _, cx| cx.notify()),
|
||||
cx.subscribe(&pane, Self::handle_pane_event),
|
||||
cx.observe_global::<CompletionProvider>({
|
||||
let mut prev_settings_version = CompletionProvider::global(cx).settings_version();
|
||||
move |this, cx| {
|
||||
this.completion_provider_changed(prev_settings_version, cx);
|
||||
prev_settings_version = CompletionProvider::global(cx).settings_version();
|
||||
}
|
||||
}),
|
||||
cx.subscribe(&context_editor_toolbar, Self::handle_toolbar_event),
|
||||
cx.subscribe(&model_summary_editor, Self::handle_summary_editor_event),
|
||||
cx.observe(
|
||||
&LanguageModelCompletionProvider::global(cx),
|
||||
|this, _, cx| {
|
||||
this.completion_provider_changed(cx);
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
Self {
|
||||
@@ -381,6 +384,7 @@ impl AssistantPanel {
|
||||
subscriptions,
|
||||
authentication_prompt: None,
|
||||
model_selector_menu_handle,
|
||||
model_summary_editor,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,10 +394,19 @@ impl AssistantPanel {
|
||||
event: &pane::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
pane::Event::Remove => cx.emit(PanelEvent::Close),
|
||||
pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
|
||||
pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
|
||||
let update_model_summary = match event {
|
||||
pane::Event::Remove => {
|
||||
cx.emit(PanelEvent::Close);
|
||||
false
|
||||
}
|
||||
pane::Event::ZoomIn => {
|
||||
cx.emit(PanelEvent::ZoomIn);
|
||||
false
|
||||
}
|
||||
pane::Event::ZoomOut => {
|
||||
cx.emit(PanelEvent::ZoomOut);
|
||||
false
|
||||
}
|
||||
|
||||
pane::Event::AddItem { item } => {
|
||||
self.workspace
|
||||
@@ -401,6 +414,7 @@ impl AssistantPanel {
|
||||
item.added_to_pane(workspace, self.pane.clone(), cx)
|
||||
})
|
||||
.ok();
|
||||
true
|
||||
}
|
||||
|
||||
pane::Event::ActivateItem { local } => {
|
||||
@@ -412,47 +426,94 @@ impl AssistantPanel {
|
||||
.ok();
|
||||
}
|
||||
cx.emit(AssistantPanelEvent::ContextEdited);
|
||||
true
|
||||
}
|
||||
|
||||
pane::Event::RemoveItem { .. } => {
|
||||
cx.emit(AssistantPanelEvent::ContextEdited);
|
||||
true
|
||||
}
|
||||
|
||||
_ => {}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if update_model_summary {
|
||||
if let Some(editor) = self.active_context_editor(cx) {
|
||||
self.show_updated_summary(&editor, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_provider_changed(
|
||||
fn handle_summary_editor_event(
|
||||
&mut self,
|
||||
prev_settings_version: usize,
|
||||
model_summary_editor: View<Editor>,
|
||||
event: &EditorEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if self.is_authenticated(cx) {
|
||||
self.authentication_prompt = None;
|
||||
|
||||
if let Some(editor) = self.active_context_editor(cx) {
|
||||
editor.update(cx, |active_context, cx| {
|
||||
active_context
|
||||
.context
|
||||
.update(cx, |context, cx| context.completion_provider_changed(cx))
|
||||
})
|
||||
if matches!(event, EditorEvent::Edited { .. }) {
|
||||
if let Some(context_editor) = self.active_context_editor(cx) {
|
||||
let new_summary = model_summary_editor.read(cx).text(cx);
|
||||
context_editor.update(cx, |context_editor, cx| {
|
||||
context_editor.context.update(cx, |context, cx| {
|
||||
if context.summary().is_none()
|
||||
&& (new_summary == DEFAULT_TAB_TITLE || new_summary.trim().is_empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
context.custom_summary(new_summary, cx)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if self.active_context_editor(cx).is_none() {
|
||||
self.new_context(cx);
|
||||
}
|
||||
cx.notify();
|
||||
} else if self.authentication_prompt.is_none()
|
||||
|| prev_settings_version != CompletionProvider::global(cx).settings_version()
|
||||
{
|
||||
self.authentication_prompt =
|
||||
Some(cx.update_global::<CompletionProvider, _>(|provider, cx| {
|
||||
provider.authentication_prompt(cx)
|
||||
}));
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_toolbar_event(
|
||||
&mut self,
|
||||
_: View<ContextEditorToolbarItem>,
|
||||
_: &ContextEditorToolbarItemEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(context_editor) = self.active_context_editor(cx) {
|
||||
context_editor.update(cx, |context_editor, cx| {
|
||||
context_editor.context.update(cx, |context, cx| {
|
||||
context.summarize(true, cx);
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_provider_changed(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(editor) = self.active_context_editor(cx) {
|
||||
editor.update(cx, |active_context, cx| {
|
||||
active_context
|
||||
.context
|
||||
.update(cx, |context, cx| context.completion_provider_changed(cx))
|
||||
})
|
||||
}
|
||||
|
||||
if self.active_context_editor(cx).is_none() {
|
||||
self.new_context(cx);
|
||||
}
|
||||
|
||||
let authentication_prompt = Self::authentication_prompt(cx);
|
||||
for context_editor in self.context_editors(cx) {
|
||||
context_editor.update(cx, |editor, cx| {
|
||||
editor.set_authentication_prompt(authentication_prompt.clone(), cx);
|
||||
});
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn authentication_prompt(cx: &mut WindowContext) -> Option<AnyView> {
|
||||
if let Some(provider) = LanguageModelCompletionProvider::read_global(cx).active_provider() {
|
||||
if !provider.is_authenticated(cx) {
|
||||
return Some(provider.authentication_prompt(cx));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn inline_assist(
|
||||
workspace: &mut Workspace,
|
||||
action: &InlineAssist,
|
||||
@@ -637,18 +698,43 @@ impl AssistantPanel {
|
||||
.push(cx.subscribe(&context_editor, Self::handle_context_editor_event));
|
||||
}
|
||||
|
||||
self.show_updated_summary(&context_editor, cx);
|
||||
|
||||
cx.emit(AssistantPanelEvent::ContextEdited);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn show_updated_summary(
|
||||
&self,
|
||||
context_editor: &View<ContextEditor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
context_editor.update(cx, |context_editor, cx| {
|
||||
let new_summary = context_editor
|
||||
.context
|
||||
.read(cx)
|
||||
.summary()
|
||||
.map(|s| s.text.clone())
|
||||
.unwrap_or_else(|| context_editor.title(cx).to_string());
|
||||
self.model_summary_editor.update(cx, |summary_editor, cx| {
|
||||
if summary_editor.text(cx) != new_summary {
|
||||
summary_editor.set_text(new_summary, cx);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_context_editor_event(
|
||||
&mut self,
|
||||
_: View<ContextEditor>,
|
||||
context_editor: View<ContextEditor>,
|
||||
event: &EditorEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
EditorEvent::TitleChanged { .. } => cx.notify(),
|
||||
EditorEvent::TitleChanged => {
|
||||
self.show_updated_summary(&context_editor, cx);
|
||||
cx.notify()
|
||||
}
|
||||
EditorEvent::Edited { .. } => cx.emit(AssistantPanelEvent::ContextEdited),
|
||||
_ => {}
|
||||
}
|
||||
@@ -686,7 +772,7 @@ impl AssistantPanel {
|
||||
}
|
||||
|
||||
fn reset_credentials(&mut self, _: &ResetKey, cx: &mut ViewContext<Self>) {
|
||||
CompletionProvider::global(cx)
|
||||
LanguageModelCompletionProvider::read_global(cx)
|
||||
.reset_credentials(cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
@@ -695,6 +781,13 @@ impl AssistantPanel {
|
||||
self.model_selector_menu_handle.toggle(cx);
|
||||
}
|
||||
|
||||
fn context_editors(&self, cx: &AppContext) -> Vec<View<ContextEditor>> {
|
||||
self.pane
|
||||
.read(cx)
|
||||
.items_of_type::<ContextEditor>()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn active_context_editor(&self, cx: &AppContext) -> Option<View<ContextEditor>> {
|
||||
self.pane
|
||||
.read(cx)
|
||||
@@ -816,11 +909,11 @@ impl AssistantPanel {
|
||||
}
|
||||
|
||||
fn is_authenticated(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
CompletionProvider::global(cx).is_authenticated()
|
||||
LanguageModelCompletionProvider::read_global(cx).is_authenticated(cx)
|
||||
}
|
||||
|
||||
fn authenticate(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||
cx.update_global::<CompletionProvider, _>(|provider, cx| provider.authenticate(cx))
|
||||
LanguageModelCompletionProvider::read_global(cx).authenticate(cx)
|
||||
}
|
||||
|
||||
fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
@@ -880,14 +973,18 @@ impl Panel for AssistantPanel {
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
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 size(&self, cx: &WindowContext) -> Pixels {
|
||||
@@ -986,6 +1083,7 @@ struct ActiveEditStep {
|
||||
|
||||
pub struct ContextEditor {
|
||||
context: Model<Context>,
|
||||
authentication_prompt: Option<AnyView>,
|
||||
fs: Arc<dyn Fs>,
|
||||
workspace: WeakView<Workspace>,
|
||||
project: Model<Project>,
|
||||
@@ -1001,9 +1099,10 @@ pub struct ContextEditor {
|
||||
assistant_panel: WeakView<AssistantPanel>,
|
||||
}
|
||||
|
||||
impl ContextEditor {
|
||||
const MAX_TAB_TITLE_LEN: usize = 16;
|
||||
const DEFAULT_TAB_TITLE: &str = "New Context";
|
||||
const MAX_TAB_TITLE_LEN: usize = 16;
|
||||
|
||||
impl ContextEditor {
|
||||
fn for_context(
|
||||
context: Model<Context>,
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -1042,6 +1141,7 @@ impl ContextEditor {
|
||||
let sections = context.read(cx).slash_command_output_sections().to_vec();
|
||||
let mut this = Self {
|
||||
context,
|
||||
authentication_prompt: None,
|
||||
editor,
|
||||
lsp_adapter_delegate,
|
||||
blocks: Default::default(),
|
||||
@@ -1061,6 +1161,15 @@ impl ContextEditor {
|
||||
this
|
||||
}
|
||||
|
||||
fn set_authentication_prompt(
|
||||
&mut self,
|
||||
authentication_prompt: Option<AnyView>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.authentication_prompt = authentication_prompt;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let command_name = DefaultSlashCommand.name();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
@@ -1087,6 +1196,10 @@ impl ContextEditor {
|
||||
}
|
||||
|
||||
fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
|
||||
if self.authentication_prompt.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if !self.apply_edit_step(cx) {
|
||||
self.send_to_model(cx);
|
||||
}
|
||||
@@ -1316,7 +1429,7 @@ impl ContextEditor {
|
||||
ContextEvent::SummaryChanged => {
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
self.context.update(cx, |context, cx| {
|
||||
context.save(None, self.fs.clone(), cx);
|
||||
context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
|
||||
});
|
||||
}
|
||||
ContextEvent::StreamedCompletion => {
|
||||
@@ -2031,16 +2144,18 @@ impl ContextEditor {
|
||||
}
|
||||
|
||||
fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
|
||||
self.context
|
||||
.update(cx, |context, cx| context.save(None, self.fs.clone(), cx));
|
||||
self.context.update(cx, |context, cx| {
|
||||
context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn title(&self, cx: &AppContext) -> String {
|
||||
fn title(&self, cx: &AppContext) -> Cow<str> {
|
||||
self.context
|
||||
.read(cx)
|
||||
.summary()
|
||||
.map(|summary| summary.text.clone())
|
||||
.unwrap_or_else(|| "New Context".into())
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
|
||||
}
|
||||
|
||||
fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
@@ -2112,19 +2227,26 @@ impl Render for ContextEditor {
|
||||
.size_full()
|
||||
.v_flex()
|
||||
.child(
|
||||
div()
|
||||
.flex_grow()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(self.editor.clone())
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.absolute()
|
||||
.bottom_0()
|
||||
.p_4()
|
||||
.justify_end()
|
||||
.child(self.render_send_button(cx)),
|
||||
),
|
||||
if let Some(authentication_prompt) = self.authentication_prompt.as_ref() {
|
||||
div()
|
||||
.flex_grow()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(authentication_prompt.clone().into_any())
|
||||
} else {
|
||||
div()
|
||||
.flex_grow()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(self.editor.clone())
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.absolute()
|
||||
.bottom_0()
|
||||
.p_4()
|
||||
.justify_end()
|
||||
.child(self.render_send_button(cx)),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2139,14 +2261,13 @@ impl Item for ContextEditor {
|
||||
type Event = editor::EditorEvent;
|
||||
|
||||
fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
|
||||
Some(util::truncate_and_trailoff(&self.title(cx), Self::MAX_TAB_TITLE_LEN).into())
|
||||
Some(util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into())
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
|
||||
match event {
|
||||
EditorEvent::Edited { .. } => {
|
||||
f(item::ItemEvent::Edit);
|
||||
f(item::ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
EditorEvent::TitleChanged => {
|
||||
f(item::ItemEvent::UpdateTab);
|
||||
@@ -2156,48 +2277,13 @@ impl Item for ContextEditor {
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
|
||||
Some(self.title(cx).into())
|
||||
Some(self.title(cx).to_string().into())
|
||||
}
|
||||
|
||||
fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(handle.clone()))
|
||||
}
|
||||
|
||||
fn breadcrumbs(
|
||||
&self,
|
||||
theme: &theme::Theme,
|
||||
cx: &AppContext,
|
||||
) -> Option<Vec<item::BreadcrumbText>> {
|
||||
let editor = self.editor.read(cx);
|
||||
let cursor = editor.selections.newest_anchor().head();
|
||||
let multibuffer = &editor.buffer().read(cx);
|
||||
let (_, symbols) = multibuffer.symbols_containing(cursor, Some(&theme.syntax()), cx)?;
|
||||
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
|
||||
let mut breadcrumbs = Vec::new();
|
||||
|
||||
let title = self.title(cx);
|
||||
if title.chars().count() > Self::MAX_TAB_TITLE_LEN {
|
||||
breadcrumbs.push(BreadcrumbText {
|
||||
text: title,
|
||||
highlights: None,
|
||||
font: Some(settings.buffer_font.clone()),
|
||||
});
|
||||
}
|
||||
|
||||
breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText {
|
||||
text: symbol.text,
|
||||
highlights: Some(symbol.highlight_ranges),
|
||||
font: Some(settings.buffer_font.clone()),
|
||||
}));
|
||||
Some(breadcrumbs)
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
Item::set_nav_history(editor, nav_history, cx)
|
||||
@@ -2405,18 +2491,21 @@ pub struct ContextEditorToolbarItem {
|
||||
workspace: WeakView<Workspace>,
|
||||
active_context_editor: Option<WeakView<ContextEditor>>,
|
||||
model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
model_summary_editor: View<Editor>,
|
||||
}
|
||||
|
||||
impl ContextEditorToolbarItem {
|
||||
pub fn new(
|
||||
workspace: &Workspace,
|
||||
model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
model_summary_editor: View<Editor>,
|
||||
) -> Self {
|
||||
Self {
|
||||
fs: workspace.app_state().fs.clone(),
|
||||
workspace: workspace.weak_handle(),
|
||||
active_context_editor: None,
|
||||
model_selector_menu_handle,
|
||||
model_summary_editor,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2485,7 +2574,7 @@ impl ContextEditorToolbarItem {
|
||||
}
|
||||
|
||||
fn render_remaining_tokens(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
|
||||
let model = CompletionProvider::global(cx).model();
|
||||
let model = LanguageModelCompletionProvider::read_global(cx).active_model()?;
|
||||
let context = &self
|
||||
.active_context_editor
|
||||
.as_ref()?
|
||||
@@ -2524,14 +2613,68 @@ impl ContextEditorToolbarItem {
|
||||
|
||||
impl Render for ContextEditorToolbarItem {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
let left_side = h_flex()
|
||||
.gap_2()
|
||||
.child(ModelSelector::new(
|
||||
self.model_selector_menu_handle.clone(),
|
||||
self.fs.clone(),
|
||||
))
|
||||
.flex_1()
|
||||
.min_w(rems(DEFAULT_TAB_TITLE.len() as f32))
|
||||
.when(self.active_context_editor.is_some(), |left_side| {
|
||||
left_side
|
||||
.child(
|
||||
IconButton::new("regenerate-context", IconName::ArrowCircle)
|
||||
.tooltip(|cx| Tooltip::text("Regenerate Summary", cx))
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
|
||||
})),
|
||||
)
|
||||
.child(self.model_summary_editor.clone())
|
||||
});
|
||||
let right_side = h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
ModelSelector::new(
|
||||
self.fs.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(
|
||||
Label::new(
|
||||
LanguageModelCompletionProvider::read_global(cx)
|
||||
.active_model()
|
||||
.map(|model| model.name().0)
|
||||
.unwrap_or_else(|| "No model selected".into()),
|
||||
)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
|
||||
}),
|
||||
)
|
||||
.with_handle(self.model_selector_menu_handle.clone()),
|
||||
)
|
||||
.children(self.render_remaining_tokens(cx))
|
||||
.child(self.render_inject_context_menu(cx))
|
||||
.child(self.render_inject_context_menu(cx));
|
||||
|
||||
h_flex()
|
||||
.size_full()
|
||||
.justify_between()
|
||||
.child(left_side)
|
||||
.child(right_side)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2559,6 +2702,11 @@ impl ToolbarItemView for ContextEditorToolbarItem {
|
||||
|
||||
impl EventEmitter<ToolbarItemEvent> for ContextEditorToolbarItem {}
|
||||
|
||||
enum ContextEditorToolbarItemEvent {
|
||||
RegenerateSummary,
|
||||
}
|
||||
impl EventEmitter<ContextEditorToolbarItemEvent> for ContextEditorToolbarItem {}
|
||||
|
||||
pub struct ContextHistory {
|
||||
picker: View<Picker<SavedContextPickerDelegate>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
@@ -2752,7 +2900,7 @@ fn make_lsp_adapter_delegate(
|
||||
project.update(cx, |project, cx| {
|
||||
// TODO: Find the right worktree.
|
||||
let worktree = project
|
||||
.worktrees()
|
||||
.worktrees(cx)
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("no worktrees when constructing ProjectLspAdapterDelegate"))?;
|
||||
Ok(ProjectLspAdapterDelegate::new(project, &worktree, cx) as Arc<dyn LspAdapterDelegate>)
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anthropic::Model as AnthropicModel;
|
||||
use client::Client;
|
||||
use completion::{
|
||||
AnthropicCompletionProvider, CloudCompletionProvider, CompletionProvider,
|
||||
LanguageModelCompletionProvider, OllamaCompletionProvider, OpenAiCompletionProvider,
|
||||
};
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, Pixels};
|
||||
use language_model::{CloudModel, LanguageModel};
|
||||
use language_model::{settings::AllLanguageModelSettings, CloudModel, LanguageModel};
|
||||
use ollama::Model as OllamaModel;
|
||||
use open_ai::Model as OpenAiModel;
|
||||
use parking_lot::RwLock;
|
||||
use schemars::{schema::Schema, JsonSchema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use settings::{update_settings_file, Settings, SettingsSources};
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -24,43 +19,9 @@ pub enum AssistantDockPosition {
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum AssistantProvider {
|
||||
ZedDotDev {
|
||||
model: CloudModel,
|
||||
},
|
||||
OpenAi {
|
||||
model: OpenAiModel,
|
||||
api_url: String,
|
||||
low_speed_timeout_in_seconds: Option<u64>,
|
||||
available_models: Vec<OpenAiModel>,
|
||||
},
|
||||
Anthropic {
|
||||
model: AnthropicModel,
|
||||
api_url: String,
|
||||
low_speed_timeout_in_seconds: Option<u64>,
|
||||
},
|
||||
Ollama {
|
||||
model: OllamaModel,
|
||||
api_url: String,
|
||||
low_speed_timeout_in_seconds: Option<u64>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for AssistantProvider {
|
||||
fn default() -> Self {
|
||||
Self::OpenAi {
|
||||
model: OpenAiModel::default(),
|
||||
api_url: open_ai::OPEN_AI_API_URL.into(),
|
||||
low_speed_timeout_in_seconds: None,
|
||||
available_models: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "name", rename_all = "snake_case")]
|
||||
pub enum AssistantProviderContent {
|
||||
pub enum AssistantProviderContentV1 {
|
||||
#[serde(rename = "zed.dev")]
|
||||
ZedDotDev { default_model: Option<CloudModel> },
|
||||
#[serde(rename = "openai")]
|
||||
@@ -91,7 +52,8 @@ pub struct AssistantSettings {
|
||||
pub dock: AssistantDockPosition,
|
||||
pub default_width: Pixels,
|
||||
pub default_height: Pixels,
|
||||
pub provider: AssistantProvider,
|
||||
pub default_model: AssistantDefaultModel,
|
||||
pub using_outdated_settings_version: bool,
|
||||
}
|
||||
|
||||
/// Assistant panel settings
|
||||
@@ -123,34 +85,142 @@ impl Default for AssistantSettingsContent {
|
||||
}
|
||||
|
||||
impl AssistantSettingsContent {
|
||||
fn upgrade(&self) -> AssistantSettingsContentV1 {
|
||||
pub fn is_version_outdated(&self) -> bool {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => settings.clone(),
|
||||
VersionedAssistantSettingsContent::V1(_) => true,
|
||||
VersionedAssistantSettingsContent::V2(_) => false,
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV1 {
|
||||
AssistantSettingsContent::Legacy(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
low_speed_timeout_in_seconds,
|
||||
..
|
||||
} => update_settings_file::<AllLanguageModelSettings>(
|
||||
fs,
|
||||
cx,
|
||||
move |content, _| {
|
||||
if content.anthropic.is_none() {
|
||||
content.anthropic =
|
||||
Some(language_model::settings::AnthropicSettingsContent {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
AssistantProviderContentV1::Ollama {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
..
|
||||
} => update_settings_file::<AllLanguageModelSettings>(
|
||||
fs,
|
||||
cx,
|
||||
move |content, _| {
|
||||
if content.ollama.is_none() {
|
||||
content.ollama =
|
||||
Some(language_model::settings::OllamaSettingsContent {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
AssistantProviderContentV1::OpenAi {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
..
|
||||
} => update_settings_file::<AllLanguageModelSettings>(
|
||||
fs,
|
||||
cx,
|
||||
move |content, _| {
|
||||
if content.openai.is_none() {
|
||||
content.openai =
|
||||
Some(language_model::settings::OpenAiSettingsContent {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*self = AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(
|
||||
self.upgrade(),
|
||||
));
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> AssistantSettingsContentV2 {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
|
||||
enabled: settings.enabled,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
default_height: settings.default_width,
|
||||
default_model: settings
|
||||
.provider
|
||||
.clone()
|
||||
.and_then(|provider| match provider {
|
||||
AssistantProviderContentV1::ZedDotDev { default_model } => {
|
||||
default_model.map(|model| AssistantDefaultModel {
|
||||
provider: "zed.dev".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::OpenAi { default_model, .. } => {
|
||||
default_model.map(|model| AssistantDefaultModel {
|
||||
provider: "openai".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Anthropic { default_model, .. } => {
|
||||
default_model.map(|model| AssistantDefaultModel {
|
||||
provider: "anthropic".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Ollama { default_model, .. } => {
|
||||
default_model.map(|model| AssistantDefaultModel {
|
||||
provider: "ollama".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
}),
|
||||
},
|
||||
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
default_height: settings.default_height,
|
||||
provider: if let Some(open_ai_api_url) = settings.openai_api_url.as_ref() {
|
||||
Some(AssistantProviderContent::OpenAi {
|
||||
default_model: settings.default_open_ai_model.clone(),
|
||||
api_url: Some(open_ai_api_url.clone()),
|
||||
low_speed_timeout_in_seconds: None,
|
||||
available_models: Some(Default::default()),
|
||||
})
|
||||
} else {
|
||||
settings.default_open_ai_model.clone().map(|open_ai_model| {
|
||||
AssistantProviderContent::OpenAi {
|
||||
default_model: Some(open_ai_model),
|
||||
api_url: None,
|
||||
low_speed_timeout_in_seconds: None,
|
||||
available_models: Some(Default::default()),
|
||||
}
|
||||
})
|
||||
},
|
||||
default_model: Some(AssistantDefaultModel {
|
||||
provider: "openai".to_string(),
|
||||
model: settings
|
||||
.default_open_ai_model
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.id()
|
||||
.to_string(),
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -161,6 +231,9 @@ impl AssistantSettingsContent {
|
||||
VersionedAssistantSettingsContent::V1(settings) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
VersionedAssistantSettingsContent::V2(settings) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => {
|
||||
settings.dock = Some(dock);
|
||||
@@ -168,74 +241,78 @@ impl AssistantSettingsContent {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_model(&mut self, new_model: LanguageModel) {
|
||||
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();
|
||||
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => match &mut settings.provider {
|
||||
Some(AssistantProviderContent::ZedDotDev {
|
||||
default_model: model,
|
||||
}) => {
|
||||
if let LanguageModel::Cloud(new_model) = new_model {
|
||||
*model = Some(new_model);
|
||||
}
|
||||
VersionedAssistantSettingsContent::V1(settings) => match provider.as_ref() {
|
||||
"zed.dev" => {
|
||||
settings.provider = Some(AssistantProviderContentV1::ZedDotDev {
|
||||
default_model: CloudModel::from_id(&model).ok(),
|
||||
});
|
||||
}
|
||||
Some(AssistantProviderContent::OpenAi {
|
||||
default_model: model,
|
||||
..
|
||||
}) => {
|
||||
if let LanguageModel::OpenAi(new_model) = new_model {
|
||||
*model = Some(new_model);
|
||||
}
|
||||
"anthropic" => {
|
||||
let (api_url, low_speed_timeout_in_seconds) = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Anthropic {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
..
|
||||
}) => (api_url.clone(), *low_speed_timeout_in_seconds),
|
||||
_ => (None, None),
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Anthropic {
|
||||
default_model: AnthropicModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
});
|
||||
}
|
||||
Some(AssistantProviderContent::Anthropic {
|
||||
default_model: model,
|
||||
..
|
||||
}) => {
|
||||
if let LanguageModel::Anthropic(new_model) = new_model {
|
||||
*model = Some(new_model);
|
||||
}
|
||||
"ollama" => {
|
||||
let (api_url, low_speed_timeout_in_seconds) = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Ollama {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
..
|
||||
}) => (api_url.clone(), *low_speed_timeout_in_seconds),
|
||||
_ => (None, None),
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Ollama {
|
||||
default_model: Some(ollama::Model::new(&model)),
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
});
|
||||
}
|
||||
Some(AssistantProviderContent::Ollama {
|
||||
default_model: model,
|
||||
..
|
||||
}) => {
|
||||
if let LanguageModel::Ollama(new_model) = new_model {
|
||||
*model = Some(new_model);
|
||||
}
|
||||
"openai" => {
|
||||
let (api_url, low_speed_timeout_in_seconds, available_models) =
|
||||
match &settings.provider {
|
||||
Some(AssistantProviderContentV1::OpenAi {
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
..
|
||||
}) => (
|
||||
api_url.clone(),
|
||||
*low_speed_timeout_in_seconds,
|
||||
available_models.clone(),
|
||||
),
|
||||
_ => (None, None, None),
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::OpenAi {
|
||||
default_model: open_ai::Model::from_id(&model).ok(),
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
});
|
||||
}
|
||||
provider => match new_model {
|
||||
LanguageModel::Cloud(model) => {
|
||||
*provider = Some(AssistantProviderContent::ZedDotDev {
|
||||
default_model: Some(model),
|
||||
})
|
||||
}
|
||||
LanguageModel::OpenAi(model) => {
|
||||
*provider = Some(AssistantProviderContent::OpenAi {
|
||||
default_model: Some(model),
|
||||
api_url: None,
|
||||
low_speed_timeout_in_seconds: None,
|
||||
available_models: Some(Default::default()),
|
||||
})
|
||||
}
|
||||
LanguageModel::Anthropic(model) => {
|
||||
*provider = Some(AssistantProviderContent::Anthropic {
|
||||
default_model: Some(model),
|
||||
api_url: None,
|
||||
low_speed_timeout_in_seconds: None,
|
||||
})
|
||||
}
|
||||
LanguageModel::Ollama(model) => {
|
||||
*provider = Some(AssistantProviderContent::Ollama {
|
||||
default_model: Some(model),
|
||||
api_url: None,
|
||||
low_speed_timeout_in_seconds: None,
|
||||
})
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
},
|
||||
VersionedAssistantSettingsContent::V2(settings) => {
|
||||
settings.default_model = Some(AssistantDefaultModel { provider, model });
|
||||
}
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => {
|
||||
if let LanguageModel::OpenAi(model) = new_model {
|
||||
if let Ok(model) = open_ai::Model::from_id(&language_model.id().0) {
|
||||
settings.default_open_ai_model = Some(model);
|
||||
}
|
||||
}
|
||||
@@ -248,21 +325,78 @@ impl AssistantSettingsContent {
|
||||
pub enum VersionedAssistantSettingsContent {
|
||||
#[serde(rename = "1")]
|
||||
V1(AssistantSettingsContentV1),
|
||||
#[serde(rename = "2")]
|
||||
V2(AssistantSettingsContentV2),
|
||||
}
|
||||
|
||||
impl Default for VersionedAssistantSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::V1(AssistantSettingsContentV1 {
|
||||
Self::V2(AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
provider: None,
|
||||
default_model: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContentV2 {
|
||||
/// Whether the Assistant is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
enabled: Option<bool>,
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
button: Option<bool>,
|
||||
/// Where to dock the assistant.
|
||||
///
|
||||
/// Default: right
|
||||
dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
default_width: Option<f32>,
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
default_height: Option<f32>,
|
||||
/// The default model to use when creating new contexts.
|
||||
default_model: Option<AssistantDefaultModel>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct AssistantDefaultModel {
|
||||
#[schemars(schema_with = "providers_schema")]
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
schemars::schema::SchemaObject {
|
||||
enum_values: Some(vec![
|
||||
"anthropic".into(),
|
||||
"ollama".into(),
|
||||
"openai".into(),
|
||||
"zed.dev".into(),
|
||||
]),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
impl Default for AssistantDefaultModel {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
provider: "openai".to_string(),
|
||||
model: "gpt-4".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContentV1 {
|
||||
/// Whether the Assistant is enabled.
|
||||
@@ -289,7 +423,7 @@ pub struct AssistantSettingsContentV1 {
|
||||
///
|
||||
/// This can either be the internal `zed.dev` service or an external `openai` service,
|
||||
/// each with their respective default models and configurations.
|
||||
provider: Option<AssistantProviderContent>,
|
||||
provider: Option<AssistantProviderContentV1>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
@@ -332,6 +466,10 @@ impl Settings for AssistantSettings {
|
||||
let mut settings = AssistantSettings::default();
|
||||
|
||||
for value in sources.defaults_and_customizations() {
|
||||
if value.is_version_outdated() {
|
||||
settings.using_outdated_settings_version = true;
|
||||
}
|
||||
|
||||
let value = value.upgrade();
|
||||
merge(&mut settings.enabled, value.enabled);
|
||||
merge(&mut settings.button, value.button);
|
||||
@@ -344,123 +482,10 @@ impl Settings for AssistantSettings {
|
||||
&mut settings.default_height,
|
||||
value.default_height.map(Into::into),
|
||||
);
|
||||
if let Some(provider) = value.provider.clone() {
|
||||
match (&mut settings.provider, provider) {
|
||||
(
|
||||
AssistantProvider::ZedDotDev { model },
|
||||
AssistantProviderContent::ZedDotDev {
|
||||
default_model: model_override,
|
||||
},
|
||||
) => {
|
||||
merge(model, model_override);
|
||||
}
|
||||
(
|
||||
AssistantProvider::OpenAi {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
},
|
||||
AssistantProviderContent::OpenAi {
|
||||
default_model: model_override,
|
||||
api_url: api_url_override,
|
||||
low_speed_timeout_in_seconds: low_speed_timeout_in_seconds_override,
|
||||
available_models: available_models_override,
|
||||
},
|
||||
) => {
|
||||
merge(model, model_override);
|
||||
merge(api_url, api_url_override);
|
||||
merge(available_models, available_models_override);
|
||||
if let Some(low_speed_timeout_in_seconds_override) =
|
||||
low_speed_timeout_in_seconds_override
|
||||
{
|
||||
*low_speed_timeout_in_seconds =
|
||||
Some(low_speed_timeout_in_seconds_override);
|
||||
}
|
||||
}
|
||||
(
|
||||
AssistantProvider::Ollama {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
AssistantProviderContent::Ollama {
|
||||
default_model: model_override,
|
||||
api_url: api_url_override,
|
||||
low_speed_timeout_in_seconds: low_speed_timeout_in_seconds_override,
|
||||
},
|
||||
) => {
|
||||
merge(model, model_override);
|
||||
merge(api_url, api_url_override);
|
||||
if let Some(low_speed_timeout_in_seconds_override) =
|
||||
low_speed_timeout_in_seconds_override
|
||||
{
|
||||
*low_speed_timeout_in_seconds =
|
||||
Some(low_speed_timeout_in_seconds_override);
|
||||
}
|
||||
}
|
||||
(
|
||||
AssistantProvider::Anthropic {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
AssistantProviderContent::Anthropic {
|
||||
default_model: model_override,
|
||||
api_url: api_url_override,
|
||||
low_speed_timeout_in_seconds: low_speed_timeout_in_seconds_override,
|
||||
},
|
||||
) => {
|
||||
merge(model, model_override);
|
||||
merge(api_url, api_url_override);
|
||||
if let Some(low_speed_timeout_in_seconds_override) =
|
||||
low_speed_timeout_in_seconds_override
|
||||
{
|
||||
*low_speed_timeout_in_seconds =
|
||||
Some(low_speed_timeout_in_seconds_override);
|
||||
}
|
||||
}
|
||||
(provider, provider_override) => {
|
||||
*provider = match provider_override {
|
||||
AssistantProviderContent::ZedDotDev {
|
||||
default_model: model,
|
||||
} => AssistantProvider::ZedDotDev {
|
||||
model: model.unwrap_or_default(),
|
||||
},
|
||||
AssistantProviderContent::OpenAi {
|
||||
default_model: model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
} => AssistantProvider::OpenAi {
|
||||
model: model.unwrap_or_default(),
|
||||
api_url: api_url.unwrap_or_else(|| open_ai::OPEN_AI_API_URL.into()),
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models: available_models.unwrap_or_default(),
|
||||
},
|
||||
AssistantProviderContent::Anthropic {
|
||||
default_model: model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
} => AssistantProvider::Anthropic {
|
||||
model: model.unwrap_or_default(),
|
||||
api_url: api_url
|
||||
.unwrap_or_else(|| anthropic::ANTHROPIC_API_URL.into()),
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
AssistantProviderContent::Ollama {
|
||||
default_model: model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
} => AssistantProvider::Ollama {
|
||||
model: model.unwrap_or_default(),
|
||||
api_url: api_url.unwrap_or_else(|| ollama::OLLAMA_API_URL.into()),
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
merge(
|
||||
&mut settings.default_model,
|
||||
value.default_model.map(Into::into),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
@@ -473,221 +498,103 @@ fn merge<T>(target: &mut T, value: Option<T>) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_completion_provider_settings(
|
||||
provider: &mut CompletionProvider,
|
||||
version: usize,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let updated = match &AssistantSettings::get_global(cx).provider {
|
||||
AssistantProvider::ZedDotDev { model } => provider
|
||||
.update_current_as::<_, CloudCompletionProvider>(|provider| {
|
||||
provider.update(model.clone(), version);
|
||||
}),
|
||||
AssistantProvider::OpenAi {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
} => provider.update_current_as::<_, OpenAiCompletionProvider>(|provider| {
|
||||
provider.update(
|
||||
choose_openai_model(&model, &available_models),
|
||||
api_url.clone(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
version,
|
||||
);
|
||||
}),
|
||||
AssistantProvider::Anthropic {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
} => provider.update_current_as::<_, AnthropicCompletionProvider>(|provider| {
|
||||
provider.update(
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
version,
|
||||
);
|
||||
}),
|
||||
AssistantProvider::Ollama {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
} => provider.update_current_as::<_, OllamaCompletionProvider>(|provider| {
|
||||
provider.update(
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
version,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
};
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use gpui::{AppContext, UpdateGlobal};
|
||||
// use settings::SettingsStore;
|
||||
|
||||
// Previously configured provider was changed to another one
|
||||
if updated.is_none() {
|
||||
provider.update_provider(|client| create_provider_from_settings(client, version, cx));
|
||||
}
|
||||
}
|
||||
// use super::*;
|
||||
|
||||
pub(crate) fn create_provider_from_settings(
|
||||
client: Arc<Client>,
|
||||
settings_version: usize,
|
||||
cx: &mut AppContext,
|
||||
) -> Arc<RwLock<dyn LanguageModelCompletionProvider>> {
|
||||
match &AssistantSettings::get_global(cx).provider {
|
||||
AssistantProvider::ZedDotDev { model } => Arc::new(RwLock::new(
|
||||
CloudCompletionProvider::new(model.clone(), client.clone(), settings_version, cx),
|
||||
)),
|
||||
AssistantProvider::OpenAi {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
} => Arc::new(RwLock::new(OpenAiCompletionProvider::new(
|
||||
choose_openai_model(&model, &available_models),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
available_models.clone(),
|
||||
))),
|
||||
AssistantProvider::Anthropic {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
} => Arc::new(RwLock::new(AnthropicCompletionProvider::new(
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
))),
|
||||
AssistantProvider::Ollama {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
} => Arc::new(RwLock::new(OllamaCompletionProvider::new(
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
cx,
|
||||
))),
|
||||
}
|
||||
}
|
||||
// #[gpui::test]
|
||||
// fn test_deserialize_assistant_settings(cx: &mut AppContext) {
|
||||
// let store = settings::SettingsStore::test(cx);
|
||||
// cx.set_global(store);
|
||||
|
||||
/// Choose which model to use for openai provider.
|
||||
/// If the model is not available, try to use the first available model, or fallback to the original model.
|
||||
fn choose_openai_model(
|
||||
model: &::open_ai::Model,
|
||||
available_models: &[::open_ai::Model],
|
||||
) -> ::open_ai::Model {
|
||||
available_models
|
||||
.iter()
|
||||
.find(|&m| m == model)
|
||||
.or_else(|| available_models.first())
|
||||
.unwrap_or_else(|| model)
|
||||
.clone()
|
||||
}
|
||||
// // Settings default to gpt-4-turbo.
|
||||
// AssistantSettings::register(cx);
|
||||
// assert_eq!(
|
||||
// AssistantSettings::get_global(cx).provider,
|
||||
// AssistantProvider::OpenAi {
|
||||
// model: OpenAiModel::FourOmni,
|
||||
// api_url: open_ai::OPEN_AI_API_URL.into(),
|
||||
// low_speed_timeout_in_seconds: None,
|
||||
// available_models: Default::default(),
|
||||
// }
|
||||
// );
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{AppContext, UpdateGlobal};
|
||||
use settings::SettingsStore;
|
||||
// // Ensure backward-compatibility.
|
||||
// SettingsStore::update_global(cx, |store, cx| {
|
||||
// store
|
||||
// .set_user_settings(
|
||||
// r#"{
|
||||
// "assistant": {
|
||||
// "openai_api_url": "test-url",
|
||||
// }
|
||||
// }"#,
|
||||
// cx,
|
||||
// )
|
||||
// .unwrap();
|
||||
// });
|
||||
// assert_eq!(
|
||||
// AssistantSettings::get_global(cx).provider,
|
||||
// AssistantProvider::OpenAi {
|
||||
// model: OpenAiModel::FourOmni,
|
||||
// api_url: "test-url".into(),
|
||||
// low_speed_timeout_in_seconds: None,
|
||||
// available_models: Default::default(),
|
||||
// }
|
||||
// );
|
||||
// SettingsStore::update_global(cx, |store, cx| {
|
||||
// store
|
||||
// .set_user_settings(
|
||||
// r#"{
|
||||
// "assistant": {
|
||||
// "default_open_ai_model": "gpt-4-0613"
|
||||
// }
|
||||
// }"#,
|
||||
// cx,
|
||||
// )
|
||||
// .unwrap();
|
||||
// });
|
||||
// assert_eq!(
|
||||
// AssistantSettings::get_global(cx).provider,
|
||||
// AssistantProvider::OpenAi {
|
||||
// model: OpenAiModel::Four,
|
||||
// api_url: open_ai::OPEN_AI_API_URL.into(),
|
||||
// low_speed_timeout_in_seconds: None,
|
||||
// available_models: Default::default(),
|
||||
// }
|
||||
// );
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_deserialize_assistant_settings(cx: &mut AppContext) {
|
||||
let store = settings::SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
|
||||
// Settings default to gpt-4-turbo.
|
||||
AssistantSettings::register(cx);
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).provider,
|
||||
AssistantProvider::OpenAi {
|
||||
model: OpenAiModel::FourOmni,
|
||||
api_url: open_ai::OPEN_AI_API_URL.into(),
|
||||
low_speed_timeout_in_seconds: None,
|
||||
available_models: Default::default(),
|
||||
}
|
||||
);
|
||||
|
||||
// Ensure backward-compatibility.
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store
|
||||
.set_user_settings(
|
||||
r#"{
|
||||
"assistant": {
|
||||
"openai_api_url": "test-url",
|
||||
}
|
||||
}"#,
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).provider,
|
||||
AssistantProvider::OpenAi {
|
||||
model: OpenAiModel::FourOmni,
|
||||
api_url: "test-url".into(),
|
||||
low_speed_timeout_in_seconds: None,
|
||||
available_models: Default::default(),
|
||||
}
|
||||
);
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store
|
||||
.set_user_settings(
|
||||
r#"{
|
||||
"assistant": {
|
||||
"default_open_ai_model": "gpt-4-0613"
|
||||
}
|
||||
}"#,
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).provider,
|
||||
AssistantProvider::OpenAi {
|
||||
model: OpenAiModel::Four,
|
||||
api_url: open_ai::OPEN_AI_API_URL.into(),
|
||||
low_speed_timeout_in_seconds: None,
|
||||
available_models: Default::default(),
|
||||
}
|
||||
);
|
||||
|
||||
// The new version supports setting a custom model when using zed.dev.
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store
|
||||
.set_user_settings(
|
||||
r#"{
|
||||
"assistant": {
|
||||
"version": "1",
|
||||
"provider": {
|
||||
"name": "zed.dev",
|
||||
"default_model": {
|
||||
"custom": {
|
||||
"name": "custom-provider"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).provider,
|
||||
AssistantProvider::ZedDotDev {
|
||||
model: CloudModel::Custom {
|
||||
name: "custom-provider".into(),
|
||||
max_tokens: None
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
// // The new version supports setting a custom model when using zed.dev.
|
||||
// SettingsStore::update_global(cx, |store, cx| {
|
||||
// store
|
||||
// .set_user_settings(
|
||||
// r#"{
|
||||
// "assistant": {
|
||||
// "version": "1",
|
||||
// "provider": {
|
||||
// "name": "zed.dev",
|
||||
// "default_model": {
|
||||
// "custom": {
|
||||
// "name": "custom-provider"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }"#,
|
||||
// cx,
|
||||
// )
|
||||
// .unwrap();
|
||||
// });
|
||||
// assert_eq!(
|
||||
// AssistantSettings::get_global(cx).provider,
|
||||
// AssistantProvider::ZedDotDev {
|
||||
// model: CloudModel::Custom {
|
||||
// name: "custom-provider".into(),
|
||||
// max_tokens: None
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
prompt_library::PromptStore, slash_command::SlashCommandLine, CompletionProvider, MessageId,
|
||||
MessageStatus,
|
||||
prompt_library::PromptStore, slash_command::SlashCommandLine, LanguageModelCompletionProvider,
|
||||
MessageId, MessageStatus,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use assistant_slash_command::{
|
||||
@@ -9,7 +9,7 @@ use assistant_slash_command::{
|
||||
use client::{self, proto, telemetry::Telemetry};
|
||||
use clock::ReplicaId;
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::Fs;
|
||||
use fs::{Fs, RemoveOptions};
|
||||
use futures::{
|
||||
future::{self, Shared},
|
||||
FutureExt, StreamExt,
|
||||
@@ -1124,7 +1124,9 @@ impl Context {
|
||||
.await;
|
||||
|
||||
let token_count = cx
|
||||
.update(|cx| CompletionProvider::global(cx).count_tokens(request, cx))?
|
||||
.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
@@ -1308,7 +1310,9 @@ impl Context {
|
||||
});
|
||||
|
||||
let raw_output = cx
|
||||
.update(|cx| CompletionProvider::global(cx).complete(request, cx))?
|
||||
.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx).complete(request, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
let operations = Self::parse_edit_operations(&raw_output);
|
||||
@@ -1612,13 +1616,14 @@ impl Context {
|
||||
.then_some(message.id)
|
||||
})?;
|
||||
|
||||
if !CompletionProvider::global(cx).is_authenticated() {
|
||||
if !LanguageModelCompletionProvider::read_global(cx).is_authenticated(cx) {
|
||||
log::info!("completion provider has no credentials");
|
||||
return None;
|
||||
}
|
||||
|
||||
let request = self.to_completion_request(cx);
|
||||
let stream = CompletionProvider::global(cx).stream_completion(request, cx);
|
||||
let stream =
|
||||
LanguageModelCompletionProvider::read_global(cx).stream_completion(request, cx);
|
||||
let assistant_message = self
|
||||
.insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx)
|
||||
.unwrap();
|
||||
@@ -1675,7 +1680,7 @@ impl Context {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.pending_completions
|
||||
.retain(|completion| completion.id != this.completion_count);
|
||||
this.summarize(cx);
|
||||
this.summarize(false, cx);
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
@@ -1698,11 +1703,14 @@ impl Context {
|
||||
});
|
||||
|
||||
if let Some(telemetry) = this.telemetry.as_ref() {
|
||||
let model = CompletionProvider::global(cx).model();
|
||||
let model_telemetry_id = LanguageModelCompletionProvider::read_global(cx)
|
||||
.active_model()
|
||||
.map(|m| m.telemetry_id())
|
||||
.unwrap_or_default();
|
||||
telemetry.report_assistant_event(
|
||||
Some(this.id.0.clone()),
|
||||
AssistantKind::Panel,
|
||||
model.telemetry_id(),
|
||||
model_telemetry_id,
|
||||
response_latency,
|
||||
error_message,
|
||||
);
|
||||
@@ -1727,7 +1735,6 @@ impl Context {
|
||||
.map(|message| message.to_request_message(self.buffer.read(cx)));
|
||||
|
||||
LanguageModelRequest {
|
||||
model: CompletionProvider::global(cx).model(),
|
||||
messages: messages.collect(),
|
||||
stop: vec![],
|
||||
temperature: 1.0,
|
||||
@@ -1968,9 +1975,9 @@ impl Context {
|
||||
self.message_anchors.insert(insertion_ix, new_anchor);
|
||||
}
|
||||
|
||||
fn summarize(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if self.message_anchors.len() >= 2 && self.summary.is_none() {
|
||||
if !CompletionProvider::global(cx).is_authenticated() {
|
||||
pub(super) fn summarize(&mut self, replace_old: bool, cx: &mut ModelContext<Self>) {
|
||||
if replace_old || (self.message_anchors.len() >= 2 && self.summary.is_none()) {
|
||||
if !LanguageModelCompletionProvider::read_global(cx).is_authenticated(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1982,24 +1989,29 @@ impl Context {
|
||||
content: "Summarize the context into a short title without punctuation.".into(),
|
||||
}));
|
||||
let request = LanguageModelRequest {
|
||||
model: CompletionProvider::global(cx).model(),
|
||||
messages: messages.collect(),
|
||||
stop: vec![],
|
||||
temperature: 1.0,
|
||||
};
|
||||
|
||||
let stream = CompletionProvider::global(cx).stream_completion(request, cx);
|
||||
let stream =
|
||||
LanguageModelCompletionProvider::read_global(cx).stream_completion(request, cx);
|
||||
self.pending_summary = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let mut messages = stream.await?;
|
||||
|
||||
let mut replaced = !replace_old;
|
||||
while let Some(message) = messages.next().await {
|
||||
let text = message?;
|
||||
let mut lines = text.lines();
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let version = this.version.clone();
|
||||
let timestamp = this.next_timestamp();
|
||||
let summary = this.summary.get_or_insert(Default::default());
|
||||
let summary = this.summary.get_or_insert(ContextSummary::default());
|
||||
if !replaced && replace_old {
|
||||
summary.text.clear();
|
||||
replaced = true;
|
||||
}
|
||||
summary.text.extend(lines.next());
|
||||
summary.timestamp = timestamp;
|
||||
let operation = ContextOperation::UpdateSummary {
|
||||
@@ -2142,35 +2154,52 @@ impl Context {
|
||||
|
||||
if let Some(summary) = summary {
|
||||
let context = this.read_with(&cx, |this, cx| this.serialize(cx))?;
|
||||
let path = if let Some(old_path) = old_path {
|
||||
old_path
|
||||
} else {
|
||||
let mut discriminant = 1;
|
||||
let mut new_path;
|
||||
loop {
|
||||
new_path = contexts_dir().join(&format!(
|
||||
"{} - {}.zed.json",
|
||||
summary.trim(),
|
||||
discriminant
|
||||
));
|
||||
if fs.is_file(&new_path).await {
|
||||
discriminant += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
let mut discriminant = 1;
|
||||
let mut new_path;
|
||||
loop {
|
||||
new_path = contexts_dir().join(&format!(
|
||||
"{} - {}.zed.json",
|
||||
summary.trim(),
|
||||
discriminant
|
||||
));
|
||||
if fs.is_file(&new_path).await {
|
||||
discriminant += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
new_path
|
||||
};
|
||||
}
|
||||
|
||||
fs.create_dir(contexts_dir().as_ref()).await?;
|
||||
fs.atomic_write(path.clone(), serde_json::to_string(&context).unwrap())
|
||||
fs.atomic_write(new_path.clone(), serde_json::to_string(&context).unwrap())
|
||||
.await?;
|
||||
this.update(&mut cx, |this, _| this.path = Some(path))?;
|
||||
if let Some(old_path) = old_path {
|
||||
if new_path != old_path {
|
||||
fs.remove_file(
|
||||
&old_path,
|
||||
RemoveOptions {
|
||||
recursive: false,
|
||||
ignore_if_not_exists: true,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, _| this.path = Some(new_path))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn custom_summary(&mut self, custom_summary: String, cx: &mut ModelContext<Self>) {
|
||||
let timestamp = self.next_timestamp();
|
||||
let summary = self.summary.get_or_insert(ContextSummary::default());
|
||||
summary.timestamp = timestamp;
|
||||
summary.done = true;
|
||||
summary.text = custom_summary;
|
||||
cx.emit(ContextEvent::SummaryChanged);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -2482,7 +2511,6 @@ mod tests {
|
||||
MessageId,
|
||||
};
|
||||
use assistant_slash_command::{ArgumentCompletion, SlashCommand};
|
||||
use completion::FakeCompletionProvider;
|
||||
use fs::FakeFs;
|
||||
use gpui::{AppContext, TestAppContext, WeakView};
|
||||
use indoc::indoc;
|
||||
@@ -2502,7 +2530,8 @@ mod tests {
|
||||
#[gpui::test]
|
||||
fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
FakeCompletionProvider::setup_test(cx);
|
||||
language_model::LanguageModelRegistry::test(cx);
|
||||
completion::LanguageModelCompletionProvider::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
assistant_panel::init(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
@@ -2634,7 +2663,8 @@ mod tests {
|
||||
fn test_message_splitting(cx: &mut AppContext) {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
FakeCompletionProvider::setup_test(cx);
|
||||
language_model::LanguageModelRegistry::test(cx);
|
||||
completion::LanguageModelCompletionProvider::test(cx);
|
||||
assistant_panel::init(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
|
||||
@@ -2727,7 +2757,8 @@ mod tests {
|
||||
#[gpui::test]
|
||||
fn test_messages_for_offsets(cx: &mut AppContext) {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
FakeCompletionProvider::setup_test(cx);
|
||||
language_model::LanguageModelRegistry::test(cx);
|
||||
completion::LanguageModelCompletionProvider::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
assistant_panel::init(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
@@ -2812,7 +2843,8 @@ mod tests {
|
||||
async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
let settings_store = cx.update(SettingsStore::test);
|
||||
cx.set_global(settings_store);
|
||||
cx.update(FakeCompletionProvider::setup_test);
|
||||
cx.update(language_model::LanguageModelRegistry::test);
|
||||
cx.update(completion::LanguageModelCompletionProvider::test);
|
||||
cx.update(Project::init_settings);
|
||||
cx.update(assistant_panel::init);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
@@ -2937,7 +2969,11 @@ mod tests {
|
||||
cx.update(prompt_library::init);
|
||||
let settings_store = cx.update(SettingsStore::test);
|
||||
cx.set_global(settings_store);
|
||||
let fake_provider = cx.update(FakeCompletionProvider::setup_test);
|
||||
|
||||
let fake_provider = cx.update(language_model::LanguageModelRegistry::test);
|
||||
cx.update(completion::LanguageModelCompletionProvider::test);
|
||||
|
||||
let fake_model = fake_provider.test_model();
|
||||
cx.update(assistant_panel::init);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
|
||||
@@ -3003,8 +3039,8 @@ mod tests {
|
||||
});
|
||||
|
||||
// Simulate the LLM completion
|
||||
fake_provider.send_last_completion_chunk(llm_response.to_string());
|
||||
fake_provider.finish_last_completion();
|
||||
fake_model.send_last_completion_chunk(llm_response.to_string());
|
||||
fake_model.finish_last_completion();
|
||||
|
||||
// Wait for the completion to be processed
|
||||
cx.run_until_parked();
|
||||
@@ -3085,7 +3121,8 @@ mod tests {
|
||||
async fn test_serialization(cx: &mut TestAppContext) {
|
||||
let settings_store = cx.update(SettingsStore::test);
|
||||
cx.set_global(settings_store);
|
||||
cx.update(FakeCompletionProvider::setup_test);
|
||||
cx.update(language_model::LanguageModelRegistry::test);
|
||||
cx.update(completion::LanguageModelCompletionProvider::test);
|
||||
cx.update(assistant_panel::init);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
let context = cx.new_model(|cx| Context::local(registry.clone(), None, cx));
|
||||
@@ -3161,7 +3198,9 @@ mod tests {
|
||||
|
||||
let settings_store = cx.update(SettingsStore::test);
|
||||
cx.set_global(settings_store);
|
||||
cx.update(FakeCompletionProvider::setup_test);
|
||||
cx.update(language_model::LanguageModelRegistry::test);
|
||||
cx.update(completion::LanguageModelCompletionProvider::test);
|
||||
|
||||
cx.update(assistant_panel::init);
|
||||
let slash_commands = cx.update(SlashCommandRegistry::default_global);
|
||||
slash_commands.register_command(FakeSlashCommand("cmd-1".into()), false);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
assistant_settings::AssistantSettings, humanize_token_count, prompts::generate_content_prompt,
|
||||
AssistantPanel, AssistantPanelEvent, CompletionProvider, Hunk, StreamingDiff,
|
||||
humanize_token_count, prompts::generate_content_prompt, AssistantPanel, AssistantPanelEvent,
|
||||
Hunk, LanguageModelCompletionProvider, ModelSelector, StreamingDiff,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
@@ -26,12 +26,12 @@ use gpui::{
|
||||
ModelContext, Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, WeakView,
|
||||
WindowContext,
|
||||
};
|
||||
use language::{Buffer, Point, Selection, TransactionId};
|
||||
use language::{Buffer, IndentKind, Point, Selection, TransactionId};
|
||||
use language_model::{LanguageModelRequest, LanguageModelRequestMessage, Role};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use rope::Rope;
|
||||
use settings::{update_settings_file, Settings};
|
||||
use settings::Settings;
|
||||
use similar::TextDiff;
|
||||
use smol::future::FutureExt;
|
||||
use std::{
|
||||
@@ -45,7 +45,7 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, ContextMenu, IconButtonShape, PopoverMenu, Tooltip};
|
||||
use ui::{prelude::*, IconButtonShape, Tooltip};
|
||||
use util::RangeExt;
|
||||
use workspace::{notifications::NotificationId, Toast, Workspace};
|
||||
|
||||
@@ -844,7 +844,10 @@ impl InlineAssistant {
|
||||
}
|
||||
|
||||
let codegen = assist.codegen.clone();
|
||||
let telemetry_id = CompletionProvider::global(cx).model().telemetry_id();
|
||||
let telemetry_id = LanguageModelCompletionProvider::read_global(cx)
|
||||
.active_model()
|
||||
.map(|m| m.telemetry_id())
|
||||
.unwrap_or_default();
|
||||
let chunks: LocalBoxFuture<Result<BoxStream<Result<String>>>> =
|
||||
if user_prompt.trim().to_lowercase() == "delete" {
|
||||
async { Ok(stream::empty().boxed()) }.boxed_local()
|
||||
@@ -854,7 +857,10 @@ impl InlineAssistant {
|
||||
async move {
|
||||
let request = request.await?;
|
||||
let chunks = cx
|
||||
.update(|cx| CompletionProvider::global(cx).stream_completion(request, cx))?
|
||||
.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx)
|
||||
.stream_completion(request, cx)
|
||||
})?
|
||||
.await?;
|
||||
Ok(chunks.boxed())
|
||||
}
|
||||
@@ -871,8 +877,8 @@ impl InlineAssistant {
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<LanguageModelRequest>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let (user_prompt, context_request, project_name, buffer, range, model) = cx
|
||||
.read_global(|this: &InlineAssistant, cx: &WindowContext| {
|
||||
let (user_prompt, context_request, project_name, buffer, range) =
|
||||
cx.read_global(|this: &InlineAssistant, cx: &WindowContext| {
|
||||
let assist = this.assists.get(&assist_id).context("invalid assist")?;
|
||||
let decorations = assist.decorations.as_ref().context("invalid assist")?;
|
||||
let editor = assist.editor.upgrade().context("invalid assist")?;
|
||||
@@ -906,15 +912,7 @@ impl InlineAssistant {
|
||||
});
|
||||
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
let range = assist.codegen.read(cx).range.clone();
|
||||
let model = CompletionProvider::global(cx).model();
|
||||
anyhow::Ok((
|
||||
user_prompt,
|
||||
context_request,
|
||||
project_name,
|
||||
buffer,
|
||||
range,
|
||||
model,
|
||||
))
|
||||
anyhow::Ok((user_prompt, context_request, project_name, buffer, range))
|
||||
})??;
|
||||
|
||||
let language = buffer.language_at(range.start);
|
||||
@@ -973,7 +971,6 @@ impl InlineAssistant {
|
||||
});
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
model,
|
||||
messages,
|
||||
stop: vec!["|END|>".to_string()],
|
||||
temperature,
|
||||
@@ -1326,22 +1323,19 @@ impl EventEmitter<PromptEditorEvent> for PromptEditor {}
|
||||
impl Render for PromptEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let gutter_dimensions = *self.gutter_dimensions.lock();
|
||||
let fs = self.fs.clone();
|
||||
|
||||
let buttons = match &self.codegen.read(cx).status {
|
||||
CodegenStatus::Idle => {
|
||||
vec![
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::None)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
|
||||
),
|
||||
IconButton::new("start", IconName::Sparkle)
|
||||
IconButton::new("start", IconName::SparkleAlt)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::None)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::for_action("Transform", &menu::Confirm, cx))
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
|
||||
@@ -1352,15 +1346,14 @@ impl Render for PromptEditor {
|
||||
vec![
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::None)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::text("Cancel Assist", cx))
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
|
||||
),
|
||||
IconButton::new("stop", IconName::Stop)
|
||||
.icon_color(Color::Error)
|
||||
.size(ButtonSize::None)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::with_meta(
|
||||
"Interrupt Transformation",
|
||||
@@ -1378,7 +1371,7 @@ impl Render for PromptEditor {
|
||||
vec![
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::None)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
|
||||
@@ -1386,8 +1379,7 @@ impl Render for PromptEditor {
|
||||
if self.edited_since_done {
|
||||
IconButton::new("restart", IconName::RotateCw)
|
||||
.icon_color(Color::Info)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.size(ButtonSize::None)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::with_meta(
|
||||
"Restart Transformation",
|
||||
@@ -1402,7 +1394,7 @@ impl Render for PromptEditor {
|
||||
} else {
|
||||
IconButton::new("confirm", IconName::Check)
|
||||
.icon_color(Color::Info)
|
||||
.size(ButtonSize::None)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::for_action("Confirm Assist", &menu::Confirm, cx))
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested);
|
||||
@@ -1428,58 +1420,27 @@ impl Render for PromptEditor {
|
||||
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
PopoverMenu::new("model-switcher")
|
||||
.menu(move |cx| {
|
||||
ContextMenu::build(cx, |mut menu, cx| {
|
||||
for model in CompletionProvider::global(cx).available_models() {
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
let model = model.clone();
|
||||
move |_| {
|
||||
Label::new(model.display_name())
|
||||
.into_any_element()
|
||||
}
|
||||
},
|
||||
{
|
||||
let fs = fs.clone();
|
||||
let model = model.clone();
|
||||
move |cx| {
|
||||
let model = model.clone();
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings| settings.set_model(model),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
menu
|
||||
})
|
||||
.into()
|
||||
})
|
||||
.trigger(
|
||||
IconButton::new("context", IconName::Settings)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"Using {}",
|
||||
CompletionProvider::global(cx)
|
||||
.model()
|
||||
.display_name()
|
||||
),
|
||||
None,
|
||||
"Change Model",
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.anchor(gpui::AnchorCorner::BottomRight),
|
||||
)
|
||||
.child(ModelSelector::new(
|
||||
self.fs.clone(),
|
||||
IconButton::new("context", IconName::Settings)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"Using {}",
|
||||
LanguageModelCompletionProvider::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());
|
||||
@@ -1502,7 +1463,7 @@ impl Render for PromptEditor {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.pr_4()
|
||||
.pr_6()
|
||||
.children(self.render_token_count(cx))
|
||||
.children(buttons),
|
||||
)
|
||||
@@ -1668,7 +1629,9 @@ impl PromptEditor {
|
||||
.await?;
|
||||
|
||||
let token_count = cx
|
||||
.update(|cx| CompletionProvider::global(cx).count_tokens(request, cx))?
|
||||
.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx)
|
||||
})?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.token_count = Some(token_count);
|
||||
@@ -1796,7 +1759,7 @@ impl PromptEditor {
|
||||
}
|
||||
|
||||
fn render_token_count(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
|
||||
let model = CompletionProvider::global(cx).model();
|
||||
let model = LanguageModelCompletionProvider::read_global(cx).active_model()?;
|
||||
let token_count = self.token_count?;
|
||||
let max_token_count = model.max_token_count();
|
||||
|
||||
@@ -2121,12 +2084,26 @@ impl Codegen {
|
||||
.collect::<Rope>();
|
||||
|
||||
let selection_start = range.start.to_point(&snapshot);
|
||||
let suggested_line_indent = snapshot
|
||||
.suggested_indents(selection_start.row..selection_start.row + 1, cx)
|
||||
|
||||
// Start with the indentation of the first line in the selection
|
||||
let mut suggested_line_indent = snapshot
|
||||
.suggested_indents(selection_start.row..=selection_start.row, cx)
|
||||
.into_values()
|
||||
.next()
|
||||
.unwrap_or_else(|| snapshot.indent_size_for_line(MultiBufferRow(selection_start.row)));
|
||||
|
||||
// If the first line in the selection does not have indentation, check the following lines
|
||||
if suggested_line_indent.len == 0 && suggested_line_indent.kind == IndentKind::Space {
|
||||
for row in selection_start.row..=range.end.to_point(&snapshot).row {
|
||||
let line_indent = snapshot.indent_size_for_line(MultiBufferRow(row));
|
||||
// Prefer tabs if a line in the selection uses tabs as indentation
|
||||
if line_indent.kind == IndentKind::Tab {
|
||||
suggested_line_indent.kind = IndentKind::Tab;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let telemetry = self.telemetry.clone();
|
||||
self.edit_position = range.start;
|
||||
self.diff = Diff::default();
|
||||
@@ -2601,7 +2578,6 @@ fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use completion::FakeCompletionProvider;
|
||||
use futures::stream::{self};
|
||||
use gpui::{Context, TestAppContext};
|
||||
use indoc::indoc;
|
||||
@@ -2609,6 +2585,7 @@ mod tests {
|
||||
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
|
||||
Point,
|
||||
};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use rand::prelude::*;
|
||||
use serde::Serialize;
|
||||
use settings::SettingsStore;
|
||||
@@ -2622,7 +2599,8 @@ mod tests {
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(|cx| FakeCompletionProvider::setup_test(cx));
|
||||
cx.update(language_model::LanguageModelRegistry::test);
|
||||
cx.update(completion::LanguageModelCompletionProvider::test);
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
@@ -2749,7 +2727,8 @@ mod tests {
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
cx.update(|cx| FakeCompletionProvider::setup_test(cx));
|
||||
cx.update(LanguageModelRegistry::test);
|
||||
cx.update(completion::LanguageModelCompletionProvider::test);
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
@@ -2808,6 +2787,62 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_autoindent_respects_tabs_in_selection(cx: &mut TestAppContext) {
|
||||
cx.update(LanguageModelRegistry::test);
|
||||
cx.update(completion::LanguageModelCompletionProvider::test);
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
func main() {
|
||||
\tx := 0
|
||||
\tfor i := 0; i < 10; i++ {
|
||||
\t\tx++
|
||||
\t}
|
||||
}
|
||||
"};
|
||||
let buffer = cx.new_model(|cx| Buffer::local(text, cx));
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(4, 2))
|
||||
});
|
||||
let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, None, None, cx));
|
||||
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.start(
|
||||
String::new(),
|
||||
future::ready(Ok(chunks_rx.map(|chunk| Ok(chunk)).boxed())),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let new_text = concat!(
|
||||
"func main() {\n",
|
||||
"\tx := 0\n",
|
||||
"\tfor x < 10 {\n",
|
||||
"\t\tx++\n",
|
||||
"\t}", //
|
||||
);
|
||||
chunks_tx.unbounded_send(new_text.to_string()).unwrap();
|
||||
drop(chunks_tx);
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
func main() {
|
||||
\tx := 0
|
||||
\tfor x < 10 {
|
||||
\t\tx++
|
||||
\t}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_strip_invalid_spans_from_codeblock() {
|
||||
assert_chunks("Lorem ipsum dolor", "Lorem ipsum dolor").await;
|
||||
|
||||
@@ -1,82 +1,128 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{assistant_settings::AssistantSettings, CompletionProvider, ToggleModelSelector};
|
||||
use crate::{assistant_settings::AssistantSettings, LanguageModelCompletionProvider};
|
||||
use fs::Fs;
|
||||
use language_model::LanguageModelRegistry;
|
||||
use settings::update_settings_file;
|
||||
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
|
||||
use ui::{prelude::*, ContextMenu, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ModelSelector {
|
||||
handle: PopoverMenuHandle<ContextMenu>,
|
||||
pub struct ModelSelector<T: PopoverTrigger> {
|
||||
handle: Option<PopoverMenuHandle<ContextMenu>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
trigger: T,
|
||||
}
|
||||
|
||||
impl ModelSelector {
|
||||
pub fn new(handle: PopoverMenuHandle<ContextMenu>, fs: Arc<dyn Fs>) -> Self {
|
||||
ModelSelector { handle, fs }
|
||||
impl<T: PopoverTrigger> ModelSelector<T> {
|
||||
pub fn new(fs: Arc<dyn Fs>, trigger: T) -> Self {
|
||||
ModelSelector {
|
||||
handle: None,
|
||||
fs,
|
||||
trigger,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_handle(mut self, handle: PopoverMenuHandle<ContextMenu>) -> Self {
|
||||
self.handle = Some(handle);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ModelSelector {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
PopoverMenu::new("model-switcher")
|
||||
.with_handle(self.handle)
|
||||
.menu(move |cx| {
|
||||
ContextMenu::build(cx, |mut menu, cx| {
|
||||
for model in CompletionProvider::global(cx).available_models() {
|
||||
impl<T: PopoverTrigger> RenderOnce for ModelSelector<T> {
|
||||
fn render(self, _: &mut WindowContext) -> impl IntoElement {
|
||||
let mut menu = PopoverMenu::new("model-switcher");
|
||||
if let Some(handle) = self.handle {
|
||||
menu = menu.with_handle(handle);
|
||||
}
|
||||
|
||||
menu.menu(move |cx| {
|
||||
ContextMenu::build(cx, |mut menu, cx| {
|
||||
for (index, provider) in LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
.enumerate()
|
||||
{
|
||||
if index > 0 {
|
||||
menu = menu.separator();
|
||||
}
|
||||
menu = menu.header(provider.name().0);
|
||||
|
||||
let available_models = provider.provided_models(cx);
|
||||
if available_models.is_empty() {
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
let model = model.clone();
|
||||
move |_| Label::new(model.display_name()).into_any_element()
|
||||
move |_| {
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::Settings))
|
||||
.child(Label::new("Configure"))
|
||||
.into_any()
|
||||
}
|
||||
},
|
||||
{
|
||||
let fs = self.fs.clone();
|
||||
let model = model.clone();
|
||||
let provider = provider.id();
|
||||
move |cx| {
|
||||
let model = model.clone();
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
LanguageModelCompletionProvider::global(cx).update(
|
||||
cx,
|
||||
move |settings| settings.set_model(model),
|
||||
|completion_provider, cx| {
|
||||
completion_provider
|
||||
.set_active_provider(provider.clone(), cx)
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
menu
|
||||
})
|
||||
.into()
|
||||
})
|
||||
.trigger(
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.overflow_x_hidden()
|
||||
.flex_grow()
|
||||
.whitespace_nowrap()
|
||||
.child(
|
||||
Label::new(
|
||||
CompletionProvider::global(cx).model().display_name(),
|
||||
|
||||
let selected_model = LanguageModelCompletionProvider::read_global(cx)
|
||||
.active_model()
|
||||
.map(|m| m.id());
|
||||
let selected_provider = LanguageModelCompletionProvider::read_global(cx)
|
||||
.active_provider()
|
||||
.map(|m| m.id());
|
||||
|
||||
for available_model in available_models {
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
let id = available_model.id();
|
||||
let provider_id = available_model.provider_id();
|
||||
let model_name = available_model.name().0.clone();
|
||||
let selected_model = selected_model.clone();
|
||||
let selected_provider = selected_provider.clone();
|
||||
move |_| {
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(Label::new(model_name.clone()))
|
||||
.when(
|
||||
selected_model.as_ref() == Some(&id)
|
||||
&& selected_provider.as_ref() == Some(&provider_id),
|
||||
|this| this.child(Icon::new(IconName::Check)),
|
||||
)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
|
||||
}),
|
||||
)
|
||||
.attach(gpui::AnchorCorner::BottomLeft)
|
||||
.into_any()
|
||||
}
|
||||
},
|
||||
{
|
||||
let fs = self.fs.clone();
|
||||
let model = available_model.clone();
|
||||
move |cx| {
|
||||
let model = model.clone();
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
menu
|
||||
})
|
||||
.into()
|
||||
})
|
||||
.trigger(self.trigger)
|
||||
.attach(gpui::AnchorCorner::BottomLeft)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
slash_command::SlashCommandCompletionProvider, AssistantPanel, CompletionProvider,
|
||||
InlineAssist, InlineAssistant,
|
||||
slash_command::SlashCommandCompletionProvider, AssistantPanel, InlineAssist, InlineAssistant,
|
||||
LanguageModelCompletionProvider,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assets::Assets;
|
||||
@@ -636,9 +636,9 @@ impl PromptLibrary {
|
||||
};
|
||||
|
||||
let prompt_editor = &self.prompt_editors[&active_prompt_id].body_editor;
|
||||
let provider = CompletionProvider::global(cx);
|
||||
let provider = LanguageModelCompletionProvider::read_global(cx);
|
||||
let initial_prompt = action.prompt.clone();
|
||||
if provider.is_authenticated() {
|
||||
if provider.is_authenticated(cx) {
|
||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.assist(&prompt_editor, None, None, initial_prompt, cx)
|
||||
})
|
||||
@@ -736,11 +736,8 @@ impl PromptLibrary {
|
||||
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
|
||||
let token_count = cx
|
||||
.update(|cx| {
|
||||
let provider = CompletionProvider::global(cx);
|
||||
let model = provider.model();
|
||||
provider.count_tokens(
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(
|
||||
LanguageModelRequest {
|
||||
model,
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: body.to_string(),
|
||||
@@ -806,7 +803,7 @@ impl PromptLibrary {
|
||||
let prompt_metadata = self.store.metadata(prompt_id)?;
|
||||
let prompt_editor = &self.prompt_editors[&prompt_id];
|
||||
let focus_handle = prompt_editor.body_editor.focus_handle(cx);
|
||||
let current_model = CompletionProvider::global(cx).model();
|
||||
let current_model = LanguageModelCompletionProvider::read_global(cx).active_model();
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
|
||||
Some(
|
||||
@@ -917,7 +914,11 @@ impl PromptLibrary {
|
||||
format!(
|
||||
"Model: {}",
|
||||
current_model
|
||||
.display_name()
|
||||
.as_ref()
|
||||
.map(|model| model
|
||||
.name()
|
||||
.0)
|
||||
.unwrap_or_default()
|
||||
),
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -284,7 +284,7 @@ fn collect_diagnostics(
|
||||
PathBuf::try_from(path)
|
||||
.ok()
|
||||
.and_then(|path| {
|
||||
project.read(cx).worktrees().find_map(|worktree| {
|
||||
project.read(cx).worktrees(cx).find_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let worktree_root_path = Path::new(worktree.root_name());
|
||||
let relative_path = path.strip_prefix(worktree_root_path).ok()?;
|
||||
|
||||
@@ -24,7 +24,7 @@ impl DocsSlashCommand {
|
||||
pub const NAME: &'static str = "docs";
|
||||
|
||||
fn path_to_cargo_toml(project: Model<Project>, cx: &mut AppContext) -> Option<Arc<Path>> {
|
||||
let worktree = project.read(cx).worktrees().next()?;
|
||||
let worktree = project.read(cx).worktrees(cx).next()?;
|
||||
let worktree = worktree.read(cx);
|
||||
let entry = worktree.entry_for_path("Cargo.toml")?;
|
||||
let path = ProjectPath {
|
||||
|
||||
@@ -10,7 +10,7 @@ use assistant_slash_command::{
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
|
||||
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use language::LspAdapterDelegate;
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -188,7 +188,7 @@ fn collect_files(
|
||||
let project_handle = project.downgrade();
|
||||
let snapshots = project
|
||||
.read(cx)
|
||||
.worktrees()
|
||||
.worktrees(cx)
|
||||
.map(|worktree| worktree.read(cx).snapshot())
|
||||
.collect::<Vec<_>>();
|
||||
cx.spawn(|mut cx| async move {
|
||||
|
||||
@@ -75,7 +75,7 @@ impl ProjectSlashCommand {
|
||||
}
|
||||
|
||||
fn path_to_cargo_toml(project: Model<Project>, cx: &mut AppContext) -> Option<Arc<Path>> {
|
||||
let worktree = project.read(cx).worktrees().next()?;
|
||||
let worktree = project.read(cx).worktrees(cx).next()?;
|
||||
let worktree = worktree.read(cx);
|
||||
let entry = worktree.entry_for_path("Cargo.toml")?;
|
||||
let path = ProjectPath {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::{
|
||||
assistant_settings::AssistantSettings, humanize_token_count,
|
||||
prompts::generate_terminal_assistant_prompt, AssistantPanel, AssistantPanelEvent,
|
||||
CompletionProvider,
|
||||
humanize_token_count, prompts::generate_terminal_assistant_prompt, AssistantPanel,
|
||||
AssistantPanelEvent, LanguageModelCompletionProvider, ModelSelector,
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
@@ -18,7 +17,7 @@ use gpui::{
|
||||
};
|
||||
use language::Buffer;
|
||||
use language_model::{LanguageModelRequest, LanguageModelRequestMessage, Role};
|
||||
use settings::{update_settings_file, Settings};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
cmp,
|
||||
sync::Arc,
|
||||
@@ -27,7 +26,7 @@ use std::{
|
||||
use terminal::Terminal;
|
||||
use terminal_view::TerminalView;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, ContextMenu, IconButtonShape, PopoverMenu, Tooltip};
|
||||
use ui::{prelude::*, IconButtonShape, Tooltip};
|
||||
use util::ResultExt;
|
||||
use workspace::{notifications::NotificationId, Toast, Workspace};
|
||||
|
||||
@@ -215,8 +214,6 @@ impl TerminalInlineAssistant {
|
||||
) -> Result<LanguageModelRequest> {
|
||||
let assist = self.assists.get(&assist_id).context("invalid assist")?;
|
||||
|
||||
let model = CompletionProvider::global(cx).model();
|
||||
|
||||
let shell = std::env::var("SHELL").ok();
|
||||
let working_directory = assist
|
||||
.terminal
|
||||
@@ -268,7 +265,6 @@ impl TerminalInlineAssistant {
|
||||
});
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
model,
|
||||
messages,
|
||||
stop: Vec::new(),
|
||||
temperature: 1.0,
|
||||
@@ -451,22 +447,19 @@ impl EventEmitter<PromptEditorEvent> for PromptEditor {}
|
||||
|
||||
impl Render for PromptEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let fs = self.fs.clone();
|
||||
|
||||
let buttons = match &self.codegen.read(cx).status {
|
||||
CodegenStatus::Idle => {
|
||||
vec![
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::None)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
|
||||
),
|
||||
IconButton::new("start", IconName::Sparkle)
|
||||
IconButton::new("start", IconName::SparkleAlt)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::None)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::for_action("Generate", &menu::Confirm, cx))
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
|
||||
@@ -477,15 +470,14 @@ impl Render for PromptEditor {
|
||||
vec![
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::None)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::text("Cancel Assist", cx))
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
|
||||
),
|
||||
IconButton::new("stop", IconName::Stop)
|
||||
.icon_color(Color::Error)
|
||||
.size(ButtonSize::None)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::with_meta(
|
||||
"Interrupt Generation",
|
||||
@@ -503,7 +495,7 @@ impl Render for PromptEditor {
|
||||
vec![
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::None)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
|
||||
@@ -511,8 +503,7 @@ impl Render for PromptEditor {
|
||||
if self.edited_since_done {
|
||||
IconButton::new("restart", IconName::RotateCw)
|
||||
.icon_color(Color::Info)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.size(ButtonSize::None)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::with_meta(
|
||||
"Restart Generation",
|
||||
@@ -527,7 +518,7 @@ impl Render for PromptEditor {
|
||||
} else {
|
||||
IconButton::new("confirm", IconName::Play)
|
||||
.icon_color(Color::Info)
|
||||
.size(ButtonSize::None)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::for_action("Execute generated command", &menu::Confirm, cx)
|
||||
})
|
||||
@@ -555,58 +546,27 @@ impl Render for PromptEditor {
|
||||
.w_12()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
PopoverMenu::new("model-switcher")
|
||||
.menu(move |cx| {
|
||||
ContextMenu::build(cx, |mut menu, cx| {
|
||||
for model in CompletionProvider::global(cx).available_models() {
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
let model = model.clone();
|
||||
move |_| {
|
||||
Label::new(model.display_name())
|
||||
.into_any_element()
|
||||
}
|
||||
},
|
||||
{
|
||||
let fs = fs.clone();
|
||||
let model = model.clone();
|
||||
move |cx| {
|
||||
let model = model.clone();
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings| settings.set_model(model),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
menu
|
||||
})
|
||||
.into()
|
||||
})
|
||||
.trigger(
|
||||
IconButton::new("context", IconName::Settings)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"Using {}",
|
||||
CompletionProvider::global(cx)
|
||||
.model()
|
||||
.display_name()
|
||||
),
|
||||
None,
|
||||
"Change Model",
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.anchor(gpui::AnchorCorner::BottomRight),
|
||||
)
|
||||
.child(ModelSelector::new(
|
||||
self.fs.clone(),
|
||||
IconButton::new("context", IconName::Settings)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"Using {}",
|
||||
LanguageModelCompletionProvider::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());
|
||||
@@ -748,7 +708,9 @@ impl PromptEditor {
|
||||
})??;
|
||||
|
||||
let token_count = cx
|
||||
.update(|cx| CompletionProvider::global(cx).count_tokens(request, cx))?
|
||||
.update(|cx| {
|
||||
LanguageModelCompletionProvider::read_global(cx).count_tokens(request, cx)
|
||||
})?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.token_count = Some(token_count);
|
||||
@@ -878,7 +840,7 @@ impl PromptEditor {
|
||||
}
|
||||
|
||||
fn render_token_count(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
|
||||
let model = CompletionProvider::global(cx).model();
|
||||
let model = LanguageModelCompletionProvider::read_global(cx).active_model()?;
|
||||
let token_count = self.token_count?;
|
||||
let max_token_count = model.max_token_count();
|
||||
|
||||
@@ -1023,8 +985,12 @@ impl Codegen {
|
||||
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
|
||||
|
||||
let telemetry = self.telemetry.clone();
|
||||
let model_telemetry_id = prompt.model.telemetry_id();
|
||||
let response = CompletionProvider::global(cx).stream_completion(prompt, cx);
|
||||
let model_telemetry_id = LanguageModelCompletionProvider::read_global(cx)
|
||||
.active_model()
|
||||
.map(|m| m.telemetry_id())
|
||||
.unwrap_or_default();
|
||||
let response =
|
||||
LanguageModelCompletionProvider::read_global(cx).stream_completion(prompt, cx);
|
||||
|
||||
self.generation = cx.spawn(|this, mut cx| async move {
|
||||
let response = response.await;
|
||||
|
||||
@@ -222,7 +222,7 @@ mod tests {
|
||||
|
||||
let worktree_ids = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.worktrees()
|
||||
.worktrees(cx)
|
||||
.map(|worktree| worktree.read(cx).id())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ client.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
gpui.workspace = true
|
||||
http.workspace = true
|
||||
http_client.workspace = true
|
||||
isahc.workspace = true
|
||||
log.workspace = true
|
||||
markdown_preview.workspace = true
|
||||
|
||||
@@ -20,7 +20,7 @@ use smol::{fs, io::AsyncReadExt};
|
||||
use settings::{Settings, SettingsSources, SettingsStore};
|
||||
use smol::{fs::File, process::Command};
|
||||
|
||||
use http::{HttpClient, HttpClientWithUrl};
|
||||
use http_client::{HttpClient, HttpClientWithUrl};
|
||||
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
||||
use std::{
|
||||
env::{
|
||||
|
||||
@@ -51,4 +51,4 @@ language = { workspace = true, features = ["test-support"] }
|
||||
live_kit_client = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
http = { workspace = true, features = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -526,7 +526,7 @@ impl Room {
|
||||
rejoined_projects.push(proto::RejoinProject {
|
||||
id: project_id,
|
||||
worktrees: project
|
||||
.worktrees()
|
||||
.worktrees(cx)
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
proto::RejoinWorktree {
|
||||
|
||||
@@ -40,4 +40,4 @@ rpc = { workspace = true, features = ["test-support"] }
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
http = { workspace = true, features = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -4,7 +4,7 @@ use super::*;
|
||||
use client::{test::FakeServer, Client, UserStore};
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::{AppContext, Context, Model, SemanticVersion, TestAppContext};
|
||||
use http::FakeHttpClient;
|
||||
use http_client::FakeHttpClient;
|
||||
use rpc::proto::{self};
|
||||
use settings::SettingsStore;
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ test-support = ["clock/test-support", "collections/test-support", "gpui/test-sup
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-recursion = "0.3"
|
||||
async-tungstenite = { version = "0.16", features = ["async-std", "async-native-tls"] }
|
||||
async-tungstenite = { workspace = true, features = ["async-std", "async-native-tls"] }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
@@ -26,7 +26,7 @@ feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http.workspace = true
|
||||
http_client.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
once_cell.workspace = true
|
||||
@@ -60,12 +60,11 @@ gpui = { workspace = true, features = ["test-support"] }
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
http = { workspace = true, features = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cocoa.workspace = true
|
||||
isahc = { workspace = true, features = ["static-curl"] }
|
||||
async-native-tls = { version = "0.5.0", features = ["vendored"] }
|
||||
|
||||
@@ -20,7 +20,7 @@ use futures::{
|
||||
use gpui::{
|
||||
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, Task, WeakModel,
|
||||
};
|
||||
use http::{HttpClient, HttpClientWithUrl};
|
||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
use postage::watch;
|
||||
@@ -233,7 +233,7 @@ pub enum EstablishConnectionError {
|
||||
#[error("{0}")]
|
||||
Other(#[from] anyhow::Error),
|
||||
#[error("{0}")]
|
||||
Http(#[from] http::Error),
|
||||
Http(#[from] http_client::Error),
|
||||
#[error("{0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("{0}")]
|
||||
@@ -1351,7 +1351,7 @@ impl Client {
|
||||
let mut url = self.rpc_url(http.clone(), None).await?;
|
||||
url.set_path("/user");
|
||||
url.set_query(Some(&format!("github_login={login}")));
|
||||
let request = Request::get(url.as_str())
|
||||
let request: http_client::Request<AsyncBody> = Request::get(url.as_str())
|
||||
.header("Authorization", format!("token {api_token}"))
|
||||
.body("".into())?;
|
||||
|
||||
@@ -1410,7 +1410,7 @@ impl Client {
|
||||
self.peer.send(self.connection_id()?, message)
|
||||
}
|
||||
|
||||
fn send_dynamic(&self, envelope: proto::Envelope) -> Result<()> {
|
||||
pub fn send_dynamic(&self, envelope: proto::Envelope) -> Result<()> {
|
||||
let connection_id = self.connection_id()?;
|
||||
self.peer.send_dynamic(connection_id, envelope)
|
||||
}
|
||||
@@ -1783,7 +1783,7 @@ mod tests {
|
||||
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::{BackgroundExecutor, Context, TestAppContext};
|
||||
use http::FakeHttpClient;
|
||||
use http_client::FakeHttpClient;
|
||||
use parking_lot::Mutex;
|
||||
use proto::TypedEnvelope;
|
||||
use settings::SettingsStore;
|
||||
|
||||
@@ -6,7 +6,7 @@ use clock::SystemClock;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::Future;
|
||||
use gpui::{AppContext, BackgroundExecutor, Task};
|
||||
use http::{self, HttpClient, HttpClientWithUrl, Method};
|
||||
use http_client::{self, HttpClient, HttpClientWithUrl, Method};
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::Mutex;
|
||||
use release_channel::ReleaseChannel;
|
||||
@@ -632,7 +632,7 @@ impl Telemetry {
|
||||
|
||||
let checksum = calculate_json_checksum(&json_bytes).unwrap_or("".to_string());
|
||||
|
||||
let request = http::Request::builder()
|
||||
let request = http_client::Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri(
|
||||
this.http_client
|
||||
@@ -661,7 +661,7 @@ mod tests {
|
||||
use chrono::TimeZone;
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::TestAppContext;
|
||||
use http::FakeHttpClient;
|
||||
use http_client::FakeHttpClient;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) {
|
||||
|
||||
@@ -20,7 +20,7 @@ test-support = ["sqlite"]
|
||||
[dependencies]
|
||||
anthropic.workspace = true
|
||||
anyhow.workspace = true
|
||||
async-tungstenite = "0.16"
|
||||
async-tungstenite.workspace = true
|
||||
aws-config = { version = "1.1.5" }
|
||||
aws-sdk-s3 = { version = "1.15.0" }
|
||||
axum = { version = "0.6", features = ["json", "headers", "ws"] }
|
||||
@@ -30,13 +30,12 @@ chrono.workspace = true
|
||||
clock.workspace = true
|
||||
clickhouse.workspace = true
|
||||
collections.workspace = true
|
||||
completion.workspace = true
|
||||
dashmap = "5.4"
|
||||
envy = "0.4.2"
|
||||
futures.workspace = true
|
||||
google_ai.workspace = true
|
||||
hex.workspace = true
|
||||
http.workspace = true
|
||||
http_client.workspace = true
|
||||
live_kit_server.workspace = true
|
||||
log.workspace = true
|
||||
nanoid.workspace = true
|
||||
@@ -91,6 +90,7 @@ git_hosting_providers.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
language_model = { workspace = true, features = ["test-support"] }
|
||||
live_kit_client = { workspace = true, features = ["test-support"] }
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
menu.workspace = true
|
||||
@@ -107,6 +107,7 @@ dev_server_projects.workspace = true
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
sea-orm = { version = "0.12.x", features = ["sqlx-sqlite"] }
|
||||
serde_json.workspace = true
|
||||
session = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
sqlx = { version = "0.7", features = ["sqlite"] }
|
||||
theme.workspace = true
|
||||
|
||||
@@ -164,10 +164,21 @@ pub fn hash_access_token(token: &str) -> String {
|
||||
/// Encrypts the given access token with the given public key to avoid leaking it on the way
|
||||
/// to the client.
|
||||
pub fn encrypt_access_token(access_token: &str, public_key: String) -> Result<String> {
|
||||
use rpc::auth::EncryptionFormat;
|
||||
|
||||
/// The encryption format to use for the access token.
|
||||
///
|
||||
/// Currently we're using the original encryption format to avoid
|
||||
/// breaking compatibility with older clients.
|
||||
///
|
||||
/// Once enough clients are capable of decrypting the newer encryption
|
||||
/// format we can start encrypting with `EncryptionFormat::V1`.
|
||||
const ENCRYPTION_FORMAT: EncryptionFormat = EncryptionFormat::V0;
|
||||
|
||||
let native_app_public_key =
|
||||
rpc::auth::PublicKey::try_from(public_key).context("failed to parse app public key")?;
|
||||
let encrypted_access_token = native_app_public_key
|
||||
.encrypt_string(access_token)
|
||||
.encrypt_string(access_token, ENCRYPTION_FORMAT)
|
||||
.context("failed to encrypt access token with public key")?;
|
||||
Ok(encrypted_access_token)
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ use futures::{
|
||||
stream::FuturesUnordered,
|
||||
FutureExt, SinkExt, StreamExt, TryStreamExt,
|
||||
};
|
||||
use http::IsahcHttpClient;
|
||||
use http_client::IsahcHttpClient;
|
||||
use prometheus::{register_int_gauge, IntGauge};
|
||||
use rpc::{
|
||||
proto::{
|
||||
|
||||
@@ -52,7 +52,7 @@ async fn test_channel_guests(
|
||||
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
|
||||
assert!(project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
let worktree_id = project.worktrees().next().unwrap().read(cx).id();
|
||||
let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
|
||||
project.create_entry((worktree_id, "b.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -76,7 +76,7 @@ async fn test_host_disconnect(
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
|
||||
let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
|
||||
let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
@@ -1144,7 +1144,7 @@ async fn test_share_project(
|
||||
});
|
||||
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
let worktree = project.worktrees().next().unwrap().read(cx);
|
||||
let worktree = project.worktrees(cx).next().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
|
||||
[
|
||||
@@ -1158,7 +1158,7 @@ async fn test_share_project(
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
let worktree = project.worktrees().next().unwrap();
|
||||
let worktree = project.worktrees(cx).next().unwrap();
|
||||
let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
|
||||
project.expand_entry(worktree_id, entry.id, cx).unwrap()
|
||||
})
|
||||
@@ -1166,7 +1166,7 @@ async fn test_share_project(
|
||||
.unwrap();
|
||||
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
let worktree = project.worktrees().next().unwrap().read(cx);
|
||||
let worktree = project.worktrees(cx).next().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
|
||||
[
|
||||
|
||||
@@ -18,7 +18,9 @@ use gpui::{
|
||||
TestAppContext, UpdateGlobal,
|
||||
};
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, Formatter, PrettierSettings},
|
||||
language_settings::{
|
||||
AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
|
||||
},
|
||||
tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig,
|
||||
LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
|
||||
};
|
||||
@@ -1375,7 +1377,7 @@ async fn test_unshare_project(
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
|
||||
let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||
executor.run_until_parked();
|
||||
|
||||
@@ -1503,7 +1505,8 @@ async fn test_project_reconnect(
|
||||
let (project_a1, _) = client_a.build_local_project("/root-1/dir1", cx_a).await;
|
||||
let (project_a2, _) = client_a.build_local_project("/root-2", cx_a).await;
|
||||
let (project_a3, _) = client_a.build_local_project("/root-3", cx_a).await;
|
||||
let worktree_a1 = project_a1.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
|
||||
let worktree_a1 =
|
||||
project_a1.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
let project1_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a1.clone(), cx))
|
||||
.await
|
||||
@@ -2306,7 +2309,7 @@ async fn test_propagate_saves_and_fs_changes(
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
|
||||
let worktree_a = project_a.read_with(cx_a, |p, _| p.worktrees().next().unwrap());
|
||||
let worktree_a = project_a.read_with(cx_a, |p, cx| p.worktrees(cx).next().unwrap());
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
@@ -2316,9 +2319,9 @@ async fn test_propagate_saves_and_fs_changes(
|
||||
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||
let project_c = client_c.build_dev_server_project(project_id, cx_c).await;
|
||||
|
||||
let worktree_b = project_b.read_with(cx_b, |p, _| p.worktrees().next().unwrap());
|
||||
let worktree_b = project_b.read_with(cx_b, |p, cx| p.worktrees(cx).next().unwrap());
|
||||
|
||||
let worktree_c = project_c.read_with(cx_c, |p, _| p.worktrees().next().unwrap());
|
||||
let worktree_c = project_c.read_with(cx_c, |p, cx| p.worktrees(cx).next().unwrap());
|
||||
|
||||
// Open and edit a buffer as both guests B and C.
|
||||
let buffer_b = project_b
|
||||
@@ -3020,8 +3023,8 @@ async fn test_fs_operations(
|
||||
.unwrap();
|
||||
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||
|
||||
let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
|
||||
let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap());
|
||||
let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
|
||||
let entry = project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
@@ -3321,7 +3324,7 @@ async fn test_local_settings(
|
||||
// As client B, join that project and observe the local settings.
|
||||
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||
|
||||
let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap());
|
||||
let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
executor.run_until_parked();
|
||||
cx_b.read(|cx| {
|
||||
let store = cx.global::<SettingsStore>();
|
||||
@@ -3733,7 +3736,7 @@ async fn test_leaving_project(
|
||||
// Client B opens a buffer.
|
||||
let buffer_b1 = project_b1
|
||||
.update(cx_b, |project, cx| {
|
||||
let worktree_id = project.worktrees().next().unwrap().read(cx).id();
|
||||
let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
|
||||
project.open_buffer((worktree_id, "a.txt"), cx)
|
||||
})
|
||||
.await
|
||||
@@ -3771,7 +3774,7 @@ async fn test_leaving_project(
|
||||
|
||||
let buffer_b2 = project_b2
|
||||
.update(cx_b, |project, cx| {
|
||||
let worktree_id = project.worktrees().next().unwrap().read(cx).id();
|
||||
let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
|
||||
project.open_buffer((worktree_id, "a.txt"), cx)
|
||||
})
|
||||
.await
|
||||
@@ -4409,10 +4412,13 @@ async fn test_formatting_buffer(
|
||||
cx_a.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
|
||||
file.defaults.formatter = Some(Formatter::External {
|
||||
command: "awk".into(),
|
||||
arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
|
||||
});
|
||||
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
|
||||
vec![Formatter::External {
|
||||
command: "awk".into(),
|
||||
arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
|
||||
}]
|
||||
.into(),
|
||||
)));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4493,7 +4499,7 @@ async fn test_prettier_formatting_buffer(
|
||||
cx_a.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
|
||||
file.defaults.formatter = Some(Formatter::Auto);
|
||||
file.defaults.formatter = Some(SelectedFormatter::Auto);
|
||||
file.defaults.prettier = Some(PrettierSettings {
|
||||
allowed: true,
|
||||
..PrettierSettings::default()
|
||||
@@ -4504,7 +4510,9 @@ async fn test_prettier_formatting_buffer(
|
||||
cx_b.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
|
||||
file.defaults.formatter = Some(Formatter::LanguageServer);
|
||||
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
|
||||
vec![Formatter::LanguageServer { name: None }].into(),
|
||||
)));
|
||||
file.defaults.prettier = Some(PrettierSettings {
|
||||
allowed: true,
|
||||
..PrettierSettings::default()
|
||||
@@ -4620,7 +4628,7 @@ async fn test_definition(
|
||||
.unwrap();
|
||||
cx_b.read(|cx| {
|
||||
assert_eq!(definitions_1.len(), 1);
|
||||
assert_eq!(project_b.read(cx).worktrees().count(), 2);
|
||||
assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
|
||||
let target_buffer = definitions_1[0].target.buffer.read(cx);
|
||||
assert_eq!(
|
||||
target_buffer.text(),
|
||||
@@ -4649,7 +4657,7 @@ async fn test_definition(
|
||||
.unwrap();
|
||||
cx_b.read(|cx| {
|
||||
assert_eq!(definitions_2.len(), 1);
|
||||
assert_eq!(project_b.read(cx).worktrees().count(), 2);
|
||||
assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
|
||||
let target_buffer = definitions_2[0].target.buffer.read(cx);
|
||||
assert_eq!(
|
||||
target_buffer.text(),
|
||||
@@ -4807,7 +4815,7 @@ async fn test_references(
|
||||
assert!(status.pending_work.is_empty());
|
||||
|
||||
assert_eq!(references.len(), 3);
|
||||
assert_eq!(project.worktrees().count(), 2);
|
||||
assert_eq!(project.worktrees(cx).count(), 2);
|
||||
|
||||
let two_buffer = references[0].buffer.read(cx);
|
||||
let three_buffer = references[2].buffer.read(cx);
|
||||
@@ -6192,7 +6200,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
let project = workspace.update(cx, |workspace, _| workspace.project().clone());
|
||||
|
||||
let worktree_id = project.update(cx, |project, cx| {
|
||||
project.worktrees().next().unwrap().read(cx).id()
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
});
|
||||
|
||||
let path_1 = ProjectPath {
|
||||
|
||||
@@ -301,7 +301,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
let is_local = project.read_with(cx, |project, _| project.is_local());
|
||||
let worktree = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.worktrees()
|
||||
.worktrees(cx)
|
||||
.filter(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
worktree.is_visible()
|
||||
@@ -423,7 +423,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
81.. => {
|
||||
let worktree = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.worktrees()
|
||||
.worktrees(cx)
|
||||
.filter(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
worktree.is_visible()
|
||||
@@ -1172,7 +1172,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
let host_worktree_snapshots =
|
||||
host_project.read_with(host_cx, |host_project, cx| {
|
||||
host_project
|
||||
.worktrees()
|
||||
.worktrees(cx)
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
(worktree.id(), worktree.snapshot())
|
||||
@@ -1180,7 +1180,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
.collect::<BTreeMap<_, _>>()
|
||||
});
|
||||
let guest_worktree_snapshots = guest_project
|
||||
.worktrees()
|
||||
.worktrees(cx)
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
(worktree.id(), worktree.snapshot())
|
||||
@@ -1538,7 +1538,7 @@ fn project_path_for_full_path(
|
||||
let root_name = components.next().unwrap().as_os_str().to_str().unwrap();
|
||||
let path = components.as_path().into();
|
||||
let worktree_id = project.read_with(cx, |project, cx| {
|
||||
project.worktrees().find_map(|worktree| {
|
||||
project.worktrees(cx).find_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
if worktree.root_name() == root_name {
|
||||
Some(worktree.id())
|
||||
|
||||
@@ -19,7 +19,7 @@ use fs::FakeFs;
|
||||
use futures::{channel::oneshot, StreamExt as _};
|
||||
use git::GitHostingProviderRegistry;
|
||||
use gpui::{BackgroundExecutor, Context, Model, Task, TestAppContext, View, VisualTestContext};
|
||||
use http::FakeHttpClient;
|
||||
use http_client::FakeHttpClient;
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
use notifications::NotificationStore;
|
||||
@@ -32,6 +32,7 @@ use rpc::{
|
||||
};
|
||||
use semantic_version::SemanticVersion;
|
||||
use serde_json::json;
|
||||
use session::Session;
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
@@ -156,6 +157,8 @@ impl TestServer {
|
||||
}
|
||||
|
||||
pub async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
cx.update(|cx| {
|
||||
if cx.has_global::<SettingsStore>() {
|
||||
panic!("Same cx used to create two test clients")
|
||||
@@ -264,7 +267,6 @@ impl TestServer {
|
||||
git_hosting_provider_registry
|
||||
.register_hosting_provider(Arc::new(git_hosting_providers::Github));
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
@@ -276,6 +278,7 @@ impl TestServer {
|
||||
fs: fs.clone(),
|
||||
build_window_options: |_, _| Default::default(),
|
||||
node_runtime: FakeNodeRuntime::new(),
|
||||
session: Session::test(),
|
||||
});
|
||||
|
||||
let os_keymap = "keymaps/default-macos.json";
|
||||
@@ -295,7 +298,8 @@ impl TestServer {
|
||||
menu::init();
|
||||
dev_server_projects::init(client.clone(), cx);
|
||||
settings::KeymapFile::load_asset(os_keymap, cx).unwrap();
|
||||
completion::FakeCompletionProvider::setup_test(cx);
|
||||
language_model::LanguageModelRegistry::test(cx);
|
||||
completion::init(cx);
|
||||
assistant::context_store::init(&client);
|
||||
});
|
||||
|
||||
@@ -403,6 +407,7 @@ impl TestServer {
|
||||
fs: fs.clone(),
|
||||
build_window_options: |_, _| Default::default(),
|
||||
node_runtime: FakeNodeRuntime::new(),
|
||||
session: Session::test(),
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
|
||||
@@ -25,7 +25,7 @@ test-support = [
|
||||
"settings/test-support",
|
||||
"util/test-support",
|
||||
"workspace/test-support",
|
||||
"http/test-support",
|
||||
"http_client/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
@@ -78,7 +78,7 @@ pretty_assertions.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
tree-sitter-markdown.workspace = true
|
||||
tree-sitter-md.workspace = true
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
http = { workspace = true, features = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1107,9 +1107,11 @@ impl Panel for ChatPanel {
|
||||
}
|
||||
|
||||
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
|
||||
settings::update_settings_file::<ChatPanelSettings>(self.fs.clone(), cx, move |settings| {
|
||||
settings.dock = Some(position)
|
||||
});
|
||||
settings::update_settings_file::<ChatPanelSettings>(
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.dock = Some(position),
|
||||
);
|
||||
}
|
||||
|
||||
fn size(&self, cx: &gpui::WindowContext) -> Pixels {
|
||||
|
||||
@@ -2806,7 +2806,7 @@ impl Panel for CollabPanel {
|
||||
settings::update_settings_file::<CollaborationPanelSettings>(
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
move |settings| settings.dock = Some(position),
|
||||
move |settings, _| settings.dock = Some(position),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -672,7 +672,7 @@ impl Panel for NotificationPanel {
|
||||
settings::update_settings_file::<NotificationPanelSettings>(
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
move |settings| settings.dock = Some(position),
|
||||
move |settings, _| settings.dock = Some(position),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,34 +16,20 @@ doctest = false
|
||||
test-support = [
|
||||
"editor/test-support",
|
||||
"language/test-support",
|
||||
"language_model/test-support",
|
||||
"project/test-support",
|
||||
"text/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anthropic = { workspace = true, features = ["schemars"] }
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http.workspace = true
|
||||
language_model.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
ollama = { workspace = true, features = ["schemars"] }
|
||||
open_ai = { workspace = true, features = ["schemars"] }
|
||||
parking_lot.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
strum.workspace = true
|
||||
theme.workspace = true
|
||||
tiktoken-rs.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
@@ -51,6 +37,7 @@ editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
language_model = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
text = { workspace = true, features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
|
||||
@@ -1,318 +0,0 @@
|
||||
use crate::{count_open_ai_tokens, LanguageModelCompletionProvider};
|
||||
use crate::{CompletionProvider, LanguageModel, LanguageModelRequest};
|
||||
use anthropic::{stream_completion, Model as AnthropicModel, Request, RequestMessage};
|
||||
use anyhow::{anyhow, Result};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
use gpui::{AnyView, AppContext, Task, TextStyle, View};
|
||||
use http::HttpClient;
|
||||
use language_model::Role;
|
||||
use settings::Settings;
|
||||
use std::time::Duration;
|
||||
use std::{env, sync::Arc};
|
||||
use strum::IntoEnumIterator;
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct AnthropicCompletionProvider {
|
||||
api_key: Option<String>,
|
||||
api_url: String,
|
||||
model: AnthropicModel,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
settings_version: usize,
|
||||
}
|
||||
|
||||
impl LanguageModelCompletionProvider for AnthropicCompletionProvider {
|
||||
fn available_models(&self) -> Vec<LanguageModel> {
|
||||
AnthropicModel::iter()
|
||||
.map(LanguageModel::Anthropic)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn settings_version(&self) -> usize {
|
||||
self.settings_version
|
||||
}
|
||||
|
||||
fn is_authenticated(&self) -> bool {
|
||||
self.api_key.is_some()
|
||||
}
|
||||
|
||||
fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
if self.is_authenticated() {
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
let api_url = self.api_url.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let api_key = if let Ok(api_key) = env::var("ANTHROPIC_API_KEY") {
|
||||
api_key
|
||||
} else {
|
||||
let (_, api_key) = cx
|
||||
.update(|cx| cx.read_credentials(&api_url))?
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("credentials not found"))?;
|
||||
String::from_utf8(api_key)?
|
||||
};
|
||||
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
|
||||
provider.update_current_as::<_, AnthropicCompletionProvider>(|provider| {
|
||||
provider.api_key = Some(api_key);
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
let delete_credentials = cx.delete_credentials(&self.api_url);
|
||||
cx.spawn(|mut cx| async move {
|
||||
delete_credentials.await.log_err();
|
||||
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
|
||||
provider.update_current_as::<_, AnthropicCompletionProvider>(|provider| {
|
||||
provider.api_key = None;
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
cx.new_view(|cx| AuthenticationPrompt::new(self.api_url.clone(), cx))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn model(&self) -> LanguageModel {
|
||||
LanguageModel::Anthropic(self.model.clone())
|
||||
}
|
||||
|
||||
fn count_tokens(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<usize>> {
|
||||
count_open_ai_tokens(request, cx.background_executor())
|
||||
}
|
||||
|
||||
fn stream_completion(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
let request = self.to_anthropic_request(request);
|
||||
|
||||
let http_client = self.http_client.clone();
|
||||
let api_key = self.api_key.clone();
|
||||
let api_url = self.api_url.clone();
|
||||
let low_speed_timeout = self.low_speed_timeout;
|
||||
async move {
|
||||
let api_key = api_key.ok_or_else(|| anyhow!("missing api key"))?;
|
||||
let request = stream_completion(
|
||||
http_client.as_ref(),
|
||||
&api_url,
|
||||
&api_key,
|
||||
request,
|
||||
low_speed_timeout,
|
||||
);
|
||||
let response = request.await?;
|
||||
let stream = response
|
||||
.filter_map(|response| async move {
|
||||
match response {
|
||||
Ok(response) => match response {
|
||||
anthropic::ResponseEvent::ContentBlockStart {
|
||||
content_block, ..
|
||||
} => match content_block {
|
||||
anthropic::ContentBlock::Text { text } => Some(Ok(text)),
|
||||
},
|
||||
anthropic::ResponseEvent::ContentBlockDelta { delta, .. } => {
|
||||
match delta {
|
||||
anthropic::TextDelta::TextDelta { text } => Some(Ok(text)),
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
Err(error) => Some(Err(error)),
|
||||
}
|
||||
})
|
||||
.boxed();
|
||||
Ok(stream)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AnthropicCompletionProvider {
|
||||
pub fn new(
|
||||
model: AnthropicModel,
|
||||
api_url: String,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
settings_version: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
api_key: None,
|
||||
api_url,
|
||||
model,
|
||||
http_client,
|
||||
low_speed_timeout,
|
||||
settings_version,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
model: AnthropicModel,
|
||||
api_url: String,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
settings_version: usize,
|
||||
) {
|
||||
self.model = model;
|
||||
self.api_url = api_url;
|
||||
self.low_speed_timeout = low_speed_timeout;
|
||||
self.settings_version = settings_version;
|
||||
}
|
||||
|
||||
fn to_anthropic_request(&self, mut request: LanguageModelRequest) -> Request {
|
||||
request.preprocess_anthropic();
|
||||
|
||||
let model = match request.model {
|
||||
LanguageModel::Anthropic(model) => model,
|
||||
_ => self.model.clone(),
|
||||
};
|
||||
|
||||
let mut system_message = String::new();
|
||||
if request
|
||||
.messages
|
||||
.first()
|
||||
.map_or(false, |message| message.role == Role::System)
|
||||
{
|
||||
system_message = request.messages.remove(0).content;
|
||||
}
|
||||
|
||||
Request {
|
||||
model,
|
||||
messages: request
|
||||
.messages
|
||||
.iter()
|
||||
.map(|msg| RequestMessage {
|
||||
role: match msg.role {
|
||||
Role::User => anthropic::Role::User,
|
||||
Role::Assistant => anthropic::Role::Assistant,
|
||||
Role::System => unreachable!("filtered out by preprocess_request"),
|
||||
},
|
||||
content: msg.content.clone(),
|
||||
})
|
||||
.collect(),
|
||||
stream: true,
|
||||
system: system_message,
|
||||
max_tokens: 4092,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthenticationPrompt {
|
||||
api_key: View<Editor>,
|
||||
api_url: String,
|
||||
}
|
||||
|
||||
impl AuthenticationPrompt {
|
||||
fn new(api_url: String, cx: &mut WindowContext) -> Self {
|
||||
Self {
|
||||
api_key: cx.new_view(|cx| {
|
||||
let mut editor = Editor::single_line(cx);
|
||||
editor.set_placeholder_text(
|
||||
"sk-000000000000000000000000000000000000000000000000",
|
||||
cx,
|
||||
);
|
||||
editor
|
||||
}),
|
||||
api_url,
|
||||
}
|
||||
}
|
||||
|
||||
fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||
let api_key = self.api_key.read(cx).text(cx);
|
||||
if api_key.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let write_credentials = cx.write_credentials(&self.api_url, "Bearer", api_key.as_bytes());
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
write_credentials.await?;
|
||||
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
|
||||
provider.update_current_as::<_, AnthropicCompletionProvider>(|provider| {
|
||||
provider.api_key = Some(api_key);
|
||||
});
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn render_api_key_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
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: rems(0.875).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: relative(1.3),
|
||||
..Default::default()
|
||||
};
|
||||
EditorElement::new(
|
||||
&self.api_key,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AuthenticationPrompt {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
const INSTRUCTIONS: [&str; 4] = [
|
||||
"To use the assistant panel or inline assistant, you need to add your Anthropic API key.",
|
||||
"You can create an API key at: https://console.anthropic.com/settings/keys",
|
||||
"",
|
||||
"Paste your Anthropic API key below and hit enter to use the assistant:",
|
||||
];
|
||||
|
||||
v_flex()
|
||||
.p_4()
|
||||
.size_full()
|
||||
.on_action(cx.listener(Self::save_api_key))
|
||||
.children(
|
||||
INSTRUCTIONS.map(|instruction| Label::new(instruction).size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.my_2()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_md()
|
||||
.child(self.render_api_key_editor(cx)),
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
"You can also assign the ANTHROPIC_API_KEY environment variable and restart Zed.",
|
||||
)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Label::new("Click on").size(LabelSize::Small))
|
||||
.child(Icon::new(IconName::ZedAssistant).size(IconSize::XSmall))
|
||||
.child(
|
||||
Label::new("in the status bar to close this panel.").size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
use crate::{
|
||||
count_open_ai_tokens, CompletionProvider, LanguageModel, LanguageModelCompletionProvider,
|
||||
LanguageModelRequest,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::{proto, Client};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt, TryFutureExt};
|
||||
use gpui::{AnyView, AppContext, Task};
|
||||
use language_model::CloudModel;
|
||||
use std::{future, sync::Arc};
|
||||
use strum::IntoEnumIterator;
|
||||
use ui::prelude::*;
|
||||
|
||||
pub struct CloudCompletionProvider {
|
||||
client: Arc<Client>,
|
||||
model: CloudModel,
|
||||
settings_version: usize,
|
||||
status: client::Status,
|
||||
_maintain_client_status: Task<()>,
|
||||
}
|
||||
|
||||
impl CloudCompletionProvider {
|
||||
pub fn new(
|
||||
model: CloudModel,
|
||||
client: Arc<Client>,
|
||||
settings_version: usize,
|
||||
cx: &mut AppContext,
|
||||
) -> Self {
|
||||
let mut status_rx = client.status();
|
||||
let status = *status_rx.borrow();
|
||||
let maintain_client_status = cx.spawn(|mut cx| async move {
|
||||
while let Some(status) = status_rx.next().await {
|
||||
let _ = cx.update_global::<CompletionProvider, _>(|provider, _cx| {
|
||||
provider.update_current_as::<_, Self>(|provider| {
|
||||
provider.status = status;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
Self {
|
||||
client,
|
||||
model,
|
||||
settings_version,
|
||||
status,
|
||||
_maintain_client_status: maintain_client_status,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, model: CloudModel, settings_version: usize) {
|
||||
self.model = model;
|
||||
self.settings_version = settings_version;
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelCompletionProvider for CloudCompletionProvider {
|
||||
fn available_models(&self) -> Vec<LanguageModel> {
|
||||
let mut custom_model = if matches!(self.model, CloudModel::Custom { .. }) {
|
||||
Some(self.model.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
CloudModel::iter()
|
||||
.filter_map(move |model| {
|
||||
if let CloudModel::Custom { .. } = model {
|
||||
custom_model.take()
|
||||
} else {
|
||||
Some(model)
|
||||
}
|
||||
})
|
||||
.map(LanguageModel::Cloud)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn settings_version(&self) -> usize {
|
||||
self.settings_version
|
||||
}
|
||||
|
||||
fn is_authenticated(&self) -> bool {
|
||||
self.status.is_connected()
|
||||
}
|
||||
|
||||
fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
cx.spawn(move |cx| async move { client.authenticate_and_connect(true, &cx).await })
|
||||
}
|
||||
|
||||
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
cx.new_view(|_cx| AuthenticationPrompt).into()
|
||||
}
|
||||
|
||||
fn reset_credentials(&self, _cx: &AppContext) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn model(&self) -> LanguageModel {
|
||||
LanguageModel::Cloud(self.model.clone())
|
||||
}
|
||||
|
||||
fn count_tokens(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<usize>> {
|
||||
match request.model {
|
||||
LanguageModel::Cloud(CloudModel::Gpt4)
|
||||
| LanguageModel::Cloud(CloudModel::Gpt4Turbo)
|
||||
| LanguageModel::Cloud(CloudModel::Gpt4Omni)
|
||||
| LanguageModel::Cloud(CloudModel::Gpt3Point5Turbo) => {
|
||||
count_open_ai_tokens(request, cx.background_executor())
|
||||
}
|
||||
LanguageModel::Cloud(
|
||||
CloudModel::Claude3_5Sonnet
|
||||
| CloudModel::Claude3Opus
|
||||
| CloudModel::Claude3Sonnet
|
||||
| CloudModel::Claude3Haiku,
|
||||
) => {
|
||||
// Can't find a tokenizer for Claude 3, so for now just use the same as OpenAI's as an approximation.
|
||||
count_open_ai_tokens(request, cx.background_executor())
|
||||
}
|
||||
LanguageModel::Cloud(CloudModel::Custom { name, .. }) => {
|
||||
let request = self.client.request(proto::CountTokensWithLanguageModel {
|
||||
model: name,
|
||||
messages: request
|
||||
.messages
|
||||
.iter()
|
||||
.map(|message| message.to_proto())
|
||||
.collect(),
|
||||
});
|
||||
async move {
|
||||
let response = request.await?;
|
||||
Ok(response.token_count as usize)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
_ => future::ready(Err(anyhow!("invalid model"))).boxed(),
|
||||
}
|
||||
}
|
||||
|
||||
fn stream_completion(
|
||||
&self,
|
||||
mut request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
request.preprocess();
|
||||
|
||||
let request = proto::CompleteWithLanguageModel {
|
||||
model: request.model.id().to_string(),
|
||||
messages: request
|
||||
.messages
|
||||
.iter()
|
||||
.map(|message| message.to_proto())
|
||||
.collect(),
|
||||
stop: request.stop,
|
||||
temperature: request.temperature,
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
};
|
||||
|
||||
self.client
|
||||
.request_stream(request)
|
||||
.map_ok(|stream| {
|
||||
stream
|
||||
.filter_map(|response| async move {
|
||||
match response {
|
||||
Ok(mut response) => Some(Ok(response.choices.pop()?.delta?.content?)),
|
||||
Err(error) => Some(Err(error)),
|
||||
}
|
||||
})
|
||||
.boxed()
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthenticationPrompt;
|
||||
|
||||
impl Render for AuthenticationPrompt {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
const LABEL: &str = "Generate and analyze code with language models. You can dialog with the assistant in this panel or transform code inline.";
|
||||
|
||||
v_flex().gap_6().p_4().child(Label::new(LABEL)).child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("sign_in", "Sign in")
|
||||
.icon_color(Color::Muted)
|
||||
.icon(IconName::Github)
|
||||
.icon_position(IconPosition::Start)
|
||||
.style(ButtonStyle::Filled)
|
||||
.full_width()
|
||||
.on_click(|_, cx| {
|
||||
CompletionProvider::global(cx)
|
||||
.authenticate(cx)
|
||||
.detach_and_log_err(cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div().flex().w_full().items_center().child(
|
||||
Label::new("Sign in to enable collaboration.")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,37 @@
|
||||
mod anthropic;
|
||||
mod cloud;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
mod fake;
|
||||
mod ollama;
|
||||
mod open_ai;
|
||||
|
||||
pub use anthropic::*;
|
||||
use anyhow::Result;
|
||||
use client::Client;
|
||||
pub use cloud::*;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use fake::*;
|
||||
use futures::{future::BoxFuture, stream::BoxStream, StreamExt};
|
||||
use gpui::{AnyView, AppContext, Task, WindowContext};
|
||||
use language_model::{LanguageModel, LanguageModelRequest};
|
||||
pub use ollama::*;
|
||||
pub use open_ai::*;
|
||||
use parking_lot::RwLock;
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
use gpui::{AppContext, Global, Model, ModelContext, Task};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
|
||||
LanguageModelRequest,
|
||||
};
|
||||
use smol::lock::{Semaphore, SemaphoreGuardArc};
|
||||
use std::{any::Any, pin::Pin, sync::Arc, task::Poll};
|
||||
use std::{pin::Pin, sync::Arc, task::Poll};
|
||||
use ui::Context;
|
||||
|
||||
pub struct CompletionResponse {
|
||||
inner: BoxStream<'static, Result<String>>,
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
let completion_provider = cx.new_model(|cx| LanguageModelCompletionProvider::new(cx));
|
||||
cx.set_global(GlobalLanguageModelCompletionProvider(completion_provider));
|
||||
}
|
||||
|
||||
struct GlobalLanguageModelCompletionProvider(Model<LanguageModelCompletionProvider>);
|
||||
|
||||
impl Global for GlobalLanguageModelCompletionProvider {}
|
||||
|
||||
pub struct LanguageModelCompletionProvider {
|
||||
active_provider: Option<Arc<dyn LanguageModelProvider>>,
|
||||
active_model: Option<Arc<dyn LanguageModel>>,
|
||||
request_limiter: Arc<Semaphore>,
|
||||
}
|
||||
|
||||
const MAX_CONCURRENT_COMPLETION_REQUESTS: usize = 4;
|
||||
|
||||
pub struct LanguageModelCompletionResponse {
|
||||
pub inner: BoxStream<'static, Result<String>>,
|
||||
_lock: SemaphoreGuardArc,
|
||||
}
|
||||
|
||||
impl futures::Stream for CompletionResponse {
|
||||
impl futures::Stream for LanguageModelCompletionResponse {
|
||||
type Item = Result<String>;
|
||||
|
||||
fn poll_next(
|
||||
@@ -36,73 +42,101 @@ impl futures::Stream for CompletionResponse {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LanguageModelCompletionProvider: Send + Sync {
|
||||
fn available_models(&self) -> Vec<LanguageModel>;
|
||||
fn settings_version(&self) -> usize;
|
||||
fn is_authenticated(&self) -> bool;
|
||||
fn authenticate(&self, cx: &AppContext) -> Task<Result<()>>;
|
||||
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView;
|
||||
fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>>;
|
||||
fn model(&self) -> LanguageModel;
|
||||
fn count_tokens(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<usize>>;
|
||||
fn stream_completion(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>;
|
||||
impl LanguageModelCompletionProvider {
|
||||
pub fn global(cx: &AppContext) -> Model<Self> {
|
||||
cx.global::<GlobalLanguageModelCompletionProvider>()
|
||||
.0
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
}
|
||||
pub fn read_global(cx: &AppContext) -> &Self {
|
||||
cx.global::<GlobalLanguageModelCompletionProvider>()
|
||||
.0
|
||||
.read(cx)
|
||||
}
|
||||
|
||||
const MAX_CONCURRENT_COMPLETION_REQUESTS: usize = 4;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn test(cx: &mut AppContext) {
|
||||
let provider = cx.new_model(|cx| {
|
||||
let mut this = Self::new(cx);
|
||||
let available_model = LanguageModelRegistry::read_global(cx)
|
||||
.available_models(cx)
|
||||
.first()
|
||||
.unwrap()
|
||||
.clone();
|
||||
this.set_active_model(available_model, cx);
|
||||
this
|
||||
});
|
||||
cx.set_global(GlobalLanguageModelCompletionProvider(provider));
|
||||
}
|
||||
|
||||
pub struct CompletionProvider {
|
||||
provider: Arc<RwLock<dyn LanguageModelCompletionProvider>>,
|
||||
client: Option<Arc<Client>>,
|
||||
request_limiter: Arc<Semaphore>,
|
||||
}
|
||||
pub fn new(cx: &mut ModelContext<Self>) -> Self {
|
||||
cx.observe(&LanguageModelRegistry::global(cx), |_, _, cx| {
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
|
||||
impl CompletionProvider {
|
||||
pub fn new(
|
||||
provider: Arc<RwLock<dyn LanguageModelCompletionProvider>>,
|
||||
client: Option<Arc<Client>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
provider,
|
||||
client,
|
||||
active_provider: None,
|
||||
active_model: None,
|
||||
request_limiter: Arc::new(Semaphore::new(MAX_CONCURRENT_COMPLETION_REQUESTS)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn available_models(&self) -> Vec<LanguageModel> {
|
||||
self.provider.read().available_models()
|
||||
pub fn active_provider(&self) -> Option<Arc<dyn LanguageModelProvider>> {
|
||||
self.active_provider.clone()
|
||||
}
|
||||
|
||||
pub fn settings_version(&self) -> usize {
|
||||
self.provider.read().settings_version()
|
||||
pub fn set_active_provider(
|
||||
&mut self,
|
||||
provider_id: LanguageModelProviderId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.active_provider = LanguageModelRegistry::read_global(cx).provider(&provider_id);
|
||||
self.active_model = None;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
self.provider.read().is_authenticated()
|
||||
pub fn active_model(&self) -> Option<Arc<dyn LanguageModel>> {
|
||||
self.active_model.clone()
|
||||
}
|
||||
|
||||
pub fn set_active_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut ModelContext<Self>) {
|
||||
if self.active_model.as_ref().map_or(false, |m| {
|
||||
m.id() == model.id() && m.provider_id() == model.provider_id()
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.active_provider =
|
||||
LanguageModelRegistry::read_global(cx).provider(&model.provider_id());
|
||||
self.active_model = Some(model.clone());
|
||||
|
||||
if let Some(provider) = self.active_provider.as_ref() {
|
||||
provider.load_model(model, cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn is_authenticated(&self, cx: &AppContext) -> bool {
|
||||
self.active_provider
|
||||
.as_ref()
|
||||
.map_or(false, |provider| provider.is_authenticated(cx))
|
||||
}
|
||||
|
||||
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
self.provider.read().authenticate(cx)
|
||||
}
|
||||
|
||||
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
self.provider.read().authentication_prompt(cx)
|
||||
self.active_provider
|
||||
.as_ref()
|
||||
.map_or(Task::ready(Ok(())), |provider| provider.authenticate(cx))
|
||||
}
|
||||
|
||||
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
self.provider.read().reset_credentials(cx)
|
||||
}
|
||||
|
||||
pub fn model(&self) -> LanguageModel {
|
||||
self.provider.read().model()
|
||||
self.active_provider
|
||||
.as_ref()
|
||||
.map_or(Task::ready(Ok(())), |provider| {
|
||||
provider.reset_credentials(cx)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn count_tokens(
|
||||
@@ -110,25 +144,31 @@ impl CompletionProvider {
|
||||
request: LanguageModelRequest,
|
||||
cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<usize>> {
|
||||
self.provider.read().count_tokens(request, cx)
|
||||
if let Some(model) = self.active_model() {
|
||||
model.count_tokens(request, cx)
|
||||
} else {
|
||||
std::future::ready(Err(anyhow!("No active model set"))).boxed()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stream_completion(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<CompletionResponse>> {
|
||||
let rate_limiter = self.request_limiter.clone();
|
||||
let provider = self.provider.clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let lock = rate_limiter.acquire_arc().await;
|
||||
let response = provider.read().stream_completion(request);
|
||||
let response = response.await?;
|
||||
Ok(CompletionResponse {
|
||||
inner: response,
|
||||
_lock: lock,
|
||||
) -> Task<Result<LanguageModelCompletionResponse>> {
|
||||
if let Some(language_model) = self.active_model() {
|
||||
let rate_limiter = self.request_limiter.clone();
|
||||
cx.spawn(|cx| async move {
|
||||
let lock = rate_limiter.acquire_arc().await;
|
||||
let response = language_model.stream_completion(request, &cx).await?;
|
||||
Ok(LanguageModelCompletionResponse {
|
||||
inner: response,
|
||||
_lock: lock,
|
||||
})
|
||||
})
|
||||
})
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("No active model set")))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn complete(&self, request: LanguageModelRequest, cx: &AppContext) -> Task<Result<String>> {
|
||||
@@ -143,63 +183,43 @@ impl CompletionProvider {
|
||||
Ok(completion)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_provider(
|
||||
&mut self,
|
||||
get_provider: impl FnOnce(Arc<Client>) -> Arc<RwLock<dyn LanguageModelCompletionProvider>>,
|
||||
) {
|
||||
if let Some(client) = &self.client {
|
||||
self.provider = get_provider(Arc::clone(client));
|
||||
} else {
|
||||
log::warn!("completion provider cannot be updated because its client was not set");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::Global for CompletionProvider {}
|
||||
|
||||
impl CompletionProvider {
|
||||
pub fn global(cx: &AppContext) -> &Self {
|
||||
cx.global::<Self>()
|
||||
}
|
||||
|
||||
pub fn update_current_as<R, T: LanguageModelCompletionProvider + 'static>(
|
||||
&mut self,
|
||||
update: impl FnOnce(&mut T) -> R,
|
||||
) -> Option<R> {
|
||||
let mut provider = self.provider.write();
|
||||
if let Some(provider) = provider.as_any_mut().downcast_mut::<T>() {
|
||||
Some(update(provider))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::StreamExt;
|
||||
use gpui::AppContext;
|
||||
use parking_lot::RwLock;
|
||||
use settings::SettingsStore;
|
||||
use smol::stream::StreamExt;
|
||||
use ui::Context;
|
||||
|
||||
use crate::{
|
||||
CompletionProvider, FakeCompletionProvider, LanguageModelRequest,
|
||||
MAX_CONCURRENT_COMPLETION_REQUESTS,
|
||||
LanguageModelCompletionProvider, LanguageModelRequest, MAX_CONCURRENT_COMPLETION_REQUESTS,
|
||||
};
|
||||
|
||||
use language_model::LanguageModelRegistry;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_rate_limiting(cx: &mut AppContext) {
|
||||
SettingsStore::test(cx);
|
||||
let fake_provider = FakeCompletionProvider::setup_test(cx);
|
||||
let fake_provider = LanguageModelRegistry::test(cx);
|
||||
|
||||
let provider = CompletionProvider::new(Arc::new(RwLock::new(fake_provider.clone())), None);
|
||||
let model = LanguageModelRegistry::read_global(cx)
|
||||
.available_models(cx)
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap();
|
||||
|
||||
let provider = cx.new_model(|cx| {
|
||||
let mut provider = LanguageModelCompletionProvider::new(cx);
|
||||
provider.set_active_model(model.clone(), cx);
|
||||
provider
|
||||
});
|
||||
|
||||
let fake_model = fake_provider.test_model();
|
||||
|
||||
// Enqueue some requests
|
||||
for i in 0..MAX_CONCURRENT_COMPLETION_REQUESTS * 2 {
|
||||
let response = provider.stream_completion(
|
||||
let response = provider.read(cx).stream_completion(
|
||||
LanguageModelRequest {
|
||||
temperature: i as f32 / 10.0,
|
||||
..Default::default()
|
||||
@@ -216,23 +236,18 @@ mod tests {
|
||||
.detach();
|
||||
}
|
||||
cx.background_executor().run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
fake_provider.completion_count(),
|
||||
fake_model.completion_count(),
|
||||
MAX_CONCURRENT_COMPLETION_REQUESTS
|
||||
);
|
||||
|
||||
// Get the first completion request that is in flight and mark it as completed.
|
||||
let completion = fake_provider
|
||||
.pending_completions()
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap();
|
||||
fake_provider.finish_completion(&completion);
|
||||
let completion = fake_model.pending_completions().into_iter().next().unwrap();
|
||||
fake_model.finish_completion(&completion);
|
||||
|
||||
// Ensure that the number of in-flight completion requests is reduced.
|
||||
assert_eq!(
|
||||
fake_provider.completion_count(),
|
||||
fake_model.completion_count(),
|
||||
MAX_CONCURRENT_COMPLETION_REQUESTS - 1
|
||||
);
|
||||
|
||||
@@ -240,32 +255,32 @@ mod tests {
|
||||
|
||||
// Ensure that another completion request was allowed to acquire the lock.
|
||||
assert_eq!(
|
||||
fake_provider.completion_count(),
|
||||
fake_model.completion_count(),
|
||||
MAX_CONCURRENT_COMPLETION_REQUESTS
|
||||
);
|
||||
|
||||
// Mark all completion requests as finished that are in flight.
|
||||
for request in fake_provider.pending_completions() {
|
||||
fake_provider.finish_completion(&request);
|
||||
for request in fake_model.pending_completions() {
|
||||
fake_model.finish_completion(&request);
|
||||
}
|
||||
|
||||
assert_eq!(fake_provider.completion_count(), 0);
|
||||
assert_eq!(fake_model.completion_count(), 0);
|
||||
|
||||
// Wait until the background tasks acquire the lock again.
|
||||
cx.background_executor().run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
fake_provider.completion_count(),
|
||||
fake_model.completion_count(),
|
||||
MAX_CONCURRENT_COMPLETION_REQUESTS - 1
|
||||
);
|
||||
|
||||
// Finish all remaining completion requests.
|
||||
for request in fake_provider.pending_completions() {
|
||||
fake_provider.finish_completion(&request);
|
||||
for request in fake_model.pending_completions() {
|
||||
fake_model.finish_completion(&request);
|
||||
}
|
||||
|
||||
cx.background_executor().run_until_parked();
|
||||
|
||||
assert_eq!(fake_provider.completion_count(), 0);
|
||||
assert_eq!(fake_model.completion_count(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use futures::{channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
use gpui::{AnyView, AppContext, Task};
|
||||
use std::sync::Arc;
|
||||
use ui::WindowContext;
|
||||
|
||||
use crate::{LanguageModel, LanguageModelCompletionProvider, LanguageModelRequest};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct FakeCompletionProvider {
|
||||
current_completion_txs: Arc<parking_lot::Mutex<HashMap<String, mpsc::UnboundedSender<String>>>>,
|
||||
}
|
||||
|
||||
impl FakeCompletionProvider {
|
||||
pub fn setup_test(cx: &mut AppContext) -> Self {
|
||||
use crate::CompletionProvider;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
let this = Self::default();
|
||||
let provider = CompletionProvider::new(Arc::new(RwLock::new(this.clone())), None);
|
||||
cx.set_global(provider);
|
||||
this
|
||||
}
|
||||
|
||||
pub fn pending_completions(&self) -> Vec<LanguageModelRequest> {
|
||||
self.current_completion_txs
|
||||
.lock()
|
||||
.keys()
|
||||
.map(|k| serde_json::from_str(k).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn completion_count(&self) -> usize {
|
||||
self.current_completion_txs.lock().len()
|
||||
}
|
||||
|
||||
pub fn send_completion_chunk(&self, request: &LanguageModelRequest, chunk: String) {
|
||||
let json = serde_json::to_string(request).unwrap();
|
||||
self.current_completion_txs
|
||||
.lock()
|
||||
.get(&json)
|
||||
.unwrap()
|
||||
.unbounded_send(chunk)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn send_last_completion_chunk(&self, chunk: String) {
|
||||
self.send_completion_chunk(self.pending_completions().last().unwrap(), chunk);
|
||||
}
|
||||
|
||||
pub fn finish_completion(&self, request: &LanguageModelRequest) {
|
||||
self.current_completion_txs
|
||||
.lock()
|
||||
.remove(&serde_json::to_string(request).unwrap())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn finish_last_completion(&self) {
|
||||
self.finish_completion(self.pending_completions().last().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelCompletionProvider for FakeCompletionProvider {
|
||||
fn available_models(&self) -> Vec<LanguageModel> {
|
||||
vec![LanguageModel::default()]
|
||||
}
|
||||
|
||||
fn settings_version(&self) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
fn is_authenticated(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn authenticate(&self, _cx: &AppContext) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn authentication_prompt(&self, _cx: &mut WindowContext) -> AnyView {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn reset_credentials(&self, _cx: &AppContext) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn model(&self) -> LanguageModel {
|
||||
LanguageModel::default()
|
||||
}
|
||||
|
||||
fn count_tokens(
|
||||
&self,
|
||||
_request: LanguageModelRequest,
|
||||
_cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<usize>> {
|
||||
futures::future::ready(Ok(0)).boxed()
|
||||
}
|
||||
|
||||
fn stream_completion(
|
||||
&self,
|
||||
_request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
self.current_completion_txs
|
||||
.lock()
|
||||
.insert(serde_json::to_string(&_request).unwrap(), tx);
|
||||
async move { Ok(rx.map(Ok).boxed()) }.boxed()
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ command_palette_hooks.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http.workspace = true
|
||||
http_client.workspace = true
|
||||
language.workspace = true
|
||||
lsp.workspace = true
|
||||
menu.workspace = true
|
||||
@@ -65,4 +65,4 @@ rpc = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
http = { workspace = true, features = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -12,8 +12,8 @@ use gpui::{
|
||||
actions, AppContext, AsyncAppContext, Context, Entity, EntityId, EventEmitter, Global, Model,
|
||||
ModelContext, Task, WeakModel,
|
||||
};
|
||||
use http::github::latest_github_release;
|
||||
use http::HttpClient;
|
||||
use http_client::github::latest_github_release;
|
||||
use http_client::HttpClient;
|
||||
use language::{
|
||||
language_settings::{all_language_settings, language_settings, InlineCompletionProvider},
|
||||
point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16,
|
||||
@@ -393,7 +393,7 @@ impl Copilot {
|
||||
Default::default(),
|
||||
cx.to_async(),
|
||||
);
|
||||
let http = http::FakeHttpClient::create(|_| async { unreachable!() });
|
||||
let http = http_client::FakeHttpClient::create(|_| async { unreachable!() });
|
||||
let node_runtime = FakeNodeRuntime::new();
|
||||
let this = cx.new_model(|cx| Self {
|
||||
server_id: LanguageServerId(0),
|
||||
|
||||
@@ -8,7 +8,7 @@ use language::{
|
||||
Buffer, OffsetRangeExt, ToOffset,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::{path::Path, sync::Arc, time::Duration};
|
||||
use std::{ops::Range, path::Path, sync::Arc, time::Duration};
|
||||
|
||||
pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
|
||||
|
||||
@@ -239,7 +239,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
buffer: &Model<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
cx: &'a AppContext,
|
||||
) -> Option<&'a str> {
|
||||
) -> Option<(&'a str, Option<Range<language::Anchor>>)> {
|
||||
let buffer_id = buffer.entity_id();
|
||||
let buffer = buffer.read(cx);
|
||||
let completion = self.active_completion()?;
|
||||
@@ -269,7 +269,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
if completion_text.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(completion_text)
|
||||
Some((completion_text, None))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
|
||||
27
crates/dap/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "dap"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-std = "1.12.0"
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types" }
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
release_channel.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
smol.workspace = true
|
||||
task.workspace = true
|
||||
util.workspace = true
|
||||
646
crates/dap/src/client.rs
Normal file
@@ -0,0 +1,646 @@
|
||||
use crate::transport::{Events, Payload, Response, Transport};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
|
||||
use dap_types::{
|
||||
requests::{
|
||||
Attach, ConfigurationDone, Continue, Disconnect, Initialize, Launch, Next, Pause, Request,
|
||||
Restart, RunInTerminal, SetBreakpoints, StartDebugging, StepBack, StepIn, StepOut,
|
||||
},
|
||||
AttachRequestArguments, ConfigurationDoneArguments, ContinueArguments, ContinueResponse,
|
||||
DisconnectArguments, InitializeRequestArgumentsPathFormat, LaunchRequestArguments,
|
||||
NextArguments, PauseArguments, RestartArguments, RunInTerminalRequestArguments,
|
||||
RunInTerminalResponse, Scope, SetBreakpointsArguments, SetBreakpointsResponse, Source,
|
||||
SourceBreakpoint, StackFrame, StartDebuggingRequestArguments, StepBackArguments,
|
||||
StepInArguments, StepOutArguments, SteppingGranularity, Variable,
|
||||
};
|
||||
use futures::{AsyncBufRead, AsyncReadExt, AsyncWrite};
|
||||
use gpui::{AppContext, AsyncAppContext};
|
||||
use parking_lot::{Mutex, MutexGuard};
|
||||
use serde_json::Value;
|
||||
use smol::{
|
||||
channel::{bounded, unbounded, Receiver, Sender},
|
||||
io::BufReader,
|
||||
net::{TcpListener, TcpStream},
|
||||
process::{self, Child},
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{Ipv4Addr, SocketAddrV4},
|
||||
path::PathBuf,
|
||||
process::Stdio,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use task::{DebugAdapterConfig, DebugConnectionType, DebugRequestType, TCPHost};
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ThreadStatus {
|
||||
#[default]
|
||||
Running,
|
||||
Stopped,
|
||||
Exited,
|
||||
Ended,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[repr(transparent)]
|
||||
pub struct DebugAdapterClientId(pub usize);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ThreadState {
|
||||
pub status: ThreadStatus,
|
||||
pub stack_frames: Vec<StackFrame>,
|
||||
pub scopes: HashMap<u64, Vec<Scope>>, // stack_frame_id -> scopes
|
||||
pub variables: HashMap<u64, Vec<Variable>>, // scope.variable_reference -> variables
|
||||
pub current_stack_frame_id: Option<u64>,
|
||||
}
|
||||
|
||||
pub struct DebugAdapterClient {
|
||||
id: DebugAdapterClientId,
|
||||
_process: Option<Child>,
|
||||
server_tx: Sender<Payload>,
|
||||
sequence_count: AtomicU64,
|
||||
capabilities: Arc<Mutex<Option<dap_types::Capabilities>>>,
|
||||
config: DebugAdapterConfig,
|
||||
thread_states: Arc<Mutex<HashMap<u64, ThreadState>>>, // thread_id -> thread_state
|
||||
sub_client: Arc<Mutex<Option<Arc<Self>>>>,
|
||||
}
|
||||
|
||||
impl DebugAdapterClient {
|
||||
/// Creates & returns a new debug adapter client
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `id`: The id that [`Project`](project::Project) uses to keep track of specific clients
|
||||
/// - `config`: The adapter specific configurations from debugger task that is starting
|
||||
/// - `command`: The command that starts the debugger
|
||||
/// - `args`: Arguments of the command that starts the debugger
|
||||
/// - `project_path`: The absolute path of the project that is being debugged
|
||||
/// - `cx`: The context that the new client belongs too
|
||||
pub async fn new<F>(
|
||||
id: DebugAdapterClientId,
|
||||
config: DebugAdapterConfig,
|
||||
command: &str,
|
||||
args: Vec<&str>,
|
||||
project_path: PathBuf,
|
||||
event_handler: F,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Arc<Self>>
|
||||
where
|
||||
F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone,
|
||||
{
|
||||
match config.connection.clone() {
|
||||
DebugConnectionType::TCP(host) => {
|
||||
Self::create_tcp_client(
|
||||
id,
|
||||
config,
|
||||
host,
|
||||
command,
|
||||
args,
|
||||
project_path,
|
||||
event_handler,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
DebugConnectionType::STDIO => {
|
||||
Self::create_stdio_client(
|
||||
id,
|
||||
config,
|
||||
command,
|
||||
args,
|
||||
project_path,
|
||||
event_handler,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a debug client that connects to an adapter through tcp
|
||||
///
|
||||
/// TCP clients don't have an error communication stream with an adapter
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `id`: The id that [`Project`](project::Project) uses to keep track of specific clients
|
||||
/// - `config`: The adapter specific configurations from debugger task that is starting
|
||||
/// - `command`: The command that starts the debugger
|
||||
/// - `args`: Arguments of the command that starts the debugger
|
||||
/// - `project_path`: The absolute path of the project that is being debugged
|
||||
/// - `cx`: The context that the new client belongs too
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn create_tcp_client<F>(
|
||||
id: DebugAdapterClientId,
|
||||
config: DebugAdapterConfig,
|
||||
host: TCPHost,
|
||||
command: &str,
|
||||
args: Vec<&str>,
|
||||
project_path: PathBuf,
|
||||
event_handler: F,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Arc<Self>>
|
||||
where
|
||||
F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone,
|
||||
{
|
||||
let mut port = host.port;
|
||||
if port.is_none() {
|
||||
port = Self::get_port().await;
|
||||
}
|
||||
|
||||
let mut command = process::Command::new(command);
|
||||
command
|
||||
.current_dir(project_path)
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.kill_on_drop(true);
|
||||
|
||||
let process = command
|
||||
.spawn()
|
||||
.with_context(|| "failed to start debug adapter.")?;
|
||||
|
||||
if let Some(delay) = host.delay {
|
||||
// some debug adapters need some time to start the TCP server
|
||||
// so we have to wait few milliseconds before we can connect to it
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(delay))
|
||||
.await;
|
||||
}
|
||||
|
||||
let address = SocketAddrV4::new(
|
||||
host.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)),
|
||||
port.unwrap(),
|
||||
);
|
||||
|
||||
let (rx, tx) = TcpStream::connect(address).await?.split();
|
||||
|
||||
Self::handle_transport(
|
||||
id,
|
||||
config,
|
||||
Box::new(BufReader::new(rx)),
|
||||
Box::new(tx),
|
||||
None,
|
||||
Some(process),
|
||||
event_handler,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
/// Get an open port to use with the tcp client when not supplied by debug config
|
||||
async fn get_port() -> Option<u16> {
|
||||
Some(
|
||||
TcpListener::bind(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 0))
|
||||
.await
|
||||
.ok()?
|
||||
.local_addr()
|
||||
.ok()?
|
||||
.port(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a debug client that connects to an adapter through std input/output
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `id`: The id that [`Project`](project::Project) uses to keep track of specific clients
|
||||
/// - `config`: The adapter specific configurations from debugger task that is starting
|
||||
/// - `command`: The command that starts the debugger
|
||||
/// - `args`: Arguments of the command that starts the debugger
|
||||
/// - `project_path`: The absolute path of the project that is being debugged
|
||||
/// - `cx`: The context that the new client belongs too
|
||||
async fn create_stdio_client<F>(
|
||||
id: DebugAdapterClientId,
|
||||
config: DebugAdapterConfig,
|
||||
command: &str,
|
||||
args: Vec<&str>,
|
||||
project_path: PathBuf,
|
||||
event_handler: F,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Arc<Self>>
|
||||
where
|
||||
F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone,
|
||||
{
|
||||
let mut command = process::Command::new(command);
|
||||
command
|
||||
.current_dir(project_path)
|
||||
.args(args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
let mut process = command
|
||||
.spawn()
|
||||
.with_context(|| "failed to spawn command.")?;
|
||||
|
||||
let stdin = process
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("Failed to open stdin"))?;
|
||||
let stdout = process
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("Failed to open stdout"))?;
|
||||
let stderr = process
|
||||
.stderr
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("Failed to open stderr"))?;
|
||||
|
||||
let stdin = Box::new(stdin);
|
||||
let stdout = Box::new(BufReader::new(stdout));
|
||||
let stderr = Box::new(BufReader::new(stderr));
|
||||
|
||||
Self::handle_transport(
|
||||
id,
|
||||
config,
|
||||
stdout,
|
||||
stdin,
|
||||
Some(stderr),
|
||||
Some(process),
|
||||
event_handler,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn handle_transport<F>(
|
||||
id: DebugAdapterClientId,
|
||||
config: DebugAdapterConfig,
|
||||
rx: Box<dyn AsyncBufRead + Unpin + Send>,
|
||||
tx: Box<dyn AsyncWrite + Unpin + Send>,
|
||||
err: Option<Box<dyn AsyncBufRead + Unpin + Send>>,
|
||||
process: Option<Child>,
|
||||
event_handler: F,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Arc<Self>>
|
||||
where
|
||||
F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone,
|
||||
{
|
||||
let (server_rx, server_tx) = Transport::start(rx, tx, err, cx);
|
||||
let (client_tx, client_rx) = unbounded::<Payload>();
|
||||
|
||||
let client = Arc::new(Self {
|
||||
id,
|
||||
config,
|
||||
_process: process,
|
||||
sub_client: Default::default(),
|
||||
server_tx: server_tx.clone(),
|
||||
capabilities: Default::default(),
|
||||
thread_states: Default::default(),
|
||||
sequence_count: AtomicU64::new(1),
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.background_executor()
|
||||
.spawn(Self::handle_recv(server_rx, client_tx))
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
cx.spawn({
|
||||
let client = client.clone();
|
||||
|mut cx| async move {
|
||||
Self::handle_events(client, client_rx, server_tx, event_handler, &mut cx).await
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Set's up a client's event handler.
|
||||
///
|
||||
/// This function should only be called once or else errors will arise
|
||||
/// # Parameters
|
||||
/// `client`: A pointer to the client to pass the event handler too
|
||||
/// `event_handler`: The function that is called to handle events
|
||||
/// should be DebugPanel::handle_debug_client_events
|
||||
/// `cx`: The context that this task will run in
|
||||
pub async fn handle_events<F>(
|
||||
this: Arc<Self>,
|
||||
client_rx: Receiver<Payload>,
|
||||
server_tx: Sender<Payload>,
|
||||
mut event_handler: F,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<()>
|
||||
where
|
||||
F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone,
|
||||
{
|
||||
while let Ok(payload) = client_rx.recv().await {
|
||||
match payload {
|
||||
Payload::Event(event) => cx.update(|cx| event_handler(*event, cx))?,
|
||||
Payload::Request(request) => {
|
||||
if RunInTerminal::COMMAND == request.command {
|
||||
Self::handle_run_in_terminal_request(request, &server_tx).await?;
|
||||
} else if StartDebugging::COMMAND == request.command {
|
||||
Self::handle_start_debugging_request(&this, request, cx).await?;
|
||||
} else {
|
||||
unreachable!("Unknown reverse request {}", request.command);
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
async fn handle_run_in_terminal_request(
|
||||
request: crate::transport::Request,
|
||||
server_tx: &Sender<Payload>,
|
||||
) -> Result<()> {
|
||||
let arguments: RunInTerminalRequestArguments =
|
||||
serde_json::from_value(request.arguments.unwrap_or_default())?;
|
||||
|
||||
let mut args = arguments.args.clone();
|
||||
let mut command = process::Command::new(args.remove(0));
|
||||
|
||||
let envs = arguments.env.as_ref().and_then(|e| e.as_object()).map(|e| {
|
||||
e.iter()
|
||||
.map(|(key, value)| (key.clone(), value.clone().to_string()))
|
||||
.collect::<HashMap<String, String>>()
|
||||
});
|
||||
|
||||
if let Some(envs) = envs {
|
||||
command.envs(envs);
|
||||
}
|
||||
|
||||
let process = command
|
||||
.current_dir(arguments.cwd)
|
||||
.args(args)
|
||||
.spawn()
|
||||
.with_context(|| "failed to spawn run in terminal command.")?;
|
||||
|
||||
server_tx
|
||||
.send(Payload::Response(Response {
|
||||
request_seq: request.seq,
|
||||
success: true,
|
||||
command: RunInTerminal::COMMAND.into(),
|
||||
message: None,
|
||||
body: Some(serde_json::to_value(RunInTerminalResponse {
|
||||
process_id: Some(process.id() as u64),
|
||||
shell_process_id: None,
|
||||
})?),
|
||||
}))
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
async fn handle_start_debugging_request(
|
||||
this: &Arc<Self>,
|
||||
request: crate::transport::Request,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
dbg!(&request);
|
||||
let arguments: StartDebuggingRequestArguments =
|
||||
serde_json::from_value(request.arguments.clone().unwrap_or_default())?;
|
||||
|
||||
let sub_client = DebugAdapterClient::new(
|
||||
DebugAdapterClientId(1),
|
||||
this.config.clone(),
|
||||
"node",
|
||||
vec![
|
||||
"/Users/remcosmits/Downloads/js-debug/src/dapDebugServer.js",
|
||||
"8134",
|
||||
"127.0.0.1",
|
||||
],
|
||||
PathBuf::from("/Users/remcosmits/Documents/code/prettier-test"),
|
||||
|event, _cx| {
|
||||
dbg!(event);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
dbg!(&arguments);
|
||||
|
||||
let res = sub_client.launch(request.arguments).await?;
|
||||
dbg!(res);
|
||||
|
||||
*this.sub_client.lock() = Some(sub_client);
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
async fn handle_recv(server_rx: Receiver<Payload>, client_tx: Sender<Payload>) -> Result<()> {
|
||||
while let Ok(payload) = server_rx.recv().await {
|
||||
match payload {
|
||||
Payload::Event(ev) => client_tx.send(Payload::Event(ev)).await?,
|
||||
Payload::Response(_) => unreachable!(),
|
||||
Payload::Request(req) => client_tx.send(Payload::Request(req)).await?,
|
||||
};
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
/// Send a request to an adapter and get a response back
|
||||
/// Note: This function will block until a response is sent back from the adapter
|
||||
pub async fn request<R: Request>(&self, arguments: R::Arguments) -> Result<R::Response> {
|
||||
let serialized_arguments = serde_json::to_value(arguments)?;
|
||||
|
||||
let (callback_tx, callback_rx) = bounded::<Result<Response>>(1);
|
||||
|
||||
let request = crate::transport::Request {
|
||||
back_ch: Some(callback_tx),
|
||||
seq: self.next_sequence_id(),
|
||||
command: R::COMMAND.to_string(),
|
||||
arguments: Some(serialized_arguments),
|
||||
};
|
||||
|
||||
self.server_tx.send(Payload::Request(request)).await?;
|
||||
|
||||
let response = callback_rx.recv().await??;
|
||||
|
||||
match response.success {
|
||||
true => Ok(serde_json::from_value(response.body.unwrap_or_default())?),
|
||||
false => Err(anyhow!("Request failed")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> DebugAdapterClientId {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn config(&self) -> DebugAdapterConfig {
|
||||
self.config.clone()
|
||||
}
|
||||
|
||||
pub fn request_type(&self) -> DebugRequestType {
|
||||
self.config.request.clone()
|
||||
}
|
||||
|
||||
pub fn capabilities(&self) -> dap_types::Capabilities {
|
||||
self.capabilities.lock().clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get the next sequence id to be used in a request
|
||||
/// # Side Effect
|
||||
/// This function also increment's client's sequence count by one
|
||||
pub fn next_sequence_id(&self) -> u64 {
|
||||
self.sequence_count.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn update_thread_state_status(&self, thread_id: u64, status: ThreadStatus) {
|
||||
if let Some(thread_state) = self.thread_states().get_mut(&thread_id) {
|
||||
thread_state.status = status;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn thread_states(&self) -> MutexGuard<HashMap<u64, ThreadState>> {
|
||||
self.thread_states.lock()
|
||||
}
|
||||
|
||||
pub fn thread_state_by_id(&self, thread_id: u64) -> ThreadState {
|
||||
self.thread_states.lock().get(&thread_id).cloned().unwrap()
|
||||
}
|
||||
|
||||
pub async fn initialize(&self) -> Result<dap_types::Capabilities> {
|
||||
let args = dap_types::InitializeRequestArguments {
|
||||
client_id: Some("zed".to_owned()),
|
||||
client_name: Some("Zed".to_owned()),
|
||||
adapter_id: self.config.id.clone(),
|
||||
locale: Some("en-us".to_owned()),
|
||||
path_format: Some(InitializeRequestArgumentsPathFormat::Path),
|
||||
supports_variable_type: Some(true),
|
||||
supports_variable_paging: Some(false),
|
||||
supports_run_in_terminal_request: Some(false), // TODO: we should support this
|
||||
supports_memory_references: Some(true),
|
||||
supports_progress_reporting: Some(true),
|
||||
supports_invalidated_event: Some(false),
|
||||
lines_start_at1: Some(true),
|
||||
columns_start_at1: Some(true),
|
||||
supports_memory_event: Some(true),
|
||||
supports_args_can_be_interpreted_by_shell: None,
|
||||
supports_start_debugging_request: Some(true),
|
||||
};
|
||||
|
||||
let capabilities = self.request::<Initialize>(args).await?;
|
||||
|
||||
*self.capabilities.lock() = Some(capabilities.clone());
|
||||
|
||||
Ok(capabilities)
|
||||
}
|
||||
|
||||
pub async fn launch(&self, args: Option<Value>) -> Result<()> {
|
||||
self.request::<Launch>(LaunchRequestArguments {
|
||||
raw: args.unwrap_or(Value::Null),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn attach(&self, args: Option<Value>) -> Result<()> {
|
||||
self.request::<Attach>(AttachRequestArguments {
|
||||
raw: args.unwrap_or(Value::Null),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn resume(&self, thread_id: u64) -> Result<ContinueResponse> {
|
||||
self.request::<Continue>(ContinueArguments {
|
||||
thread_id,
|
||||
single_thread: Some(true),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn step_over(&self, thread_id: u64) -> Result<()> {
|
||||
self.request::<Next>(NextArguments {
|
||||
thread_id,
|
||||
granularity: Some(SteppingGranularity::Statement),
|
||||
single_thread: Some(true),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn step_in(&self, thread_id: u64) -> Result<()> {
|
||||
self.request::<StepIn>(StepInArguments {
|
||||
thread_id,
|
||||
target_id: None,
|
||||
granularity: Some(SteppingGranularity::Statement),
|
||||
single_thread: Some(true),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn step_out(&self, thread_id: u64) -> Result<()> {
|
||||
self.request::<StepOut>(StepOutArguments {
|
||||
thread_id,
|
||||
granularity: Some(SteppingGranularity::Statement),
|
||||
single_thread: Some(true),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn step_back(&self, thread_id: u64) -> Result<()> {
|
||||
self.request::<StepBack>(StepBackArguments {
|
||||
thread_id,
|
||||
single_thread: Some(true),
|
||||
granularity: Some(SteppingGranularity::Statement),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn restart(&self) {
|
||||
self.request::<Restart>(RestartArguments {
|
||||
raw: self
|
||||
.config
|
||||
.request_args
|
||||
.as_ref()
|
||||
.map(|v| v.args.clone())
|
||||
.unwrap_or(Value::Null),
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
pub async fn pause(&self, thread_id: u64) {
|
||||
self.request::<Pause>(PauseArguments { thread_id })
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
pub async fn stop(&self) {
|
||||
self.request::<Disconnect>(DisconnectArguments {
|
||||
restart: Some(false),
|
||||
terminate_debuggee: Some(false),
|
||||
suspend_debuggee: Some(false),
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
pub async fn set_breakpoints(
|
||||
&self,
|
||||
path: PathBuf,
|
||||
breakpoints: Option<Vec<SourceBreakpoint>>,
|
||||
) -> Result<SetBreakpointsResponse> {
|
||||
let adapter_data = self.config.request_args.clone().map(|c| c.args);
|
||||
|
||||
self.request::<SetBreakpoints>(SetBreakpointsArguments {
|
||||
source: Source {
|
||||
path: Some(String::from(path.to_string_lossy())),
|
||||
name: None,
|
||||
source_reference: None,
|
||||
presentation_hint: None,
|
||||
origin: None,
|
||||
sources: None,
|
||||
adapter_data,
|
||||
checksums: None,
|
||||
},
|
||||
breakpoints,
|
||||
source_modified: None,
|
||||
lines: None,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn configuration_done(&self) -> Result<()> {
|
||||
self.request::<ConfigurationDone>(ConfigurationDoneArguments)
|
||||
.await
|
||||
}
|
||||
}
|
||||
3
crates/dap/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod client;
|
||||
pub mod transport;
|
||||
pub use dap_types::*;
|
||||
283
crates/dap/src/transport.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use dap_types::{
|
||||
BreakpointEvent, Capabilities, CapabilitiesEvent, ContinuedEvent, ExitedEvent,
|
||||
InvalidatedEvent, LoadedSourceEvent, MemoryEvent, ModuleEvent, OutputEvent, ProcessEvent,
|
||||
ProgressEndEvent, ProgressStartEvent, ProgressUpdateEvent, StoppedEvent, TerminatedEvent,
|
||||
ThreadEvent,
|
||||
};
|
||||
use futures::{AsyncBufRead, AsyncWrite};
|
||||
use gpui::AsyncAppContext;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde_json::Value;
|
||||
use smol::{
|
||||
channel::{unbounded, Receiver, Sender},
|
||||
io::{AsyncBufReadExt as _, AsyncReadExt as _, AsyncWriteExt},
|
||||
lock::Mutex,
|
||||
};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum Payload {
|
||||
Event(Box<Events>),
|
||||
Response(Response),
|
||||
Request(Request),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(tag = "event", content = "body")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Events {
|
||||
Initialized(Option<Capabilities>),
|
||||
Stopped(StoppedEvent),
|
||||
Continued(ContinuedEvent),
|
||||
Exited(ExitedEvent),
|
||||
Terminated(Option<TerminatedEvent>),
|
||||
Thread(ThreadEvent),
|
||||
Output(OutputEvent),
|
||||
Breakpoint(BreakpointEvent),
|
||||
Module(ModuleEvent),
|
||||
LoadedSource(LoadedSourceEvent),
|
||||
Process(ProcessEvent),
|
||||
Capabilities(CapabilitiesEvent),
|
||||
ProgressStart(ProgressStartEvent),
|
||||
ProgressUpdate(ProgressUpdateEvent),
|
||||
ProgressEnd(ProgressEndEvent),
|
||||
Invalidated(InvalidatedEvent),
|
||||
Memory(MemoryEvent),
|
||||
#[serde(untagged)]
|
||||
Other(HashMap<String, Value>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Request {
|
||||
#[serde(skip)]
|
||||
pub back_ch: Option<Sender<Result<Response>>>,
|
||||
pub seq: u64,
|
||||
pub command: String,
|
||||
pub arguments: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
pub struct Response {
|
||||
pub request_seq: u64,
|
||||
pub success: bool,
|
||||
pub command: String,
|
||||
pub message: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_empty_object")]
|
||||
pub body: Option<Value>,
|
||||
}
|
||||
|
||||
fn deserialize_empty_object<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = Value::deserialize(deserializer)?;
|
||||
if value == Value::Object(serde_json::Map::new()) {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(value))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Transport {
|
||||
pending_requests: Mutex<HashMap<u64, Sender<Result<Response>>>>,
|
||||
}
|
||||
|
||||
impl Transport {
|
||||
pub fn start(
|
||||
server_stdout: Box<dyn AsyncBufRead + Unpin + Send>,
|
||||
server_stdin: Box<dyn AsyncWrite + Unpin + Send>,
|
||||
server_stderr: Option<Box<dyn AsyncBufRead + Unpin + Send>>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> (Receiver<Payload>, Sender<Payload>) {
|
||||
let (client_tx, server_rx) = unbounded::<Payload>();
|
||||
let (server_tx, client_rx) = unbounded::<Payload>();
|
||||
|
||||
let transport = Arc::new(Self {
|
||||
pending_requests: Mutex::new(HashMap::default()),
|
||||
});
|
||||
|
||||
let _ = cx.update(|cx| {
|
||||
let transport = transport.clone();
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(Self::receive(transport.clone(), server_stdout, client_tx))
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(Self::send(transport.clone(), server_stdin, client_rx))
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
if let Some(stderr) = server_stderr {
|
||||
cx.background_executor()
|
||||
.spawn(Self::err(stderr))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
|
||||
(server_rx, server_tx)
|
||||
}
|
||||
|
||||
async fn recv_server_message(
|
||||
reader: &mut Box<dyn AsyncBufRead + Unpin + Send>,
|
||||
buffer: &mut String,
|
||||
) -> Result<Payload> {
|
||||
let mut content_length = None;
|
||||
loop {
|
||||
buffer.truncate(0);
|
||||
|
||||
if reader
|
||||
.read_line(buffer)
|
||||
.await
|
||||
.with_context(|| "reading a message from server")?
|
||||
== 0
|
||||
{
|
||||
return Err(anyhow!("reader stream closed"));
|
||||
};
|
||||
|
||||
if buffer == "\r\n" {
|
||||
break;
|
||||
}
|
||||
|
||||
let parts = buffer.trim().split_once(": ");
|
||||
|
||||
match parts {
|
||||
Some(("Content-Length", value)) => {
|
||||
content_length = Some(value.parse().context("invalid content length")?);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let content_length = content_length.context("missing content length")?;
|
||||
|
||||
let mut content = vec![0; content_length];
|
||||
reader
|
||||
.read_exact(&mut content)
|
||||
.await
|
||||
.with_context(|| "reading after a loop")?;
|
||||
|
||||
let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?;
|
||||
|
||||
Ok(serde_json::from_str::<Payload>(msg)?)
|
||||
}
|
||||
|
||||
async fn recv_server_error(
|
||||
err: &mut (impl AsyncBufRead + Unpin + Send),
|
||||
buffer: &mut String,
|
||||
) -> Result<()> {
|
||||
buffer.truncate(0);
|
||||
if err.read_line(buffer).await? == 0 {
|
||||
return Err(anyhow!("error stream closed"));
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_payload_to_server(
|
||||
&self,
|
||||
server_stdin: &mut Box<dyn AsyncWrite + Unpin + Send>,
|
||||
mut payload: Payload,
|
||||
) -> Result<()> {
|
||||
if let Payload::Request(request) = &mut payload {
|
||||
if let Some(back) = request.back_ch.take() {
|
||||
self.pending_requests.lock().await.insert(request.seq, back);
|
||||
}
|
||||
}
|
||||
self.send_string_to_server(server_stdin, serde_json::to_string(&payload)?)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn send_string_to_server(
|
||||
&self,
|
||||
server_stdin: &mut Box<dyn AsyncWrite + Unpin + Send>,
|
||||
request: String,
|
||||
) -> Result<()> {
|
||||
server_stdin
|
||||
.write_all(format!("Content-Length: {}\r\n\r\n{}", request.len(), request).as_bytes())
|
||||
.await?;
|
||||
|
||||
server_stdin.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_response(response: Response) -> Result<Response> {
|
||||
if response.success {
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(anyhow!("Received failed response"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_server_message(
|
||||
&self,
|
||||
client_tx: &Sender<Payload>,
|
||||
payload: Payload,
|
||||
) -> Result<()> {
|
||||
match payload {
|
||||
Payload::Response(res) => {
|
||||
if let Some(tx) = self.pending_requests.lock().await.remove(&res.request_seq) {
|
||||
|
||||
if !tx.is_closed() {
|
||||
tx.send(Self::process_response(res)).await?;
|
||||
} else {
|
||||
log::warn!(
|
||||
"Response stream associated with request seq: {} is closed",
|
||||
&res.request_seq
|
||||
); // TODO: Fix this case so it never happens
|
||||
}
|
||||
} else {
|
||||
client_tx.send(Payload::Response(res)).await?;
|
||||
};
|
||||
}
|
||||
|
||||
Payload::Request(_) => {
|
||||
client_tx.send(payload).await?;
|
||||
}
|
||||
Payload::Event(_) => {
|
||||
client_tx.send(payload).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn receive(
|
||||
transport: Arc<Self>,
|
||||
mut server_stdout: Box<dyn AsyncBufRead + Unpin + Send>,
|
||||
client_tx: Sender<Payload>,
|
||||
) -> Result<()> {
|
||||
let mut recv_buffer = String::new();
|
||||
loop {
|
||||
transport
|
||||
.process_server_message(
|
||||
&client_tx,
|
||||
Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await?,
|
||||
)
|
||||
.await
|
||||
.context("Process server message failed in transport::receive")?;
|
||||
}
|
||||
}
|
||||
|
||||
async fn send(
|
||||
transport: Arc<Self>,
|
||||
mut server_stdin: Box<dyn AsyncWrite + Unpin + Send>,
|
||||
client_rx: Receiver<Payload>,
|
||||
) -> Result<()> {
|
||||
while let Ok(payload) = client_rx.recv().await {
|
||||
transport
|
||||
.send_payload_to_server(&mut server_stdin, payload)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn err(mut server_stderr: Box<dyn AsyncBufRead + Unpin + Send>) -> Result<()> {
|
||||
let mut recv_buffer = String::new();
|
||||
loop {
|
||||
Self::recv_server_error(&mut server_stderr, &mut recv_buffer).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
crates/debugger_ui/Cargo.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "debugger_ui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
dap.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
task.workspace = true
|
||||
tasks_ui.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
602
crates/debugger_ui/src/debugger_panel.rs
Normal file
@@ -0,0 +1,602 @@
|
||||
use anyhow::Result;
|
||||
use dap::client::{DebugAdapterClientId, ThreadState, ThreadStatus};
|
||||
use dap::requests::{Disconnect, Scopes, StackTrace, Variables};
|
||||
use dap::{client::DebugAdapterClient, transport::Events};
|
||||
use dap::{
|
||||
Capabilities, ContinuedEvent, DisconnectArguments, ExitedEvent, Scope, ScopesArguments,
|
||||
StackFrame, StackTraceArguments, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason,
|
||||
Variable, VariablesArguments,
|
||||
};
|
||||
use editor::Editor;
|
||||
use futures::future::try_join_all;
|
||||
use gpui::{
|
||||
actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView,
|
||||
Subscription, Task, View, ViewContext, WeakView,
|
||||
};
|
||||
use std::path::Path;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use task::DebugRequestType;
|
||||
use ui::prelude::*;
|
||||
use workspace::Pane;
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
Workspace,
|
||||
};
|
||||
|
||||
use crate::debugger_panel_item::DebugPanelItem;
|
||||
|
||||
enum DebugCurrentRowHighlight {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DebugPanelEvent {
|
||||
Stopped((DebugAdapterClientId, StoppedEvent)),
|
||||
Thread((DebugAdapterClientId, ThreadEvent)),
|
||||
}
|
||||
|
||||
actions!(debug_panel, [ToggleFocus]);
|
||||
|
||||
pub struct DebugPanel {
|
||||
size: Pixels,
|
||||
pane: View<Pane>,
|
||||
focus_handle: FocusHandle,
|
||||
workspace: WeakView<Workspace>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl DebugPanel {
|
||||
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
|
||||
cx.new_view(|cx| {
|
||||
let pane = cx.new_view(|cx| {
|
||||
let mut pane = Pane::new(
|
||||
workspace.weak_handle(),
|
||||
workspace.project().clone(),
|
||||
Default::default(),
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
pane.set_can_split(false, cx);
|
||||
pane.set_can_navigate(true, cx);
|
||||
pane.display_nav_history_buttons(None);
|
||||
pane.set_should_display_tab_bar(|_| true);
|
||||
|
||||
pane
|
||||
});
|
||||
|
||||
let project = workspace.project().clone();
|
||||
|
||||
let _subscriptions = vec![cx.subscribe(&project, {
|
||||
move |this: &mut Self, _, event, cx| match event {
|
||||
project::Event::DebugClientEvent { event, client_id } => {
|
||||
Self::handle_debug_client_events(
|
||||
this,
|
||||
this.debug_client_by_id(*client_id, cx),
|
||||
event,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
project::Event::DebugClientStarted(client_id) => {
|
||||
let client = this.debug_client_by_id(*client_id, cx);
|
||||
cx.spawn(|_, _| async move {
|
||||
client.initialize().await?;
|
||||
let request_args = client.config().request_args.map(|a| a.args);
|
||||
|
||||
// send correct request based on adapter config
|
||||
match client.config().request {
|
||||
DebugRequestType::Launch => client.launch(request_args).await,
|
||||
DebugRequestType::Attach => client.attach(request_args).await,
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})];
|
||||
|
||||
Self {
|
||||
pane,
|
||||
size: px(300.),
|
||||
_subscriptions,
|
||||
focus_handle: cx.focus_handle(),
|
||||
workspace: workspace.weak_handle(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
workspace.update(&mut cx, |workspace, cx| DebugPanel::new(workspace, cx))
|
||||
})
|
||||
}
|
||||
|
||||
fn debug_client_by_id(
|
||||
&self,
|
||||
client_id: DebugAdapterClientId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Arc<DebugAdapterClient> {
|
||||
self.workspace
|
||||
.update(cx, |this, cx| {
|
||||
this.project()
|
||||
.read(cx)
|
||||
.debug_adapter_by_id(client_id)
|
||||
.unwrap()
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn handle_debug_client_events(
|
||||
this: &mut Self,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
event: &Events,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
Events::Initialized(event) => Self::handle_initialized_event(client, event, cx),
|
||||
Events::Stopped(event) => Self::handle_stopped_event(client, event, cx),
|
||||
Events::Continued(event) => Self::handle_continued_event(client, event, cx),
|
||||
Events::Exited(event) => Self::handle_exited_event(client, event, cx),
|
||||
Events::Terminated(event) => Self::handle_terminated_event(this, client, event, cx),
|
||||
Events::Thread(event) => Self::handle_thread_event(client, event, cx),
|
||||
Events::Output(_) => {}
|
||||
Events::Breakpoint(_) => {}
|
||||
Events::Module(_) => {}
|
||||
Events::LoadedSource(_) => {}
|
||||
Events::Capabilities(_) => {}
|
||||
Events::Memory(_) => {}
|
||||
Events::Process(_) => {}
|
||||
Events::ProgressEnd(_) => {}
|
||||
Events::ProgressStart(_) => {}
|
||||
Events::ProgressUpdate(_) => {}
|
||||
Events::Invalidated(_) => {}
|
||||
Events::Other(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn go_to_stack_frame(
|
||||
workspace: WeakView<Workspace>,
|
||||
stack_frame: StackFrame,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
clear_highlights: bool,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let path = stack_frame.clone().source.unwrap().path.unwrap().clone();
|
||||
let row = (stack_frame.line.saturating_sub(1)) as u32;
|
||||
let column = (stack_frame.column.saturating_sub(1)) as u32;
|
||||
|
||||
if clear_highlights {
|
||||
Self::remove_highlights(workspace.clone(), client, cx.clone()).await?;
|
||||
}
|
||||
|
||||
let task = workspace.update(&mut cx, |workspace, cx| {
|
||||
let project_path = workspace.project().read_with(cx, |project, cx| {
|
||||
project.project_path_for_absolute_path(&Path::new(&path), cx)
|
||||
});
|
||||
|
||||
if let Some(project_path) = project_path {
|
||||
workspace.open_path_preview(project_path, None, false, true, cx)
|
||||
} else {
|
||||
Task::ready(Err(anyhow::anyhow!(
|
||||
"No project path found for path: {}",
|
||||
path
|
||||
)))
|
||||
}
|
||||
})?;
|
||||
|
||||
let editor = task.await?.downcast::<Editor>().unwrap();
|
||||
|
||||
workspace.update(&mut cx, |_, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.go_to_line::<DebugCurrentRowHighlight>(
|
||||
row,
|
||||
column,
|
||||
Some(cx.theme().colors().editor_debugger_active_line_background),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async fn remove_highlights(
|
||||
workspace: WeakView<Workspace>,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let mut tasks = Vec::new();
|
||||
for thread_state in client.thread_states().values() {
|
||||
for stack_frame in thread_state.stack_frames.clone() {
|
||||
tasks.push(Self::remove_editor_highlight(
|
||||
workspace.clone(),
|
||||
stack_frame,
|
||||
cx.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !tasks.is_empty() {
|
||||
try_join_all(tasks).await?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
async fn remove_highlights_for_thread(
|
||||
workspace: WeakView<Workspace>,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
thread_id: u64,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let mut tasks = Vec::new();
|
||||
if let Some(thread_state) = client.thread_states().get(&thread_id) {
|
||||
for stack_frame in thread_state.stack_frames.clone() {
|
||||
tasks.push(Self::remove_editor_highlight(
|
||||
workspace.clone(),
|
||||
stack_frame.clone(),
|
||||
cx.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !tasks.is_empty() {
|
||||
try_join_all(tasks).await?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
async fn remove_editor_highlight(
|
||||
workspace: WeakView<Workspace>,
|
||||
stack_frame: StackFrame,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let path = stack_frame.clone().source.unwrap().path.unwrap().clone();
|
||||
|
||||
let task = workspace.update(&mut cx, |workspace, cx| {
|
||||
let project_path = workspace.project().read_with(cx, |project, cx| {
|
||||
project.project_path_for_absolute_path(&Path::new(&path), cx)
|
||||
});
|
||||
|
||||
if let Some(project_path) = project_path {
|
||||
workspace.open_path(project_path, None, false, cx)
|
||||
} else {
|
||||
Task::ready(Err(anyhow::anyhow!(
|
||||
"No project path found for path: {}",
|
||||
path
|
||||
)))
|
||||
}
|
||||
})?;
|
||||
|
||||
let editor = task.await?.downcast::<Editor>().unwrap();
|
||||
|
||||
editor.update(&mut cx, |editor, _| {
|
||||
editor.clear_row_highlights::<DebugCurrentRowHighlight>();
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_initialized_event(
|
||||
client: Arc<DebugAdapterClient>,
|
||||
_: &Option<Capabilities>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let task = this.update(&mut cx, |this, cx| {
|
||||
this.workspace.update(cx, |workspace, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
let client = client.clone();
|
||||
|
||||
project.send_breakpoints(client, cx)
|
||||
})
|
||||
})
|
||||
})??;
|
||||
|
||||
task.await?;
|
||||
|
||||
client.configuration_done().await
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn handle_continued_event(
|
||||
client: Arc<DebugAdapterClient>,
|
||||
event: &ContinuedEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let all_threads = event.all_threads_continued.unwrap_or(false);
|
||||
|
||||
if all_threads {
|
||||
for thread in client.thread_states().values_mut() {
|
||||
thread.status = ThreadStatus::Running;
|
||||
}
|
||||
} else {
|
||||
client.update_thread_state_status(event.thread_id, ThreadStatus::Running);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn handle_stopped_event(
|
||||
client: Arc<DebugAdapterClient>,
|
||||
event: &StoppedEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let Some(thread_id) = event.thread_id else {
|
||||
return;
|
||||
};
|
||||
|
||||
let client_id = client.id();
|
||||
cx.spawn({
|
||||
let event = event.clone();
|
||||
|this, mut cx| async move {
|
||||
let stack_trace_response = client
|
||||
.request::<StackTrace>(StackTraceArguments {
|
||||
thread_id,
|
||||
start_frame: None,
|
||||
levels: None,
|
||||
format: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let current_stack_frame =
|
||||
stack_trace_response.stack_frames.first().unwrap().clone();
|
||||
let mut scope_tasks = Vec::new();
|
||||
for stack_frame in stack_trace_response.stack_frames.clone().into_iter() {
|
||||
let frame_id = stack_frame.id;
|
||||
let client = client.clone();
|
||||
scope_tasks.push(async move {
|
||||
anyhow::Ok((
|
||||
frame_id,
|
||||
client
|
||||
.request::<Scopes>(ScopesArguments { frame_id })
|
||||
.await?,
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
let mut scopes: HashMap<u64, Vec<Scope>> = HashMap::new();
|
||||
let mut variables: HashMap<u64, Vec<Variable>> = HashMap::new();
|
||||
|
||||
let mut variable_tasks = Vec::new();
|
||||
for (thread_id, response) in try_join_all(scope_tasks).await? {
|
||||
scopes.insert(thread_id, response.scopes.clone());
|
||||
|
||||
for scope in response.scopes {
|
||||
let scope_reference = scope.variables_reference;
|
||||
let client = client.clone();
|
||||
variable_tasks.push(async move {
|
||||
anyhow::Ok((
|
||||
scope_reference,
|
||||
client
|
||||
.request::<Variables>(VariablesArguments {
|
||||
variables_reference: scope_reference,
|
||||
filter: None,
|
||||
start: None,
|
||||
count: None,
|
||||
format: None,
|
||||
})
|
||||
.await?,
|
||||
))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (scope_reference, response) in try_join_all(variable_tasks).await? {
|
||||
variables.insert(scope_reference, response.variables.clone());
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let mut thread_state = client.thread_states();
|
||||
let thread_state = thread_state
|
||||
.entry(thread_id)
|
||||
.or_insert(ThreadState::default());
|
||||
|
||||
thread_state.current_stack_frame_id = Some(current_stack_frame.clone().id);
|
||||
thread_state.stack_frames = stack_trace_response.stack_frames;
|
||||
thread_state.scopes = scopes;
|
||||
thread_state.variables = variables;
|
||||
thread_state.status = ThreadStatus::Stopped;
|
||||
|
||||
let existing_item = this
|
||||
.pane
|
||||
.read(cx)
|
||||
.items()
|
||||
.filter_map(|item| item.downcast::<DebugPanelItem>())
|
||||
.any(|item| {
|
||||
let item = item.read(cx);
|
||||
|
||||
item.client().id() == client_id && item.thread_id() == thread_id
|
||||
});
|
||||
|
||||
if !existing_item {
|
||||
let debug_panel = cx.view().clone();
|
||||
this.pane.update(cx, |this, cx| {
|
||||
let tab = cx.new_view(|cx| {
|
||||
DebugPanelItem::new(debug_panel, client.clone(), thread_id, cx)
|
||||
});
|
||||
|
||||
this.add_item(Box::new(tab.clone()), false, false, None, cx)
|
||||
});
|
||||
}
|
||||
|
||||
cx.emit(DebugPanelEvent::Stopped((client_id, event)));
|
||||
|
||||
if let Some(item) = this.pane.read(cx).active_item() {
|
||||
if let Some(pane) = item.downcast::<DebugPanelItem>() {
|
||||
let pane = pane.read(cx);
|
||||
if pane.thread_id() == thread_id && pane.client().id() == client_id {
|
||||
let workspace = this.workspace.clone();
|
||||
let client = client.clone();
|
||||
return cx.spawn(|_, cx| async move {
|
||||
Self::go_to_stack_frame(
|
||||
workspace,
|
||||
current_stack_frame.clone(),
|
||||
client,
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Task::ready(anyhow::Ok(()))
|
||||
})?
|
||||
.await
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
client: Arc<DebugAdapterClient>,
|
||||
event: &ThreadEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let thread_id = event.thread_id;
|
||||
|
||||
if event.reason == ThreadEventReason::Started {
|
||||
client
|
||||
.thread_states()
|
||||
.insert(thread_id, ThreadState::default());
|
||||
} else {
|
||||
client.update_thread_state_status(thread_id, ThreadStatus::Ended);
|
||||
|
||||
// TODO: we want to figure out for witch clients/threads we should remove the highlights
|
||||
cx.spawn({
|
||||
let client = client.clone();
|
||||
|this, mut cx| async move {
|
||||
let workspace = this.update(&mut cx, |this, _| this.workspace.clone())?;
|
||||
|
||||
Self::remove_highlights_for_thread(workspace, client, thread_id, cx).await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
cx.emit(DebugPanelEvent::Thread((client.id(), event.clone())));
|
||||
}
|
||||
|
||||
fn handle_exited_event(
|
||||
client: Arc<DebugAdapterClient>,
|
||||
_: &ExitedEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
cx.spawn(|_, _| async move {
|
||||
for thread_state in client.thread_states().values_mut() {
|
||||
thread_state.status = ThreadStatus::Exited;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn handle_terminated_event(
|
||||
this: &mut Self,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
event: &Option<TerminatedEvent>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let restart_args = event.clone().and_then(|e| e.restart);
|
||||
let workspace = this.workspace.clone();
|
||||
|
||||
cx.spawn(|_, cx| async move {
|
||||
let should_restart = restart_args.is_some();
|
||||
|
||||
Self::remove_highlights(workspace, client.clone(), cx).await?;
|
||||
|
||||
client
|
||||
.request::<Disconnect>(DisconnectArguments {
|
||||
restart: Some(should_restart),
|
||||
terminate_debuggee: None,
|
||||
suspend_debuggee: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
if should_restart {
|
||||
match client.request_type() {
|
||||
DebugRequestType::Launch => client.launch(restart_args).await,
|
||||
DebugRequestType::Attach => client.attach(restart_args).await,
|
||||
}
|
||||
} else {
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for DebugPanel {}
|
||||
impl EventEmitter<DebugPanelEvent> for DebugPanel {}
|
||||
impl EventEmitter<project::Event> for DebugPanel {}
|
||||
|
||||
impl FocusableView for DebugPanel {
|
||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for DebugPanel {
|
||||
fn persistent_name() -> &'static str {
|
||||
"DebugPanel"
|
||||
}
|
||||
|
||||
fn position(&self, _cx: &WindowContext) -> DockPosition {
|
||||
DockPosition::Bottom
|
||||
}
|
||||
|
||||
fn position_is_valid(&self, position: DockPosition) -> bool {
|
||||
position == DockPosition::Bottom
|
||||
}
|
||||
|
||||
fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
|
||||
|
||||
fn size(&self, _cx: &WindowContext) -> Pixels {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: Option<Pixels>, _cx: &mut ViewContext<Self>) {
|
||||
self.size = size.unwrap();
|
||||
}
|
||||
|
||||
fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
|
||||
None
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn toggle_action(&self) -> Box<dyn Action> {
|
||||
Box::new(ToggleFocus)
|
||||
}
|
||||
|
||||
fn icon_label(&self, _: &WindowContext) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn is_zoomed(&self, _cx: &WindowContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn starts_open(&self, _cx: &WindowContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
|
||||
|
||||
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
|
||||
}
|
||||
|
||||
impl Render for DebugPanel {
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("DebugPanel")
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.child(self.pane.clone())
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
514
crates/debugger_ui/src/debugger_panel_item.rs
Normal file
@@ -0,0 +1,514 @@
|
||||
use crate::debugger_panel::{DebugPanel, DebugPanelEvent};
|
||||
use anyhow::Result;
|
||||
use dap::client::{DebugAdapterClient, DebugAdapterClientId, ThreadState, ThreadStatus};
|
||||
use dap::{Scope, StackFrame, StoppedEvent, ThreadEvent, Variable};
|
||||
use gpui::{
|
||||
actions, list, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle,
|
||||
FocusableView, ListState, Subscription, View, WeakView,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use ui::WindowContext;
|
||||
use ui::{prelude::*, Tooltip};
|
||||
use workspace::item::{Item, ItemEvent};
|
||||
|
||||
pub struct DebugPanelItem {
|
||||
thread_id: u64,
|
||||
focus_handle: FocusHandle,
|
||||
stack_frame_list: ListState,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
current_stack_frame_id: Option<u64>,
|
||||
}
|
||||
|
||||
actions!(
|
||||
debug_panel_item,
|
||||
[Continue, StepOver, StepIn, StepOut, Restart, Pause, Stop]
|
||||
);
|
||||
|
||||
impl DebugPanelItem {
|
||||
pub fn new(
|
||||
debug_panel: View<DebugPanel>,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
thread_id: u64,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let weakview = cx.view().downgrade();
|
||||
let stack_frame_list =
|
||||
ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| {
|
||||
if let Some(view) = weakview.upgrade() {
|
||||
view.update(cx, |view, cx| {
|
||||
view.render_stack_frame(ix, cx).into_any_element()
|
||||
})
|
||||
} else {
|
||||
div().into_any()
|
||||
}
|
||||
});
|
||||
|
||||
let _subscriptions = vec![cx.subscribe(&debug_panel, {
|
||||
move |this: &mut Self, _, event: &DebugPanelEvent, cx| {
|
||||
match event {
|
||||
DebugPanelEvent::Stopped((client_id, event)) => {
|
||||
Self::handle_stopped_event(this, client_id, event, cx)
|
||||
}
|
||||
DebugPanelEvent::Thread((client_id, event)) => {
|
||||
Self::handle_thread_event(this, client_id, event, cx)
|
||||
}
|
||||
};
|
||||
}
|
||||
})];
|
||||
|
||||
Self {
|
||||
client,
|
||||
thread_id,
|
||||
focus_handle,
|
||||
_subscriptions,
|
||||
stack_frame_list,
|
||||
current_stack_frame_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn should_skip_event(
|
||||
this: &mut Self,
|
||||
client_id: &DebugAdapterClientId,
|
||||
thread_id: u64,
|
||||
) -> bool {
|
||||
thread_id != this.thread_id || *client_id != this.client.id()
|
||||
}
|
||||
|
||||
fn handle_stopped_event(
|
||||
this: &mut Self,
|
||||
client_id: &DebugAdapterClientId,
|
||||
event: &StoppedEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if Self::should_skip_event(this, client_id, event.thread_id.unwrap_or_default()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stack_frame_list
|
||||
.reset(this.current_thread_state().stack_frames.len());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
this: &mut Self,
|
||||
client_id: &DebugAdapterClientId,
|
||||
event: &ThreadEvent,
|
||||
_: &mut ViewContext<Self>,
|
||||
) {
|
||||
if Self::should_skip_event(this, client_id, event.thread_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: handle thread event
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ItemEvent> for DebugPanelItem {}
|
||||
|
||||
impl FocusableView for DebugPanelItem {
|
||||
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for DebugPanelItem {
|
||||
type Event = ItemEvent;
|
||||
|
||||
fn tab_content(
|
||||
&self,
|
||||
params: workspace::item::TabContentParams,
|
||||
_: &WindowContext,
|
||||
) -> AnyElement {
|
||||
Label::new(format!(
|
||||
"{} - Thread {}",
|
||||
self.client.config().id,
|
||||
self.thread_id
|
||||
))
|
||||
.color(if params.selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
|
||||
Some(SharedString::from(format!(
|
||||
"{} Thread {} - {:?}",
|
||||
self.client.config().id,
|
||||
self.thread_id,
|
||||
self.current_thread_state().status
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl DebugPanelItem {
|
||||
pub fn client(&self) -> Arc<DebugAdapterClient> {
|
||||
self.client.clone()
|
||||
}
|
||||
|
||||
pub fn thread_id(&self) -> u64 {
|
||||
self.thread_id
|
||||
}
|
||||
|
||||
fn stack_frame_for_index(&self, ix: usize) -> StackFrame {
|
||||
self.client
|
||||
.thread_state_by_id(self.thread_id)
|
||||
.stack_frames
|
||||
.get(ix)
|
||||
.cloned()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn current_thread_state(&self) -> ThreadState {
|
||||
self.client
|
||||
.thread_states()
|
||||
.get(&self.thread_id)
|
||||
.cloned()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn render_stack_frames(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w_1_3()
|
||||
.gap_3()
|
||||
.h_full()
|
||||
.child(list(self.stack_frame_list.clone()).size_full())
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_stack_frame(&self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let stack_frame = self.stack_frame_for_index(ix);
|
||||
|
||||
let source = stack_frame.source.clone();
|
||||
let selected_frame_id = self.current_stack_frame_id;
|
||||
let is_selected_frame = Some(stack_frame.id) == selected_frame_id;
|
||||
|
||||
let formatted_path = format!(
|
||||
"{}:{}",
|
||||
source.clone().and_then(|s| s.name).unwrap_or_default(),
|
||||
stack_frame.line,
|
||||
);
|
||||
|
||||
v_flex()
|
||||
.rounded_md()
|
||||
.group("")
|
||||
.id(("stack-frame", stack_frame.id))
|
||||
.tooltip({
|
||||
let formatted_path = formatted_path.clone();
|
||||
move |cx| Tooltip::text(formatted_path.clone(), cx)
|
||||
})
|
||||
.p_1()
|
||||
.when(is_selected_frame, |this| {
|
||||
this.bg(cx.theme().colors().element_hover)
|
||||
})
|
||||
.on_click(cx.listener({
|
||||
let stack_frame = stack_frame.clone();
|
||||
move |this, _, _| {
|
||||
this.current_stack_frame_id = Some(stack_frame.id);
|
||||
|
||||
// let client = this.client();
|
||||
// DebugPanel::go_to_stack_frame(&stack_frame, client, true, cx)
|
||||
// .detach_and_log_err(cx);
|
||||
|
||||
// TODO:
|
||||
// this.go_to_stack_frame(&stack_frame, this.client.clone(), false, cx)
|
||||
// .detach_and_log_err(cx);
|
||||
// cx.notify();
|
||||
}
|
||||
}))
|
||||
.hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer())
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.text_ui_sm(cx)
|
||||
.child(stack_frame.name.clone())
|
||||
.child(formatted_path),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.text_ui_xs(cx)
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.when_some(source.and_then(|s| s.path), |this, path| this.child(path)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_scopes(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let thread_state = self.current_thread_state();
|
||||
let Some(scopes) = thread_state
|
||||
.current_stack_frame_id
|
||||
.and_then(|id| thread_state.scopes.get(&id))
|
||||
else {
|
||||
return div().child("No scopes for this thread yet").into_any();
|
||||
};
|
||||
|
||||
div()
|
||||
.w_3_4()
|
||||
.gap_3()
|
||||
.text_ui_sm(cx)
|
||||
.children(
|
||||
scopes
|
||||
.iter()
|
||||
.map(|scope| self.render_scope(&thread_state, scope, cx)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_scope(
|
||||
&self,
|
||||
thread_state: &ThreadState,
|
||||
scope: &Scope,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
div()
|
||||
.id(("scope", scope.variables_reference))
|
||||
.p_1()
|
||||
.text_ui_sm(cx)
|
||||
.hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer())
|
||||
.child(scope.name.clone())
|
||||
.child(
|
||||
div()
|
||||
.ml_2()
|
||||
.child(self.render_variables(thread_state, scope, cx)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_variables(
|
||||
&self,
|
||||
thread_state: &ThreadState,
|
||||
scope: &Scope,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let Some(variables) = thread_state.variables.get(&scope.variables_reference) else {
|
||||
return div().child("No variables for this thread yet").into_any();
|
||||
};
|
||||
|
||||
div()
|
||||
.gap_3()
|
||||
.text_ui_sm(cx)
|
||||
.children(
|
||||
variables
|
||||
.iter()
|
||||
.map(|variable| self.render_variable(variable, cx)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_variable(&self, variable: &Variable, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
.id(("variable", variable.variables_reference))
|
||||
.p_1()
|
||||
.gap_1()
|
||||
.text_ui_sm(cx)
|
||||
.hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer())
|
||||
.child(variable.name.clone())
|
||||
.child(
|
||||
div()
|
||||
.text_ui_xs(cx)
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(variable.value.clone()),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
// if the debug adapter does not send the continued event,
|
||||
// and the status of the thread did not change we have to assume the thread is running
|
||||
// so we have to update the thread state status to running
|
||||
fn update_thread_state(
|
||||
this: WeakView<Self>,
|
||||
previous_status: ThreadStatus,
|
||||
all_threads_continued: Option<bool>,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if previous_status == this.current_thread_state().status {
|
||||
if all_threads_continued.unwrap_or(false) {
|
||||
for thread in this.client.thread_states().values_mut() {
|
||||
thread.status = ThreadStatus::Running;
|
||||
}
|
||||
} else {
|
||||
this.client
|
||||
.update_thread_state_status(this.thread_id, ThreadStatus::Running);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_continue_action(&mut self, _: &Continue, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
let thread_id = self.thread_id;
|
||||
let previous_status = self.current_thread_state().status;
|
||||
|
||||
cx.spawn(|this, cx| async move {
|
||||
let response = client.resume(thread_id).await?;
|
||||
|
||||
Self::update_thread_state(this, previous_status, response.all_threads_continued, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn handle_step_over_action(&mut self, _: &StepOver, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
let thread_id = self.thread_id;
|
||||
let previous_status = self.current_thread_state().status;
|
||||
|
||||
cx.spawn(|this, cx| async move {
|
||||
client.step_over(thread_id).await?;
|
||||
|
||||
Self::update_thread_state(this, previous_status, None, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn handle_step_in_action(&mut self, _: &StepIn, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
let thread_id = self.thread_id;
|
||||
let previous_status = self.current_thread_state().status;
|
||||
|
||||
cx.spawn(|this, cx| async move {
|
||||
client.step_in(thread_id).await?;
|
||||
|
||||
Self::update_thread_state(this, previous_status, None, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn handle_step_out_action(&mut self, _: &StepOut, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
let thread_id = self.thread_id;
|
||||
let previous_status = self.current_thread_state().status;
|
||||
|
||||
cx.spawn(|this, cx| async move {
|
||||
client.step_out(thread_id).await?;
|
||||
|
||||
Self::update_thread_state(this, previous_status, None, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn handle_restart_action(&mut self, _: &Restart, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(async move { client.restart().await })
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn handle_pause_action(&mut self, _: &Pause, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
let thread_id = self.thread_id;
|
||||
cx.background_executor()
|
||||
.spawn(async move { client.pause(thread_id).await })
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn handle_stop_action(&mut self, _: &Stop, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
cx.background_executor()
|
||||
.spawn(async move { client.stop().await })
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DebugPanelItem {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let thread_status = self.current_thread_state().status;
|
||||
|
||||
v_flex()
|
||||
.key_context("DebugPanelItem")
|
||||
.track_focus(&self.focus_handle)
|
||||
.capture_action(cx.listener(Self::handle_continue_action))
|
||||
.capture_action(cx.listener(Self::handle_step_over_action))
|
||||
.capture_action(cx.listener(Self::handle_step_in_action))
|
||||
.capture_action(cx.listener(Self::handle_step_out_action))
|
||||
.capture_action(cx.listener(Self::handle_restart_action))
|
||||
.capture_action(cx.listener(Self::handle_pause_action))
|
||||
.capture_action(cx.listener(Self::handle_stop_action))
|
||||
.p_2()
|
||||
.size_full()
|
||||
.items_start()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.map(|this| {
|
||||
if self.current_thread_state().status == ThreadStatus::Running {
|
||||
this.child(
|
||||
IconButton::new("debug-pause", IconName::DebugPause)
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Pause))),
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::text("Pause program", cx)),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
IconButton::new("debug-continue", IconName::DebugContinue)
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.dispatch_action(Box::new(Continue))
|
||||
}))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |cx| Tooltip::text("Continue program", cx)),
|
||||
)
|
||||
}
|
||||
})
|
||||
.child(
|
||||
IconButton::new("debug-step-over", IconName::DebugStepOver)
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.dispatch_action(Box::new(StepOver))),
|
||||
)
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |cx| Tooltip::text("Step over", cx)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-step-in", IconName::DebugStepInto)
|
||||
.on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(StepIn))))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |cx| Tooltip::text("Step in", cx)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-step-out", IconName::DebugStepOut)
|
||||
.on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(StepOut))))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |cx| Tooltip::text("Step out", cx)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-restart", IconName::DebugRestart)
|
||||
.on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Restart))))
|
||||
.disabled(
|
||||
!self
|
||||
.client
|
||||
.capabilities()
|
||||
.supports_restart_request
|
||||
.unwrap_or_default()
|
||||
|| thread_status != ThreadStatus::Stopped
|
||||
&& thread_status != ThreadStatus::Running,
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::text("Restart", cx)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-stop", IconName::DebugStop)
|
||||
.on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Stop))))
|
||||
.disabled(
|
||||
thread_status != ThreadStatus::Stopped
|
||||
&& thread_status != ThreadStatus::Running,
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::text("Stop", cx)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.size_full()
|
||||
.items_start()
|
||||
.p_1()
|
||||
.gap_4()
|
||||
.child(self.render_stack_frames(cx))
|
||||
.child(self.render_scopes(cx)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
18
crates/debugger_ui/src/lib.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use debugger_panel::{DebugPanel, ToggleFocus};
|
||||
use gpui::AppContext;
|
||||
use workspace::{StartDebugger, Workspace};
|
||||
|
||||
pub mod debugger_panel;
|
||||
mod debugger_panel_item;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _| {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|
||||
workspace.toggle_panel_focus::<DebugPanel>(cx);
|
||||
});
|
||||
workspace.register_action(|workspace: &mut Workspace, _: &StartDebugger, cx| {
|
||||
tasks_ui::toggle_modal(workspace, cx, task::TaskType::Debug).detach();
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -1320,9 +1320,8 @@ fn render_same_line_diagnostics(
|
||||
let editor_handle = editor_handle.clone();
|
||||
let parent = h_flex()
|
||||
.items_start()
|
||||
.child(v_flex().size_full().when_some_else(
|
||||
toggle_expand_label,
|
||||
|parent, label| {
|
||||
.child(v_flex().size_full().map(|parent| {
|
||||
if let Some(label) = toggle_expand_label {
|
||||
parent.child(Button::new(cx.block_id, label).on_click({
|
||||
let diagnostics = Arc::clone(&diagnostics);
|
||||
move |_, cx| {
|
||||
@@ -1353,16 +1352,15 @@ fn render_same_line_diagnostics(
|
||||
});
|
||||
}
|
||||
}))
|
||||
},
|
||||
|parent| {
|
||||
} else {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.size(IconSize::default().rems())
|
||||
.invisible()
|
||||
.flex_none(),
|
||||
)
|
||||
},
|
||||
));
|
||||
}
|
||||
}));
|
||||
let max_message_rows = if expanded {
|
||||
None
|
||||
} else {
|
||||
|
||||
@@ -28,7 +28,7 @@ test-support = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
aho-corasick = "1.1"
|
||||
aho-corasick.workspace = true
|
||||
anyhow.workspace = true
|
||||
assets.workspace = true
|
||||
chrono.workspace = true
|
||||
@@ -43,7 +43,7 @@ futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
http.workspace = true
|
||||
http_client.workspace = true
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
@@ -98,4 +98,4 @@ tree-sitter-typescript.workspace = true
|
||||
unindent.workspace = true
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
http = { workspace = true, features = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -293,6 +293,7 @@ gpui::actions!(
|
||||
SplitSelectionIntoLines,
|
||||
Tab,
|
||||
TabPrev,
|
||||
ToggleBreakpoint,
|
||||
ToggleAutoSignatureHelp,
|
||||
ToggleGitBlame,
|
||||
ToggleGitBlameInline,
|
||||
|
||||
@@ -561,7 +561,7 @@ impl DisplaySnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
// used by line_mode selections and tries to match vim behaviour
|
||||
// used by line_mode selections and tries to match vim behavior
|
||||
pub fn expand_to_line(&self, range: Range<Point>) -> Range<Point> {
|
||||
let new_start = if range.start.row == 0 {
|
||||
MultiBufferPoint::new(0, 0)
|
||||
|
||||
@@ -11,13 +11,14 @@
|
||||
//!
|
||||
//! All other submodules and structs are mostly concerned with holding editor data about the way it displays current buffer region(s).
|
||||
//!
|
||||
//! If you're looking to improve Vim mode, you should check out Vim crate that wraps Editor and overrides its behaviour.
|
||||
//! If you're looking to improve Vim mode, you should check out Vim crate that wraps Editor and overrides its behavior.
|
||||
pub mod actions;
|
||||
mod blame_entry_tooltip;
|
||||
mod blink_manager;
|
||||
mod debounced_delay;
|
||||
pub mod display_map;
|
||||
mod editor_settings;
|
||||
mod editor_settings_controls;
|
||||
mod element;
|
||||
mod git;
|
||||
mod highlight_matching_bracket;
|
||||
@@ -57,6 +58,7 @@ use debounced_delay::DebouncedDelay;
|
||||
use display_map::*;
|
||||
pub use display_map::{DisplayPoint, FoldPlaceholder};
|
||||
pub use editor_settings::{CurrentLineHighlight, EditorSettings};
|
||||
pub use editor_settings_controls::*;
|
||||
use element::LineWithInvisibles;
|
||||
pub use element::{
|
||||
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
|
||||
@@ -447,6 +449,13 @@ struct ResolvedTasks {
|
||||
templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
|
||||
position: Anchor,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Breakpoint {
|
||||
row: MultiBufferRow,
|
||||
_line: BufferRow,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
struct MultiBufferOffset(usize);
|
||||
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
|
||||
@@ -533,7 +542,7 @@ pub struct Editor {
|
||||
gutter_hovered: bool,
|
||||
hovered_link_state: Option<HoveredLinkState>,
|
||||
inline_completion_provider: Option<RegisteredInlineCompletionProvider>,
|
||||
active_inline_completion: Option<Inlay>,
|
||||
active_inline_completion: Option<(Inlay, Option<Range<Anchor>>)>,
|
||||
show_inline_completions: bool,
|
||||
inlay_hint_cache: InlayHintCache,
|
||||
expanded_hunks: ExpandedHunks,
|
||||
@@ -566,6 +575,7 @@ pub struct Editor {
|
||||
expect_bounds_change: Option<Bounds<Pixels>>,
|
||||
tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
|
||||
tasks_update_task: Option<Task<()>>,
|
||||
breakpoints: BTreeMap<(BufferId, BufferRow), Breakpoint>,
|
||||
previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
|
||||
file_header_size: u8,
|
||||
breadcrumb_header: Option<String>,
|
||||
@@ -592,7 +602,7 @@ pub struct EditorSnapshot {
|
||||
|
||||
const GIT_BLAME_GUTTER_WIDTH_CHARS: f32 = 53.;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub struct GutterDimensions {
|
||||
pub left_padding: Pixels,
|
||||
pub right_padding: Pixels,
|
||||
@@ -615,18 +625,6 @@ impl GutterDimensions {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GutterDimensions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
left_padding: Pixels::ZERO,
|
||||
right_padding: Pixels::ZERO,
|
||||
width: Pixels::ZERO,
|
||||
margin: Pixels::ZERO,
|
||||
git_blame_entries_width: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RemoteSelection {
|
||||
pub replica_id: ReplicaId,
|
||||
@@ -1893,6 +1891,7 @@ impl Editor {
|
||||
blame_subscription: None,
|
||||
file_header_size,
|
||||
tasks: Default::default(),
|
||||
breakpoints: Default::default(),
|
||||
_subscriptions: vec![
|
||||
cx.observe(&buffer, Self::on_buffer_changed),
|
||||
cx.subscribe(&buffer, Self::on_buffer_event),
|
||||
@@ -1953,7 +1952,7 @@ impl Editor {
|
||||
EditorMode::Full => "full",
|
||||
};
|
||||
|
||||
if EditorSettings::get_global(cx).jupyter.enabled {
|
||||
if EditorSettings::jupyter_enabled(cx) {
|
||||
key_context.add("jupyter");
|
||||
}
|
||||
|
||||
@@ -4953,7 +4952,7 @@ impl Editor {
|
||||
_: &AcceptInlineCompletion,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let Some(completion) = self.take_active_inline_completion(cx) else {
|
||||
let Some((completion, delete_range)) = self.take_active_inline_completion(cx) else {
|
||||
return;
|
||||
};
|
||||
if let Some(provider) = self.inline_completion_provider() {
|
||||
@@ -4964,6 +4963,10 @@ impl Editor {
|
||||
utf16_range_to_replace: None,
|
||||
text: completion.text.to_string().into(),
|
||||
});
|
||||
|
||||
if let Some(range) = delete_range {
|
||||
self.change_selections(None, cx, |s| s.select_ranges([range]))
|
||||
}
|
||||
self.insert_with_autoindent_mode(&completion.text.to_string(), None, cx);
|
||||
self.refresh_inline_completion(true, cx);
|
||||
cx.notify();
|
||||
@@ -4975,7 +4978,7 @@ impl Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if self.selections.count() == 1 && self.has_active_inline_completion(cx) {
|
||||
if let Some(completion) = self.take_active_inline_completion(cx) {
|
||||
if let Some((completion, delete_range)) = self.take_active_inline_completion(cx) {
|
||||
let mut partial_completion = completion
|
||||
.text
|
||||
.chars()
|
||||
@@ -4995,7 +4998,12 @@ impl Editor {
|
||||
utf16_range_to_replace: None,
|
||||
text: partial_completion.clone().into(),
|
||||
});
|
||||
|
||||
if let Some(range) = delete_range {
|
||||
self.change_selections(None, cx, |s| s.select_ranges([range]))
|
||||
}
|
||||
self.insert_with_autoindent_mode(&partial_completion, None, cx);
|
||||
|
||||
self.refresh_inline_completion(true, cx);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -5017,20 +5025,23 @@ impl Editor {
|
||||
pub fn has_active_inline_completion(&self, cx: &AppContext) -> bool {
|
||||
if let Some(completion) = self.active_inline_completion.as_ref() {
|
||||
let buffer = self.buffer.read(cx).read(cx);
|
||||
completion.position.is_valid(&buffer)
|
||||
completion.0.position.is_valid(&buffer)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn take_active_inline_completion(&mut self, cx: &mut ViewContext<Self>) -> Option<Inlay> {
|
||||
fn take_active_inline_completion(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<(Inlay, Option<Range<Anchor>>)> {
|
||||
let completion = self.active_inline_completion.take()?;
|
||||
self.display_map.update(cx, |map, cx| {
|
||||
map.splice_inlays(vec![completion.id], Default::default(), cx);
|
||||
map.splice_inlays(vec![completion.0.id], Default::default(), cx);
|
||||
});
|
||||
let buffer = self.buffer.read(cx).read(cx);
|
||||
|
||||
if completion.position.is_valid(&buffer) {
|
||||
if completion.0.position.is_valid(&buffer) {
|
||||
Some(completion)
|
||||
} else {
|
||||
None
|
||||
@@ -5041,6 +5052,8 @@ impl Editor {
|
||||
let selection = self.selections.newest_anchor();
|
||||
let cursor = selection.head();
|
||||
|
||||
let excerpt_id = cursor.excerpt_id;
|
||||
|
||||
if self.context_menu.read().is_none()
|
||||
&& self.completion_tasks.is_empty()
|
||||
&& selection.start == selection.end
|
||||
@@ -5049,18 +5062,28 @@ impl Editor {
|
||||
if let Some((buffer, cursor_buffer_position)) =
|
||||
self.buffer.read(cx).text_anchor_for_position(cursor, cx)
|
||||
{
|
||||
if let Some(text) =
|
||||
if let Some((text, text_anchor_range)) =
|
||||
provider.active_completion_text(&buffer, cursor_buffer_position, cx)
|
||||
{
|
||||
let text = Rope::from(text);
|
||||
let mut to_remove = Vec::new();
|
||||
if let Some(completion) = self.active_inline_completion.take() {
|
||||
to_remove.push(completion.id);
|
||||
to_remove.push(completion.0.id);
|
||||
}
|
||||
|
||||
let completion_inlay =
|
||||
Inlay::suggestion(post_inc(&mut self.next_inlay_id), cursor, text);
|
||||
self.active_inline_completion = Some(completion_inlay.clone());
|
||||
|
||||
let multibuffer_anchor_range = text_anchor_range.and_then(|range| {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
Some(
|
||||
snapshot.anchor_in_excerpt(excerpt_id, range.start)?
|
||||
..snapshot.anchor_in_excerpt(excerpt_id, range.end)?,
|
||||
)
|
||||
});
|
||||
self.active_inline_completion =
|
||||
Some((completion_inlay.clone(), multibuffer_anchor_range));
|
||||
|
||||
self.display_map.update(cx, move |map, cx| {
|
||||
map.splice_inlays(to_remove, vec![completion_inlay], cx)
|
||||
});
|
||||
@@ -5118,6 +5141,17 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_breakpoint(&self, row: DisplayRow, cx: &mut ViewContext<Self>) -> IconButton {
|
||||
IconButton::new(("breakpoint_indicator", row.0 as usize), ui::IconName::Play)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.size(ui::ButtonSize::None)
|
||||
.icon_color(Color::Error)
|
||||
.on_click(cx.listener(move |editor, _e, cx| {
|
||||
editor.focus(cx);
|
||||
editor.toggle_breakpoint_at_row(row.0, cx) //TODO handle folded
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_run_indicator(
|
||||
&self,
|
||||
_style: &EditorStyle,
|
||||
@@ -5141,7 +5175,7 @@ impl Editor {
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_close_hunk_diff_button(
|
||||
fn close_hunk_diff_button(
|
||||
&self,
|
||||
hunk: HoveredHunk,
|
||||
row: DisplayRow,
|
||||
@@ -5936,6 +5970,38 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_breakpoint(&mut self, _: &ToggleBreakpoint, cx: &mut ViewContext<Self>) {
|
||||
let cursor_position: Point = self.selections.newest(cx).head();
|
||||
self.toggle_breakpoint_at_row(cursor_position.row, cx);
|
||||
}
|
||||
|
||||
pub fn toggle_breakpoint_at_row(&mut self, row: u32, cx: &mut ViewContext<Self>) {
|
||||
let Some(project) = &self.project else {
|
||||
return;
|
||||
};
|
||||
let Some(buffer) = self.buffer.read(cx).as_singleton() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
let key = (buffer_id, row);
|
||||
|
||||
if self.breakpoints.remove(&key).is_none() {
|
||||
self.breakpoints.insert(
|
||||
key,
|
||||
Breakpoint {
|
||||
row: MultiBufferRow(row),
|
||||
_line: row,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
project.update_breakpoint(buffer, row + 1, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn gather_revert_changes(
|
||||
&mut self,
|
||||
selections: &[Selection<Anchor>],
|
||||
@@ -8912,6 +8978,31 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn go_to_line<T: 'static>(
|
||||
&mut self,
|
||||
row: u32,
|
||||
column: u32,
|
||||
highlight_color: Option<Hsla>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let snapshot = self.snapshot(cx).display_snapshot;
|
||||
let point = snapshot
|
||||
.buffer_snapshot
|
||||
.clip_point(Point::new(row, column), Bias::Left);
|
||||
let anchor = snapshot.buffer_snapshot.anchor_before(point);
|
||||
self.clear_row_highlights::<T>();
|
||||
self.highlight_rows::<T>(
|
||||
anchor..=anchor,
|
||||
Some(
|
||||
highlight_color
|
||||
.unwrap_or_else(|| cx.theme().colors().editor_highlighted_line_background),
|
||||
),
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
self.request_autoscroll(Autoscroll::center(), cx);
|
||||
}
|
||||
|
||||
fn seek_in_direction(
|
||||
&mut self,
|
||||
snapshot: &DisplaySnapshot,
|
||||
@@ -10360,7 +10451,7 @@ impl Editor {
|
||||
};
|
||||
let fs = workspace.read(cx).app_state().fs.clone();
|
||||
let current_show = TabBarSettings::get_global(cx).show;
|
||||
update_settings_file::<TabBarSettings>(fs, cx, move |setting| {
|
||||
update_settings_file::<TabBarSettings>(fs, cx, move |setting, _| {
|
||||
setting.show = Some(!current_show);
|
||||
});
|
||||
}
|
||||
@@ -10833,17 +10924,6 @@ impl Editor {
|
||||
color_fetcher: fn(&ThemeColors) -> Hsla,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let snapshot = self.snapshot(cx);
|
||||
// this is to try and catch a panic sooner
|
||||
for range in ranges {
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.summary_for_anchor::<usize>(&range.start);
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.summary_for_anchor::<usize>(&range.end);
|
||||
}
|
||||
|
||||
self.background_highlights
|
||||
.insert(TypeId::of::<T>(), (color_fetcher, Arc::from(ranges)));
|
||||
self.scrollbar_marker_state.dirty = true;
|
||||
@@ -11839,6 +11919,11 @@ impl Editor {
|
||||
let source_y = line_height * (source.row() - first_visible_line.row()).0 as f32;
|
||||
Some(gpui::Point::new(source_x, source_y))
|
||||
}
|
||||
|
||||
fn gutter_bounds(&self) -> Option<Bounds<Pixels>> {
|
||||
let bounds = self.last_bounds?;
|
||||
Some(element::gutter_bounds(bounds, self.gutter_dimensions))
|
||||
}
|
||||
}
|
||||
|
||||
fn hunks_for_selections(
|
||||
@@ -12225,7 +12310,7 @@ impl EditorSnapshot {
|
||||
self.scroll_anchor.scroll_position(&self.display_snapshot)
|
||||
}
|
||||
|
||||
pub fn gutter_dimensions(
|
||||
fn gutter_dimensions(
|
||||
&self,
|
||||
font_id: FontId,
|
||||
font_size: Pixels,
|
||||
|
||||
@@ -28,7 +28,6 @@ pub struct EditorSettings {
|
||||
pub search_wrap: bool,
|
||||
pub auto_signature_help: bool,
|
||||
pub show_signature_help_after_edits: bool,
|
||||
#[serde(default)]
|
||||
pub jupyter: Jupyter,
|
||||
}
|
||||
|
||||
@@ -69,15 +68,23 @@ pub enum DoubleClickInMultibuffer {
|
||||
Open,
|
||||
}
|
||||
|
||||
#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Jupyter {
|
||||
/// Whether the Jupyter feature is enabled.
|
||||
///
|
||||
/// Default: `false`
|
||||
/// Default: true
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct JupyterContent {
|
||||
/// Whether the Jupyter feature is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct Toolbar {
|
||||
pub breadcrumbs: bool,
|
||||
@@ -247,7 +254,7 @@ pub struct EditorSettingsContent {
|
||||
pub show_signature_help_after_edits: Option<bool>,
|
||||
|
||||
/// Jupyter REPL settings.
|
||||
pub jupyter: Option<Jupyter>,
|
||||
pub jupyter: Option<JupyterContent>,
|
||||
}
|
||||
|
||||
// Toolbar related settings
|
||||
@@ -318,6 +325,12 @@ pub struct GutterContent {
|
||||
pub folds: Option<bool>,
|
||||
}
|
||||
|
||||
impl EditorSettings {
|
||||
pub fn jupyter_enabled(cx: &AppContext) -> bool {
|
||||
EditorSettings::get_global(cx).jupyter.enabled
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings for EditorSettings {
|
||||
const KEY: Option<&'static str> = None;
|
||||
|
||||
|
||||
244
crates/editor/src/editor_settings_controls.rs
Normal file
@@ -0,0 +1,244 @@
|
||||
use gpui::{AppContext, FontWeight};
|
||||
use project::project_settings::{InlineBlameSettings, ProjectSettings};
|
||||
use settings::{EditableSettingControl, Settings};
|
||||
use theme::{FontFamilyCache, ThemeSettings};
|
||||
use ui::{
|
||||
prelude::*, CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer,
|
||||
SettingsGroup,
|
||||
};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct EditorSettingsControls {}
|
||||
|
||||
impl EditorSettingsControls {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for EditorSettingsControls {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
SettingsContainer::new()
|
||||
.child(
|
||||
SettingsGroup::new("Font")
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(BufferFontFamilyControl)
|
||||
.child(BufferFontWeightControl),
|
||||
)
|
||||
.child(BufferFontSizeControl),
|
||||
)
|
||||
.child(SettingsGroup::new("Editor").child(InlineGitBlameControl))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct BufferFontFamilyControl;
|
||||
|
||||
impl EditableSettingControl for BufferFontFamilyControl {
|
||||
type Value = SharedString;
|
||||
type Settings = ThemeSettings;
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Buffer Font Family".into()
|
||||
}
|
||||
|
||||
fn read(cx: &AppContext) -> Self::Value {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
settings.buffer_font.family.clone()
|
||||
}
|
||||
|
||||
fn apply(
|
||||
settings: &mut <Self::Settings as Settings>::FileContent,
|
||||
value: Self::Value,
|
||||
_cx: &AppContext,
|
||||
) {
|
||||
settings.buffer_font_family = Some(value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for BufferFontFamilyControl {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let value = Self::read(cx);
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::Font))
|
||||
.child(DropdownMenu::new(
|
||||
"buffer-font-family",
|
||||
value.clone(),
|
||||
ContextMenu::build(cx, |mut menu, cx| {
|
||||
let font_family_cache = FontFamilyCache::global(cx);
|
||||
|
||||
for font_name in font_family_cache.list_font_families(cx) {
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
let font_name = font_name.clone();
|
||||
move |_cx| Label::new(font_name.clone()).into_any_element()
|
||||
},
|
||||
{
|
||||
let font_name = font_name.clone();
|
||||
move |cx| {
|
||||
Self::write(font_name.clone(), cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
menu
|
||||
}),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct BufferFontSizeControl;
|
||||
|
||||
impl EditableSettingControl for BufferFontSizeControl {
|
||||
type Value = Pixels;
|
||||
type Settings = ThemeSettings;
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Buffer Font Size".into()
|
||||
}
|
||||
|
||||
fn read(cx: &AppContext) -> Self::Value {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
settings.buffer_font_size
|
||||
}
|
||||
|
||||
fn apply(
|
||||
settings: &mut <Self::Settings as Settings>::FileContent,
|
||||
value: Self::Value,
|
||||
_cx: &AppContext,
|
||||
) {
|
||||
settings.buffer_font_size = Some(value.into());
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for BufferFontSizeControl {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let value = Self::read(cx);
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::FontSize))
|
||||
.child(NumericStepper::new(
|
||||
value.to_string(),
|
||||
move |_, cx| {
|
||||
Self::write(value - px(1.), cx);
|
||||
},
|
||||
move |_, cx| {
|
||||
Self::write(value + px(1.), cx);
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct BufferFontWeightControl;
|
||||
|
||||
impl EditableSettingControl for BufferFontWeightControl {
|
||||
type Value = FontWeight;
|
||||
type Settings = ThemeSettings;
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Buffer Font Weight".into()
|
||||
}
|
||||
|
||||
fn read(cx: &AppContext) -> Self::Value {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
settings.buffer_font.weight
|
||||
}
|
||||
|
||||
fn apply(
|
||||
settings: &mut <Self::Settings as Settings>::FileContent,
|
||||
value: Self::Value,
|
||||
_cx: &AppContext,
|
||||
) {
|
||||
settings.buffer_font_weight = Some(value.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for BufferFontWeightControl {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let value = Self::read(cx);
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::FontWeight))
|
||||
.child(DropdownMenu::new(
|
||||
"buffer-font-weight",
|
||||
value.0.to_string(),
|
||||
ContextMenu::build(cx, |mut menu, _cx| {
|
||||
for weight in FontWeight::ALL {
|
||||
menu = menu.custom_entry(
|
||||
move |_cx| Label::new(weight.0.to_string()).into_any_element(),
|
||||
{
|
||||
move |cx| {
|
||||
Self::write(weight, cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
menu
|
||||
}),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct InlineGitBlameControl;
|
||||
|
||||
impl EditableSettingControl for InlineGitBlameControl {
|
||||
type Value = bool;
|
||||
type Settings = ProjectSettings;
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Inline Git Blame".into()
|
||||
}
|
||||
|
||||
fn read(cx: &AppContext) -> Self::Value {
|
||||
let settings = ProjectSettings::get_global(cx);
|
||||
settings.git.inline_blame_enabled()
|
||||
}
|
||||
|
||||
fn apply(
|
||||
settings: &mut <Self::Settings as Settings>::FileContent,
|
||||
value: Self::Value,
|
||||
_cx: &AppContext,
|
||||
) {
|
||||
if let Some(inline_blame) = settings.git.inline_blame.as_mut() {
|
||||
inline_blame.enabled = value;
|
||||
} else {
|
||||
settings.git.inline_blame = Some(InlineBlameSettings {
|
||||
enabled: false,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for InlineGitBlameControl {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let value = Self::read(cx);
|
||||
|
||||
CheckboxWithLabel::new(
|
||||
"inline-git-blame",
|
||||
Label::new(self.name()),
|
||||
value.into(),
|
||||
|selection, cx| {
|
||||
Self::write(
|
||||
match selection {
|
||||
Selection::Selected => true,
|
||||
Selection::Unselected | Selection::Indeterminate => false,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ use language::{
|
||||
FakeLspAdapter, IndentGuide, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override,
|
||||
ParsedMarkdown, Point,
|
||||
};
|
||||
use language_settings::IndentGuideSettings;
|
||||
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
|
||||
use multi_buffer::MultiBufferIndentGuide;
|
||||
use parking_lot::Mutex;
|
||||
use project::FakeFs;
|
||||
@@ -4716,12 +4716,13 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx));
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
|
||||
view.condition::<crate::EditorEvent>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||
editor
|
||||
.condition::<crate::EditorEvent>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||
.await;
|
||||
|
||||
_ = view.update(cx, |view, cx| {
|
||||
editor.update(cx, |view, cx| {
|
||||
view.change_selections(None, cx, |s| {
|
||||
s.select_display_ranges([
|
||||
DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
|
||||
@@ -4731,94 +4732,126 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(cx, |view, cx| { view.selections.display_ranges(cx) }),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 23)..DisplayPoint::new(DisplayRow(0), 27),
|
||||
DisplayPoint::new(DisplayRow(2), 35)..DisplayPoint::new(DisplayRow(2), 7),
|
||||
DisplayPoint::new(DisplayRow(3), 15)..DisplayPoint::new(DisplayRow(3), 21),
|
||||
]
|
||||
);
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::{mod3, «mod4ˇ»};
|
||||
|
||||
_ = view.update(cx, |view, cx| {
|
||||
fn fn_1«ˇ(param1: bool, param2: &str)» {
|
||||
let var1 = "«textˇ»";
|
||||
}
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
editor.update(cx, |view, cx| {
|
||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||
});
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::«{mod3, mod4}ˇ»;
|
||||
|
||||
«ˇfn fn_1(param1: bool, param2: &str) {
|
||||
let var1 = "text";
|
||||
}»
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
editor.update(cx, |view, cx| {
|
||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 28),
|
||||
DisplayPoint::new(DisplayRow(4), 1)..DisplayPoint::new(DisplayRow(2), 0),
|
||||
]
|
||||
);
|
||||
|
||||
_ = view.update(cx, |view, cx| {
|
||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
|
||||
);
|
||||
|
||||
// Trying to expand the selected syntax node one more time has no effect.
|
||||
_ = view.update(cx, |view, cx| {
|
||||
editor.update(cx, |view, cx| {
|
||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
|
||||
);
|
||||
|
||||
_ = view.update(cx, |view, cx| {
|
||||
editor.update(cx, |view, cx| {
|
||||
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 28),
|
||||
DisplayPoint::new(DisplayRow(4), 1)..DisplayPoint::new(DisplayRow(2), 0),
|
||||
]
|
||||
);
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::«{mod3, mod4}ˇ»;
|
||||
|
||||
_ = view.update(cx, |view, cx| {
|
||||
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||
«ˇfn fn_1(param1: bool, param2: &str) {
|
||||
let var1 = "text";
|
||||
}»
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 23)..DisplayPoint::new(DisplayRow(0), 27),
|
||||
DisplayPoint::new(DisplayRow(2), 35)..DisplayPoint::new(DisplayRow(2), 7),
|
||||
DisplayPoint::new(DisplayRow(3), 15)..DisplayPoint::new(DisplayRow(3), 21),
|
||||
]
|
||||
);
|
||||
|
||||
_ = view.update(cx, |view, cx| {
|
||||
editor.update(cx, |view, cx| {
|
||||
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
|
||||
DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
|
||||
DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
|
||||
]
|
||||
);
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::{mod3, «mod4ˇ»};
|
||||
|
||||
fn fn_1«ˇ(param1: bool, param2: &str)» {
|
||||
let var1 = "«textˇ»";
|
||||
}
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
editor.update(cx, |view, cx| {
|
||||
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||
});
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::{mod3, mo«ˇ»d4};
|
||||
|
||||
fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
|
||||
let var1 = "te«ˇ»xt";
|
||||
}
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Trying to shrink the selected syntax node one more time has no effect.
|
||||
_ = view.update(cx, |view, cx| {
|
||||
editor.update(cx, |view, cx| {
|
||||
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
|
||||
DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
|
||||
DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
|
||||
]
|
||||
);
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::{mod3, mo«ˇ»d4};
|
||||
|
||||
fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
|
||||
let var1 = "te«ˇ»xt";
|
||||
}
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Ensure that we keep expanding the selection if the larger selection starts or ends within
|
||||
// a fold.
|
||||
_ = view.update(cx, |view, cx| {
|
||||
editor.update(cx, |view, cx| {
|
||||
view.fold_ranges(
|
||||
vec![
|
||||
(
|
||||
@@ -4835,14 +4868,19 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||
);
|
||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 28),
|
||||
DisplayPoint::new(DisplayRow(2), 35)..DisplayPoint::new(DisplayRow(2), 7),
|
||||
DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(3), 23),
|
||||
]
|
||||
);
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
indoc! {r#"
|
||||
use mod1::mod2::«{mod3, mod4}ˇ»;
|
||||
|
||||
fn fn_1«ˇ(param1: bool, param2: &str)» {
|
||||
«let var1 = "text";ˇ»
|
||||
}
|
||||
"#},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -6253,8 +6291,8 @@ async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
let worktree = project.update(cx, |project, _| {
|
||||
let mut worktrees = project.worktrees().collect::<Vec<_>>();
|
||||
let worktree = project.update(cx, |project, cx| {
|
||||
let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
|
||||
assert_eq!(worktrees.len(), 1);
|
||||
worktrees.pop().unwrap()
|
||||
});
|
||||
@@ -6559,7 +6597,9 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
|
||||
#[gpui::test]
|
||||
async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer)
|
||||
settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
|
||||
FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
|
||||
))
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
@@ -6720,7 +6760,7 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
|
||||
#[gpui::test]
|
||||
async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.formatter = Some(language_settings::Formatter::Auto)
|
||||
settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
|
||||
});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
@@ -8171,11 +8211,13 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
|
||||
);
|
||||
cx.executor().run_until_parked();
|
||||
cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
|
||||
// TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
|
||||
// Uncommenting and commenting from this position brings in even more wrong artifacts.
|
||||
cx.assert_editor_state(
|
||||
&r#"
|
||||
<!-- ˇ<script> -->
|
||||
// ˇvar x = new Y();
|
||||
<!-- ˇ</script> -->
|
||||
// ˇ</script>
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
@@ -9317,7 +9359,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
|
||||
let worktree_id = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.worktrees().next().unwrap().read(cx).id()
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
@@ -9723,7 +9765,9 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
|
||||
#[gpui::test]
|
||||
async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.formatter = Some(language_settings::Formatter::Prettier)
|
||||
settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
|
||||
FormatterList(vec![Formatter::Prettier].into()),
|
||||
))
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
@@ -9783,7 +9827,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
|
||||
);
|
||||
|
||||
update_test_language_settings(cx, |settings| {
|
||||
settings.defaults.formatter = Some(language_settings::Formatter::Auto)
|
||||
settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
|
||||
});
|
||||
let format = editor.update(cx, |editor, cx| {
|
||||
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
|
||||
|
||||
@@ -400,7 +400,8 @@ impl EditorElement {
|
||||
register_action(view, cx, Editor::accept_partial_inline_completion);
|
||||
register_action(view, cx, Editor::accept_inline_completion);
|
||||
register_action(view, cx, Editor::revert_selected_hunks);
|
||||
register_action(view, cx, Editor::open_active_item_in_terminal)
|
||||
register_action(view, cx, Editor::open_active_item_in_terminal);
|
||||
register_action(view, cx, Editor::toggle_breakpoint);
|
||||
}
|
||||
|
||||
fn register_key_listeners(&self, cx: &mut WindowContext, layout: &EditorLayout) {
|
||||
@@ -1248,7 +1249,7 @@ impl EditorElement {
|
||||
|
||||
// Folds contained in a hunk are ignored apart from shrinking visual size
|
||||
// If a fold contains any hunks then that fold line is marked as modified
|
||||
fn layout_git_gutters(
|
||||
fn layout_gutter_git_hunks(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
gutter_hitbox: &Hitbox,
|
||||
@@ -1553,12 +1554,53 @@ impl EditorElement {
|
||||
(offset_y, length)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_breakpoints(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
gutter_hitbox: &Hitbox,
|
||||
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
|
||||
snapshot: &EditorSnapshot,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<AnyElement> {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.breakpoints
|
||||
.iter()
|
||||
.filter_map(|(_, breakpoint)| {
|
||||
if snapshot.is_line_folded(breakpoint.row) {
|
||||
return None;
|
||||
}
|
||||
let display_row = Point::new(breakpoint.row.0, 0)
|
||||
.to_display_point(snapshot)
|
||||
.row();
|
||||
let button = editor.render_breakpoint(display_row, cx);
|
||||
|
||||
let button = prepaint_gutter_button(
|
||||
button,
|
||||
display_row,
|
||||
line_height,
|
||||
gutter_dimensions,
|
||||
scroll_pixel_position,
|
||||
gutter_hitbox,
|
||||
rows_with_hunk_bounds,
|
||||
cx,
|
||||
);
|
||||
Some(button)
|
||||
})
|
||||
.collect_vec()
|
||||
})
|
||||
}
|
||||
|
||||
fn layout_run_indicators(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
gutter_hitbox: &Hitbox,
|
||||
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
|
||||
snapshot: &EditorSnapshot,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<AnyElement> {
|
||||
@@ -1602,6 +1644,7 @@ impl EditorElement {
|
||||
gutter_dimensions,
|
||||
scroll_pixel_position,
|
||||
gutter_hitbox,
|
||||
rows_with_hunk_bounds,
|
||||
cx,
|
||||
);
|
||||
Some(button)
|
||||
@@ -1610,6 +1653,7 @@ impl EditorElement {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_code_actions_indicator(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
@@ -1617,6 +1661,7 @@ impl EditorElement {
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
gutter_hitbox: &Hitbox,
|
||||
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<AnyElement> {
|
||||
let mut active = false;
|
||||
@@ -1640,6 +1685,7 @@ impl EditorElement {
|
||||
gutter_dimensions,
|
||||
scroll_pixel_position,
|
||||
gutter_hitbox,
|
||||
rows_with_hunk_bounds,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -1951,16 +1997,20 @@ impl EditorElement {
|
||||
.x_for_index(align_to.column() as usize)
|
||||
};
|
||||
|
||||
block.render(&mut BlockContext {
|
||||
context: cx,
|
||||
anchor_x,
|
||||
gutter_dimensions,
|
||||
line_height,
|
||||
em_width,
|
||||
block_id,
|
||||
max_width: text_hitbox.size.width.max(*scroll_width),
|
||||
editor_style: &self.style,
|
||||
})
|
||||
div()
|
||||
.size_full()
|
||||
.child(block.render(&mut BlockContext {
|
||||
context: cx,
|
||||
anchor_x,
|
||||
gutter_dimensions,
|
||||
line_height,
|
||||
em_width,
|
||||
block_id,
|
||||
max_width: text_hitbox.size.width.max(*scroll_width),
|
||||
editor_style: &self.style,
|
||||
}))
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
Block::ExcerptHeader {
|
||||
@@ -2805,6 +2855,9 @@ impl EditorElement {
|
||||
em_width: Pixels,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
if !self.editor.focus_handle(cx).is_focused(cx) {
|
||||
return;
|
||||
}
|
||||
let Some(newest_selection_head) = newest_selection_head else {
|
||||
return;
|
||||
};
|
||||
@@ -3163,7 +3216,7 @@ impl EditorElement {
|
||||
});
|
||||
}
|
||||
|
||||
fn diff_hunk_bounds(
|
||||
pub(super) fn diff_hunk_bounds(
|
||||
snapshot: &EditorSnapshot,
|
||||
line_height: Pixels,
|
||||
gutter_bounds: Bounds<Pixels>,
|
||||
@@ -3239,6 +3292,9 @@ impl EditorElement {
|
||||
}
|
||||
});
|
||||
|
||||
for breakpoint in layout.breakpoints.iter_mut() {
|
||||
breakpoint.paint(cx);
|
||||
}
|
||||
for test_indicator in layout.test_indicators.iter_mut() {
|
||||
test_indicator.paint(cx);
|
||||
}
|
||||
@@ -4033,20 +4089,22 @@ impl EditorElement {
|
||||
self.column_pixels(digit_count, cx)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_hunk_diff_close_indicators(
|
||||
&self,
|
||||
expanded_hunks_by_rows: HashMap<DisplayRow, ExpandedHunk>,
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
gutter_hitbox: &Hitbox,
|
||||
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
|
||||
expanded_hunks_by_rows: HashMap<DisplayRow, ExpandedHunk>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<AnyElement> {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
expanded_hunks_by_rows
|
||||
.into_iter()
|
||||
.map(|(display_row, hunk)| {
|
||||
let button = editor.render_close_hunk_diff_button(
|
||||
let button = editor.close_hunk_diff_button(
|
||||
HoveredHunk {
|
||||
multi_buffer_range: hunk.hunk_range,
|
||||
status: hunk.status,
|
||||
@@ -4063,6 +4121,7 @@ impl EditorElement {
|
||||
gutter_dimensions,
|
||||
scroll_pixel_position,
|
||||
gutter_hitbox,
|
||||
rows_with_hunk_bounds,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -4071,6 +4130,7 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepaint_gutter_button(
|
||||
button: IconButton,
|
||||
row: DisplayRow,
|
||||
@@ -4078,6 +4138,7 @@ fn prepaint_gutter_button(
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_hitbox: &Hitbox,
|
||||
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> AnyElement {
|
||||
let mut button = button.into_any_element();
|
||||
@@ -4087,14 +4148,16 @@ fn prepaint_gutter_button(
|
||||
);
|
||||
let indicator_size = button.layout_as_root(available_space, cx);
|
||||
|
||||
let blame_width = gutter_dimensions
|
||||
.git_blame_entries_width
|
||||
.unwrap_or(Pixels::ZERO);
|
||||
let blame_offset = gutter_dimensions.git_blame_entries_width;
|
||||
let gutter_offset = rows_with_hunk_bounds
|
||||
.get(&row)
|
||||
.map(|bounds| bounds.origin.x + bounds.size.width);
|
||||
let left_offset = blame_offset.max(gutter_offset).unwrap_or(Pixels::ZERO);
|
||||
|
||||
let mut x = blame_width;
|
||||
let mut x = left_offset;
|
||||
let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
|
||||
- indicator_size.width
|
||||
- blame_width;
|
||||
- left_offset;
|
||||
x += available_width / 2.;
|
||||
|
||||
let mut y = row.as_f32() * line_height - scroll_pixel_position.y;
|
||||
@@ -4942,13 +5005,8 @@ impl Element for EditorElement {
|
||||
.collect::<SmallVec<[_; 2]>>();
|
||||
|
||||
let hitbox = cx.insert_hitbox(bounds, false);
|
||||
let gutter_hitbox = cx.insert_hitbox(
|
||||
Bounds {
|
||||
origin: bounds.origin,
|
||||
size: size(gutter_dimensions.width, bounds.size.height),
|
||||
},
|
||||
false,
|
||||
);
|
||||
let gutter_hitbox =
|
||||
cx.insert_hitbox(gutter_bounds(bounds, gutter_dimensions), false);
|
||||
let text_hitbox = cx.insert_hitbox(
|
||||
Bounds {
|
||||
origin: gutter_hitbox.upper_right(),
|
||||
@@ -5071,7 +5129,7 @@ impl Element for EditorElement {
|
||||
self.layout_crease_trailers(buffer_rows.iter().copied(), &snapshot, cx)
|
||||
});
|
||||
|
||||
let display_hunks = self.layout_git_gutters(
|
||||
let display_hunks = self.layout_gutter_git_hunks(
|
||||
line_height,
|
||||
&gutter_hitbox,
|
||||
start_row..end_row,
|
||||
@@ -5298,6 +5356,27 @@ impl Element for EditorElement {
|
||||
.collect::<HashMap<_, _>>()
|
||||
});
|
||||
|
||||
let rows_with_hunk_bounds = display_hunks
|
||||
.iter()
|
||||
.filter_map(|(hunk, hitbox)| Some((hunk, hitbox.as_ref()?.bounds)))
|
||||
.fold(
|
||||
HashMap::default(),
|
||||
|mut rows_with_hunk_bounds, (hunk, bounds)| {
|
||||
match hunk {
|
||||
DisplayDiffHunk::Folded { display_row } => {
|
||||
rows_with_hunk_bounds.insert(*display_row, bounds);
|
||||
}
|
||||
DisplayDiffHunk::Unfolded {
|
||||
display_row_range, ..
|
||||
} => {
|
||||
for display_row in display_row_range.iter_rows() {
|
||||
rows_with_hunk_bounds.insert(display_row, bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
rows_with_hunk_bounds
|
||||
},
|
||||
);
|
||||
let mut _context_menu_visible = false;
|
||||
let mut code_actions_indicator = None;
|
||||
if let Some(newest_selection_head) = newest_selection_head {
|
||||
@@ -5346,6 +5425,7 @@ impl Element for EditorElement {
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
&rows_with_hunk_bounds,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -5355,12 +5435,23 @@ impl Element for EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
let breakpoints = self.layout_breakpoints(
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
&rows_with_hunk_bounds,
|
||||
&snapshot,
|
||||
cx,
|
||||
);
|
||||
|
||||
let test_indicators = if gutter_settings.runnables {
|
||||
self.layout_run_indicators(
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
&rows_with_hunk_bounds,
|
||||
&snapshot,
|
||||
cx,
|
||||
)
|
||||
@@ -5369,11 +5460,12 @@ impl Element for EditorElement {
|
||||
};
|
||||
|
||||
let close_indicators = self.layout_hunk_diff_close_indicators(
|
||||
expanded_add_hunks_by_rows,
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
&rows_with_hunk_bounds,
|
||||
expanded_add_hunks_by_rows,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -5488,6 +5580,7 @@ impl Element for EditorElement {
|
||||
selections,
|
||||
mouse_context_menu,
|
||||
test_indicators,
|
||||
breakpoints,
|
||||
close_indicators,
|
||||
code_actions_indicator,
|
||||
gutter_fold_toggles,
|
||||
@@ -5565,17 +5658,17 @@ impl Element for EditorElement {
|
||||
|
||||
self.paint_text(layout, cx);
|
||||
|
||||
if layout.gutter_hitbox.size.width > Pixels::ZERO {
|
||||
self.paint_gutter_highlights(layout, cx);
|
||||
self.paint_gutter_indicators(layout, cx);
|
||||
}
|
||||
|
||||
if !layout.blocks.is_empty() {
|
||||
cx.with_element_namespace("blocks", |cx| {
|
||||
self.paint_blocks(layout, cx);
|
||||
});
|
||||
}
|
||||
|
||||
if layout.gutter_hitbox.size.width > Pixels::ZERO {
|
||||
self.paint_gutter_highlights(layout, cx);
|
||||
self.paint_gutter_indicators(layout, cx);
|
||||
}
|
||||
|
||||
self.paint_scrollbar(layout, cx);
|
||||
self.paint_mouse_context_menu(layout, cx);
|
||||
});
|
||||
@@ -5584,6 +5677,16 @@ impl Element for EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn gutter_bounds(
|
||||
editor_bounds: Bounds<Pixels>,
|
||||
gutter_dimensions: GutterDimensions,
|
||||
) -> Bounds<Pixels> {
|
||||
Bounds {
|
||||
origin: editor_bounds.origin,
|
||||
size: size(gutter_dimensions.width, editor_bounds.size.height),
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for EditorElement {
|
||||
type Element = Self;
|
||||
|
||||
@@ -5620,6 +5723,7 @@ pub struct EditorLayout {
|
||||
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
|
||||
code_actions_indicator: Option<AnyElement>,
|
||||
test_indicators: Vec<AnyElement>,
|
||||
breakpoints: Vec<AnyElement>,
|
||||
close_indicators: Vec<AnyElement>,
|
||||
gutter_fold_toggles: Vec<Option<AnyElement>>,
|
||||
crease_trailers: Vec<Option<CreaseTrailerLayout>>,
|
||||
|
||||
@@ -7,7 +7,7 @@ use git::{
|
||||
parse_git_remote_url, GitHostingProvider, GitHostingProviderRegistry, Oid, PullRequest,
|
||||
};
|
||||
use gpui::{Model, ModelContext, Subscription, Task};
|
||||
use http::HttpClient;
|
||||
use http_client::HttpClient;
|
||||
use language::{markdown, Bias, Buffer, BufferSnapshot, Edit, LanguageRegistry, ParsedMarkdown};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use project::{Item, Project};
|
||||
|
||||
@@ -786,6 +786,7 @@ mod tests {
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
||||
definition_provider: Some(lsp::OneOf::Left(true)),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
|
||||
@@ -213,22 +213,8 @@ fn show_hover(
|
||||
};
|
||||
|
||||
if !ignore_timeout {
|
||||
if editor
|
||||
.hover_state
|
||||
.info_popovers
|
||||
.iter()
|
||||
.any(|InfoPopover { symbol_range, .. }| {
|
||||
symbol_range
|
||||
.as_text_range()
|
||||
.map(|range| {
|
||||
let hover_range = range.to_offset(&snapshot.buffer_snapshot);
|
||||
let offset = anchor.to_offset(&snapshot.buffer_snapshot);
|
||||
// LSP returns a hover result for the end index of ranges that should be hovered, so we need to
|
||||
// use an inclusive range here to check if we should dismiss the popover
|
||||
(hover_range.start..=hover_range.end).contains(&offset)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
})
|
||||
if same_info_hover(editor, &snapshot, anchor)
|
||||
|| same_diagnostic_hover(editor, &snapshot, anchor)
|
||||
{
|
||||
// Hover triggered from same location as last time. Don't show again.
|
||||
return;
|
||||
@@ -375,6 +361,43 @@ fn show_hover(
|
||||
editor.hover_state.info_task = Some(task);
|
||||
}
|
||||
|
||||
fn same_info_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anchor) -> bool {
|
||||
editor
|
||||
.hover_state
|
||||
.info_popovers
|
||||
.iter()
|
||||
.any(|InfoPopover { symbol_range, .. }| {
|
||||
symbol_range
|
||||
.as_text_range()
|
||||
.map(|range| {
|
||||
let hover_range = range.to_offset(&snapshot.buffer_snapshot);
|
||||
let offset = anchor.to_offset(&snapshot.buffer_snapshot);
|
||||
// LSP returns a hover result for the end index of ranges that should be hovered, so we need to
|
||||
// use an inclusive range here to check if we should dismiss the popover
|
||||
(hover_range.start..=hover_range.end).contains(&offset)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
fn same_diagnostic_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anchor) -> bool {
|
||||
editor
|
||||
.hover_state
|
||||
.diagnostic_popover
|
||||
.as_ref()
|
||||
.map(|diagnostic| {
|
||||
let hover_range = diagnostic
|
||||
.local_diagnostic
|
||||
.range
|
||||
.to_offset(&snapshot.buffer_snapshot);
|
||||
let offset = anchor.to_offset(&snapshot.buffer_snapshot);
|
||||
|
||||
// Here we do basically the same as in `same_info_hover`, see comment there for an explanation
|
||||
(hover_range.start..=hover_range.end).contains(&offset)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn parse_blocks(
|
||||
blocks: &[HoverBlock],
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
@@ -522,7 +545,7 @@ impl HoverState {
|
||||
pub fn focused(&self, cx: &mut ViewContext<Editor>) -> bool {
|
||||
let mut hover_popover_is_focused = false;
|
||||
for info_popover in &self.info_popovers {
|
||||
for markdown_view in &info_popover.parsed_content {
|
||||
if let Some(markdown_view) = &info_popover.parsed_content {
|
||||
if markdown_view.focus_handle(cx).is_focused(cx) {
|
||||
hover_popover_is_focused = true;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use git::diff::{DiffHunk, DiffHunkStatus};
|
||||
use gpui::{Action, AppContext, Hsla, Model, MouseButton, Subscription, Task, View};
|
||||
use gpui::{Action, AppContext, CursorStyle, Hsla, Model, MouseButton, Subscription, Task, View};
|
||||
use language::Buffer;
|
||||
use multi_buffer::{
|
||||
Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, ToPoint,
|
||||
@@ -13,7 +13,7 @@ use multi_buffer::{
|
||||
use settings::SettingsStore;
|
||||
use text::{BufferId, Point};
|
||||
use ui::{
|
||||
h_flex, v_flex, ActiveTheme, Context as _, ContextMenu, InteractiveElement, IntoElement,
|
||||
div, h_flex, v_flex, ActiveTheme, Context as _, ContextMenu, InteractiveElement, IntoElement,
|
||||
ParentElement, Pixels, Styled, ViewContext, VisualContext,
|
||||
};
|
||||
use util::{debug_panic, RangeExt};
|
||||
@@ -24,8 +24,8 @@ use crate::{
|
||||
hunk_status, hunks_for_selections,
|
||||
mouse_context_menu::MouseContextMenu,
|
||||
BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, Editor,
|
||||
EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt, RevertSelectedHunks, ToDisplayPoint,
|
||||
ToggleHunkDiff,
|
||||
EditorElement, EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt, RevertSelectedHunks,
|
||||
ToDisplayPoint, ToggleHunkDiff,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -430,7 +430,6 @@ impl Editor {
|
||||
let (editor_height, editor_with_deleted_text) =
|
||||
editor_with_deleted_text(diff_base_buffer, deleted_hunk_color, hunk, cx);
|
||||
let editor = cx.view().clone();
|
||||
let editor_model = cx.model().clone();
|
||||
let hunk = hunk.clone();
|
||||
let mut new_block_ids = self.insert_blocks(
|
||||
Some(BlockProperties {
|
||||
@@ -439,38 +438,84 @@ impl Editor {
|
||||
style: BlockStyle::Flex,
|
||||
disposition: BlockDisposition::Above,
|
||||
render: Box::new(move |cx| {
|
||||
let close_button = editor.update(cx.context, |editor, cx| {
|
||||
let editor_snapshot = editor.snapshot(cx);
|
||||
let hunk_start_row = hunk
|
||||
.multi_buffer_range
|
||||
.start
|
||||
.to_display_point(&editor_snapshot)
|
||||
.row();
|
||||
editor.render_close_hunk_diff_button(hunk.clone(), hunk_start_row, cx)
|
||||
});
|
||||
let gutter_dimensions = editor_model.read(cx).gutter_dimensions;
|
||||
let Some(gutter_bounds) = editor.read(cx).gutter_bounds() else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
let (gutter_dimensions, hunk_bounds, close_button) =
|
||||
editor.update(cx.context, |editor, cx| {
|
||||
let editor_snapshot = editor.snapshot(cx);
|
||||
let hunk_display_range = hunk
|
||||
.multi_buffer_range
|
||||
.clone()
|
||||
.to_display_points(&editor_snapshot);
|
||||
let gutter_dimensions = editor.gutter_dimensions;
|
||||
let hunk_bounds = EditorElement::diff_hunk_bounds(
|
||||
&editor_snapshot,
|
||||
cx.line_height(),
|
||||
gutter_bounds,
|
||||
&DisplayDiffHunk::Unfolded {
|
||||
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
|
||||
multi_buffer_range: hunk.multi_buffer_range.clone(),
|
||||
display_row_range: hunk_display_range.start.row()
|
||||
..hunk_display_range.end.row(),
|
||||
status: hunk.status,
|
||||
},
|
||||
);
|
||||
|
||||
let close_button = editor.close_hunk_diff_button(
|
||||
hunk.clone(),
|
||||
hunk_display_range.start.row(),
|
||||
cx,
|
||||
);
|
||||
(gutter_dimensions, hunk_bounds, close_button)
|
||||
});
|
||||
let click_editor = editor.clone();
|
||||
let clicked_hunk = hunk.clone();
|
||||
h_flex()
|
||||
.id("gutter with editor")
|
||||
.bg(deleted_hunk_color)
|
||||
.size_full()
|
||||
.child(
|
||||
v_flex()
|
||||
.justify_center()
|
||||
h_flex()
|
||||
.id("gutter")
|
||||
.max_w(gutter_dimensions.full_width())
|
||||
.min_w(gutter_dimensions.full_width())
|
||||
.size_full()
|
||||
.on_mouse_down(MouseButton::Left, {
|
||||
let click_hunk = hunk.clone();
|
||||
move |e, cx| {
|
||||
let modifiers = e.modifiers;
|
||||
if modifiers.control || modifiers.platform {
|
||||
click_editor.update(cx, |editor, cx| {
|
||||
editor.toggle_hovered_hunk(&click_hunk, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.child(close_button),
|
||||
.child(
|
||||
h_flex()
|
||||
.id("gutter hunk")
|
||||
.pl(hunk_bounds.origin.x)
|
||||
.max_w(hunk_bounds.size.width)
|
||||
.min_w(hunk_bounds.size.width)
|
||||
.size_full()
|
||||
.cursor(CursorStyle::PointingHand)
|
||||
.on_mouse_down(MouseButton::Left, {
|
||||
let click_hunk = hunk.clone();
|
||||
move |e, cx| {
|
||||
let modifiers = e.modifiers;
|
||||
if modifiers.control || modifiers.platform {
|
||||
click_editor.update(cx, |editor, cx| {
|
||||
editor.toggle_hovered_hunk(&click_hunk, cx);
|
||||
});
|
||||
} else {
|
||||
click_editor.update(cx, |editor, cx| {
|
||||
editor.open_hunk_context_menu(
|
||||
clicked_hunk.clone(),
|
||||
e.position,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.pt(ui::rems(0.25))
|
||||
.justify_start()
|
||||
.child(close_button),
|
||||
),
|
||||
)
|
||||
.child(editor_with_deleted_text.clone())
|
||||
.into_any_element()
|
||||
|
||||