Compare commits
189 Commits
lua-syntax
...
a-differen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a90f80725f | ||
|
|
4e6c37d23b | ||
|
|
0cf6259fec | ||
|
|
5cb5e92185 | ||
|
|
da61a28839 | ||
|
|
efdb769f9b | ||
|
|
9cce5a650e | ||
|
|
2021ca5bff | ||
|
|
1771250b04 | ||
|
|
18259c0fd4 | ||
|
|
41ddd1cc97 | ||
|
|
e175878008 | ||
|
|
1cfbfc199c | ||
|
|
f59f2caf7e | ||
|
|
401342c6ec | ||
|
|
0df1e4a489 | ||
|
|
9bd3e156f5 | ||
|
|
42c655751b | ||
|
|
ff1d78df3b | ||
|
|
c2e4fdf63d | ||
|
|
bf11b888c3 | ||
|
|
d562f58e76 | ||
|
|
94e4aa626d | ||
|
|
8ceba89d81 | ||
|
|
c37d6d5fed | ||
|
|
1a3597d726 | ||
|
|
c747cccde3 | ||
|
|
d81e7683ea | ||
|
|
8b29ee6033 | ||
|
|
96a75e08af | ||
|
|
06cbff6714 | ||
|
|
ce05813e7c | ||
|
|
4d1d8d6d78 | ||
|
|
1f8b14f4f1 | ||
|
|
082cc6184c | ||
|
|
6cfc4dc857 | ||
|
|
b9c48685e8 | ||
|
|
570c396e84 | ||
|
|
5fd034e604 | ||
|
|
63dab5f891 | ||
|
|
a2d6df3ed6 | ||
|
|
30e86ac939 | ||
|
|
976fc3ee97 | ||
|
|
63091459d8 | ||
|
|
659fae70f8 | ||
|
|
02e970192f | ||
|
|
5ecc67f2ef | ||
|
|
73dfb10c16 | ||
|
|
e513e81046 | ||
|
|
2fc4dec58f | ||
|
|
3891381d3e | ||
|
|
b91e929086 | ||
|
|
013a646799 | ||
|
|
ed52e759d7 | ||
|
|
6da099a9d7 | ||
|
|
5f159bc95e | ||
|
|
a4462577bf | ||
|
|
c147b58558 | ||
|
|
84fe1bfe9b | ||
|
|
657d7a911d | ||
|
|
ee05cc3ad9 | ||
|
|
5ed144f9d2 | ||
|
|
2a862b3c54 | ||
|
|
4a7c84f490 | ||
|
|
230e2e4107 | ||
|
|
d732b8ba0f | ||
|
|
7c3eecc9c7 | ||
|
|
fff37ab823 | ||
|
|
8a7a78fafb | ||
|
|
6de3ac3e17 | ||
|
|
5aae3bdc69 | ||
|
|
e298301b40 | ||
|
|
ed6bf7f161 | ||
|
|
f14d6670ba | ||
|
|
22d9b5d8ca | ||
|
|
6ed6e8bc26 | ||
|
|
4846e6fb3a | ||
|
|
cb543f9546 | ||
|
|
450d727a04 | ||
|
|
60b3eb3f76 | ||
|
|
bbe7c9a738 | ||
|
|
f6345a6995 | ||
|
|
e70d0edfac | ||
|
|
921c24e274 | ||
|
|
18f3f8097f | ||
|
|
4f6682c7fe | ||
|
|
f57dece2d5 | ||
|
|
103ad635d9 | ||
|
|
ec5e7a2653 | ||
|
|
05d3ee8555 | ||
|
|
1b34437839 | ||
|
|
3ff2c8fc38 | ||
|
|
b0b0b00fae | ||
|
|
80fb88520f | ||
|
|
aef84d453a | ||
|
|
e06d010aab | ||
|
|
14148f53d4 | ||
|
|
efde5aa2bb | ||
|
|
fcc5e27455 | ||
|
|
ed417da536 | ||
|
|
d1c67897c5 | ||
|
|
a887f3b340 | ||
|
|
f8deebc6db | ||
|
|
205f9a9f03 | ||
|
|
b0d1024f66 | ||
|
|
622ed8a032 | ||
|
|
09c51f9641 | ||
|
|
8422a81d88 | ||
|
|
6c025507b6 | ||
|
|
8f4b7aa5db | ||
|
|
3345666557 | ||
|
|
40c62cda5f | ||
|
|
349a48d937 | ||
|
|
a88af7351a | ||
|
|
efaf358876 | ||
|
|
06a226dc32 | ||
|
|
1763dd714b | ||
|
|
330e799293 | ||
|
|
51c900366d | ||
|
|
be75f17429 | ||
|
|
f373383fc1 | ||
|
|
263d9ff755 | ||
|
|
829ecda370 | ||
|
|
af5af9d7c5 | ||
|
|
97c0a0a86e | ||
|
|
7e964290bf | ||
|
|
b8a8b9c699 | ||
|
|
d1cec209d4 | ||
|
|
aceab76ae4 | ||
|
|
9c054f207e | ||
|
|
219d36f589 | ||
|
|
6fd9708eee | ||
|
|
99216acdec | ||
|
|
aef25a3bc3 | ||
|
|
9b07f36199 | ||
|
|
ff25fa24e7 | ||
|
|
05df3d1bd6 | ||
|
|
84f4d2630f | ||
|
|
b42930f5be | ||
|
|
cb2eef6fb6 | ||
|
|
73668c2c4b | ||
|
|
07f555ca3b | ||
|
|
b0e2b57462 | ||
|
|
d404d7964e | ||
|
|
69af9bedaf | ||
|
|
2ebbcf15ea | ||
|
|
631cab5e40 | ||
|
|
ba39a47c52 | ||
|
|
c34357e2ab | ||
|
|
6fdb666bb7 | ||
|
|
7a9e0b37ed | ||
|
|
d44ba92363 | ||
|
|
ca4d8fc900 | ||
|
|
352882af77 | ||
|
|
c5c4a6201b | ||
|
|
6327a5d665 | ||
|
|
57438d30e8 | ||
|
|
5c81dd7d39 | ||
|
|
aec4d5cb26 | ||
|
|
4c0750bd2f | ||
|
|
22b1a02e23 | ||
|
|
314ad5dd5f | ||
|
|
d3c68650c0 | ||
|
|
43339c6869 | ||
|
|
e505d6bf5b | ||
|
|
0200dda83d | ||
|
|
4db9ab15a7 | ||
|
|
5daadc0d30 | ||
|
|
431727fdd7 | ||
|
|
cee98f872a | ||
|
|
e99d68a66f | ||
|
|
6a3e8044b1 | ||
|
|
c2375a4164 | ||
|
|
9d54e63a11 | ||
|
|
2a919ad1d0 | ||
|
|
f13b2fd811 | ||
|
|
7c39153160 | ||
|
|
d0c2bef8c3 | ||
|
|
87b3fefdd1 | ||
|
|
66784c0b3f | ||
|
|
ad9c508a72 | ||
|
|
aaa506c061 | ||
|
|
a602c50a6c | ||
|
|
728c161e8d | ||
|
|
3975d8ea93 | ||
|
|
2d050a8130 | ||
|
|
e600e71c1c | ||
|
|
82d85fd2ed | ||
|
|
e061ebb46c |
@@ -26,3 +26,6 @@ rustflags = [
|
||||
"-C",
|
||||
"target-feature=+crt-static", # This fixes the linking issue when compiling livekit on Windows
|
||||
]
|
||||
|
||||
[env]
|
||||
MACOSX_DEPLOYMENT_TARGET = "10.15.7"
|
||||
|
||||
98
.github/workflows/ci.yml
vendored
98
.github/workflows/ci.yml
vendored
@@ -236,12 +236,24 @@ jobs:
|
||||
if: always()
|
||||
run: rm -rf ./../.cargo
|
||||
|
||||
windows_tests:
|
||||
windows_clippy:
|
||||
timeout-minutes: 60
|
||||
name: (Windows) Run Clippy and tests
|
||||
name: (Windows) Run Clippy
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: hosted-windows-2
|
||||
steps:
|
||||
# Temporarily Collect some metadata about the hardware behind our runners.
|
||||
- name: GHA Runner Info
|
||||
run: |
|
||||
Invoke-RestMethod -Headers @{"Metadata"="true"} -Method GET -Uri "http://169.254.169.254/metadata/instance/compute?api-version=2023-07-01" |
|
||||
ConvertTo-Json -Depth 10 |
|
||||
jq "{ vm_size: .vmSize, location: .location, os_disk_gb: (.storageProfile.osDisk.diskSizeGB | tonumber), rs_disk_gb: (.storageProfile.resourceDisk.size | tonumber / 1024) }"
|
||||
@{
|
||||
Cores = (Get-CimInstance Win32_Processor).NumberOfCores
|
||||
vCPUs = (Get-CimInstance Win32_Processor).NumberOfLogicalProcessors
|
||||
RamGb = [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 2)
|
||||
cpuid = (Get-CimInstance Win32_Processor).Name.Trim()
|
||||
} | ConvertTo-Json
|
||||
# more info here:- https://github.com/rust-lang/cargo/issues/13020
|
||||
- name: Enable longer pathnames for git
|
||||
run: git config --system core.longpaths true
|
||||
@@ -275,6 +287,69 @@ jobs:
|
||||
working-directory: ${{ env.ZED_WORKSPACE }}
|
||||
run: ./script/clippy.ps1
|
||||
|
||||
- name: Check dev drive space
|
||||
working-directory: ${{ env.ZED_WORKSPACE }}
|
||||
# `setup-dev-driver.ps1` creates a 100GB drive, with CI taking up ~45GB of the drive.
|
||||
run: ./script/exit-ci-if-dev-drive-is-full.ps1 95
|
||||
|
||||
# Since the Windows runners are stateful, so we need to remove the config file to prevent potential bug.
|
||||
- name: Clean CI config file
|
||||
if: always()
|
||||
run: |
|
||||
if (Test-Path "${{ env.CARGO_HOME }}/config.toml") {
|
||||
Remove-Item -Path "${{ env.CARGO_HOME }}/config.toml" -Force
|
||||
}
|
||||
|
||||
# Windows CI takes twice as long as our other platforms and fast github hosted runners are expensive.
|
||||
# But we still want to do CI, so let's only run tests on main and come back to this when we're
|
||||
# ready to self host our Windows CI (e.g. during the push for full Windows support)
|
||||
windows_tests:
|
||||
timeout-minutes: 60
|
||||
name: (Windows) Run Tests
|
||||
if: ${{ github.repository_owner == 'zed-industries' && (github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'windows')) }}
|
||||
runs-on: hosted-windows-2
|
||||
steps:
|
||||
# Temporarily Collect some metadata about the hardware behind our runners.
|
||||
- name: GHA Runner Info
|
||||
run: |
|
||||
Invoke-RestMethod -Headers @{"Metadata"="true"} -Method GET -Uri "http://169.254.169.254/metadata/instance/compute?api-version=2023-07-01" |
|
||||
ConvertTo-Json -Depth 10 |
|
||||
jq "{ vm_size: .vmSize, location: .location, os_disk_gb: (.storageProfile.osDisk.diskSizeGB | tonumber), rs_disk_gb: (.storageProfile.resourceDisk.size | tonumber / 1024) }"
|
||||
@{
|
||||
Cores = (Get-CimInstance Win32_Processor).NumberOfCores
|
||||
vCPUs = (Get-CimInstance Win32_Processor).NumberOfLogicalProcessors
|
||||
RamGb = [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 2)
|
||||
cpuid = (Get-CimInstance Win32_Processor).Name.Trim()
|
||||
} | ConvertTo-Json
|
||||
# more info here:- https://github.com/rust-lang/cargo/issues/13020
|
||||
- name: Enable longer pathnames for git
|
||||
run: git config --system core.longpaths true
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Create Dev Drive using ReFS
|
||||
run: ./script/setup-dev-driver.ps1
|
||||
|
||||
# actions/checkout does not let us clone into anywhere outside ${{ github.workspace }}, so we have to copy the clone...
|
||||
- name: Copy Git Repo to Dev Drive
|
||||
run: |
|
||||
Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.ZED_WORKSPACE }}" -Recurse
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
workspaces: ${{ env.ZED_WORKSPACE }}
|
||||
cache-provider: "github"
|
||||
|
||||
- name: Configure CI
|
||||
run: |
|
||||
mkdir -p ${{ env.CARGO_HOME }} -ErrorAction Ignore
|
||||
cp ./.cargo/ci-config.toml ${{ env.CARGO_HOME }}/config.toml
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests_windows
|
||||
with:
|
||||
@@ -292,7 +367,10 @@ jobs:
|
||||
# Since the Windows runners are stateful, so we need to remove the config file to prevent potential bug.
|
||||
- name: Clean CI config file
|
||||
if: always()
|
||||
run: Remove-Item -Path "${{ env.CARGO_HOME }}/config.toml" -Force
|
||||
run: |
|
||||
if (Test-Path "${{ env.CARGO_HOME }}/config.toml") {
|
||||
Remove-Item -Path "${{ env.CARGO_HOME }}/config.toml" -Force
|
||||
}
|
||||
|
||||
bundle-mac:
|
||||
timeout-minutes: 120
|
||||
@@ -422,6 +500,13 @@ jobs:
|
||||
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
|
||||
path: target/release/zed-*.tar.gz
|
||||
|
||||
- name: Upload Linux remote server to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.gz
|
||||
path: target/zed-remote-server-linux-x86_64.gz
|
||||
|
||||
- name: Upload app bundle to release
|
||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||
with:
|
||||
@@ -470,6 +555,13 @@ jobs:
|
||||
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz
|
||||
path: target/release/zed-*.tar.gz
|
||||
|
||||
- name: Upload Linux remote server to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.gz
|
||||
path: target/zed-remote-server-linux-aarch64.gz
|
||||
|
||||
- name: Upload app bundle to release
|
||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||
with:
|
||||
|
||||
446
Cargo.lock
generated
446
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
29
Cargo.toml
29
Cargo.toml
@@ -3,6 +3,7 @@ resolver = "2"
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/anthropic",
|
||||
"crates/askpass",
|
||||
"crates/assets",
|
||||
"crates/assistant",
|
||||
"crates/assistant2",
|
||||
@@ -168,22 +169,15 @@ members = [
|
||||
# Extensions
|
||||
#
|
||||
|
||||
"extensions/csharp",
|
||||
"extensions/deno",
|
||||
"extensions/elixir",
|
||||
"extensions/emmet",
|
||||
"extensions/erlang",
|
||||
"extensions/glsl",
|
||||
"extensions/haskell",
|
||||
"extensions/html",
|
||||
"extensions/lua",
|
||||
"extensions/perplexity",
|
||||
"extensions/proto",
|
||||
"extensions/purescript",
|
||||
"extensions/ruff",
|
||||
"extensions/slash-commands-example",
|
||||
"extensions/snippets",
|
||||
"extensions/terraform",
|
||||
"extensions/test-extension",
|
||||
"extensions/toml",
|
||||
"extensions/uiua",
|
||||
@@ -210,6 +204,7 @@ edition = "2021"
|
||||
activity_indicator = { path = "crates/activity_indicator" }
|
||||
ai = { path = "crates/ai" }
|
||||
anthropic = { path = "crates/anthropic" }
|
||||
askpass = { path = "crates/askpass" }
|
||||
assets = { path = "crates/assets" }
|
||||
assistant = { path = "crates/assistant" }
|
||||
assistant2 = { path = "crates/assistant2" }
|
||||
@@ -374,7 +369,7 @@ zeta = { path = "crates/zeta" }
|
||||
#
|
||||
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", rev = "03c2907b44b4189aac5fdeaea331f5aab5c7072e" }
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||
any_vec = "0.14"
|
||||
anyhow = "1.0.86"
|
||||
arrayvec = { version = "0.7.4", features = ["serde"] }
|
||||
@@ -455,7 +450,7 @@ livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "
|
||||
], default-features = false }
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
mlua = { version = "0.10", features = ["lua54", "vendored"] }
|
||||
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
|
||||
nanoid = "0.4"
|
||||
nbformat = { version = "0.10.0" }
|
||||
nix = "0.29"
|
||||
@@ -541,7 +536,7 @@ tiny_http = "0.8"
|
||||
toml = "0.8"
|
||||
tokio = { version = "1" }
|
||||
tower-http = "0.4.4"
|
||||
tree-sitter = { version = "0.25.2", features = ["wasm"] }
|
||||
tree-sitter = { version = "0.25.3", features = ["wasm"] }
|
||||
tree-sitter-bash = "0.23"
|
||||
tree-sitter-c = "0.23"
|
||||
tree-sitter-cpp = "0.23"
|
||||
@@ -601,12 +596,14 @@ features = [
|
||||
]
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.58"
|
||||
version = "0.60"
|
||||
features = [
|
||||
"implement",
|
||||
"Foundation_Collections",
|
||||
"Foundation_Numerics",
|
||||
"Storage",
|
||||
"Storage_Search",
|
||||
"Storage_Streams",
|
||||
"System_Threading",
|
||||
"UI_StartScreen",
|
||||
"UI_ViewManagement",
|
||||
"Wdk_System_SystemServices",
|
||||
"Win32_Globalization",
|
||||
@@ -625,9 +622,11 @@ features = [
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
"Win32_System_Console",
|
||||
"Win32_System_DataExchange",
|
||||
"Win32_System_IO",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Memory",
|
||||
"Win32_System_Ole",
|
||||
"Win32_System_Pipes",
|
||||
"Win32_System_SystemInformation",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_Threading",
|
||||
@@ -753,5 +752,9 @@ new_ret_no_self = { level = "allow" }
|
||||
should_implement_trait = { level = "allow" }
|
||||
let_underscore_future = "allow"
|
||||
|
||||
# in Rust it can be very tedious to reduce argument count without
|
||||
# running afoul of the borrow checker.
|
||||
too_many_arguments = "allow"
|
||||
|
||||
[workspace.metadata.cargo-machete]
|
||||
ignored = ["bindgen", "cbindgen", "prost_build", "serde", "component", "linkme"]
|
||||
|
||||
10
assets/icons/ai_edit.svg
Normal file
10
assets/icons/ai_edit.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5871 5.40624C12.8514 5.14195 13 4.78346 13 4.40965C13 4.03583 12.8516 3.67731 12.5873 3.41295C12.323 3.14859 11.9645 3.00005 11.5907 3C11.2169 2.99995 10.8584 3.14841 10.594 3.4127L3.92098 10.0874C3.80488 10.2031 3.71903 10.3456 3.67097 10.5024L3.01047 12.6784C2.99754 12.7217 2.99657 12.7676 3.00764 12.8113C3.01872 12.8551 3.04143 12.895 3.07337 12.9269C3.1053 12.9588 3.14528 12.9815 3.18905 12.9925C3.23282 13.0035 3.27875 13.0024 3.32197 12.9894L5.49849 12.3294C5.65508 12.2818 5.79758 12.1964 5.91349 12.0809L12.5871 5.40624Z" stroke="black" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 4L12 6" stroke="black" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.38818 3.53598V2.53598" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.56982 12.6995L9.56982 13.6995" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.38818 6.53598H3.38818" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.5698 9.69949L12.5698 9.69949" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.38818 4.53598L3.38818 3.53598" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.5698 11.6995L12.5698 12.6995" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
3
assets/icons/file_icons/luau.svg
Normal file
3
assets/icons/file_icons/luau.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.36197 1.67985C5.3748 1.41534 4.36011 2.00117 4.0956 2.98834L2.17985 10.138C1.91534 11.1252 2.50117 12.1399 3.48833 12.4044L10.638 14.3202C11.6252 14.5847 12.6399 13.9988 12.9044 13.0117L14.8202 5.86197C15.0847 4.8748 14.4988 3.86012 13.5117 3.59561L6.36197 1.67985ZM10.0457 4.58266C9.77896 4.51119 9.50479 4.66948 9.43332 4.93621L8.76235 7.44028C8.69088 7.70701 8.84917 7.98118 9.11591 8.05265L11.62 8.72362C11.8867 8.79509 12.1609 8.6368 12.2324 8.37006L12.9033 5.86599C12.9748 5.59926 12.8165 5.32509 12.5498 5.25362L10.0457 4.58266Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 707 B |
7
assets/icons/file_icons/wgsl.svg
Normal file
7
assets/icons/file_icons/wgsl.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.5 13L1.5 5H11.5L6.5 13Z" fill="black"/>
|
||||
<path d="M14 9H9L11.5 5L14 9Z" fill="black" fill-opacity="0.75"/>
|
||||
<path d="M9 9L14 9L11.5 13L9 9Z" fill="black" fill-opacity="0.65"/>
|
||||
<path d="M14 5L15.25 7L12.75 7L14 5Z" fill="black" fill-opacity="0.5"/>
|
||||
<path d="M14 9L12.75 7H15.25L14 9Z" fill="black" fill-opacity="0.55"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 432 B |
@@ -475,9 +475,7 @@
|
||||
"ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
|
||||
"ctrl-alt-d": "editor::DeleteToNextSubwordEnd",
|
||||
"ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
|
||||
// "ctrl-alt-b": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-alt-right": "editor::MoveToNextSubwordEnd",
|
||||
"ctrl-alt-f": "editor::MoveToNextSubwordEnd",
|
||||
"ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart",
|
||||
"ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart",
|
||||
"ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd",
|
||||
@@ -733,27 +731,54 @@
|
||||
"up": "menu::SelectPrevious",
|
||||
"down": "menu::SelectNext",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-y": "git::StageFile",
|
||||
"alt-shift-y": "git::UnstageFile",
|
||||
"ctrl-alt-y": "git::ToggleStaged",
|
||||
"space": "git::ToggleStaged",
|
||||
"ctrl-space": "git::StageAll",
|
||||
"ctrl-shift-space": "git::UnstageAll",
|
||||
"tab": "git_panel::FocusEditor",
|
||||
"shift-tab": "git_panel::FocusEditor",
|
||||
"escape": "git_panel::ToggleFocus",
|
||||
"ctrl-enter": "git::ShowCommitEditor",
|
||||
"alt-enter": "menu::SecondaryConfirm"
|
||||
"ctrl-enter": "git::Commit",
|
||||
"alt-enter": "menu::SecondaryConfirm",
|
||||
"backspace": "git::RestoreFile"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitCommit > Editor",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-enter": "git::Commit"
|
||||
"ctrl-enter": "git::Commit",
|
||||
"alt-l": "git::GenerateCommitMessage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-g ctrl-g": "git::Fetch",
|
||||
"ctrl-g up": "git::Push",
|
||||
"ctrl-g down": "git::Pull",
|
||||
"ctrl-g shift-up": "git::ForcePush",
|
||||
"ctrl-g d": "git::Diff",
|
||||
"ctrl-g backspace": "git::RestoreTrackedFiles",
|
||||
"ctrl-g shift-backspace": "git::TrashUntrackedFiles",
|
||||
"ctrl-space": "git::StageAll",
|
||||
"ctrl-shift-space": "git::UnstageAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitDiff > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "git::ShowCommitEditor"
|
||||
"ctrl-enter": "git::Commit",
|
||||
"ctrl-space": "git::StageAll",
|
||||
"ctrl-shift-space": "git::UnstageAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AskPass > Editor",
|
||||
"bindings": {
|
||||
"enter": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -762,8 +787,10 @@
|
||||
"escape": "git_panel::FocusChanges",
|
||||
"tab": "git_panel::FocusChanges",
|
||||
"shift-tab": "git_panel::FocusChanges",
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-enter": "git::Commit",
|
||||
"alt-up": "git_panel::FocusChanges"
|
||||
"alt-up": "git_panel::FocusChanges",
|
||||
"alt-l": "git::GenerateCommitMessage"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -834,21 +861,22 @@
|
||||
"alt-b": ["terminal::SendText", "\u001bb"],
|
||||
"alt-f": ["terminal::SendText", "\u001bf"],
|
||||
// Overrides for conflicting keybindings
|
||||
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
|
||||
"ctrl-e": ["terminal::SendKeystroke", "ctrl-e"],
|
||||
"ctrl-o": ["terminal::SendKeystroke", "ctrl-o"],
|
||||
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
|
||||
"ctrl-shift-a": "editor::SelectAll",
|
||||
"find": "buffer_search::Deploy",
|
||||
"ctrl-shift-f": "buffer_search::Deploy",
|
||||
"ctrl-shift-l": "terminal::Clear",
|
||||
"ctrl-shift-w": "pane::CloseActiveItem",
|
||||
"ctrl-e": ["terminal::SendKeystroke", "ctrl-e"],
|
||||
"up": ["terminal::SendKeystroke", "up"],
|
||||
"pageup": ["terminal::SendKeystroke", "pageup"],
|
||||
"down": ["terminal::SendKeystroke", "down"],
|
||||
"pagedown": ["terminal::SendKeystroke", "pagedown"],
|
||||
"escape": ["terminal::SendKeystroke", "escape"],
|
||||
"enter": ["terminal::SendKeystroke", "enter"],
|
||||
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
|
||||
"shift-pageup": "terminal::ScrollPageUp",
|
||||
"shift-pagedown": "terminal::ScrollPageDown",
|
||||
"shift-up": "terminal::ScrollLineUp",
|
||||
|
||||
@@ -31,13 +31,13 @@
|
||||
"enter": "menu::Confirm",
|
||||
"ctrl-enter": "menu::SecondaryConfirm",
|
||||
"cmd-enter": "menu::SecondaryConfirm",
|
||||
"cmd-escape": "menu::Cancel",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"escape": "menu::Cancel",
|
||||
"alt-shift-enter": "menu::Restart",
|
||||
"cmd-shift-w": "workspace::CloseWindow",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"cmd-escape": "menu::Cancel",
|
||||
"cmd-o": "workspace::Open",
|
||||
"cmd-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
|
||||
"cmd-+": ["zed::IncreaseBufferFontSize", { "persist": false }],
|
||||
@@ -108,8 +108,8 @@
|
||||
"cmd-right": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
|
||||
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
"cmd-up": "editor::MoveToStartOfExcerpt",
|
||||
"cmd-down": "editor::MoveToEndOfExcerpt",
|
||||
"cmd-up": "editor::MoveToBeginning",
|
||||
"cmd-down": "editor::MoveToEnd",
|
||||
"cmd-home": "editor::MoveToBeginning", // Typed via `cmd-fn-left`
|
||||
"cmd-end": "editor::MoveToEnd", // Typed via `cmd-fn-right`
|
||||
"shift-up": "editor::SelectUp",
|
||||
@@ -124,8 +124,8 @@
|
||||
"alt-shift-right": "editor::SelectToNextWordEnd", // cursorWordRightSelect
|
||||
"ctrl-shift-up": "editor::SelectToStartOfParagraph",
|
||||
"ctrl-shift-down": "editor::SelectToEndOfParagraph",
|
||||
"cmd-shift-up": "editor::SelectToStartOfExcerpt",
|
||||
"cmd-shift-down": "editor::SelectToEndOfExcerpt",
|
||||
"cmd-shift-up": "editor::SelectToBeginning",
|
||||
"cmd-shift-down": "editor::SelectToEnd",
|
||||
"cmd-a": "editor::SelectAll",
|
||||
"cmd-l": "editor::SelectLine",
|
||||
"cmd-shift-i": "editor::Format",
|
||||
@@ -172,6 +172,16 @@
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && multibuffer",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-up": "editor::MoveToStartOfExcerpt",
|
||||
"cmd-down": "editor::MoveToStartOfNextExcerpt",
|
||||
"cmd-shift-up": "editor::SelectToStartOfExcerpt",
|
||||
"cmd-shift-down": "editor::SelectToStartOfNextExcerpt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full && edit_prediction",
|
||||
"use_key_equivalents": true,
|
||||
@@ -753,21 +763,25 @@
|
||||
"cmd-up": "menu::SelectFirst",
|
||||
"cmd-down": "menu::SelectLast",
|
||||
"enter": "menu::Confirm",
|
||||
"cmd-alt-y": "git::ToggleStaged",
|
||||
"space": "git::ToggleStaged",
|
||||
"cmd-shift-space": "git::StageAll",
|
||||
"ctrl-shift-space": "git::UnstageAll",
|
||||
"cmd-y": "git::StageFile",
|
||||
"cmd-shift-y": "git::UnstageFile",
|
||||
"alt-down": "git_panel::FocusEditor",
|
||||
"tab": "git_panel::FocusEditor",
|
||||
"shift-tab": "git_panel::FocusEditor",
|
||||
"escape": "git_panel::ToggleFocus",
|
||||
"cmd-enter": "git::ShowCommitEditor"
|
||||
"cmd-enter": "git::Commit",
|
||||
"backspace": "git::RestoreFile"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitDiff > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "git::ShowCommitEditor"
|
||||
"cmd-enter": "git::Commit",
|
||||
"cmd-ctrl-y": "git::StageAll",
|
||||
"cmd-ctrl-shift-y": "git::UnstageAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -778,7 +792,24 @@
|
||||
"cmd-enter": "git::Commit",
|
||||
"tab": "git_panel::FocusChanges",
|
||||
"shift-tab": "git_panel::FocusChanges",
|
||||
"alt-up": "git_panel::FocusChanges"
|
||||
"alt-up": "git_panel::FocusChanges",
|
||||
"shift-escape": "git::ExpandCommitEditor",
|
||||
"alt-tab": "git::GenerateCommitMessage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-g ctrl-g": "git::Fetch",
|
||||
"ctrl-g up": "git::Push",
|
||||
"ctrl-g down": "git::Pull",
|
||||
"ctrl-g shift-up": "git::ForcePush",
|
||||
"ctrl-g d": "git::Diff",
|
||||
"ctrl-g backspace": "git::RestoreTrackedFiles",
|
||||
"ctrl-g shift-backspace": "git::TrashUntrackedFiles",
|
||||
"cmd-ctrl-y": "git::StageAll",
|
||||
"cmd-ctrl-shift-y": "git::UnstageAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -786,7 +817,9 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::Newline",
|
||||
"cmd-enter": "git::Commit"
|
||||
"escape": "menu::Cancel",
|
||||
"cmd-enter": "git::Commit",
|
||||
"alt-tab": "git::GenerateCommitMessage"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"a": ["vim::PushObject", { "around": true }],
|
||||
"left": "vim::Left",
|
||||
"h": "vim::Left",
|
||||
"backspace": "vim::Backspace",
|
||||
"backspace": "vim::WrappingLeft",
|
||||
"down": "vim::Down",
|
||||
"ctrl-j": "vim::Down",
|
||||
"j": "vim::Down",
|
||||
@@ -20,7 +20,7 @@
|
||||
"k": "vim::Up",
|
||||
"right": "vim::Right",
|
||||
"l": "vim::Right",
|
||||
"space": "vim::Space",
|
||||
"space": "vim::WrappingRight",
|
||||
"end": "vim::EndOfLine",
|
||||
"$": "vim::EndOfLine",
|
||||
"^": "vim::FirstNonWhitespace",
|
||||
@@ -247,7 +247,8 @@
|
||||
"context": "VimControl && VimCount",
|
||||
"bindings": {
|
||||
"0": ["vim::Number", 0],
|
||||
":": "vim::CountCommand"
|
||||
":": "vim::CountCommand",
|
||||
"%": "vim::GoToPercentage"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -720,8 +720,8 @@
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
// Whether to start a new line with a comment when a previous line is a comment as well.
|
||||
"extend_comment_on_newline": true,
|
||||
// Whether or not to ensure there's a single newline at the end of a buffer
|
||||
// when saving it.
|
||||
// Removes any lines containing only whitespace at the end of the file and
|
||||
// ensures just one newline at the end.
|
||||
"ensure_final_newline_on_save": true,
|
||||
// Whether or not to perform a buffer format before saving
|
||||
//
|
||||
@@ -845,7 +845,7 @@
|
||||
// "hunk_style": "transparent"
|
||||
// 2. Show unstaged hunks with a pattern background:
|
||||
// "hunk_style": "pattern"
|
||||
"hunk_style": "transparent"
|
||||
"hunk_style": "staged_border"
|
||||
},
|
||||
// Configuration for how direnv configuration should be loaded. May take 2 values:
|
||||
// 1. Load direnv configuration using `direnv export json` directly.
|
||||
@@ -1055,7 +1055,6 @@
|
||||
// }
|
||||
//
|
||||
"file_types": {
|
||||
"Plain Text": ["txt"],
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json"],
|
||||
"Shell Script": [".env.*"]
|
||||
},
|
||||
@@ -1175,6 +1174,7 @@
|
||||
"format_on_save": "off",
|
||||
"use_on_type_format": false,
|
||||
"allow_rewrap": "anywhere",
|
||||
"soft_wrap": "bounded",
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
@@ -1298,6 +1298,10 @@
|
||||
// "semi": false,
|
||||
// "singleQuote": true
|
||||
},
|
||||
// Settings for auto-closing of JSX tags.
|
||||
"jsx_tag_auto_close": {
|
||||
"enabled": true
|
||||
},
|
||||
// LSP Specific settings.
|
||||
"lsp": {
|
||||
// Specify the LSP name as a key here.
|
||||
|
||||
@@ -383,6 +383,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variable.special": {
|
||||
"color": "#83a598ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variant": {
|
||||
"color": "#83a598ff",
|
||||
"font_style": null,
|
||||
@@ -771,6 +776,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variable.special": {
|
||||
"color": "#83a598ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variant": {
|
||||
"color": "#83a598ff",
|
||||
"font_style": null,
|
||||
@@ -1159,6 +1169,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variable.special": {
|
||||
"color": "#83a598ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variant": {
|
||||
"color": "#83a598ff",
|
||||
"font_style": null,
|
||||
@@ -1547,6 +1562,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variable.special": {
|
||||
"color": "#066578ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variant": {
|
||||
"color": "#0b6678ff",
|
||||
"font_style": null,
|
||||
@@ -1935,6 +1955,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variable.special": {
|
||||
"color": "#066578ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variant": {
|
||||
"color": "#0b6678ff",
|
||||
"font_style": null,
|
||||
@@ -2323,6 +2348,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variable.special": {
|
||||
"color": "#066578ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"variant": {
|
||||
"color": "#0b6678ff",
|
||||
"font_style": null,
|
||||
|
||||
@@ -9,7 +9,10 @@ use gpui::{
|
||||
};
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId};
|
||||
use lsp::LanguageServerName;
|
||||
use project::{EnvironmentErrorMessage, LanguageServerProgress, Project, WorktreeId};
|
||||
use project::{
|
||||
EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
|
||||
ProjectEnvironmentEvent, WorktreeId,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
|
||||
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
|
||||
@@ -73,7 +76,22 @@ impl ActivityIndicator {
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.observe(&project, |_, _, cx| cx.notify()).detach();
|
||||
cx.subscribe(
|
||||
&project.read(cx).lsp_store(),
|
||||
|_, _, event, cx| match event {
|
||||
LspStoreEvent::LanguageServerUpdate { .. } => cx.notify(),
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
cx.subscribe(
|
||||
&project.read(cx).environment().clone(),
|
||||
|_, _, event, cx| match event {
|
||||
ProjectEnvironmentEvent::ErrorsUpdated => cx.notify(),
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
if let Some(auto_updater) = auto_updater.as_ref() {
|
||||
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
|
||||
@@ -204,7 +222,7 @@ impl ActivityIndicator {
|
||||
message: error.0.clone(),
|
||||
on_click: Some(Arc::new(move |this, window, cx| {
|
||||
this.project.update(cx, |project, cx| {
|
||||
project.remove_environment_error(cx, worktree_id);
|
||||
project.remove_environment_error(worktree_id, cx);
|
||||
});
|
||||
window.dispatch_action(Box::new(workspace::OpenLog), cx);
|
||||
})),
|
||||
|
||||
21
crates/askpass/Cargo.toml
Normal file
21
crates/askpass/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "askpass"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/askpass.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
smol.workspace = true
|
||||
tempfile.workspace = true
|
||||
util.workspace = true
|
||||
which.workspace = true
|
||||
194
crates/askpass/src/askpass.rs
Normal file
194
crates/askpass/src/askpass.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(unix)]
|
||||
use anyhow::Context as _;
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
#[cfg(unix)]
|
||||
use futures::{io::BufReader, AsyncBufReadExt as _};
|
||||
#[cfg(unix)]
|
||||
use futures::{select_biased, AsyncWriteExt as _, FutureExt as _};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use gpui::{AsyncApp, BackgroundExecutor, Task};
|
||||
#[cfg(unix)]
|
||||
use smol::fs;
|
||||
#[cfg(unix)]
|
||||
use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener};
|
||||
#[cfg(unix)]
|
||||
use util::ResultExt as _;
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum AskPassResult {
|
||||
CancelledByUser,
|
||||
Timedout,
|
||||
}
|
||||
|
||||
pub struct AskPassDelegate {
|
||||
tx: mpsc::UnboundedSender<(String, oneshot::Sender<String>)>,
|
||||
_task: Task<()>,
|
||||
}
|
||||
|
||||
impl AskPassDelegate {
|
||||
pub fn new(
|
||||
cx: &mut AsyncApp,
|
||||
password_prompt: impl Fn(String, oneshot::Sender<String>, &mut AsyncApp) + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
let (tx, mut rx) = mpsc::unbounded::<(String, oneshot::Sender<String>)>();
|
||||
let task = cx.spawn(|mut cx| async move {
|
||||
while let Some((prompt, channel)) = rx.next().await {
|
||||
password_prompt(prompt, channel, &mut cx);
|
||||
}
|
||||
});
|
||||
Self { tx, _task: task }
|
||||
}
|
||||
|
||||
pub async fn ask_password(&mut self, prompt: String) -> anyhow::Result<String> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.tx.send((prompt, tx)).await?;
|
||||
Ok(rx.await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub struct AskPassSession {
|
||||
script_path: PathBuf,
|
||||
_askpass_task: Task<()>,
|
||||
askpass_opened_rx: Option<oneshot::Receiver<()>>,
|
||||
askpass_kill_master_rx: Option<oneshot::Receiver<()>>,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl AskPassSession {
|
||||
/// This will create a new AskPassSession.
|
||||
/// You must retain this session until the master process exits.
|
||||
#[must_use]
|
||||
pub async fn new(
|
||||
executor: &BackgroundExecutor,
|
||||
mut delegate: AskPassDelegate,
|
||||
) -> anyhow::Result<Self> {
|
||||
let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?;
|
||||
let askpass_socket = temp_dir.path().join("askpass.sock");
|
||||
let askpass_script_path = temp_dir.path().join("askpass.sh");
|
||||
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
|
||||
let listener =
|
||||
UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?;
|
||||
|
||||
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
|
||||
let mut kill_tx = Some(askpass_kill_master_tx);
|
||||
|
||||
let askpass_task = executor.spawn(async move {
|
||||
let mut askpass_opened_tx = Some(askpass_opened_tx);
|
||||
|
||||
while let Ok((mut stream, _)) = listener.accept().await {
|
||||
if let Some(askpass_opened_tx) = askpass_opened_tx.take() {
|
||||
askpass_opened_tx.send(()).ok();
|
||||
}
|
||||
let mut buffer = Vec::new();
|
||||
let mut reader = BufReader::new(&mut stream);
|
||||
if reader.read_until(b'\0', &mut buffer).await.is_err() {
|
||||
buffer.clear();
|
||||
}
|
||||
let prompt = String::from_utf8_lossy(&buffer);
|
||||
if let Some(password) = delegate
|
||||
.ask_password(prompt.to_string())
|
||||
.await
|
||||
.context("failed to get askpass password")
|
||||
.log_err()
|
||||
{
|
||||
stream.write_all(password.as_bytes()).await.log_err();
|
||||
} else {
|
||||
if let Some(kill_tx) = kill_tx.take() {
|
||||
kill_tx.send(()).log_err();
|
||||
}
|
||||
// note: we expect the caller to drop this task when it's done.
|
||||
// We need to keep the stream open until the caller is done to avoid
|
||||
// spurious errors from ssh.
|
||||
std::future::pending::<()>().await;
|
||||
drop(stream);
|
||||
}
|
||||
}
|
||||
drop(temp_dir)
|
||||
});
|
||||
|
||||
anyhow::ensure!(
|
||||
which::which("nc").is_ok(),
|
||||
"Cannot find `nc` command (netcat), which is required to connect over SSH."
|
||||
);
|
||||
|
||||
// Create an askpass script that communicates back to this process.
|
||||
let askpass_script = format!(
|
||||
"{shebang}\n{print_args} | {nc} -U {askpass_socket} 2> /dev/null \n",
|
||||
// on macOS `brew install netcat` provides the GNU netcat implementation
|
||||
// which does not support -U.
|
||||
nc = if cfg!(target_os = "macos") {
|
||||
"/usr/bin/nc"
|
||||
} else {
|
||||
"nc"
|
||||
},
|
||||
askpass_socket = askpass_socket.display(),
|
||||
print_args = "printf '%s\\0' \"$@\"",
|
||||
shebang = "#!/bin/sh",
|
||||
);
|
||||
fs::write(&askpass_script_path, askpass_script).await?;
|
||||
fs::set_permissions(&askpass_script_path, std::fs::Permissions::from_mode(0o755)).await?;
|
||||
|
||||
Ok(Self {
|
||||
script_path: askpass_script_path,
|
||||
_askpass_task: askpass_task,
|
||||
askpass_kill_master_rx: Some(askpass_kill_master_rx),
|
||||
askpass_opened_rx: Some(askpass_opened_rx),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn script_path(&self) -> &Path {
|
||||
&self.script_path
|
||||
}
|
||||
|
||||
// This will run the askpass task forever, resolving as many authentication requests as needed.
|
||||
// The caller is responsible for examining the result of their own commands and cancelling this
|
||||
// future when this is no longer needed. Note that this can only be called once, but due to the
|
||||
// drop order this takes an &mut, so you can `drop()` it after you're done with the master process.
|
||||
pub async fn run(&mut self) -> AskPassResult {
|
||||
let connection_timeout = Duration::from_secs(10);
|
||||
let askpass_opened_rx = self.askpass_opened_rx.take().expect("Only call run once");
|
||||
let askpass_kill_master_rx = self
|
||||
.askpass_kill_master_rx
|
||||
.take()
|
||||
.expect("Only call run once");
|
||||
|
||||
select_biased! {
|
||||
_ = askpass_opened_rx.fuse() => {
|
||||
// Note: this await can only resolve after we are dropped.
|
||||
askpass_kill_master_rx.await.ok();
|
||||
return AskPassResult::CancelledByUser
|
||||
}
|
||||
|
||||
_ = futures::FutureExt::fuse(smol::Timer::after(connection_timeout)) => {
|
||||
return AskPassResult::Timedout
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub struct AskPassSession {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
impl AskPassSession {
|
||||
pub async fn new(_: &BackgroundExecutor, _: AskPassDelegate) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
path: PathBuf::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn script_path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> AskPassResult {
|
||||
futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(10))).await;
|
||||
AskPassResult::Timedout
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,7 @@ impl ConfigurationView {
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.when(configuration_view.is_none(), |this| {
|
||||
this.child(div().child(Label::new(format!(
|
||||
"No configuration view for {}",
|
||||
|
||||
@@ -35,10 +35,10 @@ use language_model::{
|
||||
report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelTextStream, Role,
|
||||
};
|
||||
use language_model_selector::inline_language_model_selector;
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::{CodeAction, ProjectTransaction};
|
||||
use project::{CodeAction, LspAction, ProjectTransaction};
|
||||
use prompt_store::PromptBuilder;
|
||||
use rope::Rope;
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
@@ -386,7 +386,6 @@ impl InlineAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn suggest_assist(
|
||||
&mut self,
|
||||
editor: &Entity<Editor>,
|
||||
@@ -1425,6 +1424,7 @@ enum PromptEditorEvent {
|
||||
struct PromptEditor {
|
||||
id: InlineAssistId,
|
||||
editor: Entity<Editor>,
|
||||
language_model_selector: Entity<LanguageModelSelector>,
|
||||
edited_since_done: bool,
|
||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
||||
prompt_history: VecDeque<String>,
|
||||
@@ -1438,7 +1438,6 @@ struct PromptEditor {
|
||||
_token_count_subscriptions: Vec<Subscription>,
|
||||
workspace: Option<WeakEntity<Workspace>>,
|
||||
show_rate_limit_notice: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
@@ -1567,7 +1566,6 @@ impl Render for PromptEditor {
|
||||
]
|
||||
}
|
||||
});
|
||||
let fs_clone = self.fs.clone();
|
||||
|
||||
h_flex()
|
||||
.key_context("PromptEditor")
|
||||
@@ -1590,13 +1588,29 @@ impl Render for PromptEditor {
|
||||
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(inline_language_model_selector(move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs_clone.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
}))
|
||||
.child(LanguageModelSelectorPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
IconButton::new("context", IconName::SettingsAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted),
|
||||
move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"Using {}",
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.active_model()
|
||||
.map(|model| model.name().0)
|
||||
.unwrap_or_else(|| "No model selected".into()),
|
||||
),
|
||||
None,
|
||||
"Change Model",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
gpui::Corner::TopRight,
|
||||
))
|
||||
.map(|el| {
|
||||
let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else {
|
||||
return el;
|
||||
@@ -1659,7 +1673,6 @@ impl Focusable for PromptEditor {
|
||||
impl PromptEditor {
|
||||
const MAX_LINES: u8 = 8;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
id: InlineAssistId,
|
||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
||||
@@ -1709,8 +1722,21 @@ impl PromptEditor {
|
||||
|
||||
let mut this = Self {
|
||||
id,
|
||||
fs,
|
||||
editor: prompt_editor,
|
||||
language_model_selector: cx.new(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
edited_since_done: false,
|
||||
gutter_dimensions,
|
||||
prompt_history,
|
||||
@@ -2305,7 +2331,6 @@ struct InlineAssist {
|
||||
}
|
||||
|
||||
impl InlineAssist {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
assist_id: InlineAssistId,
|
||||
group_id: InlineAssistGroupId,
|
||||
@@ -3541,10 +3566,10 @@ impl CodeActionProvider for AssistantCodeActionProvider {
|
||||
Task::ready(Ok(vec![CodeAction {
|
||||
server_id: language::LanguageServerId(0),
|
||||
range: snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end),
|
||||
lsp_action: lsp::CodeAction {
|
||||
lsp_action: LspAction::Action(Box::new(lsp::CodeAction {
|
||||
title: "Fix with Assistant".into(),
|
||||
..Default::default()
|
||||
},
|
||||
})),
|
||||
}]))
|
||||
} else {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
|
||||
@@ -19,7 +19,7 @@ use language_model::{
|
||||
report_assistant_event, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, Role,
|
||||
};
|
||||
use language_model_selector::inline_language_model_selector;
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use prompt_store::PromptBuilder;
|
||||
use settings::{update_settings_file, Settings};
|
||||
use std::{
|
||||
@@ -487,9 +487,9 @@ enum PromptEditorEvent {
|
||||
|
||||
struct PromptEditor {
|
||||
id: TerminalInlineAssistId,
|
||||
fs: Arc<dyn Fs>,
|
||||
height_in_lines: u8,
|
||||
editor: Entity<Editor>,
|
||||
language_model_selector: Entity<LanguageModelSelector>,
|
||||
edited_since_done: bool,
|
||||
prompt_history: VecDeque<String>,
|
||||
prompt_history_ix: Option<usize>,
|
||||
@@ -624,8 +624,6 @@ impl Render for PromptEditor {
|
||||
}
|
||||
};
|
||||
|
||||
let fs_clone = self.fs.clone();
|
||||
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_y_1()
|
||||
@@ -643,13 +641,29 @@ impl Render for PromptEditor {
|
||||
.w_12()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(inline_language_model_selector(move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs_clone.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
}))
|
||||
.child(LanguageModelSelectorPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
IconButton::new("change-model", IconName::SettingsAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted),
|
||||
move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"Using {}",
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.active_model()
|
||||
.map(|model| model.name().0)
|
||||
.unwrap_or_else(|| "No model selected".into()),
|
||||
),
|
||||
None,
|
||||
"Change Model",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
gpui::Corner::TopRight,
|
||||
))
|
||||
.children(
|
||||
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
|
||||
let error_message = SharedString::from(error.to_string());
|
||||
@@ -688,7 +702,6 @@ impl Focusable for PromptEditor {
|
||||
impl PromptEditor {
|
||||
const MAX_LINES: u8 = 8;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
id: TerminalInlineAssistId,
|
||||
prompt_history: VecDeque<String>,
|
||||
@@ -727,9 +740,22 @@ impl PromptEditor {
|
||||
|
||||
let mut this = Self {
|
||||
id,
|
||||
fs,
|
||||
height_in_lines: 1,
|
||||
editor: prompt_editor,
|
||||
language_model_selector: cx.new(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
edited_since_done: false,
|
||||
prompt_history,
|
||||
prompt_history_ix: None,
|
||||
|
||||
@@ -59,6 +59,7 @@ prompt_library.workspace = true
|
||||
prompt_store.workspace = true
|
||||
proto.workspace = true
|
||||
rope.workspace = true
|
||||
scripting_tool.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
@@ -81,8 +82,8 @@ zed_actions.workspace = true
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, "features" = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
language = { workspace = true, "features" = ["test-support"] }
|
||||
language_model = { workspace = true, "features" = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
indoc.workspace = true
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
use assistant_tool::{ToolRegistry, ToolWorkingSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use collections::HashMap;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{
|
||||
list, AbsoluteLength, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
|
||||
Entity, Focusable, Length, ListAlignment, ListOffset, ListState, StyleRefinement, Subscription,
|
||||
Task, TextStyleRefinement, UnderlineStyle, WeakEntity,
|
||||
Task, TextStyleRefinement, UnderlineStyle,
|
||||
};
|
||||
use language::{Buffer, Language, LanguageRegistry};
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
|
||||
use markdown::{Markdown, MarkdownStyle};
|
||||
use scripting_tool::{ScriptingTool, ScriptingToolInput};
|
||||
use settings::Settings as _;
|
||||
use std::sync::Arc;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, Disclosure, KeyBinding};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::thread::{MessageId, RequestKind, Thread, ThreadError, ThreadEvent};
|
||||
use crate::thread_store::ThreadStore;
|
||||
@@ -22,19 +22,17 @@ use crate::tool_use::{ToolUse, ToolUseStatus};
|
||||
use crate::ui::ContextPill;
|
||||
|
||||
pub struct ActiveThread {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
thread: Entity<Thread>,
|
||||
save_thread_task: Option<Task<()>>,
|
||||
messages: Vec<MessageId>,
|
||||
list_state: ListState,
|
||||
rendered_messages_by_id: HashMap<MessageId, Entity<Markdown>>,
|
||||
rendered_scripting_tool_uses: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
|
||||
editing_message: Option<(MessageId, EditMessageState)>,
|
||||
expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
|
||||
last_error: Option<ThreadError>,
|
||||
lua_language: Option<Arc<Language>>, // Used for syntax highlighting in the Lua script tool
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -46,9 +44,7 @@ impl ActiveThread {
|
||||
pub fn new(
|
||||
thread: Entity<Thread>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -58,14 +54,13 @@ impl ActiveThread {
|
||||
];
|
||||
|
||||
let mut this = Self {
|
||||
workspace,
|
||||
language_registry,
|
||||
tools,
|
||||
thread_store,
|
||||
thread: thread.clone(),
|
||||
save_thread_task: None,
|
||||
messages: Vec::new(),
|
||||
rendered_messages_by_id: HashMap::default(),
|
||||
rendered_scripting_tool_uses: HashMap::default(),
|
||||
expanded_tool_uses: HashMap::default(),
|
||||
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
|
||||
let this = cx.entity().downgrade();
|
||||
@@ -76,25 +71,21 @@ impl ActiveThread {
|
||||
}),
|
||||
editing_message: None,
|
||||
last_error: None,
|
||||
lua_language: None,
|
||||
_subscriptions: subscriptions,
|
||||
};
|
||||
|
||||
// Initialize the Lua language in the background, for syntax highlighting.
|
||||
let language_registry = this.language_registry.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if let Ok(lua_language) = language_registry.language_for_name("Lua").await {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.lua_language = Some(lua_language);
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
|
||||
this.push_message(&message.id, message.text.clone(), window, cx);
|
||||
|
||||
for tool_use in thread.read(cx).scripting_tool_uses_for_message(message.id) {
|
||||
this.render_scripting_tool_use_markdown(
|
||||
tool_use.id.clone(),
|
||||
tool_use.name.as_ref(),
|
||||
tool_use.input.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this
|
||||
@@ -187,6 +178,8 @@ impl ActiveThread {
|
||||
|
||||
text_style.refine(&TextStyleRefinement {
|
||||
font_family: Some(theme_settings.ui_font.family.clone()),
|
||||
font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
|
||||
font_features: Some(theme_settings.ui_font.features.clone()),
|
||||
font_size: Some(ui_font_size.into()),
|
||||
color: Some(cx.theme().colors().text),
|
||||
..Default::default()
|
||||
@@ -221,6 +214,8 @@ impl ActiveThread {
|
||||
},
|
||||
text: Some(TextStyleRefinement {
|
||||
font_family: Some(theme_settings.buffer_font.family.clone()),
|
||||
font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
|
||||
font_features: Some(theme_settings.buffer_font.features.clone()),
|
||||
font_size: Some(buffer_font_size.into()),
|
||||
..Default::default()
|
||||
}),
|
||||
@@ -228,6 +223,8 @@ impl ActiveThread {
|
||||
},
|
||||
inline_code: TextStyleRefinement {
|
||||
font_family: Some(theme_settings.buffer_font.family.clone()),
|
||||
font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
|
||||
font_features: Some(theme_settings.buffer_font.features.clone()),
|
||||
font_size: Some(buffer_font_size.into()),
|
||||
background_color: Some(colors.editor_foreground.opacity(0.1)),
|
||||
..Default::default()
|
||||
@@ -255,9 +252,35 @@ impl ActiveThread {
|
||||
})
|
||||
}
|
||||
|
||||
/// Renders the input of a scripting tool use to Markdown.
|
||||
///
|
||||
/// Does nothing if the tool use does not correspond to the scripting tool.
|
||||
fn render_scripting_tool_use_markdown(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
tool_name: &str,
|
||||
tool_input: serde_json::Value,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if tool_name != ScriptingTool::NAME {
|
||||
return;
|
||||
}
|
||||
|
||||
let lua_script = serde_json::from_value::<ScriptingToolInput>(tool_input)
|
||||
.map(|input| input.lua_script)
|
||||
.unwrap_or_default();
|
||||
|
||||
let lua_script =
|
||||
self.render_markdown(format!("```lua\n{lua_script}\n```").into(), window, cx);
|
||||
|
||||
self.rendered_scripting_tool_uses
|
||||
.insert(tool_use_id, lua_script);
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
&mut self,
|
||||
_: &Entity<Thread>,
|
||||
_thread: &Entity<Thread>,
|
||||
event: &ThreadEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -308,52 +331,28 @@ impl ActiveThread {
|
||||
cx.notify();
|
||||
}
|
||||
ThreadEvent::UsePendingTools => {
|
||||
let thread = self.thread.read(cx);
|
||||
let thread_id = thread.id().0.clone();
|
||||
let pending_tool_uses = thread
|
||||
.pending_tool_uses()
|
||||
.into_iter()
|
||||
.filter(|tool_use| tool_use.status.is_idle())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for tool_use in pending_tool_uses {
|
||||
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
|
||||
let task = tool.run(
|
||||
tool_use.input,
|
||||
thread_id.clone(),
|
||||
self.workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.insert_tool_output(tool_use.id.clone(), task, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.use_pending_tools(cx);
|
||||
});
|
||||
}
|
||||
ThreadEvent::ToolFinished { .. } => {
|
||||
let all_tools_finished = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.pending_tool_uses()
|
||||
.into_iter()
|
||||
.all(|tool_use| tool_use.status.is_error());
|
||||
if all_tools_finished {
|
||||
ThreadEvent::ToolFinished {
|
||||
pending_tool_use, ..
|
||||
} => {
|
||||
if let Some(tool_use) = pending_tool_use {
|
||||
self.render_scripting_tool_use_markdown(
|
||||
tool_use.id.clone(),
|
||||
tool_use.name.as_ref(),
|
||||
tool_use.input.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
if self.thread.read(cx).all_tools_finished() {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
if let Some(model) = model_registry.active_model() {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
// Insert a user message to contain the tool results.
|
||||
thread.insert_user_message(
|
||||
// TODO: Sending up a user message without any content results in the model sending back
|
||||
// responses that also don't have any content. We currently don't handle this case well,
|
||||
// so for now we provide some text to keep the model on track.
|
||||
"Here are the tool results.",
|
||||
Vec::new(),
|
||||
cx,
|
||||
);
|
||||
thread.send_to_model(model, RequestKind::Chat, true, cx);
|
||||
thread.send_tool_results_to_model(model, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -497,12 +496,17 @@ impl ActiveThread {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
let context = self.thread.read(cx).context_for_message(message_id);
|
||||
let tool_uses = self.thread.read(cx).tool_uses_for_message(message_id);
|
||||
let colors = cx.theme().colors();
|
||||
let thread = self.thread.read(cx);
|
||||
|
||||
let context = thread.context_for_message(message_id);
|
||||
let tool_uses = thread.tool_uses_for_message(message_id);
|
||||
let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id);
|
||||
|
||||
// Don't render user messages that are just there for returning tool results.
|
||||
if message.role == Role::User && self.thread.read(cx).message_has_tool_results(message_id) {
|
||||
if message.role == Role::User
|
||||
&& (thread.message_has_tool_results(message_id)
|
||||
|| thread.message_has_scripting_tool_results(message_id))
|
||||
{
|
||||
return Empty.into_any();
|
||||
}
|
||||
|
||||
@@ -515,6 +519,8 @@ impl ActiveThread {
|
||||
.filter(|(id, _)| *id == message_id)
|
||||
.map(|(_, state)| state.editor.clone());
|
||||
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
let message_content = v_flex()
|
||||
.child(
|
||||
if let Some(edit_message_editor) = edit_message_editor.clone() {
|
||||
@@ -650,22 +656,28 @@ impl ActiveThread {
|
||||
.id(("message-container", ix))
|
||||
.child(message_content)
|
||||
.map(|parent| {
|
||||
if tool_uses.is_empty() {
|
||||
if tool_uses.is_empty() && scripting_tool_uses.is_empty() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
parent.child(
|
||||
v_flex().children(
|
||||
tool_uses
|
||||
.into_iter()
|
||||
.map(|tool_use| self.render_tool_use(tool_use, cx)),
|
||||
),
|
||||
v_flex()
|
||||
.children(
|
||||
tool_uses
|
||||
.into_iter()
|
||||
.map(|tool_use| self.render_tool_use(tool_use, cx)),
|
||||
)
|
||||
.children(
|
||||
scripting_tool_uses
|
||||
.into_iter()
|
||||
.map(|tool_use| self.render_scripting_tool_use(tool_use, cx)),
|
||||
),
|
||||
)
|
||||
}),
|
||||
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
|
||||
v_flex()
|
||||
.bg(colors.editor_background)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.child(message_content),
|
||||
),
|
||||
};
|
||||
@@ -674,7 +686,6 @@ impl ActiveThread {
|
||||
}
|
||||
|
||||
fn render_tool_use(&self, tool_use: ToolUse, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let tool = ToolRegistry::global(cx).tool(&tool_use.name);
|
||||
let is_open = self
|
||||
.expanded_tool_uses
|
||||
.get(&tool_use.id)
|
||||
@@ -694,8 +705,13 @@ impl ActiveThread {
|
||||
.pl_1()
|
||||
.pr_2()
|
||||
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
|
||||
.when(is_open, |element| element.border_b_1().rounded_t(px(6.)))
|
||||
.when(!is_open, |element| element.rounded(px(6.)))
|
||||
.map(|element| {
|
||||
if is_open {
|
||||
element.border_b_1().rounded_t(px(6.))
|
||||
} else {
|
||||
element.rounded_md()
|
||||
}
|
||||
})
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -740,15 +756,122 @@ impl ActiveThread {
|
||||
.px_2p5()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(match tool.clone() {
|
||||
Some(tool) => tool.render_input(
|
||||
tool_use.input,
|
||||
self.lua_language.clone(),
|
||||
cx,
|
||||
),
|
||||
None => {
|
||||
assistant_tool::default_render_input(tool_use.input)
|
||||
.child(Label::new("Input:"))
|
||||
.child(Label::new(
|
||||
serde_json::to_string_pretty(&tool_use.input)
|
||||
.unwrap_or_default(),
|
||||
)),
|
||||
)
|
||||
.map(|parent| match tool_use.status {
|
||||
ToolUseStatus::Finished(output) => parent.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.py_1()
|
||||
.px_2p5()
|
||||
.child(Label::new("Result:"))
|
||||
.child(Label::new(output)),
|
||||
),
|
||||
ToolUseStatus::Error(err) => parent.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.py_1()
|
||||
.px_2p5()
|
||||
.child(Label::new("Error:"))
|
||||
.child(Label::new(err)),
|
||||
),
|
||||
ToolUseStatus::Pending | ToolUseStatus::Running => parent,
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_scripting_tool_use(
|
||||
&self,
|
||||
tool_use: ToolUse,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let is_open = self
|
||||
.expanded_tool_uses
|
||||
.get(&tool_use.id)
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
|
||||
div().px_2p5().child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.py_0p5()
|
||||
.pl_1()
|
||||
.pr_2()
|
||||
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
|
||||
.map(|element| {
|
||||
if is_open {
|
||||
element.border_b_1().rounded_t(px(6.))
|
||||
} else {
|
||||
element.rounded_md()
|
||||
}
|
||||
})
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Disclosure::new("tool-use-disclosure", is_open).on_click(
|
||||
cx.listener({
|
||||
let tool_use_id = tool_use.id.clone();
|
||||
move |this, _event, _window, _cx| {
|
||||
let is_open = this
|
||||
.expanded_tool_uses
|
||||
.entry(tool_use_id.clone())
|
||||
.or_insert(false);
|
||||
|
||||
*is_open = !*is_open;
|
||||
}
|
||||
}),
|
||||
))
|
||||
.child(Label::new(tool_use.name)),
|
||||
)
|
||||
.child(
|
||||
Label::new(match tool_use.status {
|
||||
ToolUseStatus::Pending => "Pending",
|
||||
ToolUseStatus::Running => "Running",
|
||||
ToolUseStatus::Finished(_) => "Finished",
|
||||
ToolUseStatus::Error(_) => "Error",
|
||||
})
|
||||
.size(LabelSize::XSmall)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
)
|
||||
.map(|parent| {
|
||||
if !is_open {
|
||||
return parent;
|
||||
}
|
||||
|
||||
let lua_script_markdown =
|
||||
self.rendered_scripting_tool_uses.get(&tool_use.id).cloned();
|
||||
|
||||
parent.child(
|
||||
v_flex()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.py_1()
|
||||
.px_2p5()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(Label::new("Input:"))
|
||||
.map(|parent| {
|
||||
if let Some(markdown) = lua_script_markdown {
|
||||
parent.child(markdown)
|
||||
} else {
|
||||
parent.child(Label::new(
|
||||
"Failed to render script input to Markdown",
|
||||
))
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -758,17 +881,16 @@ impl ActiveThread {
|
||||
.gap_0p5()
|
||||
.py_1()
|
||||
.px_2p5()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(match tool {
|
||||
Some(tool) => tool.render_output(output, cx),
|
||||
None => assistant_tool::default_render_output(output),
|
||||
}),
|
||||
.child(Label::new("Result:"))
|
||||
.child(Label::new(output)),
|
||||
),
|
||||
ToolUseStatus::Error(err) => parent.child(
|
||||
v_flex().gap_0p5().py_1().px_2p5().child(match tool {
|
||||
Some(tool) => tool.render_error(err, cx),
|
||||
None => assistant_tool::default_render_output(err),
|
||||
}),
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.py_1()
|
||||
.px_2p5()
|
||||
.child(Label::new("Error:"))
|
||||
.child(Label::new(err)),
|
||||
),
|
||||
ToolUseStatus::Pending | ToolUseStatus::Running => parent,
|
||||
}),
|
||||
|
||||
@@ -16,6 +16,7 @@ mod terminal_inline_assistant;
|
||||
mod thread;
|
||||
mod thread_history;
|
||||
mod thread_store;
|
||||
mod tool_selector;
|
||||
mod tool_use;
|
||||
mod ui;
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ impl AssistantConfiguration {
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.map(|parent| match configuration_view {
|
||||
Some(configuration_view) => parent.child(configuration_view),
|
||||
None => parent.child(div().child(Label::new(format!(
|
||||
|
||||
@@ -1,28 +1,45 @@
|
||||
use assistant_settings::AssistantSettings;
|
||||
use fs::Fs;
|
||||
use gpui::FocusHandle;
|
||||
use language_model_selector::{assistant_language_model_selector, LanguageModelSelector};
|
||||
use gpui::{Entity, FocusHandle, SharedString};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_model_selector::{
|
||||
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
|
||||
};
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, PopoverMenuHandle};
|
||||
use ui::{prelude::*, ButtonLike, PopoverMenuHandle, Tooltip};
|
||||
|
||||
pub struct AssistantModelSelector {
|
||||
selector: Entity<LanguageModelSelector>,
|
||||
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
focus_handle: FocusHandle,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
impl AssistantModelSelector {
|
||||
pub(crate) fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
focus_handle: FocusHandle,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
Self {
|
||||
fs,
|
||||
selector: cx.new(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _cx| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
menu_handle,
|
||||
focus_handle,
|
||||
menu_handle: PopoverMenuHandle::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,19 +49,43 @@ impl AssistantModelSelector {
|
||||
}
|
||||
|
||||
impl Render for AssistantModelSelector {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let fs_clone = self.fs.clone();
|
||||
assistant_language_model_selector(
|
||||
self.focus_handle.clone(),
|
||||
Some(self.menu_handle.clone()),
|
||||
cx,
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs_clone.clone(),
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let model_name = match active_model {
|
||||
Some(model) => model.name().0,
|
||||
_ => SharedString::from("No model selected"),
|
||||
};
|
||||
|
||||
LanguageModelSelectorPopoverMenu::new(
|
||||
self.selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
),
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Change Model",
|
||||
&ToggleModelSelector,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
)
|
||||
},
|
||||
gpui::Corner::BottomRight,
|
||||
)
|
||||
.with_handle(self.menu_handle.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,6 @@ pub struct AssistantPanel {
|
||||
context_editor: Option<Entity<ContextEditor>>,
|
||||
configuration: Option<Entity<AssistantConfiguration>>,
|
||||
configuration_subscription: Option<Subscription>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
local_timezone: UtcOffset,
|
||||
active_view: ActiveView,
|
||||
history_store: Entity<HistoryStore>,
|
||||
@@ -133,7 +132,7 @@ impl AssistantPanel {
|
||||
log::info!("[assistant2-debug] finished initializing ContextStore");
|
||||
|
||||
workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||
cx.new(|cx| Self::new(workspace, thread_store, context_store, tools, window, cx))
|
||||
cx.new(|cx| Self::new(workspace, thread_store, context_store, window, cx))
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -142,7 +141,6 @@ impl AssistantPanel {
|
||||
workspace: &Workspace,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
context_store: Entity<assistant_context_editor::ContextStore>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -168,30 +166,29 @@ impl AssistantPanel {
|
||||
let history_store =
|
||||
cx.new(|cx| HistoryStore::new(thread_store.clone(), context_store.clone(), cx));
|
||||
|
||||
let thread = cx.new(|cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
thread_store.clone(),
|
||||
language_registry.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
Self {
|
||||
active_view: ActiveView::Thread,
|
||||
workspace: workspace.clone(),
|
||||
project,
|
||||
workspace,
|
||||
project: project.clone(),
|
||||
fs: fs.clone(),
|
||||
language_registry: language_registry.clone(),
|
||||
language_registry,
|
||||
thread_store: thread_store.clone(),
|
||||
thread: cx.new(|cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
thread_store.clone(),
|
||||
workspace,
|
||||
language_registry,
|
||||
tools.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
thread,
|
||||
message_editor,
|
||||
context_store,
|
||||
context_editor: None,
|
||||
configuration: None,
|
||||
configuration_subscription: None,
|
||||
tools,
|
||||
local_timezone: UtcOffset::from_whole_seconds(
|
||||
chrono::Local::now().offset().local_minus_utc(),
|
||||
)
|
||||
@@ -246,9 +243,7 @@ impl AssistantPanel {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
self.thread_store.clone(),
|
||||
self.workspace.clone(),
|
||||
self.language_registry.clone(),
|
||||
self.tools.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -381,9 +376,7 @@ impl AssistantPanel {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
this.thread_store.clone(),
|
||||
this.workspace.clone(),
|
||||
this.language_registry.clone(),
|
||||
this.tools.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -1023,12 +1016,7 @@ impl Render for AssistantPanel {
|
||||
.map(|parent| match self.active_view {
|
||||
ActiveView::Thread => parent
|
||||
.child(self.render_active_thread_or_empty_state(window, cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(self.message_editor.clone()),
|
||||
)
|
||||
.child(h_flex().child(self.message_editor.clone()))
|
||||
.children(self.render_last_error(cx)),
|
||||
ActiveView::History => parent.child(self.history.clone()),
|
||||
ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
|
||||
|
||||
@@ -167,8 +167,8 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> SharedString {
|
||||
"Enter the URL that you would like to fetch".into()
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||
Some("Enter the URL that you would like to fetch".into())
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
|
||||
@@ -208,12 +208,18 @@ impl ContextStore {
|
||||
let mut text_tasks = Vec::new();
|
||||
this.update(&mut cx, |_, cx| {
|
||||
for (path, buffer_entity) in files.into_iter().zip(buffers) {
|
||||
let buffer_entity = buffer_entity?;
|
||||
let buffer = buffer_entity.read(cx);
|
||||
let (buffer_info, text_task) =
|
||||
collect_buffer_info_and_text(path, buffer_entity, buffer, cx.to_async());
|
||||
buffer_infos.push(buffer_info);
|
||||
text_tasks.push(text_task);
|
||||
// Skip all binary files and other non-UTF8 files
|
||||
if let Ok(buffer_entity) = buffer_entity {
|
||||
let buffer = buffer_entity.read(cx);
|
||||
let (buffer_info, text_task) = collect_buffer_info_and_text(
|
||||
path,
|
||||
buffer_entity,
|
||||
buffer,
|
||||
cx.to_async(),
|
||||
);
|
||||
buffer_infos.push(buffer_info);
|
||||
text_tasks.push(text_task);
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
|
||||
@@ -25,7 +25,7 @@ use crate::{
|
||||
|
||||
pub struct ContextStrip {
|
||||
context_store: Entity<ContextStore>,
|
||||
pub context_picker: Entity<ContextPicker>,
|
||||
context_picker: Entity<ContextPicker>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
focus_handle: FocusHandle,
|
||||
suggest_context_kind: SuggestContextKind,
|
||||
@@ -36,7 +36,6 @@ pub struct ContextStrip {
|
||||
}
|
||||
|
||||
impl ContextStrip {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
context_store: Entity<ContextStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
|
||||
@@ -27,6 +27,7 @@ use language::{Buffer, Point, Selection, TransactionId};
|
||||
use language_model::{report_assistant_event, LanguageModelRegistry};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::LspAction;
|
||||
use project::{CodeAction, ProjectTransaction};
|
||||
use prompt_store::PromptBuilder;
|
||||
use settings::{Settings, SettingsStore};
|
||||
@@ -479,7 +480,6 @@ impl InlineAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn suggest_assist(
|
||||
&mut self,
|
||||
editor: &Entity<Editor>,
|
||||
@@ -1450,7 +1450,6 @@ struct InlineAssistScrollLock {
|
||||
}
|
||||
|
||||
impl EditorInlineAssists {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(editor: &Entity<Editor>, window: &mut Window, cx: &mut App) -> Self {
|
||||
let (highlight_updates_tx, mut highlight_updates_rx) = async_watch::channel(());
|
||||
Self {
|
||||
@@ -1562,7 +1561,6 @@ pub struct InlineAssist {
|
||||
}
|
||||
|
||||
impl InlineAssist {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
assist_id: InlineAssistId,
|
||||
group_id: InlineAssistGroupId,
|
||||
@@ -1727,10 +1725,10 @@ impl CodeActionProvider for AssistantCodeActionProvider {
|
||||
Task::ready(Ok(vec![CodeAction {
|
||||
server_id: language::LanguageServerId(0),
|
||||
range: snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end),
|
||||
lsp_action: lsp::CodeAction {
|
||||
lsp_action: LspAction::Action(Box::new(lsp::CodeAction {
|
||||
title: "Fix with Assistant".into(),
|
||||
..Default::default()
|
||||
},
|
||||
})),
|
||||
}]))
|
||||
} else {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
|
||||
@@ -816,7 +816,6 @@ impl InlineAssistId {
|
||||
}
|
||||
|
||||
impl PromptEditor<BufferCodegen> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new_buffer(
|
||||
id: InlineAssistId,
|
||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
||||
@@ -857,6 +856,7 @@ impl PromptEditor<BufferCodegen> {
|
||||
editor
|
||||
});
|
||||
let context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||
|
||||
let context_strip = cx.new(|cx| {
|
||||
ContextStrip::new(
|
||||
@@ -880,7 +880,13 @@ impl PromptEditor<BufferCodegen> {
|
||||
context_strip,
|
||||
context_picker_menu_handle,
|
||||
model_selector: cx.new(|cx| {
|
||||
AssistantModelSelector::new(fs, prompt_editor.focus_handle(cx), window, cx)
|
||||
AssistantModelSelector::new(
|
||||
fs,
|
||||
model_selector_menu_handle,
|
||||
prompt_editor.focus_handle(cx),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
edited_since_done: false,
|
||||
prompt_history,
|
||||
@@ -969,7 +975,6 @@ impl TerminalInlineAssistId {
|
||||
}
|
||||
|
||||
impl PromptEditor<TerminalCodegen> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new_terminal(
|
||||
id: TerminalInlineAssistId,
|
||||
prompt_history: VecDeque<String>,
|
||||
@@ -1005,6 +1010,7 @@ impl PromptEditor<TerminalCodegen> {
|
||||
editor
|
||||
});
|
||||
let context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||
|
||||
let context_strip = cx.new(|cx| {
|
||||
ContextStrip::new(
|
||||
@@ -1028,7 +1034,13 @@ impl PromptEditor<TerminalCodegen> {
|
||||
context_strip,
|
||||
context_picker_menu_handle,
|
||||
model_selector: cx.new(|cx| {
|
||||
AssistantModelSelector::new(fs, prompt_editor.focus_handle(cx), window, cx)
|
||||
AssistantModelSelector::new(
|
||||
fs,
|
||||
model_selector_menu_handle.clone(),
|
||||
prompt_editor.focus_handle(cx),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
edited_since_done: false,
|
||||
prompt_history,
|
||||
|
||||
@@ -2,10 +2,11 @@ use std::sync::Arc;
|
||||
|
||||
use editor::actions::MoveUp;
|
||||
use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
|
||||
use file_icons::FileIcons;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
pulsating_between, Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription,
|
||||
TextStyle, WeakEntity,
|
||||
Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
|
||||
WeakEntity,
|
||||
};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
@@ -15,8 +16,8 @@ use std::time::Duration;
|
||||
use text::Bias;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Switch,
|
||||
TintColor, Tooltip,
|
||||
prelude::*, ButtonLike, Disclosure, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle,
|
||||
Switch, Tooltip,
|
||||
};
|
||||
use vim_mode_setting::VimModeSetting;
|
||||
use workspace::Workspace;
|
||||
@@ -27,6 +28,7 @@ use crate::context_store::{refresh_context_store_text, ContextStore};
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
use crate::thread::{RequestKind, Thread};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::tool_selector::ToolSelector;
|
||||
use crate::{Chat, ChatMode, RemoveAllContext, ToggleContextPicker};
|
||||
|
||||
pub struct MessageEditor {
|
||||
@@ -38,7 +40,9 @@ pub struct MessageEditor {
|
||||
inline_context_picker: Entity<ContextPicker>,
|
||||
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
model_selector: Entity<AssistantModelSelector>,
|
||||
tool_selector: Entity<ToolSelector>,
|
||||
use_tools: bool,
|
||||
edits_expanded: bool,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -51,9 +55,11 @@ impl MessageEditor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let tools = thread.read(cx).tools().clone();
|
||||
let context_store = cx.new(|_cx| ContextStore::new(workspace.clone()));
|
||||
let context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::auto_height(10, window, cx);
|
||||
@@ -106,9 +112,18 @@ impl MessageEditor {
|
||||
context_picker_menu_handle,
|
||||
inline_context_picker,
|
||||
inline_context_picker_menu_handle,
|
||||
model_selector: cx
|
||||
.new(|cx| AssistantModelSelector::new(fs, editor.focus_handle(cx), window, cx)),
|
||||
model_selector: cx.new(|cx| {
|
||||
AssistantModelSelector::new(
|
||||
fs,
|
||||
model_selector_menu_handle,
|
||||
editor.focus_handle(cx),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
tool_selector: cx.new(|cx| ToolSelector::new(tools, cx)),
|
||||
use_tools: false,
|
||||
edits_expanded: false,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
@@ -290,166 +305,319 @@ impl Render for MessageEditor {
|
||||
let linux = platform == PlatformStyle::Linux;
|
||||
let windows = platform == PlatformStyle::Windows;
|
||||
let button_width = if linux || windows || vim_mode_enabled {
|
||||
px(92.)
|
||||
px(82.)
|
||||
} else {
|
||||
px(64.)
|
||||
};
|
||||
|
||||
let changed_buffers = self.thread.read(cx).scripting_changed_buffers(cx);
|
||||
let changed_buffers_count = changed_buffers.len();
|
||||
|
||||
v_flex()
|
||||
.key_context("MessageEditor")
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||
this.model_selector
|
||||
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
||||
}))
|
||||
.on_action(cx.listener(Self::toggle_context_picker))
|
||||
.on_action(cx.listener(Self::remove_all_context))
|
||||
.on_action(cx.listener(Self::move_up))
|
||||
.on_action(cx.listener(Self::toggle_chat_mode))
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.bg(bg_color)
|
||||
.child(self.context_strip.clone())
|
||||
.when(is_streaming_completion, |parent| {
|
||||
let focus_handle = self.editor.focus_handle(cx).clone();
|
||||
parent.child(
|
||||
h_flex().py_3().w_full().justify_center().child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.py_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_lg()
|
||||
.shadow_md()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(gpui::Transformation::rotate(
|
||||
gpui::percentage(delta),
|
||||
))
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Generating…")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(ui::Divider::vertical())
|
||||
.child(
|
||||
Button::new("cancel-generation", "Cancel")
|
||||
.label_size(LabelSize::XSmall)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&editor::actions::Cancel,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.))),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(
|
||||
&editor::actions::Cancel,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(changed_buffers_count > 0, |parent| {
|
||||
parent.child(
|
||||
v_flex()
|
||||
.mx_2()
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.border_1()
|
||||
.border_b_0()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_t_md()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.child(
|
||||
Disclosure::new("edits-disclosure", self.edits_expanded)
|
||||
.on_click(cx.listener(|this, _ev, _window, cx| {
|
||||
this.edits_expanded = !this.edits_expanded;
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Label::new("Edits")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new("•").size(LabelSize::XSmall).color(Color::Muted))
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{} {}",
|
||||
changed_buffers_count,
|
||||
if changed_buffers_count == 1 {
|
||||
"file"
|
||||
} else {
|
||||
"files"
|
||||
}
|
||||
))
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.when(self.edits_expanded, |parent| {
|
||||
parent.child(
|
||||
v_flex().bg(cx.theme().colors().editor_background).children(
|
||||
changed_buffers.enumerate().flat_map(|(index, buffer)| {
|
||||
let file = buffer.read(cx).file()?;
|
||||
let path = file.path();
|
||||
|
||||
let parent_label = path.parent().and_then(|parent| {
|
||||
let parent_str = parent.to_string_lossy();
|
||||
|
||||
if parent_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Label::new(format!(
|
||||
"{}{}",
|
||||
parent_str,
|
||||
std::path::MAIN_SEPARATOR_STR
|
||||
))
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
let name_label = path.file_name().map(|name| {
|
||||
Label::new(name.to_string_lossy().to_string())
|
||||
.size(LabelSize::Small)
|
||||
});
|
||||
|
||||
let file_icon = FileIcons::get_icon(&path, cx)
|
||||
.map(Icon::from_path)
|
||||
.unwrap_or_else(|| Icon::new(IconName::File));
|
||||
|
||||
let element = div()
|
||||
.p_2()
|
||||
.when(index + 1 < changed_buffers_count, |parent| {
|
||||
parent
|
||||
.border_color(cx.theme().colors().border)
|
||||
.border_b_1()
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(file_icon)
|
||||
.child(
|
||||
// TODO: handle overflow
|
||||
h_flex()
|
||||
.children(parent_label)
|
||||
.children(name_label),
|
||||
)
|
||||
// TODO: show lines changed
|
||||
.child(Label::new("+").color(Color::Created))
|
||||
.child(Label::new("-").color(Color::Deleted)),
|
||||
);
|
||||
|
||||
Some(element)
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_5()
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: line_height.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
background: bg_color,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.key_context("MessageEditor")
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||
this.model_selector
|
||||
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
||||
}))
|
||||
.on_action(cx.listener(Self::toggle_context_picker))
|
||||
.on_action(cx.listener(Self::remove_all_context))
|
||||
.on_action(cx.listener(Self::move_up))
|
||||
.on_action(cx.listener(Self::toggle_chat_mode))
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.bg(bg_color)
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(self.context_strip.clone())
|
||||
.child(
|
||||
PopoverMenu::new("inline-context-picker")
|
||||
.menu(move |window, cx| {
|
||||
inline_context_picker.update(cx, |this, cx| {
|
||||
this.init(window, cx);
|
||||
});
|
||||
v_flex()
|
||||
.gap_5()
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: line_height.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Some(inline_context_picker.clone())
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
background: bg_color,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.attach(gpui::Corner::TopLeft)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: (-ThemeSettings::get_global(cx).ui_font_size(cx) * 2) - px(4.0),
|
||||
})
|
||||
.with_handle(self.inline_context_picker_menu_handle.clone()),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(
|
||||
Switch::new("use-tools", self.use_tools.into())
|
||||
.label("Tools")
|
||||
.on_click(cx.listener(|this, selection, _window, _cx| {
|
||||
this.use_tools = match selection {
|
||||
ToggleState::Selected => true,
|
||||
ToggleState::Unselected
|
||||
| ToggleState::Indeterminate => false,
|
||||
};
|
||||
}))
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&ChatMode,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
PopoverMenu::new("inline-context-picker")
|
||||
.menu(move |window, cx| {
|
||||
inline_context_picker.update(cx, |this, cx| {
|
||||
this.init(window, cx);
|
||||
});
|
||||
|
||||
Some(inline_context_picker.clone())
|
||||
})
|
||||
.attach(gpui::Corner::TopLeft)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: (-ThemeSettings::get_global(cx).ui_font_size(cx) * 2)
|
||||
- px(4.0),
|
||||
})
|
||||
.with_handle(self.inline_context_picker_menu_handle.clone()),
|
||||
)
|
||||
.child(h_flex().gap_1().child(self.model_selector.clone()).child(
|
||||
if is_streaming_completion {
|
||||
ButtonLike::new("cancel-generation")
|
||||
.width(button_width.into())
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex().gap_2().child(self.tool_selector.clone()).child(
|
||||
Switch::new("use-tools", self.use_tools.into())
|
||||
.label("Tools")
|
||||
.on_click(cx.listener(
|
||||
|this, selection, _window, _cx| {
|
||||
this.use_tools = match selection {
|
||||
ToggleState::Selected => true,
|
||||
ToggleState::Unselected
|
||||
| ToggleState::Indeterminate => false,
|
||||
};
|
||||
},
|
||||
))
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&ChatMode,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex().gap_1().child(self.model_selector.clone()).child(
|
||||
ButtonLike::new("submit-message")
|
||||
.width(button_width.into())
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(
|
||||
is_editor_empty
|
||||
|| !is_model_selected
|
||||
|| is_streaming_completion,
|
||||
)
|
||||
.child(
|
||||
Label::new("Cancel")
|
||||
.size(LabelSize::Small)
|
||||
.with_animation(
|
||||
"pulsating-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(
|
||||
0.4, 0.8,
|
||||
)),
|
||||
|label, delta| label.alpha(delta),
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
Label::new("Submit")
|
||||
.size(LabelSize::Small)
|
||||
.color(submit_label_color),
|
||||
)
|
||||
.children(
|
||||
KeyBinding::for_action_in(
|
||||
&Chat,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|binding| {
|
||||
binding
|
||||
.when(vim_mode_enabled, |kb| {
|
||||
kb.size(rems_from_px(12.))
|
||||
})
|
||||
.into_any_element()
|
||||
}),
|
||||
),
|
||||
)
|
||||
.children(
|
||||
KeyBinding::for_action_in(
|
||||
&editor::actions::Cancel,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(
|
||||
&editor::actions::Cancel,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
} else {
|
||||
ButtonLike::new("submit-message")
|
||||
.width(button_width.into())
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(is_editor_empty || !is_model_selected)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
Label::new("Submit")
|
||||
.size(LabelSize::Small)
|
||||
.color(submit_label_color),
|
||||
)
|
||||
.children(
|
||||
KeyBinding::for_action_in(
|
||||
&Chat,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(&Chat, window, cx);
|
||||
})
|
||||
.when(is_editor_empty, |button| {
|
||||
button
|
||||
.tooltip(Tooltip::text("Type a message to submit"))
|
||||
})
|
||||
.when(!is_model_selected, |button| {
|
||||
button.tooltip(Tooltip::text(
|
||||
"Select a model to continue",
|
||||
))
|
||||
})
|
||||
},
|
||||
)),
|
||||
.on_click(move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(&Chat, window, cx);
|
||||
})
|
||||
.when(is_editor_empty, |button| {
|
||||
button.tooltip(Tooltip::text(
|
||||
"Type a message to submit",
|
||||
))
|
||||
})
|
||||
.when(is_streaming_completion, |button| {
|
||||
button.tooltip(Tooltip::text(
|
||||
"Cancel to submit a new message",
|
||||
))
|
||||
})
|
||||
.when(!is_model_selected, |button| {
|
||||
button.tooltip(Tooltip::text(
|
||||
"Select a model to continue",
|
||||
))
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,13 +5,15 @@ use assistant_tool::ToolWorkingSet;
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use futures::StreamExt as _;
|
||||
use gpui::{App, Context, EventEmitter, SharedString, Task};
|
||||
use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
|
||||
Role, StopReason,
|
||||
};
|
||||
use project::Project;
|
||||
use scripting_tool::{ScriptingSession, ScriptingTool};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::{post_inc, TryFutureExt as _};
|
||||
use uuid::Uuid;
|
||||
@@ -28,7 +30,7 @@ pub enum RequestKind {
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
|
||||
pub struct ThreadId(pub Arc<str>);
|
||||
pub struct ThreadId(Arc<str>);
|
||||
|
||||
impl ThreadId {
|
||||
pub fn new() -> Self {
|
||||
@@ -71,12 +73,21 @@ pub struct Thread {
|
||||
context_by_message: HashMap<MessageId, Vec<ContextId>>,
|
||||
completion_count: usize,
|
||||
pending_completions: Vec<PendingCompletion>,
|
||||
project: Entity<Project>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
tool_use: ToolUseState,
|
||||
scripting_session: Entity<ScriptingSession>,
|
||||
scripting_tool_use: ToolUseState,
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut Context<Self>) -> Self {
|
||||
pub fn new(
|
||||
project: Entity<Project>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let scripting_session = cx.new(|cx| ScriptingSession::new(project.clone(), cx));
|
||||
|
||||
Self {
|
||||
id: ThreadId::new(),
|
||||
updated_at: Utc::now(),
|
||||
@@ -88,16 +99,20 @@ impl Thread {
|
||||
context_by_message: HashMap::default(),
|
||||
completion_count: 0,
|
||||
pending_completions: Vec::new(),
|
||||
project,
|
||||
tools,
|
||||
tool_use: ToolUseState::new(),
|
||||
scripting_session,
|
||||
scripting_tool_use: ToolUseState::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_saved(
|
||||
id: ThreadId,
|
||||
saved: SavedThread,
|
||||
project: Entity<Project>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
_cx: &mut Context<Self>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let next_message_id = MessageId(
|
||||
saved
|
||||
@@ -106,7 +121,11 @@ impl Thread {
|
||||
.map(|message| message.id.0 + 1)
|
||||
.unwrap_or(0),
|
||||
);
|
||||
let tool_use = ToolUseState::from_saved_messages(&saved.messages);
|
||||
let tool_use =
|
||||
ToolUseState::from_saved_messages(&saved.messages, |name| name != ScriptingTool::NAME);
|
||||
let scripting_tool_use =
|
||||
ToolUseState::from_saved_messages(&saved.messages, |name| name == ScriptingTool::NAME);
|
||||
let scripting_session = cx.new(|cx| ScriptingSession::new(project.clone(), cx));
|
||||
|
||||
Self {
|
||||
id,
|
||||
@@ -127,8 +146,11 @@ impl Thread {
|
||||
context_by_message: HashMap::default(),
|
||||
completion_count: 0,
|
||||
pending_completions: Vec::new(),
|
||||
project,
|
||||
tools,
|
||||
tool_use,
|
||||
scripting_session,
|
||||
scripting_tool_use,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,33 +211,65 @@ impl Thread {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
|
||||
self.tool_use.pending_tool_uses()
|
||||
/// Returns whether all of the tool uses have finished running.
|
||||
pub fn all_tools_finished(&self) -> bool {
|
||||
let mut all_pending_tool_uses = self
|
||||
.tool_use
|
||||
.pending_tool_uses()
|
||||
.into_iter()
|
||||
.chain(self.scripting_tool_use.pending_tool_uses());
|
||||
|
||||
// If the only pending tool uses left are the ones with errors, then that means that we've finished running all
|
||||
// of the pending tools.
|
||||
all_pending_tool_uses.all(|tool_use| tool_use.status.is_error())
|
||||
}
|
||||
|
||||
pub fn tool_uses_for_message(&self, id: MessageId) -> Vec<ToolUse> {
|
||||
self.tool_use.tool_uses_for_message(id)
|
||||
}
|
||||
|
||||
pub fn scripting_tool_uses_for_message(&self, id: MessageId) -> Vec<ToolUse> {
|
||||
self.scripting_tool_use.tool_uses_for_message(id)
|
||||
}
|
||||
|
||||
pub fn tool_results_for_message(&self, id: MessageId) -> Vec<&LanguageModelToolResult> {
|
||||
self.tool_use.tool_results_for_message(id)
|
||||
}
|
||||
|
||||
pub fn scripting_tool_results_for_message(
|
||||
&self,
|
||||
id: MessageId,
|
||||
) -> Vec<&LanguageModelToolResult> {
|
||||
self.scripting_tool_use.tool_results_for_message(id)
|
||||
}
|
||||
|
||||
pub fn scripting_changed_buffers<'a>(
|
||||
&self,
|
||||
cx: &'a App,
|
||||
) -> impl ExactSizeIterator<Item = &'a Entity<language::Buffer>> {
|
||||
self.scripting_session.read(cx).changed_buffers()
|
||||
}
|
||||
|
||||
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
|
||||
self.tool_use.message_has_tool_results(message_id)
|
||||
}
|
||||
|
||||
pub fn message_has_scripting_tool_results(&self, message_id: MessageId) -> bool {
|
||||
self.scripting_tool_use.message_has_tool_results(message_id)
|
||||
}
|
||||
|
||||
pub fn insert_user_message(
|
||||
&mut self,
|
||||
text: impl Into<String>,
|
||||
context: Vec<ContextSnapshot>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
) -> MessageId {
|
||||
let message_id = self.insert_message(Role::User, text, cx);
|
||||
let context_ids = context.iter().map(|context| context.id).collect::<Vec<_>>();
|
||||
self.context
|
||||
.extend(context.into_iter().map(|context| (context.id, context)));
|
||||
self.context_by_message.insert(message_id, context_ids);
|
||||
message_id
|
||||
}
|
||||
|
||||
pub fn insert_message(
|
||||
@@ -294,16 +348,22 @@ impl Thread {
|
||||
let mut request = self.to_completion_request(request_kind, cx);
|
||||
|
||||
if use_tools {
|
||||
request.tools = self
|
||||
.tools()
|
||||
.tools(cx)
|
||||
.into_iter()
|
||||
.map(|tool| LanguageModelRequestTool {
|
||||
let mut tools = Vec::new();
|
||||
tools.push(LanguageModelRequestTool {
|
||||
name: ScriptingTool::NAME.into(),
|
||||
description: ScriptingTool::DESCRIPTION.into(),
|
||||
input_schema: ScriptingTool::input_schema(),
|
||||
});
|
||||
|
||||
tools.extend(self.tools().enabled_tools(cx).into_iter().map(|tool| {
|
||||
LanguageModelRequestTool {
|
||||
name: tool.name(),
|
||||
description: tool.description(),
|
||||
input_schema: tool.input_schema(),
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
}));
|
||||
|
||||
request.tools = tools;
|
||||
}
|
||||
|
||||
self.stream_completion(request, model, cx);
|
||||
@@ -333,10 +393,13 @@ impl Thread {
|
||||
content: Vec::new(),
|
||||
cache: false,
|
||||
};
|
||||
|
||||
match request_kind {
|
||||
RequestKind::Chat => {
|
||||
self.tool_use
|
||||
.attach_tool_results(message.id, &mut request_message);
|
||||
self.scripting_tool_use
|
||||
.attach_tool_results(message.id, &mut request_message);
|
||||
}
|
||||
RequestKind::Summarize => {
|
||||
// We don't care about tool use during summarization.
|
||||
@@ -353,11 +416,13 @@ impl Thread {
|
||||
RequestKind::Chat => {
|
||||
self.tool_use
|
||||
.attach_tool_uses(message.id, &mut request_message);
|
||||
self.scripting_tool_use
|
||||
.attach_tool_uses(message.id, &mut request_message);
|
||||
}
|
||||
RequestKind::Summarize => {
|
||||
// We don't care about tool use during summarization.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
request.messages.push(request_message);
|
||||
}
|
||||
@@ -421,7 +486,7 @@ impl Thread {
|
||||
// Importantly: We do *not* want to emit a `StreamedAssistantText` event here, as it
|
||||
// will result in duplicating the text of the chunk in the rendered Markdown.
|
||||
thread.insert_message(Role::Assistant, chunk, cx);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
LanguageModelCompletionEvent::ToolUse(tool_use) => {
|
||||
@@ -430,9 +495,15 @@ impl Thread {
|
||||
.iter()
|
||||
.rfind(|message| message.role == Role::Assistant)
|
||||
{
|
||||
thread
|
||||
.tool_use
|
||||
.request_tool_use(last_assistant_message.id, tool_use);
|
||||
if tool_use.name.as_ref() == ScriptingTool::NAME {
|
||||
thread
|
||||
.scripting_tool_use
|
||||
.request_tool_use(last_assistant_message.id, tool_use);
|
||||
} else {
|
||||
thread
|
||||
.tool_use
|
||||
.request_tool_use(last_assistant_message.id, tool_use);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -550,6 +621,63 @@ impl Thread {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn use_pending_tools(&mut self, cx: &mut Context<Self>) {
|
||||
let pending_tool_uses = self
|
||||
.tool_use
|
||||
.pending_tool_uses()
|
||||
.into_iter()
|
||||
.filter(|tool_use| tool_use.status.is_idle())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for tool_use in pending_tool_uses {
|
||||
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
|
||||
let task = tool.run(tool_use.input, self.project.clone(), cx);
|
||||
|
||||
self.insert_tool_output(tool_use.id.clone(), task, cx);
|
||||
}
|
||||
}
|
||||
|
||||
let pending_scripting_tool_uses = self
|
||||
.scripting_tool_use
|
||||
.pending_tool_uses()
|
||||
.into_iter()
|
||||
.filter(|tool_use| tool_use.status.is_idle())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for scripting_tool_use in pending_scripting_tool_uses {
|
||||
let task = match ScriptingTool::deserialize_input(scripting_tool_use.input) {
|
||||
Err(err) => Task::ready(Err(err.into())),
|
||||
Ok(input) => {
|
||||
let (script_id, script_task) =
|
||||
self.scripting_session.update(cx, move |session, cx| {
|
||||
session.run_script(input.lua_script, cx)
|
||||
});
|
||||
|
||||
let session = self.scripting_session.clone();
|
||||
cx.spawn(|_, cx| async move {
|
||||
script_task.await;
|
||||
|
||||
let message = session.read_with(&cx, |session, _cx| {
|
||||
// Using a id to get the script output seems impractical.
|
||||
// Why not just include it in the Task result?
|
||||
// This is because we'll later report the script state as it runs,
|
||||
session
|
||||
.get(script_id)
|
||||
.output_message_for_llm()
|
||||
.expect("Script shouldn't still be running")
|
||||
})?;
|
||||
|
||||
Ok(message)
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
self.insert_scripting_tool_output(scripting_tool_use.id.clone(), task, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_tool_output(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
@@ -562,11 +690,14 @@ impl Thread {
|
||||
let output = output.await;
|
||||
thread
|
||||
.update(&mut cx, |thread, cx| {
|
||||
thread
|
||||
let pending_tool_use = thread
|
||||
.tool_use
|
||||
.insert_tool_output(tool_use_id.clone(), output);
|
||||
|
||||
cx.emit(ThreadEvent::ToolFinished { tool_use_id });
|
||||
cx.emit(ThreadEvent::ToolFinished {
|
||||
tool_use_id,
|
||||
pending_tool_use,
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -576,6 +707,52 @@ impl Thread {
|
||||
.run_pending_tool(tool_use_id, insert_output_task);
|
||||
}
|
||||
|
||||
pub fn insert_scripting_tool_output(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
output: Task<Result<String>>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let insert_output_task = cx.spawn(|thread, mut cx| {
|
||||
let tool_use_id = tool_use_id.clone();
|
||||
async move {
|
||||
let output = output.await;
|
||||
thread
|
||||
.update(&mut cx, |thread, cx| {
|
||||
let pending_tool_use = thread
|
||||
.scripting_tool_use
|
||||
.insert_tool_output(tool_use_id.clone(), output);
|
||||
|
||||
cx.emit(ThreadEvent::ToolFinished {
|
||||
tool_use_id,
|
||||
pending_tool_use,
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
|
||||
self.scripting_tool_use
|
||||
.run_pending_tool(tool_use_id, insert_output_task);
|
||||
}
|
||||
|
||||
pub fn send_tool_results_to_model(
|
||||
&mut self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// Insert a user message to contain the tool results.
|
||||
self.insert_user_message(
|
||||
// TODO: Sending up a user message without any content results in the model sending back
|
||||
// responses that also don't have any content. We currently don't handle this case well,
|
||||
// so for now we provide some text to keep the model on track.
|
||||
"Here are the tool results.",
|
||||
Vec::new(),
|
||||
cx,
|
||||
);
|
||||
self.send_to_model(model, RequestKind::Chat, true, cx);
|
||||
}
|
||||
|
||||
/// Cancels the last pending completion, if there are any pending.
|
||||
///
|
||||
/// Returns whether a completion was canceled.
|
||||
@@ -608,6 +785,8 @@ pub enum ThreadEvent {
|
||||
ToolFinished {
|
||||
#[allow(unused)]
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
/// The pending tool use that corresponds to this tool.
|
||||
pending_tool_use: Option<PendingToolUse>,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ pub fn init(cx: &mut App) {
|
||||
}
|
||||
|
||||
pub struct ThreadStore {
|
||||
#[allow(unused)]
|
||||
project: Entity<Project>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
context_server_manager: Entity<ContextServerManager>,
|
||||
@@ -78,7 +77,7 @@ impl ThreadStore {
|
||||
}
|
||||
|
||||
pub fn create_thread(&mut self, cx: &mut Context<Self>) -> Entity<Thread> {
|
||||
cx.new(|cx| Thread::new(self.tools.clone(), cx))
|
||||
cx.new(|cx| Thread::new(self.project.clone(), self.tools.clone(), cx))
|
||||
}
|
||||
|
||||
pub fn open_thread(
|
||||
@@ -96,7 +95,15 @@ impl ThreadStore {
|
||||
.ok_or_else(|| anyhow!("no thread found with ID: {id:?}"))?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
cx.new(|cx| Thread::from_saved(id.clone(), thread, this.tools.clone(), cx))
|
||||
cx.new(|cx| {
|
||||
Thread::from_saved(
|
||||
id.clone(),
|
||||
thread,
|
||||
this.project.clone(),
|
||||
this.tools.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -109,28 +116,35 @@ impl ThreadStore {
|
||||
updated_at: thread.updated_at(),
|
||||
messages: thread
|
||||
.messages()
|
||||
.map(|message| SavedMessage {
|
||||
id: message.id,
|
||||
role: message.role,
|
||||
text: message.text.clone(),
|
||||
tool_uses: thread
|
||||
.map(|message| {
|
||||
let all_tool_uses = thread
|
||||
.tool_uses_for_message(message.id)
|
||||
.into_iter()
|
||||
.chain(thread.scripting_tool_uses_for_message(message.id))
|
||||
.map(|tool_use| SavedToolUse {
|
||||
id: tool_use.id,
|
||||
name: tool_use.name,
|
||||
input: tool_use.input,
|
||||
})
|
||||
.collect(),
|
||||
tool_results: thread
|
||||
.collect();
|
||||
let all_tool_results = thread
|
||||
.tool_results_for_message(message.id)
|
||||
.into_iter()
|
||||
.chain(thread.scripting_tool_results_for_message(message.id))
|
||||
.map(|tool_result| SavedToolResult {
|
||||
tool_use_id: tool_result.tool_use_id.clone(),
|
||||
is_error: tool_result.is_error,
|
||||
content: tool_result.content.clone(),
|
||||
})
|
||||
.collect(),
|
||||
.collect();
|
||||
|
||||
SavedMessage {
|
||||
id: message.id,
|
||||
role: message.role,
|
||||
text: message.text.clone(),
|
||||
tool_uses: all_tool_uses,
|
||||
tool_results: all_tool_results,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
70
crates/assistant2/src/tool_selector.rs
Normal file
70
crates/assistant2/src/tool_selector.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use gpui::Entity;
|
||||
use ui::{prelude::*, ContextMenu, IconButtonShape, PopoverMenu, Tooltip};
|
||||
|
||||
pub struct ToolSelector {
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
}
|
||||
|
||||
impl ToolSelector {
|
||||
pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut Context<Self>) -> Self {
|
||||
Self { tools }
|
||||
}
|
||||
|
||||
fn build_context_menu(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ContextMenu> {
|
||||
ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||
let tools_by_source = self.tools.tools_by_source(cx);
|
||||
|
||||
for (source, tools) in tools_by_source {
|
||||
menu = match source {
|
||||
ToolSource::Native => menu.header("Zed"),
|
||||
ToolSource::ContextServer { id } => menu.separator().header(id),
|
||||
};
|
||||
|
||||
for tool in tools {
|
||||
let source = tool.source();
|
||||
let name = tool.name().into();
|
||||
let is_enabled = self.tools.is_enabled(&source, &name);
|
||||
|
||||
menu =
|
||||
menu.toggleable_entry(tool.name(), is_enabled, IconPosition::End, None, {
|
||||
let tools = self.tools.clone();
|
||||
move |_window, _cx| {
|
||||
if is_enabled {
|
||||
tools.disable(source.clone(), &[name.clone()]);
|
||||
} else {
|
||||
tools.enable(source.clone(), &[name.clone()]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
menu
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ToolSelector {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
|
||||
let this = cx.entity().clone();
|
||||
PopoverMenu::new("tool-selector")
|
||||
.menu(move |window, cx| {
|
||||
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
|
||||
})
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("tool-selector-button", IconName::SettingsAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted),
|
||||
Tooltip::text("Customize Tools"),
|
||||
)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
}
|
||||
}
|
||||
@@ -46,25 +46,39 @@ impl ToolUseState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_saved_messages(messages: &[SavedMessage]) -> Self {
|
||||
/// Constructs a [`ToolUseState`] from the given list of [`SavedMessage`]s.
|
||||
///
|
||||
/// Accepts a function to filter the tools that should be used to populate the state.
|
||||
pub fn from_saved_messages(
|
||||
messages: &[SavedMessage],
|
||||
mut filter_by_tool_name: impl FnMut(&str) -> bool,
|
||||
) -> Self {
|
||||
let mut this = Self::new();
|
||||
let mut tool_names_by_id = HashMap::default();
|
||||
|
||||
for message in messages {
|
||||
match message.role {
|
||||
Role::Assistant => {
|
||||
if !message.tool_uses.is_empty() {
|
||||
this.tool_uses_by_assistant_message.insert(
|
||||
message.id,
|
||||
message
|
||||
.tool_uses
|
||||
let tool_uses = message
|
||||
.tool_uses
|
||||
.iter()
|
||||
.filter(|tool_use| (filter_by_tool_name)(tool_use.name.as_ref()))
|
||||
.map(|tool_use| LanguageModelToolUse {
|
||||
id: tool_use.id.clone(),
|
||||
name: tool_use.name.clone().into(),
|
||||
input: tool_use.input.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
tool_names_by_id.extend(
|
||||
tool_uses
|
||||
.iter()
|
||||
.map(|tool_use| LanguageModelToolUse {
|
||||
id: tool_use.id.clone(),
|
||||
name: tool_use.name.clone().into(),
|
||||
input: tool_use.input.clone(),
|
||||
})
|
||||
.collect(),
|
||||
.map(|tool_use| (tool_use.id.clone(), tool_use.name.clone())),
|
||||
);
|
||||
|
||||
this.tool_uses_by_assistant_message
|
||||
.insert(message.id, tool_uses);
|
||||
}
|
||||
}
|
||||
Role::User => {
|
||||
@@ -76,6 +90,14 @@ impl ToolUseState {
|
||||
|
||||
for tool_result in &message.tool_results {
|
||||
let tool_use_id = tool_result.tool_use_id.clone();
|
||||
let Some(tool_use) = tool_names_by_id.get(&tool_use_id) else {
|
||||
log::warn!("no tool name found for tool use: {tool_use_id:?}");
|
||||
continue;
|
||||
};
|
||||
|
||||
if !(filter_by_tool_name)(tool_use.as_ref()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tool_uses_by_user_message.push(tool_use_id.clone());
|
||||
this.tool_results.insert(
|
||||
@@ -202,7 +224,7 @@ impl ToolUseState {
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
output: Result<String>,
|
||||
) {
|
||||
) -> Option<PendingToolUse> {
|
||||
match output {
|
||||
Ok(output) => {
|
||||
self.tool_results.insert(
|
||||
@@ -213,7 +235,7 @@ impl ToolUseState {
|
||||
is_error: false,
|
||||
},
|
||||
);
|
||||
self.pending_tool_uses_by_id.remove(&tool_use_id);
|
||||
self.pending_tool_uses_by_id.remove(&tool_use_id)
|
||||
}
|
||||
Err(err) => {
|
||||
self.tool_results.insert(
|
||||
@@ -228,6 +250,8 @@ impl ToolUseState {
|
||||
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
|
||||
tool_use.status = PendingToolUseStatus::Error(err.to_string().into());
|
||||
}
|
||||
|
||||
self.pending_tool_uses_by_id.get(&tool_use_id).cloned()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -267,6 +291,7 @@ impl ToolUseState {
|
||||
pub struct PendingToolUse {
|
||||
pub id: LanguageModelToolUseId,
|
||||
/// The ID of the Assistant message in which the tool use was requested.
|
||||
#[allow(unused)]
|
||||
pub assistant_message_id: MessageId,
|
||||
pub name: Arc<str>,
|
||||
pub input: serde_json::Value,
|
||||
|
||||
@@ -103,7 +103,7 @@ impl RenderOnce for ContextPill {
|
||||
.pl_1()
|
||||
.pb(px(1.))
|
||||
.border_1()
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.gap_1()
|
||||
.child(self.icon().size(IconSize::XSmall).color(Color::Muted));
|
||||
|
||||
|
||||
@@ -647,7 +647,6 @@ impl AssistantContext {
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
id: ContextId,
|
||||
replica_id: ReplicaId,
|
||||
@@ -768,7 +767,6 @@ impl AssistantContext {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn deserialize(
|
||||
saved_context: SavedContext,
|
||||
path: PathBuf,
|
||||
|
||||
@@ -38,7 +38,7 @@ use language_model::{
|
||||
Role,
|
||||
};
|
||||
use language_model_selector::{
|
||||
assistant_language_model_selector, LanguageModelSelector, ToggleModelSelector,
|
||||
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::Picker;
|
||||
@@ -197,7 +197,8 @@ pub struct ContextEditor {
|
||||
// the file is opened. In order to keep the worktree alive for the duration of the
|
||||
// context editor, we keep a reference here.
|
||||
dragged_file_worktrees: Vec<Entity<Worktree>>,
|
||||
language_model_selector: PopoverMenuHandle<LanguageModelSelector>,
|
||||
language_model_selector: Entity<LanguageModelSelector>,
|
||||
language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
}
|
||||
|
||||
pub const DEFAULT_TAB_TITLE: &str = "New Chat";
|
||||
@@ -263,7 +264,7 @@ impl ContextEditor {
|
||||
image_blocks: Default::default(),
|
||||
scroll_position: None,
|
||||
remote_id: None,
|
||||
fs,
|
||||
fs: fs.clone(),
|
||||
workspace,
|
||||
project,
|
||||
pending_slash_command_creases: HashMap::default(),
|
||||
@@ -275,7 +276,20 @@ impl ContextEditor {
|
||||
show_accept_terms: false,
|
||||
slash_menu_handle: Default::default(),
|
||||
dragged_file_worktrees: Vec::new(),
|
||||
language_model_selector: PopoverMenuHandle::default(),
|
||||
language_model_selector: cx.new(|cx| {
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
language_model_selector_menu_handle: PopoverMenuHandle::default(),
|
||||
};
|
||||
this.update_message_headers(cx);
|
||||
this.update_image_blocks(cx);
|
||||
@@ -521,7 +535,6 @@ impl ContextEditor {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn run_command(
|
||||
&mut self,
|
||||
command_range: Range<language::Anchor>,
|
||||
@@ -1227,7 +1240,7 @@ impl ContextEditor {
|
||||
.child("Press")
|
||||
.child(
|
||||
h_flex()
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.px_1()
|
||||
.mr_0p5()
|
||||
.border_1()
|
||||
@@ -2043,7 +2056,6 @@ impl ContextEditor {
|
||||
.unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_patch_block(
|
||||
&mut self,
|
||||
range: Range<text::Anchor>,
|
||||
@@ -2078,7 +2090,7 @@ impl ContextEditor {
|
||||
.ml(gutter_width)
|
||||
.pb_1()
|
||||
.w(max_width - gutter_width)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.border_1()
|
||||
.border_color(theme.colors().border_variant)
|
||||
.overflow_hidden()
|
||||
@@ -2375,6 +2387,46 @@ impl ContextEditor {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_language_model_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
let focus_handle = self.editor().focus_handle(cx).clone();
|
||||
let model_name = match active_model {
|
||||
Some(model) => model.name().0,
|
||||
None => SharedString::from("No model selected"),
|
||||
};
|
||||
|
||||
LanguageModelSelectorPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
),
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Change Model",
|
||||
&ToggleModelSelector,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
gpui::Corner::BottomLeft,
|
||||
)
|
||||
.with_handle(self.language_model_selector_menu_handle.clone())
|
||||
}
|
||||
|
||||
fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let last_error = self.last_error.as_ref()?;
|
||||
|
||||
@@ -2818,9 +2870,8 @@ impl Render for ContextEditor {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let fs_clone = self.fs.clone();
|
||||
|
||||
let language_model_selector = self.language_model_selector.clone();
|
||||
let language_model_selector = self.language_model_selector_menu_handle.clone();
|
||||
v_flex()
|
||||
.key_context("ContextEditor")
|
||||
.capture_action(cx.listener(ContextEditor::cancel))
|
||||
@@ -2873,18 +2924,11 @@ impl Render for ContextEditor {
|
||||
.gap_1()
|
||||
.child(self.render_inject_context_menu(cx))
|
||||
.child(ui::Divider::vertical())
|
||||
.child(div().pl_0p5().child(assistant_language_model_selector(
|
||||
self.editor().focus_handle(cx),
|
||||
Some(self.language_model_selector.clone()),
|
||||
cx,
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs_clone.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
))),
|
||||
.child(
|
||||
div()
|
||||
.pl_0p5()
|
||||
.child(self.render_language_model_selector(cx)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -3376,7 +3420,7 @@ fn invoked_slash_command_fold_placeholder(
|
||||
.ml_6()
|
||||
.gap_2()
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.child(Label::new(format!("/{}", command.name.clone())))
|
||||
.map(|parent| match &command.status {
|
||||
InvokedSlashCommandStatus::Running(_) => {
|
||||
|
||||
@@ -104,49 +104,53 @@ impl ContextStore {
|
||||
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
|
||||
let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
|
||||
|
||||
let this = cx.new(|cx: &mut Context<Self>| {
|
||||
let context_server_factory_registry =
|
||||
ContextServerFactoryRegistry::default_global(cx);
|
||||
let context_server_manager = cx.new(|cx| {
|
||||
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
|
||||
});
|
||||
let mut this = Self {
|
||||
contexts: Vec::new(),
|
||||
contexts_metadata: Vec::new(),
|
||||
context_server_manager,
|
||||
context_server_slash_command_ids: HashMap::default(),
|
||||
host_contexts: Vec::new(),
|
||||
fs,
|
||||
languages,
|
||||
slash_commands,
|
||||
telemetry,
|
||||
_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();
|
||||
let this =
|
||||
cx.new(|cx: &mut Context<Self>| {
|
||||
let context_server_factory_registry =
|
||||
ContextServerFactoryRegistry::default_global(cx);
|
||||
let context_server_manager = cx.new(|cx| {
|
||||
ContextServerManager::new(
|
||||
context_server_factory_registry,
|
||||
project.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let mut this = Self {
|
||||
contexts: Vec::new(),
|
||||
contexts_metadata: Vec::new(),
|
||||
context_server_manager,
|
||||
context_server_slash_command_ids: HashMap::default(),
|
||||
host_contexts: Vec::new(),
|
||||
fs,
|
||||
languages,
|
||||
slash_commands,
|
||||
telemetry,
|
||||
_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(())
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
}),
|
||||
client_subscription: None,
|
||||
_project_subscriptions: vec![
|
||||
cx.observe(&project, Self::handle_project_changed),
|
||||
cx.subscribe(&project, Self::handle_project_event),
|
||||
],
|
||||
project_is_shared: false,
|
||||
client: project.read(cx).client(),
|
||||
project: project.clone(),
|
||||
prompt_builder,
|
||||
};
|
||||
this.handle_project_changed(project.clone(), cx);
|
||||
this.synchronize_contexts(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
this.reload(cx).detach_and_log_err(cx);
|
||||
this
|
||||
})?;
|
||||
.log_err()
|
||||
}),
|
||||
client_subscription: None,
|
||||
_project_subscriptions: vec![
|
||||
cx.subscribe(&project, Self::handle_project_event)
|
||||
],
|
||||
project_is_shared: false,
|
||||
client: project.read(cx).client(),
|
||||
project: project.clone(),
|
||||
prompt_builder,
|
||||
};
|
||||
this.handle_project_shared(project.clone(), cx);
|
||||
this.synchronize_contexts(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
this.reload(cx).detach_and_log_err(cx);
|
||||
this
|
||||
})?;
|
||||
|
||||
Ok(this)
|
||||
})
|
||||
@@ -288,7 +292,7 @@ impl ContextStore {
|
||||
})?
|
||||
}
|
||||
|
||||
fn handle_project_changed(&mut self, _: Entity<Project>, cx: &mut Context<Self>) {
|
||||
fn handle_project_shared(&mut self, _: Entity<Project>, cx: &mut Context<Self>) {
|
||||
let is_shared = self.project.read(cx).is_shared();
|
||||
let was_shared = mem::replace(&mut self.project_is_shared, is_shared);
|
||||
if is_shared == was_shared {
|
||||
@@ -318,11 +322,14 @@ impl ContextStore {
|
||||
|
||||
fn handle_project_event(
|
||||
&mut self,
|
||||
_: Entity<Project>,
|
||||
project: Entity<Project>,
|
||||
event: &project::Event,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
project::Event::RemoteIdChanged(_) => {
|
||||
self.handle_project_shared(project, cx);
|
||||
}
|
||||
project::Event::Reshared => {
|
||||
self.advertise_contexts(cx);
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ impl ResolvedPatch {
|
||||
buffer.edit(
|
||||
edits,
|
||||
Some(AutoindentMode::Block {
|
||||
original_start_columns: Vec::new(),
|
||||
original_indent_columns: Vec::new(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -5,9 +5,9 @@ use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWor
|
||||
use editor::{CompletionProvider, Editor};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
|
||||
use language::{Anchor, Buffer, LanguageServerId, ToPoint};
|
||||
use language::{Anchor, Buffer, ToPoint};
|
||||
use parking_lot::Mutex;
|
||||
use project::{lsp_store::CompletionDocumentation, CompletionIntent};
|
||||
use project::{lsp_store::CompletionDocumentation, CompletionIntent, CompletionSource};
|
||||
use rope::Point;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
@@ -125,10 +125,8 @@ impl SlashCommandCompletionProvider {
|
||||
)),
|
||||
new_text,
|
||||
label: command.label(cx),
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
confirm,
|
||||
resolved: true,
|
||||
source: CompletionSource::Custom,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
@@ -136,7 +134,6 @@ impl SlashCommandCompletionProvider {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn complete_command_argument(
|
||||
&self,
|
||||
command_name: &str,
|
||||
@@ -225,10 +222,8 @@ impl SlashCommandCompletionProvider {
|
||||
label: new_argument.label,
|
||||
new_text,
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
confirm,
|
||||
resolved: true,
|
||||
source: CompletionSource::Custom,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
|
||||
@@ -88,7 +88,6 @@ pub trait SlashCommand: 'static + Send + Sync {
|
||||
fn accepts_arguments(&self) -> bool {
|
||||
self.requires_argument()
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
|
||||
@@ -16,9 +16,7 @@ anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
derive_more.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
parking_lot.workspace = true
|
||||
project.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
workspace.workspace = true
|
||||
ui.workspace = true
|
||||
|
||||
@@ -4,17 +4,8 @@ mod tool_working_set;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::AnyElement;
|
||||
use gpui::IntoElement;
|
||||
use gpui::{App, Task, WeakEntity, Window};
|
||||
use language::Language;
|
||||
use ui::div;
|
||||
use ui::Label;
|
||||
use ui::LabelCommon;
|
||||
use ui::LabelSize;
|
||||
use ui::ParentElement;
|
||||
use ui::SharedString;
|
||||
use workspace::Workspace;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use project::Project;
|
||||
|
||||
pub use crate::tool_registry::*;
|
||||
pub use crate::tool_working_set::*;
|
||||
@@ -23,6 +14,14 @@ pub fn init(cx: &mut App) {
|
||||
ToolRegistry::default_global(cx);
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||
pub enum ToolSource {
|
||||
/// A native tool built-in to Zed.
|
||||
Native,
|
||||
/// A tool provided by a context server.
|
||||
ContextServer { id: SharedString },
|
||||
}
|
||||
|
||||
/// A tool that can be used by a language model.
|
||||
pub trait Tool: 'static + Send + Sync {
|
||||
/// Returns the name of the tool.
|
||||
@@ -31,6 +30,11 @@ pub trait Tool: 'static + Send + Sync {
|
||||
/// Returns the description of the tool.
|
||||
fn description(&self) -> String;
|
||||
|
||||
/// Returns the source of the tool.
|
||||
fn source(&self) -> ToolSource {
|
||||
ToolSource::Native
|
||||
}
|
||||
|
||||
/// Returns the JSON schema that describes the tool's input.
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
serde_json::Value::Object(serde_json::Map::default())
|
||||
@@ -40,52 +44,7 @@ pub trait Tool: 'static + Send + Sync {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
thread_id: Arc<str>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>>;
|
||||
|
||||
/// Renders the tool's input when the user expands it.
|
||||
fn render_input(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_lua_language: Option<Arc<Language>>,
|
||||
_cx: &mut App,
|
||||
) -> AnyElement {
|
||||
default_render_input(input)
|
||||
}
|
||||
|
||||
/// Renders the tool's output when the user expands it.
|
||||
fn render_output(self: Arc<Self>, output: SharedString, _cx: &mut App) -> AnyElement {
|
||||
default_render_output(output)
|
||||
}
|
||||
|
||||
/// Renders the tool's error message when the user expands it.
|
||||
fn render_error(self: Arc<Self>, err: SharedString, _cx: &mut App) -> AnyElement {
|
||||
default_render_error(err)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_render_input(input: serde_json::Value) -> AnyElement {
|
||||
div()
|
||||
.child(Label::new("Input:").size(LabelSize::Small))
|
||||
.child(Label::new(
|
||||
serde_json::to_string_pretty(&input).unwrap_or_default(),
|
||||
))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
pub fn default_render_output(output: SharedString) -> AnyElement {
|
||||
div()
|
||||
.child(Label::new("Result:").size(LabelSize::Small))
|
||||
.child(Label::new(output))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
pub fn default_render_error(err: SharedString) -> AnyElement {
|
||||
div()
|
||||
.child(Label::new("Error:").size(LabelSize::Small))
|
||||
.child(Label::new(err))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use collections::HashMap;
|
||||
use collections::{HashMap, HashSet, IndexMap};
|
||||
use gpui::App;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::{Tool, ToolRegistry};
|
||||
use crate::{Tool, ToolRegistry, ToolSource};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Default)]
|
||||
pub struct ToolId(usize);
|
||||
@@ -19,6 +19,7 @@ pub struct ToolWorkingSet {
|
||||
struct WorkingSetState {
|
||||
context_server_tools_by_id: HashMap<ToolId, Arc<dyn Tool>>,
|
||||
context_server_tools_by_name: HashMap<String, Arc<dyn Tool>>,
|
||||
disabled_tools_by_source: HashMap<ToolSource, HashSet<Arc<str>>>,
|
||||
next_tool_id: ToolId,
|
||||
}
|
||||
|
||||
@@ -45,22 +46,80 @@ impl ToolWorkingSet {
|
||||
tools
|
||||
}
|
||||
|
||||
pub fn insert(&self, command: Arc<dyn Tool>) -> ToolId {
|
||||
pub fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
|
||||
let all_tools = self.tools(cx);
|
||||
|
||||
all_tools
|
||||
.into_iter()
|
||||
.filter(|tool| self.is_enabled(&tool.source(), &tool.name().into()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn tools_by_source(&self, cx: &App) -> IndexMap<ToolSource, Vec<Arc<dyn Tool>>> {
|
||||
let mut tools_by_source = IndexMap::default();
|
||||
|
||||
for tool in self.tools(cx) {
|
||||
tools_by_source
|
||||
.entry(tool.source())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(tool);
|
||||
}
|
||||
|
||||
for tools in tools_by_source.values_mut() {
|
||||
tools.sort_by_key(|tool| tool.name());
|
||||
}
|
||||
|
||||
tools_by_source.sort_unstable_keys();
|
||||
|
||||
tools_by_source
|
||||
}
|
||||
|
||||
pub fn insert(&self, tool: Arc<dyn Tool>) -> ToolId {
|
||||
let mut state = self.state.lock();
|
||||
let command_id = state.next_tool_id;
|
||||
let tool_id = state.next_tool_id;
|
||||
state.next_tool_id.0 += 1;
|
||||
state
|
||||
.context_server_tools_by_id
|
||||
.insert(command_id, command.clone());
|
||||
.insert(tool_id, tool.clone());
|
||||
state.tools_changed();
|
||||
command_id
|
||||
tool_id
|
||||
}
|
||||
|
||||
pub fn remove(&self, command_ids_to_remove: &[ToolId]) {
|
||||
pub fn is_enabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||
!self.is_disabled(source, name)
|
||||
}
|
||||
|
||||
pub fn is_disabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||
let state = self.state.lock();
|
||||
state
|
||||
.disabled_tools_by_source
|
||||
.get(source)
|
||||
.map_or(false, |disabled_tools| disabled_tools.contains(name))
|
||||
}
|
||||
|
||||
pub fn enable(&self, source: ToolSource, tools_to_enable: &[Arc<str>]) {
|
||||
let mut state = self.state.lock();
|
||||
state
|
||||
.disabled_tools_by_source
|
||||
.entry(source)
|
||||
.or_default()
|
||||
.retain(|name| !tools_to_enable.contains(name));
|
||||
}
|
||||
|
||||
pub fn disable(&self, source: ToolSource, tools_to_disable: &[Arc<str>]) {
|
||||
let mut state = self.state.lock();
|
||||
state
|
||||
.disabled_tools_by_source
|
||||
.entry(source)
|
||||
.or_default()
|
||||
.extend(tools_to_disable.into_iter().cloned());
|
||||
}
|
||||
|
||||
pub fn remove(&self, tool_ids_to_remove: &[ToolId]) {
|
||||
let mut state = self.state.lock();
|
||||
state
|
||||
.context_server_tools_by_id
|
||||
.retain(|id, _| !command_ids_to_remove.contains(id));
|
||||
.retain(|id, _| !tool_ids_to_remove.contains(id));
|
||||
state.tools_changed();
|
||||
}
|
||||
}
|
||||
@@ -71,7 +130,7 @@ impl WorkingSetState {
|
||||
self.context_server_tools_by_name.extend(
|
||||
self.context_server_tools_by_id
|
||||
.values()
|
||||
.map(|command| (command.name(), command.clone())),
|
||||
.map(|tool| (tool.name(), tool.clone())),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
chrono.workspace = true
|
||||
gpui.workspace = true
|
||||
project.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
mod list_worktrees_tool;
|
||||
mod now_tool;
|
||||
mod read_file_tool;
|
||||
|
||||
use assistant_tool::ToolRegistry;
|
||||
use gpui::App;
|
||||
|
||||
use crate::list_worktrees_tool::ListWorktreesTool;
|
||||
use crate::now_tool::NowTool;
|
||||
use crate::read_file_tool::ReadFileTool;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
assistant_tool::init(cx);
|
||||
|
||||
let registry = ToolRegistry::global(cx);
|
||||
registry.register_tool(NowTool);
|
||||
registry.register_tool(ListWorktreesTool);
|
||||
registry.register_tool(ReadFileTool);
|
||||
}
|
||||
|
||||
77
crates/assistant_tools/src/list_worktrees_tool.rs
Normal file
77
crates/assistant_tools/src/list_worktrees_tool.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{App, Entity, Task};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ListWorktreesToolInput {}
|
||||
|
||||
pub struct ListWorktreesTool;
|
||||
|
||||
impl Tool for ListWorktreesTool {
|
||||
fn name(&self) -> String {
|
||||
"list-worktrees".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Lists all worktrees in the current project. Use this tool when you need to find available worktrees and their IDs.".into()
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
serde_json::json!(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_input: serde_json::Value,
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
cx.spawn(|cx| async move {
|
||||
cx.update(|cx| {
|
||||
#[derive(Debug, Serialize)]
|
||||
struct WorktreeInfo {
|
||||
id: usize,
|
||||
root_name: String,
|
||||
root_dir: Option<String>,
|
||||
}
|
||||
|
||||
let worktrees = project.update(cx, |project, cx| {
|
||||
project
|
||||
.visible_worktrees(cx)
|
||||
.map(|worktree| {
|
||||
worktree.read_with(cx, |worktree, _cx| WorktreeInfo {
|
||||
id: worktree.id().to_usize(),
|
||||
root_dir: worktree
|
||||
.root_dir()
|
||||
.map(|root_dir| root_dir.to_string_lossy().to_string()),
|
||||
root_name: worktree.root_name().to_string(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
if worktrees.is_empty() {
|
||||
return Ok("No worktrees found in the current project.".to_string());
|
||||
}
|
||||
|
||||
let mut result = String::from("Worktrees in the current project:\n\n");
|
||||
for worktree in worktrees {
|
||||
result.push_str(&serde_json::to_string(&worktree)?);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
})?
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::Tool;
|
||||
use chrono::{Local, Utc};
|
||||
use gpui::{App, Task, WeakEntity, Window};
|
||||
use gpui::{App, Entity, Task};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -40,9 +42,7 @@ impl Tool for NowTool {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_thread_id: Arc<str>,
|
||||
_workspace: WeakEntity<workspace::Workspace>,
|
||||
_window: &mut Window,
|
||||
_project: Entity<Project>,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input: NowToolInput = match serde_json::from_value(input) {
|
||||
|
||||
62
crates/assistant_tools/src/read_file_tool.rs
Normal file
62
crates/assistant_tools/src/read_file_tool.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{App, Entity, Task};
|
||||
use project::{Project, ProjectPath, WorktreeId};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ReadFileToolInput {
|
||||
/// The ID of the worktree in which the file resides.
|
||||
pub worktree_id: usize,
|
||||
/// The path to the file to read.
|
||||
///
|
||||
/// This path is relative to the worktree root, it must not be an absolute path.
|
||||
pub path: Arc<Path>,
|
||||
}
|
||||
|
||||
pub struct ReadFileTool;
|
||||
|
||||
impl Tool for ReadFileTool {
|
||||
fn name(&self) -> String {
|
||||
"read-file".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Reads the content of a file specified by a worktree ID and path. Use this tool when you need to access the contents of a file in the project.".into()
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(ReadFileToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input = match serde_json::from_value::<ReadFileToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: WorktreeId::from_usize(input.worktree_id),
|
||||
path: input.path,
|
||||
};
|
||||
cx.spawn(|cx| async move {
|
||||
let buffer = cx
|
||||
.update(|cx| {
|
||||
project.update(cx, |project, cx| project.open_buffer(project_path, cx))
|
||||
})?
|
||||
.await?;
|
||||
|
||||
cx.update(|cx| buffer.read(cx).text())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -663,11 +663,13 @@ impl std::fmt::Debug for BufferDiff {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum BufferDiffEvent {
|
||||
DiffChanged {
|
||||
changed_range: Option<Range<text::Anchor>>,
|
||||
},
|
||||
LanguageChanged,
|
||||
HunksStagedOrUnstaged(Option<Rope>),
|
||||
}
|
||||
|
||||
impl EventEmitter<BufferDiffEvent> for BufferDiff {}
|
||||
@@ -762,6 +764,17 @@ impl BufferDiff {
|
||||
self.secondary_diff.clone()
|
||||
}
|
||||
|
||||
pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some(secondary_diff) = &self.secondary_diff {
|
||||
secondary_diff.update(cx, |diff, _| {
|
||||
diff.inner.pending_hunks.clear();
|
||||
});
|
||||
cx.emit(BufferDiffEvent::DiffChanged {
|
||||
changed_range: Some(Anchor::MIN..Anchor::MAX),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stage_or_unstage_hunks(
|
||||
&mut self,
|
||||
stage: bool,
|
||||
@@ -784,6 +797,9 @@ impl BufferDiff {
|
||||
}
|
||||
});
|
||||
}
|
||||
cx.emit(BufferDiffEvent::HunksStagedOrUnstaged(
|
||||
new_index_text.clone(),
|
||||
));
|
||||
if let Some((first, last)) = hunks.first().zip(hunks.last()) {
|
||||
let changed_range = first.buffer_range.start..last.buffer_range.end;
|
||||
cx.emit(BufferDiffEvent::DiffChanged {
|
||||
@@ -812,7 +828,6 @@ impl BufferDiff {
|
||||
Some(start..end)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn update_diff(
|
||||
this: Entity<BufferDiff>,
|
||||
buffer: text::BufferSnapshot,
|
||||
@@ -822,8 +837,8 @@ impl BufferDiff {
|
||||
language: Option<Arc<Language>>,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> anyhow::Result<Option<Range<Anchor>>> {
|
||||
let snapshot = if base_text_changed || language_changed {
|
||||
) -> anyhow::Result<BufferDiffSnapshot> {
|
||||
let inner = if base_text_changed || language_changed {
|
||||
cx.update(|cx| {
|
||||
Self::build(
|
||||
buffer.clone(),
|
||||
@@ -845,18 +860,45 @@ impl BufferDiff {
|
||||
})?
|
||||
.await
|
||||
};
|
||||
|
||||
this.update(cx, |this, _| this.set_state(snapshot, &buffer))
|
||||
Ok(BufferDiffSnapshot {
|
||||
inner,
|
||||
secondary_diff: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_diff_from(
|
||||
pub fn set_snapshot(
|
||||
&mut self,
|
||||
buffer: &text::BufferSnapshot,
|
||||
other: &Entity<Self>,
|
||||
new_snapshot: BufferDiffSnapshot,
|
||||
language_changed: bool,
|
||||
secondary_changed_range: Option<Range<Anchor>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Range<Anchor>> {
|
||||
let other = other.read(cx).inner.clone();
|
||||
self.set_state(other, buffer)
|
||||
let changed_range = self.set_state(new_snapshot.inner, buffer);
|
||||
if language_changed {
|
||||
cx.emit(BufferDiffEvent::LanguageChanged);
|
||||
}
|
||||
|
||||
let changed_range = match (secondary_changed_range, changed_range) {
|
||||
(None, None) => None,
|
||||
(Some(unstaged_range), None) => self.range_to_hunk_range(unstaged_range, &buffer, cx),
|
||||
(None, Some(uncommitted_range)) => Some(uncommitted_range),
|
||||
(Some(unstaged_range), Some(uncommitted_range)) => {
|
||||
let mut start = uncommitted_range.start;
|
||||
let mut end = uncommitted_range.end;
|
||||
if let Some(unstaged_range) = self.range_to_hunk_range(unstaged_range, &buffer, cx)
|
||||
{
|
||||
start = unstaged_range.start.min(&uncommitted_range.start, &buffer);
|
||||
end = unstaged_range.end.max(&uncommitted_range.end, &buffer);
|
||||
}
|
||||
Some(start..end)
|
||||
}
|
||||
};
|
||||
|
||||
cx.emit(BufferDiffEvent::DiffChanged {
|
||||
changed_range: changed_range.clone(),
|
||||
});
|
||||
changed_range
|
||||
}
|
||||
|
||||
fn set_state(
|
||||
@@ -900,6 +942,14 @@ impl BufferDiff {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hunks<'a>(
|
||||
&'a self,
|
||||
buffer_snapshot: &'a text::BufferSnapshot,
|
||||
cx: &'a App,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
self.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer_snapshot, cx)
|
||||
}
|
||||
|
||||
pub fn hunks_intersecting_range<'a>(
|
||||
&'a self,
|
||||
range: Range<text::Anchor>,
|
||||
|
||||
@@ -418,6 +418,8 @@ impl Telemetry {
|
||||
|
||||
fn report_event(self: &Arc<Self>, event: Event) {
|
||||
let mut state = self.state.lock();
|
||||
// RUST_LOG=telemetry=trace to debug telemetry events
|
||||
log::trace!(target: "telemetry", "{:?}", event);
|
||||
|
||||
if !state.settings.metrics {
|
||||
return;
|
||||
|
||||
@@ -229,7 +229,6 @@ impl Database {
|
||||
}
|
||||
|
||||
/// Creates a new channel message.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn create_channel_message(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
||||
@@ -122,7 +122,6 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn get_or_create_user_by_github_account_tx(
|
||||
&self,
|
||||
github_login: &str,
|
||||
|
||||
@@ -289,7 +289,6 @@ impl LlmDatabase {
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn record_usage(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
@@ -554,7 +553,6 @@ impl LlmDatabase {
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn update_usage_for_measure(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
|
||||
@@ -33,7 +33,6 @@ pub struct LlmTokenClaims {
|
||||
const LLM_TOKEN_LIFETIME: Duration = Duration::from_secs(60 * 60);
|
||||
|
||||
impl LlmTokenClaims {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn create(
|
||||
user: &user::Model,
|
||||
is_staff: bool,
|
||||
|
||||
@@ -308,7 +308,7 @@ impl Server {
|
||||
.add_request_handler(forward_read_only_project_request::<proto::InlayHints>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::ResolveInlayHint>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitBranches>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitGetBranches>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenUnstagedDiff>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenUncommittedDiff>)
|
||||
.add_request_handler(
|
||||
@@ -393,9 +393,6 @@ impl Server {
|
||||
.add_request_handler(forward_mutating_project_request::<proto::OpenContext>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::CreateContext>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::SynchronizeContexts>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Push>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Pull>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Fetch>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Stage>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Unstage>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Commit>)
|
||||
@@ -405,6 +402,10 @@ impl Server {
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitCheckoutFiles>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::SetIndexText>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::OpenCommitMessageBuffer>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GitDiff>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GitCreateBranch>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GitChangeBranch>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::CheckForPushedCommits>)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
|
||||
.add_message_handler(update_context)
|
||||
.add_request_handler({
|
||||
@@ -696,7 +697,6 @@ impl Server {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn handle_connection(
|
||||
self: &Arc<Self>,
|
||||
connection: Connection,
|
||||
@@ -1080,7 +1080,6 @@ pub fn routes(server: Arc<Server>) -> Router<(), Body> {
|
||||
.layer(Extension(server))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn handle_websocket_request(
|
||||
TypedHeader(ProtocolVersion(protocol_version)): TypedHeader<ProtocolVersion>,
|
||||
app_version_header: Option<TypedHeader<AppVersionHeader>>,
|
||||
|
||||
@@ -3,7 +3,6 @@ use crate::{
|
||||
tests::{rust_lang, TestServer},
|
||||
};
|
||||
use call::ActiveCall;
|
||||
use collections::HashMap;
|
||||
use editor::{
|
||||
actions::{
|
||||
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, Redo, Rename,
|
||||
@@ -1983,7 +1982,6 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
blame_entry("3a3a3a", 2..3),
|
||||
blame_entry("4c4c4c", 3..4),
|
||||
],
|
||||
permalinks: HashMap::default(), // This field is deprecrated
|
||||
messages: [
|
||||
("1b1b1b", "message for idx-0"),
|
||||
("0d0d0d", "message for idx-1"),
|
||||
@@ -2027,6 +2025,15 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
let buffer_id_b = editor_b.update(cx_b, |editor_b, cx| {
|
||||
editor_b
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.remote_id()
|
||||
});
|
||||
|
||||
// client_b now requests git blame for the open buffer
|
||||
editor_b.update_in(cx_b, |editor_b, window, cx| {
|
||||
@@ -2045,6 +2052,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
&(0..4)
|
||||
.map(|row| RowInfo {
|
||||
buffer_row: Some(row),
|
||||
buffer_id: Some(buffer_id_b),
|
||||
..Default::default()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -2092,6 +2100,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
&(0..4)
|
||||
.map(|row| RowInfo {
|
||||
buffer_row: Some(row),
|
||||
buffer_id: Some(buffer_id_b),
|
||||
..Default::default()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -2127,6 +2136,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
&(0..4)
|
||||
.map(|row| RowInfo {
|
||||
buffer_row: Some(row),
|
||||
buffer_id: Some(buffer_id_b),
|
||||
..Default::default()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
|
||||
@@ -6741,19 +6741,24 @@ async fn test_remote_git_branches(
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/project", cx_a).await;
|
||||
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
let root_path = ProjectPath::root_path(worktree_id);
|
||||
// Client A sees that a guest has joined.
|
||||
// Client A sees that a guest has joined and the repo has been populated
|
||||
executor.run_until_parked();
|
||||
|
||||
let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
|
||||
|
||||
let root_path = ProjectPath::root_path(worktree_id);
|
||||
|
||||
let branches_b = cx_b
|
||||
.update(|cx| project_b.update(cx, |project, cx| project.branches(root_path.clone(), cx)))
|
||||
.update(|cx| repo_b.update(cx, |repository, _| repository.branches()))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let new_branch = branches[2];
|
||||
@@ -6765,13 +6770,10 @@ async fn test_remote_git_branches(
|
||||
|
||||
assert_eq!(branches_b, branches_set);
|
||||
|
||||
cx_b.update(|cx| {
|
||||
project_b.update(cx, |project, cx| {
|
||||
project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
@@ -6788,13 +6790,15 @@ async fn test_remote_git_branches(
|
||||
assert_eq!(host_branch.name, branches[2]);
|
||||
|
||||
// Also try creating a new branch
|
||||
cx_b.update(|cx| {
|
||||
project_b.update(cx, |project, cx| {
|
||||
project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_b.update(|cx| repo_b.read(cx).create_branch("totally-new-branch"))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
cx_b.update(|cx| repo_b.read(cx).change_branch("totally-new-branch"))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
|
||||
@@ -463,7 +463,6 @@ impl<T: RandomizedTest> TestPlan<T> {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn apply_server_operation(
|
||||
plan: Arc<Mutex<Self>>,
|
||||
deterministic: BackgroundExecutor,
|
||||
|
||||
@@ -276,11 +276,13 @@ async fn test_ssh_collaboration_git_branches(
|
||||
// has some git repositories
|
||||
executor.run_until_parked();
|
||||
|
||||
let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
|
||||
let root_path = ProjectPath::root_path(worktree_id);
|
||||
|
||||
let branches_b = cx_b
|
||||
.update(|cx| project_b.update(cx, |project, cx| project.branches(root_path.clone(), cx)))
|
||||
.update(|cx| repo_b.read(cx).branches())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let new_branch = branches[2];
|
||||
@@ -292,13 +294,10 @@ async fn test_ssh_collaboration_git_branches(
|
||||
|
||||
assert_eq!(&branches_b, &branches_set);
|
||||
|
||||
cx_b.update(|cx| {
|
||||
project_b.update(cx, |project, cx| {
|
||||
project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
@@ -317,13 +316,15 @@ async fn test_ssh_collaboration_git_branches(
|
||||
assert_eq!(server_branch.name, branches[2]);
|
||||
|
||||
// Also try creating a new branch
|
||||
cx_b.update(|cx| {
|
||||
project_b.update(cx, |project, cx| {
|
||||
project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_b.update(|cx| repo_b.read(cx).create_branch("totally-new-branch"))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
cx_b.update(|cx| repo_b.read(cx).change_branch("totally-new-branch"))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
|
||||
@@ -323,7 +323,7 @@ impl ChatPanel {
|
||||
.my_0p5()
|
||||
.px_0p5()
|
||||
.gap_x_1()
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.child(Icon::new(IconName::ReplyArrowRight).color(Color::Muted))
|
||||
.when(reply_to_message.is_none(), |el| {
|
||||
el.child(
|
||||
@@ -358,7 +358,7 @@ impl ChatPanel {
|
||||
.my_0p5()
|
||||
.px_0p5()
|
||||
.gap_x_1()
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.overflow_hidden()
|
||||
.hover(|style| style.bg(cx.theme().colors().element_background))
|
||||
.child(Icon::new(IconName::ReplyArrowRight).color(Color::Muted))
|
||||
@@ -476,7 +476,7 @@ impl ChatPanel {
|
||||
div()
|
||||
.group("")
|
||||
.bg(background)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.overflow_hidden()
|
||||
.px_1p5()
|
||||
.py_0p5()
|
||||
@@ -563,7 +563,7 @@ impl ChatPanel {
|
||||
.child(
|
||||
div()
|
||||
.px_1()
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.text_ui_xs(cx)
|
||||
.bg(cx.theme().colors().background)
|
||||
.child("New messages"),
|
||||
@@ -589,7 +589,7 @@ impl ChatPanel {
|
||||
div()
|
||||
.w_6()
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover).rounded_md())
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover).rounded_sm())
|
||||
.child(child)
|
||||
}
|
||||
|
||||
@@ -604,7 +604,7 @@ impl ChatPanel {
|
||||
.absolute()
|
||||
.right_2()
|
||||
.overflow_hidden()
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.border_color(cx.theme().colors().element_selected)
|
||||
.border_1()
|
||||
.when(!self.has_open_menu(message_id), |el| {
|
||||
|
||||
@@ -10,9 +10,9 @@ use gpui::{
|
||||
};
|
||||
use language::{
|
||||
language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry,
|
||||
LanguageServerId, ToOffset,
|
||||
ToOffset,
|
||||
};
|
||||
use project::{search::SearchQuery, Completion};
|
||||
use project::{search::SearchQuery, Completion, CompletionSource};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
@@ -309,11 +309,9 @@ impl MessageEditor {
|
||||
old_range: range.clone(),
|
||||
new_text,
|
||||
label,
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(0), // TODO: Make this optional or something?
|
||||
lsp_completion: Default::default(), // TODO: Make this optional or something?
|
||||
confirm: None,
|
||||
resolved: true,
|
||||
documentation: None,
|
||||
source: CompletionSource::Custom,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
@@ -531,7 +529,7 @@ impl Render for MessageEditor {
|
||||
.px_2()
|
||||
.py_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.child(EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
|
||||
@@ -869,7 +869,6 @@ impl CollabPanel {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_participant_project(
|
||||
&self,
|
||||
project_id: u64,
|
||||
|
||||
@@ -300,7 +300,7 @@ impl NotificationPanel {
|
||||
.hover(|style| {
|
||||
style
|
||||
.bg(cx.theme().colors().element_selected)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
})
|
||||
.child(Label::new(relative_timestamp).color(Color::Muted))
|
||||
.tooltip(move |_, cx| {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::fmt::Display;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
@@ -8,7 +9,7 @@ use parking_lot::RwLock;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
pub trait Component {
|
||||
fn scope() -> Option<&'static str>;
|
||||
fn scope() -> Option<ComponentScope>;
|
||||
fn name() -> &'static str {
|
||||
std::any::type_name::<Self>()
|
||||
}
|
||||
@@ -31,7 +32,7 @@ pub static COMPONENT_DATA: LazyLock<RwLock<ComponentRegistry>> =
|
||||
LazyLock::new(|| RwLock::new(ComponentRegistry::new()));
|
||||
|
||||
pub struct ComponentRegistry {
|
||||
components: Vec<(Option<&'static str>, &'static str, Option<&'static str>)>,
|
||||
components: Vec<(Option<ComponentScope>, &'static str, Option<&'static str>)>,
|
||||
previews: HashMap<&'static str, fn(&mut Window, &mut App) -> AnyElement>,
|
||||
}
|
||||
|
||||
@@ -77,18 +78,23 @@ pub struct ComponentId(pub &'static str);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ComponentMetadata {
|
||||
id: ComponentId,
|
||||
name: SharedString,
|
||||
scope: Option<SharedString>,
|
||||
scope: Option<ComponentScope>,
|
||||
description: Option<SharedString>,
|
||||
preview: Option<fn(&mut Window, &mut App) -> AnyElement>,
|
||||
}
|
||||
|
||||
impl ComponentMetadata {
|
||||
pub fn id(&self) -> ComponentId {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
pub fn name(&self) -> SharedString {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
pub fn scope(&self) -> Option<SharedString> {
|
||||
pub fn scope(&self) -> Option<ComponentScope> {
|
||||
self.scope.clone()
|
||||
}
|
||||
|
||||
@@ -152,14 +158,16 @@ pub fn components() -> AllComponents {
|
||||
let data = COMPONENT_DATA.read();
|
||||
let mut all_components = AllComponents::new();
|
||||
|
||||
for &(scope, name, description) in &data.components {
|
||||
let scope = scope.map(Into::into);
|
||||
for (ref scope, name, description) in &data.components {
|
||||
let preview = data.previews.get(name).cloned();
|
||||
let component_name = SharedString::new_static(name);
|
||||
let id = ComponentId(name);
|
||||
all_components.insert(
|
||||
ComponentId(name),
|
||||
id.clone(),
|
||||
ComponentMetadata {
|
||||
name: name.into(),
|
||||
scope,
|
||||
id,
|
||||
name: component_name,
|
||||
scope: scope.clone(),
|
||||
description: description.map(Into::into),
|
||||
preview,
|
||||
},
|
||||
@@ -169,6 +177,59 @@ pub fn components() -> AllComponents {
|
||||
all_components
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum ComponentScope {
|
||||
Layout,
|
||||
Input,
|
||||
Notification,
|
||||
Editor,
|
||||
Collaboration,
|
||||
VersionControl,
|
||||
Unknown(SharedString),
|
||||
}
|
||||
|
||||
impl Display for ComponentScope {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ComponentScope::Layout => write!(f, "Layout"),
|
||||
ComponentScope::Input => write!(f, "Input"),
|
||||
ComponentScope::Notification => write!(f, "Notification"),
|
||||
ComponentScope::Editor => write!(f, "Editor"),
|
||||
ComponentScope::Collaboration => write!(f, "Collaboration"),
|
||||
ComponentScope::VersionControl => write!(f, "Version Control"),
|
||||
ComponentScope::Unknown(name) => write!(f, "Unknown: {}", name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ComponentScope {
|
||||
fn from(value: &str) -> Self {
|
||||
match value {
|
||||
"Layout" => ComponentScope::Layout,
|
||||
"Input" => ComponentScope::Input,
|
||||
"Notification" => ComponentScope::Notification,
|
||||
"Editor" => ComponentScope::Editor,
|
||||
"Collaboration" => ComponentScope::Collaboration,
|
||||
"Version Control" | "VersionControl" => ComponentScope::VersionControl,
|
||||
_ => ComponentScope::Unknown(SharedString::new(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ComponentScope {
|
||||
fn from(value: String) -> Self {
|
||||
match value.as_str() {
|
||||
"Layout" => ComponentScope::Layout,
|
||||
"Input" => ComponentScope::Input,
|
||||
"Notification" => ComponentScope::Notification,
|
||||
"Editor" => ComponentScope::Editor,
|
||||
"Collaboration" => ComponentScope::Collaboration,
|
||||
"Version Control" | "VersionControl" => ComponentScope::VersionControl,
|
||||
_ => ComponentScope::Unknown(SharedString::new(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Which side of the preview to show labels on
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ExampleLabelSide {
|
||||
@@ -177,8 +238,8 @@ pub enum ExampleLabelSide {
|
||||
/// Right side
|
||||
Right,
|
||||
/// Top side
|
||||
Top,
|
||||
#[default]
|
||||
Top,
|
||||
/// Bottom side
|
||||
Bottom,
|
||||
}
|
||||
@@ -208,6 +269,7 @@ impl RenderOnce for ComponentExample {
|
||||
.text_size(px(10.))
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.when(self.grow, |this| this.flex_1())
|
||||
.when(!self.grow, |this| this.flex_none())
|
||||
.child(self.element)
|
||||
.child(self.variant_name)
|
||||
.into_any_element()
|
||||
|
||||
@@ -15,7 +15,12 @@ path = "src/component_preview.rs"
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
client.workspace = true
|
||||
component.workspace = true
|
||||
gpui.workspace = true
|
||||
languages.workspace = true
|
||||
project.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
notifications.workspace = true
|
||||
collections.workspace = true
|
||||
|
||||
@@ -2,18 +2,51 @@
|
||||
//!
|
||||
//! A view for exploring Zed components.
|
||||
|
||||
use component::{components, ComponentMetadata};
|
||||
use gpui::{list, prelude::*, uniform_list, App, EventEmitter, FocusHandle, Focusable, Window};
|
||||
use std::iter::Iterator;
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::UserStore;
|
||||
use component::{components, ComponentId, ComponentMetadata};
|
||||
use gpui::{
|
||||
list, prelude::*, uniform_list, App, Entity, EventEmitter, FocusHandle, Focusable, Task,
|
||||
WeakEntity, Window,
|
||||
};
|
||||
|
||||
use collections::HashMap;
|
||||
|
||||
use gpui::{ListState, ScrollHandle, UniformListScrollHandle};
|
||||
use ui::{prelude::*, ListItem};
|
||||
use languages::LanguageRegistry;
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use project::Project;
|
||||
use ui::{prelude::*, Divider, ListItem, ListSubHeader};
|
||||
|
||||
use workspace::{item::ItemEvent, Item, Workspace, WorkspaceId};
|
||||
use workspace::{AppState, ItemId, SerializableItem};
|
||||
|
||||
pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
||||
let app_state = app_state.clone();
|
||||
|
||||
cx.observe_new(move |workspace: &mut Workspace, _, cx| {
|
||||
let app_state = app_state.clone();
|
||||
let weak_workspace = cx.entity().downgrade();
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.observe_new(|workspace: &mut Workspace, _, _cx| {
|
||||
workspace.register_action(
|
||||
|workspace, _: &workspace::OpenComponentPreview, window, cx| {
|
||||
let component_preview = cx.new(|cx| ComponentPreview::new(window, cx));
|
||||
move |workspace, _: &workspace::OpenComponentPreview, window, cx| {
|
||||
let app_state = app_state.clone();
|
||||
|
||||
let language_registry = app_state.languages.clone();
|
||||
let user_store = app_state.user_store.clone();
|
||||
|
||||
let component_preview = cx.new(|cx| {
|
||||
ComponentPreview::new(
|
||||
weak_workspace.clone(),
|
||||
language_registry,
|
||||
user_store,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(component_preview),
|
||||
None,
|
||||
@@ -27,43 +60,105 @@ pub fn init(cx: &mut App) {
|
||||
.detach();
|
||||
}
|
||||
|
||||
enum PreviewEntry {
|
||||
AllComponents,
|
||||
Separator,
|
||||
Component(ComponentMetadata),
|
||||
SectionHeader(SharedString),
|
||||
}
|
||||
|
||||
impl From<ComponentMetadata> for PreviewEntry {
|
||||
fn from(component: ComponentMetadata) -> Self {
|
||||
PreviewEntry::Component(component)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SharedString> for PreviewEntry {
|
||||
fn from(section_header: SharedString) -> Self {
|
||||
PreviewEntry::SectionHeader(section_header)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||
enum PreviewPage {
|
||||
#[default]
|
||||
AllComponents,
|
||||
Component(ComponentId),
|
||||
}
|
||||
|
||||
struct ComponentPreview {
|
||||
focus_handle: FocusHandle,
|
||||
_view_scroll_handle: ScrollHandle,
|
||||
nav_scroll_handle: UniformListScrollHandle,
|
||||
component_map: HashMap<ComponentId, ComponentMetadata>,
|
||||
active_page: PreviewPage,
|
||||
components: Vec<ComponentMetadata>,
|
||||
component_list: ListState,
|
||||
selected_index: usize,
|
||||
cursor_index: usize,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
user_store: Entity<UserStore>,
|
||||
}
|
||||
|
||||
impl ComponentPreview {
|
||||
pub fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let components = components().all_sorted();
|
||||
let initial_length = components.len();
|
||||
pub fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
user_store: Entity<UserStore>,
|
||||
selected_index: impl Into<Option<usize>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let sorted_components = components().all_sorted();
|
||||
let selected_index = selected_index.into().unwrap_or(0);
|
||||
|
||||
let component_list = ListState::new(initial_length, gpui::ListAlignment::Top, px(500.0), {
|
||||
let this = cx.entity().downgrade();
|
||||
move |ix, window: &mut Window, cx: &mut App| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.render_preview(ix, window, cx).into_any_element()
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
});
|
||||
let component_list = ListState::new(
|
||||
sorted_components.len(),
|
||||
gpui::ListAlignment::Top,
|
||||
px(1500.0),
|
||||
{
|
||||
let this = cx.entity().downgrade();
|
||||
move |ix, window: &mut Window, cx: &mut App| {
|
||||
this.update(cx, |this, cx| {
|
||||
let component = this.get_component(ix);
|
||||
this.render_preview(&component, window, cx)
|
||||
.into_any_element()
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Self {
|
||||
let mut component_preview = Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
_view_scroll_handle: ScrollHandle::new(),
|
||||
nav_scroll_handle: UniformListScrollHandle::new(),
|
||||
components,
|
||||
language_registry,
|
||||
user_store,
|
||||
workspace,
|
||||
active_page: PreviewPage::AllComponents,
|
||||
component_map: components().0,
|
||||
components: sorted_components,
|
||||
component_list,
|
||||
selected_index: 0,
|
||||
cursor_index: selected_index,
|
||||
};
|
||||
|
||||
if component_preview.cursor_index > 0 {
|
||||
component_preview.scroll_to_preview(component_preview.cursor_index, cx);
|
||||
}
|
||||
|
||||
component_preview.update_component_list(cx);
|
||||
|
||||
component_preview
|
||||
}
|
||||
|
||||
fn scroll_to_preview(&mut self, ix: usize, cx: &mut Context<Self>) {
|
||||
self.component_list.scroll_to_reveal_item(ix);
|
||||
self.selected_index = ix;
|
||||
self.cursor_index = ix;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_active_page(&mut self, page: PreviewPage, cx: &mut Context<Self>) {
|
||||
self.active_page = page;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -71,32 +166,173 @@ impl ComponentPreview {
|
||||
self.components[ix].clone()
|
||||
}
|
||||
|
||||
fn scope_ordered_entries(&self) -> Vec<PreviewEntry> {
|
||||
use std::collections::HashMap;
|
||||
|
||||
let mut scope_groups: HashMap<Option<ComponentScope>, Vec<ComponentMetadata>> =
|
||||
HashMap::default();
|
||||
|
||||
for component in &self.components {
|
||||
scope_groups
|
||||
.entry(component.scope())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(component.clone());
|
||||
}
|
||||
|
||||
for components in scope_groups.values_mut() {
|
||||
components.sort_by_key(|c| c.name().to_lowercase());
|
||||
}
|
||||
|
||||
let mut entries = Vec::new();
|
||||
|
||||
let known_scopes = [
|
||||
ComponentScope::Layout,
|
||||
ComponentScope::Input,
|
||||
ComponentScope::Editor,
|
||||
ComponentScope::Notification,
|
||||
ComponentScope::Collaboration,
|
||||
ComponentScope::VersionControl,
|
||||
];
|
||||
|
||||
// Always show all components first
|
||||
entries.push(PreviewEntry::AllComponents);
|
||||
entries.push(PreviewEntry::Separator);
|
||||
|
||||
for scope in known_scopes.iter() {
|
||||
let scope_key = Some(scope.clone());
|
||||
if let Some(components) = scope_groups.remove(&scope_key) {
|
||||
if !components.is_empty() {
|
||||
entries.push(PreviewEntry::SectionHeader(scope.to_string().into()));
|
||||
|
||||
for component in components {
|
||||
entries.push(PreviewEntry::Component(component));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (scope, components) in &scope_groups {
|
||||
if let Some(ComponentScope::Unknown(_)) = scope {
|
||||
if !components.is_empty() {
|
||||
if let Some(scope_value) = scope {
|
||||
entries.push(PreviewEntry::SectionHeader(scope_value.to_string().into()));
|
||||
}
|
||||
|
||||
for component in components {
|
||||
entries.push(PreviewEntry::Component(component.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(components) = scope_groups.get(&None) {
|
||||
if !components.is_empty() {
|
||||
entries.push(PreviewEntry::Separator);
|
||||
entries.push(PreviewEntry::SectionHeader("Uncategorized".into()));
|
||||
|
||||
for component in components {
|
||||
entries.push(PreviewEntry::Component(component.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entries
|
||||
}
|
||||
|
||||
fn render_sidebar_entry(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
entry: &PreviewEntry,
|
||||
cx: &Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let component = self.get_component(ix);
|
||||
match entry {
|
||||
PreviewEntry::Component(component_metadata) => {
|
||||
let id = component_metadata.id();
|
||||
let selected = self.active_page == PreviewPage::Component(id.clone());
|
||||
ListItem::new(ix)
|
||||
.child(Label::new(component_metadata.name().clone()).color(Color::Default))
|
||||
.selectable(true)
|
||||
.toggle_state(selected)
|
||||
.inset(true)
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
let id = id.clone();
|
||||
this.set_active_page(PreviewPage::Component(id), cx);
|
||||
}))
|
||||
.into_any_element()
|
||||
}
|
||||
PreviewEntry::SectionHeader(shared_string) => ListSubHeader::new(shared_string)
|
||||
.inset(true)
|
||||
.into_any_element(),
|
||||
PreviewEntry::AllComponents => {
|
||||
let selected = self.active_page == PreviewPage::AllComponents;
|
||||
|
||||
ListItem::new(ix)
|
||||
.child(Label::new(component.name().clone()).color(Color::Default))
|
||||
.selectable(true)
|
||||
.toggle_state(selected)
|
||||
.inset(true)
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.scroll_to_preview(ix, cx);
|
||||
}))
|
||||
ListItem::new(ix)
|
||||
.child(Label::new("All Components").color(Color::Default))
|
||||
.selectable(true)
|
||||
.toggle_state(selected)
|
||||
.inset(true)
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.set_active_page(PreviewPage::AllComponents, cx);
|
||||
}))
|
||||
.into_any_element()
|
||||
}
|
||||
PreviewEntry::Separator => ListItem::new(ix)
|
||||
.child(h_flex().pt_3().child(Divider::horizontal_dashed()))
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_component_list(&mut self, cx: &mut Context<Self>) {
|
||||
let new_len = self.scope_ordered_entries().len();
|
||||
let entries = self.scope_ordered_entries();
|
||||
let weak_entity = cx.entity().downgrade();
|
||||
|
||||
let new_list = ListState::new(
|
||||
new_len,
|
||||
gpui::ListAlignment::Top,
|
||||
px(1500.0),
|
||||
move |ix, window, cx| {
|
||||
let entry = &entries[ix];
|
||||
|
||||
weak_entity
|
||||
.update(cx, |this, cx| match entry {
|
||||
PreviewEntry::Component(component) => this
|
||||
.render_preview(component, window, cx)
|
||||
.into_any_element(),
|
||||
PreviewEntry::SectionHeader(shared_string) => this
|
||||
.render_scope_header(ix, shared_string.clone(), window, cx)
|
||||
.into_any_element(),
|
||||
PreviewEntry::AllComponents => div().w_full().h_0().into_any_element(),
|
||||
PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
|
||||
})
|
||||
.unwrap()
|
||||
},
|
||||
);
|
||||
|
||||
self.component_list = new_list;
|
||||
}
|
||||
|
||||
fn render_scope_header(
|
||||
&self,
|
||||
_ix: usize,
|
||||
title: SharedString,
|
||||
_window: &Window,
|
||||
_cx: &App,
|
||||
) -> impl IntoElement {
|
||||
h_flex()
|
||||
.w_full()
|
||||
.h_10()
|
||||
.items_center()
|
||||
.child(Headline::new(title).size(HeadlineSize::XSmall))
|
||||
.child(Divider::horizontal())
|
||||
}
|
||||
|
||||
fn render_preview(
|
||||
&self,
|
||||
ix: usize,
|
||||
component: &ComponentMetadata,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
let component = self.get_component(ix);
|
||||
|
||||
let name = component.name();
|
||||
let scope = component.scope();
|
||||
|
||||
@@ -108,7 +344,7 @@ impl ComponentPreview {
|
||||
v_flex()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.w_full()
|
||||
.gap_4()
|
||||
.py_4()
|
||||
@@ -142,10 +378,71 @@ impl ComponentPreview {
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_all_components(&self) -> impl IntoElement {
|
||||
v_flex()
|
||||
.id("component-list")
|
||||
.px_8()
|
||||
.pt_4()
|
||||
.size_full()
|
||||
.child(
|
||||
list(self.component_list.clone())
|
||||
.flex_grow()
|
||||
.with_sizing_behavior(gpui::ListSizingBehavior::Auto),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_component_page(
|
||||
&mut self,
|
||||
component_id: &ComponentId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let component = self.component_map.get(&component_id);
|
||||
|
||||
if let Some(component) = component {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.flex_initial()
|
||||
.min_h_full()
|
||||
.child(self.render_preview(component, window, cx))
|
||||
.into_any_element()
|
||||
} else {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child("Component not found")
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn test_status_toast(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let status_toast = StatusToast::new(
|
||||
"`zed/new-notification-system` created!",
|
||||
window,
|
||||
cx,
|
||||
|this, _, cx| {
|
||||
this.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
|
||||
.action(
|
||||
"Open Pull Request",
|
||||
cx.listener(|_, _, _, cx| cx.open_url("https://github.com/")),
|
||||
)
|
||||
},
|
||||
);
|
||||
workspace.toggle_status_toast(window, cx, status_toast)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ComponentPreview {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let sidebar_entries = self.scope_ordered_entries();
|
||||
let active_page = self.active_page.clone();
|
||||
|
||||
h_flex()
|
||||
.id("component-preview")
|
||||
.key_context("ComponentPreview")
|
||||
@@ -155,35 +452,47 @@ impl Render for ComponentPreview {
|
||||
.track_focus(&self.focus_handle)
|
||||
.px_2()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
uniform_list(
|
||||
cx.entity().clone(),
|
||||
"component-nav",
|
||||
self.components.len(),
|
||||
move |this, range, _window, cx| {
|
||||
range
|
||||
.map(|ix| this.render_sidebar_entry(ix, ix == this.selected_index, cx))
|
||||
.collect()
|
||||
},
|
||||
)
|
||||
.track_scroll(self.nav_scroll_handle.clone())
|
||||
.pt_4()
|
||||
.w(px(240.))
|
||||
.h_full()
|
||||
.flex_grow(),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.id("component-list")
|
||||
.px_8()
|
||||
.pt_4()
|
||||
.size_full()
|
||||
.h_full()
|
||||
.child(
|
||||
list(self.component_list.clone())
|
||||
.flex_grow()
|
||||
.with_sizing_behavior(gpui::ListSizingBehavior::Auto),
|
||||
uniform_list(
|
||||
cx.entity().clone(),
|
||||
"component-nav",
|
||||
sidebar_entries.len(),
|
||||
move |this, range, _window, cx| {
|
||||
range
|
||||
.map(|ix| {
|
||||
this.render_sidebar_entry(ix, &sidebar_entries[ix], cx)
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
)
|
||||
.track_scroll(self.nav_scroll_handle.clone())
|
||||
.pt_4()
|
||||
.w(px(240.))
|
||||
.h_full()
|
||||
.flex_1(),
|
||||
)
|
||||
.child(
|
||||
div().w_full().pb_4().child(
|
||||
Button::new("toast-test", "Launch Toast")
|
||||
.on_click(cx.listener({
|
||||
move |this, _, window, cx| {
|
||||
this.test_status_toast(window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
.full_width(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(match active_page {
|
||||
PreviewPage::AllComponents => self.render_all_components().into_any_element(),
|
||||
PreviewPage::Component(id) => self
|
||||
.render_component_page(&id, window, cx)
|
||||
.into_any_element(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,13 +522,26 @@ impl Item for ComponentPreview {
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: Option<WorkspaceId>,
|
||||
window: &mut Window,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<gpui::Entity<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Some(cx.new(|cx| Self::new(window, cx)))
|
||||
let language_registry = self.language_registry.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let weak_workspace = self.workspace.clone();
|
||||
let selected_index = self.cursor_index;
|
||||
|
||||
Some(cx.new(|cx| {
|
||||
Self::new(
|
||||
weak_workspace,
|
||||
language_registry,
|
||||
user_store,
|
||||
selected_index,
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
|
||||
@@ -227,6 +549,59 @@ impl Item for ComponentPreview {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: impl serializable item for component preview so it will restore with the workspace
|
||||
// ref: https://github.com/zed-industries/zed/blob/32201ac70a501e63dfa2ade9c00f85aea2d4dd94/crates/image_viewer/src/image_viewer.rs#L199
|
||||
// Use `ImageViewer` as a model for how to do it, except it'll be even simpler
|
||||
impl SerializableItem for ComponentPreview {
|
||||
fn serialized_item_kind() -> &'static str {
|
||||
"ComponentPreview"
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_workspace_id: WorkspaceId,
|
||||
_item_id: ItemId,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<gpui::Result<Entity<Self>>> {
|
||||
let user_store = project.read(cx).user_store().clone();
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
|
||||
window.spawn(cx, |mut cx| async move {
|
||||
let user_store = user_store.clone();
|
||||
let language_registry = language_registry.clone();
|
||||
let weak_workspace = workspace.clone();
|
||||
cx.update(|_, cx| {
|
||||
Ok(cx.new(|cx| {
|
||||
ComponentPreview::new(weak_workspace, language_registry, user_store, None, cx)
|
||||
}))
|
||||
})?
|
||||
})
|
||||
}
|
||||
|
||||
fn cleanup(
|
||||
_workspace_id: WorkspaceId,
|
||||
_alive_items: Vec<ItemId>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Task<gpui::Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
// window.spawn(cx, |_| {
|
||||
// ...
|
||||
// })
|
||||
}
|
||||
|
||||
fn serialize(
|
||||
&mut self,
|
||||
_workspace: &mut Workspace,
|
||||
_item_id: ItemId,
|
||||
_closing: bool,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<Task<gpui::Result<()>>> {
|
||||
// TODO: Serialize the active index so we can re-open to the same place
|
||||
None
|
||||
}
|
||||
|
||||
fn should_serialize(&self, _event: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,4 +31,3 @@ settings.workspace = true
|
||||
smol.workspace = true
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail};
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{App, Entity, Task, Window};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use assistant_tool::{Tool, ToolSource};
|
||||
use gpui::{App, Entity, Task};
|
||||
use project::Project;
|
||||
|
||||
use crate::manager::ContextServerManager;
|
||||
use crate::types;
|
||||
@@ -36,6 +37,12 @@ impl Tool for ContextServerTool {
|
||||
self.tool.description.clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn source(&self) -> ToolSource {
|
||||
ToolSource::ContextServer {
|
||||
id: self.server_id.clone().into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
match &self.tool.input_schema {
|
||||
serde_json::Value::Null => {
|
||||
@@ -49,13 +56,11 @@ impl Tool for ContextServerTool {
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: std::sync::Arc<Self>,
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_thread_id: Arc<str>,
|
||||
_workspace: gpui::WeakEntity<workspace::Workspace>,
|
||||
_: &mut Window,
|
||||
_project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> gpui::Task<gpui::Result<String>> {
|
||||
) -> Task<Result<String>> {
|
||||
if let Some(server) = self.server_manager.read(cx).get_server(&self.server_id) {
|
||||
cx.foreground_executor().spawn({
|
||||
let tool_name = self.tool.name.clone();
|
||||
|
||||
@@ -623,16 +623,21 @@ impl Copilot {
|
||||
|
||||
pub fn sign_out(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx);
|
||||
if let CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) = &self.server {
|
||||
let server = server.clone();
|
||||
cx.background_spawn(async move {
|
||||
server
|
||||
.request::<request::SignOut>(request::SignOutParams {})
|
||||
.await?;
|
||||
match &self.server {
|
||||
CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) => {
|
||||
let server = server.clone();
|
||||
cx.background_spawn(async move {
|
||||
server
|
||||
.request::<request::SignOut>(request::SignOutParams {})
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
CopilotServer::Disabled => cx.background_spawn(async move {
|
||||
clear_copilot_config_dir().await;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("copilot hasn't started yet")))
|
||||
}),
|
||||
_ => Task::ready(Err(anyhow!("copilot hasn't started yet"))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1016,6 +1021,10 @@ async fn clear_copilot_dir() {
|
||||
remove_matching(paths::copilot_dir(), |_| true).await
|
||||
}
|
||||
|
||||
async fn clear_copilot_config_dir() {
|
||||
remove_matching(copilot_chat::copilot_chat_config_dir(), |_| true).await
|
||||
}
|
||||
|
||||
async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||
const SERVER_PATH: &str = "dist/language-server.js";
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@ use std::sync::OnceLock;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::DateTime;
|
||||
use collections::HashSet;
|
||||
use fs::Fs;
|
||||
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
|
||||
use gpui::{prelude::*, App, AsyncApp, Global};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use paths::home_dir;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::watch_config_file;
|
||||
use settings::watch_config_dir;
|
||||
use strum::EnumIter;
|
||||
|
||||
pub const COPILOT_CHAT_COMPLETION_URL: &str = "https://api.githubcopilot.com/chat/completions";
|
||||
@@ -212,7 +213,7 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<dyn HttpClient>, cx: &mut App) {
|
||||
cx.set_global(GlobalCopilotChat(copilot_chat));
|
||||
}
|
||||
|
||||
fn copilot_chat_config_dir() -> &'static PathBuf {
|
||||
pub fn copilot_chat_config_dir() -> &'static PathBuf {
|
||||
static COPILOT_CHAT_CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||
|
||||
COPILOT_CHAT_CONFIG_DIR.get_or_init(|| {
|
||||
@@ -237,27 +238,18 @@ impl CopilotChat {
|
||||
}
|
||||
|
||||
pub fn new(fs: Arc<dyn Fs>, client: Arc<dyn HttpClient>, cx: &App) -> Self {
|
||||
let config_paths = copilot_chat_config_paths();
|
||||
|
||||
let resolve_config_path = {
|
||||
let fs = fs.clone();
|
||||
async move {
|
||||
for config_path in config_paths.iter() {
|
||||
if fs.metadata(config_path).await.is_ok_and(|v| v.is_some()) {
|
||||
return config_path.clone();
|
||||
}
|
||||
}
|
||||
config_paths[0].clone()
|
||||
}
|
||||
};
|
||||
let config_paths: HashSet<PathBuf> = copilot_chat_config_paths().into_iter().collect();
|
||||
let dir_path = copilot_chat_config_dir();
|
||||
|
||||
cx.spawn(|cx| async move {
|
||||
let config_file = resolve_config_path.await;
|
||||
let mut config_file_rx = watch_config_file(cx.background_executor(), fs, config_file);
|
||||
|
||||
while let Some(contents) = config_file_rx.next().await {
|
||||
let mut parent_watch_rx = watch_config_dir(
|
||||
cx.background_executor(),
|
||||
fs.clone(),
|
||||
dir_path.clone(),
|
||||
config_paths,
|
||||
);
|
||||
while let Some(contents) = parent_watch_rx.next().await {
|
||||
let oauth_token = extract_oauth_token(contents);
|
||||
|
||||
cx.update(|cx| {
|
||||
if let Some(this) = Self::global(cx).as_ref() {
|
||||
this.update(cx, |this, cx| {
|
||||
|
||||
@@ -122,7 +122,7 @@ impl CopilotCodeVerification {
|
||||
.p_1()
|
||||
.border_1()
|
||||
.border_muted(cx)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.cursor_pointer()
|
||||
.justify_between()
|
||||
.on_mouse_down(gpui::MouseButton::Left, {
|
||||
|
||||
@@ -311,7 +311,10 @@ impl ProjectDiagnosticsEditor {
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
|
||||
workspace.activate_item(&existing, true, true, window, cx);
|
||||
let is_active = workspace
|
||||
.active_item(cx)
|
||||
.is_some_and(|item| item.item_id() == existing.item_id());
|
||||
workspace.activate_item(&existing, true, !is_active, window, cx);
|
||||
} else {
|
||||
let workspace_handle = cx.entity().downgrade();
|
||||
|
||||
@@ -973,7 +976,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.px_1()
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.bg(color.surface_background.opacity(0.5))
|
||||
.map(|stack| {
|
||||
stack.child(
|
||||
|
||||
@@ -94,6 +94,7 @@ ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
languages = {workspace = true, features = ["test-support"] }
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
multi_buffer = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -340,7 +340,9 @@ gpui::actions!(
|
||||
MoveToPreviousWordStart,
|
||||
MoveToStartOfParagraph,
|
||||
MoveToStartOfExcerpt,
|
||||
MoveToStartOfNextExcerpt,
|
||||
MoveToEndOfExcerpt,
|
||||
MoveToEndOfPreviousExcerpt,
|
||||
MoveUp,
|
||||
Newline,
|
||||
NewlineAbove,
|
||||
@@ -378,7 +380,9 @@ gpui::actions!(
|
||||
SelectAll,
|
||||
SelectAllMatches,
|
||||
SelectToStartOfExcerpt,
|
||||
SelectToStartOfNextExcerpt,
|
||||
SelectToEndOfExcerpt,
|
||||
SelectToEndOfPreviousExcerpt,
|
||||
SelectDown,
|
||||
SelectEnclosingSymbol,
|
||||
SelectLargerSyntaxNode,
|
||||
|
||||
@@ -7,7 +7,6 @@ use std::time::Duration;
|
||||
|
||||
pub struct BlinkManager {
|
||||
blink_interval: Duration,
|
||||
|
||||
blink_epoch: usize,
|
||||
blinking_paused: bool,
|
||||
visible: bool,
|
||||
@@ -24,7 +23,6 @@ impl BlinkManager {
|
||||
|
||||
Self {
|
||||
blink_interval,
|
||||
|
||||
blink_epoch: 0,
|
||||
blinking_paused: false,
|
||||
visible: true,
|
||||
|
||||
@@ -2,12 +2,13 @@ use anyhow::Context as _;
|
||||
use gpui::{App, Context, Entity, Window};
|
||||
use language::Language;
|
||||
use url::Url;
|
||||
use workspace::{OpenOptions, OpenVisible};
|
||||
|
||||
use crate::lsp_ext::find_specific_language_server_in_selection;
|
||||
|
||||
use crate::{element::register_action, Editor, SwitchSourceHeader};
|
||||
|
||||
const CLANGD_SERVER_NAME: &str = "clangd";
|
||||
use project::lsp_store::clangd_ext::CLANGD_SERVER_NAME;
|
||||
|
||||
fn is_c_language(language: &Language) -> bool {
|
||||
return language.name() == "C++".into() || language.name() == "C".into();
|
||||
@@ -46,7 +47,7 @@ pub fn switch_source_header(
|
||||
project.request_lsp(
|
||||
buffer,
|
||||
project::LanguageServerToQuery::Other(server_to_query),
|
||||
project::lsp_ext_command::SwitchSourceHeader,
|
||||
project::lsp_store::lsp_ext_command::SwitchSourceHeader,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -72,7 +73,7 @@ pub fn switch_source_header(
|
||||
|
||||
workspace
|
||||
.update_in(&mut cx, |workspace, window, cx| {
|
||||
workspace.open_abs_path(path, false, window, cx)
|
||||
workspace.open_abs_path(path, OpenOptions { visible: Some(OpenVisible::None), ..Default::default() }, window, cx)
|
||||
})
|
||||
.with_context(|| {
|
||||
format!(
|
||||
|
||||
@@ -6,11 +6,11 @@ use gpui::{
|
||||
};
|
||||
use language::Buffer;
|
||||
use language::CodeLabel;
|
||||
use lsp::LanguageServerId;
|
||||
use markdown::Markdown;
|
||||
use multi_buffer::{Anchor, ExcerptId};
|
||||
use ordered_float::OrderedFloat;
|
||||
use project::lsp_store::CompletionDocumentation;
|
||||
use project::CompletionSource;
|
||||
use project::{CodeAction, Completion, TaskSourceKind};
|
||||
|
||||
use std::{
|
||||
@@ -233,11 +233,9 @@ impl CompletionsMenu {
|
||||
runs: Default::default(),
|
||||
filter_range: Default::default(),
|
||||
},
|
||||
server_id: LanguageServerId(usize::MAX),
|
||||
documentation: None,
|
||||
lsp_completion: Default::default(),
|
||||
confirm: None,
|
||||
resolved: true,
|
||||
source: CompletionSource::Custom,
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -500,7 +498,12 @@ impl CompletionsMenu {
|
||||
// Ignore font weight for syntax highlighting, as we'll use it
|
||||
// for fuzzy matches.
|
||||
highlight.font_weight = None;
|
||||
if completion.lsp_completion.deprecated.unwrap_or(false) {
|
||||
if completion
|
||||
.source
|
||||
.lsp_completion(false)
|
||||
.and_then(|lsp_completion| lsp_completion.deprecated)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
highlight.strikethrough = Some(StrikethroughStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
@@ -534,7 +537,7 @@ impl CompletionsMenu {
|
||||
};
|
||||
let color_swatch = completion
|
||||
.color()
|
||||
.map(|color| div().size_4().bg(color).rounded_sm());
|
||||
.map(|color| div().size_4().bg(color).rounded_xs());
|
||||
|
||||
div().min_w(px(280.)).max_w(px(540.)).child(
|
||||
ListItem::new(mat.candidate_id)
|
||||
@@ -708,7 +711,12 @@ impl CompletionsMenu {
|
||||
|
||||
let completion = &completions[mat.candidate_id];
|
||||
let sort_key = completion.sort_key();
|
||||
let sort_text = completion.lsp_completion.sort_text.as_deref();
|
||||
let sort_text =
|
||||
if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source {
|
||||
lsp_completion.sort_text.as_deref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let score = Reverse(OrderedFloat(mat.score));
|
||||
|
||||
if mat.score >= 0.2 {
|
||||
@@ -851,7 +859,7 @@ impl CodeActionsItem {
|
||||
|
||||
pub fn label(&self) -> String {
|
||||
match self {
|
||||
Self::CodeAction { action, .. } => action.lsp_action.title.clone(),
|
||||
Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
|
||||
Self::Task(_, task) => task.resolved_label.clone(),
|
||||
}
|
||||
}
|
||||
@@ -984,7 +992,7 @@ impl CodeActionsMenu {
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
|
||||
action.lsp_action.title.replace("\n", ""),
|
||||
action.lsp_action.title().replace("\n", ""),
|
||||
)
|
||||
.when(selected, |this| {
|
||||
this.text_color(colors.text_accent)
|
||||
@@ -1029,7 +1037,7 @@ impl CodeActionsMenu {
|
||||
.max_by_key(|(_, action)| match action {
|
||||
CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
|
||||
CodeActionsItem::CodeAction { action, .. } => {
|
||||
action.lsp_action.title.chars().count()
|
||||
action.lsp_action.title().chars().count()
|
||||
}
|
||||
})
|
||||
.map(|(ix, _)| ix),
|
||||
|
||||
@@ -113,7 +113,6 @@ pub struct DisplayMap {
|
||||
}
|
||||
|
||||
impl DisplayMap {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
buffer: Entity<MultiBuffer>,
|
||||
font: Font,
|
||||
|
||||
@@ -726,7 +726,6 @@ impl BlockMap {
|
||||
self.show_excerpt_controls
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn header_and_footer_blocks<'a, R, T>(
|
||||
show_excerpt_controls: bool,
|
||||
excerpt_footer_height: u32,
|
||||
|
||||
@@ -28,6 +28,7 @@ mod hover_popover;
|
||||
mod indent_guides;
|
||||
mod inlay_hint_cache;
|
||||
pub mod items;
|
||||
mod jsx_tag_auto_close;
|
||||
mod linked_editing_ranges;
|
||||
mod lsp_ext;
|
||||
mod mouse_context_menu;
|
||||
@@ -137,8 +138,9 @@ use multi_buffer::{
|
||||
use project::{
|
||||
lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
|
||||
project_settings::{GitGutterSetting, ProjectSettings},
|
||||
CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink,
|
||||
PrepareRenameResponse, Project, ProjectItem, ProjectTransaction, TaskSourceKind,
|
||||
CodeAction, Completion, CompletionIntent, CompletionSource, DocumentHighlight, InlayHint,
|
||||
Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, ProjectTransaction,
|
||||
TaskSourceKind,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use rpc::{proto::*, ErrorExt};
|
||||
@@ -260,6 +262,7 @@ enum DisplayDiffHunk {
|
||||
display_row: DisplayRow,
|
||||
},
|
||||
Unfolded {
|
||||
is_created_file: bool,
|
||||
diff_base_byte_range: Range<usize>,
|
||||
display_row_range: Range<DisplayRow>,
|
||||
multi_buffer_range: Range<Anchor>,
|
||||
@@ -724,6 +727,7 @@ pub struct Editor {
|
||||
use_autoclose: bool,
|
||||
use_auto_surround: bool,
|
||||
auto_replace_emoji_shortcode: bool,
|
||||
jsx_tag_auto_close_enabled_in_any_buffer: bool,
|
||||
show_git_blame_gutter: bool,
|
||||
show_git_blame_inline: bool,
|
||||
show_git_blame_inline_delay_task: Option<Task<()>>,
|
||||
@@ -1010,8 +1014,8 @@ pub struct ClipboardSelection {
|
||||
pub len: usize,
|
||||
/// Whether this was a full-line selection.
|
||||
pub is_entire_line: bool,
|
||||
/// The column where this selection originally started.
|
||||
pub start_column: u32,
|
||||
/// The indentation of the first line when this content was originally copied.
|
||||
pub first_line_indent: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -1199,7 +1203,7 @@ impl Editor {
|
||||
.bg(cx.theme().colors().ghost_element_background)
|
||||
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||
.rounded_sm()
|
||||
.rounded_xs()
|
||||
.size_full()
|
||||
.cursor_pointer()
|
||||
.child("⋯")
|
||||
@@ -1247,11 +1251,6 @@ impl Editor {
|
||||
let mut project_subscriptions = Vec::new();
|
||||
if mode == EditorMode::Full {
|
||||
if let Some(project) = project.as_ref() {
|
||||
if buffer.read(cx).is_singleton() {
|
||||
project_subscriptions.push(cx.observe_in(project, window, |_, _, _, cx| {
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
}));
|
||||
}
|
||||
project_subscriptions.push(cx.subscribe_in(
|
||||
project,
|
||||
window,
|
||||
@@ -1410,6 +1409,7 @@ impl Editor {
|
||||
use_autoclose: true,
|
||||
use_auto_surround: true,
|
||||
auto_replace_emoji_shortcode: false,
|
||||
jsx_tag_auto_close_enabled_in_any_buffer: false,
|
||||
leader_peer_id: None,
|
||||
remote_id: None,
|
||||
hover_state: Default::default(),
|
||||
@@ -1493,6 +1493,7 @@ impl Editor {
|
||||
|
||||
this.end_selection(window, cx);
|
||||
this.scroll_manager.show_scrollbar(window, cx);
|
||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut this, &buffer, cx);
|
||||
|
||||
if mode == EditorMode::Full {
|
||||
let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars();
|
||||
@@ -1572,13 +1573,16 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(extension) = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|buffer| buffer.read(cx).file()?.path().extension()?.to_str())
|
||||
{
|
||||
key_context.set("extension", extension.to_string());
|
||||
if let Some(singleton_buffer) = self.buffer.read(cx).as_singleton() {
|
||||
if let Some(extension) = singleton_buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.and_then(|file| file.path().extension()?.to_str())
|
||||
{
|
||||
key_context.set("extension", extension.to_string());
|
||||
}
|
||||
} else {
|
||||
key_context.add("multibuffer");
|
||||
}
|
||||
|
||||
if has_active_edit_prediction {
|
||||
@@ -2247,6 +2251,9 @@ impl Editor {
|
||||
cx.subscribe(&other, |this, other, other_evt, cx| match other_evt {
|
||||
EditorEvent::SelectionsChanged { local: true } => {
|
||||
let other_selections = other.read(cx).selections.disjoint.to_vec();
|
||||
if other_selections.is_empty() {
|
||||
return;
|
||||
}
|
||||
this.selections.change_with(cx, |selections| {
|
||||
selections.select_anchors(other_selections);
|
||||
});
|
||||
@@ -2258,6 +2265,9 @@ impl Editor {
|
||||
cx.subscribe_self::<EditorEvent>(move |this, this_evt, cx| match this_evt {
|
||||
EditorEvent::SelectionsChanged { local: true } => {
|
||||
let these_selections = this.selections.disjoint.to_vec();
|
||||
if these_selections.is_empty() {
|
||||
return;
|
||||
}
|
||||
other.update(cx, |other_editor, cx| {
|
||||
other_editor.selections.change_with(cx, |selections| {
|
||||
selections.select_anchors(these_selections);
|
||||
@@ -2344,7 +2354,7 @@ impl Editor {
|
||||
pub fn edit_with_block_indent<I, S, T>(
|
||||
&mut self,
|
||||
edits: I,
|
||||
original_start_columns: Vec<u32>,
|
||||
original_indent_columns: Vec<Option<u32>>,
|
||||
cx: &mut Context<Self>,
|
||||
) where
|
||||
I: IntoIterator<Item = (Range<S>, T)>,
|
||||
@@ -2359,7 +2369,7 @@ impl Editor {
|
||||
buffer.edit(
|
||||
edits,
|
||||
Some(AutoindentMode::Block {
|
||||
original_start_columns,
|
||||
original_indent_columns,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
@@ -3095,6 +3105,9 @@ impl Editor {
|
||||
drop(snapshot);
|
||||
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
let initial_buffer_versions =
|
||||
jsx_tag_auto_close::construct_initial_buffer_versions_map(this, &edits, cx);
|
||||
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(edits, this.autoindent_mode.clone(), cx);
|
||||
});
|
||||
@@ -3182,6 +3195,7 @@ impl Editor {
|
||||
this.trigger_completion_on_input(&text, trigger_in_words, window, cx);
|
||||
linked_editing_ranges::refresh_linked_ranges(this, window, cx);
|
||||
this.refresh_inline_completion(true, false, window, cx);
|
||||
jsx_tag_auto_close::handle_from(this, initial_buffer_versions, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3466,7 +3480,7 @@ impl Editor {
|
||||
|
||||
pub fn insert(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let autoindent = text.is_empty().not().then(|| AutoindentMode::Block {
|
||||
original_start_columns: Vec::new(),
|
||||
original_indent_columns: Vec::new(),
|
||||
});
|
||||
self.insert_with_autoindent_mode(text, autoindent, window, cx);
|
||||
}
|
||||
@@ -5917,7 +5931,6 @@ impl Editor {
|
||||
const EDIT_PREDICTION_POPOVER_PADDING_X: Pixels = Pixels(24.);
|
||||
const EDIT_PREDICTION_POPOVER_PADDING_Y: Pixels = Pixels(2.);
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_edit_prediction_popover(
|
||||
&mut self,
|
||||
text_bounds: &Bounds<Pixels>,
|
||||
@@ -6029,7 +6042,6 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_edit_prediction_modifier_jump_popover(
|
||||
&mut self,
|
||||
text_bounds: &Bounds<Pixels>,
|
||||
@@ -6125,7 +6137,6 @@ impl Editor {
|
||||
Some((element, origin))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_edit_prediction_scroll_popover(
|
||||
&mut self,
|
||||
to_y: impl Fn(Size<Pixels>) -> Pixels,
|
||||
@@ -6156,7 +6167,6 @@ impl Editor {
|
||||
Some((element, origin))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_edit_prediction_eager_jump_popover(
|
||||
&mut self,
|
||||
text_bounds: &Bounds<Pixels>,
|
||||
@@ -6226,7 +6236,6 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_edit_prediction_end_of_line_popover(
|
||||
self: &mut Editor,
|
||||
label: &'static str,
|
||||
@@ -6285,7 +6294,6 @@ impl Editor {
|
||||
Some((element, origin))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_edit_prediction_diff_popover(
|
||||
self: &Editor,
|
||||
text_bounds: &Bounds<Pixels>,
|
||||
@@ -6536,7 +6544,7 @@ impl Editor {
|
||||
.pl_1()
|
||||
.pr(padding_right)
|
||||
.gap_1()
|
||||
.rounded(px(6.))
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.bg(Self::edit_prediction_line_popover_bg_color(cx))
|
||||
.border_color(Self::edit_prediction_callout_popover_border_color(cx))
|
||||
@@ -6593,7 +6601,6 @@ impl Editor {
|
||||
editor_bg_color.blend(accent_color.opacity(0.6))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_edit_prediction_cursor_popover(
|
||||
&self,
|
||||
min_width: Pixels,
|
||||
@@ -7843,7 +7850,7 @@ impl Editor {
|
||||
for hunk in &hunks {
|
||||
self.prepare_restore_change(&mut revert_changes, hunk, cx);
|
||||
}
|
||||
self.do_stage_or_unstage(false, buffer_id, hunks.into_iter(), window, cx);
|
||||
self.do_stage_or_unstage(false, buffer_id, hunks.into_iter(), cx);
|
||||
}
|
||||
drop(chunk_by);
|
||||
if !revert_changes.is_empty() {
|
||||
@@ -7881,6 +7888,9 @@ impl Editor {
|
||||
hunk: &MultiBufferDiffHunk,
|
||||
cx: &mut App,
|
||||
) -> Option<()> {
|
||||
if hunk.is_created_file() {
|
||||
return None;
|
||||
}
|
||||
let buffer = self.buffer.read(cx);
|
||||
let diff = buffer.diff_for(hunk.buffer_id)?;
|
||||
let buffer = buffer.buffer(hunk.buffer_id)?;
|
||||
@@ -8690,7 +8700,9 @@ impl Editor {
|
||||
clipboard_selections.push(ClipboardSelection {
|
||||
len,
|
||||
is_entire_line,
|
||||
start_column: selection.start.column,
|
||||
first_line_indent: buffer
|
||||
.indent_size_for_line(MultiBufferRow(selection.start.row))
|
||||
.len,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -8769,7 +8781,7 @@ impl Editor {
|
||||
clipboard_selections.push(ClipboardSelection {
|
||||
len,
|
||||
is_entire_line,
|
||||
start_column: start.column,
|
||||
first_line_indent: buffer.indent_size_for_line(MultiBufferRow(start.row)).len,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -8799,8 +8811,8 @@ impl Editor {
|
||||
let old_selections = this.selections.all::<usize>(cx);
|
||||
let all_selections_were_entire_line =
|
||||
clipboard_selections.iter().all(|s| s.is_entire_line);
|
||||
let first_selection_start_column =
|
||||
clipboard_selections.first().map(|s| s.start_column);
|
||||
let first_selection_indent_column =
|
||||
clipboard_selections.first().map(|s| s.first_line_indent);
|
||||
if clipboard_selections.len() != old_selections.len() {
|
||||
clipboard_selections.drain(..);
|
||||
}
|
||||
@@ -8815,21 +8827,21 @@ impl Editor {
|
||||
|
||||
let mut start_offset = 0;
|
||||
let mut edits = Vec::new();
|
||||
let mut original_start_columns = Vec::new();
|
||||
let mut original_indent_columns = Vec::new();
|
||||
for (ix, selection) in old_selections.iter().enumerate() {
|
||||
let to_insert;
|
||||
let entire_line;
|
||||
let original_start_column;
|
||||
let original_indent_column;
|
||||
if let Some(clipboard_selection) = clipboard_selections.get(ix) {
|
||||
let end_offset = start_offset + clipboard_selection.len;
|
||||
to_insert = &clipboard_text[start_offset..end_offset];
|
||||
entire_line = clipboard_selection.is_entire_line;
|
||||
start_offset = end_offset + 1;
|
||||
original_start_column = Some(clipboard_selection.start_column);
|
||||
original_indent_column = Some(clipboard_selection.first_line_indent);
|
||||
} else {
|
||||
to_insert = clipboard_text.as_str();
|
||||
entire_line = all_selections_were_entire_line;
|
||||
original_start_column = first_selection_start_column
|
||||
original_indent_column = first_selection_indent_column
|
||||
}
|
||||
|
||||
// If the corresponding selection was empty when this slice of the
|
||||
@@ -8845,7 +8857,7 @@ impl Editor {
|
||||
};
|
||||
|
||||
edits.push((range, to_insert));
|
||||
original_start_columns.extend(original_start_column);
|
||||
original_indent_columns.push(original_indent_column);
|
||||
}
|
||||
drop(snapshot);
|
||||
|
||||
@@ -8853,7 +8865,7 @@ impl Editor {
|
||||
edits,
|
||||
if auto_indent_on_paste {
|
||||
Some(AutoindentMode::Block {
|
||||
original_start_columns,
|
||||
original_indent_columns,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -9829,6 +9841,31 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_to_start_of_next_excerpt(
|
||||
&mut self,
|
||||
_: &MoveToStartOfNextExcerpt,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
selection.collapse_to(
|
||||
movement::start_of_excerpt(
|
||||
map,
|
||||
selection.head(),
|
||||
workspace::searchable::Direction::Next,
|
||||
),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_to_end_of_excerpt(
|
||||
&mut self,
|
||||
_: &MoveToEndOfExcerpt,
|
||||
@@ -9854,6 +9891,31 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_to_end_of_previous_excerpt(
|
||||
&mut self,
|
||||
_: &MoveToEndOfPreviousExcerpt,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
selection.collapse_to(
|
||||
movement::end_of_excerpt(
|
||||
map,
|
||||
selection.head(),
|
||||
workspace::searchable::Direction::Prev,
|
||||
),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_to_start_of_excerpt(
|
||||
&mut self,
|
||||
_: &SelectToStartOfExcerpt,
|
||||
@@ -9875,6 +9937,27 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_to_start_of_next_excerpt(
|
||||
&mut self,
|
||||
_: &SelectToStartOfNextExcerpt,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(
|
||||
movement::start_of_excerpt(map, head, workspace::searchable::Direction::Next),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_to_end_of_excerpt(
|
||||
&mut self,
|
||||
_: &SelectToEndOfExcerpt,
|
||||
@@ -9896,6 +9979,27 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_to_end_of_previous_excerpt(
|
||||
&mut self,
|
||||
_: &SelectToEndOfPreviousExcerpt,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(
|
||||
movement::end_of_excerpt(map, head, workspace::searchable::Direction::Prev),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_to_beginning(
|
||||
&mut self,
|
||||
_: &MoveToBeginning,
|
||||
@@ -11528,7 +11632,7 @@ impl Editor {
|
||||
fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let selection = self.selections.newest::<Point>(cx);
|
||||
self.go_to_hunk_after_or_before_position(
|
||||
self.go_to_hunk_before_or_after_position(
|
||||
&snapshot,
|
||||
selection.head(),
|
||||
Direction::Next,
|
||||
@@ -11537,7 +11641,7 @@ impl Editor {
|
||||
);
|
||||
}
|
||||
|
||||
fn go_to_hunk_after_or_before_position(
|
||||
fn go_to_hunk_before_or_after_position(
|
||||
&mut self,
|
||||
snapshot: &EditorSnapshot,
|
||||
position: Point,
|
||||
@@ -11588,7 +11692,7 @@ impl Editor {
|
||||
) {
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let selection = self.selections.newest::<Point>(cx);
|
||||
self.go_to_hunk_after_or_before_position(
|
||||
self.go_to_hunk_before_or_after_position(
|
||||
&snapshot,
|
||||
selection.head(),
|
||||
Direction::Prev,
|
||||
@@ -13657,13 +13761,13 @@ impl Editor {
|
||||
pub fn toggle_staged_selected_diff_hunks(
|
||||
&mut self,
|
||||
_: &::git::ToggleStaged,
|
||||
window: &mut Window,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect();
|
||||
let stage = self.has_stageable_diff_hunks_in_ranges(&ranges, &snapshot);
|
||||
self.stage_or_unstage_diff_hunks(stage, &ranges, window, cx);
|
||||
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
|
||||
}
|
||||
|
||||
pub fn stage_and_next(
|
||||
@@ -13687,16 +13791,53 @@ impl Editor {
|
||||
pub fn stage_or_unstage_diff_hunks(
|
||||
&mut self,
|
||||
stage: bool,
|
||||
ranges: &[Range<Anchor>],
|
||||
window: &mut Window,
|
||||
ranges: Vec<Range<Anchor>>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let chunk_by = self
|
||||
.diff_hunks_in_ranges(&ranges, &snapshot)
|
||||
.chunk_by(|hunk| hunk.buffer_id);
|
||||
for (buffer_id, hunks) in &chunk_by {
|
||||
self.do_stage_or_unstage(stage, buffer_id, hunks, window, cx);
|
||||
let task = self.save_buffers_for_ranges_if_needed(&ranges, cx);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
task.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let snapshot = this.buffer.read(cx).snapshot(cx);
|
||||
let chunk_by = this
|
||||
.diff_hunks_in_ranges(&ranges, &snapshot)
|
||||
.chunk_by(|hunk| hunk.buffer_id);
|
||||
for (buffer_id, hunks) in &chunk_by {
|
||||
this.do_stage_or_unstage(stage, buffer_id, hunks, cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn save_buffers_for_ranges_if_needed(
|
||||
&mut self,
|
||||
ranges: &[Range<Anchor>],
|
||||
cx: &mut Context<'_, Editor>,
|
||||
) -> Task<Result<()>> {
|
||||
let multibuffer = self.buffer.read(cx);
|
||||
let snapshot = multibuffer.read(cx);
|
||||
let buffer_ids: HashSet<_> = ranges
|
||||
.iter()
|
||||
.flat_map(|range| snapshot.buffer_ids_for_range(range.clone()))
|
||||
.collect();
|
||||
drop(snapshot);
|
||||
|
||||
let mut buffers = HashSet::default();
|
||||
for buffer_id in buffer_ids {
|
||||
if let Some(buffer_entity) = multibuffer.buffer(buffer_id) {
|
||||
let buffer = buffer_entity.read(cx);
|
||||
if buffer.file().is_some_and(|file| file.disk_state().exists()) && buffer.is_dirty()
|
||||
{
|
||||
buffers.insert(buffer_entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(project) = &self.project {
|
||||
project.update(cx, |project, cx| project.save_buffers(buffers, cx))
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13709,26 +13850,11 @@ impl Editor {
|
||||
let ranges = self.selections.disjoint_anchor_ranges().collect::<Vec<_>>();
|
||||
|
||||
if ranges.iter().any(|range| range.start != range.end) {
|
||||
self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx);
|
||||
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let newest_range = self.selections.newest::<Point>(cx).range();
|
||||
|
||||
let run_twice = snapshot
|
||||
.hunks_for_ranges([newest_range])
|
||||
.first()
|
||||
.is_some_and(|hunk| {
|
||||
let next_line = Point::new(hunk.row_range.end.0 + 1, 0);
|
||||
self.hunk_after_position(&snapshot, next_line)
|
||||
.is_some_and(|other| other.row_range == hunk.row_range)
|
||||
});
|
||||
|
||||
if run_twice {
|
||||
self.go_to_next_hunk(&GoToHunk, window, cx);
|
||||
}
|
||||
self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx);
|
||||
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
|
||||
self.go_to_next_hunk(&GoToHunk, window, cx);
|
||||
}
|
||||
|
||||
@@ -13737,31 +13863,16 @@ impl Editor {
|
||||
stage: bool,
|
||||
buffer_id: BufferId,
|
||||
hunks: impl Iterator<Item = MultiBufferDiffHunk>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let Some(project) = self.project.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else {
|
||||
return;
|
||||
};
|
||||
let Some(diff) = self.buffer.read(cx).diff_for(buffer_id) else {
|
||||
return;
|
||||
};
|
||||
) -> Option<()> {
|
||||
let project = self.project.as_ref()?;
|
||||
let buffer = project.read(cx).buffer_for_id(buffer_id, cx)?;
|
||||
let diff = self.buffer.read(cx).diff_for(buffer_id)?;
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let file_exists = buffer_snapshot
|
||||
.file()
|
||||
.is_some_and(|file| file.disk_state().exists());
|
||||
let Some((repo, path)) = project
|
||||
.read(cx)
|
||||
.repository_and_path_for_buffer_id(buffer_id, cx)
|
||||
else {
|
||||
log::debug!("no git repo for buffer id");
|
||||
return;
|
||||
};
|
||||
|
||||
let new_index_text = diff.update(cx, |diff, cx| {
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.stage_or_unstage_hunks(
|
||||
stage,
|
||||
&hunks
|
||||
@@ -13777,20 +13888,7 @@ impl Editor {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
if file_exists {
|
||||
let buffer_store = project.read(cx).buffer_store().clone();
|
||||
buffer_store
|
||||
.update(cx, |buffer_store, cx| buffer_store.save_buffer(buffer, cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
let recv = repo
|
||||
.read(cx)
|
||||
.set_index_text(&path, new_index_text.map(|rope| rope.to_string()));
|
||||
|
||||
cx.background_spawn(async move { recv.await? })
|
||||
.detach_and_notify_err(window, cx);
|
||||
None
|
||||
}
|
||||
|
||||
pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context<Self>) {
|
||||
@@ -14183,6 +14281,13 @@ impl Editor {
|
||||
EditorSettings::override_global(editor_settings, cx);
|
||||
}
|
||||
|
||||
pub fn line_numbers_enabled(&self, cx: &App) -> bool {
|
||||
if let Some(show_line_numbers) = self.show_line_numbers {
|
||||
return show_line_numbers;
|
||||
}
|
||||
EditorSettings::get_global(cx).gutter.line_numbers
|
||||
}
|
||||
|
||||
pub fn should_use_relative_line_numbers(&self, cx: &mut App) -> bool {
|
||||
self.use_relative_line_numbers
|
||||
.unwrap_or(EditorSettings::get_global(cx).relative_line_numbers)
|
||||
@@ -14858,14 +14963,14 @@ impl Editor {
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> BTreeMap<DisplayRow, Background> {
|
||||
) -> BTreeMap<DisplayRow, LineHighlight> {
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let mut used_highlight_orders = HashMap::default();
|
||||
self.highlighted_rows
|
||||
.iter()
|
||||
.flat_map(|(_, highlighted_rows)| highlighted_rows.iter())
|
||||
.fold(
|
||||
BTreeMap::<DisplayRow, Background>::new(),
|
||||
BTreeMap::<DisplayRow, LineHighlight>::new(),
|
||||
|mut unique_rows, highlight| {
|
||||
let start = highlight.range.start.to_display_point(&snapshot);
|
||||
let end = highlight.range.end.to_display_point(&snapshot);
|
||||
@@ -15378,6 +15483,7 @@ impl Editor {
|
||||
let buffer = self.buffer.read(cx);
|
||||
self.registered_buffers
|
||||
.retain(|buffer_id, _| buffer.buffer(*buffer_id).is_some());
|
||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
|
||||
cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
|
||||
}
|
||||
multi_buffer::Event::ExcerptsEdited {
|
||||
@@ -15397,6 +15503,7 @@ impl Editor {
|
||||
}
|
||||
multi_buffer::Event::Reparsed(buffer_id) => {
|
||||
self.tasks_update_task = Some(self.refresh_runnables(window, cx));
|
||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
|
||||
|
||||
cx.emit(EditorEvent::Reparsed(*buffer_id));
|
||||
}
|
||||
@@ -15405,19 +15512,15 @@ impl Editor {
|
||||
}
|
||||
multi_buffer::Event::LanguageChanged(buffer_id) => {
|
||||
linked_editing_ranges::refresh_linked_ranges(self, window, cx);
|
||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
|
||||
cx.emit(EditorEvent::Reparsed(*buffer_id));
|
||||
cx.notify();
|
||||
}
|
||||
multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged),
|
||||
multi_buffer::Event::Saved => cx.emit(EditorEvent::Saved),
|
||||
multi_buffer::Event::FileHandleChanged | multi_buffer::Event::Reloaded => {
|
||||
cx.emit(EditorEvent::TitleChanged)
|
||||
}
|
||||
// multi_buffer::Event::DiffBaseChanged => {
|
||||
// self.scrollbar_marker_state.dirty = true;
|
||||
// cx.emit(EditorEvent::DiffBaseChanged);
|
||||
// cx.notify();
|
||||
// }
|
||||
multi_buffer::Event::FileHandleChanged
|
||||
| multi_buffer::Event::Reloaded
|
||||
| multi_buffer::Event::BufferDiffChanged => cx.emit(EditorEvent::TitleChanged),
|
||||
multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed),
|
||||
multi_buffer::Event::DiagnosticsUpdated => {
|
||||
self.refresh_active_diagnostics(cx);
|
||||
@@ -16305,7 +16408,7 @@ fn get_uncommitted_diff_for_buffer(
|
||||
}
|
||||
});
|
||||
cx.spawn(|mut cx| async move {
|
||||
let diffs = futures::future::join_all(tasks).await;
|
||||
let diffs = future::join_all(tasks).await;
|
||||
buffer
|
||||
.update(&mut cx, |buffer, cx| {
|
||||
for diff in diffs.into_iter().flatten() {
|
||||
@@ -16875,38 +16978,41 @@ fn snippet_completions(
|
||||
Some(Completion {
|
||||
old_range: range,
|
||||
new_text: snippet.body.clone(),
|
||||
resolved: false,
|
||||
source: CompletionSource::Lsp {
|
||||
server_id: LanguageServerId(usize::MAX),
|
||||
resolved: true,
|
||||
lsp_completion: Box::new(lsp::CompletionItem {
|
||||
label: snippet.prefix.first().unwrap().clone(),
|
||||
kind: Some(CompletionItemKind::SNIPPET),
|
||||
label_details: snippet.description.as_ref().map(|description| {
|
||||
lsp::CompletionItemLabelDetails {
|
||||
detail: Some(description.clone()),
|
||||
description: None,
|
||||
}
|
||||
}),
|
||||
insert_text_format: Some(InsertTextFormat::SNIPPET),
|
||||
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
|
||||
lsp::InsertReplaceEdit {
|
||||
new_text: snippet.body.clone(),
|
||||
insert: lsp_range,
|
||||
replace: lsp_range,
|
||||
},
|
||||
)),
|
||||
filter_text: Some(snippet.body.clone()),
|
||||
sort_text: Some(char::MAX.to_string()),
|
||||
..lsp::CompletionItem::default()
|
||||
}),
|
||||
lsp_defaults: None,
|
||||
},
|
||||
label: CodeLabel {
|
||||
text: matching_prefix.clone(),
|
||||
runs: vec![],
|
||||
runs: Vec::new(),
|
||||
filter_range: 0..matching_prefix.len(),
|
||||
},
|
||||
server_id: LanguageServerId(usize::MAX),
|
||||
documentation: snippet
|
||||
.description
|
||||
.clone()
|
||||
.map(|description| CompletionDocumentation::SingleLine(description.into())),
|
||||
lsp_completion: lsp::CompletionItem {
|
||||
label: snippet.prefix.first().unwrap().clone(),
|
||||
kind: Some(CompletionItemKind::SNIPPET),
|
||||
label_details: snippet.description.as_ref().map(|description| {
|
||||
lsp::CompletionItemLabelDetails {
|
||||
detail: Some(description.clone()),
|
||||
description: None,
|
||||
}
|
||||
}),
|
||||
insert_text_format: Some(InsertTextFormat::SNIPPET),
|
||||
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
|
||||
lsp::InsertReplaceEdit {
|
||||
new_text: snippet.body.clone(),
|
||||
insert: lsp_range,
|
||||
replace: lsp_range,
|
||||
},
|
||||
)),
|
||||
filter_text: Some(snippet.body.clone()),
|
||||
sort_text: Some(char::MAX.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
confirm: None,
|
||||
})
|
||||
})
|
||||
@@ -17254,6 +17360,7 @@ impl EditorSnapshot {
|
||||
if hunk_display_end.column() > 0 {
|
||||
end_row.0 += 1;
|
||||
}
|
||||
let is_created_file = hunk.is_created_file();
|
||||
DisplayDiffHunk::Unfolded {
|
||||
status: hunk.status(),
|
||||
diff_base_byte_range: hunk.diff_base_byte_range,
|
||||
@@ -17263,6 +17370,7 @@ impl EditorSnapshot {
|
||||
hunk.buffer_id,
|
||||
hunk.buffer_range,
|
||||
),
|
||||
is_created_file,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18402,3 +18510,27 @@ impl Render for MissingEditPredictionKeybindingTooltip {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct LineHighlight {
|
||||
pub background: Background,
|
||||
pub border: Option<gpui::Hsla>,
|
||||
}
|
||||
|
||||
impl From<Hsla> for LineHighlight {
|
||||
fn from(hsla: Hsla) -> Self {
|
||||
Self {
|
||||
background: hsla.into(),
|
||||
border: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Background> for LineHighlight {
|
||||
fn from(background: Background) -> Self {
|
||||
Self {
|
||||
background,
|
||||
border: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4931,6 +4931,34 @@ async fn test_paste_multiline(cx: &mut TestAppContext) {
|
||||
)
|
||||
);
|
||||
"});
|
||||
|
||||
// Copy an indented block, starting mid-line
|
||||
cx.set_state(indoc! {"
|
||||
const a: B = (
|
||||
c(),
|
||||
somethin«g(
|
||||
e,
|
||||
f
|
||||
)ˇ»
|
||||
);
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
|
||||
|
||||
// Paste it on a line with a lower indent level
|
||||
cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
|
||||
cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
const a: B = (
|
||||
c(),
|
||||
something(
|
||||
e,
|
||||
f
|
||||
)
|
||||
);
|
||||
g(
|
||||
e,
|
||||
f
|
||||
)ˇ"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -12306,24 +12334,6 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
|
||||
},
|
||||
};
|
||||
|
||||
let item_0_out = lsp::CompletionItem {
|
||||
commit_characters: Some(default_commit_characters.clone()),
|
||||
insert_text_format: Some(default_insert_text_format),
|
||||
..item_0
|
||||
};
|
||||
let items_out = iter::once(item_0_out)
|
||||
.chain(items[1..].iter().map(|item| lsp::CompletionItem {
|
||||
commit_characters: Some(default_commit_characters.clone()),
|
||||
data: Some(default_data.clone()),
|
||||
insert_text_mode: Some(default_insert_text_mode),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: default_edit_range,
|
||||
new_text: item.label.clone(),
|
||||
})),
|
||||
..item.clone()
|
||||
}))
|
||||
.collect::<Vec<lsp::CompletionItem>>();
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
@@ -12342,10 +12352,11 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
|
||||
|
||||
let completion_data = default_data.clone();
|
||||
let completion_characters = default_commit_characters.clone();
|
||||
let completion_items = items.clone();
|
||||
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
|
||||
let default_data = completion_data.clone();
|
||||
let default_commit_characters = completion_characters.clone();
|
||||
let items = items.clone();
|
||||
let items = completion_items.clone();
|
||||
async move {
|
||||
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
||||
items,
|
||||
@@ -12394,7 +12405,7 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
|
||||
.iter()
|
||||
.map(|mat| mat.string.clone())
|
||||
.collect::<Vec<String>>(),
|
||||
items_out
|
||||
items
|
||||
.iter()
|
||||
.map(|completion| completion.label.clone())
|
||||
.collect::<Vec<String>>()
|
||||
@@ -12407,14 +12418,18 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
|
||||
// with 4 from the end.
|
||||
assert_eq!(
|
||||
*resolved_items.lock(),
|
||||
[
|
||||
&items_out[0..16],
|
||||
&items_out[items_out.len() - 4..items_out.len()]
|
||||
]
|
||||
.concat()
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<lsp::CompletionItem>>()
|
||||
[&items[0..16], &items[items.len() - 4..items.len()]]
|
||||
.concat()
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|mut item| {
|
||||
if item.data.is_none() {
|
||||
item.data = Some(default_data.clone());
|
||||
}
|
||||
item
|
||||
})
|
||||
.collect::<Vec<lsp::CompletionItem>>(),
|
||||
"Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
|
||||
);
|
||||
resolved_items.lock().clear();
|
||||
|
||||
@@ -12425,9 +12440,15 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
|
||||
// Completions that have already been resolved are skipped.
|
||||
assert_eq!(
|
||||
*resolved_items.lock(),
|
||||
items_out[items_out.len() - 16..items_out.len() - 4]
|
||||
items[items.len() - 16..items.len() - 4]
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|mut item| {
|
||||
if item.data.is_none() {
|
||||
item.data = Some(default_data.clone());
|
||||
}
|
||||
item
|
||||
})
|
||||
.collect::<Vec<lsp::CompletionItem>>()
|
||||
);
|
||||
resolved_items.lock().clear();
|
||||
@@ -16385,6 +16406,199 @@ async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut Test
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
cx.update(|cx| {
|
||||
let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
|
||||
"keymaps/default-linux.json",
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
cx.bind_keys(default_key_bindings);
|
||||
});
|
||||
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| {
|
||||
let multi_buffer = MultiBuffer::build_multi(
|
||||
[
|
||||
("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
|
||||
("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
|
||||
("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
|
||||
("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
let mut editor = Editor::new(
|
||||
EditorMode::Full,
|
||||
multi_buffer.clone(),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
|
||||
// fold all but the second buffer, so that we test navigating between two
|
||||
// adjacent folded buffers, as well as folded buffers at the start and
|
||||
// end the multibuffer
|
||||
editor.fold_buffer(buffer_ids[0], cx);
|
||||
editor.fold_buffer(buffer_ids[2], cx);
|
||||
editor.fold_buffer(buffer_ids[3], cx);
|
||||
|
||||
editor
|
||||
});
|
||||
cx.simulate_resize(size(px(1000.), px(1000.)));
|
||||
|
||||
let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
ˇ[FOLDED]
|
||||
[EXCERPT]
|
||||
a1
|
||||
b1
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"
|
||||
});
|
||||
cx.simulate_keystroke("down");
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
ˇa1
|
||||
b1
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"
|
||||
});
|
||||
cx.simulate_keystroke("down");
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
a1
|
||||
ˇb1
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"
|
||||
});
|
||||
cx.simulate_keystroke("down");
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
a1
|
||||
b1
|
||||
ˇ[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"
|
||||
});
|
||||
cx.simulate_keystroke("down");
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
a1
|
||||
b1
|
||||
[EXCERPT]
|
||||
ˇ[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"
|
||||
});
|
||||
for _ in 0..5 {
|
||||
cx.simulate_keystroke("down");
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
a1
|
||||
b1
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
ˇ[FOLDED]
|
||||
"
|
||||
});
|
||||
}
|
||||
|
||||
cx.simulate_keystroke("up");
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
a1
|
||||
b1
|
||||
[EXCERPT]
|
||||
ˇ[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"
|
||||
});
|
||||
cx.simulate_keystroke("up");
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
a1
|
||||
b1
|
||||
ˇ[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"
|
||||
});
|
||||
cx.simulate_keystroke("up");
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
a1
|
||||
ˇb1
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"
|
||||
});
|
||||
cx.simulate_keystroke("up");
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
ˇa1
|
||||
b1
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"
|
||||
});
|
||||
for _ in 0..5 {
|
||||
cx.simulate_keystroke("up");
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
ˇ[FOLDED]
|
||||
[EXCERPT]
|
||||
a1
|
||||
b1
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_inline_completion_text(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -16779,6 +16993,245 @@ async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
|
||||
"});
|
||||
}
|
||||
|
||||
mod autoclose_tags {
|
||||
use super::*;
|
||||
use language::language_settings::JsxTagAutoCloseSettings;
|
||||
use languages::language;
|
||||
|
||||
async fn test_setup(cx: &mut TestAppContext) -> EditorTestContext {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.jsx_tag_auto_close = Some(JsxTagAutoCloseSettings { enabled: true });
|
||||
});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| {
|
||||
let language = language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into());
|
||||
|
||||
buffer.set_language(Some(language), cx)
|
||||
});
|
||||
|
||||
cx
|
||||
}
|
||||
|
||||
macro_rules! check {
|
||||
($name:ident, $initial:literal + $input:literal => $expected:expr) => {
|
||||
#[gpui::test]
|
||||
async fn $name(cx: &mut TestAppContext) {
|
||||
let mut cx = test_setup(cx).await;
|
||||
cx.set_state($initial);
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input($input, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state($expected);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
check!(
|
||||
test_basic,
|
||||
"<divˇ" + ">" => "<div>ˇ</div>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_basic_nested,
|
||||
"<div><divˇ</div>" + ">" => "<div><div>ˇ</div></div>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_basic_ignore_already_closed,
|
||||
"<div><divˇ</div></div>" + ">" => "<div><div>ˇ</div></div>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_doesnt_autoclose_closing_tag,
|
||||
"</divˇ" + ">" => "</div>ˇ"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_jsx_attr,
|
||||
"<div attr={</div>}ˇ" + ">" => "<div attr={</div>}>ˇ</div>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_ignores_closing_tags_in_expr_block,
|
||||
"<div><divˇ{</div>}</div>" + ">" => "<div><div>ˇ</div>{</div>}</div>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_doesnt_autoclose_on_gt_in_expr,
|
||||
"<div attr={1 ˇ" + ">" => "<div attr={1 >ˇ"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_ignores_closing_tags_with_different_tag_names,
|
||||
"<div><divˇ</div></span>" + ">" => "<div><div>ˇ</div></div></span>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_autocloses_in_jsx_expression,
|
||||
"<div>{<divˇ}</div>" + ">" => "<div>{<div>ˇ</div>}</div>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_doesnt_autoclose_already_closed_in_jsx_expression,
|
||||
"<div>{<divˇ</div>}</div>" + ">" => "<div>{<div>ˇ</div>}</div>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_autocloses_fragment,
|
||||
"<ˇ" + ">" => "<>ˇ</>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_does_not_include_type_argument_in_autoclose_tag_name,
|
||||
"<Component<T> attr={boolean_value}ˇ" + ">" => "<Component<T> attr={boolean_value}>ˇ</Component>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_does_not_autoclose_doctype,
|
||||
"<!DOCTYPE htmlˇ" + ">" => "<!DOCTYPE html>ˇ"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_does_not_autoclose_comment,
|
||||
"<!-- comment --ˇ" + ">" => "<!-- comment -->ˇ"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_multi_cursor_autoclose_same_tag,
|
||||
r#"
|
||||
<divˇ
|
||||
<divˇ
|
||||
"#
|
||||
+ ">" =>
|
||||
r#"
|
||||
<div>ˇ</div>
|
||||
<div>ˇ</div>
|
||||
"#
|
||||
);
|
||||
|
||||
check!(
|
||||
test_multi_cursor_autoclose_different_tags,
|
||||
r#"
|
||||
<divˇ
|
||||
<spanˇ
|
||||
"#
|
||||
+ ">" =>
|
||||
r#"
|
||||
<div>ˇ</div>
|
||||
<span>ˇ</span>
|
||||
"#
|
||||
);
|
||||
|
||||
check!(
|
||||
test_multi_cursor_autoclose_some_dont_autoclose_others,
|
||||
r#"
|
||||
<divˇ
|
||||
<div /ˇ
|
||||
<spanˇ</span>
|
||||
<!DOCTYPE htmlˇ
|
||||
</headˇ
|
||||
<Component<T>ˇ
|
||||
ˇ
|
||||
"#
|
||||
+ ">" =>
|
||||
r#"
|
||||
<div>ˇ</div>
|
||||
<div />ˇ
|
||||
<span>ˇ</span>
|
||||
<!DOCTYPE html>ˇ
|
||||
</head>ˇ
|
||||
<Component<T>>ˇ</Component>
|
||||
>ˇ
|
||||
"#
|
||||
);
|
||||
|
||||
check!(
|
||||
test_doesnt_mess_up_trailing_text,
|
||||
"<divˇfoobar" + ">" => "<div>ˇ</div>foobar"
|
||||
);
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multibuffer(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.jsx_tag_auto_close = Some(JsxTagAutoCloseSettings { enabled: true });
|
||||
});
|
||||
|
||||
let buffer_a = cx.new(|cx| {
|
||||
let mut buf = language::Buffer::local("<div", cx);
|
||||
buf.set_language(
|
||||
Some(language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into())),
|
||||
cx,
|
||||
);
|
||||
buf
|
||||
});
|
||||
let buffer_b = cx.new(|cx| {
|
||||
let mut buf = language::Buffer::local("<pre", cx);
|
||||
buf.set_language(
|
||||
Some(language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into())),
|
||||
cx,
|
||||
);
|
||||
buf
|
||||
});
|
||||
let buffer_c = cx.new(|cx| {
|
||||
let buf = language::Buffer::local("<span", cx);
|
||||
buf
|
||||
});
|
||||
let buffer = cx.new(|cx| {
|
||||
let mut buf = MultiBuffer::new(language::Capability::ReadWrite);
|
||||
buf.push_excerpts(
|
||||
buffer_a,
|
||||
[ExcerptRange {
|
||||
context: text::Anchor::MIN..text::Anchor::MAX,
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
buf.push_excerpts(
|
||||
buffer_b,
|
||||
[ExcerptRange {
|
||||
context: text::Anchor::MIN..text::Anchor::MAX,
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
buf.push_excerpts(
|
||||
buffer_c,
|
||||
[ExcerptRange {
|
||||
context: text::Anchor::MIN..text::Anchor::MAX,
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
buf
|
||||
});
|
||||
let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
|
||||
|
||||
let mut cx = EditorTestContext::for_editor(editor, cx).await;
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
selections.select(vec![
|
||||
Selection::from_offset(4),
|
||||
Selection::from_offset(9),
|
||||
Selection::from_offset(15),
|
||||
])
|
||||
})
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input(">", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.assert_editor_state("<div>ˇ</div>\n<pre>ˇ</pre>\n<span>ˇ");
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
|
||||
point..point
|
||||
|
||||
@@ -20,10 +20,10 @@ use crate::{
|
||||
DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
|
||||
EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GoToHunk,
|
||||
GoToPreviousHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
|
||||
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineUp, OpenExcerpts, PageDown,
|
||||
PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap,
|
||||
StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
|
||||
FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
|
||||
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp,
|
||||
OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight,
|
||||
Selection, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS,
|
||||
CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
|
||||
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
||||
};
|
||||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||||
@@ -77,7 +77,7 @@ use ui::{
|
||||
POPOVER_Y_PADDING,
|
||||
};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use util::{debug_panic, maybe, RangeExt, ResultExt};
|
||||
use util::{debug_panic, RangeExt, ResultExt};
|
||||
use workspace::{item::Item, notifications::NotifyTaskExt};
|
||||
|
||||
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;
|
||||
@@ -282,7 +282,9 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::move_to_beginning);
|
||||
register_action(editor, window, Editor::move_to_end);
|
||||
register_action(editor, window, Editor::move_to_start_of_excerpt);
|
||||
register_action(editor, window, Editor::move_to_start_of_next_excerpt);
|
||||
register_action(editor, window, Editor::move_to_end_of_excerpt);
|
||||
register_action(editor, window, Editor::move_to_end_of_previous_excerpt);
|
||||
register_action(editor, window, Editor::select_up);
|
||||
register_action(editor, window, Editor::select_down);
|
||||
register_action(editor, window, Editor::select_left);
|
||||
@@ -296,7 +298,9 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::select_to_start_of_paragraph);
|
||||
register_action(editor, window, Editor::select_to_end_of_paragraph);
|
||||
register_action(editor, window, Editor::select_to_start_of_excerpt);
|
||||
register_action(editor, window, Editor::select_to_start_of_next_excerpt);
|
||||
register_action(editor, window, Editor::select_to_end_of_excerpt);
|
||||
register_action(editor, window, Editor::select_to_end_of_previous_excerpt);
|
||||
register_action(editor, window, Editor::select_to_beginning);
|
||||
register_action(editor, window, Editor::select_to_end);
|
||||
register_action(editor, window, Editor::select_all);
|
||||
@@ -954,7 +958,6 @@ impl EditorElement {
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_selections(
|
||||
&self,
|
||||
start_anchor: Anchor,
|
||||
@@ -1126,7 +1129,6 @@ impl EditorElement {
|
||||
cursors
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_visible_cursors(
|
||||
&self,
|
||||
snapshot: &EditorSnapshot,
|
||||
@@ -1480,7 +1482,6 @@ impl EditorElement {
|
||||
axis_pair(horizontal_scrollbar, vertical_scrollbar)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepaint_crease_toggles(
|
||||
&self,
|
||||
crease_toggles: &mut [Option<AnyElement>],
|
||||
@@ -1515,7 +1516,6 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepaint_crease_trailers(
|
||||
&self,
|
||||
trailers: Vec<Option<AnyElement>>,
|
||||
@@ -1592,7 +1592,6 @@ impl EditorElement {
|
||||
display_hunks
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_inline_diagnostics(
|
||||
&self,
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
@@ -1691,7 +1690,7 @@ impl EditorElement {
|
||||
let pos_y = content_origin.y
|
||||
+ line_height * (row.0 as f32 - scroll_pixel_position.y / line_height);
|
||||
|
||||
let window_ix = row.minus(start_row) as usize;
|
||||
let window_ix = row.0.saturating_sub(start_row.0) as usize;
|
||||
let pos_x = {
|
||||
let crease_trailer_layout = &crease_trailers[window_ix];
|
||||
let line_layout = &line_layouts[window_ix];
|
||||
@@ -1724,7 +1723,7 @@ impl EditorElement {
|
||||
.h(line_height)
|
||||
.w_full()
|
||||
.px_1()
|
||||
.rounded_sm()
|
||||
.rounded_xs()
|
||||
.opacity(opacity)
|
||||
.bg(severity_to_color(&diagnostic_to_render.severity)
|
||||
.color(cx)
|
||||
@@ -1743,7 +1742,6 @@ impl EditorElement {
|
||||
elements
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_inline_blame(
|
||||
&self,
|
||||
display_row: DisplayRow,
|
||||
@@ -1823,7 +1821,6 @@ impl EditorElement {
|
||||
Some(element)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_blame_entries(
|
||||
&self,
|
||||
buffer_rows: &[RowInfo],
|
||||
@@ -1892,7 +1889,6 @@ impl EditorElement {
|
||||
Some(shaped_lines)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_indent_guides(
|
||||
&self,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
@@ -2010,7 +2006,6 @@ impl EditorElement {
|
||||
(offset_y, length)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_run_indicators(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
@@ -2104,7 +2099,6 @@ impl EditorElement {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_code_actions_indicator(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
@@ -2203,7 +2197,6 @@ impl EditorElement {
|
||||
relative_rows
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_line_numbers(
|
||||
&self,
|
||||
gutter_hitbox: Option<&Hitbox>,
|
||||
@@ -2419,7 +2412,6 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepaint_lines(
|
||||
&self,
|
||||
start_row: DisplayRow,
|
||||
@@ -2446,7 +2438,6 @@ impl EditorElement {
|
||||
line_elements
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_block(
|
||||
&self,
|
||||
block: &Block,
|
||||
@@ -2676,24 +2667,21 @@ impl EditorElement {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Div {
|
||||
let file_status = maybe!({
|
||||
let project = self.editor.read(cx).project.as_ref()?.read(cx);
|
||||
let (repo, path) =
|
||||
project.repository_and_path_for_buffer_id(for_excerpt.buffer_id, cx)?;
|
||||
let status = repo.read(cx).repository_entry.status_for_path(&path)?;
|
||||
Some(status.status)
|
||||
})
|
||||
.filter(|_| {
|
||||
self.editor
|
||||
.read(cx)
|
||||
.buffer
|
||||
.read(cx)
|
||||
.all_diff_hunks_expanded()
|
||||
});
|
||||
|
||||
let include_root = self
|
||||
.editor
|
||||
let editor = self.editor.read(cx);
|
||||
let file_status = editor
|
||||
.buffer
|
||||
.read(cx)
|
||||
.all_diff_hunks_expanded()
|
||||
.then(|| {
|
||||
editor
|
||||
.project
|
||||
.as_ref()?
|
||||
.read(cx)
|
||||
.status_for_buffer_id(for_excerpt.buffer_id, cx)
|
||||
})
|
||||
.flatten();
|
||||
|
||||
let include_root = editor
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
|
||||
@@ -2705,7 +2693,7 @@ impl EditorElement {
|
||||
let parent_path = path.as_ref().and_then(|path| {
|
||||
Some(path.parent()?.to_string_lossy().to_string() + std::path::MAIN_SEPARATOR_STR)
|
||||
});
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
div()
|
||||
@@ -2720,7 +2708,7 @@ impl EditorElement {
|
||||
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
|
||||
.pl_0p5()
|
||||
.pr_5()
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.shadow_md()
|
||||
.border_1()
|
||||
.map(|div| {
|
||||
@@ -2744,7 +2732,7 @@ impl EditorElement {
|
||||
header.child(
|
||||
div()
|
||||
.hover(|style| style.bg(colors.element_selected))
|
||||
.rounded_sm()
|
||||
.rounded_xs()
|
||||
.child(
|
||||
ButtonLike::new("toggle-buffer-fold")
|
||||
.style(ui::ButtonStyle::Transparent)
|
||||
@@ -2778,8 +2766,7 @@ impl EditorElement {
|
||||
)
|
||||
})
|
||||
.children(
|
||||
self.editor
|
||||
.read(cx)
|
||||
editor
|
||||
.addons
|
||||
.values()
|
||||
.filter_map(|addon| {
|
||||
@@ -2950,7 +2937,6 @@ impl EditorElement {
|
||||
}))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_blocks(
|
||||
&self,
|
||||
rows: Range<DisplayRow>,
|
||||
@@ -3135,7 +3121,6 @@ impl EditorElement {
|
||||
|
||||
/// Returns true if any of the blocks changed size since the previous frame. This will trigger
|
||||
/// a restart of rendering for the editor based on the new sizes.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_blocks(
|
||||
&self,
|
||||
blocks: &mut Vec<BlockLayout>,
|
||||
@@ -3179,7 +3164,6 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_sticky_buffer_header(
|
||||
&self,
|
||||
StickyHeaderExcerpt {
|
||||
@@ -3254,7 +3238,6 @@ impl EditorElement {
|
||||
header
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_cursor_popovers(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
@@ -3443,7 +3426,6 @@ impl EditorElement {
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_gutter_menu(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
@@ -3496,7 +3478,6 @@ impl EditorElement {
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_popovers_above_or_below_line(
|
||||
&self,
|
||||
target_position: gpui::Point<Pixels>,
|
||||
@@ -3610,7 +3591,6 @@ impl EditorElement {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_context_menu_aside(
|
||||
&self,
|
||||
y_flipped: bool,
|
||||
@@ -3806,7 +3786,6 @@ impl EditorElement {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_hover_popovers(
|
||||
&self,
|
||||
snapshot: &EditorSnapshot,
|
||||
@@ -3923,7 +3902,6 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_diff_hunk_controls(
|
||||
&self,
|
||||
row_range: Range<DisplayRow>,
|
||||
@@ -3952,6 +3930,7 @@ impl EditorElement {
|
||||
display_row_range,
|
||||
multi_buffer_range,
|
||||
status,
|
||||
is_created_file,
|
||||
..
|
||||
} = &hunk
|
||||
{
|
||||
@@ -3983,6 +3962,7 @@ impl EditorElement {
|
||||
display_row_range.start.0,
|
||||
status,
|
||||
multi_buffer_range.clone(),
|
||||
*is_created_file,
|
||||
line_height,
|
||||
&editor,
|
||||
cx,
|
||||
@@ -4006,7 +3986,6 @@ impl EditorElement {
|
||||
controls
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_signature_help(
|
||||
&self,
|
||||
hitbox: &Hitbox,
|
||||
@@ -4134,46 +4113,74 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
let mut paint_highlight =
|
||||
|highlight_row_start: DisplayRow, highlight_row_end: DisplayRow, color| {
|
||||
let origin = point(
|
||||
layout.hitbox.origin.x,
|
||||
layout.hitbox.origin.y
|
||||
+ (highlight_row_start.as_f32() - scroll_top)
|
||||
* layout.position_map.line_height,
|
||||
);
|
||||
let size = size(
|
||||
layout.hitbox.size.width,
|
||||
layout.position_map.line_height
|
||||
* highlight_row_end.next_row().minus(highlight_row_start) as f32,
|
||||
);
|
||||
window.paint_quad(fill(Bounds { origin, size }, color));
|
||||
};
|
||||
let mut paint_highlight = |highlight_row_start: DisplayRow,
|
||||
highlight_row_end: DisplayRow,
|
||||
highlight: crate::LineHighlight,
|
||||
edges| {
|
||||
let origin = point(
|
||||
layout.hitbox.origin.x,
|
||||
layout.hitbox.origin.y
|
||||
+ (highlight_row_start.as_f32() - scroll_top)
|
||||
* layout.position_map.line_height,
|
||||
);
|
||||
let size = size(
|
||||
layout.hitbox.size.width,
|
||||
layout.position_map.line_height
|
||||
* highlight_row_end.next_row().minus(highlight_row_start) as f32,
|
||||
);
|
||||
let mut quad = fill(Bounds { origin, size }, highlight.background);
|
||||
if let Some(border_color) = highlight.border {
|
||||
quad.border_color = border_color;
|
||||
quad.border_widths = edges
|
||||
}
|
||||
window.paint_quad(quad);
|
||||
};
|
||||
|
||||
let mut current_paint: Option<(gpui::Background, Range<DisplayRow>)> = None;
|
||||
let mut current_paint: Option<(LineHighlight, Range<DisplayRow>, Edges<Pixels>)> =
|
||||
None;
|
||||
for (&new_row, &new_background) in &layout.highlighted_rows {
|
||||
match &mut current_paint {
|
||||
Some((current_background, current_range)) => {
|
||||
Some((current_background, current_range, mut edges)) => {
|
||||
let current_background = *current_background;
|
||||
let new_range_started = current_background != new_background
|
||||
|| current_range.end.next_row() != new_row;
|
||||
if new_range_started {
|
||||
if current_range.end.next_row() == new_row {
|
||||
edges.bottom = px(0.);
|
||||
};
|
||||
paint_highlight(
|
||||
current_range.start,
|
||||
current_range.end,
|
||||
current_background,
|
||||
edges,
|
||||
);
|
||||
current_paint = Some((new_background, new_row..new_row));
|
||||
let edges = Edges {
|
||||
top: if current_range.end.next_row() != new_row {
|
||||
px(1.)
|
||||
} else {
|
||||
px(0.)
|
||||
},
|
||||
bottom: px(1.),
|
||||
..Default::default()
|
||||
};
|
||||
current_paint = Some((new_background, new_row..new_row, edges));
|
||||
continue;
|
||||
} else {
|
||||
current_range.end = current_range.end.next_row();
|
||||
}
|
||||
}
|
||||
None => current_paint = Some((new_background, new_row..new_row)),
|
||||
None => {
|
||||
let edges = Edges {
|
||||
top: px(1.),
|
||||
bottom: px(1.),
|
||||
..Default::default()
|
||||
};
|
||||
current_paint = Some((new_background, new_row..new_row, edges))
|
||||
}
|
||||
};
|
||||
}
|
||||
if let Some((color, range)) = current_paint {
|
||||
paint_highlight(range.start, range.end, color);
|
||||
if let Some((color, range, edges)) = current_paint {
|
||||
paint_highlight(range.start, range.end, color, edges);
|
||||
}
|
||||
|
||||
let scroll_left =
|
||||
@@ -4351,6 +4358,11 @@ impl EditorElement {
|
||||
fn paint_gutter_diff_hunks(layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
|
||||
let is_light = cx.theme().appearance().is_light();
|
||||
|
||||
let hunk_style = ProjectSettings::get_global(cx)
|
||||
.git
|
||||
.hunk_style
|
||||
.unwrap_or_default();
|
||||
|
||||
if layout.display_hunks.is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -4414,9 +4426,23 @@ impl EditorElement {
|
||||
if let Some((hunk_bounds, mut background_color, corner_radii, secondary_status)) =
|
||||
hunk_to_paint
|
||||
{
|
||||
if secondary_status.has_secondary_hunk() {
|
||||
background_color =
|
||||
background_color.opacity(if is_light { 0.2 } else { 0.32 });
|
||||
match hunk_style {
|
||||
GitHunkStyleSetting::Transparent | GitHunkStyleSetting::Pattern => {
|
||||
if secondary_status.has_secondary_hunk() {
|
||||
background_color =
|
||||
background_color.opacity(if is_light { 0.2 } else { 0.32 });
|
||||
}
|
||||
}
|
||||
GitHunkStyleSetting::StagedPattern
|
||||
| GitHunkStyleSetting::StagedTransparent => {
|
||||
if !secondary_status.has_secondary_hunk() {
|
||||
background_color =
|
||||
background_color.opacity(if is_light { 0.2 } else { 0.32 });
|
||||
}
|
||||
}
|
||||
GitHunkStyleSetting::StagedBorder | GitHunkStyleSetting::Border => {
|
||||
// Don't change the background color
|
||||
}
|
||||
}
|
||||
|
||||
// Flatten the background color with the editor color to prevent
|
||||
@@ -4627,6 +4653,7 @@ impl EditorElement {
|
||||
};
|
||||
window.set_cursor_style(cursor_style, &layout.position_map.text_hitbox);
|
||||
|
||||
self.paint_lines_background(layout, window, cx);
|
||||
let invisible_display_ranges = self.paint_highlights(layout, window);
|
||||
self.paint_lines(&invisible_display_ranges, layout, window, cx);
|
||||
self.paint_redactions(layout, window);
|
||||
@@ -4717,6 +4744,18 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_lines_background(
|
||||
&mut self,
|
||||
layout: &mut EditorLayout,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() {
|
||||
let row = DisplayRow(layout.visible_display_row_range.start.0 + ix as u32);
|
||||
line_with_invisibles.draw_background(layout, row, layout.content_origin, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_redactions(&mut self, layout: &EditorLayout, window: &mut Window) {
|
||||
if layout.redacted_ranges.is_empty() {
|
||||
return;
|
||||
@@ -5255,7 +5294,6 @@ impl EditorElement {
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn paint_highlighted_range(
|
||||
&self,
|
||||
range: Range<DisplayPoint>,
|
||||
@@ -5681,7 +5719,6 @@ impl AcceptEditPredictionBinding {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepaint_gutter_button(
|
||||
button: IconButton,
|
||||
row: DisplayRow,
|
||||
@@ -5932,7 +5969,6 @@ impl fmt::Debug for LineFragment {
|
||||
}
|
||||
|
||||
impl LineWithInvisibles {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn from_chunks<'a>(
|
||||
chunks: impl Iterator<Item = HighlightedChunk<'a>>,
|
||||
editor_style: &EditorStyle,
|
||||
@@ -6137,7 +6173,6 @@ impl LineWithInvisibles {
|
||||
layouts
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
line_height: Pixels,
|
||||
@@ -6172,7 +6207,6 @@ impl LineWithInvisibles {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw(
|
||||
&self,
|
||||
layout: &EditorLayout,
|
||||
@@ -6216,7 +6250,35 @@ impl LineWithInvisibles {
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_background(
|
||||
&self,
|
||||
layout: &EditorLayout,
|
||||
row: DisplayRow,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let line_height = layout.position_map.line_height;
|
||||
let line_y = line_height
|
||||
* (row.as_f32() - layout.position_map.scroll_pixel_position.y / line_height);
|
||||
|
||||
let mut fragment_origin =
|
||||
content_origin + gpui::point(-layout.position_map.scroll_pixel_position.x, line_y);
|
||||
|
||||
for fragment in &self.fragments {
|
||||
match fragment {
|
||||
LineFragment::Text(line) => {
|
||||
line.paint_background(fragment_origin, line_height, window, cx)
|
||||
.log_err();
|
||||
fragment_origin.x += line.width;
|
||||
}
|
||||
LineFragment::Element { size, .. } => {
|
||||
fragment_origin.x += size.width;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_invisibles(
|
||||
&self,
|
||||
selection_ranges: &[Range<DisplayPoint>],
|
||||
@@ -6736,10 +6798,10 @@ impl Element for EditorElement {
|
||||
.update(cx, |editor, cx| editor.highlighted_display_rows(window, cx));
|
||||
|
||||
let is_light = cx.theme().appearance().is_light();
|
||||
let use_pattern = ProjectSettings::get_global(cx)
|
||||
let hunk_style = ProjectSettings::get_global(cx)
|
||||
.git
|
||||
.hunk_style
|
||||
.map_or(false, |style| matches!(style, GitHunkStyleSetting::Pattern));
|
||||
.unwrap_or_default();
|
||||
|
||||
for (ix, row_info) in row_infos.iter().enumerate() {
|
||||
let Some(diff_status) = row_info.diff_status else {
|
||||
@@ -6759,26 +6821,74 @@ impl Element for EditorElement {
|
||||
|
||||
let unstaged = diff_status.has_secondary_hunk();
|
||||
let hunk_opacity = if is_light { 0.16 } else { 0.12 };
|
||||
let slash_width = line_height.0 / 1.5; // ~16 by default
|
||||
|
||||
let staged_background =
|
||||
solid_background(background_color.opacity(hunk_opacity));
|
||||
let unstaged_background = if use_pattern {
|
||||
pattern_slash(
|
||||
background_color.opacity(hunk_opacity),
|
||||
window.rem_size().0 * 1.125, // ~18 by default
|
||||
)
|
||||
} else {
|
||||
solid_background(background_color.opacity(if is_light {
|
||||
0.08
|
||||
} else {
|
||||
0.04
|
||||
}))
|
||||
let staged_highlight: LineHighlight = match hunk_style {
|
||||
GitHunkStyleSetting::Transparent
|
||||
| GitHunkStyleSetting::Pattern
|
||||
| GitHunkStyleSetting::Border => {
|
||||
solid_background(background_color.opacity(hunk_opacity)).into()
|
||||
}
|
||||
GitHunkStyleSetting::StagedPattern => {
|
||||
pattern_slash(background_color.opacity(hunk_opacity), slash_width)
|
||||
.into()
|
||||
}
|
||||
GitHunkStyleSetting::StagedTransparent => {
|
||||
solid_background(background_color.opacity(if is_light {
|
||||
0.08
|
||||
} else {
|
||||
0.04
|
||||
}))
|
||||
.into()
|
||||
}
|
||||
GitHunkStyleSetting::StagedBorder => LineHighlight {
|
||||
background: (background_color.opacity(if is_light {
|
||||
0.08
|
||||
} else {
|
||||
0.06
|
||||
}))
|
||||
.into(),
|
||||
border: Some(if is_light {
|
||||
background_color.opacity(0.48)
|
||||
} else {
|
||||
background_color.opacity(0.36)
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
let unstaged_highlight = match hunk_style {
|
||||
GitHunkStyleSetting::Transparent => {
|
||||
solid_background(background_color.opacity(if is_light {
|
||||
0.08
|
||||
} else {
|
||||
0.04
|
||||
}))
|
||||
.into()
|
||||
}
|
||||
GitHunkStyleSetting::Pattern => {
|
||||
pattern_slash(background_color.opacity(hunk_opacity), slash_width)
|
||||
.into()
|
||||
}
|
||||
GitHunkStyleSetting::Border => LineHighlight {
|
||||
background: (background_color.opacity(if is_light {
|
||||
0.08
|
||||
} else {
|
||||
0.02
|
||||
}))
|
||||
.into(),
|
||||
border: Some(background_color.opacity(0.5)),
|
||||
},
|
||||
GitHunkStyleSetting::StagedPattern
|
||||
| GitHunkStyleSetting::StagedTransparent
|
||||
| GitHunkStyleSetting::StagedBorder => {
|
||||
solid_background(background_color.opacity(hunk_opacity)).into()
|
||||
}
|
||||
};
|
||||
|
||||
let background = if unstaged {
|
||||
unstaged_background
|
||||
unstaged_highlight
|
||||
} else {
|
||||
staged_background
|
||||
staged_highlight
|
||||
};
|
||||
|
||||
highlighted_rows
|
||||
@@ -7562,7 +7672,6 @@ struct ScrollbarRangeData {
|
||||
}
|
||||
|
||||
impl ScrollbarRangeData {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
scrollbar_bounds: Bounds<Pixels>,
|
||||
letter_size: Size<Pixels>,
|
||||
@@ -7627,7 +7736,7 @@ pub struct EditorLayout {
|
||||
indent_guides: Option<Vec<IndentGuideLayout>>,
|
||||
visible_display_row_range: Range<DisplayRow>,
|
||||
active_rows: BTreeMap<DisplayRow, bool>,
|
||||
highlighted_rows: BTreeMap<DisplayRow, gpui::Background>,
|
||||
highlighted_rows: BTreeMap<DisplayRow, LineHighlight>,
|
||||
line_elements: SmallVec<[AnyElement; 1]>,
|
||||
line_numbers: Arc<HashMap<MultiBufferRow, LineNumberLayout>>,
|
||||
display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
|
||||
@@ -8790,6 +8899,7 @@ fn diff_hunk_controls(
|
||||
row: u32,
|
||||
status: &DiffHunkStatus,
|
||||
hunk_range: Range<Anchor>,
|
||||
is_created_file: bool,
|
||||
line_height: Pixels,
|
||||
editor: &Entity<Editor>,
|
||||
cx: &mut App,
|
||||
@@ -8798,13 +8908,16 @@ fn diff_hunk_controls(
|
||||
.h(line_height)
|
||||
.mr_1()
|
||||
.gap_1()
|
||||
.px_1()
|
||||
.px_0p5()
|
||||
.pb_1()
|
||||
.border_x_1()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_b_lg()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.gap_1()
|
||||
.occlude()
|
||||
.shadow_md()
|
||||
.child(if status.has_secondary_hunk() {
|
||||
Button::new(("stage", row as u64), "Stage")
|
||||
.alpha(if status.is_pending() { 0.66 } else { 1.0 })
|
||||
@@ -8822,12 +8935,11 @@ fn diff_hunk_controls(
|
||||
})
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
move |_event, window, cx| {
|
||||
move |_event, _window, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.stage_or_unstage_diff_hunks(
|
||||
true,
|
||||
&[hunk_range.start..hunk_range.start],
|
||||
window,
|
||||
vec![hunk_range.start..hunk_range.start],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -8850,12 +8962,11 @@ fn diff_hunk_controls(
|
||||
})
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
move |_event, window, cx| {
|
||||
move |_event, _window, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.stage_or_unstage_diff_hunks(
|
||||
false,
|
||||
&[hunk_range.start..hunk_range.start],
|
||||
window,
|
||||
vec![hunk_range.start..hunk_range.start],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -8863,7 +8974,7 @@ fn diff_hunk_controls(
|
||||
})
|
||||
})
|
||||
.child(
|
||||
Button::new("discard", "Restore")
|
||||
Button::new("restore", "Restore")
|
||||
.tooltip({
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
move |window, cx| {
|
||||
@@ -8885,7 +8996,8 @@ fn diff_hunk_controls(
|
||||
editor.restore_hunks_in_ranges(vec![point..point], window, cx);
|
||||
});
|
||||
}
|
||||
}),
|
||||
})
|
||||
.disabled(is_created_file),
|
||||
)
|
||||
.when(
|
||||
!editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(),
|
||||
@@ -8914,7 +9026,7 @@ fn diff_hunk_controls(
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let position =
|
||||
hunk_range.end.to_point(&snapshot.buffer_snapshot);
|
||||
editor.go_to_hunk_after_or_before_position(
|
||||
editor.go_to_hunk_before_or_after_position(
|
||||
&snapshot,
|
||||
position,
|
||||
Direction::Next,
|
||||
@@ -8950,7 +9062,7 @@ fn diff_hunk_controls(
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let point =
|
||||
hunk_range.start.to_point(&snapshot.buffer_snapshot);
|
||||
editor.go_to_hunk_after_or_before_position(
|
||||
editor.go_to_hunk_before_or_after_position(
|
||||
&snapshot,
|
||||
point,
|
||||
Direction::Prev,
|
||||
|
||||
@@ -195,9 +195,12 @@ impl GitBlame {
|
||||
) -> impl 'a + Iterator<Item = Option<BlameEntry>> {
|
||||
self.sync(cx);
|
||||
|
||||
let buffer_id = self.buffer_snapshot.remote_id();
|
||||
let mut cursor = self.entries.cursor::<u32>(&());
|
||||
rows.into_iter().map(move |info| {
|
||||
let row = info.buffer_row?;
|
||||
let row = info
|
||||
.buffer_row
|
||||
.filter(|_| info.buffer_id == Some(buffer_id))?;
|
||||
cursor.seek_forward(&row, Bias::Right, &());
|
||||
cursor.item()?.blame.clone()
|
||||
})
|
||||
@@ -367,7 +370,6 @@ impl GitBlame {
|
||||
async move {
|
||||
let Some(Blame {
|
||||
entries,
|
||||
permalinks,
|
||||
messages,
|
||||
remote_url,
|
||||
}) = blame.await?
|
||||
@@ -376,13 +378,8 @@ impl GitBlame {
|
||||
};
|
||||
|
||||
let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row);
|
||||
let commit_details = parse_commit_messages(
|
||||
messages,
|
||||
remote_url,
|
||||
&permalinks,
|
||||
provider_registry,
|
||||
)
|
||||
.await;
|
||||
let commit_details =
|
||||
parse_commit_messages(messages, remote_url, provider_registry).await;
|
||||
|
||||
anyhow::Ok(Some((entries, commit_details)))
|
||||
}
|
||||
@@ -474,7 +471,6 @@ fn build_blame_entry_sum_tree(entries: Vec<BlameEntry>, max_row: u32) -> SumTree
|
||||
async fn parse_commit_messages(
|
||||
messages: impl IntoIterator<Item = (Oid, String)>,
|
||||
remote_url: Option<String>,
|
||||
deprecated_permalinks: &HashMap<Oid, Url>,
|
||||
provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
) -> HashMap<Oid, ParsedCommitMessage> {
|
||||
let mut commit_details = HashMap::default();
|
||||
@@ -492,11 +488,7 @@ async fn parse_commit_messages(
|
||||
},
|
||||
))
|
||||
} else {
|
||||
// DEPRECATED (18 Apr 24): Sending permalinks over the wire is deprecated. Clients
|
||||
// now do the parsing. This is here for backwards compatibility, so that
|
||||
// when an old peer sends a client no `parsed_remote_url` but `deprecated_permalinks`,
|
||||
// we fall back to that.
|
||||
deprecated_permalinks.get(&oid).cloned()
|
||||
continue;
|
||||
};
|
||||
|
||||
let remote = parsed_remote_url
|
||||
@@ -535,6 +527,7 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{cmp, env, ops::Range, path::Path};
|
||||
use text::BufferId;
|
||||
use unindent::Unindent as _;
|
||||
use util::{path, RandomCharIter};
|
||||
|
||||
@@ -552,16 +545,18 @@ mod tests {
|
||||
#[track_caller]
|
||||
fn assert_blame_rows(
|
||||
blame: &mut GitBlame,
|
||||
buffer_id: BufferId,
|
||||
rows: Range<u32>,
|
||||
expected: Vec<Option<BlameEntry>>,
|
||||
cx: &mut Context<GitBlame>,
|
||||
) {
|
||||
assert_eq!(
|
||||
pretty_assertions::assert_eq!(
|
||||
blame
|
||||
.blame_for_rows(
|
||||
&rows
|
||||
.map(|row| RowInfo {
|
||||
buffer_row: Some(row),
|
||||
buffer_id: Some(buffer_id),
|
||||
..Default::default()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -694,6 +689,7 @@ mod tests {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id());
|
||||
|
||||
let git_blame = cx.new(|cx| GitBlame::new(buffer.clone(), project, false, true, cx));
|
||||
|
||||
@@ -701,12 +697,13 @@ mod tests {
|
||||
|
||||
git_blame.update(cx, |blame, cx| {
|
||||
// All lines
|
||||
assert_eq!(
|
||||
pretty_assertions::assert_eq!(
|
||||
blame
|
||||
.blame_for_rows(
|
||||
&(0..8)
|
||||
.map(|buffer_row| RowInfo {
|
||||
buffer_row: Some(buffer_row),
|
||||
buffer_id: Some(buffer_id),
|
||||
..Default::default()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -725,12 +722,13 @@ mod tests {
|
||||
]
|
||||
);
|
||||
// Subset of lines
|
||||
assert_eq!(
|
||||
pretty_assertions::assert_eq!(
|
||||
blame
|
||||
.blame_for_rows(
|
||||
&(1..4)
|
||||
.map(|buffer_row| RowInfo {
|
||||
buffer_row: Some(buffer_row),
|
||||
buffer_id: Some(buffer_id),
|
||||
..Default::default()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -744,12 +742,13 @@ mod tests {
|
||||
]
|
||||
);
|
||||
// Subset of lines, with some not displayed
|
||||
assert_eq!(
|
||||
pretty_assertions::assert_eq!(
|
||||
blame
|
||||
.blame_for_rows(
|
||||
&[
|
||||
RowInfo {
|
||||
buffer_row: Some(1),
|
||||
buffer_id: Some(buffer_id),
|
||||
..Default::default()
|
||||
},
|
||||
Default::default(),
|
||||
@@ -800,6 +799,7 @@ mod tests {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id());
|
||||
|
||||
let git_blame = cx.new(|cx| GitBlame::new(buffer.clone(), project, false, true, cx));
|
||||
|
||||
@@ -810,6 +810,7 @@ mod tests {
|
||||
// lines.
|
||||
assert_blame_rows(
|
||||
blame,
|
||||
buffer_id,
|
||||
0..4,
|
||||
vec![
|
||||
Some(blame_entry("1b1b1b", 0..4)),
|
||||
@@ -828,6 +829,7 @@ mod tests {
|
||||
git_blame.update(cx, |blame, cx| {
|
||||
assert_blame_rows(
|
||||
blame,
|
||||
buffer_id,
|
||||
0..2,
|
||||
vec![None, Some(blame_entry("1b1b1b", 0..4))],
|
||||
cx,
|
||||
@@ -840,6 +842,7 @@ mod tests {
|
||||
git_blame.update(cx, |blame, cx| {
|
||||
assert_blame_rows(
|
||||
blame,
|
||||
buffer_id,
|
||||
1..4,
|
||||
vec![
|
||||
None,
|
||||
@@ -852,7 +855,13 @@ mod tests {
|
||||
|
||||
// Before we insert a newline at the end, sanity check:
|
||||
git_blame.update(cx, |blame, cx| {
|
||||
assert_blame_rows(blame, 3..4, vec![Some(blame_entry("1b1b1b", 0..4))], cx);
|
||||
assert_blame_rows(
|
||||
blame,
|
||||
buffer_id,
|
||||
3..4,
|
||||
vec![Some(blame_entry("1b1b1b", 0..4))],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
// Insert a newline at the end
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
@@ -862,6 +871,7 @@ mod tests {
|
||||
git_blame.update(cx, |blame, cx| {
|
||||
assert_blame_rows(
|
||||
blame,
|
||||
buffer_id,
|
||||
3..5,
|
||||
vec![Some(blame_entry("1b1b1b", 0..4)), None],
|
||||
cx,
|
||||
@@ -870,7 +880,13 @@ mod tests {
|
||||
|
||||
// Before we insert a newline at the start, sanity check:
|
||||
git_blame.update(cx, |blame, cx| {
|
||||
assert_blame_rows(blame, 2..3, vec![Some(blame_entry("1b1b1b", 0..4))], cx);
|
||||
assert_blame_rows(
|
||||
blame,
|
||||
buffer_id,
|
||||
2..3,
|
||||
vec![Some(blame_entry("1b1b1b", 0..4))],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Usage example
|
||||
@@ -882,6 +898,7 @@ mod tests {
|
||||
git_blame.update(cx, |blame, cx| {
|
||||
assert_blame_rows(
|
||||
blame,
|
||||
buffer_id,
|
||||
2..4,
|
||||
vec![None, Some(blame_entry("1b1b1b", 0..4))],
|
||||
cx,
|
||||
|
||||
@@ -241,8 +241,10 @@ impl Editor {
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
return self.navigate_to_hover_links(None, links, modifiers.alt, window, cx);
|
||||
let navigate_task =
|
||||
self.navigate_to_hover_links(None, links, modifiers.alt, window, cx);
|
||||
self.select(SelectPhase::End, window, cx);
|
||||
return navigate_task;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +260,7 @@ impl Editor {
|
||||
cx,
|
||||
);
|
||||
|
||||
if point.as_valid().is_some() {
|
||||
let navigate_task = if point.as_valid().is_some() {
|
||||
if modifiers.shift {
|
||||
self.go_to_type_definition(&GoToTypeDefinition, window, cx)
|
||||
} else {
|
||||
@@ -266,7 +268,9 @@ impl Editor {
|
||||
}
|
||||
} else {
|
||||
Task::ready(Ok(Navigated::No))
|
||||
}
|
||||
};
|
||||
self.select(SelectPhase::End, window, cx);
|
||||
return navigate_task;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ use theme::ThemeSettings;
|
||||
use ui::{prelude::*, theme_is_transparent, Scrollbar, ScrollbarState};
|
||||
use url::Url;
|
||||
use util::TryFutureExt;
|
||||
use workspace::Workspace;
|
||||
use workspace::{OpenOptions, OpenVisible, Workspace};
|
||||
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
|
||||
|
||||
pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
|
||||
@@ -632,8 +632,15 @@ pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App)
|
||||
if uri.scheme() == "file" {
|
||||
if let Some(workspace) = window.root::<Workspace>().flatten() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let task =
|
||||
workspace.open_abs_path(PathBuf::from(uri.path()), false, window, cx);
|
||||
let task = workspace.open_abs_path(
|
||||
PathBuf::from(uri.path()),
|
||||
OpenOptions {
|
||||
visible: Some(OpenVisible::None),
|
||||
..Default::default()
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn_in(window, |_, mut cx| async move {
|
||||
let item = task.await?;
|
||||
|
||||
@@ -38,10 +38,14 @@ use text::{BufferId, Selection};
|
||||
use theme::{Theme, ThemeSettings};
|
||||
use ui::{prelude::*, IconDecorationKind};
|
||||
use util::{paths::PathExt, ResultExt, TryFutureExt};
|
||||
use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams};
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, FollowEvent},
|
||||
searchable::SearchOptions,
|
||||
OpenVisible,
|
||||
};
|
||||
use workspace::{
|
||||
item::{Dedup, ItemSettings, SerializableItem, TabContentParams},
|
||||
OpenOptions,
|
||||
};
|
||||
use workspace::{
|
||||
item::{FollowableItem, Item, ItemEvent, ProjectItem},
|
||||
@@ -1157,7 +1161,15 @@ impl SerializableItem for Editor {
|
||||
}
|
||||
None => {
|
||||
let open_by_abs_path = workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_abs_path(abs_path.clone(), false, window, cx)
|
||||
workspace.open_abs_path(
|
||||
abs_path.clone(),
|
||||
OpenOptions {
|
||||
visible: Some(OpenVisible::None),
|
||||
..Default::default()
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
window.spawn(cx, |mut cx| async move {
|
||||
let editor = open_by_abs_path?.await?.downcast::<Editor>().with_context(|| format!("Failed to downcast to Editor after opening abs path {abs_path:?}"))?;
|
||||
|
||||
616
crates/editor/src/jsx_tag_auto_close.rs
Normal file
616
crates/editor/src/jsx_tag_auto_close.rs
Normal file
@@ -0,0 +1,616 @@
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use gpui::{Context, Entity, Window};
|
||||
use multi_buffer::{MultiBuffer, ToOffset};
|
||||
use std::ops::Range;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use language::{BufferSnapshot, JsxTagAutoCloseConfig, Node};
|
||||
use text::{Anchor, OffsetRangeExt as _};
|
||||
|
||||
use crate::Editor;
|
||||
|
||||
pub struct JsxTagCompletionState {
|
||||
edit_index: usize,
|
||||
open_tag_range: Range<usize>,
|
||||
}
|
||||
|
||||
/// Index of the named child within an open or close tag
|
||||
/// that corresponds to the tag name
|
||||
/// Note that this is not configurable, i.e. we assume the first
|
||||
/// named child of a tag node is the tag name
|
||||
const TS_NODE_TAG_NAME_CHILD_INDEX: usize = 0;
|
||||
|
||||
/// Maximum number of parent elements to walk back when checking if an open tag
|
||||
/// is already closed.
|
||||
///
|
||||
/// See the comment in `generate_auto_close_edits` for more details
|
||||
const ALREADY_CLOSED_PARENT_ELEMENT_WALK_BACK_LIMIT: usize = 2;
|
||||
|
||||
pub(crate) fn should_auto_close(
|
||||
buffer: &BufferSnapshot,
|
||||
edited_ranges: &[Range<usize>],
|
||||
config: &JsxTagAutoCloseConfig,
|
||||
) -> Option<Vec<JsxTagCompletionState>> {
|
||||
let mut to_auto_edit = vec![];
|
||||
for (index, edited_range) in edited_ranges.iter().enumerate() {
|
||||
let text = buffer
|
||||
.text_for_range(edited_range.clone())
|
||||
.collect::<String>();
|
||||
if !text.ends_with(">") {
|
||||
continue;
|
||||
}
|
||||
let Some(layer) = buffer.smallest_syntax_layer_containing(edited_range.clone()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(node) = layer
|
||||
.node()
|
||||
.named_descendant_for_byte_range(edited_range.start, edited_range.end)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let mut jsx_open_tag_node = node;
|
||||
if node.grammar_name() != config.open_tag_node_name {
|
||||
if let Some(parent) = node.parent() {
|
||||
if parent.grammar_name() == config.open_tag_node_name {
|
||||
jsx_open_tag_node = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
if jsx_open_tag_node.grammar_name() != config.open_tag_node_name {
|
||||
continue;
|
||||
}
|
||||
|
||||
let first_two_chars: Option<[char; 2]> = {
|
||||
let mut chars = buffer
|
||||
.text_for_range(jsx_open_tag_node.byte_range())
|
||||
.flat_map(|chunk| chunk.chars());
|
||||
if let (Some(c1), Some(c2)) = (chars.next(), chars.next()) {
|
||||
Some([c1, c2])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(chars) = first_two_chars {
|
||||
if chars[0] != '<' {
|
||||
continue;
|
||||
}
|
||||
if chars[1] == '!' || chars[1] == '/' {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
to_auto_edit.push(JsxTagCompletionState {
|
||||
edit_index: index,
|
||||
open_tag_range: jsx_open_tag_node.byte_range(),
|
||||
});
|
||||
}
|
||||
if to_auto_edit.is_empty() {
|
||||
return None;
|
||||
} else {
|
||||
return Some(to_auto_edit);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn generate_auto_close_edits(
|
||||
buffer: &BufferSnapshot,
|
||||
ranges: &[Range<usize>],
|
||||
config: &JsxTagAutoCloseConfig,
|
||||
state: Vec<JsxTagCompletionState>,
|
||||
) -> Result<Vec<(Range<Anchor>, String)>> {
|
||||
let mut edits = Vec::with_capacity(state.len());
|
||||
for auto_edit in state {
|
||||
let edited_range = ranges[auto_edit.edit_index].clone();
|
||||
let Some(layer) = buffer.smallest_syntax_layer_containing(edited_range.clone()) else {
|
||||
continue;
|
||||
};
|
||||
let layer_root_node = layer.node();
|
||||
let Some(open_tag) = layer_root_node.descendant_for_byte_range(
|
||||
auto_edit.open_tag_range.start,
|
||||
auto_edit.open_tag_range.end,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
assert!(open_tag.kind() == config.open_tag_node_name);
|
||||
let tag_name = open_tag
|
||||
.named_child(TS_NODE_TAG_NAME_CHILD_INDEX)
|
||||
.filter(|node| node.kind() == config.tag_name_node_name)
|
||||
.map_or("".to_string(), |node| {
|
||||
buffer.text_for_range(node.byte_range()).collect::<String>()
|
||||
});
|
||||
|
||||
/*
|
||||
* Naive check to see if the tag is already closed
|
||||
* Essentially all we do is count the number of open and close tags
|
||||
* with the same tag name as the open tag just entered by the user
|
||||
* The search is limited to some scope determined by
|
||||
* `ALREADY_CLOSED_PARENT_ELEMENT_WALK_BACK_LIMIT`
|
||||
*
|
||||
* The limit is preferable to walking up the tree until we find a non-tag node,
|
||||
* and then checking the entire tree, as this is unnecessarily expensive, and
|
||||
* risks false positives
|
||||
* eg. a `</div>` tag without a corresponding opening tag exists 25 lines away
|
||||
* and the user typed in `<div>`, intuitively we still want to auto-close it because
|
||||
* the other `</div>` tag is almost certainly not supposed to be the closing tag for the
|
||||
* current element
|
||||
*
|
||||
* We have to walk up the tree some amount because tree-sitters error correction is not
|
||||
* designed to handle this case, and usually does not represent the tree structure
|
||||
* in the way we might expect,
|
||||
*
|
||||
* We half to walk up the tree until we hit an element with a different open tag name (`doing_deep_search == true`)
|
||||
* because tree-sitter may pair the new open tag with the root of the tree's closing tag leaving the
|
||||
* root's opening tag unclosed.
|
||||
* e.g
|
||||
* ```
|
||||
* <div>
|
||||
* <div>|cursor here|
|
||||
* </div>
|
||||
* ```
|
||||
* in Astro/vue/svelte tree-sitter represented the tree as
|
||||
* (
|
||||
* (jsx_element
|
||||
* (jsx_opening_element
|
||||
* "<div>")
|
||||
* )
|
||||
* (jsx_element
|
||||
* (jsx_opening_element
|
||||
* "<div>") // <- cursor is here
|
||||
* (jsx_closing_element
|
||||
* "</div>")
|
||||
* )
|
||||
* )
|
||||
* so if we only walked to the first `jsx_element` node,
|
||||
* we would mistakenly identify the div entered by the
|
||||
* user as already being closed, despite this clearly
|
||||
* being false
|
||||
*
|
||||
* The errors with the tree-sitter tree caused by error correction,
|
||||
* are also why the naive algorithm was chosen, as the alternative
|
||||
* approach would be to maintain or construct a full parse tree (like tree-sitter)
|
||||
* that better represents errors in a way that we can simply check
|
||||
* the enclosing scope of the entered tag for a closing tag
|
||||
* This is far more complex and expensive, and was deemed impractical
|
||||
* given that the naive algorithm is sufficient in the majority of cases.
|
||||
*/
|
||||
{
|
||||
let tag_node_name_equals = |node: &Node, tag_name_node_name: &str, name: &str| {
|
||||
let is_empty = name.len() == 0;
|
||||
if let Some(node_name) = node.named_child(TS_NODE_TAG_NAME_CHILD_INDEX) {
|
||||
if node_name.kind() != tag_name_node_name {
|
||||
return is_empty;
|
||||
}
|
||||
let range = node_name.byte_range();
|
||||
return buffer.text_for_range(range).equals_str(name);
|
||||
}
|
||||
return is_empty;
|
||||
};
|
||||
|
||||
let tree_root_node = {
|
||||
let mut ancestors = Vec::with_capacity(
|
||||
// estimate of max, not based on any data,
|
||||
// but trying to avoid excessive reallocation
|
||||
16,
|
||||
);
|
||||
ancestors.push(layer_root_node);
|
||||
let mut cur = layer_root_node;
|
||||
// walk down the tree until we hit the open tag
|
||||
// note: this is what node.parent() does internally
|
||||
while let Some(descendant) = cur.child_with_descendant(open_tag) {
|
||||
if descendant == open_tag {
|
||||
break;
|
||||
}
|
||||
ancestors.push(descendant);
|
||||
cur = descendant;
|
||||
}
|
||||
|
||||
assert!(ancestors.len() > 0);
|
||||
|
||||
let mut tree_root_node = open_tag;
|
||||
|
||||
let mut parent_element_node_count = 0;
|
||||
let mut doing_deep_search = false;
|
||||
|
||||
for &ancestor in ancestors.iter().rev() {
|
||||
tree_root_node = ancestor;
|
||||
let is_element = ancestor.kind() == config.jsx_element_node_name;
|
||||
let is_error = ancestor.is_error();
|
||||
if is_error || !is_element {
|
||||
break;
|
||||
}
|
||||
if is_element {
|
||||
let is_first = parent_element_node_count == 0;
|
||||
if !is_first {
|
||||
let has_open_tag_with_same_tag_name = ancestor
|
||||
.named_child(0)
|
||||
.filter(|n| n.kind() == config.open_tag_node_name)
|
||||
.map_or(false, |element_open_tag_node| {
|
||||
tag_node_name_equals(
|
||||
&element_open_tag_node,
|
||||
&config.tag_name_node_name,
|
||||
&tag_name,
|
||||
)
|
||||
});
|
||||
if has_open_tag_with_same_tag_name {
|
||||
doing_deep_search = true;
|
||||
} else if doing_deep_search {
|
||||
break;
|
||||
}
|
||||
}
|
||||
parent_element_node_count += 1;
|
||||
if !doing_deep_search
|
||||
&& parent_element_node_count
|
||||
>= ALREADY_CLOSED_PARENT_ELEMENT_WALK_BACK_LIMIT
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
tree_root_node
|
||||
};
|
||||
|
||||
let mut unclosed_open_tag_count: i32 = 0;
|
||||
|
||||
let mut cursor = layer_root_node.walk();
|
||||
|
||||
let mut stack = Vec::with_capacity(tree_root_node.descendant_count());
|
||||
stack.extend(tree_root_node.children(&mut cursor));
|
||||
|
||||
let mut has_erroneous_close_tag = false;
|
||||
let mut erroneous_close_tag_node_name = "";
|
||||
let mut erroneous_close_tag_name_node_name = "";
|
||||
if let Some(name) = config.erroneous_close_tag_node_name.as_deref() {
|
||||
has_erroneous_close_tag = true;
|
||||
erroneous_close_tag_node_name = name;
|
||||
erroneous_close_tag_name_node_name = config
|
||||
.erroneous_close_tag_name_node_name
|
||||
.as_deref()
|
||||
.unwrap_or(&config.tag_name_node_name);
|
||||
}
|
||||
|
||||
let is_after_open_tag = |node: &Node| {
|
||||
return node.start_byte() < open_tag.start_byte()
|
||||
&& node.end_byte() < open_tag.start_byte();
|
||||
};
|
||||
|
||||
// perf: use cursor for more efficient traversal
|
||||
// if child -> go to child
|
||||
// else if next sibling -> go to next sibling
|
||||
// else -> go to parent
|
||||
// if parent == tree_root_node -> break
|
||||
while let Some(node) = stack.pop() {
|
||||
let kind = node.kind();
|
||||
if kind == config.open_tag_node_name {
|
||||
if tag_node_name_equals(&node, &config.tag_name_node_name, &tag_name) {
|
||||
unclosed_open_tag_count += 1;
|
||||
}
|
||||
} else if kind == config.close_tag_node_name {
|
||||
if tag_node_name_equals(&node, &config.tag_name_node_name, &tag_name) {
|
||||
unclosed_open_tag_count -= 1;
|
||||
}
|
||||
} else if has_erroneous_close_tag && kind == erroneous_close_tag_node_name {
|
||||
if tag_node_name_equals(&node, erroneous_close_tag_name_node_name, &tag_name) {
|
||||
if !is_after_open_tag(&node) {
|
||||
unclosed_open_tag_count -= 1;
|
||||
}
|
||||
}
|
||||
} else if kind == config.jsx_element_node_name {
|
||||
// perf: filter only open,close,element,erroneous nodes
|
||||
stack.extend(node.children(&mut cursor));
|
||||
}
|
||||
}
|
||||
|
||||
if unclosed_open_tag_count <= 0 {
|
||||
// skip if already closed
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let edit_anchor = buffer.anchor_after(edited_range.end);
|
||||
let edit_range = edit_anchor..edit_anchor;
|
||||
edits.push((edit_range, format!("</{}>", tag_name)));
|
||||
}
|
||||
return Ok(edits);
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_enabled_in_any_buffer(
|
||||
editor: &mut Editor,
|
||||
multi_buffer: &Entity<MultiBuffer>,
|
||||
cx: &Context<Editor>,
|
||||
) {
|
||||
editor.jsx_tag_auto_close_enabled_in_any_buffer = {
|
||||
let multi_buffer = multi_buffer.read(cx);
|
||||
let mut found_enabled = false;
|
||||
multi_buffer.for_each_buffer(|buffer| {
|
||||
let buffer = buffer.read(cx);
|
||||
let snapshot = buffer.snapshot();
|
||||
for syntax_layer in snapshot.syntax_layers() {
|
||||
let language = syntax_layer.language;
|
||||
if language.config().jsx_tag_auto_close.is_none() {
|
||||
continue;
|
||||
}
|
||||
let language_settings = language::language_settings::language_settings(
|
||||
Some(language.name()),
|
||||
snapshot.file(),
|
||||
cx,
|
||||
);
|
||||
if language_settings.jsx_tag_auto_close.enabled {
|
||||
found_enabled = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
found_enabled
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) type InitialBufferVersionsMap = HashMap<language::BufferId, clock::Global>;
|
||||
|
||||
pub(crate) fn construct_initial_buffer_versions_map<
|
||||
D: ToOffset + Copy,
|
||||
_S: Into<std::sync::Arc<str>>,
|
||||
>(
|
||||
editor: &Editor,
|
||||
edits: &[(Range<D>, _S)],
|
||||
cx: &Context<Editor>,
|
||||
) -> InitialBufferVersionsMap {
|
||||
let mut initial_buffer_versions = InitialBufferVersionsMap::default();
|
||||
|
||||
if !editor.jsx_tag_auto_close_enabled_in_any_buffer {
|
||||
return initial_buffer_versions;
|
||||
}
|
||||
|
||||
for (edit_range, _) in edits {
|
||||
let edit_range_buffer = editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.excerpt_containing(edit_range.end, cx)
|
||||
.map(|e| e.1);
|
||||
if let Some(buffer) = edit_range_buffer {
|
||||
let (buffer_id, buffer_version) =
|
||||
buffer.read_with(cx, |buffer, _| (buffer.remote_id(), buffer.version.clone()));
|
||||
initial_buffer_versions.insert(buffer_id, buffer_version);
|
||||
}
|
||||
}
|
||||
return initial_buffer_versions;
|
||||
}
|
||||
|
||||
pub(crate) fn handle_from(
|
||||
editor: &Editor,
|
||||
initial_buffer_versions: InitialBufferVersionsMap,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
if !editor.jsx_tag_auto_close_enabled_in_any_buffer {
|
||||
return;
|
||||
}
|
||||
|
||||
struct JsxAutoCloseEditContext {
|
||||
buffer: Entity<language::Buffer>,
|
||||
config: language::JsxTagAutoCloseConfig,
|
||||
edits: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
let mut edit_contexts =
|
||||
HashMap::<(language::BufferId, language::LanguageId), JsxAutoCloseEditContext>::default();
|
||||
|
||||
for (buffer_id, buffer_version_initial) in initial_buffer_versions {
|
||||
let Some(buffer) = editor.buffer.read(cx).buffer(buffer_id) else {
|
||||
continue;
|
||||
};
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
for edit in buffer.read(cx).edits_since(&buffer_version_initial) {
|
||||
let Some(language) = snapshot.language_at(edit.new.end) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(config) = language.config().jsx_tag_auto_close.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let language_settings = snapshot.settings_at(edit.new.end, cx);
|
||||
if !language_settings.jsx_tag_auto_close.enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
edit_contexts
|
||||
.entry((snapshot.remote_id(), language.id()))
|
||||
.or_insert_with(|| JsxAutoCloseEditContext {
|
||||
buffer: buffer.clone(),
|
||||
config: config.clone(),
|
||||
edits: vec![],
|
||||
})
|
||||
.edits
|
||||
.push(edit.new);
|
||||
}
|
||||
}
|
||||
|
||||
for ((buffer_id, _), auto_close_context) in edit_contexts {
|
||||
let JsxAutoCloseEditContext {
|
||||
buffer,
|
||||
config: jsx_tag_auto_close_config,
|
||||
edits: edited_ranges,
|
||||
} = auto_close_context;
|
||||
|
||||
let (buffer_version_initial, mut buffer_parse_status_rx) =
|
||||
buffer.read_with(cx, |buffer, _| (buffer.version(), buffer.parse_status()));
|
||||
|
||||
cx.spawn_in(window, |this, mut cx| async move {
|
||||
let Some(buffer_parse_status) = buffer_parse_status_rx.recv().await.ok() else {
|
||||
return Some(());
|
||||
};
|
||||
if buffer_parse_status == language::ParseStatus::Parsing {
|
||||
let Some(language::ParseStatus::Idle) = buffer_parse_status_rx.recv().await.ok()
|
||||
else {
|
||||
return Some(());
|
||||
};
|
||||
}
|
||||
|
||||
let buffer_snapshot = buffer.read_with(&cx, |buf, _| buf.snapshot()).ok()?;
|
||||
|
||||
let Some(edit_behavior_state) =
|
||||
should_auto_close(&buffer_snapshot, &edited_ranges, &jsx_tag_auto_close_config)
|
||||
else {
|
||||
return Some(());
|
||||
};
|
||||
|
||||
let ensure_no_edits_since_start = || -> Option<()> {
|
||||
// <div>wef,wefwef
|
||||
let has_edits_since_start = this
|
||||
.read_with(&cx, |this, cx| {
|
||||
this.buffer.read_with(cx, |buffer, cx| {
|
||||
buffer.buffer(buffer_id).map_or(true, |buffer| {
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
buffer.has_edits_since(&buffer_version_initial)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
if has_edits_since_start {
|
||||
Err(anyhow!(
|
||||
"Auto-close Operation Failed - Buffer has edits since start"
|
||||
))
|
||||
.log_err()?;
|
||||
}
|
||||
|
||||
Some(())
|
||||
};
|
||||
|
||||
ensure_no_edits_since_start()?;
|
||||
|
||||
let edits = cx
|
||||
.background_executor()
|
||||
.spawn({
|
||||
let buffer_snapshot = buffer_snapshot.clone();
|
||||
async move {
|
||||
generate_auto_close_edits(
|
||||
&buffer_snapshot,
|
||||
&edited_ranges,
|
||||
&jsx_tag_auto_close_config,
|
||||
edit_behavior_state,
|
||||
)
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let edits = edits
|
||||
.context("Auto-close Operation Failed - Failed to compute edits")
|
||||
.log_err()?;
|
||||
|
||||
if edits.is_empty() {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
// check again after awaiting background task before applying edits
|
||||
ensure_no_edits_since_start()?;
|
||||
|
||||
let multi_buffer_snapshot = this
|
||||
.read_with(&cx, |this, cx| {
|
||||
this.buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx))
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
let mut base_selections = Vec::new();
|
||||
let mut buffer_selection_map = HashMap::default();
|
||||
|
||||
{
|
||||
let selections = this
|
||||
.read_with(&cx, |this, _| this.selections.disjoint_anchors().clone())
|
||||
.ok()?;
|
||||
for selection in selections.iter() {
|
||||
let Some(selection_buffer_offset_head) =
|
||||
multi_buffer_snapshot.point_to_buffer_offset(selection.head())
|
||||
else {
|
||||
base_selections.push(selection.clone());
|
||||
continue;
|
||||
};
|
||||
let Some(selection_buffer_offset_tail) =
|
||||
multi_buffer_snapshot.point_to_buffer_offset(selection.tail())
|
||||
else {
|
||||
base_selections.push(selection.clone());
|
||||
continue;
|
||||
};
|
||||
|
||||
let is_entirely_in_buffer = selection_buffer_offset_head.0.remote_id()
|
||||
== buffer_id
|
||||
&& selection_buffer_offset_tail.0.remote_id() == buffer_id;
|
||||
if !is_entirely_in_buffer {
|
||||
base_selections.push(selection.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
let selection_buffer_offset_head = selection_buffer_offset_head.1;
|
||||
let selection_buffer_offset_tail = selection_buffer_offset_tail.1;
|
||||
buffer_selection_map.insert(
|
||||
(selection_buffer_offset_head, selection_buffer_offset_tail),
|
||||
(selection.clone(), None),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut any_selections_need_update = false;
|
||||
for edit in &edits {
|
||||
let edit_range_offset = edit.0.to_offset(&buffer_snapshot);
|
||||
if edit_range_offset.start != edit_range_offset.end {
|
||||
continue;
|
||||
}
|
||||
if let Some(selection) =
|
||||
buffer_selection_map.get_mut(&(edit_range_offset.start, edit_range_offset.end))
|
||||
{
|
||||
if selection.0.head().bias() != text::Bias::Right
|
||||
|| selection.0.tail().bias() != text::Bias::Right
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if selection.1.is_none() {
|
||||
any_selections_need_update = true;
|
||||
selection.1 = Some(
|
||||
selection
|
||||
.0
|
||||
.clone()
|
||||
.map(|anchor| multi_buffer_snapshot.anchor_before(anchor)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buffer
|
||||
.update(&mut cx, |buffer, cx| {
|
||||
buffer.edit(edits, None, cx);
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
if any_selections_need_update {
|
||||
let multi_buffer_snapshot = this
|
||||
.read_with(&cx, |this, cx| {
|
||||
this.buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx))
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
base_selections.extend(buffer_selection_map.values().map(|selection| {
|
||||
match &selection.1 {
|
||||
Some(left_biased_selection) => left_biased_selection.clone(),
|
||||
None => selection.0.clone(),
|
||||
}
|
||||
}));
|
||||
|
||||
let base_selections = base_selections
|
||||
.into_iter()
|
||||
.map(|selection| {
|
||||
selection.map(|anchor| anchor.to_offset(&multi_buffer_snapshot))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
this.update_in(&mut cx, |this, window, cx| {
|
||||
this.change_selections_inner(None, false, window, cx, |s| {
|
||||
s.select(base_selections);
|
||||
});
|
||||
})
|
||||
.ok()?;
|
||||
}
|
||||
|
||||
Some(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
@@ -448,7 +448,9 @@ pub fn end_of_excerpt(
|
||||
if start.row() > DisplayRow(0) {
|
||||
*start.row_mut() -= 1;
|
||||
}
|
||||
map.clip_point(start, Bias::Left)
|
||||
start = map.clip_point(start, Bias::Left);
|
||||
*start.column_mut() = 0;
|
||||
start
|
||||
}
|
||||
Direction::Next => {
|
||||
let mut end = excerpt.end_anchor().to_display_point(&map);
|
||||
|
||||
@@ -4,7 +4,7 @@ use anyhow::Context as _;
|
||||
use gpui::{App, AppContext as _, Context, Entity, Window};
|
||||
use language::{Capability, Language};
|
||||
use multi_buffer::MultiBuffer;
|
||||
use project::lsp_ext_command::ExpandMacro;
|
||||
use project::lsp_store::{lsp_ext_command::ExpandMacro, rust_analyzer_ext::RUST_ANALYZER_NAME};
|
||||
use text::ToPointUtf16;
|
||||
|
||||
use crate::{
|
||||
@@ -12,8 +12,6 @@ use crate::{
|
||||
ExpandMacroRecursively, OpenDocs,
|
||||
};
|
||||
|
||||
const RUST_ANALYZER_NAME: &str = "rust-analyzer";
|
||||
|
||||
fn is_rust_language(language: &Language) -> bool {
|
||||
language.name() == "Rust".into()
|
||||
}
|
||||
@@ -131,7 +129,7 @@ pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mu
|
||||
project.request_lsp(
|
||||
buffer,
|
||||
project::LanguageServerToQuery::Other(server_to_query),
|
||||
project::lsp_ext_command::OpenDocs { position },
|
||||
project::lsp_store::lsp_ext_command::OpenDocs { position },
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -223,7 +223,6 @@ impl ScrollManager {
|
||||
self.anchor.scroll_position(snapshot)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn set_scroll_position(
|
||||
&mut self,
|
||||
scroll_position: gpui::Point<f32>,
|
||||
@@ -298,7 +297,6 @@ impl ScrollManager {
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn set_anchor(
|
||||
&mut self,
|
||||
anchor: ScrollAnchor,
|
||||
|
||||
@@ -429,12 +429,14 @@ impl EditorTestContext {
|
||||
if expected_selections.len() > 0 {
|
||||
assert!(
|
||||
is_selected,
|
||||
"excerpt {} should be selected. Got {:?}",
|
||||
ix,
|
||||
self.editor_state()
|
||||
"excerpt {ix} should be selected. got {:?}",
|
||||
self.editor_state(),
|
||||
);
|
||||
} else {
|
||||
assert!(!is_selected, "excerpt {} should not be selected", ix);
|
||||
assert!(
|
||||
!is_selected,
|
||||
"excerpt {ix} should not be selected, got: {selections:?}",
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ collections.workspace = true
|
||||
env_logger.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
language.workspace = true
|
||||
|
||||
@@ -5,7 +5,6 @@ use client::{Client, UserStore};
|
||||
use clock::RealSystemClock;
|
||||
use collections::BTreeMap;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use git::GitHostingProviderRegistry;
|
||||
use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, Entity};
|
||||
use http_client::{HttpClient, Method};
|
||||
use language::LanguageRegistry;
|
||||
@@ -274,8 +273,7 @@ async fn run_evaluation(
|
||||
let repos_dir = Path::new(EVAL_REPOS_DIR);
|
||||
let db_path = Path::new(EVAL_DB_PATH);
|
||||
let api_key = std::env::var("OPENAI_API_KEY").unwrap();
|
||||
let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new());
|
||||
let fs = Arc::new(RealFs::new(git_hosting_provider_registry, None)) as Arc<dyn Fs>;
|
||||
let fs = Arc::new(RealFs::new(None)) as Arc<dyn Fs>;
|
||||
let clock = Arc::new(RealSystemClock);
|
||||
let client = cx
|
||||
.update(|cx| {
|
||||
@@ -399,7 +397,6 @@ async fn run_evaluation(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn run_eval_project(
|
||||
evaluation_project: EvaluationProject,
|
||||
user_store: &Entity<UserStore>,
|
||||
|
||||
@@ -17,6 +17,7 @@ async-compression.workspace = true
|
||||
async-tar.workspace = true
|
||||
async-trait.workspace = true
|
||||
collections.workspace = true
|
||||
convert_case.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::{
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use convert_case::{Case, Casing as _};
|
||||
use futures::io::BufReader;
|
||||
use futures::AsyncReadExt;
|
||||
use http_client::{self, AsyncBody, HttpClient};
|
||||
@@ -97,6 +98,11 @@ impl ExtensionBuilder {
|
||||
}
|
||||
|
||||
for (grammar_name, grammar_metadata) in &extension_manifest.grammars {
|
||||
let snake_cased_grammar_name = grammar_name.to_case(Case::Snake);
|
||||
if grammar_name.as_ref() != snake_cased_grammar_name.as_str() {
|
||||
bail!("grammar name '{grammar_name}' must be written in snake_case: {snake_cased_grammar_name}");
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"compiling grammar {grammar_name} for extension {}",
|
||||
extension_dir.display()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use fs::Fs;
|
||||
use language::LanguageName;
|
||||
@@ -85,6 +85,61 @@ pub struct ExtensionManifest {
|
||||
pub indexed_docs_providers: BTreeMap<Arc<str>, IndexedDocsProviderEntry>,
|
||||
#[serde(default)]
|
||||
pub snippets: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub capabilities: Vec<ExtensionCapability>,
|
||||
}
|
||||
|
||||
impl ExtensionManifest {
|
||||
pub fn allow_exec(
|
||||
&self,
|
||||
desired_command: &str,
|
||||
desired_args: &[impl AsRef<str> + std::fmt::Debug],
|
||||
) -> Result<()> {
|
||||
let is_allowed = self.capabilities.iter().any(|capability| match capability {
|
||||
ExtensionCapability::ProcessExec { command, args } if command == desired_command => {
|
||||
for (ix, arg) in args.iter().enumerate() {
|
||||
if arg == "**" {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ix >= desired_args.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if arg != "*" && arg != desired_args[ix].as_ref() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if args.len() < desired_args.len() {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
|
||||
if !is_allowed {
|
||||
bail!(
|
||||
"capability for process:exec {desired_command} {desired_args:?} was not listed in the extension manifest",
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A capability for an extension.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum ExtensionCapability {
|
||||
#[serde(rename = "process:exec")]
|
||||
ProcessExec {
|
||||
/// The command to execute.
|
||||
command: String,
|
||||
/// The arguments to pass to the command. Use `*` for a single wildcard argument.
|
||||
/// If the last element is `**`, then any trailing arguments are allowed.
|
||||
args: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
@@ -218,5 +273,104 @@ fn manifest_from_old_manifest(
|
||||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
capabilities: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn extension_manifest() -> ExtensionManifest {
|
||||
ExtensionManifest {
|
||||
id: "test".into(),
|
||||
name: "Test".to_string(),
|
||||
version: "1.0.0".into(),
|
||||
schema_version: SchemaVersion::ZERO,
|
||||
description: None,
|
||||
repository: None,
|
||||
authors: vec![],
|
||||
lib: Default::default(),
|
||||
themes: vec![],
|
||||
icon_themes: vec![],
|
||||
languages: vec![],
|
||||
grammars: BTreeMap::default(),
|
||||
language_servers: BTreeMap::default(),
|
||||
context_servers: BTreeMap::default(),
|
||||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
capabilities: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allow_exact_match() {
|
||||
let manifest = ExtensionManifest {
|
||||
capabilities: vec![ExtensionCapability::ProcessExec {
|
||||
command: "ls".to_string(),
|
||||
args: vec!["-la".to_string()],
|
||||
}],
|
||||
..extension_manifest()
|
||||
};
|
||||
|
||||
assert!(manifest.allow_exec("ls", &["-la"]).is_ok());
|
||||
assert!(manifest.allow_exec("ls", &["-l"]).is_err());
|
||||
assert!(manifest.allow_exec("pwd", &[] as &[&str]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allow_wildcard_arg() {
|
||||
let manifest = ExtensionManifest {
|
||||
capabilities: vec![ExtensionCapability::ProcessExec {
|
||||
command: "git".to_string(),
|
||||
args: vec!["*".to_string()],
|
||||
}],
|
||||
..extension_manifest()
|
||||
};
|
||||
|
||||
assert!(manifest.allow_exec("git", &["status"]).is_ok());
|
||||
assert!(manifest.allow_exec("git", &["commit"]).is_ok());
|
||||
assert!(manifest.allow_exec("git", &["status", "-s"]).is_err()); // too many args
|
||||
assert!(manifest.allow_exec("npm", &["install"]).is_err()); // wrong command
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allow_double_wildcard() {
|
||||
let manifest = ExtensionManifest {
|
||||
capabilities: vec![ExtensionCapability::ProcessExec {
|
||||
command: "cargo".to_string(),
|
||||
args: vec!["test".to_string(), "**".to_string()],
|
||||
}],
|
||||
..extension_manifest()
|
||||
};
|
||||
|
||||
assert!(manifest.allow_exec("cargo", &["test"]).is_ok());
|
||||
assert!(manifest.allow_exec("cargo", &["test", "--all"]).is_ok());
|
||||
assert!(manifest
|
||||
.allow_exec("cargo", &["test", "--all", "--no-fail-fast"])
|
||||
.is_ok());
|
||||
assert!(manifest.allow_exec("cargo", &["build"]).is_err()); // wrong first arg
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allow_mixed_wildcards() {
|
||||
let manifest = ExtensionManifest {
|
||||
capabilities: vec![ExtensionCapability::ProcessExec {
|
||||
command: "docker".to_string(),
|
||||
args: vec!["run".to_string(), "*".to_string(), "**".to_string()],
|
||||
}],
|
||||
..extension_manifest()
|
||||
};
|
||||
|
||||
assert!(manifest.allow_exec("docker", &["run", "nginx"]).is_ok());
|
||||
assert!(manifest.allow_exec("docker", &["run"]).is_err());
|
||||
assert!(manifest
|
||||
.allow_exec("docker", &["run", "ubuntu", "bash"])
|
||||
.is_ok());
|
||||
assert!(manifest
|
||||
.allow_exec("docker", &["run", "alpine", "sh", "-c", "echo hello"])
|
||||
.is_ok());
|
||||
assert!(manifest.allow_exec("docker", &["ps"]).is_err()); // wrong first arg
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +195,6 @@ static mut EXTENSION: Option<Box<dyn Extension>> = None;
|
||||
pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "/version_bytes"));
|
||||
|
||||
mod wit {
|
||||
#![allow(clippy::too_many_arguments, clippy::missing_safety_doc)]
|
||||
|
||||
wit_bindgen::generate!({
|
||||
skip: ["init-extension"],
|
||||
|
||||
@@ -218,7 +218,6 @@ impl ExtensionStore {
|
||||
cx.global::<GlobalExtensionStore>().0.clone()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
extensions_dir: PathBuf,
|
||||
build_dir: Option<PathBuf>,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user