Compare commits
258 Commits
v0.139.3
...
v0.141.0-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3562cad225 | ||
|
|
3d4f275c52 | ||
|
|
cd2533de5a | ||
|
|
acc9648753 | ||
|
|
bc35235800 | ||
|
|
2ca83b2f17 | ||
|
|
ddf07253c4 | ||
|
|
aff7a83815 | ||
|
|
8524e87319 | ||
|
|
59c005b086 | ||
|
|
522692ef50 | ||
|
|
d665f28671 | ||
|
|
8c4fb34f6e | ||
|
|
d4891a62bb | ||
|
|
17bc0d1b17 | ||
|
|
db0d843fb1 | ||
|
|
ad4e52842c | ||
|
|
ca18549e02 | ||
|
|
5cbb360952 | ||
|
|
5ff7c893be | ||
|
|
99e4b3a4cf | ||
|
|
9af4b6bfc7 | ||
|
|
c84d432b5f | ||
|
|
6cea9813ad | ||
|
|
490a75aee6 | ||
|
|
4f364d6d09 | ||
|
|
89d2ace713 | ||
|
|
84a44bef8a | ||
|
|
6b9ddbfef2 | ||
|
|
8af8493da6 | ||
|
|
39edbe1c50 | ||
|
|
f6fa6600bc | ||
|
|
b55961b57a | ||
|
|
01b836a191 | ||
|
|
41180b8d81 | ||
|
|
81475ac4cd | ||
|
|
ba59e66314 | ||
|
|
5ede48337c | ||
|
|
3701e190ce | ||
|
|
5dc26c261d | ||
|
|
64d815a176 | ||
|
|
5dc54863a4 | ||
|
|
e4ba336971 | ||
|
|
195a270e18 | ||
|
|
3a26a4809d | ||
|
|
479c5df491 | ||
|
|
51404d4ea0 | ||
|
|
05c4c7872c | ||
|
|
0af6e442a7 | ||
|
|
7003b0f211 | ||
|
|
f489c8b79f | ||
|
|
be02b2faf4 | ||
|
|
258a8a37d8 | ||
|
|
78e0f71a28 | ||
|
|
59104a08fd | ||
|
|
7aa28c9b24 | ||
|
|
bb1d52b485 | ||
|
|
ca035dbdd8 | ||
|
|
71cc95d315 | ||
|
|
3707734f0a | ||
|
|
e19627d92f | ||
|
|
bb75d87285 | ||
|
|
eecbf203dc | ||
|
|
7fe5c27597 | ||
|
|
221edfc267 | ||
|
|
d95c424d18 | ||
|
|
d6d56191da | ||
|
|
2e87e1d26e | ||
|
|
e8862c45cc | ||
|
|
0c28b6a11a | ||
|
|
16fce64d3a | ||
|
|
b075ce8f04 | ||
|
|
54828ab836 | ||
|
|
6322351f00 | ||
|
|
78091fa91e | ||
|
|
d5735dab9a | ||
|
|
c793bbde84 | ||
|
|
03c54623d4 | ||
|
|
0afb3abfd2 | ||
|
|
2b46a4a0e9 | ||
|
|
bedf57db89 | ||
|
|
4855da53df | ||
|
|
15d3e54ae3 | ||
|
|
064bdab459 | ||
|
|
38cb95f427 | ||
|
|
7cc2538fe1 | ||
|
|
fc19cc0ddf | ||
|
|
e6def62c23 | ||
|
|
ff2347dff5 | ||
|
|
6319ae0b4a | ||
|
|
a8bd602334 | ||
|
|
af45db6d1e | ||
|
|
fab4b01655 | ||
|
|
2f6cb49d84 | ||
|
|
411ee7a47c | ||
|
|
831f7dbbc0 | ||
|
|
78fd378702 | ||
|
|
d5a6ca4914 | ||
|
|
ea69846281 | ||
|
|
ff8486e67f | ||
|
|
9bc3c6810b | ||
|
|
45ae0dcc2d | ||
|
|
e40c49a143 | ||
|
|
0d43d484f6 | ||
|
|
6ca09bd4ba | ||
|
|
53f702c92f | ||
|
|
b03653321f | ||
|
|
993109aee1 | ||
|
|
4cb45e63f4 | ||
|
|
1413b5af93 | ||
|
|
d9c21b4eb1 | ||
|
|
44f66aa426 | ||
|
|
3b84b106e2 | ||
|
|
3539a7c04a | ||
|
|
a8481099ca | ||
|
|
6c28b7e8b8 | ||
|
|
0d8e6e6b12 | ||
|
|
bf03f66d02 | ||
|
|
0f59607100 | ||
|
|
902d7150fe | ||
|
|
55ba80ddd1 | ||
|
|
dcb8dc16ca | ||
|
|
eb7a09b459 | ||
|
|
1248788780 | ||
|
|
64bb79b71d | ||
|
|
a5af5b2883 | ||
|
|
068b1c235c | ||
|
|
8edfd0a963 | ||
|
|
0ed5327b1c | ||
|
|
10d3ad4e33 | ||
|
|
066cdc2297 | ||
|
|
01ba1ddef7 | ||
|
|
86167138a9 | ||
|
|
e0c1ab650e | ||
|
|
407188f816 | ||
|
|
6181ac6bad | ||
|
|
0705fb9b97 | ||
|
|
042be3529d | ||
|
|
1a40e98413 | ||
|
|
af8e7af265 | ||
|
|
ce51c264a6 | ||
|
|
702fd8f168 | ||
|
|
2e758dcb64 | ||
|
|
38d9ee3731 | ||
|
|
95c69d0696 | ||
|
|
599102573a | ||
|
|
da281d6d8f | ||
|
|
22dc88ed3d | ||
|
|
3c0310273b | ||
|
|
14bf07c916 | ||
|
|
284559742d | ||
|
|
85acc2be44 | ||
|
|
6c70a809ec | ||
|
|
a35947c883 | ||
|
|
f8ad5fe3e9 | ||
|
|
e2cfbc54ad | ||
|
|
0a13b9ee01 | ||
|
|
eb7b5a7131 | ||
|
|
7798f64d1b | ||
|
|
21764c38dd | ||
|
|
cfbf5dca7a | ||
|
|
2f43d52e7e | ||
|
|
e1f4dfc068 | ||
|
|
9e3c5f3e12 | ||
|
|
f780504b68 | ||
|
|
76b0120665 | ||
|
|
0ac9af94e0 | ||
|
|
ec086945fc | ||
|
|
8451dba6a7 | ||
|
|
7f56f4e78e | ||
|
|
6fa347dff7 | ||
|
|
c3df9b79c6 | ||
|
|
72dac24acf | ||
|
|
001f17c011 | ||
|
|
3c3dad6830 | ||
|
|
1b28f93c64 | ||
|
|
2fd00a8f35 | ||
|
|
bee3441c78 | ||
|
|
113546f766 | ||
|
|
5e9f9b4edd | ||
|
|
ec95a33d8c | ||
|
|
b82350979f | ||
|
|
e16bbe048f | ||
|
|
ab41eddd8b | ||
|
|
4cb8d6f40e | ||
|
|
127b9ed857 | ||
|
|
c30f6a1582 | ||
|
|
8ccd2a0c99 | ||
|
|
57b87be3a0 | ||
|
|
80c14c9198 | ||
|
|
ec9e700e70 | ||
|
|
a06189bbed | ||
|
|
53b0720d54 | ||
|
|
b6ea393d14 | ||
|
|
98659eabf1 | ||
|
|
3722275cfa | ||
|
|
ef84ce76e3 | ||
|
|
44a58647e4 | ||
|
|
4e98c23463 | ||
|
|
1914a42b1c | ||
|
|
6afed19a00 | ||
|
|
2509af723f | ||
|
|
8078e58494 | ||
|
|
b69c3129d0 | ||
|
|
e2c5ce588b | ||
|
|
7005aaa54d | ||
|
|
9db269735d | ||
|
|
77e88c1ded | ||
|
|
57c40299a5 | ||
|
|
0d5485bd6c | ||
|
|
a600799840 | ||
|
|
05b6581147 | ||
|
|
43d1a8040d | ||
|
|
e829a8c3b0 | ||
|
|
87845a349d | ||
|
|
953393f6ce | ||
|
|
75f8be6a0f | ||
|
|
e174f16d50 | ||
|
|
aa60fc2f19 | ||
|
|
5548773b2e | ||
|
|
3eac83eece | ||
|
|
243a0e764d | ||
|
|
6fa6e0718c | ||
|
|
834089feb1 | ||
|
|
9174858225 | ||
|
|
a910f192db | ||
|
|
5f5e6b8616 | ||
|
|
07dbd2bce8 | ||
|
|
48581167b7 | ||
|
|
00dfd217d8 | ||
|
|
22490f7968 | ||
|
|
880940856d | ||
|
|
c354793871 | ||
|
|
2f057785f7 | ||
|
|
fd39f20842 | ||
|
|
0c7e745be8 | ||
|
|
3000f6ea22 | ||
|
|
377e24b798 | ||
|
|
a0c0f1ebcd | ||
|
|
70ce06cb95 | ||
|
|
9a5b97db00 | ||
|
|
0b75afd322 | ||
|
|
4fd698a093 | ||
|
|
b50846205c | ||
|
|
a574036efd | ||
|
|
89641acf2f | ||
|
|
611bf2d905 | ||
|
|
f476a8bc2a | ||
|
|
d3d0d01571 | ||
|
|
29d29f5a90 | ||
|
|
9824e40878 | ||
|
|
1ad8d6ab1c | ||
|
|
8745719687 | ||
|
|
c7c19609b3 | ||
|
|
428c143fbb | ||
|
|
f3460d440c | ||
|
|
071270fe88 | ||
|
|
a59dd7d06d |
@@ -1,15 +0,0 @@
|
||||
# This config is different from config.toml in this directory, as the latter is recognized by Cargo.
|
||||
# This file is placed in $HOME/.cargo/config.toml on CI runs. Cargo then merges Zeds .cargo/config.toml with $HOME/.cargo/config.toml
|
||||
# with preference for settings from Zeds config.toml.
|
||||
# TL;DR: If a value is set in both ci-config.toml and config.toml, config.toml value takes precedence.
|
||||
# Arrays are merged together though. See: https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure
|
||||
# The intent for this file is to configure CI build process with a divergance from Zed developers experience; for example, in this config file
|
||||
# we use `-D warnings` for rustflags (which makes compilation fail in presence of warnings during build process). Placing that in developers `config.toml`
|
||||
# would be incovenient.
|
||||
# We *could* override things like RUSTFLAGS manually by setting them as environment variables, but that is less DRY; worse yet, if you forget to set proper environment variables
|
||||
# in one spot, that's going to trigger a rebuild of all of the artifacts. Using ci-config.toml we can define these overrides for CI in one spot and not worry about it.
|
||||
[build]
|
||||
rustflags = ["-D", "warnings"]
|
||||
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
5
.cargo/collab-config.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
# This file is used to build collab in a Docker image.
|
||||
# In particular, we don't use clang.
|
||||
[build]
|
||||
# v0 mangling scheme provides more detailed backtraces around closures
|
||||
rustflags = ["-C", "symbol-mangling-version=v0", "--cfg", "tokio_unstable"]
|
||||
@@ -4,3 +4,11 @@ rustflags = ["-C", "symbol-mangling-version=v0", "--cfg", "tokio_unstable"]
|
||||
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
38
.github/ISSUE_TEMPLATE/0_feature_request.yml
vendored
@@ -2,23 +2,23 @@ name: Feature Request
|
||||
description: "Tip: open this issue template from within Zed with the `request feature` command palette action"
|
||||
labels: ["admin read", "triage", "enhancement"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Check for existing issues
|
||||
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
|
||||
options:
|
||||
- label: Completed
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Check for existing issues
|
||||
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
|
||||
options:
|
||||
- label: Completed
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: |
|
||||
If applicable, add mockups / screenshots to help present your vision of the feature
|
||||
description: Drag images into the text input below
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: |
|
||||
If applicable, add mockups / screenshots to help present your vision of the feature
|
||||
description: Drag images into the text input below
|
||||
validations:
|
||||
required: false
|
||||
|
||||
75
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
@@ -1,40 +1,45 @@
|
||||
name: Bug Report
|
||||
description: |
|
||||
Use this template for **non-crash-related** bug reports.
|
||||
Tip: open this issue template from within Zed with the `file bug report` command palette action.
|
||||
Use this template for **non-crash-related** bug reports.
|
||||
Tip: open this issue template from within Zed with the `file bug report` command palette action.
|
||||
labels: ["admin read", "triage", "defect"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Check for existing issues
|
||||
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
|
||||
options:
|
||||
- label: Completed
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug / provide steps to reproduce it
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Check for existing issues
|
||||
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
|
||||
options:
|
||||
- label: Completed
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: If applicable, add mockups / screenshots to help explain present your vision of the feature
|
||||
description: Drag issues into the text input below
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
|
||||
description: |
|
||||
Drag Zed.log into the text input below.
|
||||
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug / provide steps to reproduce it
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: If applicable, add mockups / screenshots to help explain present your vision of the feature
|
||||
description: Drag issues into the text input below
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
|
||||
description: |
|
||||
Drag Zed.log into the text input below.
|
||||
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
|
||||
value: |
|
||||
<details><summary>Zed.log</summary><pre>
|
||||
<!-- Click below this line and paste or drag-and-drop your log-->
|
||||
|
||||
<!-- Click above this line and paste or drag-and-drop your log--></pre></details>
|
||||
validations:
|
||||
required: false
|
||||
|
||||
61
.github/ISSUE_TEMPLATE/2_crash_report.yml
vendored
@@ -1,33 +1,38 @@
|
||||
name: Crash Report
|
||||
description: |
|
||||
Use this template for crash reports.
|
||||
Use this template for crash reports.
|
||||
labels: ["admin read", "triage", "defect", "panic / crash"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Check for existing issues
|
||||
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
|
||||
options:
|
||||
- label: Completed
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug / provide steps to reproduce it
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Check for existing issues
|
||||
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
|
||||
options:
|
||||
- label: Completed
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
|
||||
description: |
|
||||
Drag Zed.log into the text input below.
|
||||
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug / provide steps to reproduce it
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
|
||||
description: |
|
||||
Drag Zed.log into the text input below.
|
||||
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
|
||||
value: |
|
||||
<details><summary>Zed.log</summary><pre>
|
||||
<!-- Click below this line and paste or drag-and-drop your log-->
|
||||
|
||||
<!-- Click above this line and paste or drag-and-drop your log--></pre></details>
|
||||
validations:
|
||||
required: false
|
||||
|
||||
106
.github/workflows/ci.yml
vendored
@@ -38,9 +38,6 @@ jobs:
|
||||
- name: Remove untracked files
|
||||
run: git clean -df
|
||||
|
||||
- name: Set up default .cargo/config.toml
|
||||
run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
|
||||
|
||||
- name: Check spelling
|
||||
run: |
|
||||
if ! which typos > /dev/null; then
|
||||
@@ -54,6 +51,9 @@ jobs:
|
||||
- name: Check unused dependencies
|
||||
uses: bnjbvr/cargo-machete@main
|
||||
|
||||
- name: Check licenses are present
|
||||
run: script/check-licenses
|
||||
|
||||
- name: Check license generation
|
||||
run: script/generate-licenses /tmp/zed_licenses_output
|
||||
|
||||
@@ -74,8 +74,8 @@ jobs:
|
||||
version: v1.29.0
|
||||
- uses: bufbuild/buf-breaking-action@v1
|
||||
with:
|
||||
input: "crates/rpc/proto/"
|
||||
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/rpc/proto/"
|
||||
input: "crates/proto/proto/"
|
||||
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/"
|
||||
|
||||
macos_tests:
|
||||
timeout-minutes: 60
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
clean: false
|
||||
|
||||
- name: cargo clippy
|
||||
run: cargo xtask clippy
|
||||
run: ./script/clippy
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
@@ -101,7 +101,6 @@ jobs:
|
||||
- name: Build other binaries and features
|
||||
run: cargo build --workspace --bins --all-features; cargo check -p gpui --features "macos-blade"
|
||||
|
||||
# todo(linux): Actually run the tests
|
||||
linux_tests:
|
||||
timeout-minutes: 60
|
||||
name: (Linux) Run Clippy and tests
|
||||
@@ -118,7 +117,10 @@ jobs:
|
||||
clean: false
|
||||
|
||||
- name: cargo clippy
|
||||
run: cargo xtask clippy
|
||||
run: ./script/clippy
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
- name: Build Zed
|
||||
run: cargo build -p zed
|
||||
@@ -140,7 +142,7 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: cargo clippy
|
||||
run: cargo xtask clippy
|
||||
run: ./script/clippy
|
||||
|
||||
- name: Build Zed
|
||||
run: cargo build -p zed
|
||||
@@ -305,9 +307,6 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create and upload Linux .tar.gz bundle
|
||||
run: script/bundle-linux
|
||||
|
||||
@@ -328,3 +327,86 @@ jobs:
|
||||
body: ""
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
bundle-linux-aarch64:
|
||||
timeout-minutes: 60
|
||||
name: Create arm64 Linux bundle
|
||||
runs-on:
|
||||
- hosted-linux-arm-1
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
needs: [linux_tests]
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
- name: "Setup jq"
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
|
||||
- name: Set up Clang
|
||||
run: |
|
||||
sudo apt-get update
|
||||
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 libzstd-dev libvulkan1 libgit2-dev
|
||||
echo "/usr/lib/llvm-10/bin" >> $GITHUB_PATH
|
||||
|
||||
- uses: rui314/setup-mold@v1
|
||||
with:
|
||||
mold_version: 2.32.0
|
||||
|
||||
- name: rustup
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Determine version and release channel
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
version=$(script/get-crate-version zed)
|
||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||
echo "Publishing version: ${version} on release channel ${channel}"
|
||||
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
|
||||
|
||||
expected_tag_name=""
|
||||
case ${channel} in
|
||||
stable)
|
||||
expected_tag_name="v${version}";;
|
||||
preview)
|
||||
expected_tag_name="v${version}-pre";;
|
||||
nightly)
|
||||
expected_tag_name="v${version}-nightly";;
|
||||
*)
|
||||
echo "can't publish a release on channel ${channel}"
|
||||
exit 1;;
|
||||
esac
|
||||
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
|
||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create and upload Linux .tar.gz bundle
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Linux bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@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
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
files: target/release/zed-linux-aarch64.tar.gz
|
||||
body: ""
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
2
.github/workflows/danger.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: 9
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
5
.github/workflows/deploy_collab.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
uses: ./.github/actions/check_style
|
||||
|
||||
- name: Run clippy
|
||||
run: cargo xtask clippy
|
||||
run: ./script/clippy
|
||||
|
||||
tests:
|
||||
name: Run tests
|
||||
@@ -75,6 +75,9 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Set up default .cargo/config.toml
|
||||
run: cp ./.cargo/collab-config.toml ./.cargo/config.toml
|
||||
|
||||
- name: Build docker image
|
||||
run: docker build . --build-arg GITHUB_SHA=$GITHUB_SHA --tag registry.digitalocean.com/zed/collab:$GITHUB_SHA
|
||||
|
||||
|
||||
2
.github/workflows/release_nightly.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
uses: ./.github/actions/check_style
|
||||
|
||||
- name: Run clippy
|
||||
run: cargo xtask clippy
|
||||
run: ./script/clippy
|
||||
tests:
|
||||
timeout-minutes: 60
|
||||
name: Run tests
|
||||
|
||||
17
.mailmap
@@ -9,12 +9,18 @@
|
||||
# Keep these entries sorted alphabetically.
|
||||
# In Zed: `editor: sort lines case sensitive`
|
||||
|
||||
Alex Viscreanu <alexviscreanu@gmail.com>
|
||||
Alex Viscreanu <alexviscreanu@gmail.com> <alexandru.viscreanu@kiwi.com>
|
||||
Antonio Scandurra <me@as-cii.com>
|
||||
Antonio Scandurra <me@as-cii.com> <antonio@zed.dev>
|
||||
Bennet Bo Fenner <bennet@zed.dev>
|
||||
Bennet Bo Fenner <bennet@zed.dev> <53836821+bennetbo@users.noreply.github.com>
|
||||
Bennet Bo Fenner <bennet@zed.dev> <bennetbo@gmx.de>
|
||||
Christian Bergschneider <christian.bergschneider@gmx.de>
|
||||
Christian Bergschneider <christian.bergschneider@gmx.de> <magiclake@gmx.de>
|
||||
Conrad Irwin <conrad@zed.dev>
|
||||
Conrad Irwin <conrad@zed.dev> <conrad.irwin@gmail.com>
|
||||
Evren Sen <146845123+evrsen@users.noreply.github.com>
|
||||
Fernando Tagawa <tagawafernando@gmail.com>
|
||||
Fernando Tagawa <tagawafernando@gmail.com> <fernando.tagawa.gamail.com@gmail.com>
|
||||
Greg Morenz <greg-morenz@droid.cafe>
|
||||
@@ -48,12 +54,23 @@ Nate Butler <iamnbutler@gmail.com> <nate@zed.dev>
|
||||
Nathan Sobo <nathan@zed.dev>
|
||||
Nathan Sobo <nathan@zed.dev> <nathan@warp.dev>
|
||||
Nathan Sobo <nathan@zed.dev> <nathansobo@gmail.com>
|
||||
Peter Tripp <peter@zed.dev>
|
||||
Peter Tripp <peter@zed.dev> <petertripp@gmail.com>
|
||||
Petros Amoiridis <petros@hey.com>
|
||||
Petros Amoiridis <petros@hey.com> <petros@zed.dev>
|
||||
Piotr Osiewicz <piotr@zed.dev>
|
||||
Piotr Osiewicz <piotr@zed.dev> <24362066+osiewicz@users.noreply.github.com>
|
||||
Rashid Almheiri <r.muhairi@pm.me>
|
||||
Rashid Almheiri <r.muhairi@pm.me> <69181766+huwaireb@users.noreply.github.com>
|
||||
Richard Feldman <oss@rtfeldman.com>
|
||||
Richard Feldman <oss@rtfeldman.com> <richard@zed.dev>
|
||||
Robert Clover <git@clo4.net>
|
||||
Robert Clover <git@clo4.net> <robert@clover.gdn>
|
||||
Sergey Onufrienko <sergey@onufrienko.com>
|
||||
Thorsten Ball <thorsten@zed.dev>
|
||||
Thorsten Ball <thorsten@zed.dev> <me@thorstenball.com>
|
||||
Thorsten Ball <thorsten@zed.dev> <mrnugget@gmail.com>
|
||||
Vitaly Slobodin <vitaliy.slobodin@gmail.com>
|
||||
Vitaly Slobodin <vitaliy.slobodin@gmail.com> <vitaly_slobodin@fastmail.com>
|
||||
WindSoilder <WindSoilder@outlook.com>
|
||||
张小白 <364772080@qq.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"label": "clippy",
|
||||
"command": "cargo",
|
||||
"args": ["xtask", "clippy"]
|
||||
"command": "./script/clippy",
|
||||
"args": []
|
||||
}
|
||||
]
|
||||
|
||||
594
Cargo.lock
generated
36
Cargo.toml
@@ -61,22 +61,28 @@ members = [
|
||||
"crates/multi_buffer",
|
||||
"crates/node_runtime",
|
||||
"crates/notifications",
|
||||
"crates/ollama",
|
||||
"crates/open_ai",
|
||||
"crates/outline",
|
||||
"crates/outline_panel",
|
||||
"crates/paths",
|
||||
"crates/picker",
|
||||
"crates/prettier",
|
||||
"crates/project",
|
||||
"crates/project_panel",
|
||||
"crates/project_symbols",
|
||||
"crates/proto",
|
||||
"crates/quick_action_bar",
|
||||
"crates/recent_projects",
|
||||
"crates/refineable",
|
||||
"crates/refineable/derive_refineable",
|
||||
"crates/release_channel",
|
||||
"crates/dev_server_projects",
|
||||
"crates/repl",
|
||||
"crates/rich_text",
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/rustdoc",
|
||||
"crates/task",
|
||||
"crates/tasks_ui",
|
||||
"crates/search",
|
||||
@@ -130,6 +136,7 @@ members = [
|
||||
"extensions/prisma",
|
||||
"extensions/purescript",
|
||||
"extensions/ruby",
|
||||
"extensions/snippets",
|
||||
"extensions/svelte",
|
||||
"extensions/terraform",
|
||||
"extensions/toml",
|
||||
@@ -163,7 +170,6 @@ clock = { path = "crates/clock" }
|
||||
collab = { path = "crates/collab" }
|
||||
collab_ui = { path = "crates/collab_ui" }
|
||||
collections = { path = "crates/collections" }
|
||||
color = { path = "crates/color" }
|
||||
command_palette = { path = "crates/command_palette" }
|
||||
command_palette_hooks = { path = "crates/command_palette_hooks" }
|
||||
copilot = { path = "crates/copilot" }
|
||||
@@ -207,13 +213,17 @@ menu = { path = "crates/menu" }
|
||||
multi_buffer = { path = "crates/multi_buffer" }
|
||||
node_runtime = { path = "crates/node_runtime" }
|
||||
notifications = { path = "crates/notifications" }
|
||||
ollama = { path = "crates/ollama" }
|
||||
open_ai = { path = "crates/open_ai" }
|
||||
outline = { path = "crates/outline" }
|
||||
outline_panel = { path = "crates/outline_panel" }
|
||||
paths = { path = "crates/paths" }
|
||||
picker = { path = "crates/picker" }
|
||||
plugin = { path = "crates/plugin" }
|
||||
plugin_macros = { path = "crates/plugin_macros" }
|
||||
prettier = { path = "crates/prettier" }
|
||||
project = { path = "crates/project" }
|
||||
proto = { path = "crates/proto" }
|
||||
worktree = { path = "crates/worktree" }
|
||||
project_panel = { path = "crates/project_panel" }
|
||||
project_symbols = { path = "crates/project_symbols" }
|
||||
@@ -221,9 +231,11 @@ quick_action_bar = { path = "crates/quick_action_bar" }
|
||||
recent_projects = { path = "crates/recent_projects" }
|
||||
release_channel = { path = "crates/release_channel" }
|
||||
dev_server_projects = { path = "crates/dev_server_projects" }
|
||||
repl = { path = "crates/repl" }
|
||||
rich_text = { path = "crates/rich_text" }
|
||||
rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
rustdoc = { path = "crates/rustdoc" }
|
||||
task = { path = "crates/task" }
|
||||
tasks_ui = { path = "crates/tasks_ui" }
|
||||
search = { path = "crates/search" }
|
||||
@@ -257,29 +269,32 @@ workspace = { path = "crates/workspace" }
|
||||
zed = { path = "crates/zed" }
|
||||
zed_actions = { path = "crates/zed_actions" }
|
||||
|
||||
alacritty_terminal = "0.23"
|
||||
anyhow = "1.0.57"
|
||||
any_vec = "0.13"
|
||||
ashpd = "0.8.0"
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
async-dispatcher = { version = "0.1"}
|
||||
async-fs = "1.6"
|
||||
async-recursion = "1.0.0"
|
||||
async-tar = "0.4.2"
|
||||
async-trait = "0.1"
|
||||
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
||||
bitflags = "2.4.2"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "bdaf8c534fbbc9fbca71d1cf272f45640b3a068d" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "bdaf8c534fbbc9fbca71d1cf272f45640b3a068d" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "bdaf8c534fbbc9fbca71d1cf272f45640b3a068d" }
|
||||
blade-graphics = { git = "https://github.com/zed-industries/blade", rev = "33fd51359d113c03b785e28f4a6cf75bacb0b26d" }
|
||||
blade-macros = { git = "https://github.com/zed-industries/blade", rev = "33fd51359d113c03b785e28f4a6cf75bacb0b26d" }
|
||||
blade-util = { git = "https://github.com/zed-industries/blade", rev = "33fd51359d113c03b785e28f4a6cf75bacb0b26d" }
|
||||
cap-std = "3.0"
|
||||
cargo_toml = "0.20"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
clickhouse = { version = "0.11.6" }
|
||||
cocoa = "0.25"
|
||||
ctor = "0.2.6"
|
||||
signal-hook = "0.3.17"
|
||||
core-foundation = { version = "0.9.3" }
|
||||
core-foundation-sys = "0.8.6"
|
||||
derive_more = "0.99.17"
|
||||
dirs = "4.0"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.9"
|
||||
exec = "0.3.1"
|
||||
@@ -287,12 +302,14 @@ fork = "0.1.23"
|
||||
futures = "0.3"
|
||||
futures-batch = "0.6.1"
|
||||
futures-lite = "1.13"
|
||||
git2 = { version = "0.18", default-features = false }
|
||||
git2 = { version = "0.19", default-features = false }
|
||||
globset = "0.4"
|
||||
heed = { version = "0.20.1", features = ["read-txn-no-tls"] }
|
||||
hex = "0.4.3"
|
||||
html5ever = "0.27.0"
|
||||
ignore = "0.4.22"
|
||||
image = "0.23"
|
||||
indexmap = { version = "1.6.2", features = ["serde"] }
|
||||
indoc = "1"
|
||||
# We explicitly disable http2 support in isahc.
|
||||
isahc = { version = "1.7.2", default-features = false, features = [
|
||||
@@ -307,6 +324,7 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
nanoid = "0.4"
|
||||
nix = "0.28"
|
||||
num-format = "0.4.4"
|
||||
once_cell = "1.19.0"
|
||||
ordered-float = "2.1.1"
|
||||
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
||||
@@ -323,6 +341,7 @@ rand = "0.8.5"
|
||||
refineable = { path = "./crates/refineable" }
|
||||
regex = "1.5"
|
||||
repair_json = "0.1.0"
|
||||
runtimelib = { version="0.12", default-features = false, features = ["async-dispatcher-runtime"] }
|
||||
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
|
||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||
schemars = "0.8"
|
||||
@@ -338,6 +357,8 @@ serde_repr = "0.1"
|
||||
sha2 = "0.10"
|
||||
shellexpand = "2.1.0"
|
||||
shlex = "1.3.0"
|
||||
signal-hook = "0.3.17"
|
||||
similar = "1.3"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
strum = { version = "0.25.0", features = ["derive"] }
|
||||
@@ -399,12 +420,13 @@ wit-component = "0.201"
|
||||
sys-locale = "0.3.1"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.56.0"
|
||||
version = "0.57"
|
||||
features = [
|
||||
"implement",
|
||||
"Foundation_Numerics",
|
||||
"System",
|
||||
"System_Threading",
|
||||
"UI_ViewManagement",
|
||||
"Wdk_System_SystemServices",
|
||||
"Win32_Globalization",
|
||||
"Win32_Graphics_Direct2D",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.78-bookworm as builder
|
||||
FROM rust:1.79-bookworm as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
|
||||
6
assets/icons/context.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 5H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.5 8H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 10.9502H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 683 B |
@@ -74,6 +74,7 @@
|
||||
"ib": "storage",
|
||||
"ico": "image",
|
||||
"ini": "settings",
|
||||
"inl": "cpp",
|
||||
"j2k": "image",
|
||||
"java": "java",
|
||||
"jfif": "image",
|
||||
|
||||
1
assets/icons/list_tree.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-tree"><path d="M21 12h-8"/><path d="M21 6H8"/><path d="M21 18h-8"/><path d="M3 6v4c0 1.1.9 2 2 2h3"/><path d="M3 10v6c0 1.1.9 2 2 2h3"/></svg>
|
||||
|
After Width: | Height: | Size: 349 B |
1
assets/icons/rotate_ccw.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-ccw"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
|
||||
|
After Width: | Height: | Size: 302 B |
1
assets/icons/rotate_cw.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-cw"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
|
||||
|
After Width: | Height: | Size: 303 B |
1
assets/icons/search_selection.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-text-quote"><path d="M17 6H3"/><path d="M21 12H8"/><path d="M21 18H8"/><path d="M3 12v6"/></svg>
|
||||
|
After Width: | Height: | Size: 299 B |
1
assets/icons/sparkle.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sparkle"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/></svg>
|
||||
|
After Width: | Height: | Size: 481 B |
3
assets/icons/sparkle_filled.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.937 15.5C9.84772 15.1539 9.66734 14.8381 9.41462 14.5854C9.1619 14.3327 8.84607 14.1523 8.5 14.063L2.365 12.481C2.26033 12.4513 2.16821 12.3883 2.10261 12.3014C2.03702 12.2146 2.00153 12.1088 2.00153 12C2.00153 11.8912 2.03702 11.7854 2.10261 11.6986C2.16821 11.6118 2.26033 11.5487 2.365 11.519L8.5 9.93601C8.84595 9.84681 9.16169 9.66658 9.4144 9.41404C9.66711 9.16151 9.84757 8.84589 9.937 8.50001L11.519 2.36501C11.5484 2.25992 11.6114 2.16735 11.6983 2.1014C11.7853 2.03545 11.8914 1.99976 12.0005 1.99976C12.1096 1.99976 12.2157 2.03545 12.3027 2.1014C12.3896 2.16735 12.4526 2.25992 12.482 2.36501L14.063 8.50001C14.1523 8.84608 14.3327 9.1619 14.5854 9.41462C14.8381 9.66734 15.1539 9.84773 15.5 9.93701L21.635 11.518C21.7405 11.5471 21.8335 11.61 21.8998 11.6971C21.9661 11.7841 22.0021 11.8906 22.0021 12C22.0021 12.1094 21.9661 12.2159 21.8998 12.3029C21.8335 12.39 21.7405 12.4529 21.635 12.482L15.5 14.063C15.1539 14.1523 14.8381 14.3327 14.5854 14.5854C14.3327 14.8381 14.1523 15.1539 14.063 15.5L12.481 21.635C12.4516 21.7401 12.3886 21.8327 12.3017 21.8986C12.2147 21.9646 12.1086 22.0003 11.9995 22.0003C11.8904 22.0003 11.7843 21.9646 11.6973 21.8986C11.6104 21.8327 11.5474 21.7401 11.518 21.635L9.937 15.5Z" fill="black" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
3
assets/icons/stop.svg
Normal file
@@ -0,0 +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>
|
||||
|
After Width: | Height: | Size: 369 B |
1
assets/icons/text-cursor.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-text-cursor"><path d="M17 22h-1a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4h1"/><path d="M7 22h1a4 4 0 0 0 4-4v-1"/><path d="M7 2h1a4 4 0 0 1 4 4v1"/></svg>
|
||||
|
After Width: | Height: | Size: 345 B |
@@ -51,21 +51,22 @@
|
||||
// "alt-h": "editor::DeleteToPreviousWordStart",
|
||||
// "alt-d": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-x": "editor::Cut",
|
||||
"ctrl-c": "editor::Copy",
|
||||
"ctrl-insert": "editor::Copy",
|
||||
"ctrl-v": "editor::Paste",
|
||||
"ctrl-c": "editor::Copy",
|
||||
"shift-insert": "editor::Paste",
|
||||
"ctrl-v": "editor::Paste",
|
||||
"ctrl-y": "editor::Redo",
|
||||
"ctrl-z": "editor::Undo",
|
||||
"ctrl-shift-z": "editor::Redo",
|
||||
"up": "editor::MoveUp",
|
||||
// "ctrl-up": "editor::MoveToStartOfParagraph", todo(linux) Should be "scroll down by 1 line"
|
||||
"ctrl-up": "editor::LineUp",
|
||||
"ctrl-down": "editor::LineDown",
|
||||
"pageup": "editor::PageUp",
|
||||
// "shift-pageup": "editor::MovePageUp", todo(linux) should be 'select page up'
|
||||
"shift-pageup": "editor::SelectPageUp",
|
||||
"home": "editor::MoveToBeginningOfLine",
|
||||
"down": "editor::MoveDown",
|
||||
// "ctrl-down": "editor::MoveToEndOfParagraph", todo(linux) should be "scroll up by 1 line"
|
||||
"pagedown": "editor::PageDown",
|
||||
// "shift-pagedown": "editor::MovePageDown", todo(linux) should be 'select page down'
|
||||
"shift-pagedown": "editor::SelectPageDown",
|
||||
"end": "editor::MoveToEndOfLine",
|
||||
"left": "editor::MoveLeft",
|
||||
"right": "editor::MoveRight",
|
||||
@@ -204,18 +205,6 @@
|
||||
"alt-m": "assistant::ToggleModelSelector"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ConversationEditor > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
"enter": "assistant::ConfirmCommand",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "PromptLibrary",
|
||||
"bindings": {
|
||||
@@ -232,7 +221,8 @@
|
||||
"shift-enter": "search::SelectPrevMatch",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
"ctrl-h": "search::ToggleReplace"
|
||||
"ctrl-h": "search::ToggleReplace",
|
||||
"ctrl-l": "search::ToggleSelection"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -296,6 +286,7 @@
|
||||
"ctrl-alt-g": "search::SelectNextMatch",
|
||||
"ctrl-alt-shift-g": "search::SelectPrevMatch",
|
||||
"ctrl-alt-shift-h": "search::ToggleReplace",
|
||||
"ctrl-alt-shift-l": "search::ToggleSelection",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"alt-c": "search::ToggleCaseSensitive",
|
||||
"alt-w": "search::ToggleWholeWord",
|
||||
@@ -305,6 +296,13 @@
|
||||
"ctrl-alt-shift-x": "search::ToggleRegex"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"bindings": {
|
||||
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
|
||||
"ctrl-e": ["terminal::SendKeystroke", "ctrl-e"]
|
||||
}
|
||||
},
|
||||
// Bindings from VS Code
|
||||
{
|
||||
"context": "Editor",
|
||||
@@ -429,7 +427,7 @@
|
||||
"alt-8": ["workspace::ActivatePane", 7],
|
||||
"alt-9": ["workspace::ActivatePane", 8],
|
||||
"ctrl-alt-b": "workspace::ToggleLeftDock",
|
||||
"ctrl-b": "workspace::ToggleRightDock",
|
||||
"ctrl-b": "workspace::ToggleLeftDock",
|
||||
"ctrl-j": "workspace::ToggleBottomDock",
|
||||
"ctrl-alt-y": "workspace::CloseAllDocks",
|
||||
"ctrl-shift-f": "pane::DeploySearch",
|
||||
@@ -449,6 +447,7 @@
|
||||
"ctrl-shift-p": "command_palette::Toggle",
|
||||
"ctrl-shift-m": "diagnostics::Deploy",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-b": "outline_panel::ToggleFocus",
|
||||
"ctrl-?": "assistant::ToggleFocus",
|
||||
"ctrl-alt-s": "workspace::SaveAll",
|
||||
"ctrl-k m": "language_selector::Toggle",
|
||||
@@ -554,12 +553,37 @@
|
||||
"ctrl-enter": "assistant::InlineAssist"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextEditor > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
"enter": "assistant::ConfirmCommand",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar && !in_replace",
|
||||
"bindings": {
|
||||
"ctrl-enter": "project_search::SearchInNew"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "OutlinePanel",
|
||||
"bindings": {
|
||||
"left": "outline_panel::CollapseSelectedEntry",
|
||||
"right": "outline_panel::ExpandSelectedEntry",
|
||||
"ctrl-alt-c": "outline_panel::CopyPath",
|
||||
"alt-ctrl-shift-c": "outline_panel::CopyRelativePath",
|
||||
"alt-ctrl-r": "outline_panel::RevealInFinder",
|
||||
"space": "outline_panel::Open",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrev"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
@@ -581,7 +605,10 @@
|
||||
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-ctrl-r": "project_panel::RevealInFinder",
|
||||
"alt-shift-f": "project_panel::NewSearchInDirectory"
|
||||
"alt-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrev",
|
||||
"escape": "menu::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -176,6 +176,12 @@
|
||||
"replace_enabled": true
|
||||
}
|
||||
],
|
||||
"cmd-alt-l": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
"selection_search_enabled": true
|
||||
}
|
||||
],
|
||||
"cmd-e": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
@@ -222,7 +228,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ConversationEditor > Editor",
|
||||
"context": "ContextEditor > Editor",
|
||||
"bindings": {
|
||||
"cmd-enter": "assistant::Assist",
|
||||
"cmd-s": "workspace::Save",
|
||||
@@ -250,7 +256,8 @@
|
||||
"shift-enter": "search::SelectPrevMatch",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"cmd-f": "search::FocusSearch",
|
||||
"cmd-alt-f": "search::ToggleReplace"
|
||||
"cmd-alt-f": "search::ToggleReplace",
|
||||
"cmd-alt-l": "search::ToggleSelection"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -316,6 +323,7 @@
|
||||
"cmd-g": "search::SelectNextMatch",
|
||||
"cmd-shift-g": "search::SelectPrevMatch",
|
||||
"cmd-shift-h": "search::ToggleReplace",
|
||||
"cmd-alt-l": "search::ToggleSelection",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"alt-cmd-c": "search::ToggleCaseSensitive",
|
||||
"alt-cmd-w": "search::ToggleWholeWord",
|
||||
@@ -467,6 +475,7 @@
|
||||
"cmd-shift-p": "command_palette::Toggle",
|
||||
"cmd-shift-m": "diagnostics::Deploy",
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
"cmd-shift-b": "outline_panel::ToggleFocus",
|
||||
"cmd-?": "assistant::ToggleFocus",
|
||||
"cmd-alt-s": "workspace::SaveAll",
|
||||
"cmd-k m": "language_selector::Toggle",
|
||||
@@ -576,6 +585,19 @@
|
||||
"cmd-enter": "project_search::SearchInNew"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "OutlinePanel",
|
||||
"bindings": {
|
||||
"left": "outline_panel::CollapseSelectedEntry",
|
||||
"right": "outline_panel::ExpandSelectedEntry",
|
||||
"cmd-alt-c": "outline_panel::CopyPath",
|
||||
"alt-cmd-shift-c": "outline_panel::CopyRelativePath",
|
||||
"alt-cmd-r": "outline_panel::RevealInFinder",
|
||||
"space": "outline_panel::Open",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrev"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"right": "vim::Right",
|
||||
"space": "vim::Space",
|
||||
"$": "vim::EndOfLine",
|
||||
"end": "vim::EndOfLine",
|
||||
"^": "vim::FirstNonWhitespace",
|
||||
"_": "vim::StartOfLineDownward",
|
||||
"g _": "vim::EndOfLineDownward",
|
||||
@@ -80,6 +81,7 @@
|
||||
"g shift-e": ["vim::PreviousWordEnd", { "ignorePunctuation": true }],
|
||||
|
||||
"/": "vim::Search",
|
||||
"g /": "pane::DeploySearch",
|
||||
"?": [
|
||||
"vim::Search",
|
||||
{
|
||||
@@ -139,7 +141,8 @@
|
||||
"ctrl-q": "vim::ToggleVisualBlock",
|
||||
"shift-k": "editor::Hover",
|
||||
"shift-r": "vim::ToggleReplace",
|
||||
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
|
||||
"0": "vim::StartOfLine",
|
||||
"home": "vim::StartOfLine",
|
||||
"ctrl-f": "vim::PageDown",
|
||||
"pagedown": "vim::PageDown",
|
||||
"ctrl-b": "vim::PageUp",
|
||||
@@ -245,9 +248,10 @@
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g v": "vim::RestoreVisualSelection",
|
||||
"g ]": "editor::GoToDiagnostic",
|
||||
"g [": "editor::GoToPrevDiagnostic",
|
||||
"g i": ["workspace::SendKeystrokes", "` ^ i"],
|
||||
"g i": "vim::InsertAtPrevious",
|
||||
"g ,": "vim::ChangeListNewer",
|
||||
"g ;": "vim::ChangeListOlder",
|
||||
"shift-h": "vim::WindowTop",
|
||||
@@ -381,6 +385,10 @@
|
||||
"shift-s": "vim::SubstituteLine",
|
||||
">": ["vim::PushOperator", "Indent"],
|
||||
"<": ["vim::PushOperator", "Outdent"],
|
||||
"g u": ["vim::PushOperator", "Lowercase"],
|
||||
"g shift-u": ["vim::PushOperator", "Uppercase"],
|
||||
"g ~": ["vim::PushOperator", "OppositeCase"],
|
||||
"\"": ["vim::PushOperator", "Register"],
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
// tree-sitter related commands
|
||||
@@ -395,13 +403,14 @@
|
||||
{
|
||||
"context": "Editor && vim_mode == visual && vim_operator == none && !VimWaiting",
|
||||
"bindings": {
|
||||
"\"": ["vim::PushOperator", "Register"],
|
||||
// tree-sitter related commands
|
||||
"[ x": "editor::SelectLargerSyntaxNode",
|
||||
"] x": "editor::SelectSmallerSyntaxNode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimCount",
|
||||
"context": "Editor && VimCount && vim_mode != insert",
|
||||
"bindings": {
|
||||
"0": ["vim::Number", 0]
|
||||
}
|
||||
@@ -430,6 +439,27 @@
|
||||
"d": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == gu",
|
||||
"bindings": {
|
||||
"g u": "vim::CurrentLine",
|
||||
"u": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == gU",
|
||||
"bindings": {
|
||||
"g shift-u": "vim::CurrentLine",
|
||||
"shift-u": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == g~",
|
||||
"bindings": {
|
||||
"g ~": "vim::CurrentLine",
|
||||
"~": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == d",
|
||||
"bindings": {
|
||||
@@ -606,8 +636,7 @@
|
||||
"ctrl-u": "editor::DeleteToBeginningOfLine",
|
||||
"ctrl-t": "vim::Indent",
|
||||
"ctrl-d": "vim::Outdent",
|
||||
"ctrl-r \"": "editor::Paste",
|
||||
"ctrl-r +": "editor::Paste"
|
||||
"ctrl-r": ["vim::PushOperator", "Register"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -628,6 +657,13 @@
|
||||
"ctrl-[": ["vim::SwitchMode", "Normal"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == insert && VimWaiting",
|
||||
"bindings": {
|
||||
"escape": "vim::NormalBefore",
|
||||
"ctrl-[": "vim::NormalBefore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar && !in_replace",
|
||||
"bindings": {
|
||||
|
||||
@@ -119,10 +119,10 @@
|
||||
// The debounce delay before re-querying the language server for completion
|
||||
// documentation when not included in original completion list.
|
||||
"completion_documentation_secondary_query_debounce": 300,
|
||||
// Whether to show wrap guides in the editor. Setting this to true will
|
||||
// show a guide at the 'preferred_line_length' value if 'soft_wrap' is set to
|
||||
// 'preferred_line_length', and will show any additional guides as specified
|
||||
// by the 'wrap_guides' setting.
|
||||
// Whether to show wrap guides (vertical rulers) in the editor.
|
||||
// Setting this to true will show a guide at the 'preferred_line_length' value
|
||||
// if softwrap is set to 'preferred_line_length', and will show any
|
||||
// additional guides as specified by the 'wrap_guides' setting.
|
||||
"show_wrap_guides": true,
|
||||
// Character counts at which to show wrap guides in the editor.
|
||||
"wrap_guides": [],
|
||||
@@ -152,9 +152,9 @@
|
||||
// Otherwise(when `true`), the closing characters are always skipped over and auto-removed
|
||||
// no matter how they were inserted.
|
||||
"always_treat_brackets_as_autoclosed": false,
|
||||
// Controls whether copilot provides suggestion immediately
|
||||
// or waits for a `copilot::Toggle`
|
||||
"show_copilot_suggestions": true,
|
||||
// Controls whether inline completions are shown immediately (true)
|
||||
// or manually by triggering `editor::ShowInlineCompletion` (false).
|
||||
"show_inline_completions": true,
|
||||
// Whether to show tabs and spaces in the editor.
|
||||
// This setting can take three values:
|
||||
//
|
||||
@@ -164,6 +164,12 @@
|
||||
// "none"
|
||||
// 3. Draw all invisible symbols:
|
||||
// "all"
|
||||
// 4. Draw whitespaces at boundaries only:
|
||||
// "boundaries"
|
||||
// For a whitespace to be on a boundary, any of the following conditions need to be met:
|
||||
// - It is a tab
|
||||
// - It is adjacent to an edge (start or end)
|
||||
// - It is adjacent to a whitespace (left or right)
|
||||
"show_whitespaces": "selection",
|
||||
// Settings related to calls in Zed
|
||||
"calls": {
|
||||
@@ -177,7 +183,9 @@
|
||||
// Whether to show breadcrumbs.
|
||||
"breadcrumbs": true,
|
||||
// Whether to show quick action buttons.
|
||||
"quick_actions": true
|
||||
"quick_actions": true,
|
||||
// Whether to show the Selections menu in the editor toolbar
|
||||
"selections_menu": true
|
||||
},
|
||||
// Scrollbar related settings
|
||||
"scrollbar": {
|
||||
@@ -296,6 +304,29 @@
|
||||
/// when a directory has only one directory inside.
|
||||
"auto_fold_dirs": false
|
||||
},
|
||||
"outline_panel": {
|
||||
// Whether to show the outline panel button in the status bar
|
||||
"button": true,
|
||||
// Default width of the outline panel.
|
||||
"default_width": 300,
|
||||
// Where to dock the outline panel. Can be 'left' or 'right'.
|
||||
"dock": "left",
|
||||
// Whether to show file icons in the outline panel.
|
||||
"file_icons": true,
|
||||
// Whether to show folder icons or chevrons for directories in the outline panel.
|
||||
"folder_icons": true,
|
||||
// Whether to show the git status in the outline panel.
|
||||
"git_status": true,
|
||||
// Amount of indentation for nested items.
|
||||
"indent_size": 20,
|
||||
// Whether to reveal it in the outline panel automatically,
|
||||
// when a corresponding outline entry becomes active.
|
||||
// Gitignored entries are never auto revealed.
|
||||
"auto_reveal_entries": true,
|
||||
/// Whether to fold directories automatically
|
||||
/// when a directory has only one directory inside.
|
||||
"auto_fold_dirs": true
|
||||
},
|
||||
"collaboration_panel": {
|
||||
// Whether to show the collaboration panel button in the status bar.
|
||||
"button": true,
|
||||
@@ -355,6 +386,9 @@
|
||||
"show_call_status_icon": true,
|
||||
// Whether to use language servers to provide code intelligence.
|
||||
"enable_language_server": true,
|
||||
// Whether to perform linked edits of associated ranges, if the language server supports it.
|
||||
// For example, when editing opening <html> tag, the contents of the closing </html> tag will be edited as well.
|
||||
"linked_edits": true,
|
||||
// The list of language servers to use (or disable) for all languages.
|
||||
//
|
||||
// This is typically customized on a per-language basis.
|
||||
@@ -491,9 +525,8 @@
|
||||
// "delay_ms": 600
|
||||
}
|
||||
},
|
||||
"copilot": {
|
||||
// The set of glob patterns for which copilot should be disabled
|
||||
// in any matching file.
|
||||
"inline_completions": {
|
||||
// A list of globs representing files that inline completions should be disabled for.
|
||||
"disabled_globs": [".env"]
|
||||
},
|
||||
// Settings specific to journaling
|
||||
@@ -698,7 +731,7 @@
|
||||
}
|
||||
},
|
||||
"JavaScript": {
|
||||
"language_servers": ["typescript-language-server", "!vtsls", ".."],
|
||||
"language_servers": ["!typescript-language-server", "vtsls", "..."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
@@ -741,7 +774,7 @@
|
||||
}
|
||||
},
|
||||
"TSX": {
|
||||
"language_servers": ["typescript-language-server", "!vtsls", ".."],
|
||||
"language_servers": ["!typescript-language-server", "vtsls", "..."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
@@ -752,7 +785,7 @@
|
||||
}
|
||||
},
|
||||
"TypeScript": {
|
||||
"language_servers": ["typescript-language-server", "!vtsls", ".."],
|
||||
"language_servers": ["!typescript-language-server", "vtsls", "..."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
|
||||
@@ -3,22 +3,22 @@ use editor::Editor;
|
||||
use extension::ExtensionStore;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
actions, svg, AppContext, CursorStyle, EventEmitter, InteractiveElement as _, Model,
|
||||
ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, View,
|
||||
ViewContext, VisualContext as _,
|
||||
actions, anchored, deferred, percentage, Animation, AnimationExt as _, AppContext, CursorStyle,
|
||||
DismissEvent, EventEmitter, InteractiveElement as _, Model, ParentElement as _, Render,
|
||||
SharedString, StatefulInteractiveElement, Styled, Transformation, View, ViewContext,
|
||||
VisualContext as _,
|
||||
};
|
||||
use language::{
|
||||
LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId, LanguageServerName,
|
||||
};
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName};
|
||||
use project::{LanguageServerProgress, Project};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cmp::Reverse, fmt::Write, sync::Arc};
|
||||
use ui::prelude::*;
|
||||
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
|
||||
use ui::{prelude::*, ContextMenu};
|
||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||
|
||||
actions!(activity_indicator, [ShowErrorMessage]);
|
||||
|
||||
const DOWNLOAD_ICON: &str = "icons/download.svg";
|
||||
const WARNING_ICON: &str = "icons/warning.svg";
|
||||
|
||||
pub enum Event {
|
||||
ShowError { lsp_name: Arc<str>, error: String },
|
||||
}
|
||||
@@ -27,6 +27,7 @@ pub struct ActivityIndicator {
|
||||
statuses: Vec<LspStatus>,
|
||||
project: Model<Project>,
|
||||
auto_updater: Option<Model<AutoUpdater>>,
|
||||
context_menu: Option<View<ContextMenu>>,
|
||||
}
|
||||
|
||||
struct LspStatus {
|
||||
@@ -35,14 +36,14 @@ struct LspStatus {
|
||||
}
|
||||
|
||||
struct PendingWork<'a> {
|
||||
language_server_name: &'a str,
|
||||
language_server_id: LanguageServerId,
|
||||
progress_token: &'a str,
|
||||
progress: &'a LanguageServerProgress,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Content {
|
||||
icon: Option<&'static str>,
|
||||
icon: Option<gpui::AnyElement>,
|
||||
message: String,
|
||||
on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
|
||||
}
|
||||
@@ -78,6 +79,7 @@ impl ActivityIndicator {
|
||||
statuses: Default::default(),
|
||||
project: project.clone(),
|
||||
auto_updater,
|
||||
context_menu: None,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -151,7 +153,7 @@ impl ActivityIndicator {
|
||||
.read(cx)
|
||||
.language_server_statuses()
|
||||
.rev()
|
||||
.filter_map(|status| {
|
||||
.filter_map(|(server_id, status)| {
|
||||
if status.pending_work.is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -159,7 +161,7 @@ impl ActivityIndicator {
|
||||
.pending_work
|
||||
.iter()
|
||||
.map(|(token, progress)| PendingWork {
|
||||
language_server_name: status.name.as_str(),
|
||||
language_server_id: server_id,
|
||||
progress_token: token.as_str(),
|
||||
progress,
|
||||
})
|
||||
@@ -175,33 +177,44 @@ impl ActivityIndicator {
|
||||
// Show any language server has pending activity.
|
||||
let mut pending_work = self.pending_language_server_work(cx);
|
||||
if let Some(PendingWork {
|
||||
language_server_name,
|
||||
progress_token,
|
||||
progress,
|
||||
..
|
||||
}) = pending_work.next()
|
||||
{
|
||||
let mut message = language_server_name.to_string();
|
||||
|
||||
message.push_str(": ");
|
||||
if let Some(progress_message) = progress.message.as_ref() {
|
||||
message.push_str(progress_message);
|
||||
} else {
|
||||
message.push_str(progress_token);
|
||||
}
|
||||
let mut message = progress
|
||||
.title
|
||||
.as_deref()
|
||||
.unwrap_or(progress_token)
|
||||
.to_string();
|
||||
|
||||
if let Some(percentage) = progress.percentage {
|
||||
write!(&mut message, " ({}%)", percentage).unwrap();
|
||||
}
|
||||
|
||||
if let Some(progress_message) = progress.message.as_ref() {
|
||||
message.push_str(": ");
|
||||
message.push_str(progress_message);
|
||||
}
|
||||
|
||||
let additional_work_count = pending_work.count();
|
||||
if additional_work_count > 0 {
|
||||
write!(&mut message, " + {} more", additional_work_count).unwrap();
|
||||
}
|
||||
|
||||
return Content {
|
||||
icon: None,
|
||||
icon: Some(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
message,
|
||||
on_click: None,
|
||||
on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -222,7 +235,11 @@ impl ActivityIndicator {
|
||||
|
||||
if !downloading.is_empty() {
|
||||
return Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!("Downloading {}...", downloading.join(", "),),
|
||||
on_click: None,
|
||||
};
|
||||
@@ -230,7 +247,11 @@ impl ActivityIndicator {
|
||||
|
||||
if !checking_for_update.is_empty() {
|
||||
return Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!(
|
||||
"Checking for updates to {}...",
|
||||
checking_for_update.join(", "),
|
||||
@@ -241,7 +262,11 @@ impl ActivityIndicator {
|
||||
|
||||
if !failed.is_empty() {
|
||||
return Content {
|
||||
icon: Some(WARNING_ICON),
|
||||
icon: Some(
|
||||
Icon::new(IconName::ExclamationTriangle)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!(
|
||||
"Failed to download {}. Click to show error.",
|
||||
failed.join(", "),
|
||||
@@ -255,7 +280,11 @@ impl ActivityIndicator {
|
||||
// Show any formatting failure
|
||||
if let Some(failure) = self.project.read(cx).last_formatting_failure() {
|
||||
return Content {
|
||||
icon: Some(WARNING_ICON),
|
||||
icon: Some(
|
||||
Icon::new(IconName::ExclamationTriangle)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!("Formatting failed: {}. Click to see logs.", failure),
|
||||
on_click: Some(Arc::new(|_, cx| {
|
||||
cx.dispatch_action(Box::new(workspace::OpenLog));
|
||||
@@ -267,17 +296,29 @@ impl ActivityIndicator {
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
return match &updater.read(cx).status() {
|
||||
AutoUpdateStatus::Checking => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: "Checking for Zed updates…".to_string(),
|
||||
on_click: None,
|
||||
},
|
||||
AutoUpdateStatus::Downloading => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: "Downloading Zed update…".to_string(),
|
||||
on_click: None,
|
||||
},
|
||||
AutoUpdateStatus::Installing => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: "Installing Zed update…".to_string(),
|
||||
on_click: None,
|
||||
},
|
||||
@@ -285,14 +326,18 @@ impl ActivityIndicator {
|
||||
icon: None,
|
||||
message: "Click to restart and update Zed".to_string(),
|
||||
on_click: Some(Arc::new({
|
||||
let restart = workspace::Restart {
|
||||
let reload = workspace::Reload {
|
||||
binary_path: Some(binary_path.clone()),
|
||||
};
|
||||
move |_, cx| workspace::restart(&restart, cx)
|
||||
move |_, cx| workspace::reload(&reload, cx)
|
||||
})),
|
||||
},
|
||||
AutoUpdateStatus::Errored => Content {
|
||||
icon: Some(WARNING_ICON),
|
||||
icon: Some(
|
||||
Icon::new(IconName::ExclamationTriangle)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: "Auto update failed".to_string(),
|
||||
on_click: Some(Arc::new(|this, cx| {
|
||||
this.dismiss_error_message(&Default::default(), cx)
|
||||
@@ -307,7 +352,11 @@ impl ActivityIndicator {
|
||||
{
|
||||
if let Some(extension_id) = extension_store.outstanding_operations().keys().next() {
|
||||
return Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!("Updating {extension_id} extension…"),
|
||||
on_click: None,
|
||||
};
|
||||
@@ -316,6 +365,75 @@ impl ActivityIndicator {
|
||||
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn toggle_language_server_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.context_menu.take().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.build_lsp_work_context_menu(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn build_lsp_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let mut has_work = false;
|
||||
let this = cx.view().downgrade();
|
||||
let context_menu = ContextMenu::build(cx, |mut menu, cx| {
|
||||
for work in self.pending_language_server_work(cx) {
|
||||
has_work = true;
|
||||
|
||||
let this = this.clone();
|
||||
let title = SharedString::from(
|
||||
work.progress
|
||||
.title
|
||||
.as_deref()
|
||||
.unwrap_or(work.progress_token)
|
||||
.to_string(),
|
||||
);
|
||||
if work.progress.is_cancellable {
|
||||
let language_server_id = work.language_server_id;
|
||||
let token = work.progress_token.to_string();
|
||||
menu = menu.custom_entry(
|
||||
move |_| {
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(Label::new(title.clone()))
|
||||
.child(Icon::new(IconName::XCircle))
|
||||
.into_any_element()
|
||||
},
|
||||
move |cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.project.update(cx, |project, cx| {
|
||||
project.cancel_language_server_work(
|
||||
language_server_id,
|
||||
Some(token.clone()),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
this.context_menu.take();
|
||||
})
|
||||
.ok();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
menu = menu.label(title.clone());
|
||||
}
|
||||
}
|
||||
menu
|
||||
});
|
||||
|
||||
if has_work {
|
||||
cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
|
||||
this.context_menu.take();
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
cx.focus_view(&context_menu);
|
||||
self.context_menu = Some(context_menu);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for ActivityIndicator {}
|
||||
@@ -338,8 +456,17 @@ impl Render for ActivityIndicator {
|
||||
}
|
||||
|
||||
result
|
||||
.children(content.icon.map(|icon| svg().path(icon)))
|
||||
.gap_2()
|
||||
.children(content.icon)
|
||||
.child(Label::new(SharedString::from(content.message)).size(LabelSize::Small))
|
||||
.children(self.context_menu.as_ref().map(|menu| {
|
||||
deferred(
|
||||
anchored()
|
||||
.anchor(gpui::AnchorCorner::BottomLeft)
|
||||
.child(menu.clone()),
|
||||
)
|
||||
.with_priority(1)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,18 +35,22 @@ language.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
ollama = { workspace = true, features = ["schemars"] }
|
||||
open_ai = { workspace = true, features = ["schemars"] }
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
rope.workspace = true
|
||||
rustdoc.workspace = true
|
||||
schemars.workspace = true
|
||||
search.workspace = true
|
||||
semantic_index.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
similar.workspace = true
|
||||
smol.workspace = true
|
||||
strsim = "0.11"
|
||||
strum.workspace = true
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
pub mod assistant_panel;
|
||||
pub mod assistant_settings;
|
||||
mod completion_provider;
|
||||
mod context_store;
|
||||
mod inline_assistant;
|
||||
mod model_selector;
|
||||
mod prompt_library;
|
||||
mod prompts;
|
||||
mod saved_conversation;
|
||||
mod search;
|
||||
mod slash_command;
|
||||
mod streaming_diff;
|
||||
|
||||
pub use assistant_panel::AssistantPanel;
|
||||
|
||||
use assistant_settings::{AnthropicModel, AssistantSettings, CloudModel, OpenAiModel};
|
||||
use assistant_settings::{AnthropicModel, AssistantSettings, CloudModel, OllamaModel, OpenAiModel};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::{proto, Client};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
pub(crate) use completion_provider::*;
|
||||
pub(crate) use context_store::*;
|
||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
pub(crate) use inline_assistant::*;
|
||||
pub(crate) use model_selector::*;
|
||||
pub(crate) use saved_conversation::*;
|
||||
use rustdoc::RustdocStore;
|
||||
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use slash_command::{
|
||||
active_command, default_command, fetch_command, file_command, project_command, prompt_command,
|
||||
rustdoc_command, search_command, tabs_command,
|
||||
active_command, default_command, diagnostics_command, fetch_command, file_command, now_command,
|
||||
project_command, prompt_command, rustdoc_command, search_command, tabs_command,
|
||||
};
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
sync::Arc,
|
||||
};
|
||||
pub(crate) use streaming_diff::*;
|
||||
use util::paths::EMBEDDINGS_DIR;
|
||||
|
||||
actions!(
|
||||
assistant,
|
||||
@@ -91,6 +91,7 @@ pub enum LanguageModel {
|
||||
Cloud(CloudModel),
|
||||
OpenAi(OpenAiModel),
|
||||
Anthropic(AnthropicModel),
|
||||
Ollama(OllamaModel),
|
||||
}
|
||||
|
||||
impl Default for LanguageModel {
|
||||
@@ -105,6 +106,7 @@ impl LanguageModel {
|
||||
LanguageModel::OpenAi(model) => format!("openai/{}", model.id()),
|
||||
LanguageModel::Anthropic(model) => format!("anthropic/{}", model.id()),
|
||||
LanguageModel::Cloud(model) => format!("zed.dev/{}", model.id()),
|
||||
LanguageModel::Ollama(model) => format!("ollama/{}", model.id()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +115,7 @@ impl LanguageModel {
|
||||
LanguageModel::OpenAi(model) => model.display_name().into(),
|
||||
LanguageModel::Anthropic(model) => model.display_name().into(),
|
||||
LanguageModel::Cloud(model) => model.display_name().into(),
|
||||
LanguageModel::Ollama(model) => model.display_name().into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +124,7 @@ impl LanguageModel {
|
||||
LanguageModel::OpenAi(model) => model.max_token_count(),
|
||||
LanguageModel::Anthropic(model) => model.max_token_count(),
|
||||
LanguageModel::Cloud(model) => model.max_token_count(),
|
||||
LanguageModel::Ollama(model) => model.max_token_count(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +133,7 @@ impl LanguageModel {
|
||||
LanguageModel::OpenAi(model) => model.id(),
|
||||
LanguageModel::Anthropic(model) => model.id(),
|
||||
LanguageModel::Cloud(model) => model.id(),
|
||||
LanguageModel::Ollama(model) => model.id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,6 +184,7 @@ impl LanguageModelRequest {
|
||||
match &self.model {
|
||||
LanguageModel::OpenAi(_) => {}
|
||||
LanguageModel::Anthropic(_) => {}
|
||||
LanguageModel::Ollama(_) => {}
|
||||
LanguageModel::Cloud(model) => match model {
|
||||
CloudModel::Claude3Opus | CloudModel::Claude3Sonnet | CloudModel::Claude3Haiku => {
|
||||
preprocess_anthropic_request(self);
|
||||
@@ -264,7 +270,7 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
async move {
|
||||
let embedding_provider = CloudEmbeddingProvider::new(client.clone());
|
||||
let semantic_index = SemanticIndex::new(
|
||||
EMBEDDINGS_DIR.join("semantic-index-db.0.mdb"),
|
||||
paths::embeddings_dir().join("semantic-index-db.0.mdb"),
|
||||
Arc::new(embedding_provider),
|
||||
&mut cx,
|
||||
)
|
||||
@@ -280,6 +286,7 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
register_slash_commands(cx);
|
||||
assistant_panel::init(cx);
|
||||
inline_assistant::init(client.telemetry().clone(), cx);
|
||||
RustdocStore::init_global(cx);
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(Assistant::NAMESPACE);
|
||||
@@ -307,6 +314,8 @@ fn register_slash_commands(cx: &mut AppContext) {
|
||||
slash_command_registry.register_command(search_command::SearchSlashCommand, true);
|
||||
slash_command_registry.register_command(prompt_command::PromptSlashCommand, true);
|
||||
slash_command_registry.register_command(default_command::DefaultSlashCommand, true);
|
||||
slash_command_registry.register_command(now_command::NowSlashCommand, true);
|
||||
slash_command_registry.register_command(diagnostics_command::DiagnosticsCommand, true);
|
||||
slash_command_registry.register_command(rustdoc_command::RustdocSlashCommand, false);
|
||||
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::fmt;
|
||||
|
||||
pub use anthropic::Model as AnthropicModel;
|
||||
use gpui::Pixels;
|
||||
pub use ollama::Model as OllamaModel;
|
||||
pub use open_ai::Model as OpenAiModel;
|
||||
use schemars::{
|
||||
schema::{InstanceType, Metadata, Schema, SchemaObject},
|
||||
@@ -168,6 +169,11 @@ pub enum AssistantProvider {
|
||||
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 {
|
||||
@@ -197,6 +203,12 @@ pub enum AssistantProviderContent {
|
||||
api_url: Option<String>,
|
||||
low_speed_timeout_in_seconds: Option<u64>,
|
||||
},
|
||||
#[serde(rename = "ollama")]
|
||||
Ollama {
|
||||
default_model: Option<OllamaModel>,
|
||||
api_url: Option<String>,
|
||||
low_speed_timeout_in_seconds: Option<u64>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -328,6 +340,13 @@ impl AssistantSettingsContent {
|
||||
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,
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -472,6 +491,27 @@ impl Settings for AssistantSettings {
|
||||
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,
|
||||
@@ -519,6 +559,15 @@ impl Settings for AssistantSettings {
|
||||
.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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ mod anthropic;
|
||||
mod cloud;
|
||||
#[cfg(test)]
|
||||
mod fake;
|
||||
mod ollama;
|
||||
mod open_ai;
|
||||
|
||||
pub use anthropic::*;
|
||||
pub use cloud::*;
|
||||
#[cfg(test)]
|
||||
pub use fake::*;
|
||||
pub use ollama::*;
|
||||
pub use open_ai::*;
|
||||
|
||||
use crate::{
|
||||
@@ -50,6 +52,18 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
)),
|
||||
AssistantProvider::Ollama {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
} => CompletionProvider::Ollama(OllamaCompletionProvider::new(
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
cx,
|
||||
)),
|
||||
};
|
||||
cx.set_global(provider);
|
||||
|
||||
@@ -87,6 +101,24 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
settings_version,
|
||||
);
|
||||
}
|
||||
|
||||
(
|
||||
CompletionProvider::Ollama(provider),
|
||||
AssistantProvider::Ollama {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
) => {
|
||||
provider.update(
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
(CompletionProvider::Cloud(provider), AssistantProvider::ZedDotDev { model }) => {
|
||||
provider.update(model.clone(), settings_version);
|
||||
}
|
||||
@@ -130,6 +162,23 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
settings_version,
|
||||
));
|
||||
}
|
||||
(
|
||||
_,
|
||||
AssistantProvider::Ollama {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
) => {
|
||||
*provider = CompletionProvider::Ollama(OllamaCompletionProvider::new(
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -142,6 +191,7 @@ pub enum CompletionProvider {
|
||||
Cloud(CloudCompletionProvider),
|
||||
#[cfg(test)]
|
||||
Fake(FakeCompletionProvider),
|
||||
Ollama(OllamaCompletionProvider),
|
||||
}
|
||||
|
||||
impl gpui::Global for CompletionProvider {}
|
||||
@@ -165,6 +215,10 @@ impl CompletionProvider {
|
||||
.available_models()
|
||||
.map(LanguageModel::Cloud)
|
||||
.collect(),
|
||||
CompletionProvider::Ollama(provider) => provider
|
||||
.available_models()
|
||||
.map(|model| LanguageModel::Ollama(model.clone()))
|
||||
.collect(),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
}
|
||||
@@ -175,6 +229,7 @@ impl CompletionProvider {
|
||||
CompletionProvider::OpenAi(provider) => provider.settings_version(),
|
||||
CompletionProvider::Anthropic(provider) => provider.settings_version(),
|
||||
CompletionProvider::Cloud(provider) => provider.settings_version(),
|
||||
CompletionProvider::Ollama(provider) => provider.settings_version(),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
}
|
||||
@@ -185,6 +240,7 @@ impl CompletionProvider {
|
||||
CompletionProvider::OpenAi(provider) => provider.is_authenticated(),
|
||||
CompletionProvider::Anthropic(provider) => provider.is_authenticated(),
|
||||
CompletionProvider::Cloud(provider) => provider.is_authenticated(),
|
||||
CompletionProvider::Ollama(provider) => provider.is_authenticated(),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => true,
|
||||
}
|
||||
@@ -195,6 +251,7 @@ impl CompletionProvider {
|
||||
CompletionProvider::OpenAi(provider) => provider.authenticate(cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.authenticate(cx),
|
||||
CompletionProvider::Cloud(provider) => provider.authenticate(cx),
|
||||
CompletionProvider::Ollama(provider) => provider.authenticate(cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => Task::ready(Ok(())),
|
||||
}
|
||||
@@ -205,6 +262,7 @@ impl CompletionProvider {
|
||||
CompletionProvider::OpenAi(provider) => provider.authentication_prompt(cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.authentication_prompt(cx),
|
||||
CompletionProvider::Cloud(provider) => provider.authentication_prompt(cx),
|
||||
CompletionProvider::Ollama(provider) => provider.authentication_prompt(cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
}
|
||||
@@ -215,6 +273,7 @@ impl CompletionProvider {
|
||||
CompletionProvider::OpenAi(provider) => provider.reset_credentials(cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.reset_credentials(cx),
|
||||
CompletionProvider::Cloud(_) => Task::ready(Ok(())),
|
||||
CompletionProvider::Ollama(provider) => provider.reset_credentials(cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => Task::ready(Ok(())),
|
||||
}
|
||||
@@ -225,6 +284,7 @@ impl CompletionProvider {
|
||||
CompletionProvider::OpenAi(provider) => LanguageModel::OpenAi(provider.model()),
|
||||
CompletionProvider::Anthropic(provider) => LanguageModel::Anthropic(provider.model()),
|
||||
CompletionProvider::Cloud(provider) => LanguageModel::Cloud(provider.model()),
|
||||
CompletionProvider::Ollama(provider) => LanguageModel::Ollama(provider.model()),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => LanguageModel::default(),
|
||||
}
|
||||
@@ -239,6 +299,7 @@ impl CompletionProvider {
|
||||
CompletionProvider::OpenAi(provider) => provider.count_tokens(request, cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.count_tokens(request, cx),
|
||||
CompletionProvider::Cloud(provider) => provider.count_tokens(request, cx),
|
||||
CompletionProvider::Ollama(provider) => provider.count_tokens(request, cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => futures::FutureExt::boxed(futures::future::ready(Ok(0))),
|
||||
}
|
||||
@@ -252,6 +313,7 @@ impl CompletionProvider {
|
||||
CompletionProvider::OpenAi(provider) => provider.complete(request),
|
||||
CompletionProvider::Anthropic(provider) => provider.complete(request),
|
||||
CompletionProvider::Cloud(provider) => provider.complete(request),
|
||||
CompletionProvider::Ollama(provider) => provider.complete(request),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(provider) => provider.complete(),
|
||||
}
|
||||
|
||||
@@ -349,7 +349,7 @@ impl Render for AuthenticationPrompt {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Label::new("Click on").size(LabelSize::Small))
|
||||
.child(Icon::new(IconName::Ai).size(IconSize::XSmall))
|
||||
.child(Icon::new(IconName::ZedAssistant).size(IconSize::XSmall))
|
||||
.child(
|
||||
Label::new("in the status bar to close this panel.").size(LabelSize::Small),
|
||||
),
|
||||
|
||||
348
crates/assistant/src/completion_provider/ollama.rs
Normal file
@@ -0,0 +1,348 @@
|
||||
use crate::{
|
||||
assistant_settings::OllamaModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use futures::StreamExt as _;
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt};
|
||||
use gpui::{AnyView, AppContext, Task};
|
||||
use http::HttpClient;
|
||||
use ollama::{
|
||||
get_models, preload_model, stream_chat_completion, ChatMessage, ChatOptions, ChatRequest,
|
||||
Role as OllamaRole,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
|
||||
const OLLAMA_DOWNLOAD_URL: &str = "https://ollama.com/download";
|
||||
const OLLAMA_LIBRARY_URL: &str = "https://ollama.com/library";
|
||||
|
||||
pub struct OllamaCompletionProvider {
|
||||
api_url: String,
|
||||
model: OllamaModel,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
settings_version: usize,
|
||||
available_models: Vec<OllamaModel>,
|
||||
}
|
||||
|
||||
impl OllamaCompletionProvider {
|
||||
pub fn new(
|
||||
model: OllamaModel,
|
||||
api_url: String,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
settings_version: usize,
|
||||
cx: &AppContext,
|
||||
) -> Self {
|
||||
cx.spawn({
|
||||
let api_url = api_url.clone();
|
||||
let client = http_client.clone();
|
||||
let model = model.name.clone();
|
||||
|
||||
|_| async move {
|
||||
if model.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
preload_model(client.as_ref(), &api_url, &model).await
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Self {
|
||||
api_url,
|
||||
model,
|
||||
http_client,
|
||||
low_speed_timeout,
|
||||
settings_version,
|
||||
available_models: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
model: OllamaModel,
|
||||
api_url: String,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
settings_version: usize,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
cx.spawn({
|
||||
let api_url = api_url.clone();
|
||||
let client = self.http_client.clone();
|
||||
let model = model.name.clone();
|
||||
|
||||
|_| async move { preload_model(client.as_ref(), &api_url, &model).await }
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
if model.name.is_empty() {
|
||||
self.select_first_available_model()
|
||||
} else {
|
||||
self.model = model;
|
||||
}
|
||||
|
||||
self.api_url = api_url;
|
||||
self.low_speed_timeout = low_speed_timeout;
|
||||
self.settings_version = settings_version;
|
||||
}
|
||||
|
||||
pub fn available_models(&self) -> impl Iterator<Item = &OllamaModel> {
|
||||
self.available_models.iter()
|
||||
}
|
||||
|
||||
pub fn select_first_available_model(&mut self) {
|
||||
if let Some(model) = self.available_models.first() {
|
||||
self.model = model.clone();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn settings_version(&self) -> usize {
|
||||
self.settings_version
|
||||
}
|
||||
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
!self.available_models.is_empty()
|
||||
}
|
||||
|
||||
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
if self.is_authenticated() {
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
self.fetch_models(cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
self.fetch_models(cx)
|
||||
}
|
||||
|
||||
pub fn fetch_models(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
let http_client = self.http_client.clone();
|
||||
let api_url = self.api_url.clone();
|
||||
|
||||
// As a proxy for the server being "authenticated", we'll check if its up by fetching the models
|
||||
cx.spawn(|mut cx| async move {
|
||||
let models = get_models(http_client.as_ref(), &api_url, None).await?;
|
||||
|
||||
let mut models: Vec<OllamaModel> = models
|
||||
.into_iter()
|
||||
// Since there is no metadata from the Ollama API
|
||||
// indicating which models are embedding models,
|
||||
// simply filter out models with "-embed" in their name
|
||||
.filter(|model| !model.name.contains("-embed"))
|
||||
.map(|model| OllamaModel::new(&model.name))
|
||||
.collect();
|
||||
|
||||
models.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
|
||||
if let CompletionProvider::Ollama(provider) = provider {
|
||||
provider.available_models = models;
|
||||
|
||||
if !provider.available_models.is_empty() && provider.model.name.is_empty() {
|
||||
provider.select_first_available_model()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
let fetch_models = Box::new(move |cx: &mut WindowContext| {
|
||||
cx.update_global::<CompletionProvider, _>(|provider, cx| {
|
||||
if let CompletionProvider::Ollama(provider) = provider {
|
||||
provider.fetch_models(cx)
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
cx.new_view(|cx| DownloadOllamaMessage::new(fetch_models, cx))
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn model(&self) -> OllamaModel {
|
||||
self.model.clone()
|
||||
}
|
||||
|
||||
pub fn count_tokens(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
_cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<usize>> {
|
||||
// There is no endpoint for this _yet_ in Ollama
|
||||
// see: https://github.com/ollama/ollama/issues/1716 and https://github.com/ollama/ollama/issues/3582
|
||||
let token_count = request
|
||||
.messages
|
||||
.iter()
|
||||
.map(|msg| msg.content.chars().count())
|
||||
.sum::<usize>()
|
||||
/ 4;
|
||||
|
||||
async move { Ok(token_count) }.boxed()
|
||||
}
|
||||
|
||||
pub fn complete(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
let request = self.to_ollama_request(request);
|
||||
|
||||
let http_client = self.http_client.clone();
|
||||
let api_url = self.api_url.clone();
|
||||
let low_speed_timeout = self.low_speed_timeout;
|
||||
async move {
|
||||
let request =
|
||||
stream_chat_completion(http_client.as_ref(), &api_url, request, low_speed_timeout);
|
||||
let response = request.await?;
|
||||
let stream = response
|
||||
.filter_map(|response| async move {
|
||||
match response {
|
||||
Ok(delta) => {
|
||||
let content = match delta.message {
|
||||
ChatMessage::User { content } => content,
|
||||
ChatMessage::Assistant { content } => content,
|
||||
ChatMessage::System { content } => content,
|
||||
};
|
||||
Some(Ok(content))
|
||||
}
|
||||
Err(error) => Some(Err(error)),
|
||||
}
|
||||
})
|
||||
.boxed();
|
||||
Ok(stream)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn to_ollama_request(&self, request: LanguageModelRequest) -> ChatRequest {
|
||||
let model = match request.model {
|
||||
LanguageModel::Ollama(model) => model,
|
||||
_ => self.model(),
|
||||
};
|
||||
|
||||
ChatRequest {
|
||||
model: model.name,
|
||||
messages: request
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(|msg| match msg.role {
|
||||
Role::User => ChatMessage::User {
|
||||
content: msg.content,
|
||||
},
|
||||
Role::Assistant => ChatMessage::Assistant {
|
||||
content: msg.content,
|
||||
},
|
||||
Role::System => ChatMessage::System {
|
||||
content: msg.content,
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
keep_alive: model.keep_alive.unwrap_or_default(),
|
||||
stream: true,
|
||||
options: Some(ChatOptions {
|
||||
num_ctx: Some(model.max_tokens),
|
||||
stop: Some(request.stop),
|
||||
temperature: Some(request.temperature),
|
||||
..Default::default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Role> for ollama::Role {
|
||||
fn from(val: Role) -> Self {
|
||||
match val {
|
||||
Role::User => OllamaRole::User,
|
||||
Role::Assistant => OllamaRole::Assistant,
|
||||
Role::System => OllamaRole::System,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DownloadOllamaMessage {
|
||||
retry_connection: Box<dyn Fn(&mut WindowContext) -> Task<Result<()>>>,
|
||||
}
|
||||
|
||||
impl DownloadOllamaMessage {
|
||||
pub fn new(
|
||||
retry_connection: Box<dyn Fn(&mut WindowContext) -> Task<Result<()>>>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
Self { retry_connection }
|
||||
}
|
||||
|
||||
fn render_download_button(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
ButtonLike::new("download_ollama_button")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.child(Label::new("Get Ollama"))
|
||||
.on_click(move |_, cx| cx.open_url(OLLAMA_DOWNLOAD_URL))
|
||||
}
|
||||
|
||||
fn render_retry_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
ButtonLike::new("retry_ollama_models")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.child(Label::new("Retry"))
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
let connected = (this.retry_connection)(cx);
|
||||
|
||||
cx.spawn(|_this, _cx| async move {
|
||||
connected.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_next_steps(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.p_4()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new("Once Ollama is on your machine, make sure to download a model or two.")
|
||||
.size(LabelSize::Large),
|
||||
)
|
||||
.child(
|
||||
h_flex().w_full().p_4().justify_center().gap_2().child(
|
||||
ButtonLike::new("view-models")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.child(Label::new("View Available Models"))
|
||||
.on_click(move |_, cx| cx.open_url(OLLAMA_LIBRARY_URL)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DownloadOllamaMessage {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.p_4()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.child(Label::new("To use Ollama models via the assistant, Ollama must be running on your machine with at least one model downloaded.").size(LabelSize::Large))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.p_4()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
self.render_download_button(cx)
|
||||
)
|
||||
.child(
|
||||
self.render_retry_button(cx)
|
||||
)
|
||||
)
|
||||
.child(self.render_next_steps(cx))
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
@@ -336,7 +336,7 @@ impl Render for AuthenticationPrompt {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Label::new("Click on").size(LabelSize::Small))
|
||||
.child(Icon::new(IconName::Ai).size(IconSize::XSmall))
|
||||
.child(Icon::new(IconName::ZedAssistant).size(IconSize::XSmall))
|
||||
.child(
|
||||
Label::new("in the status bar to close this panel.").size(LabelSize::Small),
|
||||
),
|
||||
|
||||
225
crates/assistant/src/context_store.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{AppContext, Model, ModelContext, Task};
|
||||
use paths::contexts_dir;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc, time::Duration};
|
||||
use ui::Context;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedMessage {
|
||||
pub id: MessageId,
|
||||
pub start: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedContext {
|
||||
pub id: Option<String>,
|
||||
pub zed: String,
|
||||
pub version: String,
|
||||
pub text: String,
|
||||
pub messages: Vec<SavedMessage>,
|
||||
pub message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
pub summary: String,
|
||||
pub slash_command_output_sections: Vec<SlashCommandOutputSection<usize>>,
|
||||
}
|
||||
|
||||
impl SavedContext {
|
||||
pub const VERSION: &'static str = "0.3.0";
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedContextV0_2_0 {
|
||||
pub id: Option<String>,
|
||||
pub zed: String,
|
||||
pub version: String,
|
||||
pub text: String,
|
||||
pub messages: Vec<SavedMessage>,
|
||||
pub message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedContextV0_1_0 {
|
||||
id: Option<String>,
|
||||
zed: String,
|
||||
version: String,
|
||||
text: String,
|
||||
messages: Vec<SavedMessage>,
|
||||
message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: String,
|
||||
api_url: Option<String>,
|
||||
model: OpenAiModel,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SavedContextMetadata {
|
||||
pub title: String,
|
||||
pub path: PathBuf,
|
||||
pub mtime: chrono::DateTime<chrono::Local>,
|
||||
}
|
||||
|
||||
pub struct ContextStore {
|
||||
contexts_metadata: Vec<SavedContextMetadata>,
|
||||
fs: Arc<dyn Fs>,
|
||||
_watch_updates: Task<Option<()>>,
|
||||
}
|
||||
|
||||
impl ContextStore {
|
||||
pub fn new(fs: Arc<dyn Fs>, cx: &mut AppContext) -> Task<Result<Model<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
|
||||
let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
|
||||
|
||||
let this = cx.new_model(|cx: &mut ModelContext<Self>| Self {
|
||||
contexts_metadata: Vec::new(),
|
||||
fs,
|
||||
_watch_updates: cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
while events.next().await.is_some() {
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
}),
|
||||
})?;
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
.await
|
||||
.log_err();
|
||||
Ok(this)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(&self, path: PathBuf, cx: &AppContext) -> Task<Result<SavedContext>> {
|
||||
let fs = self.fs.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
let saved_context = fs.load(&path).await?;
|
||||
let saved_context_json = serde_json::from_str::<serde_json::Value>(&saved_context)?;
|
||||
match saved_context_json
|
||||
.get("version")
|
||||
.ok_or_else(|| anyhow!("version not found"))?
|
||||
{
|
||||
serde_json::Value::String(version) => match version.as_str() {
|
||||
SavedContext::VERSION => {
|
||||
Ok(serde_json::from_value::<SavedContext>(saved_context_json)?)
|
||||
}
|
||||
"0.2.0" => {
|
||||
let saved_context =
|
||||
serde_json::from_value::<SavedContextV0_2_0>(saved_context_json)?;
|
||||
Ok(SavedContext {
|
||||
id: saved_context.id,
|
||||
zed: saved_context.zed,
|
||||
version: saved_context.version,
|
||||
text: saved_context.text,
|
||||
messages: saved_context.messages,
|
||||
message_metadata: saved_context.message_metadata,
|
||||
summary: saved_context.summary,
|
||||
slash_command_output_sections: Vec::new(),
|
||||
})
|
||||
}
|
||||
"0.1.0" => {
|
||||
let saved_context =
|
||||
serde_json::from_value::<SavedContextV0_1_0>(saved_context_json)?;
|
||||
Ok(SavedContext {
|
||||
id: saved_context.id,
|
||||
zed: saved_context.zed,
|
||||
version: saved_context.version,
|
||||
text: saved_context.text,
|
||||
messages: saved_context.messages,
|
||||
message_metadata: saved_context.message_metadata,
|
||||
summary: saved_context.summary,
|
||||
slash_command_output_sections: Vec::new(),
|
||||
})
|
||||
}
|
||||
_ => Err(anyhow!("unrecognized saved context version: {}", version)),
|
||||
},
|
||||
_ => Err(anyhow!("version not found on saved context")),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn search(&self, query: String, cx: &AppContext) -> Task<Vec<SavedContextMetadata>> {
|
||||
let metadata = self.contexts_metadata.clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
if query.is_empty() {
|
||||
metadata
|
||||
} else {
|
||||
let candidates = metadata
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, metadata)| StringMatchCandidate::new(id, metadata.title.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| metadata[mat.candidate_id].clone())
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
fs.create_dir(contexts_dir()).await?;
|
||||
|
||||
let mut paths = fs.read_dir(contexts_dir()).await?;
|
||||
let mut contexts = Vec::<SavedContextMetadata>::new();
|
||||
while let Some(path) = paths.next().await {
|
||||
let path = path?;
|
||||
if path.extension() != Some(OsStr::new("json")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pattern = r" - \d+.zed.json$";
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
|
||||
let metadata = fs.metadata(&path).await?;
|
||||
if let Some((file_name, metadata)) = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.zip(metadata)
|
||||
{
|
||||
// This is used to filter out contexts saved by the new assistant.
|
||||
if !re.is_match(file_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(title) = re.replace(file_name, "").lines().next() {
|
||||
contexts.push(SavedContextMetadata {
|
||||
title: title.to_string(),
|
||||
path,
|
||||
mtime: metadata.mtime.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
contexts.sort_unstable_by_key(|context| Reverse(context.mtime));
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.contexts_metadata = contexts;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use crate::{assistant_settings::AssistantSettings, CompletionProvider, ToggleModelSelector};
|
||||
use fs::Fs;
|
||||
use settings::update_settings_file;
|
||||
use ui::{popover_menu, prelude::*, ButtonLike, ContextMenu, PopoverMenuHandle, Tooltip};
|
||||
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ModelSelector {
|
||||
@@ -19,7 +19,7 @@ impl ModelSelector {
|
||||
|
||||
impl RenderOnce for ModelSelector {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
popover_menu("model-switcher")
|
||||
PopoverMenu::new("model-switcher")
|
||||
.with_handle(self.handle)
|
||||
.menu(move |cx| {
|
||||
ContextMenu::build(cx, |mut menu, cx| {
|
||||
|
||||
@@ -13,9 +13,9 @@ use futures::{
|
||||
};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
actions, point, size, AnyElement, AppContext, BackgroundExecutor, Bounds, DevicePixels,
|
||||
EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TitlebarOptions,
|
||||
UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
|
||||
actions, percentage, point, size, Animation, AnimationExt, AppContext, BackgroundExecutor,
|
||||
Bounds, EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TitlebarOptions,
|
||||
Transformation, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
|
||||
};
|
||||
use heed::{types::SerdeBincode, Database, RoTxn};
|
||||
use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
|
||||
@@ -23,17 +23,20 @@ use parking_lot::RwLock;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use rope::Rope;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
future::Future,
|
||||
path::PathBuf,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
div, prelude::*, IconButtonShape, ListHeader, ListItem, ListItemSpacing, ListSubHeader,
|
||||
ParentElement, Render, SharedString, Styled, TitleBar, Tooltip, ViewContext, VisualContext,
|
||||
div, prelude::*, IconButtonShape, ListItem, ListItemSpacing, ParentElement, Render,
|
||||
SharedString, Styled, TitleBar, Tooltip, ViewContext, VisualContext,
|
||||
};
|
||||
use util::{paths::PROMPTS_DIR, ResultExt, TryFutureExt};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -45,7 +48,7 @@ actions!(
|
||||
/// Init starts loading the PromptStore in the background and assigns
|
||||
/// a shared future to a global.
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
let db_path = PROMPTS_DIR.join("prompts-library-db.0.mdb");
|
||||
let db_path = paths::prompts_dir().join("prompts-library-db.0.mdb");
|
||||
let prompt_store_future = PromptStore::new(db_path, cx.background_executor().clone())
|
||||
.then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
|
||||
.boxed()
|
||||
@@ -77,11 +80,7 @@ pub fn open_prompt_library(
|
||||
cx.spawn(|cx| async move {
|
||||
let store = store.await?;
|
||||
cx.update(|cx| {
|
||||
let bounds = Bounds::centered(
|
||||
None,
|
||||
size(DevicePixels::from(1024), DevicePixels::from(768)),
|
||||
cx,
|
||||
);
|
||||
let bounds = Bounds::centered(None, size(px(1024.0), px(768.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
titlebar: Some(TitlebarOptions {
|
||||
@@ -94,7 +93,7 @@ pub fn open_prompt_library(
|
||||
},
|
||||
|cx| cx.new_view(|cx| PromptLibrary::new(store, language_registry, cx)),
|
||||
)
|
||||
})
|
||||
})?
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -121,41 +120,23 @@ struct PromptEditor {
|
||||
struct PromptPickerDelegate {
|
||||
store: Arc<PromptStore>,
|
||||
selected_index: usize,
|
||||
entries: Vec<PromptPickerEntry>,
|
||||
matches: Vec<PromptMetadata>,
|
||||
}
|
||||
|
||||
enum PromptPickerEvent {
|
||||
Selected { prompt_id: Option<PromptId> },
|
||||
Selected { prompt_id: PromptId },
|
||||
Confirmed { prompt_id: PromptId },
|
||||
Deleted { prompt_id: PromptId },
|
||||
ToggledDefault { prompt_id: PromptId },
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PromptPickerEntry {
|
||||
DefaultPromptsHeader,
|
||||
DefaultPromptsEmpty,
|
||||
AllPromptsHeader,
|
||||
AllPromptsEmpty,
|
||||
Prompt(PromptMetadata),
|
||||
}
|
||||
|
||||
impl PromptPickerEntry {
|
||||
fn prompt_id(&self) -> Option<PromptId> {
|
||||
match self {
|
||||
PromptPickerEntry::Prompt(metadata) => Some(metadata.id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PromptPickerEvent> for Picker<PromptPickerDelegate> {}
|
||||
|
||||
impl PickerDelegate for PromptPickerDelegate {
|
||||
type ListItem = AnyElement;
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.entries.len()
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
@@ -164,14 +145,11 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
let prompt_id = if let Some(PromptPickerEntry::Prompt(prompt)) =
|
||||
self.entries.get(self.selected_index)
|
||||
{
|
||||
Some(prompt.id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
cx.emit(PromptPickerEvent::Selected { prompt_id });
|
||||
if let Some(prompt) = self.matches.get(self.selected_index) {
|
||||
cx.emit(PromptPickerEvent::Selected {
|
||||
prompt_id: prompt.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
@@ -180,48 +158,24 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
let search = self.store.search(query);
|
||||
let prev_prompt_id = self
|
||||
.entries
|
||||
.get(self.selected_index)
|
||||
.and_then(|mat| mat.prompt_id());
|
||||
let prev_prompt_id = self.matches.get(self.selected_index).map(|mat| mat.id);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let (entries, selected_index) = cx
|
||||
let (matches, selected_index) = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
let prompts = search.await;
|
||||
let (default_prompts, prompts) = prompts
|
||||
.into_iter()
|
||||
.partition::<Vec<_>, _>(|prompt| prompt.default);
|
||||
|
||||
let mut entries = Vec::new();
|
||||
entries.push(PromptPickerEntry::DefaultPromptsHeader);
|
||||
if default_prompts.is_empty() {
|
||||
entries.push(PromptPickerEntry::DefaultPromptsEmpty);
|
||||
} else {
|
||||
entries.extend(default_prompts.into_iter().map(PromptPickerEntry::Prompt));
|
||||
}
|
||||
|
||||
entries.push(PromptPickerEntry::AllPromptsHeader);
|
||||
if prompts.is_empty() {
|
||||
entries.push(PromptPickerEntry::AllPromptsEmpty);
|
||||
} else {
|
||||
entries.extend(prompts.into_iter().map(PromptPickerEntry::Prompt));
|
||||
}
|
||||
let matches = search.await;
|
||||
|
||||
let selected_index = prev_prompt_id
|
||||
.and_then(|prev_prompt_id| {
|
||||
entries
|
||||
.iter()
|
||||
.position(|entry| entry.prompt_id() == Some(prev_prompt_id))
|
||||
matches.iter().position(|entry| entry.id == prev_prompt_id)
|
||||
})
|
||||
.or_else(|| entries.iter().position(|entry| entry.prompt_id().is_some()))
|
||||
.unwrap_or(0);
|
||||
(entries, selected_index)
|
||||
(matches, selected_index)
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate.entries = entries;
|
||||
this.delegate.matches = matches;
|
||||
this.delegate.set_selected_index(selected_index, cx);
|
||||
cx.notify();
|
||||
})
|
||||
@@ -230,7 +184,7 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some(PromptPickerEntry::Prompt(prompt)) = self.entries.get(self.selected_index) {
|
||||
if let Some(prompt) = self.matches.get(self.selected_index) {
|
||||
cx.emit(PromptPickerEvent::Confirmed {
|
||||
prompt_id: prompt.id,
|
||||
});
|
||||
@@ -245,81 +199,73 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let prompt = self.entries.get(ix)?;
|
||||
let element = match prompt {
|
||||
PromptPickerEntry::DefaultPromptsHeader => ListHeader::new("Default Prompts")
|
||||
.inset(true)
|
||||
.start_slot(Icon::new(IconName::ZedAssistant))
|
||||
.selected(selected)
|
||||
.into_any_element(),
|
||||
PromptPickerEntry::DefaultPromptsEmpty => {
|
||||
ListSubHeader::new("Star a prompt to add it to the default context")
|
||||
.inset(true)
|
||||
.selected(selected)
|
||||
.into_any_element()
|
||||
}
|
||||
PromptPickerEntry::AllPromptsHeader => ListHeader::new("All Prompts")
|
||||
.inset(true)
|
||||
.start_slot(Icon::new(IconName::Library))
|
||||
.selected(selected)
|
||||
.into_any_element(),
|
||||
PromptPickerEntry::AllPromptsEmpty => ListSubHeader::new("No prompts")
|
||||
.inset(true)
|
||||
.selected(selected)
|
||||
.into_any_element(),
|
||||
PromptPickerEntry::Prompt(prompt) => {
|
||||
let default = prompt.default;
|
||||
let prompt_id = prompt.id;
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.selected(selected)
|
||||
.child(Label::new(
|
||||
prompt.title.clone().unwrap_or("Untitled".into()),
|
||||
))
|
||||
.end_hover_slot(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
IconButton::new("delete-prompt", IconName::Trash)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(PromptPickerEvent::Deleted { prompt_id })
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
IconButton::new(
|
||||
"toggle-default-prompt",
|
||||
if default {
|
||||
IconName::ZedAssistantFilled
|
||||
} else {
|
||||
IconName::ZedAssistant
|
||||
},
|
||||
)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(
|
||||
if default {
|
||||
"Remove from Default Prompt"
|
||||
} else {
|
||||
"Add to Default Prompt"
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(
|
||||
move |_, _, cx| {
|
||||
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
|
||||
},
|
||||
)),
|
||||
),
|
||||
let prompt = self.matches.get(ix)?;
|
||||
let default = prompt.default;
|
||||
let prompt_id = prompt.id;
|
||||
let element = ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.selected(selected)
|
||||
.child(h_flex().h_5().line_height(relative(1.)).child(Label::new(
|
||||
prompt.title.clone().unwrap_or("Untitled".into()),
|
||||
)))
|
||||
.end_slot::<IconButton>(default.then(|| {
|
||||
IconButton::new("toggle-default-prompt", IconName::SparkleFilled)
|
||||
.selected(true)
|
||||
.icon_color(Color::Accent)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Remove from Default Prompt", cx))
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
|
||||
}))
|
||||
}))
|
||||
.end_hover_slot(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
IconButton::new("delete-prompt", IconName::Trash)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(PromptPickerEvent::Deleted { prompt_id })
|
||||
})),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
};
|
||||
.child(
|
||||
IconButton::new("toggle-default-prompt", IconName::Sparkle)
|
||||
.selected(default)
|
||||
.selected_icon(IconName::SparkleFilled)
|
||||
.icon_color(if default { Color::Accent } else { Color::Muted })
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(
|
||||
if default {
|
||||
"Remove from Default Prompt"
|
||||
} else {
|
||||
"Add to Default Prompt"
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
|
||||
})),
|
||||
),
|
||||
);
|
||||
Some(element)
|
||||
}
|
||||
|
||||
fn render_editor(&self, editor: &View<Editor>, cx: &mut ViewContext<Picker<Self>>) -> Div {
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_md()
|
||||
.overflow_hidden()
|
||||
.flex_none()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.mx_2()
|
||||
.child(editor.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptLibrary {
|
||||
@@ -331,11 +277,13 @@ impl PromptLibrary {
|
||||
let delegate = PromptPickerDelegate {
|
||||
store: store.clone(),
|
||||
selected_index: 0,
|
||||
entries: Vec::new(),
|
||||
matches: Vec::new(),
|
||||
};
|
||||
|
||||
let picker = cx.new_view(|cx| {
|
||||
let picker = Picker::list(delegate, cx).modal(false).max_height(None);
|
||||
let picker = Picker::uniform_list(delegate, cx)
|
||||
.modal(false)
|
||||
.max_height(None);
|
||||
picker.focus(cx);
|
||||
picker
|
||||
});
|
||||
@@ -358,11 +306,7 @@ impl PromptLibrary {
|
||||
) {
|
||||
match event {
|
||||
PromptPickerEvent::Selected { prompt_id } => {
|
||||
if let Some(prompt_id) = *prompt_id {
|
||||
self.load_prompt(prompt_id, false, cx);
|
||||
} else {
|
||||
self.focus_picker(&Default::default(), cx);
|
||||
}
|
||||
self.load_prompt(*prompt_id, false, cx);
|
||||
}
|
||||
PromptPickerEvent::Confirmed { prompt_id } => {
|
||||
self.load_prompt(*prompt_id, true, cx);
|
||||
@@ -506,6 +450,7 @@ impl PromptLibrary {
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_use_modal_editing(false);
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
|
||||
editor.set_completion_provider(Box::new(
|
||||
SlashCommandCompletionProvider::new(commands, None, None),
|
||||
@@ -549,21 +494,23 @@ impl PromptLibrary {
|
||||
if let Some(prompt_id) = prompt_id {
|
||||
if picker
|
||||
.delegate
|
||||
.entries
|
||||
.matches
|
||||
.get(picker.delegate.selected_index())
|
||||
.map_or(true, |old_selected_prompt| {
|
||||
old_selected_prompt.prompt_id() != Some(prompt_id)
|
||||
old_selected_prompt.id != prompt_id
|
||||
})
|
||||
{
|
||||
if let Some(ix) = picker
|
||||
.delegate
|
||||
.entries
|
||||
.matches
|
||||
.iter()
|
||||
.position(|mat| mat.prompt_id() == Some(prompt_id))
|
||||
.position(|mat| mat.id == prompt_id)
|
||||
{
|
||||
picker.set_selected_index(ix, true, cx);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
picker.focus(cx);
|
||||
}
|
||||
});
|
||||
cx.notify();
|
||||
@@ -642,19 +589,6 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel_last_inline_assist(
|
||||
&mut self,
|
||||
_: &editor::actions::Cancel,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let canceled = InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.cancel_last_inline_assist(cx)
|
||||
});
|
||||
if !canceled {
|
||||
cx.propagate();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_prompt_editor_event(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
@@ -746,14 +680,13 @@ impl PromptLibrary {
|
||||
.child(
|
||||
h_flex()
|
||||
.p(Spacing::Small.rems(cx))
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.h(TitleBar::height(cx))
|
||||
.w_full()
|
||||
.flex_none()
|
||||
.justify_end()
|
||||
.child(
|
||||
IconButton::new("new-prompt", IconName::Plus)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::for_action("New Prompt", &NewPrompt, cx))
|
||||
.on_click(|_, cx| {
|
||||
@@ -775,21 +708,29 @@ impl PromptLibrary {
|
||||
.flex_none()
|
||||
.min_w_64()
|
||||
.children(self.active_prompt_id.and_then(|prompt_id| {
|
||||
let buffer_font = ThemeSettings::get_global(cx).buffer_font.family.clone();
|
||||
let prompt_metadata = self.store.metadata(prompt_id)?;
|
||||
let prompt_editor = &self.prompt_editors[&prompt_id];
|
||||
let focus_handle = prompt_editor.editor.focus_handle(cx);
|
||||
let current_model = CompletionProvider::global(cx).model();
|
||||
let token_count = prompt_editor.token_count.map(|count| count.to_string());
|
||||
|
||||
Some(
|
||||
h_flex()
|
||||
.id("prompt-editor-inner")
|
||||
.size_full()
|
||||
.items_start()
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.focus(&focus_handle);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.on_action(cx.listener(Self::focus_picker))
|
||||
.on_action(cx.listener(Self::inline_assist))
|
||||
.on_action(cx.listener(Self::cancel_last_inline_assist))
|
||||
.flex_grow()
|
||||
.h_full()
|
||||
.pt(Spacing::Large.rems(cx))
|
||||
.pl(Spacing::Large.rems(cx))
|
||||
.pt(Spacing::XXLarge.rems(cx))
|
||||
.pl(Spacing::XXLarge.rems(cx))
|
||||
.child(prompt_editor.editor.clone()),
|
||||
)
|
||||
.child(
|
||||
@@ -797,49 +738,92 @@ impl PromptLibrary {
|
||||
.w_12()
|
||||
.py(Spacing::Large.rems(cx))
|
||||
.justify_start()
|
||||
.items_center()
|
||||
.gap_4()
|
||||
.child(
|
||||
IconButton::new(
|
||||
"toggle-default-prompt",
|
||||
if prompt_metadata.default {
|
||||
IconName::ZedAssistantFilled
|
||||
} else {
|
||||
IconName::ZedAssistant
|
||||
},
|
||||
)
|
||||
.size(ButtonSize::Large)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action(
|
||||
if prompt_metadata.default {
|
||||
"Remove from Default Prompt"
|
||||
} else {
|
||||
"Add to Default Prompt"
|
||||
},
|
||||
&ToggleDefaultPrompt,
|
||||
cx,
|
||||
.items_end()
|
||||
.gap_1()
|
||||
.child(h_flex().h_8().font_family(buffer_font).when_some_else(
|
||||
token_count,
|
||||
|tokens_ready, token_count| {
|
||||
tokens_ready.pr_3().justify_end().child(
|
||||
// This isn't actually a button, it just let's us easily add
|
||||
// a tooltip to the token count.
|
||||
Button::new("token_count", token_count.clone())
|
||||
.style(ButtonStyle::Transparent)
|
||||
.color(Color::Muted)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
format!("{} tokens", token_count,),
|
||||
None,
|
||||
format!(
|
||||
"Model: {}",
|
||||
current_model.display_name()
|
||||
),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(ToggleDefaultPrompt));
|
||||
}),
|
||||
},
|
||||
|tokens_loading| {
|
||||
tokens_loading.w_12().justify_center().child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(4)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(
|
||||
percentage(delta),
|
||||
))
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
))
|
||||
.child(
|
||||
h_flex().justify_center().w_12().h_8().child(
|
||||
IconButton::new("toggle-default-prompt", IconName::Sparkle)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.selected(prompt_metadata.default)
|
||||
.selected_icon(IconName::SparkleFilled)
|
||||
.icon_color(if prompt_metadata.default {
|
||||
Color::Accent
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(
|
||||
if prompt_metadata.default {
|
||||
"Remove from Default Prompt"
|
||||
} else {
|
||||
"Add to Default Prompt"
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(ToggleDefaultPrompt));
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("delete-prompt", IconName::Trash)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action("Delete Prompt", &DeletePrompt, cx)
|
||||
})
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(DeletePrompt));
|
||||
}),
|
||||
)
|
||||
.children(prompt_editor.token_count.map(|token_count| {
|
||||
h_flex()
|
||||
.justify_center()
|
||||
.child(Label::new(token_count.to_string()))
|
||||
})),
|
||||
h_flex().justify_center().w_12().h_8().child(
|
||||
IconButton::new("delete-prompt", IconName::Trash)
|
||||
.size(ButtonSize::Large)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action(
|
||||
"Delete Prompt",
|
||||
&DeletePrompt,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(DeletePrompt));
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}))
|
||||
@@ -848,6 +832,9 @@ impl PromptLibrary {
|
||||
|
||||
impl Render for PromptLibrary {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let ui_font = theme::setup_ui_font(cx);
|
||||
let theme = cx.theme().clone();
|
||||
|
||||
h_flex()
|
||||
.id("prompt-manager")
|
||||
.key_context("PromptLibrary")
|
||||
@@ -858,6 +845,8 @@ impl Render for PromptLibrary {
|
||||
}))
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.font(ui_font)
|
||||
.text_color(theme.colors().text)
|
||||
.child(self.render_prompt_list(cx))
|
||||
.child(self.render_active_prompt(cx))
|
||||
}
|
||||
@@ -1026,7 +1015,7 @@ impl PromptStore {
|
||||
let cached_metadata = self.metadata_cache.read().metadata.clone();
|
||||
let executor = self.executor.clone();
|
||||
self.executor.spawn(async move {
|
||||
if query.is_empty() {
|
||||
let mut matches = if query.is_empty() {
|
||||
cached_metadata
|
||||
} else {
|
||||
let candidates = cached_metadata
|
||||
@@ -1052,7 +1041,9 @@ impl PromptStore {
|
||||
.into_iter()
|
||||
.map(|mat| cached_metadata[mat.candidate_id].clone())
|
||||
.collect()
|
||||
}
|
||||
};
|
||||
matches.sort_by_key(|metadata| Reverse(metadata.default));
|
||||
matches
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::paths::CONVERSATIONS_DIR;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedMessage {
|
||||
pub id: MessageId,
|
||||
pub start: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedConversation {
|
||||
pub id: Option<String>,
|
||||
pub zed: String,
|
||||
pub version: String,
|
||||
pub text: String,
|
||||
pub messages: Vec<SavedMessage>,
|
||||
pub message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
impl SavedConversation {
|
||||
pub const VERSION: &'static str = "0.2.0";
|
||||
|
||||
pub async fn load(path: &Path, fs: &dyn Fs) -> Result<Self> {
|
||||
let saved_conversation = fs.load(path).await?;
|
||||
let saved_conversation_json =
|
||||
serde_json::from_str::<serde_json::Value>(&saved_conversation)?;
|
||||
match saved_conversation_json
|
||||
.get("version")
|
||||
.ok_or_else(|| anyhow!("version not found"))?
|
||||
{
|
||||
serde_json::Value::String(version) => match version.as_str() {
|
||||
Self::VERSION => Ok(serde_json::from_value::<Self>(saved_conversation_json)?),
|
||||
"0.1.0" => {
|
||||
let saved_conversation =
|
||||
serde_json::from_value::<SavedConversationV0_1_0>(saved_conversation_json)?;
|
||||
Ok(Self {
|
||||
id: saved_conversation.id,
|
||||
zed: saved_conversation.zed,
|
||||
version: saved_conversation.version,
|
||||
text: saved_conversation.text,
|
||||
messages: saved_conversation.messages,
|
||||
message_metadata: saved_conversation.message_metadata,
|
||||
summary: saved_conversation.summary,
|
||||
})
|
||||
}
|
||||
_ => Err(anyhow!(
|
||||
"unrecognized saved conversation version: {}",
|
||||
version
|
||||
)),
|
||||
},
|
||||
_ => Err(anyhow!("version not found on saved conversation")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedConversationV0_1_0 {
|
||||
id: Option<String>,
|
||||
zed: String,
|
||||
version: String,
|
||||
text: String,
|
||||
messages: Vec<SavedMessage>,
|
||||
message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: String,
|
||||
api_url: Option<String>,
|
||||
model: OpenAiModel,
|
||||
}
|
||||
|
||||
pub struct SavedConversationMetadata {
|
||||
pub title: String,
|
||||
pub path: PathBuf,
|
||||
pub mtime: chrono::DateTime<chrono::Local>,
|
||||
}
|
||||
|
||||
impl SavedConversationMetadata {
|
||||
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
|
||||
fs.create_dir(&CONVERSATIONS_DIR).await?;
|
||||
|
||||
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
|
||||
let mut conversations = Vec::<SavedConversationMetadata>::new();
|
||||
while let Some(path) = paths.next().await {
|
||||
let path = path?;
|
||||
if path.extension() != Some(OsStr::new("json")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pattern = r" - \d+.zed.json$";
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
|
||||
let metadata = fs.metadata(&path).await?;
|
||||
if let Some((file_name, metadata)) = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.zip(metadata)
|
||||
{
|
||||
// This is used to filter out conversations saved by the new assistant.
|
||||
if !re.is_match(file_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let title = re.replace(file_name, "");
|
||||
conversations.push(Self {
|
||||
title: title.into_owned(),
|
||||
path,
|
||||
mtime: metadata.mtime.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
|
||||
|
||||
Ok(conversations)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::assistant_panel::ConversationEditor;
|
||||
use crate::assistant_panel::ContextEditor;
|
||||
use anyhow::Result;
|
||||
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry};
|
||||
use editor::{CompletionProvider, Editor};
|
||||
@@ -18,8 +18,10 @@ use workspace::Workspace;
|
||||
|
||||
pub mod active_command;
|
||||
pub mod default_command;
|
||||
pub mod diagnostics_command;
|
||||
pub mod fetch_command;
|
||||
pub mod file_command;
|
||||
pub mod now_command;
|
||||
pub mod project_command;
|
||||
pub mod prompt_command;
|
||||
pub mod rustdoc_command;
|
||||
@@ -29,7 +31,7 @@ pub mod tabs_command;
|
||||
pub(crate) struct SlashCommandCompletionProvider {
|
||||
commands: Arc<SlashCommandRegistry>,
|
||||
cancel_flag: Mutex<Arc<AtomicBool>>,
|
||||
editor: Option<WeakView<ConversationEditor>>,
|
||||
editor: Option<WeakView<ContextEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
}
|
||||
|
||||
@@ -43,7 +45,7 @@ pub(crate) struct SlashCommandLine {
|
||||
impl SlashCommandCompletionProvider {
|
||||
pub fn new(
|
||||
commands: Arc<SlashCommandRegistry>,
|
||||
editor: Option<WeakView<ConversationEditor>>,
|
||||
editor: Option<WeakView<ContextEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
@@ -216,6 +218,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_position: Anchor,
|
||||
_: editor::CompletionContext,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
let Some((name, argument, command_range, argument_range)) =
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput};
|
||||
use super::{
|
||||
file_command::{build_entry_output_section, codeblock_fence_for_path},
|
||||
SlashCommand, SlashCommandOutput,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use editor::Editor;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
use ui::{IntoElement, WindowContext};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use ui::WindowContext;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct ActiveSlashCommand;
|
||||
@@ -24,9 +27,9 @@ impl SlashCommand for ActiveSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
@@ -60,14 +63,8 @@ impl SlashCommand for ActiveSlashCommand {
|
||||
let text = cx.background_executor().spawn({
|
||||
let path = path.clone();
|
||||
async move {
|
||||
let path = path
|
||||
.as_ref()
|
||||
.map(|path| path.to_string_lossy())
|
||||
.unwrap_or_else(|| Cow::Borrowed("untitled"));
|
||||
|
||||
let mut output = String::with_capacity(path.len() + snapshot.len() + 9);
|
||||
output.push_str("```");
|
||||
output.push_str(&path);
|
||||
let mut output = String::new();
|
||||
output.push_str(&codeblock_fence_for_path(path.as_deref(), None));
|
||||
output.push('\n');
|
||||
for chunk in snapshot.as_rope().chunks() {
|
||||
output.push_str(chunk);
|
||||
@@ -84,18 +81,12 @@ impl SlashCommand for ActiveSlashCommand {
|
||||
let range = 0..text.len();
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
sections: vec![build_entry_output_section(
|
||||
range,
|
||||
render_placeholder: Arc::new(move |id, unfold, _| {
|
||||
FilePlaceholder {
|
||||
id,
|
||||
path: path.clone(),
|
||||
line_range: None,
|
||||
unfold,
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
path.as_deref(),
|
||||
false,
|
||||
None,
|
||||
)],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{prompt_command::PromptPlaceholder, SlashCommand, SlashCommandOutput};
|
||||
use super::{SlashCommand, SlashCommandOutput};
|
||||
use crate::prompt_library::PromptStore;
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
@@ -31,7 +31,7 @@ impl SlashCommand for DefaultSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
@@ -53,7 +53,7 @@ impl SlashCommand for DefaultSlashCommand {
|
||||
let prompts = store.default_prompt_metadata();
|
||||
|
||||
let mut text = String::new();
|
||||
writeln!(text, "Default Prompt:").unwrap();
|
||||
text.push('\n');
|
||||
for prompt in prompts {
|
||||
if let Some(title) = prompt.title {
|
||||
writeln!(text, "/prompt {}", title).unwrap();
|
||||
@@ -61,17 +61,15 @@ impl SlashCommand for DefaultSlashCommand {
|
||||
}
|
||||
text.pop();
|
||||
|
||||
if text.is_empty() {
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
Ok(SlashCommandOutput {
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range: 0..text.len(),
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
PromptPlaceholder {
|
||||
title: "Default".into(),
|
||||
id,
|
||||
unfold,
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
icon: IconName::Library,
|
||||
label: "Default".into(),
|
||||
}],
|
||||
text,
|
||||
run_commands_in_text: true,
|
||||
|
||||
419
crates/assistant/src/slash_command/diagnostics_command.rs
Normal file
@@ -0,0 +1,419 @@
|
||||
use super::{SlashCommand, SlashCommandOutput};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use fuzzy::{PathMatch, StringMatchCandidate};
|
||||
use gpui::{AppContext, Model, Task, View, WeakView};
|
||||
use language::{
|
||||
Anchor, BufferSnapshot, DiagnosticEntry, DiagnosticSeverity, LspAdapterDelegate,
|
||||
OffsetRangeExt, ToOffset,
|
||||
};
|
||||
use project::{DiagnosticSummary, PathMatchCandidateSet, Project};
|
||||
use rope::Point;
|
||||
use std::fmt::Write;
|
||||
use std::{
|
||||
ops::Range,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use util::paths::PathMatcher;
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct DiagnosticsCommand;
|
||||
|
||||
impl DiagnosticsCommand {
|
||||
fn search_paths(
|
||||
&self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &View<Workspace>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
if query.is_empty() {
|
||||
let workspace = workspace.read(cx);
|
||||
let entries = workspace.recent_navigation_history(Some(10), cx);
|
||||
let path_prefix: Arc<str> = "".into();
|
||||
Task::ready(
|
||||
entries
|
||||
.into_iter()
|
||||
.map(|(entry, _)| PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: entry.worktree_id.to_usize(),
|
||||
path: entry.path.clone(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
|
||||
let candidate_sets = worktrees
|
||||
.into_iter()
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
PathMatchCandidateSet {
|
||||
snapshot: worktree.snapshot(),
|
||||
include_ignored: worktree
|
||||
.root_entry()
|
||||
.map_or(false, |entry| entry.is_ignored),
|
||||
include_root_name: false,
|
||||
candidates: project::Candidates::Entries,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
fuzzy::match_path_sets(
|
||||
candidate_sets.as_slice(),
|
||||
query.as_str(),
|
||||
None,
|
||||
false,
|
||||
100,
|
||||
&cancellation_flag,
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommand for DiagnosticsCommand {
|
||||
fn name(&self) -> String {
|
||||
"diagnostics".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert diagnostics".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Diagnostics".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
let query = query.split_whitespace().last().unwrap_or("").to_string();
|
||||
|
||||
let paths = self.search_paths(query.clone(), cancellation_flag.clone(), &workspace, cx);
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
let mut matches: Vec<String> = paths
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|path_match| {
|
||||
format!(
|
||||
"{}{}",
|
||||
path_match.path_prefix,
|
||||
path_match.path.to_string_lossy()
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
matches.extend(
|
||||
fuzzy::match_strings(
|
||||
&Options::match_candidates_for_args(),
|
||||
&query,
|
||||
false,
|
||||
10,
|
||||
&cancellation_flag,
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|candidate| candidate.string),
|
||||
);
|
||||
|
||||
Ok(matches)
|
||||
})
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
let options = Options::parse(argument);
|
||||
|
||||
let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
|
||||
cx.spawn(move |_| async move {
|
||||
let (text, sections) = task.await?;
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: sections
|
||||
.into_iter()
|
||||
.map(|(range, placeholder_type)| SlashCommandOutputSection {
|
||||
range,
|
||||
icon: match placeholder_type {
|
||||
PlaceholderType::Root(_, _) => IconName::ExclamationTriangle,
|
||||
PlaceholderType::File(_) => IconName::File,
|
||||
PlaceholderType::Diagnostic(DiagnosticType::Error, _) => {
|
||||
IconName::XCircle
|
||||
}
|
||||
PlaceholderType::Diagnostic(DiagnosticType::Warning, _) => {
|
||||
IconName::ExclamationTriangle
|
||||
}
|
||||
},
|
||||
label: match placeholder_type {
|
||||
PlaceholderType::Root(summary, source) => {
|
||||
let mut label = String::new();
|
||||
label.push_str("Diagnostics");
|
||||
if let Some(source) = source {
|
||||
write!(label, " ({})", source).unwrap();
|
||||
}
|
||||
|
||||
if summary.error_count > 0 || summary.warning_count > 0 {
|
||||
label.push(':');
|
||||
|
||||
if summary.error_count > 0 {
|
||||
write!(label, " {} errors", summary.error_count).unwrap();
|
||||
if summary.warning_count > 0 {
|
||||
label.push_str(",");
|
||||
}
|
||||
}
|
||||
|
||||
if summary.warning_count > 0 {
|
||||
write!(label, " {} warnings", summary.warning_count)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
label.into()
|
||||
}
|
||||
PlaceholderType::File(file_path) => file_path.into(),
|
||||
PlaceholderType::Diagnostic(_, message) => message.into(),
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Options {
|
||||
include_warnings: bool,
|
||||
path_matcher: Option<PathMatcher>,
|
||||
}
|
||||
|
||||
const INCLUDE_WARNINGS_ARGUMENT: &str = "--include-warnings";
|
||||
|
||||
impl Options {
|
||||
fn parse(arguments_line: Option<&str>) -> Self {
|
||||
arguments_line
|
||||
.map(|arguments_line| {
|
||||
let args = arguments_line.split_whitespace().collect::<Vec<_>>();
|
||||
let mut include_warnings = false;
|
||||
let mut path_matcher = None;
|
||||
for arg in args {
|
||||
if arg == INCLUDE_WARNINGS_ARGUMENT {
|
||||
include_warnings = true;
|
||||
} else {
|
||||
path_matcher = PathMatcher::new(&[arg.to_owned()]).log_err();
|
||||
}
|
||||
}
|
||||
Self {
|
||||
include_warnings,
|
||||
path_matcher,
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn match_candidates_for_args() -> [StringMatchCandidate; 1] {
|
||||
[StringMatchCandidate::new(
|
||||
0,
|
||||
INCLUDE_WARNINGS_ARGUMENT.to_string(),
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_diagnostics(
|
||||
project: Model<Project>,
|
||||
options: Options,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<(String, Vec<(Range<usize>, PlaceholderType)>)>> {
|
||||
let error_source = if let Some(path_matcher) = &options.path_matcher {
|
||||
debug_assert_eq!(path_matcher.sources().len(), 1);
|
||||
Some(path_matcher.sources().first().cloned().unwrap_or_default())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let project_handle = project.downgrade();
|
||||
let diagnostic_summaries: Vec<_> = project.read(cx).diagnostic_summaries(false, cx).collect();
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
let mut text = String::new();
|
||||
if let Some(error_source) = error_source.as_ref() {
|
||||
writeln!(text, "diagnostics: {}", error_source).unwrap();
|
||||
} else {
|
||||
writeln!(text, "diagnostics").unwrap();
|
||||
}
|
||||
let mut sections: Vec<(Range<usize>, PlaceholderType)> = Vec::new();
|
||||
|
||||
let mut project_summary = DiagnosticSummary::default();
|
||||
for (project_path, _, summary) in diagnostic_summaries {
|
||||
if let Some(path_matcher) = &options.path_matcher {
|
||||
if !path_matcher.is_match(&project_path.path) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
project_summary.error_count += summary.error_count;
|
||||
if options.include_warnings {
|
||||
project_summary.warning_count += summary.warning_count;
|
||||
} else if summary.error_count == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let last_end = text.len();
|
||||
let file_path = project_path.path.to_string_lossy().to_string();
|
||||
writeln!(&mut text, "{file_path}").unwrap();
|
||||
|
||||
if let Some(buffer) = project_handle
|
||||
.update(&mut cx, |project, cx| project.open_buffer(project_path, cx))?
|
||||
.await
|
||||
.log_err()
|
||||
{
|
||||
collect_buffer_diagnostics(
|
||||
&mut text,
|
||||
&mut sections,
|
||||
cx.read_model(&buffer, |buffer, _| buffer.snapshot())?,
|
||||
options.include_warnings,
|
||||
);
|
||||
}
|
||||
|
||||
sections.push((
|
||||
last_end..text.len().saturating_sub(1),
|
||||
PlaceholderType::File(file_path),
|
||||
))
|
||||
}
|
||||
sections.push((
|
||||
0..text.len(),
|
||||
PlaceholderType::Root(project_summary, error_source),
|
||||
));
|
||||
|
||||
Ok((text, sections))
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_buffer_diagnostics(
|
||||
text: &mut String,
|
||||
sections: &mut Vec<(Range<usize>, PlaceholderType)>,
|
||||
snapshot: BufferSnapshot,
|
||||
include_warnings: bool,
|
||||
) {
|
||||
for (_, group) in snapshot.diagnostic_groups(None) {
|
||||
let entry = &group.entries[group.primary_ix];
|
||||
collect_diagnostic(text, sections, entry, &snapshot, include_warnings)
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_diagnostic(
|
||||
text: &mut String,
|
||||
sections: &mut Vec<(Range<usize>, PlaceholderType)>,
|
||||
entry: &DiagnosticEntry<Anchor>,
|
||||
snapshot: &BufferSnapshot,
|
||||
include_warnings: bool,
|
||||
) {
|
||||
const EXCERPT_EXPANSION_SIZE: u32 = 2;
|
||||
const MAX_MESSAGE_LENGTH: usize = 2000;
|
||||
|
||||
let ty = match entry.diagnostic.severity {
|
||||
DiagnosticSeverity::WARNING => {
|
||||
if !include_warnings {
|
||||
return;
|
||||
}
|
||||
DiagnosticType::Warning
|
||||
}
|
||||
DiagnosticSeverity::ERROR => DiagnosticType::Error,
|
||||
_ => return,
|
||||
};
|
||||
let prev_len = text.len();
|
||||
|
||||
let range = entry.range.to_point(snapshot);
|
||||
let diagnostic_row_number = range.start.row + 1;
|
||||
|
||||
let start_row = range.start.row.saturating_sub(EXCERPT_EXPANSION_SIZE);
|
||||
let end_row = (range.end.row + EXCERPT_EXPANSION_SIZE).min(snapshot.max_point().row) + 1;
|
||||
let excerpt_range =
|
||||
Point::new(start_row, 0).to_offset(&snapshot)..Point::new(end_row, 0).to_offset(&snapshot);
|
||||
|
||||
text.push_str("```");
|
||||
if let Some(language_name) = snapshot.language().map(|l| l.code_fence_block_name()) {
|
||||
text.push_str(&language_name);
|
||||
}
|
||||
text.push('\n');
|
||||
|
||||
let mut buffer_text = String::new();
|
||||
for chunk in snapshot.text_for_range(excerpt_range) {
|
||||
buffer_text.push_str(chunk);
|
||||
}
|
||||
|
||||
for (i, line) in buffer_text.lines().enumerate() {
|
||||
let line_number = start_row + i as u32 + 1;
|
||||
writeln!(text, "{}", line).unwrap();
|
||||
|
||||
if line_number == diagnostic_row_number {
|
||||
text.push_str("//");
|
||||
let prev_len = text.len();
|
||||
write!(text, " {}: ", ty.as_str()).unwrap();
|
||||
let padding = text.len() - prev_len;
|
||||
|
||||
let message = util::truncate(&entry.diagnostic.message, MAX_MESSAGE_LENGTH)
|
||||
.replace('\n', format!("\n//{:padding$}", "").as_str());
|
||||
|
||||
writeln!(text, "{message}").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(text, "```").unwrap();
|
||||
sections.push((
|
||||
prev_len..text.len().saturating_sub(1),
|
||||
PlaceholderType::Diagnostic(ty, entry.diagnostic.message.clone()),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum PlaceholderType {
|
||||
Root(DiagnosticSummary, Option<String>),
|
||||
File(String),
|
||||
Diagnostic(DiagnosticType, String),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum DiagnosticType {
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl DiagnosticType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
DiagnosticType::Warning => "warning",
|
||||
DiagnosticType::Error => "error",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -5,12 +7,19 @@ use anyhow::{anyhow, bail, Context, Result};
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use html_to_markdown::convert_html_to_markdown;
|
||||
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
|
||||
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use language::LspAdapterDelegate;
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
enum ContentType {
|
||||
Html,
|
||||
Plaintext,
|
||||
Json,
|
||||
}
|
||||
|
||||
pub(crate) struct FetchSlashCommand;
|
||||
|
||||
impl FetchSlashCommand {
|
||||
@@ -37,7 +46,53 @@ impl FetchSlashCommand {
|
||||
);
|
||||
}
|
||||
|
||||
convert_html_to_markdown(&body[..])
|
||||
let Some(content_type) = response.headers().get("content-type") else {
|
||||
bail!("missing Content-Type header");
|
||||
};
|
||||
let content_type = content_type
|
||||
.to_str()
|
||||
.context("invalid Content-Type header")?;
|
||||
let content_type = match content_type {
|
||||
"text/html" => ContentType::Html,
|
||||
"text/plain" => ContentType::Plaintext,
|
||||
"application/json" => ContentType::Json,
|
||||
_ => ContentType::Html,
|
||||
};
|
||||
|
||||
match content_type {
|
||||
ContentType::Html => {
|
||||
let mut handlers: Vec<TagHandler> = vec![
|
||||
Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
|
||||
Rc::new(RefCell::new(markdown::ParagraphHandler)),
|
||||
Rc::new(RefCell::new(markdown::HeadingHandler)),
|
||||
Rc::new(RefCell::new(markdown::ListHandler)),
|
||||
Rc::new(RefCell::new(markdown::TableHandler::new())),
|
||||
Rc::new(RefCell::new(markdown::StyledTextHandler)),
|
||||
];
|
||||
if url.contains("wikipedia.org") {
|
||||
use html_to_markdown::structure::wikipedia;
|
||||
|
||||
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
|
||||
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
|
||||
handlers.push(Rc::new(
|
||||
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
|
||||
));
|
||||
} else {
|
||||
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
|
||||
}
|
||||
|
||||
convert_html_to_markdown(&body[..], &mut handlers)
|
||||
}
|
||||
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
|
||||
ContentType::Json => {
|
||||
let json: serde_json::Value = serde_json::from_slice(&body)?;
|
||||
|
||||
Ok(format!(
|
||||
"```json\n{}\n```",
|
||||
serde_json::to_string_pretty(&json)?
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +114,7 @@ impl SlashCommand for FetchSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
@@ -98,37 +153,11 @@ impl SlashCommand for FetchSlashCommand {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
FetchPlaceholder {
|
||||
id,
|
||||
unfold,
|
||||
url: url.clone(),
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
icon: IconName::AtSign,
|
||||
label: format!("fetch {}", url).into(),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct FetchPlaceholder {
|
||||
pub id: ElementId,
|
||||
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
|
||||
pub url: SharedString,
|
||||
}
|
||||
|
||||
impl RenderOnce for FetchPlaceholder {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let unfold = self.unfold;
|
||||
|
||||
ButtonLike::new(self.id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::AtSign))
|
||||
.child(Label::new(format!("fetch {url}", url = self.url)))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
use super::{SlashCommand, SlashCommandOutput};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use fs::Fs;
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{AppContext, RenderOnce, SharedString, Task, View, WeakView};
|
||||
use gpui::{AppContext, Model, Task, View, WeakView};
|
||||
use language::{LineEnding, LspAdapterDelegate};
|
||||
use project::PathMatchCandidateSet;
|
||||
use project::{PathMatchCandidateSet, Worktree};
|
||||
use std::{
|
||||
fmt::Write,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use ui::prelude::*;
|
||||
use util::{paths::PathMatcher, ResultExt};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct FileSlashCommand;
|
||||
@@ -58,7 +61,7 @@ impl FileSlashCommand {
|
||||
.root_entry()
|
||||
.map_or(false, |entry| entry.is_ignored),
|
||||
include_root_name: true,
|
||||
directories_only: false,
|
||||
candidates: project::Candidates::Entries,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -98,7 +101,7 @@ impl SlashCommand for FileSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
@@ -139,88 +142,254 @@ impl SlashCommand for FileSlashCommand {
|
||||
return Task::ready(Err(anyhow!("missing path")));
|
||||
};
|
||||
|
||||
let path = PathBuf::from(argument);
|
||||
let abs_path = workspace
|
||||
.read(cx)
|
||||
.visible_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()?;
|
||||
worktree.absolutize(&relative_path).ok()
|
||||
});
|
||||
|
||||
let Some(abs_path) = abs_path else {
|
||||
return Task::ready(Err(anyhow!("missing path")));
|
||||
};
|
||||
|
||||
let fs = workspace.read(cx).app_state().fs.clone();
|
||||
let argument = argument.to_string();
|
||||
let text = cx.background_executor().spawn(async move {
|
||||
let mut content = fs.load(&abs_path).await?;
|
||||
LineEnding::normalize(&mut content);
|
||||
let mut output = String::with_capacity(argument.len() + content.len() + 9);
|
||||
output.push_str("```");
|
||||
output.push_str(&argument);
|
||||
output.push('\n');
|
||||
output.push_str(&content);
|
||||
if !output.ends_with('\n') {
|
||||
output.push('\n');
|
||||
}
|
||||
output.push_str("```");
|
||||
anyhow::Ok(output)
|
||||
});
|
||||
let task = collect_files(
|
||||
workspace.read(cx).visible_worktrees(cx).collect(),
|
||||
argument,
|
||||
fs,
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let text = text.await?;
|
||||
let range = 0..text.len();
|
||||
let (text, ranges) = task.await?;
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
FilePlaceholder {
|
||||
path: Some(path.clone()),
|
||||
line_range: None,
|
||||
id,
|
||||
unfold,
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
sections: ranges
|
||||
.into_iter()
|
||||
.map(|(range, path, entry_type)| {
|
||||
build_entry_output_section(
|
||||
range,
|
||||
Some(&path),
|
||||
entry_type == EntryType::Directory,
|
||||
None,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct FilePlaceholder {
|
||||
pub path: Option<PathBuf>,
|
||||
pub line_range: Option<Range<u32>>,
|
||||
pub id: ElementId,
|
||||
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum EntryType {
|
||||
File,
|
||||
Directory,
|
||||
}
|
||||
|
||||
impl RenderOnce for FilePlaceholder {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let unfold = self.unfold;
|
||||
let title = if let Some(path) = self.path.as_ref() {
|
||||
SharedString::from(path.to_string_lossy().to_string())
|
||||
} else {
|
||||
SharedString::from("untitled")
|
||||
};
|
||||
fn collect_files(
|
||||
worktrees: Vec<Model<Worktree>>,
|
||||
glob_input: &str,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<(String, Vec<(Range<usize>, PathBuf, EntryType)>)>> {
|
||||
let Ok(matcher) = PathMatcher::new(&[glob_input.to_owned()]) else {
|
||||
return Task::ready(Err(anyhow!("invalid path")));
|
||||
};
|
||||
|
||||
ButtonLike::new(self.id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::File))
|
||||
.child(Label::new(title))
|
||||
.when_some(self.line_range, |button, line_range| {
|
||||
button.child(Label::new(":")).child(Label::new(format!(
|
||||
"{}-{}",
|
||||
line_range.start, line_range.end
|
||||
)))
|
||||
})
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
let path = PathBuf::try_from(glob_input).ok();
|
||||
let file_path = if let Some(path) = &path {
|
||||
worktrees.iter().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()?;
|
||||
worktree.absolutize(&relative_path).ok()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(abs_path) = file_path {
|
||||
if abs_path.is_file() {
|
||||
let filename = path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
return cx.background_executor().spawn(async move {
|
||||
let mut text = String::new();
|
||||
collect_file_content(&mut text, fs, filename.clone(), abs_path.clone().into())
|
||||
.await?;
|
||||
let text_range = 0..text.len();
|
||||
Ok((
|
||||
text,
|
||||
vec![(text_range, path.unwrap_or_default(), EntryType::File)],
|
||||
))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let snapshots = worktrees
|
||||
.iter()
|
||||
.map(|worktree| worktree.read(cx).snapshot())
|
||||
.collect::<Vec<_>>();
|
||||
cx.background_executor().spawn(async move {
|
||||
let mut text = String::new();
|
||||
let mut ranges = Vec::new();
|
||||
for snapshot in snapshots {
|
||||
let mut directory_stack: Vec<(Arc<Path>, String, usize)> = Vec::new();
|
||||
let mut folded_directory_names_stack = Vec::new();
|
||||
let mut is_top_level_directory = true;
|
||||
for entry in snapshot.entries(false, 0) {
|
||||
let mut path_buf = PathBuf::new();
|
||||
path_buf.push(snapshot.root_name());
|
||||
path_buf.push(&entry.path);
|
||||
if !matcher.is_match(&path_buf) {
|
||||
continue;
|
||||
}
|
||||
|
||||
while let Some((dir, _, _)) = directory_stack.last() {
|
||||
if entry.path.starts_with(dir) {
|
||||
break;
|
||||
}
|
||||
let (_, entry_name, start) = directory_stack.pop().unwrap();
|
||||
ranges.push((
|
||||
start..text.len().saturating_sub(1),
|
||||
PathBuf::from(entry_name),
|
||||
EntryType::Directory,
|
||||
));
|
||||
}
|
||||
|
||||
let filename = entry
|
||||
.path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
|
||||
if entry.is_dir() {
|
||||
// Auto-fold directories that contain no files
|
||||
let mut child_entries = snapshot.child_entries(&entry.path);
|
||||
if let Some(child) = child_entries.next() {
|
||||
if child_entries.next().is_none() && child.kind.is_dir() {
|
||||
if is_top_level_directory {
|
||||
is_top_level_directory = false;
|
||||
folded_directory_names_stack
|
||||
.push(path_buf.to_string_lossy().to_string());
|
||||
} else {
|
||||
folded_directory_names_stack.push(filename.to_string());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Skip empty directories
|
||||
folded_directory_names_stack.clear();
|
||||
continue;
|
||||
}
|
||||
let prefix_paths = folded_directory_names_stack.drain(..).as_slice().join("/");
|
||||
let entry_start = text.len();
|
||||
if prefix_paths.is_empty() {
|
||||
if is_top_level_directory {
|
||||
text.push_str(&path_buf.to_string_lossy());
|
||||
is_top_level_directory = false;
|
||||
} else {
|
||||
text.push_str(&filename);
|
||||
}
|
||||
directory_stack.push((entry.path.clone(), filename, entry_start));
|
||||
} else {
|
||||
let entry_name = format!("{}/{}", prefix_paths, &filename);
|
||||
text.push_str(&entry_name);
|
||||
directory_stack.push((entry.path.clone(), entry_name, entry_start));
|
||||
}
|
||||
text.push('\n');
|
||||
} else if entry.is_file() {
|
||||
if let Some(abs_path) = snapshot.absolutize(&entry.path).log_err() {
|
||||
let prev_len = text.len();
|
||||
collect_file_content(
|
||||
&mut text,
|
||||
fs.clone(),
|
||||
filename.clone(),
|
||||
abs_path.into(),
|
||||
)
|
||||
.await?;
|
||||
ranges.push((
|
||||
prev_len..text.len(),
|
||||
PathBuf::from(filename),
|
||||
EntryType::File,
|
||||
));
|
||||
text.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some((dir, _, start)) = directory_stack.pop() {
|
||||
let mut root_path = PathBuf::new();
|
||||
root_path.push(snapshot.root_name());
|
||||
root_path.push(&dir);
|
||||
ranges.push((start..text.len(), root_path, EntryType::Directory));
|
||||
}
|
||||
}
|
||||
Ok((text, ranges))
|
||||
})
|
||||
}
|
||||
|
||||
async fn collect_file_content(
|
||||
buffer: &mut String,
|
||||
fs: Arc<dyn Fs>,
|
||||
filename: String,
|
||||
abs_path: Arc<Path>,
|
||||
) -> Result<()> {
|
||||
let mut content = fs.load(&abs_path).await?;
|
||||
LineEnding::normalize(&mut content);
|
||||
buffer.reserve(filename.len() + content.len() + 9);
|
||||
buffer.push_str(&codeblock_fence_for_path(
|
||||
Some(&PathBuf::from(filename)),
|
||||
None,
|
||||
));
|
||||
buffer.push_str(&content);
|
||||
if !buffer.ends_with('\n') {
|
||||
buffer.push('\n');
|
||||
}
|
||||
buffer.push_str("```");
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
pub fn codeblock_fence_for_path(path: Option<&Path>, row_range: Option<Range<u32>>) -> String {
|
||||
let mut text = String::new();
|
||||
write!(text, "```").unwrap();
|
||||
|
||||
if let Some(path) = path {
|
||||
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
|
||||
write!(text, "{} ", extension).unwrap();
|
||||
}
|
||||
|
||||
write!(text, "{}", path.display()).unwrap();
|
||||
} else {
|
||||
write!(text, "untitled").unwrap();
|
||||
}
|
||||
|
||||
if let Some(row_range) = row_range {
|
||||
write!(text, ":{}-{}", row_range.start + 1, row_range.end + 1).unwrap();
|
||||
}
|
||||
|
||||
text.push('\n');
|
||||
text
|
||||
}
|
||||
|
||||
pub fn build_entry_output_section(
|
||||
range: Range<usize>,
|
||||
path: Option<&Path>,
|
||||
is_directory: bool,
|
||||
line_range: Option<Range<u32>>,
|
||||
) -> SlashCommandOutputSection<usize> {
|
||||
let mut label = if let Some(path) = path {
|
||||
path.to_string_lossy().to_string()
|
||||
} else {
|
||||
"untitled".to_string()
|
||||
};
|
||||
if let Some(line_range) = line_range {
|
||||
write!(label, ":{}-{}", line_range.start, line_range.end).unwrap();
|
||||
}
|
||||
|
||||
let icon = if is_directory {
|
||||
IconName::Folder
|
||||
} else {
|
||||
IconName::File
|
||||
};
|
||||
|
||||
SlashCommandOutputSection {
|
||||
range,
|
||||
icon,
|
||||
label: label.into(),
|
||||
}
|
||||
}
|
||||
|
||||
82
crates/assistant/src/slash_command/now_command.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
||||
use chrono::{DateTime, Local};
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct NowSlashCommand;
|
||||
|
||||
impl SlashCommand for NowSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"now".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert the current date and time".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert current date and time".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let now = Local::now();
|
||||
let text = format!("Today is {now}.", now = now.to_rfc3339());
|
||||
let range = 0..text.len();
|
||||
|
||||
Task::ready(Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
icon: IconName::CountdownTimer,
|
||||
label: now.to_rfc3339().into(),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct NowPlaceholder {
|
||||
pub id: ElementId,
|
||||
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
|
||||
pub now: DateTime<Local>,
|
||||
}
|
||||
|
||||
impl RenderOnce for NowPlaceholder {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let unfold = self.unfold;
|
||||
|
||||
ButtonLike::new(self.id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::CountdownTimer))
|
||||
.child(Label::new(self.now.to_rfc3339()))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ use std::{
|
||||
path::Path,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct ProjectSlashCommand;
|
||||
@@ -102,7 +102,7 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
@@ -138,15 +138,8 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
ButtonLike::new(id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::FileTree))
|
||||
.child(Label::new("Project"))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
.into_any_element()
|
||||
}),
|
||||
icon: IconName::FileTree,
|
||||
label: "Project".into(),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ use assistant_slash_command::SlashCommandOutputSection;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct PromptSlashCommand;
|
||||
@@ -28,7 +28,7 @@ impl SlashCommand for PromptSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
query: String,
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
@@ -69,42 +69,20 @@ impl SlashCommand for PromptSlashCommand {
|
||||
}
|
||||
});
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let prompt = prompt.await?;
|
||||
let mut prompt = prompt.await?;
|
||||
if prompt.is_empty() {
|
||||
prompt.push('\n');
|
||||
}
|
||||
let range = 0..prompt.len();
|
||||
Ok(SlashCommandOutput {
|
||||
text: prompt,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
PromptPlaceholder {
|
||||
id,
|
||||
unfold,
|
||||
title: title.clone(),
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
icon: IconName::Library,
|
||||
label: title,
|
||||
}],
|
||||
run_commands_in_text: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct PromptPlaceholder {
|
||||
pub title: SharedString,
|
||||
pub id: ElementId,
|
||||
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
|
||||
}
|
||||
|
||||
impl RenderOnce for PromptPlaceholder {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let unfold = self.unfold;
|
||||
ButtonLike::new(self.id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::Library))
|
||||
.child(Label::new(self.title))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,45 +7,37 @@ use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutp
|
||||
use fs::Fs;
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{AppContext, Model, Task, WeakView};
|
||||
use html_to_markdown::convert_rustdoc_to_markdown;
|
||||
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use language::LspAdapterDelegate;
|
||||
use project::{Project, ProjectPath};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use rustdoc::{convert_rustdoc_to_markdown, CrateName, LocalProvider, RustdocSource, RustdocStore};
|
||||
use ui::prelude::*;
|
||||
use util::{maybe, ResultExt};
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum RustdocSource {
|
||||
/// The docs were sourced from local `cargo doc` output.
|
||||
Local,
|
||||
/// The docs were sourced from `docs.rs`.
|
||||
DocsDotRs,
|
||||
}
|
||||
|
||||
pub(crate) struct RustdocSlashCommand;
|
||||
|
||||
impl RustdocSlashCommand {
|
||||
async fn build_message(
|
||||
fs: Arc<dyn Fs>,
|
||||
http_client: Arc<HttpClientWithUrl>,
|
||||
crate_name: String,
|
||||
crate_name: CrateName,
|
||||
module_path: Vec<String>,
|
||||
path_to_cargo_toml: Option<&Path>,
|
||||
) -> Result<(RustdocSource, String)> {
|
||||
let cargo_workspace_root = path_to_cargo_toml.and_then(|path| path.parent());
|
||||
if let Some(cargo_workspace_root) = cargo_workspace_root {
|
||||
let mut local_cargo_doc_path = cargo_workspace_root.join("target/doc");
|
||||
local_cargo_doc_path.push(&crate_name);
|
||||
local_cargo_doc_path.push(crate_name.as_ref());
|
||||
if !module_path.is_empty() {
|
||||
local_cargo_doc_path.push(module_path.join("/"));
|
||||
}
|
||||
local_cargo_doc_path.push("index.html");
|
||||
|
||||
if let Ok(contents) = fs.load(&local_cargo_doc_path).await {
|
||||
return Ok((
|
||||
RustdocSource::Local,
|
||||
convert_rustdoc_to_markdown(contents.as_bytes())?,
|
||||
));
|
||||
let (markdown, _items) = convert_rustdoc_to_markdown(contents.as_bytes())?;
|
||||
|
||||
return Ok((RustdocSource::Local, markdown));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,10 +70,9 @@ impl RustdocSlashCommand {
|
||||
);
|
||||
}
|
||||
|
||||
Ok((
|
||||
RustdocSource::DocsDotRs,
|
||||
convert_rustdoc_to_markdown(&body[..])?,
|
||||
))
|
||||
let (markdown, _items) = convert_rustdoc_to_markdown(&body[..])?;
|
||||
|
||||
Ok((RustdocSource::DocsDotRs, markdown))
|
||||
}
|
||||
|
||||
fn path_to_cargo_toml(project: Model<Project>, cx: &mut AppContext) -> Option<Arc<Path>> {
|
||||
@@ -116,13 +107,42 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
_query: String,
|
||||
self: Arc<Self>,
|
||||
query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
let index_provider_deps = maybe!({
|
||||
let workspace = workspace.ok_or_else(|| anyhow!("no workspace"))?;
|
||||
let workspace = workspace
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("workspace was dropped"))?;
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let fs = project.read(cx).fs().clone();
|
||||
let cargo_workspace_root = Self::path_to_cargo_toml(project, cx)
|
||||
.and_then(|path| path.parent().map(|path| path.to_path_buf()))
|
||||
.ok_or_else(|| anyhow!("no Cargo workspace root found"))?;
|
||||
|
||||
anyhow::Ok((fs, cargo_workspace_root))
|
||||
});
|
||||
|
||||
let store = RustdocStore::global(cx);
|
||||
cx.background_executor().spawn(async move {
|
||||
if let Some((crate_name, rest)) = query.split_once(':') {
|
||||
if rest.is_empty() {
|
||||
if let Some((fs, cargo_workspace_root)) = index_provider_deps.log_err() {
|
||||
let provider = Box::new(LocalProvider::new(fs, cargo_workspace_root));
|
||||
// We don't need to hold onto this task, as the `RustdocStore` will hold it
|
||||
// until it completes.
|
||||
let _ = store.clone().index(crate_name.into(), provider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let items = store.search(query).await;
|
||||
Ok(items)
|
||||
})
|
||||
}
|
||||
|
||||
fn run(
|
||||
@@ -142,91 +162,77 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let fs = project.read(cx).fs().clone();
|
||||
let http_client = workspace.read(cx).client().http_client();
|
||||
let path_to_cargo_toml = Self::path_to_cargo_toml(project, cx);
|
||||
|
||||
let mut path_components = argument.split("::");
|
||||
let crate_name = match path_components
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("missing crate name"))
|
||||
{
|
||||
Ok(crate_name) => crate_name.to_string(),
|
||||
Ok(crate_name) => CrateName::from(crate_name),
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
};
|
||||
let module_path = path_components.map(ToString::to_string).collect::<Vec<_>>();
|
||||
let path_to_cargo_toml = Self::path_to_cargo_toml(project, cx);
|
||||
let item_path = path_components.map(ToString::to_string).collect::<Vec<_>>();
|
||||
|
||||
let text = cx.background_executor().spawn({
|
||||
let rustdoc_store = RustdocStore::global(cx);
|
||||
let crate_name = crate_name.clone();
|
||||
let module_path = module_path.clone();
|
||||
let item_path = item_path.clone();
|
||||
async move {
|
||||
Self::build_message(
|
||||
fs,
|
||||
http_client,
|
||||
crate_name,
|
||||
module_path,
|
||||
path_to_cargo_toml.as_deref(),
|
||||
)
|
||||
.await
|
||||
let item_docs = rustdoc_store
|
||||
.load(
|
||||
crate_name.clone(),
|
||||
if item_path.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(item_path.join("::"))
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Ok(item_docs) = item_docs {
|
||||
anyhow::Ok((RustdocSource::Index, item_docs.docs().to_owned()))
|
||||
} else {
|
||||
Self::build_message(
|
||||
fs,
|
||||
http_client,
|
||||
crate_name,
|
||||
item_path,
|
||||
path_to_cargo_toml.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let crate_name = SharedString::from(crate_name);
|
||||
let module_path = if module_path.is_empty() {
|
||||
let module_path = if item_path.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(SharedString::from(module_path.join("::")))
|
||||
Some(SharedString::from(item_path.join("::")))
|
||||
};
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let (source, text) = text.await?;
|
||||
let range = 0..text.len();
|
||||
let crate_path = module_path
|
||||
.map(|module_path| format!("{}::{}", crate_name, module_path))
|
||||
.unwrap_or_else(|| crate_name.to_string());
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
RustdocPlaceholder {
|
||||
id,
|
||||
unfold,
|
||||
source,
|
||||
crate_name: crate_name.clone(),
|
||||
module_path: module_path.clone(),
|
||||
icon: IconName::FileRust,
|
||||
label: format!(
|
||||
"rustdoc ({source}): {crate_path}",
|
||||
source = match source {
|
||||
RustdocSource::Index => "index",
|
||||
RustdocSource::Local => "local",
|
||||
RustdocSource::DocsDotRs => "docs.rs",
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
)
|
||||
.into(),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct RustdocPlaceholder {
|
||||
pub id: ElementId,
|
||||
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
|
||||
pub source: RustdocSource,
|
||||
pub crate_name: SharedString,
|
||||
pub module_path: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl RenderOnce for RustdocPlaceholder {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let unfold = self.unfold;
|
||||
|
||||
let crate_path = self
|
||||
.module_path
|
||||
.map(|module_path| format!("{crate_name}::{module_path}", crate_name = self.crate_name))
|
||||
.unwrap_or(self.crate_name.to_string());
|
||||
|
||||
ButtonLike::new(self.id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::FileRust))
|
||||
.child(Label::new(format!(
|
||||
"rustdoc ({source}): {crate_path}",
|
||||
source = match self.source {
|
||||
RustdocSource::Local => "local",
|
||||
RustdocSource::DocsDotRs => "docs.rs",
|
||||
}
|
||||
)))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput};
|
||||
use super::{
|
||||
file_command::{build_entry_output_section, codeblock_fence_for_path},
|
||||
SlashCommand, SlashCommandOutput,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
@@ -9,7 +12,7 @@ use std::{
|
||||
path::PathBuf,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex, Icon, IconName};
|
||||
use ui::{prelude::*, IconName};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -44,7 +47,7 @@ impl SlashCommand for SearchSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
@@ -125,9 +128,8 @@ impl SlashCommand for SearchSlashCommand {
|
||||
let range_start = result.range.start.min(file_content.len());
|
||||
let range_end = result.range.end.min(file_content.len());
|
||||
|
||||
let start_line =
|
||||
file_content[0..range_start].matches('\n').count() as u32 + 1;
|
||||
let end_line = file_content[0..range_end].matches('\n').count() as u32 + 1;
|
||||
let start_row = file_content[0..range_start].matches('\n').count() as u32;
|
||||
let end_row = file_content[0..range_end].matches('\n').count() as u32;
|
||||
let start_line_byte_offset = file_content[0..range_start]
|
||||
.rfind('\n')
|
||||
.map(|pos| pos + 1)
|
||||
@@ -138,47 +140,30 @@ impl SlashCommand for SearchSlashCommand {
|
||||
.unwrap_or_else(|| file_content.len());
|
||||
|
||||
let section_start_ix = text.len();
|
||||
writeln!(
|
||||
text,
|
||||
"```{}:{}-{}",
|
||||
result.path.display(),
|
||||
start_line,
|
||||
end_line,
|
||||
)
|
||||
.unwrap();
|
||||
text.push_str(&codeblock_fence_for_path(
|
||||
Some(&result.path),
|
||||
Some(start_row..end_row),
|
||||
));
|
||||
|
||||
let mut excerpt =
|
||||
file_content[start_line_byte_offset..end_line_byte_offset].to_string();
|
||||
LineEnding::normalize(&mut excerpt);
|
||||
text.push_str(&excerpt);
|
||||
writeln!(text, "\n```\n").unwrap();
|
||||
let section_end_ix = text.len() - 1;
|
||||
|
||||
sections.push(SlashCommandOutputSection {
|
||||
range: section_start_ix..section_end_ix,
|
||||
render_placeholder: Arc::new(move |id, unfold, _| {
|
||||
FilePlaceholder {
|
||||
id,
|
||||
path: Some(full_path.clone()),
|
||||
line_range: Some(start_line..end_line),
|
||||
unfold,
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
});
|
||||
sections.push(build_entry_output_section(
|
||||
section_start_ix..section_end_ix,
|
||||
Some(&full_path),
|
||||
false,
|
||||
Some(start_row + 1..end_row + 1),
|
||||
));
|
||||
}
|
||||
|
||||
let query = SharedString::from(query);
|
||||
sections.push(SlashCommandOutputSection {
|
||||
range: 0..text.len(),
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
ButtonLike::new(id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::MagnifyingGlass))
|
||||
.child(Label::new(query.clone()))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
.into_any_element()
|
||||
}),
|
||||
icon: IconName::MagnifyingGlass,
|
||||
label: query,
|
||||
});
|
||||
|
||||
SlashCommandOutput {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput};
|
||||
use super::{
|
||||
file_command::{build_entry_output_section, codeblock_fence_for_path},
|
||||
SlashCommand, SlashCommandOutput,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use gpui::{AppContext, Entity, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use std::{fmt::Write, path::Path, sync::Arc};
|
||||
use ui::{IntoElement, WindowContext};
|
||||
use std::{fmt::Write, sync::Arc};
|
||||
use ui::WindowContext;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct TabsSlashCommand;
|
||||
@@ -29,7 +31,7 @@ impl SlashCommand for TabsSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancel: Arc<std::sync::atomic::AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
@@ -77,15 +79,7 @@ impl SlashCommand for TabsSlashCommand {
|
||||
let mut text = String::new();
|
||||
for (full_path, buffer, _) in open_buffers {
|
||||
let section_start_ix = text.len();
|
||||
writeln!(
|
||||
text,
|
||||
"```{}\n",
|
||||
full_path
|
||||
.as_deref()
|
||||
.unwrap_or(Path::new("untitled"))
|
||||
.display()
|
||||
)
|
||||
.unwrap();
|
||||
text.push_str(&codeblock_fence_for_path(full_path.as_deref(), None));
|
||||
for chunk in buffer.as_rope().chunks() {
|
||||
text.push_str(chunk);
|
||||
}
|
||||
@@ -94,19 +88,12 @@ impl SlashCommand for TabsSlashCommand {
|
||||
}
|
||||
writeln!(text, "```\n").unwrap();
|
||||
let section_end_ix = text.len() - 1;
|
||||
|
||||
sections.push(SlashCommandOutputSection {
|
||||
range: section_start_ix..section_end_ix,
|
||||
render_placeholder: Arc::new(move |id, unfold, _| {
|
||||
FilePlaceholder {
|
||||
id,
|
||||
path: full_path.clone(),
|
||||
line_range: None,
|
||||
unfold,
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
});
|
||||
sections.push(build_entry_output_section(
|
||||
section_start_ix..section_end_ix,
|
||||
full_path.as_deref(),
|
||||
false,
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(SlashCommandOutput {
|
||||
|
||||
@@ -18,4 +18,5 @@ derive_more.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
parking_lot.workspace = true
|
||||
serde.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
mod slash_command_registry;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::{AnyElement, AppContext, ElementId, Task, WeakView, WindowContext};
|
||||
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext};
|
||||
use language::{CodeLabel, LspAdapterDelegate};
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use slash_command_registry::*;
|
||||
use std::{
|
||||
ops::Range,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use workspace::Workspace;
|
||||
use workspace::{ui::IconName, Workspace};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
SlashCommandRegistry::default_global(cx);
|
||||
@@ -22,7 +23,7 @@ pub trait SlashCommand: 'static + Send + Sync {
|
||||
fn description(&self) -> String;
|
||||
fn menu_text(&self) -> String;
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
query: String,
|
||||
cancel: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
@@ -55,8 +56,9 @@ pub struct SlashCommandOutput {
|
||||
pub run_commands_in_text: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct SlashCommandOutputSection<T> {
|
||||
pub range: Range<T>,
|
||||
pub render_placeholder: RenderFoldPlaceholder,
|
||||
pub icon: IconName,
|
||||
pub label: SharedString,
|
||||
}
|
||||
|
||||
@@ -86,10 +86,16 @@ impl Render for Breadcrumbs {
|
||||
.style(ButtonStyle::Subtle)
|
||||
.on_click(move |_, cx| {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
outline::toggle(editor, &outline::Toggle, cx)
|
||||
outline::toggle(editor, &editor::actions::ToggleOutline, cx)
|
||||
}
|
||||
})
|
||||
.tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)),
|
||||
.tooltip(|cx| {
|
||||
Tooltip::for_action(
|
||||
"Show symbol outline",
|
||||
&editor::actions::ToggleOutline,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
),
|
||||
None => element
|
||||
// Match the height of the `ButtonLike` in the other arm.
|
||||
|
||||
@@ -13,6 +13,7 @@ path = "src/call.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
no-webrtc = ["live_kit_client/no-webrtc"]
|
||||
test-support = [
|
||||
"client/test-support",
|
||||
"collections/test-support",
|
||||
|
||||
@@ -267,7 +267,7 @@ impl Room {
|
||||
.await
|
||||
{
|
||||
Ok(()) => Ok(room),
|
||||
Err(error) => Err(anyhow!("room creation failed: {:?}", error)),
|
||||
Err(error) => Err(error.context("room creation failed")),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::channel_chat::ChannelChatEvent;
|
||||
use super::*;
|
||||
use client::{test::FakeServer, Client, UserStore};
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::{AppContext, Context, Model, TestAppContext};
|
||||
use gpui::{AppContext, Context, Model, SemanticVersion, TestAppContext};
|
||||
use http::FakeHttpClient;
|
||||
use rpc::proto::{self};
|
||||
use settings::SettingsStore;
|
||||
@@ -340,7 +340,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
release_channel::init("0.0.0", cx);
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
client::init_settings(cx);
|
||||
|
||||
let clock = Arc::new(FakeSystemClock::default());
|
||||
|
||||
@@ -19,9 +19,9 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
libc.workspace = true
|
||||
ipc-channel = "0.18"
|
||||
once_cell.workspace = true
|
||||
paths.workspace = true
|
||||
release_channel.workspace = true
|
||||
serde.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -161,10 +161,7 @@ mod linux {
|
||||
env,
|
||||
ffi::OsString,
|
||||
io,
|
||||
os::{
|
||||
linux::net::SocketAddrExt,
|
||||
unix::net::{SocketAddr, UnixDatagram},
|
||||
},
|
||||
os::unix::net::{SocketAddr, UnixDatagram},
|
||||
path::{Path, PathBuf},
|
||||
process::{self, ExitStatus},
|
||||
thread,
|
||||
@@ -223,12 +220,9 @@ mod linux {
|
||||
}
|
||||
|
||||
fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
|
||||
let uid: u32 = unsafe { libc::getuid() };
|
||||
let sock_addr =
|
||||
SocketAddr::from_abstract_name(format!("zed-{}-{}", *RELEASE_CHANNEL, uid))?;
|
||||
|
||||
let sock_path = paths::support_dir().join(format!("zed-{}.sock", *RELEASE_CHANNEL));
|
||||
let sock = UnixDatagram::unbound()?;
|
||||
if sock.connect_addr(&sock_addr).is_err() {
|
||||
if sock.connect(&sock_path).is_err() {
|
||||
self.boot_background(ipc_url)?;
|
||||
} else {
|
||||
sock.send(ipc_url.as_bytes())?;
|
||||
|
||||
@@ -24,12 +24,14 @@ chrono = { workspace = true, features = ["serde"] }
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
once_cell.workspace = true
|
||||
paths.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
rand.workspace = true
|
||||
@@ -50,6 +52,7 @@ time.workspace = true
|
||||
tiny_http = "0.8"
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
worktree.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
@@ -60,6 +63,12 @@ settings = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
http = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cocoa.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
async-native-tls = {"version" = "0.5.0", features = ["vendored"]}
|
||||
# This is an indirect dependency of async-tungstenite that is included
|
||||
|
||||
@@ -509,7 +509,7 @@ impl Client {
|
||||
let credentials_provider: Arc<dyn CredentialsProvider + Send + Sync + 'static> =
|
||||
if use_zed_development_auth {
|
||||
Arc::new(DevelopmentCredentialsProvider {
|
||||
path: util::paths::CONFIG_DIR.join("development_auth"),
|
||||
path: paths::config_dir().join("development_auth"),
|
||||
})
|
||||
} else {
|
||||
Arc::new(KeychainCredentialsProvider)
|
||||
@@ -1429,6 +1429,31 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_dynamic(
|
||||
&self,
|
||||
envelope: proto::Envelope,
|
||||
request_type: &'static str,
|
||||
) -> impl Future<Output = Result<proto::Envelope>> {
|
||||
let client_id = self.id();
|
||||
log::debug!(
|
||||
"rpc request start. client_id:{}. name:{}",
|
||||
client_id,
|
||||
request_type
|
||||
);
|
||||
let response = self
|
||||
.connection_id()
|
||||
.map(|conn_id| self.peer.request_dynamic(conn_id, envelope, request_type));
|
||||
async move {
|
||||
let response = response?.await;
|
||||
log::debug!(
|
||||
"rpc request finish. client_id:{}. name:{}",
|
||||
client_id,
|
||||
request_type
|
||||
);
|
||||
Ok(response?.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn respond<T: RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) -> Result<()> {
|
||||
log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME);
|
||||
self.peer.respond(receipt, response)
|
||||
@@ -1704,6 +1729,7 @@ mod tests {
|
||||
use gpui::{BackgroundExecutor, Context, TestAppContext};
|
||||
use http::FakeHttpClient;
|
||||
use parking_lot::Mutex;
|
||||
use proto::TypedEnvelope;
|
||||
use settings::SettingsStore;
|
||||
use std::future;
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ mod event_coalescer;
|
||||
use crate::{ChannelId, TelemetrySettings};
|
||||
use chrono::{DateTime, Utc};
|
||||
use clock::SystemClock;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::Future;
|
||||
use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
|
||||
use gpui::{AppContext, BackgroundExecutor, Task};
|
||||
use http::{self, HttpClient, HttpClientWithUrl, Method};
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::Mutex;
|
||||
@@ -23,6 +24,7 @@ use tempfile::NamedTempFile;
|
||||
#[cfg(not(debug_assertions))]
|
||||
use util::ResultExt;
|
||||
use util::TryFutureExt;
|
||||
use worktree::{UpdatedEntriesSet, WorktreeId};
|
||||
|
||||
use self::event_coalescer::EventCoalescer;
|
||||
|
||||
@@ -39,7 +41,6 @@ struct TelemetryState {
|
||||
installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
|
||||
session_id: Option<String>, // Per app launch
|
||||
release_channel: Option<&'static str>,
|
||||
app_metadata: AppMetadata,
|
||||
architecture: &'static str,
|
||||
events_queue: Vec<EventWrapper>,
|
||||
flush_events_task: Option<Task<()>>,
|
||||
@@ -48,6 +49,29 @@ struct TelemetryState {
|
||||
first_event_date_time: Option<DateTime<Utc>>,
|
||||
event_coalescer: EventCoalescer,
|
||||
max_queue_size: usize,
|
||||
worktree_id_map: WorktreeIdMap,
|
||||
|
||||
os_name: String,
|
||||
app_version: String,
|
||||
os_version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct WorktreeIdMap(HashMap<String, ProjectCache>);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ProjectCache {
|
||||
name: String,
|
||||
worktree_ids_reported: HashSet<WorktreeId>,
|
||||
}
|
||||
|
||||
impl ProjectCache {
|
||||
fn new(name: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
worktree_ids_reported: HashSet::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -71,6 +95,87 @@ static ZED_CLIENT_CHECKSUM_SEED: Lazy<Option<Vec<u8>>> = Lazy::new(|| {
|
||||
})
|
||||
});
|
||||
|
||||
pub fn os_name() -> String {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
"macOS".to_string()
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
format!("Linux {}", gpui::guess_compositor())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
"Windows".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Note: This might do blocking IO! Only call from background threads
|
||||
pub fn os_version() -> String {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use cocoa::base::nil;
|
||||
use cocoa::foundation::NSProcessInfo;
|
||||
|
||||
unsafe {
|
||||
let process_info = cocoa::foundation::NSProcessInfo::processInfo(nil);
|
||||
let version = process_info.operatingSystemVersion();
|
||||
gpui::SemanticVersion::new(
|
||||
version.majorVersion as usize,
|
||||
version.minorVersion as usize,
|
||||
version.patchVersion as usize,
|
||||
)
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::path::Path;
|
||||
|
||||
let content = if let Ok(file) = std::fs::read_to_string(&Path::new("/etc/os-release")) {
|
||||
file
|
||||
} else if let Ok(file) = std::fs::read_to_string(&Path::new("/usr/lib/os-release")) {
|
||||
file
|
||||
} else {
|
||||
log::error!("Failed to load /etc/os-release, /usr/lib/os-release");
|
||||
"".to_string()
|
||||
};
|
||||
let mut name = "unknown".to_string();
|
||||
let mut version = "unknown".to_string();
|
||||
|
||||
for line in content.lines() {
|
||||
if line.starts_with("ID=") {
|
||||
name = line.trim_start_matches("ID=").trim_matches('"').to_string();
|
||||
}
|
||||
if line.starts_with("VERSION_ID=") {
|
||||
version = line
|
||||
.trim_start_matches("VERSION_ID=")
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
format!("{} {}", name, version)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let mut info = unsafe { std::mem::zeroed() };
|
||||
let status = unsafe { windows::Wdk::System::SystemServices::RtlGetVersion(&mut info) };
|
||||
if status.is_ok() {
|
||||
gpui::SemanticVersion::new(
|
||||
info.dwMajorVersion as _,
|
||||
info.dwMinorVersion as _,
|
||||
info.dwBuildNumber as _,
|
||||
)
|
||||
.to_string()
|
||||
} else {
|
||||
"unknown".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Telemetry {
|
||||
pub fn new(
|
||||
clock: Arc<dyn SystemClock>,
|
||||
@@ -84,7 +189,6 @@ impl Telemetry {
|
||||
|
||||
let state = Arc::new(Mutex::new(TelemetryState {
|
||||
settings: *TelemetrySettings::get_global(cx),
|
||||
app_metadata: cx.app_metadata(),
|
||||
architecture: env::consts::ARCH,
|
||||
release_channel,
|
||||
installation_id: None,
|
||||
@@ -97,6 +201,20 @@ impl Telemetry {
|
||||
first_event_date_time: None,
|
||||
event_coalescer: EventCoalescer::new(clock.clone()),
|
||||
max_queue_size: MAX_QUEUE_LEN,
|
||||
worktree_id_map: WorktreeIdMap(HashMap::from_iter([
|
||||
(
|
||||
"yarn.lock".to_string(),
|
||||
ProjectCache::new("yarn".to_string()),
|
||||
),
|
||||
(
|
||||
"package.json".to_string(),
|
||||
ProjectCache::new("node".to_string()),
|
||||
),
|
||||
])),
|
||||
|
||||
os_version: None,
|
||||
os_name: os_name(),
|
||||
app_version: release_channel::AppVersion::global(cx).to_string(),
|
||||
}));
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
@@ -105,7 +223,7 @@ impl Telemetry {
|
||||
let state = state.clone();
|
||||
async move {
|
||||
if let Some(tempfile) =
|
||||
NamedTempFile::new_in(util::paths::CONFIG_DIR.as_path()).log_err()
|
||||
NamedTempFile::new_in(paths::config_dir().as_path()).log_err()
|
||||
{
|
||||
state.lock().log_file = Some(tempfile);
|
||||
}
|
||||
@@ -168,6 +286,9 @@ impl Telemetry {
|
||||
let mut state = self.state.lock();
|
||||
state.installation_id = installation_id.map(|id| id.into());
|
||||
state.session_id = Some(session_id);
|
||||
state.app_version = release_channel::AppVersion::global(cx).to_string();
|
||||
state.os_name = os_name();
|
||||
|
||||
drop(state);
|
||||
|
||||
let this = self.clone();
|
||||
@@ -360,6 +481,52 @@ impl Telemetry {
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_discovered_project_events(
|
||||
self: &Arc<Self>,
|
||||
worktree_id: WorktreeId,
|
||||
updated_entries_set: &UpdatedEntriesSet,
|
||||
) {
|
||||
let project_names: Vec<String> = {
|
||||
let mut state = self.state.lock();
|
||||
state
|
||||
.worktree_id_map
|
||||
.0
|
||||
.iter_mut()
|
||||
.filter_map(|(project_file_name, project_type_telemetry)| {
|
||||
if project_type_telemetry
|
||||
.worktree_ids_reported
|
||||
.contains(&worktree_id)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let project_file_found = updated_entries_set.iter().any(|(path, _, _)| {
|
||||
path.as_ref()
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(|name_str| name_str == project_file_name)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
if !project_file_found {
|
||||
return None;
|
||||
}
|
||||
|
||||
project_type_telemetry
|
||||
.worktree_ids_reported
|
||||
.insert(worktree_id);
|
||||
|
||||
Some(project_type_telemetry.name.clone())
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Done on purpose to avoid calling `self.state.lock()` multiple times
|
||||
for project_name in project_names {
|
||||
self.report_app_event(format!("open {} project", project_name));
|
||||
}
|
||||
}
|
||||
|
||||
fn report_event(self: &Arc<Self>, event: Event) {
|
||||
let mut state = self.state.lock();
|
||||
|
||||
@@ -423,10 +590,6 @@ impl Telemetry {
|
||||
return;
|
||||
}
|
||||
|
||||
if ZED_CLIENT_CHECKSUM_SEED.is_none() {
|
||||
return;
|
||||
};
|
||||
|
||||
let this = self.clone();
|
||||
self.executor
|
||||
.spawn(
|
||||
@@ -445,20 +608,14 @@ impl Telemetry {
|
||||
|
||||
{
|
||||
let state = this.state.lock();
|
||||
|
||||
let request_body = EventRequestBody {
|
||||
installation_id: state.installation_id.as_deref().map(Into::into),
|
||||
session_id: state.session_id.clone(),
|
||||
is_staff: state.is_staff,
|
||||
app_version: state
|
||||
.app_metadata
|
||||
.app_version
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
os_name: state.app_metadata.os_name.to_string(),
|
||||
os_version: state
|
||||
.app_metadata
|
||||
.os_version
|
||||
.map(|version| version.to_string()),
|
||||
app_version: state.app_version.clone(),
|
||||
os_name: state.os_name.clone(),
|
||||
os_version: state.os_version.clone(),
|
||||
architecture: state.architecture.to_string(),
|
||||
|
||||
release_channel: state.release_channel.map(Into::into),
|
||||
@@ -468,9 +625,7 @@ impl Telemetry {
|
||||
serde_json::to_writer(&mut json_bytes, &request_body)?;
|
||||
}
|
||||
|
||||
let Some(checksum) = calculate_json_checksum(&json_bytes) else {
|
||||
return Ok(());
|
||||
};
|
||||
let checksum = calculate_json_checksum(&json_bytes).unwrap_or("".to_string());
|
||||
|
||||
let request = http::Request::builder()
|
||||
.method(Method::POST)
|
||||
|
||||
@@ -96,6 +96,7 @@ node_runtime.workspace = true
|
||||
notifications = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
recent_projects = { workspace = true }
|
||||
release_channel.workspace = true
|
||||
dev_server_projects.workspace = true
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -308,6 +308,13 @@ pub async fn post_panic(
|
||||
.map_err(|_| Error::Http(StatusCode::BAD_REQUEST, "invalid json".into()))?;
|
||||
let panic = report.panic;
|
||||
|
||||
if panic.os_name == "Linux" && panic.os_version == Some("1.0.0".to_string()) {
|
||||
return Err(Error::Http(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"invalid os version".into(),
|
||||
))?;
|
||||
}
|
||||
|
||||
tracing::error!(
|
||||
service = "client",
|
||||
version = %panic.app_version,
|
||||
@@ -394,12 +401,7 @@ pub async fn post_events(
|
||||
))?;
|
||||
};
|
||||
|
||||
if checksum != expected {
|
||||
return Err(Error::Http(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"invalid checksum".into(),
|
||||
))?;
|
||||
}
|
||||
let checksum_matched = checksum == expected;
|
||||
|
||||
let request_body: telemetry_events::EventRequestBody =
|
||||
serde_json::from_slice(&body).map_err(|err| {
|
||||
@@ -424,6 +426,7 @@ pub async fn post_events(
|
||||
&request_body,
|
||||
first_event_at,
|
||||
country_code.clone(),
|
||||
checksum_matched,
|
||||
)),
|
||||
// Needed for clients sending old copilot_event types
|
||||
Event::Copilot(_) => {}
|
||||
@@ -436,6 +439,7 @@ pub async fn post_events(
|
||||
&request_body,
|
||||
first_event_at,
|
||||
country_code.clone(),
|
||||
checksum_matched,
|
||||
))
|
||||
}
|
||||
Event::Call(event) => to_upload.call_events.push(CallEventRow::from_event(
|
||||
@@ -443,6 +447,7 @@ pub async fn post_events(
|
||||
&wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
Event::Assistant(event) => {
|
||||
to_upload
|
||||
@@ -452,6 +457,7 @@ pub async fn post_events(
|
||||
&wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
))
|
||||
}
|
||||
Event::Cpu(event) => to_upload.cpu_events.push(CpuEventRow::from_event(
|
||||
@@ -459,36 +465,42 @@ pub async fn post_events(
|
||||
&wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
Event::Memory(event) => to_upload.memory_events.push(MemoryEventRow::from_event(
|
||||
event.clone(),
|
||||
&wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
Event::App(event) => to_upload.app_events.push(AppEventRow::from_event(
|
||||
event.clone(),
|
||||
&wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
Event::Setting(event) => to_upload.setting_events.push(SettingEventRow::from_event(
|
||||
event.clone(),
|
||||
&wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
Event::Edit(event) => to_upload.edit_events.push(EditEventRow::from_event(
|
||||
event.clone(),
|
||||
&wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
Event::Action(event) => to_upload.action_events.push(ActionEventRow::from_event(
|
||||
event.clone(),
|
||||
&wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
Event::Extension(event) => {
|
||||
let metadata = app
|
||||
@@ -503,6 +515,7 @@ pub async fn post_events(
|
||||
&request_body,
|
||||
metadata,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -650,29 +663,30 @@ where
|
||||
|
||||
#[derive(Serialize, Debug, clickhouse::Row)]
|
||||
pub struct EditorEventRow {
|
||||
pub installation_id: String,
|
||||
pub operation: String,
|
||||
pub app_version: String,
|
||||
pub file_extension: String,
|
||||
pub os_name: String,
|
||||
pub os_version: String,
|
||||
pub release_channel: String,
|
||||
pub signed_in: bool,
|
||||
pub vim_mode: bool,
|
||||
installation_id: String,
|
||||
operation: String,
|
||||
app_version: String,
|
||||
file_extension: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
release_channel: String,
|
||||
signed_in: bool,
|
||||
vim_mode: bool,
|
||||
#[serde(serialize_with = "serialize_country_code")]
|
||||
pub country_code: String,
|
||||
pub region_code: String,
|
||||
pub city: String,
|
||||
pub time: i64,
|
||||
pub copilot_enabled: bool,
|
||||
pub copilot_enabled_for_language: bool,
|
||||
pub historical_event: bool,
|
||||
pub architecture: String,
|
||||
pub is_staff: Option<bool>,
|
||||
pub session_id: Option<String>,
|
||||
pub major: Option<i32>,
|
||||
pub minor: Option<i32>,
|
||||
pub patch: Option<i32>,
|
||||
country_code: String,
|
||||
region_code: String,
|
||||
city: String,
|
||||
time: i64,
|
||||
copilot_enabled: bool,
|
||||
copilot_enabled_for_language: bool,
|
||||
historical_event: bool,
|
||||
architecture: String,
|
||||
is_staff: Option<bool>,
|
||||
session_id: Option<String>,
|
||||
major: Option<i32>,
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
}
|
||||
|
||||
impl EditorEventRow {
|
||||
@@ -682,6 +696,7 @@ impl EditorEventRow {
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
country_code: Option<String>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -692,6 +707,7 @@ impl EditorEventRow {
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
checksum_matched,
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
@@ -716,25 +732,26 @@ impl EditorEventRow {
|
||||
|
||||
#[derive(Serialize, Debug, clickhouse::Row)]
|
||||
pub struct InlineCompletionEventRow {
|
||||
pub installation_id: String,
|
||||
pub provider: String,
|
||||
pub suggestion_accepted: bool,
|
||||
pub app_version: String,
|
||||
pub file_extension: String,
|
||||
pub os_name: String,
|
||||
pub os_version: String,
|
||||
pub release_channel: String,
|
||||
pub signed_in: bool,
|
||||
installation_id: String,
|
||||
provider: String,
|
||||
suggestion_accepted: bool,
|
||||
app_version: String,
|
||||
file_extension: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
release_channel: String,
|
||||
signed_in: bool,
|
||||
#[serde(serialize_with = "serialize_country_code")]
|
||||
pub country_code: String,
|
||||
pub region_code: String,
|
||||
pub city: String,
|
||||
pub time: i64,
|
||||
pub is_staff: Option<bool>,
|
||||
pub session_id: Option<String>,
|
||||
pub major: Option<i32>,
|
||||
pub minor: Option<i32>,
|
||||
pub patch: Option<i32>,
|
||||
country_code: String,
|
||||
region_code: String,
|
||||
city: String,
|
||||
time: i64,
|
||||
is_staff: Option<bool>,
|
||||
session_id: Option<String>,
|
||||
major: Option<i32>,
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
}
|
||||
|
||||
impl InlineCompletionEventRow {
|
||||
@@ -744,6 +761,7 @@ impl InlineCompletionEventRow {
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
country_code: Option<String>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -754,6 +772,7 @@ impl InlineCompletionEventRow {
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
checksum_matched,
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
@@ -780,6 +799,9 @@ pub struct CallEventRow {
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
release_channel: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
checksum_matched: bool,
|
||||
|
||||
// ClientEventBase
|
||||
installation_id: String,
|
||||
@@ -799,6 +821,7 @@ impl CallEventRow {
|
||||
wrapper: &EventWrapper,
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -809,7 +832,10 @@ impl CallEventRow {
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
checksum_matched,
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
installation_id: body.installation_id.clone().unwrap_or_default(),
|
||||
session_id: body.session_id.clone(),
|
||||
is_staff: body.is_staff,
|
||||
@@ -828,7 +854,10 @@ pub struct AssistantEventRow {
|
||||
major: Option<i32>,
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
release_channel: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
|
||||
// ClientEventBase
|
||||
installation_id: Option<String>,
|
||||
@@ -850,6 +879,7 @@ impl AssistantEventRow {
|
||||
wrapper: &EventWrapper,
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -860,7 +890,10 @@ impl AssistantEventRow {
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
checksum_matched,
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
installation_id: body.installation_id.clone(),
|
||||
session_id: body.session_id.clone(),
|
||||
is_staff: body.is_staff,
|
||||
@@ -878,18 +911,21 @@ impl AssistantEventRow {
|
||||
|
||||
#[derive(Debug, clickhouse::Row, Serialize)]
|
||||
pub struct CpuEventRow {
|
||||
pub installation_id: Option<String>,
|
||||
pub is_staff: Option<bool>,
|
||||
pub usage_as_percentage: f32,
|
||||
pub core_count: u32,
|
||||
pub app_version: String,
|
||||
pub release_channel: String,
|
||||
pub time: i64,
|
||||
pub session_id: Option<String>,
|
||||
installation_id: Option<String>,
|
||||
is_staff: Option<bool>,
|
||||
usage_as_percentage: f32,
|
||||
core_count: u32,
|
||||
app_version: String,
|
||||
release_channel: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
time: i64,
|
||||
session_id: Option<String>,
|
||||
// pub normalized_cpu_usage: f64, MATERIALIZED
|
||||
pub major: Option<i32>,
|
||||
pub minor: Option<i32>,
|
||||
pub patch: Option<i32>,
|
||||
major: Option<i32>,
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
}
|
||||
|
||||
impl CpuEventRow {
|
||||
@@ -898,6 +934,7 @@ impl CpuEventRow {
|
||||
wrapper: &EventWrapper,
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -908,7 +945,10 @@ impl CpuEventRow {
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
checksum_matched,
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
installation_id: body.installation_id.clone(),
|
||||
session_id: body.session_id.clone(),
|
||||
is_staff: body.is_staff,
|
||||
@@ -926,7 +966,10 @@ pub struct MemoryEventRow {
|
||||
major: Option<i32>,
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
release_channel: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
|
||||
// ClientEventBase
|
||||
installation_id: Option<String>,
|
||||
@@ -945,6 +988,7 @@ impl MemoryEventRow {
|
||||
wrapper: &EventWrapper,
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -955,7 +999,10 @@ impl MemoryEventRow {
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
checksum_matched,
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
installation_id: body.installation_id.clone(),
|
||||
session_id: body.session_id.clone(),
|
||||
is_staff: body.is_staff,
|
||||
@@ -973,7 +1020,10 @@ pub struct AppEventRow {
|
||||
major: Option<i32>,
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
release_channel: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
|
||||
// ClientEventBase
|
||||
installation_id: Option<String>,
|
||||
@@ -991,6 +1041,7 @@ impl AppEventRow {
|
||||
wrapper: &EventWrapper,
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -1001,7 +1052,10 @@ impl AppEventRow {
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
checksum_matched,
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
installation_id: body.installation_id.clone(),
|
||||
session_id: body.session_id.clone(),
|
||||
is_staff: body.is_staff,
|
||||
@@ -1018,7 +1072,10 @@ pub struct SettingEventRow {
|
||||
major: Option<i32>,
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
release_channel: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
|
||||
// ClientEventBase
|
||||
installation_id: Option<String>,
|
||||
@@ -1036,6 +1093,7 @@ impl SettingEventRow {
|
||||
wrapper: &EventWrapper,
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -1045,8 +1103,11 @@ impl SettingEventRow {
|
||||
app_version: body.app_version.clone(),
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
checksum_matched,
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
installation_id: body.installation_id.clone(),
|
||||
session_id: body.session_id.clone(),
|
||||
is_staff: body.is_staff,
|
||||
@@ -1064,7 +1125,10 @@ pub struct ExtensionEventRow {
|
||||
major: Option<i32>,
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
release_channel: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
|
||||
// ClientEventBase
|
||||
installation_id: Option<String>,
|
||||
@@ -1087,6 +1151,7 @@ impl ExtensionEventRow {
|
||||
body: &EventRequestBody,
|
||||
extension_metadata: Option<ExtensionMetadata>,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -1097,7 +1162,10 @@ impl ExtensionEventRow {
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
checksum_matched,
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
installation_id: body.installation_id.clone(),
|
||||
session_id: body.session_id.clone(),
|
||||
is_staff: body.is_staff,
|
||||
@@ -1126,7 +1194,10 @@ pub struct EditEventRow {
|
||||
major: Option<i32>,
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
release_channel: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
|
||||
// ClientEventBase
|
||||
installation_id: Option<String>,
|
||||
@@ -1148,6 +1219,7 @@ impl EditEventRow {
|
||||
wrapper: &EventWrapper,
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -1161,7 +1233,10 @@ impl EditEventRow {
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
checksum_matched,
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
installation_id: body.installation_id.clone(),
|
||||
session_id: body.session_id.clone(),
|
||||
is_staff: body.is_staff,
|
||||
@@ -1180,7 +1255,10 @@ pub struct ActionEventRow {
|
||||
major: Option<i32>,
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
release_channel: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
|
||||
// ClientEventBase
|
||||
installation_id: Option<String>,
|
||||
@@ -1200,6 +1278,7 @@ impl ActionEventRow {
|
||||
wrapper: &EventWrapper,
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -1210,7 +1289,10 @@ impl ActionEventRow {
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
checksum_matched,
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
installation_id: body.installation_id.clone(),
|
||||
session_id: body.session_id.clone(),
|
||||
is_staff: body.is_staff,
|
||||
|
||||
@@ -277,7 +277,7 @@ mod test {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_verify_access_token(cx: &mut gpui::TestAppContext) {
|
||||
let test_db = crate::db::TestDb::postgres(cx.executor().clone());
|
||||
let test_db = crate::db::TestDb::sqlite(cx.executor().clone());
|
||||
let db = test_db.db();
|
||||
|
||||
let user = db
|
||||
|
||||
@@ -379,6 +379,7 @@ fn metadata_from_extension_and_version(
|
||||
|
||||
pub fn convert_time_to_chrono(time: time::PrimitiveDateTime) -> chrono::DateTime<Utc> {
|
||||
chrono::DateTime::from_naive_utc_and_offset(
|
||||
#[allow(deprecated)]
|
||||
chrono::NaiveDateTime::from_timestamp_opt(time.assume_utc().unix_timestamp(), 0).unwrap(),
|
||||
Utc,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,8 @@ mod buffer_tests;
|
||||
mod channel_tests;
|
||||
mod contributor_tests;
|
||||
mod db_tests;
|
||||
// we only run postgres tests on macos right now
|
||||
#[cfg(target_os = "macos")]
|
||||
mod embedding_tests;
|
||||
mod extension_tests;
|
||||
mod feature_flag_tests;
|
||||
@@ -108,6 +110,7 @@ impl TestDb {
|
||||
#[macro_export]
|
||||
macro_rules! test_both_dbs {
|
||||
($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => {
|
||||
#[cfg(target_os = "macos")]
|
||||
#[gpui::test]
|
||||
async fn $postgres_test_name(cx: &mut gpui::TestAppContext) {
|
||||
let test_db = $crate::db::TestDb::postgres(cx.executor().clone());
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use super::*;
|
||||
use crate::test_both_dbs;
|
||||
use gpui::TestAppContext;
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
use std::sync::Arc;
|
||||
use tests::TestDb;
|
||||
|
||||
test_both_dbs!(
|
||||
test_get_users,
|
||||
@@ -564,9 +562,10 @@ fn test_fuzzy_like_string() {
|
||||
assert_eq!(Database::fuzzy_like_string(" z "), "%z%");
|
||||
}
|
||||
|
||||
#[cfg(target = "macos")]
|
||||
#[gpui::test]
|
||||
async fn test_fuzzy_search_users(cx: &mut TestAppContext) {
|
||||
let test_db = TestDb::postgres(cx.executor());
|
||||
async fn test_fuzzy_search_users(cx: &mut gpui::TestAppContext) {
|
||||
let test_db = tests::TestDb::postgres(cx.executor());
|
||||
let db = test_db.db();
|
||||
for (i, github_login) in [
|
||||
"California",
|
||||
|
||||
@@ -548,6 +548,9 @@ impl Server {
|
||||
.add_request_handler(user_handler(
|
||||
forward_mutating_project_request::<proto::RestartLanguageServers>,
|
||||
))
|
||||
.add_request_handler(user_handler(
|
||||
forward_mutating_project_request::<proto::LinkedEditingRange>,
|
||||
))
|
||||
.add_message_handler(create_buffer_for_peer)
|
||||
.add_request_handler(update_buffer)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::RefreshInlayHints>)
|
||||
|
||||
@@ -68,6 +68,7 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
|
||||
assert_eq!(projects.len(), 1);
|
||||
assert_eq!(projects[0].path, "/remote");
|
||||
workspace::join_dev_server_project(
|
||||
projects[0].id,
|
||||
projects[0].project_id.unwrap(),
|
||||
client.app_state.clone(),
|
||||
None,
|
||||
@@ -207,6 +208,7 @@ async fn create_dev_server_project(
|
||||
assert_eq!(projects.len(), 1);
|
||||
assert_eq!(projects[0].path, "/remote");
|
||||
workspace::join_dev_server_project(
|
||||
projects[0].id,
|
||||
projects[0].project_id.unwrap(),
|
||||
client_app_state,
|
||||
None,
|
||||
@@ -491,6 +493,7 @@ async fn test_dev_server_reconnect(
|
||||
.update(cx2, |store, cx| {
|
||||
let projects = store.dev_server_projects();
|
||||
workspace::join_dev_server_project(
|
||||
projects[0].id,
|
||||
projects[0].project_id.unwrap(),
|
||||
client2.app_state.clone(),
|
||||
None,
|
||||
@@ -572,7 +575,8 @@ async fn test_save_as_remote(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::Tes
|
||||
|
||||
let title = remote_workspace
|
||||
.update(&mut cx, |ws, cx| {
|
||||
ws.active_item(cx).unwrap().tab_description(0, &cx).unwrap()
|
||||
let active_item = ws.active_item(cx).unwrap();
|
||||
active_item.tab_description(0, &cx).unwrap()
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -28,8 +28,9 @@ use language::{
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use project::{
|
||||
project_settings::{InlineBlameSettings, ProjectSettings},
|
||||
SERVER_PROGRESS_DEBOUNCE_TIMEOUT,
|
||||
SERVER_PROGRESS_THROTTLE_TIMEOUT,
|
||||
};
|
||||
use recent_projects::disconnected_overlay::DisconnectedOverlay;
|
||||
use rpc::RECEIVE_TIMEOUT;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
@@ -59,6 +60,7 @@ async fn test_host_disconnect(
|
||||
.await;
|
||||
|
||||
cx_b.update(editor::init);
|
||||
cx_b.update(recent_projects::init);
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
@@ -83,7 +85,7 @@ async fn test_host_disconnect(
|
||||
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||
cx_a.background_executor.run_until_parked();
|
||||
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
|
||||
|
||||
let workspace_b = cx_b
|
||||
.add_window(|cx| Workspace::new(None, project_b.clone(), client_b.app_state.clone(), cx));
|
||||
@@ -120,14 +122,13 @@ async fn test_host_disconnect(
|
||||
|
||||
project_b.read_with(cx_b, |project, _| project.is_read_only());
|
||||
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
|
||||
|
||||
// Ensure client B's edited state is reset and that the whole window is blurred.
|
||||
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
assert_eq!(cx.focused(), None);
|
||||
assert!(!workspace.is_edited())
|
||||
assert!(workspace.active_modal::<DisconnectedOverlay>(cx).is_some());
|
||||
assert!(!workspace.is_edited());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -1005,6 +1006,8 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
fake_language_server.start_progress("the-token").await;
|
||||
|
||||
executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
|
||||
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
|
||||
token: lsp::NumberOrString::String("the-token".to_string()),
|
||||
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
|
||||
@@ -1014,11 +1017,10 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
},
|
||||
)),
|
||||
});
|
||||
executor.advance_clock(SERVER_PROGRESS_DEBOUNCE_TIMEOUT);
|
||||
executor.run_until_parked();
|
||||
|
||||
project_a.read_with(cx_a, |project, _| {
|
||||
let status = project.language_server_statuses().next().unwrap();
|
||||
let status = project.language_server_statuses().next().unwrap().1;
|
||||
assert_eq!(status.name, "the-language-server");
|
||||
assert_eq!(status.pending_work.len(), 1);
|
||||
assert_eq!(
|
||||
@@ -1035,10 +1037,11 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||
|
||||
project_b.read_with(cx_b, |project, _| {
|
||||
let status = project.language_server_statuses().next().unwrap();
|
||||
let status = project.language_server_statuses().next().unwrap().1;
|
||||
assert_eq!(status.name, "the-language-server");
|
||||
});
|
||||
|
||||
executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
|
||||
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
|
||||
token: lsp::NumberOrString::String("the-token".to_string()),
|
||||
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
|
||||
@@ -1048,11 +1051,10 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
},
|
||||
)),
|
||||
});
|
||||
executor.advance_clock(SERVER_PROGRESS_DEBOUNCE_TIMEOUT);
|
||||
executor.run_until_parked();
|
||||
|
||||
project_a.read_with(cx_a, |project, _| {
|
||||
let status = project.language_server_statuses().next().unwrap();
|
||||
let status = project.language_server_statuses().next().unwrap().1;
|
||||
assert_eq!(status.name, "the-language-server");
|
||||
assert_eq!(status.pending_work.len(), 1);
|
||||
assert_eq!(
|
||||
@@ -1062,7 +1064,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
});
|
||||
|
||||
project_b.read_with(cx_b, |project, _| {
|
||||
let status = project.language_server_statuses().next().unwrap();
|
||||
let status = project.language_server_statuses().next().unwrap().1;
|
||||
assert_eq!(status.name, "the-language-server");
|
||||
assert_eq!(status.pending_work.len(), 1);
|
||||
assert_eq!(
|
||||
|
||||
@@ -1378,7 +1378,7 @@ async fn test_unshare_project(
|
||||
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||
executor.run_until_parked();
|
||||
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
|
||||
|
||||
project_b
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||
@@ -1403,7 +1403,7 @@ async fn test_unshare_project(
|
||||
.unwrap();
|
||||
executor.run_until_parked();
|
||||
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
|
||||
|
||||
assert!(project_c.read_with(cx_c, |project, _| project.is_disconnected()));
|
||||
|
||||
@@ -1415,7 +1415,7 @@ async fn test_unshare_project(
|
||||
let project_c2 = client_c.build_dev_server_project(project_id, cx_c).await;
|
||||
executor.run_until_parked();
|
||||
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
|
||||
project_c2
|
||||
.update(cx_c, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||
.await
|
||||
@@ -1522,7 +1522,7 @@ async fn test_project_reconnect(
|
||||
executor.run_until_parked();
|
||||
|
||||
let worktree1_id = worktree_a1.read_with(cx_a, |worktree, _| {
|
||||
assert!(worktree.as_local().unwrap().is_shared());
|
||||
assert!(worktree.has_update_observer());
|
||||
worktree.id()
|
||||
});
|
||||
let (worktree_a2, _) = project_a1
|
||||
@@ -1534,7 +1534,7 @@ async fn test_project_reconnect(
|
||||
executor.run_until_parked();
|
||||
|
||||
let worktree2_id = worktree_a2.read_with(cx_a, |tree, _| {
|
||||
assert!(tree.as_local().unwrap().is_shared());
|
||||
assert!(tree.has_update_observer());
|
||||
tree.id()
|
||||
});
|
||||
executor.run_until_parked();
|
||||
@@ -1567,9 +1567,7 @@ async fn test_project_reconnect(
|
||||
assert_eq!(project.collaborators().len(), 1);
|
||||
});
|
||||
|
||||
worktree_a1.read_with(cx_a, |tree, _| {
|
||||
assert!(tree.as_local().unwrap().is_shared())
|
||||
});
|
||||
worktree_a1.read_with(cx_a, |tree, _| assert!(tree.has_update_observer()));
|
||||
|
||||
// While client A is disconnected, add and remove files from client A's project.
|
||||
client_a
|
||||
@@ -1611,7 +1609,7 @@ async fn test_project_reconnect(
|
||||
.await;
|
||||
|
||||
let worktree3_id = worktree_a3.read_with(cx_a, |tree, _| {
|
||||
assert!(!tree.as_local().unwrap().is_shared());
|
||||
assert!(!tree.has_update_observer());
|
||||
tree.id()
|
||||
});
|
||||
executor.run_until_parked();
|
||||
@@ -1634,7 +1632,7 @@ async fn test_project_reconnect(
|
||||
|
||||
project_a1.read_with(cx_a, |project, cx| {
|
||||
assert!(project.is_shared());
|
||||
assert!(worktree_a1.read(cx).as_local().unwrap().is_shared());
|
||||
assert!(worktree_a1.read(cx).has_update_observer());
|
||||
assert_eq!(
|
||||
worktree_a1
|
||||
.read(cx)
|
||||
@@ -1652,7 +1650,7 @@ async fn test_project_reconnect(
|
||||
"subdir2/i.txt"
|
||||
]
|
||||
);
|
||||
assert!(worktree_a3.read(cx).as_local().unwrap().is_shared());
|
||||
assert!(worktree_a3.read(cx).has_update_observer());
|
||||
assert_eq!(
|
||||
worktree_a3
|
||||
.read(cx)
|
||||
@@ -1733,7 +1731,7 @@ async fn test_project_reconnect(
|
||||
executor.run_until_parked();
|
||||
|
||||
let worktree4_id = worktree_a4.read_with(cx_a, |tree, _| {
|
||||
assert!(tree.as_local().unwrap().is_shared());
|
||||
assert!(tree.has_update_observer());
|
||||
tree.id()
|
||||
});
|
||||
project_a1.update(cx_a, |project, cx| {
|
||||
@@ -4774,7 +4772,7 @@ async fn test_references(
|
||||
// User is informed that a request is pending.
|
||||
executor.run_until_parked();
|
||||
project_b.read_with(cx_b, |project, _| {
|
||||
let status = project.language_server_statuses().next().cloned().unwrap();
|
||||
let status = project.language_server_statuses().next().unwrap().1;
|
||||
assert_eq!(status.name, "my-fake-lsp-adapter");
|
||||
assert_eq!(
|
||||
status.pending_work.values().next().unwrap().message,
|
||||
@@ -4804,7 +4802,7 @@ async fn test_references(
|
||||
executor.run_until_parked();
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
// User is informed that a request is no longer pending.
|
||||
let status = project.language_server_statuses().next().unwrap();
|
||||
let status = project.language_server_statuses().next().unwrap().1;
|
||||
assert!(status.pending_work.is_empty());
|
||||
|
||||
assert_eq!(references.len(), 3);
|
||||
@@ -4832,7 +4830,7 @@ async fn test_references(
|
||||
// User is informed that a request is pending.
|
||||
executor.run_until_parked();
|
||||
project_b.read_with(cx_b, |project, _| {
|
||||
let status = project.language_server_statuses().next().cloned().unwrap();
|
||||
let status = project.language_server_statuses().next().unwrap().1;
|
||||
assert_eq!(status.name, "my-fake-lsp-adapter");
|
||||
assert_eq!(
|
||||
status.pending_work.values().next().unwrap().message,
|
||||
@@ -4849,7 +4847,7 @@ async fn test_references(
|
||||
// User is informed that the request is no longer pending.
|
||||
executor.run_until_parked();
|
||||
project_b.read_with(cx_b, |project, _| {
|
||||
let status = project.language_server_statuses().next().unwrap();
|
||||
let status = project.language_server_statuses().next().unwrap().1;
|
||||
assert!(status.pending_work.is_empty());
|
||||
});
|
||||
}
|
||||
@@ -4906,7 +4904,15 @@ async fn test_project_search(
|
||||
let mut results = HashMap::default();
|
||||
let mut search_rx = project_b.update(cx_b, |project, cx| {
|
||||
project.search(
|
||||
SearchQuery::text("world", false, false, false, Vec::new(), Vec::new()).unwrap(),
|
||||
SearchQuery::text(
|
||||
"world",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -14,7 +14,9 @@ use language::{
|
||||
};
|
||||
use lsp::FakeLanguageServer;
|
||||
use pretty_assertions::assert_eq;
|
||||
use project::{search::SearchQuery, Project, ProjectPath, SearchResult};
|
||||
use project::{
|
||||
search::SearchQuery, Project, ProjectPath, SearchResult, DEFAULT_COMPLETION_CONTEXT,
|
||||
};
|
||||
use rand::{
|
||||
distributions::{Alphanumeric, DistString},
|
||||
prelude::*,
|
||||
@@ -303,7 +305,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
.filter(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
worktree.is_visible()
|
||||
&& worktree.entries(false).any(|e| e.is_file())
|
||||
&& worktree.entries(false, 0).any(|e| e.is_file())
|
||||
&& worktree.root_entry().map_or(false, |e| e.is_dir())
|
||||
})
|
||||
.choose(rng)
|
||||
@@ -425,14 +427,14 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
.filter(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
worktree.is_visible()
|
||||
&& worktree.entries(false).any(|e| e.is_file())
|
||||
&& worktree.entries(false, 0).any(|e| e.is_file())
|
||||
})
|
||||
.choose(rng)
|
||||
});
|
||||
let Some(worktree) = worktree else { continue };
|
||||
let full_path = worktree.read_with(cx, |worktree, _| {
|
||||
let entry = worktree
|
||||
.entries(false)
|
||||
.entries(false, 0)
|
||||
.filter(|e| e.is_file())
|
||||
.choose(rng)
|
||||
.unwrap();
|
||||
@@ -829,7 +831,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
.map_ok(|_| ())
|
||||
.boxed(),
|
||||
LspRequestKind::Completion => project
|
||||
.completions(&buffer, offset, cx)
|
||||
.completions(&buffer, offset, DEFAULT_COMPLETION_CONTEXT, cx)
|
||||
.map_ok(|_| ())
|
||||
.boxed(),
|
||||
LspRequestKind::CodeAction => project
|
||||
@@ -873,8 +875,15 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
|
||||
let mut search = project.update(cx, |project, cx| {
|
||||
project.search(
|
||||
SearchQuery::text(query, false, false, false, Vec::new(), Vec::new())
|
||||
.unwrap(),
|
||||
SearchQuery::text(
|
||||
query,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1204,8 +1213,8 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
guest_project.remote_id(),
|
||||
);
|
||||
assert_eq!(
|
||||
guest_snapshot.entries(false).collect::<Vec<_>>(),
|
||||
host_snapshot.entries(false).collect::<Vec<_>>(),
|
||||
guest_snapshot.entries(false, 0).collect::<Vec<_>>(),
|
||||
host_snapshot.entries(false, 0).collect::<Vec<_>>(),
|
||||
"{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}",
|
||||
client.username,
|
||||
host_snapshot.abs_path(),
|
||||
|
||||
@@ -69,7 +69,6 @@ struct TestPlan<T: RandomizedTest> {
|
||||
pub struct UserTestPlan {
|
||||
pub user_id: UserId,
|
||||
pub username: String,
|
||||
pub allow_client_reconnection: bool,
|
||||
pub allow_client_disconnection: bool,
|
||||
next_root_id: usize,
|
||||
operation_ix: usize,
|
||||
@@ -237,7 +236,6 @@ impl<T: RandomizedTest> TestPlan<T> {
|
||||
next_root_id: 0,
|
||||
operation_ix: 0,
|
||||
allow_client_disconnection,
|
||||
allow_client_reconnection,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ impl TestServer {
|
||||
}
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
release_channel::init("0.0.0", cx);
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
client::init_settings(cx);
|
||||
});
|
||||
|
||||
@@ -277,11 +277,7 @@ impl TestServer {
|
||||
node_runtime: FakeNodeRuntime::new(),
|
||||
});
|
||||
|
||||
let os_keymap = if cfg!(target_os = "linux") {
|
||||
"keymaps/default-linux.json"
|
||||
} else {
|
||||
"keymaps/default-macos.json"
|
||||
};
|
||||
let os_keymap = "keymaps/default-macos.json";
|
||||
|
||||
cx.update(|cx| {
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
@@ -327,7 +323,7 @@ impl TestServer {
|
||||
}
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
release_channel::init("0.0.0", cx);
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
client::init_settings(cx);
|
||||
});
|
||||
let (dev_server_id, _) = split_dev_server_token(&access_token).unwrap();
|
||||
|
||||
@@ -35,10 +35,12 @@ call.workspace = true
|
||||
channel.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
emojis.workspace = true
|
||||
extensions_ui.workspace = true
|
||||
feedback.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
|
||||
@@ -228,11 +228,11 @@ impl ChannelView {
|
||||
&self.editor,
|
||||
move |this, _, e: &EditorEvent, cx| {
|
||||
match e {
|
||||
EditorEvent::Reparsed => {
|
||||
EditorEvent::Reparsed(_) => {
|
||||
this.focus_position_from_link(position.clone(), false, cx);
|
||||
this._reparse_subscription.take();
|
||||
}
|
||||
EditorEvent::Edited | EditorEvent::SelectionsChanged { local: true } => {
|
||||
EditorEvent::Edited { .. } | EditorEvent::SelectionsChanged { local: true } => {
|
||||
this._reparse_subscription.take();
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -22,7 +22,7 @@ use settings::Settings;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{
|
||||
popover_menu, prelude::*, Avatar, Button, ContextMenu, IconButton, IconName, KeyBinding, Label,
|
||||
prelude::*, Avatar, Button, ContextMenu, IconButton, IconName, KeyBinding, Label, PopoverMenu,
|
||||
TabBar, Tooltip,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
@@ -679,7 +679,7 @@ impl ChatPanel {
|
||||
cx,
|
||||
div()
|
||||
.child(
|
||||
popover_menu(("menu", message_id))
|
||||
PopoverMenu::new(("menu", message_id))
|
||||
.trigger(IconButton::new(
|
||||
("trigger", message_id),
|
||||
IconName::Ellipsis,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use channel::{ChannelChat, ChannelStore, MessageParams};
|
||||
use client::{UserId, UserStore};
|
||||
use collections::HashSet;
|
||||
@@ -25,8 +25,15 @@ use crate::panel_settings::MessageEditorSettings;
|
||||
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
|
||||
|
||||
lazy_static! {
|
||||
static ref MENTIONS_SEARCH: SearchQuery =
|
||||
SearchQuery::regex("@[-_\\w]+", false, false, false, Vec::new(), Vec::new()).unwrap();
|
||||
static ref MENTIONS_SEARCH: SearchQuery = SearchQuery::regex(
|
||||
"@[-_\\w]+",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default()
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub struct MessageEditor {
|
||||
@@ -46,6 +53,7 @@ impl CompletionProvider for MessageEditorCompletionProvider {
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_position: language::Anchor,
|
||||
_: editor::CompletionContext,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<anyhow::Result<Vec<Completion>>> {
|
||||
let Some(handle) = self.0.upgrade() else {
|
||||
@@ -132,7 +140,7 @@ impl MessageEditor {
|
||||
|
||||
let markdown = language_registry.language_for_name("Markdown");
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let markdown = markdown.await?;
|
||||
let markdown = markdown.await.context("failed to load Markdown language")?;
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language(Some(markdown), cx)
|
||||
})
|
||||
|
||||
@@ -10,11 +10,12 @@ use gpui::{
|
||||
use project::{Project, RepositoryEntry};
|
||||
use recent_projects::RecentProjects;
|
||||
use rpc::proto::{self, DevServerStatus};
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
use ui::{
|
||||
h_flex, popover_menu, prelude::*, Avatar, AvatarAudioStatusIndicator, Button, ButtonLike,
|
||||
ButtonStyle, ContextMenu, Icon, IconButton, IconName, Indicator, TintColor, TitleBar, Tooltip,
|
||||
h_flex, prelude::*, Avatar, AvatarAudioStatusIndicator, Button, ButtonLike, ButtonStyle,
|
||||
ContextMenu, Icon, IconButton, IconName, Indicator, PopoverMenu, TintColor, TitleBar, Tooltip,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use vcs_menu::{BranchList, OpenRecent as ToggleVcsMenu};
|
||||
@@ -58,6 +59,8 @@ impl Render for CollabTitlebarItem {
|
||||
let project_id = self.project.read(cx).remote_id();
|
||||
let workspace = self.workspace.upgrade();
|
||||
|
||||
let platform_supported = cfg!(target_os = "macos");
|
||||
|
||||
TitleBar::new("collab-titlebar", Box::new(workspace::CloseWindow))
|
||||
// note: on windows titlebar behaviour is handled by the platform implementation
|
||||
.when(cfg!(not(windows)), |this| {
|
||||
@@ -71,6 +74,7 @@ impl Render for CollabTitlebarItem {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.children(self.render_application_menu(cx))
|
||||
.children(self.render_project_host(cx))
|
||||
.child(self.render_project_name(cx))
|
||||
.children(self.render_project_branch(cx))
|
||||
@@ -243,7 +247,9 @@ impl Render for CollabTitlebarItem {
|
||||
)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(
|
||||
if is_muted {
|
||||
if !platform_supported {
|
||||
"Cannot share microphone"
|
||||
} else if is_muted {
|
||||
"Unmute microphone"
|
||||
} else {
|
||||
"Mute microphone"
|
||||
@@ -253,7 +259,8 @@ impl Render for CollabTitlebarItem {
|
||||
})
|
||||
.style(ButtonStyle::Subtle)
|
||||
.icon_size(IconSize::Small)
|
||||
.selected(is_muted)
|
||||
.selected(platform_supported && is_muted)
|
||||
.disabled(!platform_supported)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Negative))
|
||||
.on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
|
||||
)
|
||||
@@ -271,8 +278,11 @@ impl Render for CollabTitlebarItem {
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Negative))
|
||||
.icon_size(IconSize::Small)
|
||||
.selected(is_deafened)
|
||||
.disabled(!platform_supported)
|
||||
.tooltip(move |cx| {
|
||||
if can_use_microphone {
|
||||
if !platform_supported {
|
||||
Tooltip::text("Cannot share microphone", cx)
|
||||
} else if can_use_microphone {
|
||||
Tooltip::with_meta(
|
||||
"Deafen Audio",
|
||||
None,
|
||||
@@ -291,10 +301,13 @@ impl Render for CollabTitlebarItem {
|
||||
.style(ButtonStyle::Subtle)
|
||||
.icon_size(IconSize::Small)
|
||||
.selected(is_screen_sharing)
|
||||
.disabled(!platform_supported)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(
|
||||
if is_screen_sharing {
|
||||
if !platform_supported {
|
||||
"Cannot share screen"
|
||||
} else if is_screen_sharing {
|
||||
"Stop Sharing Screen"
|
||||
} else {
|
||||
"Share Screen"
|
||||
@@ -375,8 +388,173 @@ impl CollabTitlebarItem {
|
||||
}
|
||||
}
|
||||
|
||||
// resolve if you are in a room -> render_project_owner
|
||||
// render_project_owner -> resolve if you are in a room -> Option<foo>
|
||||
pub fn render_application_menu(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
|
||||
cfg!(not(target_os = "macos")).then(|| {
|
||||
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
|
||||
let font = cx.text_style().font();
|
||||
let font_id = cx.text_system().resolve_font(&font);
|
||||
let width = cx
|
||||
.text_system()
|
||||
.typographic_bounds(font_id, ui_font_size, 'm')
|
||||
.unwrap()
|
||||
.size
|
||||
.width
|
||||
* 3.0;
|
||||
|
||||
PopoverMenu::new("application-menu")
|
||||
.menu(move |cx| {
|
||||
let width = width;
|
||||
ContextMenu::build(cx, move |menu, _cx| {
|
||||
let width = width;
|
||||
menu.header("Workspace")
|
||||
.action("Open Command Palette", Box::new(command_palette::Toggle))
|
||||
.custom_row(move |cx| {
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_between()
|
||||
.cursor(gpui::CursorStyle::Arrow)
|
||||
.child(Label::new("Buffer Font Size"))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.child(div().w(px(16.0)))
|
||||
.child(
|
||||
IconButton::new(
|
||||
"reset-buffer-zoom",
|
||||
IconName::RotateCcw,
|
||||
)
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(
|
||||
zed_actions::ResetBufferFontSize,
|
||||
))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("--buffer-zoom", IconName::Dash)
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(
|
||||
zed_actions::DecreaseBufferFontSize,
|
||||
))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w(width)
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_around()
|
||||
.child(Label::new(
|
||||
theme::get_buffer_font_size(cx).to_string(),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("+-buffer-zoom", IconName::Plus)
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(
|
||||
zed_actions::IncreaseBufferFontSize,
|
||||
))
|
||||
}),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.custom_row(move |cx| {
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_between()
|
||||
.cursor(gpui::CursorStyle::Arrow)
|
||||
.child(Label::new("UI Font Size"))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.child(
|
||||
IconButton::new(
|
||||
"reset-ui-zoom",
|
||||
IconName::RotateCcw,
|
||||
)
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(
|
||||
zed_actions::ResetUiFontSize,
|
||||
))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("--ui-zoom", IconName::Dash)
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(
|
||||
zed_actions::DecreaseUiFontSize,
|
||||
))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w(width)
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_around()
|
||||
.child(Label::new(
|
||||
theme::get_ui_font_size(cx).to_string(),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("+-ui-zoom", IconName::Plus)
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(
|
||||
zed_actions::IncreaseUiFontSize,
|
||||
))
|
||||
}),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.header("Project")
|
||||
.action(
|
||||
"Add Folder to Project...",
|
||||
Box::new(workspace::AddFolderToProject),
|
||||
)
|
||||
.action("Open a new Project...", Box::new(workspace::Open))
|
||||
.action(
|
||||
"Open Recent Projects...",
|
||||
Box::new(recent_projects::OpenRecent {
|
||||
create_new_window: false,
|
||||
}),
|
||||
)
|
||||
.header("Help")
|
||||
.action("About Zed", Box::new(zed_actions::About))
|
||||
.action("Welcome", Box::new(workspace::Welcome))
|
||||
.link(
|
||||
"Documentation",
|
||||
Box::new(zed_actions::OpenBrowser {
|
||||
url: "https://zed.dev/docs".into(),
|
||||
}),
|
||||
)
|
||||
.action("Give Feedback", Box::new(feedback::GiveFeedback))
|
||||
.action("Check for Updates", Box::new(auto_update::Check))
|
||||
.action("View Telemetry", Box::new(zed_actions::OpenTelemetryLog))
|
||||
.action(
|
||||
"View Dependency Licenses",
|
||||
Box::new(zed_actions::OpenLicenses),
|
||||
)
|
||||
.separator()
|
||||
.action("Quit", Box::new(zed_actions::Quit))
|
||||
})
|
||||
.into()
|
||||
})
|
||||
.trigger(
|
||||
IconButton::new("application-menu", ui::IconName::Menu)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(|cx| Tooltip::text("Open Application Menu", cx))
|
||||
.icon_size(IconSize::Small),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn render_project_host(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
|
||||
if let Some(dev_server) =
|
||||
@@ -413,6 +591,17 @@ impl CollabTitlebarItem {
|
||||
);
|
||||
}
|
||||
|
||||
if self.project.read(cx).is_disconnected() {
|
||||
return Some(
|
||||
Button::new("disconnected", "Disconnected")
|
||||
.disabled(true)
|
||||
.color(Color::Disabled)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.label_size(LabelSize::Small)
|
||||
.into_any_element(),
|
||||
);
|
||||
}
|
||||
|
||||
let host = self.project.read(cx).host()?;
|
||||
let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
|
||||
let participant_index = self
|
||||
@@ -686,7 +875,7 @@ impl CollabTitlebarItem {
|
||||
.on_click(|_, cx| {
|
||||
if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
|
||||
if auto_updater.read(cx).status().is_updated() {
|
||||
workspace::restart(&Default::default(), cx);
|
||||
workspace::reload(&Default::default(), cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -717,12 +906,13 @@ impl CollabTitlebarItem {
|
||||
|
||||
pub fn render_user_menu_button(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
|
||||
if let Some(user) = self.user_store.read(cx).current_user() {
|
||||
popover_menu("user-menu")
|
||||
PopoverMenu::new("user-menu")
|
||||
.menu(|cx| {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
.action("Key Bindings", Box::new(zed_actions::OpenKeymap))
|
||||
.action("Themes…", theme_selector::Toggle::default().boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
.separator()
|
||||
.action("Sign Out", client::SignOut.boxed_clone())
|
||||
})
|
||||
@@ -745,12 +935,13 @@ impl CollabTitlebarItem {
|
||||
)
|
||||
.anchor(gpui::AnchorCorner::TopRight)
|
||||
} else {
|
||||
popover_menu("user-menu")
|
||||
PopoverMenu::new("user-menu")
|
||||
.menu(|cx| {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
.action("Key Bindings", Box::new(zed_actions::OpenKeymap))
|
||||
.action("Themes…", theme_selector::Toggle::default().boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
})
|
||||
.into()
|
||||
})
|
||||
|
||||
@@ -13,8 +13,8 @@ use call::{report_call_event_for_room, ActiveCall};
|
||||
pub use collab_panel::CollabPanel;
|
||||
pub use collab_titlebar_item::CollabTitlebarItem;
|
||||
use gpui::{
|
||||
actions, point, AppContext, DevicePixels, Pixels, PlatformDisplay, Size, Task,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowContext, WindowKind, WindowOptions,
|
||||
actions, point, AppContext, Pixels, PlatformDisplay, Size, Task, WindowBackgroundAppearance,
|
||||
WindowBounds, WindowContext, WindowKind, WindowOptions,
|
||||
};
|
||||
use panel_settings::MessageEditorSettings;
|
||||
pub use panel_settings::{
|
||||
@@ -22,6 +22,7 @@ pub use panel_settings::{
|
||||
};
|
||||
use release_channel::ReleaseChannel;
|
||||
use settings::Settings;
|
||||
use ui::px;
|
||||
use workspace::{notifications::DetachAndPromptErr, AppState};
|
||||
|
||||
actions!(
|
||||
@@ -96,22 +97,19 @@ pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
|
||||
|
||||
fn notification_window_options(
|
||||
screen: Rc<dyn PlatformDisplay>,
|
||||
window_size: Size<Pixels>,
|
||||
size: Size<Pixels>,
|
||||
cx: &AppContext,
|
||||
) -> WindowOptions {
|
||||
let notification_margin_width = DevicePixels::from(16);
|
||||
let notification_margin_height = DevicePixels::from(-0) - DevicePixels::from(48);
|
||||
let notification_margin_width = px(16.);
|
||||
let notification_margin_height = px(-48.);
|
||||
|
||||
let screen_bounds = screen.bounds();
|
||||
let size: Size<DevicePixels> = window_size.into();
|
||||
|
||||
let bounds = gpui::Bounds::<DevicePixels> {
|
||||
origin: screen_bounds.upper_right()
|
||||
let bounds = gpui::Bounds::<Pixels> {
|
||||
origin: screen.bounds().upper_right()
|
||||
- point(
|
||||
size.width + notification_margin_width,
|
||||
notification_margin_height,
|
||||
),
|
||||
size: window_size.into(),
|
||||
size,
|
||||
};
|
||||
|
||||
let app_id = ReleaseChannel::global(cx).app_id();
|
||||
|
||||
@@ -3,9 +3,8 @@ use crate::notifications::collab_notification::CollabNotification;
|
||||
use call::{ActiveCall, IncomingCall};
|
||||
use futures::StreamExt;
|
||||
use gpui::{prelude::*, AppContext, WindowHandle};
|
||||
use settings::Settings;
|
||||
|
||||
use std::sync::{Arc, Weak};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, Button, Label};
|
||||
use util::ResultExt;
|
||||
use workspace::AppState;
|
||||
@@ -113,13 +112,7 @@ impl IncomingCallNotification {
|
||||
|
||||
impl Render for IncomingCallNotification {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
// TODO: Is there a better place for us to initialize the font?
|
||||
let (ui_font, ui_font_size) = {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
(theme_settings.ui_font.clone(), theme_settings.ui_font_size)
|
||||
};
|
||||
|
||||
cx.set_rem_size(ui_font_size);
|
||||
let ui_font = theme::setup_ui_font(cx);
|
||||
|
||||
div().size_full().font(ui_font).child(
|
||||
CollabNotification::new(
|
||||
|
||||
@@ -4,10 +4,10 @@ use call::{room, ActiveCall};
|
||||
use client::User;
|
||||
use collections::HashMap;
|
||||
use gpui::{AppContext, Size};
|
||||
use settings::Settings;
|
||||
use std::sync::{Arc, Weak};
|
||||
use theme::ThemeSettings;
|
||||
|
||||
use ui::{prelude::*, Button, Label};
|
||||
use util::ResultExt;
|
||||
use workspace::AppState;
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
@@ -27,16 +27,21 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
|
||||
for screen in cx.displays() {
|
||||
let options = notification_window_options(screen, window_size, cx);
|
||||
let window = cx.open_window(options, |cx| {
|
||||
cx.new_view(|_| {
|
||||
ProjectSharedNotification::new(
|
||||
owner.clone(),
|
||||
*project_id,
|
||||
worktree_root_names.clone(),
|
||||
app_state.clone(),
|
||||
)
|
||||
let Some(window) = cx
|
||||
.open_window(options, |cx| {
|
||||
cx.new_view(|_| {
|
||||
ProjectSharedNotification::new(
|
||||
owner.clone(),
|
||||
*project_id,
|
||||
worktree_root_names.clone(),
|
||||
app_state.clone(),
|
||||
)
|
||||
})
|
||||
})
|
||||
});
|
||||
.log_err()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
notification_windows
|
||||
.entry(*project_id)
|
||||
.or_insert(Vec::new())
|
||||
@@ -118,13 +123,7 @@ impl ProjectSharedNotification {
|
||||
|
||||
impl Render for ProjectSharedNotification {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
// TODO: Is there a better place for us to initialize the font?
|
||||
let (ui_font, ui_font_size) = {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
(theme_settings.ui_font.clone(), theme_settings.ui_font_size)
|
||||
};
|
||||
|
||||
cx.set_rem_size(ui_font_size);
|
||||
let ui_font = theme::setup_ui_font(cx);
|
||||
|
||||
div().size_full().font(ui_font).child(
|
||||
CollabNotification::new(
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
//! # Color
|
||||
//!
|
||||
//! The `color` crate provides a set utilities for working with colors. It is a wrapper around the [`palette`](https://docs.rs/palette) crate with some additional functionality.
|
||||
//!
|
||||
//! It is used to create a manipulate colors when building themes.
|
||||
//!
|
||||
//! === In development note ===
|
||||
//!
|
||||
//! This crate is meant to sit between gpui and the theme/ui for all the color related stuff.
|
||||
//!
|
||||
//! It could be folded into gpui, ui or theme potentially but for now we'll continue
|
||||
//! to develop it in isolation.
|
||||
//!
|
||||
//! Once we have a good idea of the needs of the theme system and color in gpui in general I see 3 paths:
|
||||
//! 1. Use `palette` (or another color library) directly in gpui and everywhere else, rather than rolling our own color system.
|
||||
//! 2. Keep this crate as a thin wrapper around `palette` and use it everywhere except gpui, and convert to gpui's color system when needed.
|
||||
//! 3. Build the needed functionality into gpui and keep using its color system everywhere.
|
||||
//!
|
||||
//! I'm leaning towards 2 in the short term and 1 in the long term, but we'll need to discuss it more.
|
||||
//!
|
||||
//! === End development note ===
|
||||
use palette::{
|
||||
blend::Blend, convert::FromColorUnclamped, encoding, rgb::Rgb, Clamp, Mix, Srgb, WithAlpha,
|
||||
};
|
||||
|
||||
/// The types of blend modes supported
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum BlendMode {
|
||||
/// Multiplies the colors, resulting in a darker color. This mode is useful for creating shadows.
|
||||
Multiply,
|
||||
/// Lightens the color by adding the source and destination colors. It results in a lighter color.
|
||||
Screen,
|
||||
/// Combines Multiply and Screen blend modes. Parts of the image that are lighter than 50% gray are lightened, and parts that are darker are darkened.
|
||||
Overlay,
|
||||
/// Selects the darker of the base or blend color as the resulting color. Useful for darkening images without affecting the overall contrast.
|
||||
Darken,
|
||||
/// Selects the lighter of the base or blend color as the resulting color. Useful for lightening images without affecting the overall contrast.
|
||||
Lighten,
|
||||
/// Brightens the base color to reflect the blend color. The result is a lightened image.
|
||||
Dodge,
|
||||
/// Darkens the base color to reflect the blend color. The result is a darkened image.
|
||||
Burn,
|
||||
/// Similar to Overlay, but with a stronger effect. Hard Light can either multiply or screen colors, depending on the blend color.
|
||||
HardLight,
|
||||
/// A softer version of Hard Light. Soft Light either darkens or lightens colors, depending on the blend color.
|
||||
SoftLight,
|
||||
/// Subtracts the darker of the two constituent colors from the lighter color. Difference mode is useful for creating more vivid colors.
|
||||
Difference,
|
||||
/// Similar to Difference, but with a lower contrast. Exclusion mode produces an effect similar to Difference but with less intensity.
|
||||
Exclusion,
|
||||
}
|
||||
|
||||
/// Converts a hexadecimal color string to a `palette::Hsla` color.
|
||||
///
|
||||
/// This function supports the following hex formats:
|
||||
/// `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`.
|
||||
pub fn hex_to_hsla(s: &str) -> Result<RGBAColor, String> {
|
||||
let hex = s.trim_start_matches('#');
|
||||
|
||||
// Expand shorthand formats #RGB and #RGBA to #RRGGBB and #RRGGBBAA
|
||||
let h = hex.as_bytes();
|
||||
let arr: [u8; 8] = match h.len() {
|
||||
// #RGB => #RRGGBBAA
|
||||
3 => [h[0], h[0], h[1], h[1], h[2], h[2], b'f', b'f'],
|
||||
// #RGBA => #RRGGBBAA
|
||||
4 => [h[0], h[0], h[1], h[1], h[2], h[2], h[3], h[3]],
|
||||
// #RRGGBB => #RRGGBBAA
|
||||
6 => [h[0], h[1], h[2], h[3], h[4], h[5], b'f', b'f'],
|
||||
// Already in #RRGGBBAA
|
||||
8 => h.try_into().unwrap(),
|
||||
_ => return Err("Invalid hexadecimal string length".to_string()),
|
||||
};
|
||||
|
||||
let hex =
|
||||
std::str::from_utf8(&arr).map_err(|_| format!("Invalid hexadecimal string: {}", s))?;
|
||||
let hex_val =
|
||||
u32::from_str_radix(hex, 16).map_err(|_| format!("Invalid hexadecimal string: {}", s))?;
|
||||
|
||||
Ok(RGBAColor {
|
||||
r: ((hex_val >> 24) & 0xFF) as f32 / 255.0,
|
||||
g: ((hex_val >> 16) & 0xFF) as f32 / 255.0,
|
||||
b: ((hex_val >> 8) & 0xFF) as f32 / 255.0,
|
||||
a: (hex_val & 0xFF) as f32 / 255.0,
|
||||
})
|
||||
}
|
||||
|
||||
// These derives implement to and from palette's color types.
|
||||
#[derive(FromColorUnclamped, WithAlpha, Debug, Clone)]
|
||||
#[palette(skip_derives(Rgb), rgb_standard = "encoding::Srgb")]
|
||||
pub struct RGBAColor {
|
||||
r: f32,
|
||||
g: f32,
|
||||
b: f32,
|
||||
// Let Palette know this is our alpha channel.
|
||||
#[palette(alpha)]
|
||||
a: f32,
|
||||
}
|
||||
|
||||
impl FromColorUnclamped<RGBAColor> for RGBAColor {
|
||||
fn from_color_unclamped(color: RGBAColor) -> RGBAColor {
|
||||
color
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> FromColorUnclamped<Rgb<S, f32>> for RGBAColor
|
||||
where
|
||||
Srgb: FromColorUnclamped<Rgb<S, f32>>,
|
||||
{
|
||||
fn from_color_unclamped(color: Rgb<S, f32>) -> RGBAColor {
|
||||
let srgb = Srgb::from_color_unclamped(color);
|
||||
RGBAColor {
|
||||
r: srgb.red,
|
||||
g: srgb.green,
|
||||
b: srgb.blue,
|
||||
a: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> FromColorUnclamped<RGBAColor> for Rgb<S, f32>
|
||||
where
|
||||
Rgb<S, f32>: FromColorUnclamped<Srgb>,
|
||||
{
|
||||
fn from_color_unclamped(color: RGBAColor) -> Self {
|
||||
Self::from_color_unclamped(Srgb::new(color.r, color.g, color.b))
|
||||
}
|
||||
}
|
||||
|
||||
impl Clamp for RGBAColor {
|
||||
fn clamp(self) -> Self {
|
||||
RGBAColor {
|
||||
r: self.r.min(1.0).max(0.0),
|
||||
g: self.g.min(1.0).max(0.0),
|
||||
b: self.b.min(1.0).max(0.0),
|
||||
a: self.a.min(1.0).max(0.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RGBAColor {
|
||||
/// Creates a new color from the given RGBA values.
|
||||
///
|
||||
/// This color can be used to convert to any [`palette::Color`] type.
|
||||
pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||
RGBAColor { r, g, b, a }
|
||||
}
|
||||
|
||||
/// Returns a set of states for this color.
|
||||
pub fn states(self, is_light: bool) -> ColorStates {
|
||||
states_for_color(self, is_light)
|
||||
}
|
||||
|
||||
/// Mixes this color with another [`palette::Hsl`] color at the given `mix_ratio`.
|
||||
pub fn mixed(&self, other: RGBAColor, mix_ratio: f32) -> Self {
|
||||
let srgb_self = Srgb::new(self.r, self.g, self.b);
|
||||
let srgb_other = Srgb::new(other.r, other.g, other.b);
|
||||
|
||||
// Directly mix the colors as sRGB values
|
||||
let mixed = srgb_self.mix(srgb_other, mix_ratio);
|
||||
RGBAColor::from_color_unclamped(mixed)
|
||||
}
|
||||
|
||||
pub fn blend(&self, other: RGBAColor, blend_mode: BlendMode) -> Self {
|
||||
let srgb_self = Srgb::new(self.r, self.g, self.b);
|
||||
let srgb_other = Srgb::new(other.r, other.g, other.b);
|
||||
|
||||
let blended = match blend_mode {
|
||||
// replace hsl methods with the respective sRGB methods
|
||||
BlendMode::Multiply => srgb_self.multiply(srgb_other),
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
|
||||
Self {
|
||||
r: blended.red,
|
||||
g: blended.green,
|
||||
b: blended.blue,
|
||||
a: self.a,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of colors for different states of an element.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ColorStates {
|
||||
/// The default color.
|
||||
pub default: RGBAColor,
|
||||
/// The color when the mouse is hovering over the element.
|
||||
pub hover: RGBAColor,
|
||||
/// The color when the mouse button is held down on the element.
|
||||
pub active: RGBAColor,
|
||||
/// The color when the element is focused with the keyboard.
|
||||
pub focused: RGBAColor,
|
||||
/// The color when the element is disabled.
|
||||
pub disabled: RGBAColor,
|
||||
}
|
||||
|
||||
/// Returns a set of colors for different states of an element.
|
||||
///
|
||||
/// todo("This should take a theme and use appropriate colors from it")
|
||||
pub fn states_for_color(color: RGBAColor, is_light: bool) -> ColorStates {
|
||||
let adjustment_factor = if is_light { 0.1 } else { -0.1 };
|
||||
let hover_adjustment = 1.0 - adjustment_factor;
|
||||
let active_adjustment = 1.0 - 2.0 * adjustment_factor;
|
||||
let focused_adjustment = 1.0 - 3.0 * adjustment_factor;
|
||||
let disabled_adjustment = 1.0 - 4.0 * adjustment_factor;
|
||||
|
||||
let make_adjustment = |color: RGBAColor, adjustment: f32| -> RGBAColor {
|
||||
// Adjust lightness for each state
|
||||
// Note: Adjustment logic may differ; simplify as needed for sRGB
|
||||
RGBAColor::new(
|
||||
color.r * adjustment,
|
||||
color.g * adjustment,
|
||||
color.b * adjustment,
|
||||
color.a,
|
||||
)
|
||||
};
|
||||
|
||||
let color = color.clamp();
|
||||
|
||||
ColorStates {
|
||||
default: color.clone(),
|
||||
hover: make_adjustment(color.clone(), hover_adjustment),
|
||||
active: make_adjustment(color.clone(), active_adjustment),
|
||||
focused: make_adjustment(color.clone(), focused_adjustment),
|
||||
disabled: make_adjustment(color.clone(), disabled_adjustment),
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ lsp.workspace = true
|
||||
menu.workspace = true
|
||||
node_runtime.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
|
||||
@@ -33,7 +33,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::{fs::remove_matching, maybe, paths, ResultExt};
|
||||
use util::{fs::remove_matching, maybe, ResultExt};
|
||||
|
||||
pub use copilot_completion_provider::CopilotCompletionProvider;
|
||||
pub use sign_in::CopilotCodeVerification;
|
||||
@@ -968,7 +968,7 @@ fn uri_for_buffer(buffer: &Model<Buffer>, cx: &AppContext) -> lsp::Url {
|
||||
}
|
||||
|
||||
async fn clear_copilot_dir() {
|
||||
remove_matching(&paths::COPILOT_DIR, |_| true).await
|
||||
remove_matching(paths::copilot_dir(), |_| true).await
|
||||
}
|
||||
|
||||
async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||
@@ -979,7 +979,7 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||
let release =
|
||||
latest_github_release("zed-industries/copilot", true, false, http.clone()).await?;
|
||||
|
||||
let version_dir = &*paths::COPILOT_DIR.join(format!("copilot-{}", release.tag_name));
|
||||
let version_dir = &paths::copilot_dir().join(format!("copilot-{}", release.tag_name));
|
||||
|
||||
fs::create_dir_all(version_dir).await?;
|
||||
let server_path = version_dir.join(SERVER_PATH);
|
||||
@@ -1003,7 +1003,7 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||
let archive = Archive::new(decompressed_bytes);
|
||||
archive.unpack(dist_dir).await?;
|
||||
|
||||
remove_matching(&paths::COPILOT_DIR, |entry| entry != version_dir).await;
|
||||
remove_matching(paths::copilot_dir(), |entry| entry != version_dir).await;
|
||||
}
|
||||
|
||||
Ok(server_path)
|
||||
@@ -1016,7 +1016,7 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||
// Fetch a cached binary, if it exists
|
||||
maybe!(async {
|
||||
let mut last_version_dir = None;
|
||||
let mut entries = fs::read_dir(paths::COPILOT_DIR.as_path()).await?;
|
||||
let mut entries = fs::read_dir(paths::copilot_dir()).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
@@ -1044,7 +1044,6 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use language::BufferId;
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_buffer_management(cx: &mut TestAppContext) {
|
||||
@@ -1258,16 +1257,5 @@ mod tests {
|
||||
fn load(&self, _: &AppContext) -> Task<Result<String>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn buffer_reloaded(
|
||||
&self,
|
||||
_: BufferId,
|
||||
_: &clock::Global,
|
||||
_: language::LineEnding,
|
||||
_: Option<std::time::SystemTime>,
|
||||
_: &mut AppContext,
|
||||
) {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ gpui.workspace = true
|
||||
indoc.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
paths.workspace = true
|
||||
release_channel.workspace = true
|
||||
smol.workspace = true
|
||||
sqlez.workspace = true
|
||||
|
||||
@@ -7,10 +7,10 @@ use anyhow::Context;
|
||||
use gpui::AppContext;
|
||||
pub use indoc::indoc;
|
||||
pub use lazy_static;
|
||||
pub use paths::database_dir;
|
||||
pub use smol;
|
||||
pub use sqlez;
|
||||
pub use sqlez_macros;
|
||||
pub use util::paths::DB_DIR;
|
||||
|
||||
use release_channel::ReleaseChannel;
|
||||
pub use release_channel::RELEASE_CHANNEL;
|
||||
@@ -145,7 +145,7 @@ macro_rules! define_connection {
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
$crate::lazy_static::lazy_static! {
|
||||
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db(&$crate::DB_DIR, &$crate::RELEASE_CHANNEL)));
|
||||
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db($crate::database_dir(), &$crate::RELEASE_CHANNEL)));
|
||||
}
|
||||
};
|
||||
(pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr;) => {
|
||||
@@ -176,7 +176,7 @@ macro_rules! define_connection {
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
$crate::lazy_static::lazy_static! {
|
||||
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db(&$crate::DB_DIR, &$crate::RELEASE_CHANNEL)));
|
||||
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db($crate::database_dir(), &$crate::RELEASE_CHANNEL)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ impl ProjectDiagnosticsEditor {
|
||||
let focus_handle = cx.focus_handle();
|
||||
cx.on_focus_in(&focus_handle, |this, cx| this.focus_in(cx))
|
||||
.detach();
|
||||
cx.on_focus_out(&focus_handle, |this, cx| this.focus_out(cx))
|
||||
cx.on_focus_out(&focus_handle, |this, _event, cx| this.focus_out(cx))
|
||||
.detach();
|
||||
|
||||
let excerpts = cx.new_model(|cx| {
|
||||
@@ -867,10 +867,12 @@ fn compare_diagnostics(
|
||||
snapshot: &language::BufferSnapshot,
|
||||
) -> Ordering {
|
||||
use language::ToOffset;
|
||||
// The old diagnostics may point to a previously open Buffer for this file.
|
||||
if !old.range.start.is_valid(snapshot) {
|
||||
|
||||
// The diagnostics may point to a previously open Buffer for this file.
|
||||
if !old.range.start.is_valid(snapshot) || !new.range.start.is_valid(snapshot) {
|
||||
return Ordering::Greater;
|
||||
}
|
||||
|
||||
old.range
|
||||
.start
|
||||
.to_offset(snapshot)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
percentage, rems, Animation, AnimationExt, EventEmitter, IntoElement, ParentElement, Render,
|
||||
Styled, Subscription, Transformation, View, ViewContext, WeakView,
|
||||
rems, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, View,
|
||||
ViewContext, WeakView,
|
||||
};
|
||||
use language::Diagnostic;
|
||||
use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip};
|
||||
@@ -61,42 +59,7 @@ impl Render for DiagnosticIndicator {
|
||||
.child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
|
||||
};
|
||||
|
||||
let has_in_progress_checks = self
|
||||
.workspace
|
||||
.upgrade()
|
||||
.and_then(|workspace| {
|
||||
workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics()
|
||||
.next()
|
||||
})
|
||||
.is_some();
|
||||
|
||||
let status = if has_in_progress_checks {
|
||||
Some(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(delta)))
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Checking…")
|
||||
.size(LabelSize::Small)
|
||||
.into_any_element(),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
} else if let Some(diagnostic) = &self.current_diagnostic {
|
||||
let status = if let Some(diagnostic) = &self.current_diagnostic {
|
||||
let message = diagnostic.message.split('\n').next().unwrap().to_string();
|
||||
Some(
|
||||
Button::new("diagnostic_message", message)
|
||||
|
||||
@@ -30,6 +30,7 @@ test-support = [
|
||||
[dependencies]
|
||||
aho-corasick = "1.1"
|
||||
anyhow.workspace = true
|
||||
assets.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! This module contains all actions supported by [`Editor`].
|
||||
use super::*;
|
||||
use gpui::action_as;
|
||||
use util::serde::default_true;
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
@@ -125,6 +126,11 @@ pub struct ExpandExcerptsDown {
|
||||
#[serde(default)]
|
||||
pub(super) lines: u32,
|
||||
}
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
pub struct ShowCompletions {
|
||||
#[serde(default)]
|
||||
pub(super) trigger: Option<char>,
|
||||
}
|
||||
|
||||
impl_actions!(
|
||||
editor,
|
||||
@@ -147,6 +153,7 @@ impl_actions!(
|
||||
SelectToBeginningOfLine,
|
||||
SelectToEndOfLine,
|
||||
SelectUpByLines,
|
||||
ShowCompletions,
|
||||
ToggleCodeActions,
|
||||
ToggleComments,
|
||||
UnfoldAt,
|
||||
@@ -163,6 +170,7 @@ gpui::actions!(
|
||||
AddSelectionBelow,
|
||||
Backspace,
|
||||
Cancel,
|
||||
CancelLanguageServerWork,
|
||||
ConfirmRename,
|
||||
ContextMenuFirst,
|
||||
ContextMenuLast,
|
||||
@@ -273,8 +281,9 @@ gpui::actions!(
|
||||
SelectToPreviousWordStart,
|
||||
SelectToStartOfParagraph,
|
||||
SelectUp,
|
||||
SelectPageDown,
|
||||
SelectPageUp,
|
||||
ShowCharacterPalette,
|
||||
ShowCompletions,
|
||||
ShowInlineCompletion,
|
||||
ShuffleLines,
|
||||
SortLinesCaseInsensitive,
|
||||
@@ -284,11 +293,13 @@ gpui::actions!(
|
||||
TabPrev,
|
||||
ToggleGitBlame,
|
||||
ToggleGitBlameInline,
|
||||
ToggleSelectionMenu,
|
||||
ToggleHunkDiff,
|
||||
ToggleInlayHints,
|
||||
ToggleLineNumbers,
|
||||
ToggleIndentGuides,
|
||||
ToggleSoftWrap,
|
||||
ToggleTabBar,
|
||||
Transpose,
|
||||
Undo,
|
||||
UndoSelection,
|
||||
@@ -297,3 +308,7 @@ gpui::actions!(
|
||||
UniqueLinesCaseSensitive,
|
||||
]
|
||||
);
|
||||
|
||||
action_as!(outline, ToggleOutline as Toggle);
|
||||
|
||||
action_as!(go_to_line, ToggleGoToLine as Toggle);
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
//! [EditorElement]: crate::element::EditorElement
|
||||
|
||||
mod block_map;
|
||||
mod flap_map;
|
||||
mod crease_map;
|
||||
mod fold_map;
|
||||
mod inlay_map;
|
||||
mod tab_map;
|
||||
@@ -33,7 +33,7 @@ pub use block_map::{
|
||||
};
|
||||
use block_map::{BlockRow, BlockSnapshot};
|
||||
use collections::{HashMap, HashSet};
|
||||
pub use flap_map::*;
|
||||
pub use crease_map::*;
|
||||
pub use fold_map::{Fold, FoldId, FoldPlaceholder, FoldPoint};
|
||||
use fold_map::{FoldMap, FoldSnapshot};
|
||||
use gpui::{
|
||||
@@ -52,8 +52,14 @@ use multi_buffer::{
|
||||
ToOffset, ToPoint,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::ops::Add;
|
||||
use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
borrow::Cow,
|
||||
fmt::Debug,
|
||||
num::NonZeroU32,
|
||||
ops::{Add, Range, Sub},
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::{Bias, TreeMap};
|
||||
use tab_map::{TabMap, TabSnapshot};
|
||||
use text::LineIndent;
|
||||
@@ -100,7 +106,7 @@ pub struct DisplayMap {
|
||||
/// Regions of inlays that should be highlighted.
|
||||
inlay_highlights: InlayHighlights,
|
||||
/// A container for explicitly foldable ranges, which supersede indentation based fold range suggestions.
|
||||
flap_map: FlapMap,
|
||||
crease_map: CreaseMap,
|
||||
fold_placeholder: FoldPlaceholder,
|
||||
pub clip_at_line_ends: bool,
|
||||
}
|
||||
@@ -133,7 +139,7 @@ impl DisplayMap {
|
||||
excerpt_header_height,
|
||||
excerpt_footer_height,
|
||||
);
|
||||
let flap_map = FlapMap::default();
|
||||
let crease_map = CreaseMap::default();
|
||||
|
||||
cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
|
||||
|
||||
@@ -145,7 +151,7 @@ impl DisplayMap {
|
||||
tab_map,
|
||||
wrap_map,
|
||||
block_map,
|
||||
flap_map,
|
||||
crease_map,
|
||||
fold_placeholder,
|
||||
text_highlights: Default::default(),
|
||||
inlay_highlights: Default::default(),
|
||||
@@ -172,7 +178,7 @@ impl DisplayMap {
|
||||
tab_snapshot,
|
||||
wrap_snapshot,
|
||||
block_snapshot,
|
||||
flap_snapshot: self.flap_map.snapshot(),
|
||||
crease_snapshot: self.crease_map.snapshot(),
|
||||
text_highlights: self.text_highlights.clone(),
|
||||
inlay_highlights: self.inlay_highlights.clone(),
|
||||
clip_at_line_ends: self.clip_at_line_ends,
|
||||
@@ -241,22 +247,22 @@ impl DisplayMap {
|
||||
self.block_map.read(snapshot, edits);
|
||||
}
|
||||
|
||||
pub fn insert_flaps(
|
||||
pub fn insert_creases(
|
||||
&mut self,
|
||||
flaps: impl IntoIterator<Item = Flap>,
|
||||
creases: impl IntoIterator<Item = Crease>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Vec<FlapId> {
|
||||
) -> Vec<CreaseId> {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
self.flap_map.insert(flaps, &snapshot)
|
||||
self.crease_map.insert(creases, &snapshot)
|
||||
}
|
||||
|
||||
pub fn remove_flaps(
|
||||
pub fn remove_creases(
|
||||
&mut self,
|
||||
flap_ids: impl IntoIterator<Item = FlapId>,
|
||||
crease_ids: impl IntoIterator<Item = CreaseId>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
self.flap_map.remove(flap_ids, &snapshot)
|
||||
self.crease_map.remove(crease_ids, &snapshot)
|
||||
}
|
||||
|
||||
pub fn insert_blocks(
|
||||
@@ -466,7 +472,7 @@ pub struct HighlightedChunk<'a> {
|
||||
pub struct DisplaySnapshot {
|
||||
pub buffer_snapshot: MultiBufferSnapshot,
|
||||
pub fold_snapshot: FoldSnapshot,
|
||||
pub flap_snapshot: FlapSnapshot,
|
||||
pub crease_snapshot: CreaseSnapshot,
|
||||
inlay_snapshot: InlaySnapshot,
|
||||
tab_snapshot: TabSnapshot,
|
||||
wrap_snapshot: WrapSnapshot,
|
||||
@@ -949,13 +955,13 @@ impl DisplaySnapshot {
|
||||
buffer_row: MultiBufferRow,
|
||||
) -> Option<(Range<Point>, FoldPlaceholder)> {
|
||||
let start = MultiBufferPoint::new(buffer_row.0, self.buffer_snapshot.line_len(buffer_row));
|
||||
if let Some(flap) = self
|
||||
.flap_snapshot
|
||||
if let Some(crease) = self
|
||||
.crease_snapshot
|
||||
.query_row(buffer_row, &self.buffer_snapshot)
|
||||
{
|
||||
Some((
|
||||
flap.range.to_point(&self.buffer_snapshot),
|
||||
flap.placeholder.clone(),
|
||||
crease.range.to_point(&self.buffer_snapshot),
|
||||
crease.placeholder.clone(),
|
||||
))
|
||||
} else if self.starts_indent(MultiBufferRow(start.row))
|
||||
&& !self.is_line_folded(MultiBufferRow(start.row))
|
||||
@@ -1015,6 +1021,22 @@ impl Debug for DisplayPoint {
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for DisplayPoint {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, other: Self) -> Self::Output {
|
||||
DisplayPoint(BlockPoint(self.0 .0 + other.0 .0))
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for DisplayPoint {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, other: Self) -> Self::Output {
|
||||
DisplayPoint(BlockPoint(self.0 .0 - other.0 .0))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq, Deserialize, Hash)]
|
||||
#[serde(transparent)]
|
||||
pub struct DisplayRow(pub u32);
|
||||
@@ -1027,6 +1049,14 @@ impl Add for DisplayRow {
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for DisplayRow {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, other: Self) -> Self::Output {
|
||||
DisplayRow(self.0 - other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl DisplayPoint {
|
||||
pub fn new(row: DisplayRow, column: u32) -> Self {
|
||||
Self(BlockPoint(Point::new(row.0, column)))
|
||||
@@ -1097,14 +1127,11 @@ impl ToDisplayPoint for Anchor {
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
movement,
|
||||
test::{editor_test_context::EditorTestContext, marked_display_snapshot},
|
||||
};
|
||||
use crate::{movement, test::marked_display_snapshot};
|
||||
use gpui::{div, font, observe, px, AppContext, BorrowAppContext, Context, Element, Hsla};
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
|
||||
Buffer, Language, LanguageConfig, LanguageMatcher, SelectionGoal,
|
||||
Buffer, Language, LanguageConfig, LanguageMatcher,
|
||||
};
|
||||
use project::Project;
|
||||
use rand::{prelude::*, Rng};
|
||||
@@ -1379,6 +1406,7 @@ pub mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[gpui::test(retries = 5)]
|
||||
async fn test_soft_wraps(cx: &mut gpui::TestAppContext) {
|
||||
cx.background_executor
|
||||
@@ -1387,7 +1415,7 @@ pub mod tests {
|
||||
init_test(cx, |_| {});
|
||||
});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let mut cx = crate::test::editor_test_context::EditorTestContext::new(cx).await;
|
||||
let editor = cx.editor.clone();
|
||||
let window = cx.window;
|
||||
|
||||
@@ -1443,39 +1471,39 @@ pub mod tests {
|
||||
movement::up(
|
||||
&snapshot,
|
||||
DisplayPoint::new(DisplayRow(1), 10),
|
||||
SelectionGoal::None,
|
||||
language::SelectionGoal::None,
|
||||
false,
|
||||
&text_layout_details,
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(0), 7),
|
||||
SelectionGoal::HorizontalPosition(x.0)
|
||||
language::SelectionGoal::HorizontalPosition(x.0)
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
movement::down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(DisplayRow(0), 7),
|
||||
SelectionGoal::HorizontalPosition(x.0),
|
||||
language::SelectionGoal::HorizontalPosition(x.0),
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(1), 10),
|
||||
SelectionGoal::HorizontalPosition(x.0)
|
||||
language::SelectionGoal::HorizontalPosition(x.0)
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
movement::down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(DisplayRow(1), 10),
|
||||
SelectionGoal::HorizontalPosition(x.0),
|
||||
language::SelectionGoal::HorizontalPosition(x.0),
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(2), 4),
|
||||
SelectionGoal::HorizontalPosition(x.0)
|
||||
language::SelectionGoal::HorizontalPosition(x.0)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1665,6 +1693,8 @@ pub mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// todo(linux) fails due to pixel differences in text rendering
|
||||
#[cfg(target_os = "macos")]
|
||||
#[gpui::test]
|
||||
async fn test_chunks_with_soft_wrapping(cx: &mut gpui::TestAppContext) {
|
||||
use unindent::Unindent as _;
|
||||
@@ -1916,7 +1946,7 @@ pub mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_flaps(cx: &mut gpui::AppContext) {
|
||||
fn test_creases(cx: &mut gpui::AppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let text = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll";
|
||||
@@ -1939,8 +1969,8 @@ pub mod tests {
|
||||
let range =
|
||||
snapshot.anchor_before(Point::new(2, 0))..snapshot.anchor_after(Point::new(3, 3));
|
||||
|
||||
map.flap_map.insert(
|
||||
[Flap::new(
|
||||
map.crease_map.insert(
|
||||
[Crease::new(
|
||||
range,
|
||||
FoldPlaceholder::test(),
|
||||
|_row, _status, _toggle, _cx| div(),
|
||||
|
||||
@@ -1157,7 +1157,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::display_map::inlay_map::InlayMap;
|
||||
use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
|
||||
use gpui::{div, font, px, Element};
|
||||
use gpui::{div, font, px, AssetSource, Element};
|
||||
use multi_buffer::MultiBuffer;
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
@@ -1452,6 +1452,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[gpui::test]
|
||||
fn test_blocks_on_wrapped_lines(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(|cx| init_test(cx));
|
||||
@@ -1940,6 +1941,12 @@ mod tests {
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
cx.text_system()
|
||||
.add_fonts(vec![assets::Assets
|
||||
.load("fonts/zed-mono/zed-mono-extended.ttf")
|
||||
.unwrap()
|
||||
.unwrap()])
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
impl TransformBlock {
|
||||
|
||||
@@ -9,36 +9,36 @@ use ui::WindowContext;
|
||||
use crate::FoldPlaceholder;
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
|
||||
pub struct FlapId(usize);
|
||||
pub struct CreaseId(usize);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FlapMap {
|
||||
snapshot: FlapSnapshot,
|
||||
next_id: FlapId,
|
||||
id_to_range: HashMap<FlapId, Range<Anchor>>,
|
||||
pub struct CreaseMap {
|
||||
snapshot: CreaseSnapshot,
|
||||
next_id: CreaseId,
|
||||
id_to_range: HashMap<CreaseId, Range<Anchor>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct FlapSnapshot {
|
||||
flaps: SumTree<FlapItem>,
|
||||
pub struct CreaseSnapshot {
|
||||
creases: SumTree<CreaseItem>,
|
||||
}
|
||||
|
||||
impl FlapSnapshot {
|
||||
/// Returns the first Flap starting on the specified buffer row.
|
||||
impl CreaseSnapshot {
|
||||
/// Returns the first Crease starting on the specified buffer row.
|
||||
pub fn query_row<'a>(
|
||||
&'a self,
|
||||
row: MultiBufferRow,
|
||||
snapshot: &'a MultiBufferSnapshot,
|
||||
) -> Option<&'a Flap> {
|
||||
) -> Option<&'a Crease> {
|
||||
let start = snapshot.anchor_before(Point::new(row.0, 0));
|
||||
let mut cursor = self.flaps.cursor::<ItemSummary>();
|
||||
let mut cursor = self.creases.cursor::<ItemSummary>();
|
||||
cursor.seek(&start, Bias::Left, snapshot);
|
||||
while let Some(item) = cursor.item() {
|
||||
match Ord::cmp(&item.flap.range.start.to_point(snapshot).row, &row.0) {
|
||||
match Ord::cmp(&item.crease.range.start.to_point(snapshot).row, &row.0) {
|
||||
Ordering::Less => cursor.next(snapshot),
|
||||
Ordering::Equal => {
|
||||
if item.flap.range.start.is_valid(snapshot) {
|
||||
return Some(&item.flap);
|
||||
if item.crease.range.start.is_valid(snapshot) {
|
||||
return Some(&item.crease);
|
||||
} else {
|
||||
cursor.next(snapshot);
|
||||
}
|
||||
@@ -49,17 +49,17 @@ impl FlapSnapshot {
|
||||
return None;
|
||||
}
|
||||
|
||||
pub fn flap_items_with_offsets(
|
||||
pub fn crease_items_with_offsets(
|
||||
&self,
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
) -> Vec<(FlapId, Range<Point>)> {
|
||||
let mut cursor = self.flaps.cursor::<ItemSummary>();
|
||||
) -> Vec<(CreaseId, Range<Point>)> {
|
||||
let mut cursor = self.creases.cursor::<ItemSummary>();
|
||||
let mut results = Vec::new();
|
||||
|
||||
cursor.next(snapshot);
|
||||
while let Some(item) = cursor.item() {
|
||||
let start_point = item.flap.range.start.to_point(snapshot);
|
||||
let end_point = item.flap.range.end.to_point(snapshot);
|
||||
let start_point = item.crease.range.start.to_point(snapshot);
|
||||
let end_point = item.crease.range.end.to_point(snapshot);
|
||||
results.push((item.id, start_point..end_point));
|
||||
cursor.next(snapshot);
|
||||
}
|
||||
@@ -82,14 +82,14 @@ type RenderTrailerFn =
|
||||
Arc<dyn Send + Sync + Fn(MultiBufferRow, bool, &mut WindowContext) -> AnyElement>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Flap {
|
||||
pub struct Crease {
|
||||
pub range: Range<Anchor>,
|
||||
pub placeholder: FoldPlaceholder,
|
||||
pub render_toggle: RenderToggleFn,
|
||||
pub render_trailer: RenderTrailerFn,
|
||||
}
|
||||
|
||||
impl Flap {
|
||||
impl Crease {
|
||||
pub fn new<RenderToggle, ToggleElement, RenderTrailer, TrailerElement>(
|
||||
range: Range<Anchor>,
|
||||
placeholder: FoldPlaceholder,
|
||||
@@ -115,7 +115,7 @@ impl Flap {
|
||||
+ 'static,
|
||||
TrailerElement: IntoElement,
|
||||
{
|
||||
Flap {
|
||||
Crease {
|
||||
range,
|
||||
placeholder,
|
||||
render_toggle: Arc::new(move |row, folded, toggle, cx| {
|
||||
@@ -128,50 +128,52 @@ impl Flap {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Flap {
|
||||
impl std::fmt::Debug for Crease {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Flap").field("range", &self.range).finish()
|
||||
f.debug_struct("Crease")
|
||||
.field("range", &self.range)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct FlapItem {
|
||||
id: FlapId,
|
||||
flap: Flap,
|
||||
struct CreaseItem {
|
||||
id: CreaseId,
|
||||
crease: Crease,
|
||||
}
|
||||
|
||||
impl FlapMap {
|
||||
pub fn snapshot(&self) -> FlapSnapshot {
|
||||
impl CreaseMap {
|
||||
pub fn snapshot(&self) -> CreaseSnapshot {
|
||||
self.snapshot.clone()
|
||||
}
|
||||
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
flaps: impl IntoIterator<Item = Flap>,
|
||||
creases: impl IntoIterator<Item = Crease>,
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
) -> Vec<FlapId> {
|
||||
) -> Vec<CreaseId> {
|
||||
let mut new_ids = Vec::new();
|
||||
self.snapshot.flaps = {
|
||||
let mut new_flaps = SumTree::new();
|
||||
let mut cursor = self.snapshot.flaps.cursor::<ItemSummary>();
|
||||
for flap in flaps {
|
||||
new_flaps.append(cursor.slice(&flap.range, Bias::Left, snapshot), snapshot);
|
||||
self.snapshot.creases = {
|
||||
let mut new_creases = SumTree::new();
|
||||
let mut cursor = self.snapshot.creases.cursor::<ItemSummary>();
|
||||
for crease in creases {
|
||||
new_creases.append(cursor.slice(&crease.range, Bias::Left, snapshot), snapshot);
|
||||
|
||||
let id = self.next_id;
|
||||
self.next_id.0 += 1;
|
||||
self.id_to_range.insert(id, flap.range.clone());
|
||||
new_flaps.push(FlapItem { flap, id }, snapshot);
|
||||
self.id_to_range.insert(id, crease.range.clone());
|
||||
new_creases.push(CreaseItem { crease, id }, snapshot);
|
||||
new_ids.push(id);
|
||||
}
|
||||
new_flaps.append(cursor.suffix(snapshot), snapshot);
|
||||
new_flaps
|
||||
new_creases.append(cursor.suffix(snapshot), snapshot);
|
||||
new_creases
|
||||
};
|
||||
new_ids
|
||||
}
|
||||
|
||||
pub fn remove(
|
||||
&mut self,
|
||||
ids: impl IntoIterator<Item = FlapId>,
|
||||
ids: impl IntoIterator<Item = CreaseId>,
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
) {
|
||||
let mut removals = Vec::new();
|
||||
@@ -184,24 +186,24 @@ impl FlapMap {
|
||||
AnchorRangeExt::cmp(a_range, b_range, snapshot).then(b_id.cmp(&a_id))
|
||||
});
|
||||
|
||||
self.snapshot.flaps = {
|
||||
let mut new_flaps = SumTree::new();
|
||||
let mut cursor = self.snapshot.flaps.cursor::<ItemSummary>();
|
||||
self.snapshot.creases = {
|
||||
let mut new_creases = SumTree::new();
|
||||
let mut cursor = self.snapshot.creases.cursor::<ItemSummary>();
|
||||
|
||||
for (id, range) in removals {
|
||||
new_flaps.append(cursor.slice(&range, Bias::Left, snapshot), snapshot);
|
||||
new_creases.append(cursor.slice(&range, Bias::Left, snapshot), snapshot);
|
||||
while let Some(item) = cursor.item() {
|
||||
cursor.next(snapshot);
|
||||
if item.id == id {
|
||||
break;
|
||||
} else {
|
||||
new_flaps.push(item.clone(), snapshot);
|
||||
new_creases.push(item.clone(), snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new_flaps.append(cursor.suffix(snapshot), snapshot);
|
||||
new_flaps
|
||||
new_creases.append(cursor.suffix(snapshot), snapshot);
|
||||
new_creases
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -227,17 +229,17 @@ impl sum_tree::Summary for ItemSummary {
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Item for FlapItem {
|
||||
impl sum_tree::Item for CreaseItem {
|
||||
type Summary = ItemSummary;
|
||||
|
||||
fn summary(&self) -> Self::Summary {
|
||||
ItemSummary {
|
||||
range: self.flap.range.clone(),
|
||||
range: self.crease.range.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements `SeekTarget` for `Range<Anchor>` to enable seeking within a `SumTree` of `FlapItem`s.
|
||||
/// Implements `SeekTarget` for `Range<Anchor>` to enable seeking within a `SumTree` of `CreaseItem`s.
|
||||
impl SeekTarget<'_, ItemSummary, ItemSummary> for Range<Anchor> {
|
||||
fn cmp(&self, cursor_location: &ItemSummary, snapshot: &MultiBufferSnapshot) -> Ordering {
|
||||
AnchorRangeExt::cmp(self, &cursor_location.range, snapshot)
|
||||
@@ -257,48 +259,48 @@ mod test {
|
||||
use multi_buffer::MultiBuffer;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_insert_and_remove_flaps(cx: &mut AppContext) {
|
||||
fn test_insert_and_remove_creases(cx: &mut AppContext) {
|
||||
let text = "line1\nline2\nline3\nline4\nline5";
|
||||
let buffer = MultiBuffer::build_simple(text, cx);
|
||||
let snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
|
||||
let mut flap_map = FlapMap::default();
|
||||
let mut crease_map = CreaseMap::default();
|
||||
|
||||
// Insert flaps
|
||||
let flaps = [
|
||||
Flap::new(
|
||||
// Insert creases
|
||||
let creases = [
|
||||
Crease::new(
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 5)),
|
||||
FoldPlaceholder::test(),
|
||||
|_row, _folded, _toggle, _cx| div(),
|
||||
|_row, _folded, _cx| div(),
|
||||
),
|
||||
Flap::new(
|
||||
Crease::new(
|
||||
snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_after(Point::new(3, 5)),
|
||||
FoldPlaceholder::test(),
|
||||
|_row, _folded, _toggle, _cx| div(),
|
||||
|_row, _folded, _cx| div(),
|
||||
),
|
||||
];
|
||||
let flap_ids = flap_map.insert(flaps, &snapshot);
|
||||
assert_eq!(flap_ids.len(), 2);
|
||||
let crease_ids = crease_map.insert(creases, &snapshot);
|
||||
assert_eq!(crease_ids.len(), 2);
|
||||
|
||||
// Verify flaps are inserted
|
||||
let flap_snapshot = flap_map.snapshot();
|
||||
assert!(flap_snapshot
|
||||
// Verify creases are inserted
|
||||
let crease_snapshot = crease_map.snapshot();
|
||||
assert!(crease_snapshot
|
||||
.query_row(MultiBufferRow(1), &snapshot)
|
||||
.is_some());
|
||||
assert!(flap_snapshot
|
||||
assert!(crease_snapshot
|
||||
.query_row(MultiBufferRow(3), &snapshot)
|
||||
.is_some());
|
||||
|
||||
// Remove flaps
|
||||
flap_map.remove(flap_ids, &snapshot);
|
||||
// Remove creases
|
||||
crease_map.remove(crease_ids, &snapshot);
|
||||
|
||||
// Verify flaps are removed
|
||||
let flap_snapshot = flap_map.snapshot();
|
||||
assert!(flap_snapshot
|
||||
// Verify creases are removed
|
||||
let crease_snapshot = crease_map.snapshot();
|
||||
assert!(crease_snapshot
|
||||
.query_row(MultiBufferRow(1), &snapshot)
|
||||
.is_none());
|
||||
assert!(flap_snapshot
|
||||
assert!(crease_snapshot
|
||||
.query_row(MultiBufferRow(3), &snapshot)
|
||||
.is_none());
|
||||
}
|
||||